fix: improve aster-webui media rotation controls
Build And Push Container / build-and-push (push) Successful in 1m29s
Build And Push Container / build-and-push (push) Successful in 1m29s
This commit is contained in:
+225
-36
@@ -83,12 +83,15 @@ struct RotationSnapshot {
|
|||||||
switch_time: u32,
|
switch_time: u32,
|
||||||
specs_enabled: bool,
|
specs_enabled: bool,
|
||||||
memes_enabled: bool,
|
memes_enabled: bool,
|
||||||
|
gifs_enabled: bool,
|
||||||
active_images: Vec<String>,
|
active_images: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RotationSnapshot {
|
impl RotationSnapshot {
|
||||||
fn rotation_active(&self) -> bool {
|
fn rotation_active(&self) -> bool {
|
||||||
self.custom_panel && (self.specs_enabled || (self.memes_enabled && !self.active_images.is_empty()))
|
self.custom_panel
|
||||||
|
&& (self.specs_enabled
|
||||||
|
|| ((self.memes_enabled || self.gifs_enabled) && !self.active_images.is_empty()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +110,7 @@ struct GifSetManifest {
|
|||||||
frames: Vec<GifFrameMeta>,
|
frames: Vec<GifFrameMeta>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
struct ActiveFrame {
|
struct ActiveFrame {
|
||||||
key: String,
|
key: String,
|
||||||
image_name: String,
|
image_name: String,
|
||||||
@@ -137,6 +140,7 @@ struct SetupView {
|
|||||||
switch_time: String,
|
switch_time: String,
|
||||||
specs_enabled: bool,
|
specs_enabled: bool,
|
||||||
memes_enabled: bool,
|
memes_enabled: bool,
|
||||||
|
gifs_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize)]
|
#[derive(Clone, Serialize)]
|
||||||
@@ -158,6 +162,7 @@ struct DisplayStatus {
|
|||||||
custom_panel: bool,
|
custom_panel: bool,
|
||||||
specs_enabled: bool,
|
specs_enabled: bool,
|
||||||
memes_enabled: bool,
|
memes_enabled: bool,
|
||||||
|
gifs_enabled: bool,
|
||||||
rotation_active: bool,
|
rotation_active: bool,
|
||||||
switch_time: String,
|
switch_time: String,
|
||||||
active_images: Vec<String>,
|
active_images: Vec<String>,
|
||||||
@@ -200,6 +205,7 @@ struct ActivateRequest {
|
|||||||
switch_time: Option<u32>,
|
switch_time: Option<u32>,
|
||||||
specs_enabled: Option<bool>,
|
specs_enabled: Option<bool>,
|
||||||
memes_enabled: Option<bool>,
|
memes_enabled: Option<bool>,
|
||||||
|
gifs_enabled: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -272,6 +278,7 @@ 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,
|
||||||
rotation_active: false,
|
rotation_active: false,
|
||||||
switch_time: "10".into(),
|
switch_time: "10".into(),
|
||||||
active_images: Vec::new(),
|
active_images: Vec::new(),
|
||||||
@@ -309,6 +316,7 @@ async fn ensure_layout(state: &AppState) -> Result<()> {
|
|||||||
"switchTime": "10",
|
"switchTime": "10",
|
||||||
"nativeSpecs": false,
|
"nativeSpecs": false,
|
||||||
"nativeMemes": true,
|
"nativeMemes": true,
|
||||||
|
"nativeGifs": true,
|
||||||
"operationMode": 0,
|
"operationMode": 0,
|
||||||
"theme": 1,
|
"theme": 1,
|
||||||
"diskUpdate": 300,
|
"diskUpdate": 300,
|
||||||
@@ -435,14 +443,18 @@ async fn api_activate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let memes_enabled = payload
|
let has_static_selection = valid.iter().any(|name| !is_gif_manifest_name(name));
|
||||||
.memes_enabled
|
let has_gif_selection = valid.iter().any(|name| is_gif_manifest_name(name));
|
||||||
.unwrap_or(!valid.is_empty());
|
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 || valid.is_empty()) {
|
if !specs_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 or select at least one meme image",
|
"Enable specs, memes with a still image, or GIFs with an animated image",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,7 +463,14 @@ async fn api_activate(
|
|||||||
Err(err) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
|
Err(err) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
set_custom_panels(&mut monitor, switch_time, specs_enabled, memes_enabled, &valid);
|
set_custom_panels(
|
||||||
|
&mut monitor,
|
||||||
|
switch_time,
|
||||||
|
specs_enabled,
|
||||||
|
memes_enabled,
|
||||||
|
gifs_enabled,
|
||||||
|
&valid,
|
||||||
|
);
|
||||||
|
|
||||||
if let Err(err) = save_monitor_json(&state, &monitor).await {
|
if let Err(err) = save_monitor_json(&state, &monitor).await {
|
||||||
return error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string());
|
return error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string());
|
||||||
@@ -462,6 +481,7 @@ async fn api_activate(
|
|||||||
"switchTime": switch_time,
|
"switchTime": switch_time,
|
||||||
"specsEnabled": specs_enabled,
|
"specsEnabled": specs_enabled,
|
||||||
"memesEnabled": memes_enabled,
|
"memesEnabled": memes_enabled,
|
||||||
|
"gifsEnabled": gifs_enabled,
|
||||||
"activeImages": valid,
|
"activeImages": valid,
|
||||||
}))
|
}))
|
||||||
.into_response()
|
.into_response()
|
||||||
@@ -475,7 +495,7 @@ async fn api_disable(State(state): State<Arc<AppState>>) -> Response {
|
|||||||
let switch_time = current_switch_time(&monitor);
|
let switch_time = current_switch_time(&monitor);
|
||||||
let active_images = current_active_images(&monitor);
|
let active_images = current_active_images(&monitor);
|
||||||
|
|
||||||
set_custom_panels(&mut monitor, switch_time, false, false, &active_images);
|
set_custom_panels(&mut monitor, switch_time, false, false, false, &active_images);
|
||||||
|
|
||||||
if let Err(err) = save_monitor_json(&state, &monitor).await {
|
if let Err(err) = save_monitor_json(&state, &monitor).await {
|
||||||
return error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string());
|
return error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string());
|
||||||
@@ -510,10 +530,18 @@ async fn api_delete(
|
|||||||
let switch_time = current_switch_time(&monitor);
|
let switch_time = current_switch_time(&monitor);
|
||||||
let specs_enabled = current_specs_enabled(&monitor);
|
let specs_enabled = current_specs_enabled(&monitor);
|
||||||
let memes_enabled = current_memes_enabled(&monitor);
|
let memes_enabled = current_memes_enabled(&monitor);
|
||||||
|
let gifs_enabled = current_gifs_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 {
|
||||||
set_custom_panels(&mut monitor, switch_time, specs_enabled, memes_enabled, &active_images);
|
set_custom_panels(
|
||||||
|
&mut monitor,
|
||||||
|
switch_time,
|
||||||
|
specs_enabled,
|
||||||
|
memes_enabled,
|
||||||
|
gifs_enabled,
|
||||||
|
&active_images,
|
||||||
|
);
|
||||||
if let Err(err) = save_monitor_json(&state, &monitor).await {
|
if let Err(err) = save_monitor_json(&state, &monitor).await {
|
||||||
return error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string());
|
return error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string());
|
||||||
}
|
}
|
||||||
@@ -581,6 +609,7 @@ async fn build_state_response(state: &AppState) -> Result<StateResponse> {
|
|||||||
.to_string();
|
.to_string();
|
||||||
let specs_enabled = current_specs_enabled(&monitor);
|
let specs_enabled = current_specs_enabled(&monitor);
|
||||||
let memes_enabled = current_memes_enabled(&monitor);
|
let memes_enabled = current_memes_enabled(&monitor);
|
||||||
|
let gifs_enabled = current_gifs_enabled(&monitor);
|
||||||
|
|
||||||
Ok(StateResponse {
|
Ok(StateResponse {
|
||||||
monitor_path: state.monitor_path.display().to_string(),
|
monitor_path: state.monitor_path.display().to_string(),
|
||||||
@@ -590,6 +619,7 @@ async fn build_state_response(state: &AppState) -> Result<StateResponse> {
|
|||||||
switch_time,
|
switch_time,
|
||||||
specs_enabled,
|
specs_enabled,
|
||||||
memes_enabled,
|
memes_enabled,
|
||||||
|
gifs_enabled,
|
||||||
},
|
},
|
||||||
active_images: current_active_images(&monitor),
|
active_images: current_active_images(&monitor),
|
||||||
images: list_images(state).await?,
|
images: list_images(state).await?,
|
||||||
@@ -816,12 +846,22 @@ fn current_memes_enabled(monitor: &Value) -> bool {
|
|||||||
.unwrap_or_else(|| !current_active_images(monitor).is_empty())
|
.unwrap_or_else(|| !current_active_images(monitor).is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn current_gifs_enabled(monitor: &Value) -> bool {
|
||||||
|
monitor
|
||||||
|
.get("setup")
|
||||||
|
.and_then(Value::as_object)
|
||||||
|
.and_then(|setup| setup.get("nativeGifs"))
|
||||||
|
.and_then(Value::as_bool)
|
||||||
|
.unwrap_or(true)
|
||||||
|
}
|
||||||
|
|
||||||
fn rotation_snapshot(monitor: &Value) -> RotationSnapshot {
|
fn rotation_snapshot(monitor: &Value) -> RotationSnapshot {
|
||||||
RotationSnapshot {
|
RotationSnapshot {
|
||||||
custom_panel: current_custom_panel_enabled(monitor),
|
custom_panel: current_custom_panel_enabled(monitor),
|
||||||
switch_time: current_switch_time(monitor),
|
switch_time: current_switch_time(monitor),
|
||||||
specs_enabled: current_specs_enabled(monitor),
|
specs_enabled: current_specs_enabled(monitor),
|
||||||
memes_enabled: current_memes_enabled(monitor),
|
memes_enabled: current_memes_enabled(monitor),
|
||||||
|
gifs_enabled: current_gifs_enabled(monitor),
|
||||||
active_images: current_active_images(monitor),
|
active_images: current_active_images(monitor),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -831,9 +871,10 @@ fn set_custom_panels(
|
|||||||
switch_time: u32,
|
switch_time: u32,
|
||||||
specs_enabled: bool,
|
specs_enabled: bool,
|
||||||
memes_enabled: bool,
|
memes_enabled: bool,
|
||||||
|
gifs_enabled: bool,
|
||||||
images: &[String],
|
images: &[String],
|
||||||
) {
|
) {
|
||||||
let enabled = specs_enabled || (memes_enabled && !images.is_empty());
|
let enabled = specs_enabled || ((memes_enabled || gifs_enabled) && !images.is_empty());
|
||||||
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")
|
||||||
@@ -845,6 +886,7 @@ fn set_custom_panels(
|
|||||||
setup.insert("switchTime".into(), Value::String(switch_time.to_string()));
|
setup.insert("switchTime".into(), Value::String(switch_time.to_string()));
|
||||||
setup.insert("nativeSpecs".into(), Value::Bool(specs_enabled));
|
setup.insert("nativeSpecs".into(), Value::Bool(specs_enabled));
|
||||||
setup.insert("nativeMemes".into(), Value::Bool(memes_enabled));
|
setup.insert("nativeMemes".into(), Value::Bool(memes_enabled));
|
||||||
|
setup.insert("nativeGifs".into(), Value::Bool(gifs_enabled));
|
||||||
}
|
}
|
||||||
|
|
||||||
monitor["mianban"] = Value::Array(
|
monitor["mianban"] = Value::Array(
|
||||||
@@ -1224,15 +1266,16 @@ fn load_gif_manifest(image_dir: &PathBuf, name: &str) -> Result<GifSetManifest>
|
|||||||
|
|
||||||
fn build_rotation_slots(snapshot: &RotationSnapshot, image_dir: &PathBuf) -> Vec<RotationSlot> {
|
fn build_rotation_slots(snapshot: &RotationSnapshot, image_dir: &PathBuf) -> Vec<RotationSlot> {
|
||||||
let mut slots = Vec::new();
|
let mut slots = Vec::new();
|
||||||
if snapshot.memes_enabled {
|
if snapshot.memes_enabled || snapshot.gifs_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;
|
||||||
|
}
|
||||||
match load_gif_manifest(image_dir, name) {
|
match load_gif_manifest(image_dir, name) {
|
||||||
Ok(manifest) if !manifest.frames.is_empty() => {
|
Ok(manifest) if !manifest.frames.is_empty() => {
|
||||||
let mut total_ms = 0u64;
|
|
||||||
let mut frames = Vec::new();
|
let mut frames = Vec::new();
|
||||||
for (idx, frame) in manifest.frames.iter().enumerate() {
|
for (idx, frame) in manifest.frames.iter().enumerate() {
|
||||||
total_ms += frame.delay_ms.max(100) as u64;
|
|
||||||
frames.push((
|
frames.push((
|
||||||
ActiveFrame {
|
ActiveFrame {
|
||||||
key: format!("{name}#{idx}"),
|
key: format!("{name}#{idx}"),
|
||||||
@@ -1243,14 +1286,14 @@ fn build_rotation_slots(snapshot: &RotationSnapshot, image_dir: &PathBuf) -> Vec
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
slots.push(RotationSlot {
|
slots.push(RotationSlot {
|
||||||
total_duration: Duration::from_millis(total_ms.max(100)),
|
total_duration: Duration::from_secs(snapshot.switch_time as u64),
|
||||||
frames,
|
frames,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(err) => warn!("Skipping GIF set {name}: {err}"),
|
Err(err) => warn!("Skipping GIF set {name}: {err}"),
|
||||||
}
|
}
|
||||||
} else {
|
} 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![(
|
||||||
@@ -1312,22 +1355,8 @@ fn current_slot_frame(
|
|||||||
for (slot_idx, slot) in slots.iter().enumerate() {
|
for (slot_idx, slot) in slots.iter().enumerate() {
|
||||||
let slot_ms = slot.total_duration.as_millis() as u64;
|
let slot_ms = slot.total_duration.as_millis() as u64;
|
||||||
if elapsed_ms < cursor + slot_ms {
|
if elapsed_ms < cursor + slot_ms {
|
||||||
if slot.frames.len() == 1 {
|
|
||||||
return Some((slot_idx, slot.frames[0].0.clone()));
|
|
||||||
}
|
|
||||||
let local_ms = elapsed_ms - cursor;
|
let local_ms = elapsed_ms - cursor;
|
||||||
let mut frame_cursor = 0u64;
|
return slot_frame_at(slot, slot_idx, local_ms);
|
||||||
for (frame, delay) in &slot.frames {
|
|
||||||
let delay_ms = delay.as_millis() as u64;
|
|
||||||
if local_ms < frame_cursor + delay_ms {
|
|
||||||
return Some((slot_idx, frame.clone()));
|
|
||||||
}
|
|
||||||
frame_cursor += delay_ms;
|
|
||||||
}
|
|
||||||
return slot
|
|
||||||
.frames
|
|
||||||
.last()
|
|
||||||
.map(|(frame, _)| (slot_idx, frame.clone()));
|
|
||||||
}
|
}
|
||||||
cursor += slot_ms;
|
cursor += slot_ms;
|
||||||
}
|
}
|
||||||
@@ -1338,6 +1367,40 @@ fn current_slot_frame(
|
|||||||
.map(|frame| (slots.len() - 1, frame))
|
.map(|frame| (slots.len() - 1, frame))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn slot_frame_at(slot: &RotationSlot, slot_idx: usize, local_ms: u64) -> Option<(usize, ActiveFrame)> {
|
||||||
|
if slot.frames.len() == 1 {
|
||||||
|
return Some((slot_idx, slot.frames[0].0.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let frame_cycle_ms = frame_cycle_ms(slot);
|
||||||
|
let frame_elapsed_ms = if frame_cycle_ms == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
local_ms % frame_cycle_ms
|
||||||
|
};
|
||||||
|
let mut frame_cursor = 0u64;
|
||||||
|
|
||||||
|
for (frame, delay) in &slot.frames {
|
||||||
|
let delay_ms = delay.as_millis() as u64;
|
||||||
|
if frame_elapsed_ms < frame_cursor + delay_ms {
|
||||||
|
return Some((slot_idx, frame.clone()));
|
||||||
|
}
|
||||||
|
frame_cursor += delay_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
slot.frames
|
||||||
|
.last()
|
||||||
|
.map(|(frame, _)| (slot_idx, frame.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frame_cycle_ms(slot: &RotationSlot) -> u64 {
|
||||||
|
slot.frames
|
||||||
|
.iter()
|
||||||
|
.map(|(_, delay)| delay.as_millis() as u64)
|
||||||
|
.sum::<u64>()
|
||||||
|
.max(100)
|
||||||
|
}
|
||||||
|
|
||||||
fn spawn_display_worker(state: Arc<AppState>, config: DisplayConfig) {
|
fn spawn_display_worker(state: Arc<AppState>, config: DisplayConfig) {
|
||||||
thread::spawn(move || run_display_worker(state, config));
|
thread::spawn(move || run_display_worker(state, config));
|
||||||
}
|
}
|
||||||
@@ -1439,6 +1502,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.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();
|
||||||
@@ -1454,7 +1518,7 @@ fn run_display_session(state: &AppState, screen: &mut AooScreen) -> Result<()> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let specs_only = snapshot.specs_enabled && (!snapshot.memes_enabled || snapshot.active_images.is_empty());
|
let specs_only = snapshot.specs_enabled && slots.is_empty();
|
||||||
if let Some((_slot_idx, active_frame)) =
|
if let Some((_slot_idx, active_frame)) =
|
||||||
current_slot_frame(&slots, specs_only, snapshot.switch_time, cycle_started_at)
|
current_slot_frame(&slots, specs_only, snapshot.switch_time, cycle_started_at)
|
||||||
{
|
{
|
||||||
@@ -1547,13 +1611,18 @@ fn rotation_sleep(rotation_active: bool, slots: &[RotationSlot], cycle_started_a
|
|||||||
for slot in slots {
|
for slot in slots {
|
||||||
let slot_ms = slot.total_duration.as_millis() as u64;
|
let slot_ms = slot.total_duration.as_millis() as u64;
|
||||||
if elapsed_ms < cursor + slot_ms {
|
if elapsed_ms < cursor + slot_ms {
|
||||||
|
if slot.frames.len() <= 1 {
|
||||||
|
return Duration::from_millis(750);
|
||||||
|
}
|
||||||
let local_ms = elapsed_ms - cursor;
|
let local_ms = elapsed_ms - cursor;
|
||||||
|
let frame_elapsed_ms = local_ms % frame_cycle_ms(slot);
|
||||||
let mut frame_cursor = 0u64;
|
let mut frame_cursor = 0u64;
|
||||||
for (_, delay) in &slot.frames {
|
for (_, delay) in &slot.frames {
|
||||||
let delay_ms = delay.as_millis() as u64;
|
let delay_ms = delay.as_millis() as u64;
|
||||||
if local_ms < frame_cursor + delay_ms {
|
if frame_elapsed_ms < frame_cursor + delay_ms {
|
||||||
let remaining = frame_cursor + delay_ms - local_ms;
|
let frame_remaining = frame_cursor + delay_ms - frame_elapsed_ms;
|
||||||
return Duration::from_millis(remaining.clamp(100, 750));
|
let slot_remaining = slot_ms.saturating_sub(local_ms);
|
||||||
|
return Duration::from_millis(frame_remaining.min(slot_remaining).clamp(100, 750));
|
||||||
}
|
}
|
||||||
frame_cursor += delay_ms;
|
frame_cursor += delay_ms;
|
||||||
}
|
}
|
||||||
@@ -1564,6 +1633,119 @@ fn rotation_sleep(rotation_active: bool, slots: &[RotationSlot], cycle_started_a
|
|||||||
Duration::from_millis(250)
|
Duration::from_millis(250)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
fn test_frame(key: &str) -> ActiveFrame {
|
||||||
|
ActiveFrame {
|
||||||
|
key: key.into(),
|
||||||
|
image_name: format!("{key}.jpg"),
|
||||||
|
display_name: "gif".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn slot_frame_at_loops_animated_frames_for_full_slot() {
|
||||||
|
let slot = RotationSlot {
|
||||||
|
total_duration: Duration::from_secs(10),
|
||||||
|
frames: vec![
|
||||||
|
(test_frame("gif#0"), Duration::from_millis(100)),
|
||||||
|
(test_frame("gif#1"), Duration::from_millis(100)),
|
||||||
|
(test_frame("gif#2"), Duration::from_millis(100)),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(slot_frame_at(&slot, 0, 50).unwrap().1.key, "gif#0");
|
||||||
|
assert_eq!(slot_frame_at(&slot, 0, 150).unwrap().1.key, "gif#1");
|
||||||
|
assert_eq!(slot_frame_at(&slot, 0, 250).unwrap().1.key, "gif#2");
|
||||||
|
assert_eq!(slot_frame_at(&slot, 0, 350).unwrap().1.key, "gif#0");
|
||||||
|
assert_eq!(slot_frame_at(&slot, 0, 9_950).unwrap().1.key, "gif#0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_rotation_slots_uses_switch_time_for_gif_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";
|
||||||
|
fs::write(
|
||||||
|
image_dir.join(manifest_name),
|
||||||
|
r#"{
|
||||||
|
"kind": "gif_set",
|
||||||
|
"version": 1,
|
||||||
|
"label": "panel-test.gif",
|
||||||
|
"preview": ".gifframe-test-000.jpg",
|
||||||
|
"frames": [
|
||||||
|
{ "name": ".gifframe-test-000.jpg", "delay_ms": 100 },
|
||||||
|
{ "name": ".gifframe-test-001.jpg", "delay_ms": 100 }
|
||||||
|
]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let snapshot = RotationSnapshot {
|
||||||
|
custom_panel: true,
|
||||||
|
switch_time: 7,
|
||||||
|
specs_enabled: false,
|
||||||
|
memes_enabled: false,
|
||||||
|
gifs_enabled: true,
|
||||||
|
active_images: vec![manifest_name.into()],
|
||||||
|
};
|
||||||
|
|
||||||
|
let slots = build_rotation_slots(&snapshot, &image_dir);
|
||||||
|
fs::remove_dir_all(&image_dir).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(slots.len(), 1);
|
||||||
|
assert_eq!(slots[0].total_duration, Duration::from_secs(7));
|
||||||
|
assert_eq!(slots[0].frames.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_rotation_slots_skips_gif_when_gifs_are_disabled() {
|
||||||
|
let unique = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
let image_dir = std::env::temp_dir().join(format!("aster-webui-gif-disabled-test-{unique}"));
|
||||||
|
fs::create_dir_all(&image_dir).unwrap();
|
||||||
|
let manifest_name = "panel-test.gifset.json";
|
||||||
|
fs::write(
|
||||||
|
image_dir.join(manifest_name),
|
||||||
|
r#"{
|
||||||
|
"kind": "gif_set",
|
||||||
|
"version": 1,
|
||||||
|
"label": "panel-test.gif",
|
||||||
|
"preview": ".gifframe-test-000.jpg",
|
||||||
|
"frames": [
|
||||||
|
{ "name": ".gifframe-test-000.jpg", "delay_ms": 100 },
|
||||||
|
{ "name": ".gifframe-test-001.jpg", "delay_ms": 100 }
|
||||||
|
]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let snapshot = RotationSnapshot {
|
||||||
|
custom_panel: true,
|
||||||
|
switch_time: 7,
|
||||||
|
specs_enabled: false,
|
||||||
|
memes_enabled: false,
|
||||||
|
gifs_enabled: false,
|
||||||
|
active_images: vec![manifest_name.into()],
|
||||||
|
};
|
||||||
|
|
||||||
|
let slots = build_rotation_slots(&snapshot, &image_dir);
|
||||||
|
fs::remove_dir_all(&image_dir).unwrap();
|
||||||
|
|
||||||
|
assert!(slots.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn read_display_status(status: &Arc<RwLock<DisplayStatus>>) -> DisplayStatus {
|
fn read_display_status(status: &Arc<RwLock<DisplayStatus>>) -> DisplayStatus {
|
||||||
status
|
status
|
||||||
.read()
|
.read()
|
||||||
@@ -1763,6 +1945,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 on LCD</label>
|
<label class="toggle-pill"><input id="specsEnabled" type="checkbox"> System Specs on LCD</label>
|
||||||
<label class="toggle-pill"><input id="memesEnabled" type="checkbox"> Memes on LCD</label>
|
<label class="toggle-pill"><input id="memesEnabled" type="checkbox"> Memes on LCD</label>
|
||||||
|
<label class="toggle-pill"><input id="gifsEnabled" type="checkbox"> GIFs on LCD</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-grid">
|
<div class="status-grid">
|
||||||
@@ -1803,7 +1986,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, memes_enabled: false },
|
setup: { switch_time: "10", custom_panel: false, specs_enabled: false, memes_enabled: false, gifs_enabled: true },
|
||||||
display: {},
|
display: {},
|
||||||
system: {},
|
system: {},
|
||||||
dirty: false,
|
dirty: false,
|
||||||
@@ -1814,6 +1997,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
switchTime: document.getElementById("switchTime"),
|
switchTime: document.getElementById("switchTime"),
|
||||||
specsEnabled: document.getElementById("specsEnabled"),
|
specsEnabled: document.getElementById("specsEnabled"),
|
||||||
memesEnabled: document.getElementById("memesEnabled"),
|
memesEnabled: document.getElementById("memesEnabled"),
|
||||||
|
gifsEnabled: document.getElementById("gifsEnabled"),
|
||||||
customPanelValue: document.getElementById("customPanelValue"),
|
customPanelValue: document.getElementById("customPanelValue"),
|
||||||
imageCountValue: document.getElementById("imageCountValue"),
|
imageCountValue: document.getElementById("imageCountValue"),
|
||||||
displayStatusValue: document.getElementById("displayStatusValue"),
|
displayStatusValue: document.getElementById("displayStatusValue"),
|
||||||
@@ -1913,6 +2097,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
switch_time: el.switchTime.value || data.setup.switch_time || "10",
|
switch_time: el.switchTime.value || data.setup.switch_time || "10",
|
||||||
specs_enabled: el.specsEnabled.checked,
|
specs_enabled: el.specsEnabled.checked,
|
||||||
memes_enabled: el.memesEnabled.checked,
|
memes_enabled: el.memesEnabled.checked,
|
||||||
|
gifs_enabled: el.gifsEnabled.checked,
|
||||||
}
|
}
|
||||||
: data.setup;
|
: data.setup;
|
||||||
state.display = data.display || {};
|
state.display = data.display || {};
|
||||||
@@ -1921,10 +2106,12 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
el.switchTime.value = data.setup.switch_time || "10";
|
el.switchTime.value = data.setup.switch_time || "10";
|
||||||
el.specsEnabled.checked = !!data.setup.specs_enabled;
|
el.specsEnabled.checked = !!data.setup.specs_enabled;
|
||||||
el.memesEnabled.checked = !!data.setup.memes_enabled;
|
el.memesEnabled.checked = !!data.setup.memes_enabled;
|
||||||
|
el.gifsEnabled.checked = data.setup.gifs_enabled !== false;
|
||||||
}
|
}
|
||||||
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" : "",
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
el.customPanelValue.textContent = state.setup.custom_panel
|
el.customPanelValue.textContent = state.setup.custom_panel
|
||||||
? (enabledModes.join(" + ") || "Active")
|
? (enabledModes.join(" + ") || "Active")
|
||||||
@@ -1972,6 +2159,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
switch_time: Number.parseInt(el.switchTime.value || "10", 10),
|
switch_time: Number.parseInt(el.switchTime.value || "10", 10),
|
||||||
specs_enabled: el.specsEnabled.checked,
|
specs_enabled: el.specsEnabled.checked,
|
||||||
memes_enabled: el.memesEnabled.checked,
|
memes_enabled: el.memesEnabled.checked,
|
||||||
|
gifs_enabled: el.gifsEnabled.checked,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
clearDirty();
|
clearDirty();
|
||||||
@@ -2091,6 +2279,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
el.switchTime.addEventListener("change", markDirty);
|
el.switchTime.addEventListener("change", markDirty);
|
||||||
el.specsEnabled.addEventListener("change", markDirty);
|
el.specsEnabled.addEventListener("change", markDirty);
|
||||||
el.memesEnabled.addEventListener("change", markDirty);
|
el.memesEnabled.addEventListener("change", markDirty);
|
||||||
|
el.gifsEnabled.addEventListener("change", markDirty);
|
||||||
|
|
||||||
load({ force: true }).catch((err) => toast(err.message));
|
load({ force: true }).catch((err) => toast(err.message));
|
||||||
setInterval(() => load().catch(() => {}), 5000);
|
setInterval(() => load().catch(() => {}), 5000);
|
||||||
|
|||||||
Reference in New Issue
Block a user