feat: add built-in ricardo display mode
Build And Push Container / build-and-push (push) Successful in 58s
Build And Push Container / build-and-push (push) Successful in 58s
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 4.5 MiB |
@@ -10,7 +10,10 @@ use chrono::Utc;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::{
|
||||
images::{is_gif_manifest_name, load_gif_manifest, load_panel_rgb, MIN_GIF_FRAME_DELAY_MS},
|
||||
images::{
|
||||
is_gif_manifest_name, load_gif_manifest, load_panel_rgb, MIN_GIF_FRAME_DELAY_MS,
|
||||
RICARDO_MANIFEST_NAME,
|
||||
},
|
||||
monitor::{load_monitor_json_sync, rotation_snapshot},
|
||||
system::{overlay_system_specs, render_system_panel},
|
||||
types::{ActiveFrame, AppState, DisplayConfig, DisplayStatus, RotationSlot, RotationSnapshot},
|
||||
@@ -36,7 +39,7 @@ pub(crate) fn initial_display_status(config: &DisplayConfig) -> DisplayStatus {
|
||||
custom_panel: false,
|
||||
specs_enabled: false,
|
||||
memes_enabled: false,
|
||||
gifs_enabled: false,
|
||||
ricardo_enabled: false,
|
||||
rotation_active: false,
|
||||
switch_time: "10".into(),
|
||||
active_images: Vec::new(),
|
||||
@@ -153,7 +156,7 @@ fn run_display_session(state: &AppState, screen: &mut AooScreen) -> Result<()> {
|
||||
status.custom_panel = snapshot.custom_panel;
|
||||
status.specs_enabled = snapshot.specs_enabled;
|
||||
status.memes_enabled = snapshot.memes_enabled;
|
||||
status.gifs_enabled = snapshot.gifs_enabled;
|
||||
status.ricardo_enabled = snapshot.ricardo_enabled;
|
||||
status.rotation_active = snapshot.rotation_active();
|
||||
status.switch_time = snapshot.switch_time.to_string();
|
||||
status.active_images = snapshot.active_images.clone();
|
||||
@@ -239,37 +242,11 @@ fn build_rotation_slots(
|
||||
image_dir: &std::path::PathBuf,
|
||||
) -> Vec<RotationSlot> {
|
||||
let mut slots = Vec::new();
|
||||
if snapshot.memes_enabled || snapshot.gifs_enabled {
|
||||
if snapshot.memes_enabled {
|
||||
for name in &snapshot.active_images {
|
||||
if is_gif_manifest_name(name) {
|
||||
if !snapshot.gifs_enabled {
|
||||
continue;
|
||||
}
|
||||
match load_gif_manifest(image_dir, name) {
|
||||
Ok(manifest) if !manifest.frames.is_empty() => {
|
||||
let mut frames = Vec::new();
|
||||
for (idx, frame) in manifest.frames.iter().enumerate() {
|
||||
frames.push((
|
||||
ActiveFrame {
|
||||
key: format!("{name}#{idx}"),
|
||||
image_name: frame.name.clone(),
|
||||
display_name: format!("{} [{}]", manifest.label, idx + 1),
|
||||
animated: true,
|
||||
},
|
||||
Duration::from_millis(
|
||||
frame.delay_ms.max(MIN_GIF_FRAME_DELAY_MS) as u64
|
||||
),
|
||||
));
|
||||
}
|
||||
slots.push(RotationSlot {
|
||||
total_duration: Duration::from_secs(snapshot.switch_time as u64),
|
||||
frames,
|
||||
});
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(err) => warn!("Skipping GIF set {name}: {err}"),
|
||||
}
|
||||
} else if snapshot.memes_enabled {
|
||||
continue;
|
||||
} else {
|
||||
slots.push(RotationSlot {
|
||||
total_duration: Duration::from_secs(snapshot.switch_time as u64),
|
||||
frames: vec![(
|
||||
@@ -281,8 +258,32 @@ fn build_rotation_slots(
|
||||
},
|
||||
Duration::from_secs(snapshot.switch_time as u64),
|
||||
)],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if snapshot.ricardo_enabled {
|
||||
match load_gif_manifest(image_dir, RICARDO_MANIFEST_NAME) {
|
||||
Ok(manifest) if !manifest.frames.is_empty() => {
|
||||
let mut frames = Vec::new();
|
||||
for (idx, frame) in manifest.frames.iter().enumerate() {
|
||||
frames.push((
|
||||
ActiveFrame {
|
||||
key: format!("{RICARDO_MANIFEST_NAME}#{idx}"),
|
||||
image_name: frame.name.clone(),
|
||||
display_name: format!("{} [{}]", manifest.label, idx + 1),
|
||||
animated: true,
|
||||
},
|
||||
Duration::from_millis(frame.delay_ms.max(MIN_GIF_FRAME_DELAY_MS) as u64),
|
||||
));
|
||||
}
|
||||
slots.push(RotationSlot {
|
||||
total_duration: Duration::from_secs(snapshot.switch_time as u64),
|
||||
frames,
|
||||
});
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(err) => warn!("Skipping Ricardo GIF: {err}"),
|
||||
}
|
||||
}
|
||||
if slots.is_empty() && snapshot.specs_enabled {
|
||||
@@ -633,14 +634,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_rotation_slots_uses_switch_time_for_gif_slot_duration() {
|
||||
fn build_rotation_slots_uses_switch_time_for_ricardo_slot_duration() {
|
||||
let unique = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let image_dir = std::env::temp_dir().join(format!("aster-webui-gif-test-{unique}"));
|
||||
fs::create_dir_all(&image_dir).unwrap();
|
||||
let manifest_name = "panel-test.gifset.json";
|
||||
let manifest_name = RICARDO_MANIFEST_NAME;
|
||||
fs::write(
|
||||
image_dir.join(manifest_name),
|
||||
r#"{
|
||||
@@ -662,8 +663,8 @@ mod tests {
|
||||
specs_enabled: false,
|
||||
specs_mode: "cards".into(),
|
||||
memes_enabled: false,
|
||||
gifs_enabled: true,
|
||||
active_images: vec![manifest_name.into()],
|
||||
ricardo_enabled: true,
|
||||
active_images: Vec::new(),
|
||||
};
|
||||
|
||||
let slots = build_rotation_slots(&snapshot, &image_dir);
|
||||
@@ -675,7 +676,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_rotation_slots_skips_gif_when_gifs_are_disabled() {
|
||||
fn build_rotation_slots_skips_uploaded_gif_when_only_memes_are_enabled() {
|
||||
let unique = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
@@ -704,8 +705,8 @@ mod tests {
|
||||
switch_time: 7,
|
||||
specs_enabled: false,
|
||||
specs_mode: "cards".into(),
|
||||
memes_enabled: false,
|
||||
gifs_enabled: false,
|
||||
memes_enabled: true,
|
||||
ricardo_enabled: false,
|
||||
active_images: vec![manifest_name.into()],
|
||||
};
|
||||
|
||||
|
||||
@@ -15,9 +15,12 @@ use crate::{
|
||||
};
|
||||
|
||||
pub(crate) const NYAN_CAT_GIF: &[u8] = include_bytes!("../assets/nyan-cat.gif");
|
||||
pub(crate) const RICARDO_GIF: &[u8] = include_bytes!("../assets/ricardo.gif");
|
||||
pub(crate) const MIN_GIF_FRAME_DELAY_MS: u32 = 33;
|
||||
const GIF_FRAME_PREFIX: &str = ".gifframe-";
|
||||
const GIF_MANIFEST_SUFFIX: &str = ".gifset.json";
|
||||
pub(crate) const RICARDO_MANIFEST_NAME: &str = "builtin-ricardo.gifset.json";
|
||||
const RICARDO_FRAME_STEM: &str = "builtin-ricardo";
|
||||
|
||||
pub(crate) async fn list_images(state: &AppState) -> Result<Vec<ImageView>> {
|
||||
let mut out = Vec::new();
|
||||
@@ -36,6 +39,9 @@ pub(crate) async fn list_images(state: &AppState) -> Result<Vec<ImageView>> {
|
||||
}
|
||||
let meta = entry.metadata().await?;
|
||||
if is_gif_manifest_name(&name) {
|
||||
if is_builtin_ricardo_manifest_name(&name) {
|
||||
continue;
|
||||
}
|
||||
let raw = fs::read_to_string(&path).await?;
|
||||
let manifest: GifSetManifest = serde_json::from_str(&raw)
|
||||
.with_context(|| format!("Failed to parse GIF manifest {}", path.display()))?;
|
||||
@@ -85,6 +91,33 @@ pub(crate) async fn convert_and_store_image(
|
||||
}
|
||||
|
||||
async fn convert_and_store_gif(state: &AppState, file_name: &str, bytes: &[u8]) -> Result<String> {
|
||||
store_gif_frames(state, file_name, bytes, None, None).await
|
||||
}
|
||||
|
||||
pub(crate) async fn ensure_builtin_ricardo_gif(state: &AppState) -> Result<()> {
|
||||
let manifest_path = state.image_dir.join(RICARDO_MANIFEST_NAME);
|
||||
if fs::try_exists(&manifest_path).await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
store_gif_frames(
|
||||
state,
|
||||
"ricardo.gif",
|
||||
RICARDO_GIF,
|
||||
Some(RICARDO_MANIFEST_NAME),
|
||||
Some(RICARDO_FRAME_STEM),
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
async fn store_gif_frames(
|
||||
state: &AppState,
|
||||
file_name: &str,
|
||||
bytes: &[u8],
|
||||
manifest_name_override: Option<&str>,
|
||||
frame_stem_override: Option<&str>,
|
||||
) -> Result<String> {
|
||||
let decoder = GifDecoder::new(Cursor::new(bytes))
|
||||
.with_context(|| format!("Unsupported GIF format: {file_name}"))?;
|
||||
let frames = decoder
|
||||
@@ -96,9 +129,12 @@ async fn convert_and_store_gif(state: &AppState, file_name: &str, bytes: &[u8])
|
||||
}
|
||||
let source_frame_count = frames.len();
|
||||
|
||||
let base = make_image_stem(file_name);
|
||||
let base = frame_stem_override.unwrap_or(file_name);
|
||||
let clean_base = make_image_stem(base);
|
||||
let timestamp = Local::now().format("%Y%m%d-%H%M%S");
|
||||
let manifest_name = format!("panel-{timestamp}-{base}{GIF_MANIFEST_SUFFIX}");
|
||||
let manifest_name = manifest_name_override
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_else(|| format!("panel-{timestamp}-{clean_base}{GIF_MANIFEST_SUFFIX}"));
|
||||
let mut rendered_frames = Vec::new();
|
||||
|
||||
for frame in frames {
|
||||
@@ -116,7 +152,11 @@ async fn convert_and_store_gif(state: &AppState, file_name: &str, bytes: &[u8])
|
||||
|
||||
let mut stored_frames = Vec::new();
|
||||
for (idx, (rendered, delay_ms)) in collapsed_frames.into_iter().enumerate() {
|
||||
let frame_name = format!("{GIF_FRAME_PREFIX}{timestamp}-{base}-{idx:03}.jpg");
|
||||
let frame_name = if manifest_name_override.is_some() {
|
||||
format!("{GIF_FRAME_PREFIX}{clean_base}-{idx:03}.jpg")
|
||||
} else {
|
||||
format!("{GIF_FRAME_PREFIX}{timestamp}-{clean_base}-{idx:03}.jpg")
|
||||
};
|
||||
let frame_path = state.image_dir.join(&frame_name);
|
||||
rendered
|
||||
.save_with_format(&frame_path, ImageFormat::Jpeg)
|
||||
@@ -210,6 +250,10 @@ pub(crate) fn is_gif_manifest_name(name: &str) -> bool {
|
||||
name.ends_with(GIF_MANIFEST_SUFFIX)
|
||||
}
|
||||
|
||||
pub(crate) fn is_builtin_ricardo_manifest_name(name: &str) -> bool {
|
||||
name == RICARDO_MANIFEST_NAME
|
||||
}
|
||||
|
||||
fn is_gif_frame_name(name: &str) -> bool {
|
||||
name.starts_with(GIF_FRAME_PREFIX)
|
||||
}
|
||||
|
||||
@@ -6,13 +6,14 @@ use tokio::fs;
|
||||
|
||||
use crate::{
|
||||
display::read_display_status,
|
||||
images::list_images,
|
||||
images::{ensure_builtin_ricardo_gif, list_images},
|
||||
system::collect_system_view,
|
||||
types::{AppState, RotationSnapshot, SetupView, StateResponse},
|
||||
};
|
||||
|
||||
pub(crate) async fn ensure_layout(state: &AppState) -> Result<()> {
|
||||
fs::create_dir_all(&state.image_dir).await?;
|
||||
ensure_builtin_ricardo_gif(state).await?;
|
||||
|
||||
if fs::try_exists(&state.monitor_path).await? {
|
||||
return Ok(());
|
||||
@@ -34,7 +35,7 @@ pub(crate) async fn ensure_layout(state: &AppState) -> Result<()> {
|
||||
"nativeSpecs": false,
|
||||
"nativeSpecsMode": "cards",
|
||||
"nativeMemes": true,
|
||||
"nativeGifs": true,
|
||||
"nativeRicardo": false,
|
||||
"operationMode": 0,
|
||||
"theme": 1,
|
||||
"diskUpdate": 300,
|
||||
@@ -96,7 +97,7 @@ pub(crate) async fn build_state_response(state: &AppState) -> Result<StateRespon
|
||||
let specs_enabled = current_specs_enabled(&monitor);
|
||||
let specs_mode = current_specs_mode(&monitor);
|
||||
let memes_enabled = current_memes_enabled(&monitor);
|
||||
let gifs_enabled = current_gifs_enabled(&monitor);
|
||||
let ricardo_enabled = current_ricardo_enabled(&monitor);
|
||||
|
||||
Ok(StateResponse {
|
||||
monitor_path: state.monitor_path.display().to_string(),
|
||||
@@ -107,7 +108,7 @@ pub(crate) async fn build_state_response(state: &AppState) -> Result<StateRespon
|
||||
specs_enabled,
|
||||
specs_mode,
|
||||
memes_enabled,
|
||||
gifs_enabled,
|
||||
ricardo_enabled,
|
||||
},
|
||||
active_images: current_active_images(&monitor),
|
||||
images: list_images(state).await?,
|
||||
@@ -123,6 +124,7 @@ pub(crate) fn current_active_images(monitor: &Value) -> Vec<String> {
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|panel| panel.get("img").and_then(Value::as_str))
|
||||
.filter(|name| *name != crate::images::RICARDO_MANIFEST_NAME)
|
||||
.map(ToString::to_string)
|
||||
.collect()
|
||||
}
|
||||
@@ -175,13 +177,17 @@ pub(crate) fn current_specs_mode(monitor: &Value) -> String {
|
||||
.unwrap_or_else(|| "cards".into())
|
||||
}
|
||||
|
||||
pub(crate) fn current_gifs_enabled(monitor: &Value) -> bool {
|
||||
pub(crate) fn current_ricardo_enabled(monitor: &Value) -> bool {
|
||||
monitor
|
||||
.get("setup")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|setup| setup.get("nativeGifs"))
|
||||
.and_then(|setup| {
|
||||
setup
|
||||
.get("nativeRicardo")
|
||||
.or_else(|| setup.get("nativeGifs"))
|
||||
})
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(true)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub(crate) fn rotation_snapshot(monitor: &Value) -> RotationSnapshot {
|
||||
@@ -191,7 +197,7 @@ pub(crate) fn rotation_snapshot(monitor: &Value) -> RotationSnapshot {
|
||||
specs_enabled: current_specs_enabled(monitor),
|
||||
specs_mode: current_specs_mode(monitor),
|
||||
memes_enabled: current_memes_enabled(monitor),
|
||||
gifs_enabled: current_gifs_enabled(monitor),
|
||||
ricardo_enabled: current_ricardo_enabled(monitor),
|
||||
active_images: current_active_images(monitor),
|
||||
}
|
||||
}
|
||||
@@ -202,10 +208,10 @@ pub(crate) fn set_custom_panels(
|
||||
specs_enabled: bool,
|
||||
specs_mode: &str,
|
||||
memes_enabled: bool,
|
||||
gifs_enabled: bool,
|
||||
ricardo_enabled: bool,
|
||||
images: &[String],
|
||||
) {
|
||||
let enabled = specs_enabled || ((memes_enabled || gifs_enabled) && !images.is_empty());
|
||||
let enabled = specs_enabled || (memes_enabled && !images.is_empty()) || ricardo_enabled;
|
||||
let setup = monitor
|
||||
.as_object_mut()
|
||||
.expect("monitor config must be an object")
|
||||
@@ -221,16 +227,21 @@ pub(crate) fn set_custom_panels(
|
||||
Value::String(normalize_specs_mode(specs_mode)),
|
||||
);
|
||||
setup.insert("nativeMemes".into(), Value::Bool(memes_enabled));
|
||||
setup.insert("nativeGifs".into(), Value::Bool(gifs_enabled));
|
||||
setup.insert("nativeRicardo".into(), Value::Bool(ricardo_enabled));
|
||||
}
|
||||
|
||||
let mut diy_images = images.to_vec();
|
||||
if ricardo_enabled {
|
||||
diy_images.push(crate::images::RICARDO_MANIFEST_NAME.into());
|
||||
}
|
||||
|
||||
monitor["mianban"] = Value::Array(
|
||||
(1..=images.len())
|
||||
(1..=diy_images.len())
|
||||
.map(|index| Value::Number((index as u64).into()))
|
||||
.collect(),
|
||||
);
|
||||
monitor["diy"] = Value::Array(
|
||||
images
|
||||
diy_images
|
||||
.iter()
|
||||
.map(|name| json!({"type": 5, "img": name, "sensor": []}))
|
||||
.collect(),
|
||||
|
||||
@@ -13,13 +13,13 @@ use tokio::fs;
|
||||
|
||||
use crate::{
|
||||
images::{
|
||||
convert_and_store_image, delete_image_asset, is_gif_manifest_name, list_images,
|
||||
NYAN_CAT_GIF,
|
||||
convert_and_store_image, delete_image_asset, is_builtin_ricardo_manifest_name,
|
||||
is_gif_manifest_name, list_images, NYAN_CAT_GIF,
|
||||
},
|
||||
monitor::{
|
||||
build_state_response, current_active_images, current_gifs_enabled, current_memes_enabled,
|
||||
current_specs_enabled, current_specs_mode, current_switch_time, load_monitor_json,
|
||||
save_monitor_json, set_custom_panels,
|
||||
build_state_response, current_active_images, current_memes_enabled,
|
||||
current_ricardo_enabled, current_specs_enabled, current_specs_mode, current_switch_time,
|
||||
load_monitor_json, save_monitor_json, set_custom_panels,
|
||||
},
|
||||
types::{ActivateRequest, AppState, DeleteRequest, ErrorResponse},
|
||||
ui::index_response,
|
||||
@@ -103,6 +103,7 @@ async fn api_activate(
|
||||
) -> Response {
|
||||
let switch_time = payload.switch_time.unwrap_or(10).clamp(1, 600);
|
||||
let specs_enabled = payload.specs_enabled.unwrap_or(false);
|
||||
let ricardo_enabled = payload.ricardo_enabled.unwrap_or(false);
|
||||
let specs_mode = payload
|
||||
.specs_mode
|
||||
.as_deref()
|
||||
@@ -128,17 +129,12 @@ async fn api_activate(
|
||||
}
|
||||
|
||||
let has_static_selection = valid.iter().any(|name| !is_gif_manifest_name(name));
|
||||
let has_gif_selection = valid.iter().any(|name| is_gif_manifest_name(name));
|
||||
let memes_enabled = payload.memes_enabled.unwrap_or(has_static_selection);
|
||||
let gifs_enabled = payload.gifs_enabled.unwrap_or(has_gif_selection);
|
||||
|
||||
if !specs_enabled
|
||||
&& !(memes_enabled && has_static_selection)
|
||||
&& !(gifs_enabled && has_gif_selection)
|
||||
{
|
||||
if !specs_enabled && !(memes_enabled && has_static_selection) && !ricardo_enabled {
|
||||
return error_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Enable specs, memes with a still image, or GIFs with an animated image",
|
||||
"Enable specs, memes with a still image, or Ricardo",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -153,7 +149,7 @@ async fn api_activate(
|
||||
specs_enabled,
|
||||
&specs_mode,
|
||||
memes_enabled,
|
||||
gifs_enabled,
|
||||
ricardo_enabled,
|
||||
&valid,
|
||||
);
|
||||
|
||||
@@ -167,7 +163,7 @@ async fn api_activate(
|
||||
"specsEnabled": specs_enabled,
|
||||
"specsMode": specs_mode,
|
||||
"memesEnabled": memes_enabled,
|
||||
"gifsEnabled": gifs_enabled,
|
||||
"ricardoEnabled": ricardo_enabled,
|
||||
"activeImages": valid,
|
||||
}))
|
||||
.into_response()
|
||||
@@ -206,6 +202,9 @@ async fn api_delete(
|
||||
if payload.name.contains('/') || payload.name.contains('\\') {
|
||||
return error_response(StatusCode::BAD_REQUEST, "Invalid file name");
|
||||
}
|
||||
if is_builtin_ricardo_manifest_name(&payload.name) {
|
||||
return error_response(StatusCode::BAD_REQUEST, "Ricardo is a built-in asset");
|
||||
}
|
||||
|
||||
if let Err(err) = delete_image_asset(&state, &payload.name).await {
|
||||
return match err.downcast_ref::<std::io::Error>() {
|
||||
@@ -226,7 +225,7 @@ async fn api_delete(
|
||||
let specs_enabled = current_specs_enabled(&monitor);
|
||||
let specs_mode = current_specs_mode(&monitor);
|
||||
let memes_enabled = current_memes_enabled(&monitor);
|
||||
let gifs_enabled = current_gifs_enabled(&monitor);
|
||||
let ricardo_enabled = current_ricardo_enabled(&monitor);
|
||||
let before = active_images.len();
|
||||
active_images.retain(|name| name != &payload.name);
|
||||
if active_images.len() != before {
|
||||
@@ -236,7 +235,7 @@ async fn api_delete(
|
||||
specs_enabled,
|
||||
&specs_mode,
|
||||
memes_enabled,
|
||||
gifs_enabled,
|
||||
ricardo_enabled,
|
||||
&active_images,
|
||||
);
|
||||
if let Err(err) = save_monitor_json(&state, &monitor).await {
|
||||
|
||||
@@ -49,7 +49,7 @@ pub(crate) struct RotationSnapshot {
|
||||
pub(crate) specs_enabled: bool,
|
||||
pub(crate) specs_mode: String,
|
||||
pub(crate) memes_enabled: bool,
|
||||
pub(crate) gifs_enabled: bool,
|
||||
pub(crate) ricardo_enabled: bool,
|
||||
pub(crate) active_images: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@ impl RotationSnapshot {
|
||||
pub(crate) fn rotation_active(&self) -> bool {
|
||||
self.custom_panel
|
||||
&& (self.specs_enabled
|
||||
|| ((self.memes_enabled || self.gifs_enabled) && !self.active_images.is_empty()))
|
||||
|| (self.memes_enabled && !self.active_images.is_empty())
|
||||
|| self.ricardo_enabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +111,7 @@ pub(crate) struct SetupView {
|
||||
pub(crate) specs_enabled: bool,
|
||||
pub(crate) specs_mode: String,
|
||||
pub(crate) memes_enabled: bool,
|
||||
pub(crate) gifs_enabled: bool,
|
||||
pub(crate) ricardo_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
@@ -133,7 +134,7 @@ pub(crate) struct DisplayStatus {
|
||||
pub(crate) custom_panel: bool,
|
||||
pub(crate) specs_enabled: bool,
|
||||
pub(crate) memes_enabled: bool,
|
||||
pub(crate) gifs_enabled: bool,
|
||||
pub(crate) ricardo_enabled: bool,
|
||||
pub(crate) rotation_active: bool,
|
||||
pub(crate) switch_time: String,
|
||||
pub(crate) active_images: Vec<String>,
|
||||
@@ -177,7 +178,7 @@ pub(crate) struct ActivateRequest {
|
||||
pub(crate) specs_enabled: Option<bool>,
|
||||
pub(crate) specs_mode: Option<String>,
|
||||
pub(crate) memes_enabled: Option<bool>,
|
||||
pub(crate) gifs_enabled: Option<bool>,
|
||||
pub(crate) ricardo_enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
@@ -145,7 +145,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
.subtle { color: var(--faint); font-size: .82rem; }
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: 132px minmax(220px, 1fr) auto;
|
||||
grid-template-columns: 132px minmax(220px, 1fr) 180px auto;
|
||||
gap: 10px;
|
||||
align-items: end;
|
||||
}
|
||||
@@ -197,6 +197,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
margin-top: 12px;
|
||||
}
|
||||
.select-wrap select {
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--line);
|
||||
@@ -304,13 +305,17 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(176px, 1fr));
|
||||
gap: 10px;
|
||||
align-items: stretch;
|
||||
}
|
||||
.tile {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
border: 1px solid var(--line-soft);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #0b1016;
|
||||
min-width: 0;
|
||||
min-height: 246px;
|
||||
}
|
||||
.tile.selected {
|
||||
border-color: var(--accent-2);
|
||||
@@ -327,11 +332,16 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
.tile-body {
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
grid-template-rows: 4.7em 2.2em 1fr auto;
|
||||
gap: 9px;
|
||||
min-height: 0;
|
||||
}
|
||||
.tile-title {
|
||||
min-height: 2.5em;
|
||||
line-height: 1.28;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
word-break: break-word;
|
||||
color: var(--text);
|
||||
}
|
||||
@@ -339,8 +349,17 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
color: var(--faint);
|
||||
font-size: .82rem;
|
||||
line-height: 1.25;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.mini {
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
align-self: end;
|
||||
}
|
||||
.mini { display: flex; gap: 7px; flex-wrap: wrap; align-items: center; }
|
||||
.mini button {
|
||||
min-height: 34px;
|
||||
padding: 7px 10px;
|
||||
@@ -491,6 +510,13 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
<span>Upload Source</span>
|
||||
<input id="upload" type="file" accept="image/*">
|
||||
</label>
|
||||
<label class="select-wrap">
|
||||
<span>Specs View</span>
|
||||
<select id="specsMode">
|
||||
<option value="cards">Cards</option>
|
||||
<option value="meters">Htop Style</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="actions">
|
||||
<button id="disable" class="danger" type="button">Disable</button>
|
||||
</div>
|
||||
@@ -498,16 +524,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
<div class="toggle-row">
|
||||
<label class="toggle-pill"><input id="specsEnabled" type="checkbox"> System Specs</label>
|
||||
<label class="toggle-pill"><input id="memesEnabled" type="checkbox"> Memes</label>
|
||||
<label class="toggle-pill"><input id="gifsEnabled" type="checkbox"> GIFs</label>
|
||||
</div>
|
||||
<div class="toggle-row">
|
||||
<label class="select-wrap">
|
||||
<span>Screen Layout</span>
|
||||
<select id="specsMode">
|
||||
<option value="cards">Cards</option>
|
||||
<option value="meters">Meters</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="toggle-pill"><input id="ricardoEnabled" type="checkbox"> Ricardo</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -573,7 +590,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
const state = {
|
||||
images: [],
|
||||
activeImages: [],
|
||||
setup: { switch_time: "10", custom_panel: false, specs_enabled: false, specs_mode: "cards", memes_enabled: false, gifs_enabled: true },
|
||||
setup: { switch_time: "10", custom_panel: false, specs_enabled: false, specs_mode: "cards", memes_enabled: false, ricardo_enabled: false },
|
||||
display: {},
|
||||
system: {},
|
||||
activeTab: "screen-control",
|
||||
@@ -588,7 +605,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
specsEnabled: document.getElementById("specsEnabled"),
|
||||
specsMode: document.getElementById("specsMode"),
|
||||
memesEnabled: document.getElementById("memesEnabled"),
|
||||
gifsEnabled: document.getElementById("gifsEnabled"),
|
||||
ricardoEnabled: document.getElementById("ricardoEnabled"),
|
||||
customPanelValue: document.getElementById("customPanelValue"),
|
||||
imageCountValue: document.getElementById("imageCountValue"),
|
||||
displayStatusValue: document.getElementById("displayStatusValue"),
|
||||
@@ -732,7 +749,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
specs_enabled: el.specsEnabled.checked,
|
||||
specs_mode: el.specsMode.value || data.setup.specs_mode || "cards",
|
||||
memes_enabled: el.memesEnabled.checked,
|
||||
gifs_enabled: el.gifsEnabled.checked,
|
||||
ricardo_enabled: el.ricardoEnabled.checked,
|
||||
}
|
||||
: data.setup;
|
||||
state.display = data.display || {};
|
||||
@@ -742,12 +759,12 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
el.specsEnabled.checked = !!data.setup.specs_enabled;
|
||||
el.specsMode.value = data.setup.specs_mode || "cards";
|
||||
el.memesEnabled.checked = !!data.setup.memes_enabled;
|
||||
el.gifsEnabled.checked = data.setup.gifs_enabled !== false;
|
||||
el.ricardoEnabled.checked = !!data.setup.ricardo_enabled;
|
||||
}
|
||||
const enabledModes = [
|
||||
state.setup.specs_enabled ? "Specs" : "",
|
||||
state.setup.memes_enabled ? "Memes" : "",
|
||||
state.setup.gifs_enabled ? "GIFs" : "",
|
||||
state.setup.ricardo_enabled ? "Ricardo" : "",
|
||||
].filter(Boolean);
|
||||
el.customPanelValue.textContent = state.setup.custom_panel
|
||||
? (enabledModes.join(" + ") || "Active")
|
||||
@@ -802,7 +819,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
specs_enabled: el.specsEnabled.checked,
|
||||
specs_mode: el.specsMode.value,
|
||||
memes_enabled: el.memesEnabled.checked,
|
||||
gifs_enabled: el.gifsEnabled.checked,
|
||||
ricardo_enabled: el.ricardoEnabled.checked,
|
||||
}),
|
||||
});
|
||||
clearDirty();
|
||||
@@ -833,7 +850,8 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
const imageMap = new Map(state.images.map((image) => [image.name, image]));
|
||||
el.gallery.innerHTML = "";
|
||||
for (const image of state.images) {
|
||||
const selected = state.activeImages.includes(image.name);
|
||||
const selectable = !image.animated;
|
||||
const selected = selectable && state.activeImages.includes(image.name);
|
||||
const tile = document.createElement("article");
|
||||
const safeLabel = escapeHtml(image.label);
|
||||
const safeName = escapeHtml(image.name);
|
||||
@@ -842,16 +860,18 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
tile.innerHTML = `
|
||||
<img src="${safeUrl}" alt="${safeName}">
|
||||
<div class="tile-body">
|
||||
<div class="tile-title">${safeLabel}</div>
|
||||
<div class="tile-meta">${bytes(image.size)}${gifFrameLabel(image)}</div>
|
||||
<div class="tile-title" title="${safeLabel}">${safeLabel}</div>
|
||||
<div class="tile-meta" title="${bytes(image.size)}${gifFrameLabel(image)}">${bytes(image.size)}${gifFrameLabel(image)}</div>
|
||||
<div class="mini">
|
||||
<button class="ghost" type="button" title="${selected ? "Remove" : "Add"}">${selected ? "Remove" : "Add"}</button>
|
||||
<button class="ghost" type="button" title="${selectable ? (selected ? "Remove" : "Add") : "Animated GIFs are handled by the Ricardo toggle"}"${selectable ? "" : " disabled"}>${selectable ? (selected ? "Remove" : "Add") : "Built-in only"}</button>
|
||||
<button class="danger" type="button">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const [toggleBtn, deleteBtn] = tile.querySelectorAll("button");
|
||||
toggleBtn.addEventListener("click", () => toggle(image.name));
|
||||
if (selectable) {
|
||||
toggleBtn.addEventListener("click", () => toggle(image.name));
|
||||
}
|
||||
deleteBtn.addEventListener("click", () => removeImage(image.name));
|
||||
el.gallery.appendChild(tile);
|
||||
}
|
||||
@@ -927,7 +947,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
el.specsEnabled.addEventListener("change", markDirty);
|
||||
el.specsMode.addEventListener("change", markDirty);
|
||||
el.memesEnabled.addEventListener("change", markDirty);
|
||||
el.gifsEnabled.addEventListener("change", markDirty);
|
||||
el.ricardoEnabled.addEventListener("change", markDirty);
|
||||
el.tabButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => setActiveTab(button.dataset.tabTarget));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user