fix(ui): truly borderless toast, 1s power poller with resume and alerts

This commit is contained in:
katR 2026-10-11 18:45:32 +05:30
parent e7a83ba472
commit db7cbf93bb
3 changed files with 47 additions and 25 deletions

View file

@ -456,11 +456,35 @@ async fn run_daemon_async() -> Result<(), Box<dyn std::error::Error + Send + Syn
true true
} }
let cmd_tx_power = cmd_tx.clone();
std::thread::spawn(move || {
let mut last_ac = crate::config::ScytheConfig::is_ac_power_connected();
loop {
std::thread::sleep(std::time::Duration::from_secs(1));
let now_ac = crate::config::ScytheConfig::is_ac_power_connected();
if now_ac != last_ac {
last_ac = now_ac;
let _ = cmd_tx_power.send(Command::PowerStateChanged(now_ac));
}
}
});
fn spawn_power_toast(title: &str, subtitle: &str, icon: &str) {
let exe = std::env::current_exe()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| "scythe".to_string());
let _ = std::process::Command::new(exe)
.args(["--toast", title, subtitle, icon])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
}
let recorder_handle = thread::spawn(move || { let recorder_handle = thread::spawn(move || {
let mut config = crate::config::ScytheConfig::load(); let mut config = crate::config::ScytheConfig::load();
let mut replay_effective = effective_replay_active(&config); let mut replay_effective = effective_replay_active(&config);
replay_state_clone.store(replay_effective, Ordering::SeqCst); replay_state_clone.store(replay_effective, Ordering::SeqCst);
let mut last_power_state = crate::config::ScytheConfig::is_ac_power_connected();
let active_bitrate = if config.replay_enabled { let active_bitrate = if config.replay_enabled {
config.replay_bitrate_kbps config.replay_bitrate_kbps
@ -746,7 +770,6 @@ async fn run_daemon_async() -> Result<(), Box<dyn std::error::Error + Send + Syn
config = new_config; config = new_config;
replay_effective = effective_replay_active(&config); replay_effective = effective_replay_active(&config);
replay_state_clone.store(replay_effective, Ordering::SeqCst); replay_state_clone.store(replay_effective, Ordering::SeqCst);
last_power_state = crate::config::ScytheConfig::is_ac_power_connected();
if !replay_effective && config.replay_enabled { if !replay_effective && config.replay_enabled {
while ring.try_pop().is_some() {} while ring.try_pop().is_some() {}
eprintln!("Replay paused: power-only armed and running on battery."); eprintln!("Replay paused: power-only armed and running on battery.");
@ -784,6 +807,25 @@ async fn run_daemon_async() -> Result<(), Box<dyn std::error::Error + Send + Syn
}; };
ticker = crossbeam_channel::tick(std::time::Duration::from_nanos(1_000_000_000 / config.fps.max(1) as u64)); ticker = crossbeam_channel::tick(std::time::Duration::from_nanos(1_000_000_000 / config.fps.max(1) as u64));
}, },
Command::PowerStateChanged(on_ac) => {
let was_effective = replay_effective;
replay_effective = effective_replay_active(&config);
replay_state_clone.store(replay_effective, Ordering::SeqCst);
if !replay_effective && (was_effective || !on_ac) {
while ring.try_pop().is_some() {}
eprintln!("Power lost: replay buffer paused and drained.");
if config.autostart_power_only && config.show_replay_notifications {
spawn_power_toast("POWER", "power lost, replay paused", "error");
}
} else if replay_effective && !was_effective {
encoder.force_keyframe();
has_new_frame = true;
println!("Power restored: replay buffer resumed.");
if config.show_replay_notifications {
spawn_power_toast("POWER", "power restored, replay resumed", "replay");
}
}
},
Command::SaveReplay => { Command::SaveReplay => {
if replay_effective { if replay_effective {
let now = std::time::Instant::now(); let now = std::time::Instant::now();
@ -1028,18 +1070,6 @@ async fn run_daemon_async() -> Result<(), Box<dyn std::error::Error + Send + Syn
} }
let is_streaming = stream_sender.is_some(); let is_streaming = stream_sender.is_some();
let power_now = crate::config::ScytheConfig::is_ac_power_connected();
if power_now != last_power_state {
last_power_state = power_now;
replay_effective = effective_replay_active(&config);
replay_state_clone.store(replay_effective, Ordering::SeqCst);
if !replay_effective {
while ring.try_pop().is_some() {}
eprintln!("AC power lost: replay buffer paused and drained.");
} else {
println!("AC power restored: replay buffer resumed.");
}
}
if !normal_recording && !replay_effective && !is_streaming { if !normal_recording && !replay_effective && !is_streaming {
has_new_frame = false; has_new_frame = false;
continue; continue;

View file

@ -38,6 +38,7 @@ pub enum Command {
CycleAudioMode, CycleAudioMode,
ToggleCursor, ToggleCursor,
ReloadConfig, ReloadConfig,
PowerStateChanged(bool),
StopDaemon, StopDaemon,
ShowOverlay, ShowOverlay,
GetStatus, GetStatus,

View file

@ -3626,10 +3626,7 @@ fn render_scythe_notification_card(
_ => accent, _ => accent,
}; };
// 1. Deep window drop shadow // 1. Obsidian panel fill (#111111, zero roundness, no outline, no shadow bloom)
draw_window_shadow(painter, rect, 0.75 * anim_alpha);
// 2. Obsidian panel fill (#111111, zero roundness, no outline)
painter.rect_filled( painter.rect_filled(
rect, rect,
CornerRadius::ZERO, CornerRadius::ZERO,
@ -3656,7 +3653,7 @@ fn render_scythe_notification_card(
Color32::from_rgba_unmultiplied(active_color.r(), active_color.g(), active_color.b(), (255.0 * anim_alpha) as u8), Color32::from_rgba_unmultiplied(active_color.r(), active_color.g(), active_color.b(), (255.0 * anim_alpha) as u8),
); );
// 5. Square icon cell (28x28 recessed #0e0e0e, hairline #222222) // 5. Square icon cell (28x28 recessed #0e0e0e, no outline)
let badge_size = 28.0_f32; let badge_size = 28.0_f32;
let badge_rect = egui::Rect::from_min_size( let badge_rect = egui::Rect::from_min_size(
egui::pos2(rect.left() + 26.0, rect.top() + 14.0), egui::pos2(rect.left() + 26.0, rect.top() + 14.0),
@ -3667,12 +3664,6 @@ fn render_scythe_notification_card(
CornerRadius::ZERO, CornerRadius::ZERO,
Color32::from_rgba_unmultiplied(14, 14, 14, (255.0 * anim_alpha) as u8), Color32::from_rgba_unmultiplied(14, 14, 14, (255.0 * anim_alpha) as u8),
); );
painter.rect_stroke(
badge_rect,
CornerRadius::ZERO,
Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(34, 34, 34, (220.0 * anim_alpha) as u8)),
egui::StrokeKind::Inside,
);
// 6. Icon render inside badge // 6. Icon render inside badge
let icon_center = badge_rect.center(); let icon_center = badge_rect.center();