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 tracing::{error, info, warn};
|
||||||
|
|
||||||
use crate::{
|
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},
|
monitor::{load_monitor_json_sync, rotation_snapshot},
|
||||||
system::{overlay_system_specs, render_system_panel},
|
system::{overlay_system_specs, render_system_panel},
|
||||||
types::{ActiveFrame, AppState, DisplayConfig, DisplayStatus, RotationSlot, RotationSnapshot},
|
types::{ActiveFrame, AppState, DisplayConfig, DisplayStatus, RotationSlot, RotationSnapshot},
|
||||||
@@ -36,7 +39,7 @@ pub(crate) fn initial_display_status(config: &DisplayConfig) -> DisplayStatus {
|
|||||||
custom_panel: false,
|
custom_panel: false,
|
||||||
specs_enabled: false,
|
specs_enabled: false,
|
||||||
memes_enabled: false,
|
memes_enabled: false,
|
||||||
gifs_enabled: false,
|
ricardo_enabled: false,
|
||||||
rotation_active: false,
|
rotation_active: false,
|
||||||
switch_time: "10".into(),
|
switch_time: "10".into(),
|
||||||
active_images: Vec::new(),
|
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.custom_panel = snapshot.custom_panel;
|
||||||
status.specs_enabled = snapshot.specs_enabled;
|
status.specs_enabled = snapshot.specs_enabled;
|
||||||
status.memes_enabled = snapshot.memes_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.rotation_active = snapshot.rotation_active();
|
||||||
status.switch_time = snapshot.switch_time.to_string();
|
status.switch_time = snapshot.switch_time.to_string();
|
||||||
status.active_images = snapshot.active_images.clone();
|
status.active_images = snapshot.active_images.clone();
|
||||||
@@ -239,37 +242,11 @@ fn build_rotation_slots(
|
|||||||
image_dir: &std::path::PathBuf,
|
image_dir: &std::path::PathBuf,
|
||||||
) -> Vec<RotationSlot> {
|
) -> Vec<RotationSlot> {
|
||||||
let mut slots = Vec::new();
|
let mut slots = Vec::new();
|
||||||
if snapshot.memes_enabled || snapshot.gifs_enabled {
|
if snapshot.memes_enabled {
|
||||||
for name in &snapshot.active_images {
|
for name in &snapshot.active_images {
|
||||||
if is_gif_manifest_name(name) {
|
if is_gif_manifest_name(name) {
|
||||||
if !snapshot.gifs_enabled {
|
continue;
|
||||||
continue;
|
} else {
|
||||||
}
|
|
||||||
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 {
|
|
||||||
slots.push(RotationSlot {
|
slots.push(RotationSlot {
|
||||||
total_duration: Duration::from_secs(snapshot.switch_time as u64),
|
total_duration: Duration::from_secs(snapshot.switch_time as u64),
|
||||||
frames: vec![(
|
frames: vec![(
|
||||||
@@ -281,8 +258,32 @@ fn build_rotation_slots(
|
|||||||
},
|
},
|
||||||
Duration::from_secs(snapshot.switch_time as u64),
|
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 {
|
if slots.is_empty() && snapshot.specs_enabled {
|
||||||
@@ -633,14 +634,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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()
|
let unique = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_nanos();
|
.as_nanos();
|
||||||
let image_dir = std::env::temp_dir().join(format!("aster-webui-gif-test-{unique}"));
|
let image_dir = std::env::temp_dir().join(format!("aster-webui-gif-test-{unique}"));
|
||||||
fs::create_dir_all(&image_dir).unwrap();
|
fs::create_dir_all(&image_dir).unwrap();
|
||||||
let manifest_name = "panel-test.gifset.json";
|
let manifest_name = RICARDO_MANIFEST_NAME;
|
||||||
fs::write(
|
fs::write(
|
||||||
image_dir.join(manifest_name),
|
image_dir.join(manifest_name),
|
||||||
r#"{
|
r#"{
|
||||||
@@ -662,8 +663,8 @@ mod tests {
|
|||||||
specs_enabled: false,
|
specs_enabled: false,
|
||||||
specs_mode: "cards".into(),
|
specs_mode: "cards".into(),
|
||||||
memes_enabled: false,
|
memes_enabled: false,
|
||||||
gifs_enabled: true,
|
ricardo_enabled: true,
|
||||||
active_images: vec![manifest_name.into()],
|
active_images: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let slots = build_rotation_slots(&snapshot, &image_dir);
|
let slots = build_rotation_slots(&snapshot, &image_dir);
|
||||||
@@ -675,7 +676,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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()
|
let unique = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -704,8 +705,8 @@ mod tests {
|
|||||||
switch_time: 7,
|
switch_time: 7,
|
||||||
specs_enabled: false,
|
specs_enabled: false,
|
||||||
specs_mode: "cards".into(),
|
specs_mode: "cards".into(),
|
||||||
memes_enabled: false,
|
memes_enabled: true,
|
||||||
gifs_enabled: false,
|
ricardo_enabled: false,
|
||||||
active_images: vec![manifest_name.into()],
|
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 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;
|
pub(crate) const MIN_GIF_FRAME_DELAY_MS: u32 = 33;
|
||||||
const GIF_FRAME_PREFIX: &str = ".gifframe-";
|
const GIF_FRAME_PREFIX: &str = ".gifframe-";
|
||||||
const GIF_MANIFEST_SUFFIX: &str = ".gifset.json";
|
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>> {
|
pub(crate) async fn list_images(state: &AppState) -> Result<Vec<ImageView>> {
|
||||||
let mut out = Vec::new();
|
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?;
|
let meta = entry.metadata().await?;
|
||||||
if is_gif_manifest_name(&name) {
|
if is_gif_manifest_name(&name) {
|
||||||
|
if is_builtin_ricardo_manifest_name(&name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let raw = fs::read_to_string(&path).await?;
|
let raw = fs::read_to_string(&path).await?;
|
||||||
let manifest: GifSetManifest = serde_json::from_str(&raw)
|
let manifest: GifSetManifest = serde_json::from_str(&raw)
|
||||||
.with_context(|| format!("Failed to parse GIF manifest {}", path.display()))?;
|
.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> {
|
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))
|
let decoder = GifDecoder::new(Cursor::new(bytes))
|
||||||
.with_context(|| format!("Unsupported GIF format: {file_name}"))?;
|
.with_context(|| format!("Unsupported GIF format: {file_name}"))?;
|
||||||
let frames = decoder
|
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 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 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();
|
let mut rendered_frames = Vec::new();
|
||||||
|
|
||||||
for frame in frames {
|
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();
|
let mut stored_frames = Vec::new();
|
||||||
for (idx, (rendered, delay_ms)) in collapsed_frames.into_iter().enumerate() {
|
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);
|
let frame_path = state.image_dir.join(&frame_name);
|
||||||
rendered
|
rendered
|
||||||
.save_with_format(&frame_path, ImageFormat::Jpeg)
|
.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)
|
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 {
|
fn is_gif_frame_name(name: &str) -> bool {
|
||||||
name.starts_with(GIF_FRAME_PREFIX)
|
name.starts_with(GIF_FRAME_PREFIX)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ use tokio::fs;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
display::read_display_status,
|
display::read_display_status,
|
||||||
images::list_images,
|
images::{ensure_builtin_ricardo_gif, list_images},
|
||||||
system::collect_system_view,
|
system::collect_system_view,
|
||||||
types::{AppState, RotationSnapshot, SetupView, StateResponse},
|
types::{AppState, RotationSnapshot, SetupView, StateResponse},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) async fn ensure_layout(state: &AppState) -> Result<()> {
|
pub(crate) async fn ensure_layout(state: &AppState) -> Result<()> {
|
||||||
fs::create_dir_all(&state.image_dir).await?;
|
fs::create_dir_all(&state.image_dir).await?;
|
||||||
|
ensure_builtin_ricardo_gif(state).await?;
|
||||||
|
|
||||||
if fs::try_exists(&state.monitor_path).await? {
|
if fs::try_exists(&state.monitor_path).await? {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -34,7 +35,7 @@ pub(crate) async fn ensure_layout(state: &AppState) -> Result<()> {
|
|||||||
"nativeSpecs": false,
|
"nativeSpecs": false,
|
||||||
"nativeSpecsMode": "cards",
|
"nativeSpecsMode": "cards",
|
||||||
"nativeMemes": true,
|
"nativeMemes": true,
|
||||||
"nativeGifs": true,
|
"nativeRicardo": false,
|
||||||
"operationMode": 0,
|
"operationMode": 0,
|
||||||
"theme": 1,
|
"theme": 1,
|
||||||
"diskUpdate": 300,
|
"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_enabled = current_specs_enabled(&monitor);
|
||||||
let specs_mode = current_specs_mode(&monitor);
|
let specs_mode = current_specs_mode(&monitor);
|
||||||
let memes_enabled = current_memes_enabled(&monitor);
|
let memes_enabled = current_memes_enabled(&monitor);
|
||||||
let gifs_enabled = current_gifs_enabled(&monitor);
|
let ricardo_enabled = current_ricardo_enabled(&monitor);
|
||||||
|
|
||||||
Ok(StateResponse {
|
Ok(StateResponse {
|
||||||
monitor_path: state.monitor_path.display().to_string(),
|
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_enabled,
|
||||||
specs_mode,
|
specs_mode,
|
||||||
memes_enabled,
|
memes_enabled,
|
||||||
gifs_enabled,
|
ricardo_enabled,
|
||||||
},
|
},
|
||||||
active_images: current_active_images(&monitor),
|
active_images: current_active_images(&monitor),
|
||||||
images: list_images(state).await?,
|
images: list_images(state).await?,
|
||||||
@@ -123,6 +124,7 @@ pub(crate) fn current_active_images(monitor: &Value) -> Vec<String> {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.filter_map(|panel| panel.get("img").and_then(Value::as_str))
|
.filter_map(|panel| panel.get("img").and_then(Value::as_str))
|
||||||
|
.filter(|name| *name != crate::images::RICARDO_MANIFEST_NAME)
|
||||||
.map(ToString::to_string)
|
.map(ToString::to_string)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -175,13 +177,17 @@ pub(crate) fn current_specs_mode(monitor: &Value) -> String {
|
|||||||
.unwrap_or_else(|| "cards".into())
|
.unwrap_or_else(|| "cards".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn current_gifs_enabled(monitor: &Value) -> bool {
|
pub(crate) fn current_ricardo_enabled(monitor: &Value) -> bool {
|
||||||
monitor
|
monitor
|
||||||
.get("setup")
|
.get("setup")
|
||||||
.and_then(Value::as_object)
|
.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)
|
.and_then(Value::as_bool)
|
||||||
.unwrap_or(true)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn rotation_snapshot(monitor: &Value) -> RotationSnapshot {
|
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_enabled: current_specs_enabled(monitor),
|
||||||
specs_mode: current_specs_mode(monitor),
|
specs_mode: current_specs_mode(monitor),
|
||||||
memes_enabled: current_memes_enabled(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),
|
active_images: current_active_images(monitor),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,10 +208,10 @@ pub(crate) fn set_custom_panels(
|
|||||||
specs_enabled: bool,
|
specs_enabled: bool,
|
||||||
specs_mode: &str,
|
specs_mode: &str,
|
||||||
memes_enabled: bool,
|
memes_enabled: bool,
|
||||||
gifs_enabled: bool,
|
ricardo_enabled: bool,
|
||||||
images: &[String],
|
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
|
let setup = monitor
|
||||||
.as_object_mut()
|
.as_object_mut()
|
||||||
.expect("monitor config must be an object")
|
.expect("monitor config must be an object")
|
||||||
@@ -221,16 +227,21 @@ pub(crate) fn set_custom_panels(
|
|||||||
Value::String(normalize_specs_mode(specs_mode)),
|
Value::String(normalize_specs_mode(specs_mode)),
|
||||||
);
|
);
|
||||||
setup.insert("nativeMemes".into(), Value::Bool(memes_enabled));
|
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(
|
monitor["mianban"] = Value::Array(
|
||||||
(1..=images.len())
|
(1..=diy_images.len())
|
||||||
.map(|index| Value::Number((index as u64).into()))
|
.map(|index| Value::Number((index as u64).into()))
|
||||||
.collect(),
|
.collect(),
|
||||||
);
|
);
|
||||||
monitor["diy"] = Value::Array(
|
monitor["diy"] = Value::Array(
|
||||||
images
|
diy_images
|
||||||
.iter()
|
.iter()
|
||||||
.map(|name| json!({"type": 5, "img": name, "sensor": []}))
|
.map(|name| json!({"type": 5, "img": name, "sensor": []}))
|
||||||
.collect(),
|
.collect(),
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ use tokio::fs;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
images::{
|
images::{
|
||||||
convert_and_store_image, delete_image_asset, is_gif_manifest_name, list_images,
|
convert_and_store_image, delete_image_asset, is_builtin_ricardo_manifest_name,
|
||||||
NYAN_CAT_GIF,
|
is_gif_manifest_name, list_images, NYAN_CAT_GIF,
|
||||||
},
|
},
|
||||||
monitor::{
|
monitor::{
|
||||||
build_state_response, current_active_images, current_gifs_enabled, current_memes_enabled,
|
build_state_response, current_active_images, current_memes_enabled,
|
||||||
current_specs_enabled, current_specs_mode, current_switch_time, load_monitor_json,
|
current_ricardo_enabled, current_specs_enabled, current_specs_mode, current_switch_time,
|
||||||
save_monitor_json, set_custom_panels,
|
load_monitor_json, save_monitor_json, set_custom_panels,
|
||||||
},
|
},
|
||||||
types::{ActivateRequest, AppState, DeleteRequest, ErrorResponse},
|
types::{ActivateRequest, AppState, DeleteRequest, ErrorResponse},
|
||||||
ui::index_response,
|
ui::index_response,
|
||||||
@@ -103,6 +103,7 @@ async fn api_activate(
|
|||||||
) -> Response {
|
) -> Response {
|
||||||
let switch_time = payload.switch_time.unwrap_or(10).clamp(1, 600);
|
let switch_time = payload.switch_time.unwrap_or(10).clamp(1, 600);
|
||||||
let specs_enabled = payload.specs_enabled.unwrap_or(false);
|
let specs_enabled = payload.specs_enabled.unwrap_or(false);
|
||||||
|
let ricardo_enabled = payload.ricardo_enabled.unwrap_or(false);
|
||||||
let specs_mode = payload
|
let specs_mode = payload
|
||||||
.specs_mode
|
.specs_mode
|
||||||
.as_deref()
|
.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_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 memes_enabled = payload.memes_enabled.unwrap_or(has_static_selection);
|
||||||
let gifs_enabled = payload.gifs_enabled.unwrap_or(has_gif_selection);
|
|
||||||
|
|
||||||
if !specs_enabled
|
if !specs_enabled && !(memes_enabled && has_static_selection) && !ricardo_enabled {
|
||||||
&& !(memes_enabled && has_static_selection)
|
|
||||||
&& !(gifs_enabled && has_gif_selection)
|
|
||||||
{
|
|
||||||
return error_response(
|
return error_response(
|
||||||
StatusCode::BAD_REQUEST,
|
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_enabled,
|
||||||
&specs_mode,
|
&specs_mode,
|
||||||
memes_enabled,
|
memes_enabled,
|
||||||
gifs_enabled,
|
ricardo_enabled,
|
||||||
&valid,
|
&valid,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -167,7 +163,7 @@ async fn api_activate(
|
|||||||
"specsEnabled": specs_enabled,
|
"specsEnabled": specs_enabled,
|
||||||
"specsMode": specs_mode,
|
"specsMode": specs_mode,
|
||||||
"memesEnabled": memes_enabled,
|
"memesEnabled": memes_enabled,
|
||||||
"gifsEnabled": gifs_enabled,
|
"ricardoEnabled": ricardo_enabled,
|
||||||
"activeImages": valid,
|
"activeImages": valid,
|
||||||
}))
|
}))
|
||||||
.into_response()
|
.into_response()
|
||||||
@@ -206,6 +202,9 @@ async fn api_delete(
|
|||||||
if payload.name.contains('/') || payload.name.contains('\\') {
|
if payload.name.contains('/') || payload.name.contains('\\') {
|
||||||
return error_response(StatusCode::BAD_REQUEST, "Invalid file name");
|
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 {
|
if let Err(err) = delete_image_asset(&state, &payload.name).await {
|
||||||
return match err.downcast_ref::<std::io::Error>() {
|
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_enabled = current_specs_enabled(&monitor);
|
||||||
let specs_mode = current_specs_mode(&monitor);
|
let specs_mode = current_specs_mode(&monitor);
|
||||||
let memes_enabled = current_memes_enabled(&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();
|
let before = active_images.len();
|
||||||
active_images.retain(|name| name != &payload.name);
|
active_images.retain(|name| name != &payload.name);
|
||||||
if active_images.len() != before {
|
if active_images.len() != before {
|
||||||
@@ -236,7 +235,7 @@ async fn api_delete(
|
|||||||
specs_enabled,
|
specs_enabled,
|
||||||
&specs_mode,
|
&specs_mode,
|
||||||
memes_enabled,
|
memes_enabled,
|
||||||
gifs_enabled,
|
ricardo_enabled,
|
||||||
&active_images,
|
&active_images,
|
||||||
);
|
);
|
||||||
if let Err(err) = save_monitor_json(&state, &monitor).await {
|
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_enabled: bool,
|
||||||
pub(crate) specs_mode: String,
|
pub(crate) specs_mode: String,
|
||||||
pub(crate) memes_enabled: bool,
|
pub(crate) memes_enabled: bool,
|
||||||
pub(crate) gifs_enabled: bool,
|
pub(crate) ricardo_enabled: bool,
|
||||||
pub(crate) active_images: Vec<String>,
|
pub(crate) active_images: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +57,8 @@ impl RotationSnapshot {
|
|||||||
pub(crate) fn rotation_active(&self) -> bool {
|
pub(crate) fn rotation_active(&self) -> bool {
|
||||||
self.custom_panel
|
self.custom_panel
|
||||||
&& (self.specs_enabled
|
&& (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_enabled: bool,
|
||||||
pub(crate) specs_mode: String,
|
pub(crate) specs_mode: String,
|
||||||
pub(crate) memes_enabled: bool,
|
pub(crate) memes_enabled: bool,
|
||||||
pub(crate) gifs_enabled: bool,
|
pub(crate) ricardo_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize)]
|
#[derive(Clone, Serialize)]
|
||||||
@@ -133,7 +134,7 @@ pub(crate) struct DisplayStatus {
|
|||||||
pub(crate) custom_panel: bool,
|
pub(crate) custom_panel: bool,
|
||||||
pub(crate) specs_enabled: bool,
|
pub(crate) specs_enabled: bool,
|
||||||
pub(crate) memes_enabled: bool,
|
pub(crate) memes_enabled: bool,
|
||||||
pub(crate) gifs_enabled: bool,
|
pub(crate) ricardo_enabled: bool,
|
||||||
pub(crate) rotation_active: bool,
|
pub(crate) rotation_active: bool,
|
||||||
pub(crate) switch_time: String,
|
pub(crate) switch_time: String,
|
||||||
pub(crate) active_images: Vec<String>,
|
pub(crate) active_images: Vec<String>,
|
||||||
@@ -177,7 +178,7 @@ pub(crate) struct ActivateRequest {
|
|||||||
pub(crate) specs_enabled: Option<bool>,
|
pub(crate) specs_enabled: Option<bool>,
|
||||||
pub(crate) specs_mode: Option<String>,
|
pub(crate) specs_mode: Option<String>,
|
||||||
pub(crate) memes_enabled: Option<bool>,
|
pub(crate) memes_enabled: Option<bool>,
|
||||||
pub(crate) gifs_enabled: Option<bool>,
|
pub(crate) ricardo_enabled: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
.subtle { color: var(--faint); font-size: .82rem; }
|
.subtle { color: var(--faint); font-size: .82rem; }
|
||||||
.controls {
|
.controls {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 132px minmax(220px, 1fr) auto;
|
grid-template-columns: 132px minmax(220px, 1fr) 180px auto;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
}
|
}
|
||||||
@@ -197,6 +197,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
.select-wrap select {
|
.select-wrap select {
|
||||||
|
width: 100%;
|
||||||
min-height: 42px;
|
min-height: 42px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
@@ -304,13 +305,17 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(176px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(176px, 1fr));
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
.tile {
|
.tile {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
border: 1px solid var(--line-soft);
|
border: 1px solid var(--line-soft);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #0b1016;
|
background: #0b1016;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
min-height: 246px;
|
||||||
}
|
}
|
||||||
.tile.selected {
|
.tile.selected {
|
||||||
border-color: var(--accent-2);
|
border-color: var(--accent-2);
|
||||||
@@ -327,11 +332,16 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
.tile-body {
|
.tile-body {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-template-rows: 4.7em 2.2em 1fr auto;
|
||||||
gap: 9px;
|
gap: 9px;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
.tile-title {
|
.tile-title {
|
||||||
min-height: 2.5em;
|
|
||||||
line-height: 1.28;
|
line-height: 1.28;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
@@ -339,8 +349,17 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
color: var(--faint);
|
color: var(--faint);
|
||||||
font-size: .82rem;
|
font-size: .82rem;
|
||||||
line-height: 1.25;
|
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 {
|
.mini button {
|
||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
@@ -491,6 +510,13 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
<span>Upload Source</span>
|
<span>Upload Source</span>
|
||||||
<input id="upload" type="file" accept="image/*">
|
<input id="upload" type="file" accept="image/*">
|
||||||
</label>
|
</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">
|
<div class="actions">
|
||||||
<button id="disable" class="danger" type="button">Disable</button>
|
<button id="disable" class="danger" type="button">Disable</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -498,16 +524,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
<div class="toggle-row">
|
<div class="toggle-row">
|
||||||
<label class="toggle-pill"><input id="specsEnabled" type="checkbox"> System Specs</label>
|
<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="memesEnabled" type="checkbox"> Memes</label>
|
||||||
<label class="toggle-pill"><input id="gifsEnabled" type="checkbox"> GIFs</label>
|
<label class="toggle-pill"><input id="ricardoEnabled" type="checkbox"> Ricardo</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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -573,7 +590,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
const state = {
|
const state = {
|
||||||
images: [],
|
images: [],
|
||||||
activeImages: [],
|
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: {},
|
display: {},
|
||||||
system: {},
|
system: {},
|
||||||
activeTab: "screen-control",
|
activeTab: "screen-control",
|
||||||
@@ -588,7 +605,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
specsEnabled: document.getElementById("specsEnabled"),
|
specsEnabled: document.getElementById("specsEnabled"),
|
||||||
specsMode: document.getElementById("specsMode"),
|
specsMode: document.getElementById("specsMode"),
|
||||||
memesEnabled: document.getElementById("memesEnabled"),
|
memesEnabled: document.getElementById("memesEnabled"),
|
||||||
gifsEnabled: document.getElementById("gifsEnabled"),
|
ricardoEnabled: document.getElementById("ricardoEnabled"),
|
||||||
customPanelValue: document.getElementById("customPanelValue"),
|
customPanelValue: document.getElementById("customPanelValue"),
|
||||||
imageCountValue: document.getElementById("imageCountValue"),
|
imageCountValue: document.getElementById("imageCountValue"),
|
||||||
displayStatusValue: document.getElementById("displayStatusValue"),
|
displayStatusValue: document.getElementById("displayStatusValue"),
|
||||||
@@ -732,7 +749,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
specs_enabled: el.specsEnabled.checked,
|
specs_enabled: el.specsEnabled.checked,
|
||||||
specs_mode: el.specsMode.value || data.setup.specs_mode || "cards",
|
specs_mode: el.specsMode.value || data.setup.specs_mode || "cards",
|
||||||
memes_enabled: el.memesEnabled.checked,
|
memes_enabled: el.memesEnabled.checked,
|
||||||
gifs_enabled: el.gifsEnabled.checked,
|
ricardo_enabled: el.ricardoEnabled.checked,
|
||||||
}
|
}
|
||||||
: data.setup;
|
: data.setup;
|
||||||
state.display = data.display || {};
|
state.display = data.display || {};
|
||||||
@@ -742,12 +759,12 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
el.specsEnabled.checked = !!data.setup.specs_enabled;
|
el.specsEnabled.checked = !!data.setup.specs_enabled;
|
||||||
el.specsMode.value = data.setup.specs_mode || "cards";
|
el.specsMode.value = data.setup.specs_mode || "cards";
|
||||||
el.memesEnabled.checked = !!data.setup.memes_enabled;
|
el.memesEnabled.checked = !!data.setup.memes_enabled;
|
||||||
el.gifsEnabled.checked = data.setup.gifs_enabled !== false;
|
el.ricardoEnabled.checked = !!data.setup.ricardo_enabled;
|
||||||
}
|
}
|
||||||
const enabledModes = [
|
const enabledModes = [
|
||||||
state.setup.specs_enabled ? "Specs" : "",
|
state.setup.specs_enabled ? "Specs" : "",
|
||||||
state.setup.memes_enabled ? "Memes" : "",
|
state.setup.memes_enabled ? "Memes" : "",
|
||||||
state.setup.gifs_enabled ? "GIFs" : "",
|
state.setup.ricardo_enabled ? "Ricardo" : "",
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
el.customPanelValue.textContent = state.setup.custom_panel
|
el.customPanelValue.textContent = state.setup.custom_panel
|
||||||
? (enabledModes.join(" + ") || "Active")
|
? (enabledModes.join(" + ") || "Active")
|
||||||
@@ -802,7 +819,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
specs_enabled: el.specsEnabled.checked,
|
specs_enabled: el.specsEnabled.checked,
|
||||||
specs_mode: el.specsMode.value,
|
specs_mode: el.specsMode.value,
|
||||||
memes_enabled: el.memesEnabled.checked,
|
memes_enabled: el.memesEnabled.checked,
|
||||||
gifs_enabled: el.gifsEnabled.checked,
|
ricardo_enabled: el.ricardoEnabled.checked,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
clearDirty();
|
clearDirty();
|
||||||
@@ -833,7 +850,8 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
const imageMap = new Map(state.images.map((image) => [image.name, image]));
|
const imageMap = new Map(state.images.map((image) => [image.name, image]));
|
||||||
el.gallery.innerHTML = "";
|
el.gallery.innerHTML = "";
|
||||||
for (const image of state.images) {
|
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 tile = document.createElement("article");
|
||||||
const safeLabel = escapeHtml(image.label);
|
const safeLabel = escapeHtml(image.label);
|
||||||
const safeName = escapeHtml(image.name);
|
const safeName = escapeHtml(image.name);
|
||||||
@@ -842,16 +860,18 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
tile.innerHTML = `
|
tile.innerHTML = `
|
||||||
<img src="${safeUrl}" alt="${safeName}">
|
<img src="${safeUrl}" alt="${safeName}">
|
||||||
<div class="tile-body">
|
<div class="tile-body">
|
||||||
<div class="tile-title">${safeLabel}</div>
|
<div class="tile-title" title="${safeLabel}">${safeLabel}</div>
|
||||||
<div class="tile-meta">${bytes(image.size)}${gifFrameLabel(image)}</div>
|
<div class="tile-meta" title="${bytes(image.size)}${gifFrameLabel(image)}">${bytes(image.size)}${gifFrameLabel(image)}</div>
|
||||||
<div class="mini">
|
<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>
|
<button class="danger" type="button">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
const [toggleBtn, deleteBtn] = tile.querySelectorAll("button");
|
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));
|
deleteBtn.addEventListener("click", () => removeImage(image.name));
|
||||||
el.gallery.appendChild(tile);
|
el.gallery.appendChild(tile);
|
||||||
}
|
}
|
||||||
@@ -927,7 +947,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
el.specsEnabled.addEventListener("change", markDirty);
|
el.specsEnabled.addEventListener("change", markDirty);
|
||||||
el.specsMode.addEventListener("change", markDirty);
|
el.specsMode.addEventListener("change", markDirty);
|
||||||
el.memesEnabled.addEventListener("change", markDirty);
|
el.memesEnabled.addEventListener("change", markDirty);
|
||||||
el.gifsEnabled.addEventListener("change", markDirty);
|
el.ricardoEnabled.addEventListener("change", markDirty);
|
||||||
el.tabButtons.forEach((button) => {
|
el.tabButtons.forEach((button) => {
|
||||||
button.addEventListener("click", () => setActiveTab(button.dataset.tabTarget));
|
button.addEventListener("click", () => setActiveTab(button.dataset.tabTarget));
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user