diff --git a/src/bin/vrec-daemon.rs b/src/bin/vrec-daemon.rs index e97b1c4..de648d5 100644 --- a/src/bin/vrec-daemon.rs +++ b/src/bin/vrec-daemon.rs @@ -2,14 +2,16 @@ use vrec::capture::{Frame, FrameSource}; use vrec::encoder::VaapiEncoder; use vrec::ring::Packet; use vrec::muxer::Muxer; -use vrec::ipc::Command; +use vrec::ipc::{Command, DaemonStatus}; use ringbuf::HeapRb; use ringbuf::traits::{RingBuffer, Consumer, Observer}; use crossbeam_channel::bounded; use std::thread; use std::env; use std::os::unix::net::UnixListener; -use std::io::Read; +use std::io::{Read, Write}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::Arc; #[tokio::main] async fn main() -> Result<(), Box> { @@ -40,7 +42,12 @@ async fn main() -> Result<(), Box> { let (mux_tx, mux_rx) = bounded::>(1); let (audio_tx, audio_rx) = bounded::>(100); let audio_info = vrec::capture::audio::AudioCapture::new(audio_tx).ok(); - + + let is_recording_state = Arc::new(AtomicBool::new(false)); + let record_start_state = Arc::new(AtomicU64::new(0)); + let replay_enabled_state = Arc::new(AtomicBool::new(true)); + let audio_muted_state = Arc::new(AtomicBool::new(false)); + let _ = frame_tx.send(first_frame); let capture_tx = frame_tx.clone(); @@ -60,8 +67,15 @@ async fn main() -> Result<(), Box> { } }); + let rec_state_clone = Arc::clone(&is_recording_state); + let rec_start_clone = Arc::clone(&record_start_state); + let replay_state_clone = Arc::clone(&replay_enabled_state); + let audio_muted_clone = Arc::clone(&audio_muted_state); + thread::spawn(move || { let mut config = vrec::config::VrecConfig::load(); + replay_state_clone.store(config.replay_enabled, Ordering::SeqCst); + let mut encoder = VaapiEncoder::new_with_bitrate(width, height, config.record_bitrate_kbps) .expect("Failed to init encoder"); let codec_ctx_ptr = encoder.codec_ctx() as usize; @@ -111,6 +125,7 @@ async fn main() -> Result<(), Box> { match cmd { Command::ReloadConfig => { config = vrec::config::VrecConfig::load(); + replay_state_clone.store(config.replay_enabled, Ordering::SeqCst); println!("Daemon config reloaded!"); let new_capacity = (config.replay_duration_sec * 120).max(120) as usize; if ring.capacity().get() != new_capacity { @@ -128,6 +143,9 @@ async fn main() -> Result<(), Box> { if !normal_recording { normal_recording = true; normal_waiting_keyframe = true; + rec_state_clone.store(true, Ordering::SeqCst); + let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs(); + rec_start_clone.store(now, Ordering::SeqCst); println!("StartRecording requested, waiting for keyframe..."); } }, @@ -138,6 +156,8 @@ async fn main() -> Result<(), Box> { } normal_recording = false; normal_waiting_keyframe = false; + rec_state_clone.store(false, Ordering::SeqCst); + rec_start_clone.store(0, Ordering::SeqCst); }, Command::ToggleRecording => { if normal_recording { @@ -147,17 +167,32 @@ async fn main() -> Result<(), Box> { } normal_recording = false; normal_waiting_keyframe = false; + rec_state_clone.store(false, Ordering::SeqCst); + rec_start_clone.store(0, Ordering::SeqCst); } else { normal_recording = true; normal_waiting_keyframe = true; + rec_state_clone.store(true, Ordering::SeqCst); + let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs(); + rec_start_clone.store(now, Ordering::SeqCst); println!("ToggleRecording: StartRecording requested, waiting for keyframe..."); } }, + Command::ToggleAudio => { + let cur = audio_muted_clone.load(Ordering::SeqCst); + audio_muted_clone.store(!cur, Ordering::SeqCst); + println!("Audio mute toggled: {}", !cur); + }, _ => {} } } - while let Ok(audio_chunk) = audio_rx.try_recv() { + while let Ok(mut audio_chunk) = audio_rx.try_recv() { + if audio_muted_clone.load(Ordering::Relaxed) { + for sample in audio_chunk.iter_mut() { + *sample = 0.0; + } + } if let Some(enc) = audio_encoder.as_mut() && let Ok(audio_packets) = enc.encode_pcm(&audio_chunk) { for mut pkt in audio_packets { @@ -215,6 +250,27 @@ async fn main() -> Result<(), Box> { if stream.read_exact(&mut payload).is_ok() && let Ok(cmd) = serde_json::from_slice::(&payload) { match cmd { + Command::GetStatus => { + let rec = is_recording_state.load(Ordering::SeqCst); + let start_ts = record_start_state.load(Ordering::SeqCst); + let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs(); + let duration = if rec && start_ts > 0 { + now.saturating_sub(start_ts) + } else { + 0 + }; + let status = DaemonStatus { + is_recording: rec, + recording_duration_sec: duration, + is_replay_active: replay_enabled_state.load(Ordering::SeqCst), + audio_muted: audio_muted_state.load(Ordering::SeqCst), + }; + if let Ok(resp) = serde_json::to_vec(&status) { + let len_resp = (resp.len() as u32).to_le_bytes(); + let _ = stream.write_all(&len_resp); + let _ = stream.write_all(&resp); + } + }, Command::StopDaemon => { break; }, diff --git a/src/ipc.rs b/src/ipc.rs index c3c74e4..7cde227 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -6,9 +6,19 @@ pub enum Command { StartRecording, StopRecording, ToggleRecording, + ToggleAudio, ReloadConfig, StopDaemon, ShowOverlay, + GetStatus, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct DaemonStatus { + pub is_recording: bool, + pub recording_duration_sec: u64, + pub is_replay_active: bool, + pub audio_muted: bool, } pub fn send_command(cmd: Command) -> Result<(), Box> { @@ -24,3 +34,24 @@ pub fn send_command(cmd: Command) -> Result<(), Box Result> { + use std::env; + use std::os::unix::net::UnixStream; + use std::io::{Read, Write}; + + let socket_path = format!("{}/vrec.sock", env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string())); + let mut stream = UnixStream::connect(&socket_path)?; + let payload = serde_json::to_vec(&Command::GetStatus)?; + let len_buf = (payload.len() as u32).to_le_bytes(); + stream.write_all(&len_buf)?; + stream.write_all(&payload)?; + + let mut resp_len_buf = [0u8; 4]; + stream.read_exact(&mut resp_len_buf)?; + let resp_len = u32::from_le_bytes(resp_len_buf) as usize; + let mut resp_payload = vec![0u8; resp_len]; + stream.read_exact(&mut resp_payload)?; + let status: DaemonStatus = serde_json::from_slice(&resp_payload)?; + Ok(status) +} diff --git a/src/overlay.rs b/src/overlay.rs index 6a56daf..d0f9da9 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -1,9 +1,14 @@ use gtk::prelude::*; -use gtk::{Application, ApplicationWindow, Label, CssProvider, StyleContext}; -use gtk_layer_shell::{LayerShell, Layer}; +use gtk::{ + Application, ApplicationWindow, Box, Button, ComboBoxText, CssProvider, Entry, Label, + Orientation, Stack, StackTransitionType, StyleContext, Switch, +}; +use gtk_layer_shell::{Layer, LayerShell}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use std::time::Duration; use crate::config::VrecConfig; -use crate::settings_ui; +use crate::ipc::{self, Command}; pub fn show_notification_overlay() { if std::env::var("WAYLAND_DISPLAY").is_err() && std::env::var("DISPLAY").is_err() { @@ -20,44 +25,48 @@ pub fn show_notification_overlay() { app.connect_activate(|app| { let window = ApplicationWindow::builder() .application(app) - .default_width(300) - .default_height(80) + .default_width(320) + .default_height(70) .build(); window.init_layer_shell(); window.set_layer(Layer::Overlay); - window.set_namespace("vrec-overlay"); - window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 40); + window.set_namespace("vrec-notification"); + window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 30); window.set_anchor(gtk_layer_shell::Edge::Top, true); - + let css_provider = CssProvider::new(); let css = r#" window { - background-color: rgba(30, 30, 30, 0.9); - border-radius: 12px; + background-color: rgba(18, 18, 22, 0.94); + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.15); + box-shadow: 0px 8px 24px rgba(0, 0, 0, 0.6); + } + label { color: #ffffff; font-weight: bold; - font-size: 20px; - padding: 15px; - box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.5); + font-size: 17px; + padding: 12px 24px; } "#; - css_provider.load_from_data(css.as_bytes()).unwrap(); - StyleContext::add_provider_for_screen( - &gdk::Screen::default().unwrap(), - &css_provider, - gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, - ); + let _ = css_provider.load_from_data(css.as_bytes()); + if let Some(screen) = gdk::Screen::default() { + StyleContext::add_provider_for_screen( + &screen, + &css_provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); + } let label = Label::new(Some("✅ Replay Saved!")); window.add(&label); - window.show_all(); let window_clone = window.clone(); - glib::timeout_add_local(Duration::from_secs(2), move || { + gtk::glib::timeout_add_local(Duration::from_secs(2), move || { window_clone.close(); - glib::ControlFlow::Break + gtk::glib::ControlFlow::Break }); }); @@ -75,86 +84,310 @@ pub fn show_menu_overlay() { } let app = Application::builder() - .application_id("com.vrec.menu") + .application_id("com.vrec.hud") .build(); app.connect_activate(|app| { let config = VrecConfig::load(); - + let window = ApplicationWindow::builder() .application(app) - .default_width(700) - .default_height(200) + .default_width(820) + .default_height(220) .build(); window.init_layer_shell(); window.set_layer(Layer::Overlay); - window.set_namespace("vrec-menu"); + window.set_namespace("vrec-hud"); window.set_keyboard_interactivity(true); - window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 100); + window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 70); window.set_anchor(gtk_layer_shell::Edge::Top, true); - let main_box = gtk::Box::new(gtk::Orientation::Horizontal, 20); - main_box.set_margin_start(20); - main_box.set_margin_end(20); - main_box.set_margin_top(20); - main_box.set_margin_bottom(20); - main_box.set_halign(gtk::Align::Center); - - let replay_box = gtk::Box::new(gtk::Orientation::Vertical, 10); - let replay_label = if config.replay_enabled { "Instant Replay\n🟢 ON" } else { "Instant Replay\n⚪ OFF" }; - let replay_btn = gtk::Button::with_label(replay_label); - replay_btn.set_size_request(200, 100); - let replay_settings_btn = gtk::Button::with_label("⚙️ Replay Settings"); - - let cfg_clone1 = config.clone(); - replay_settings_btn.connect_clicked(move |_| { - settings_ui::open_replay_settings(&cfg_clone1); + // Main Stack (Allows toggling between HUD cards and Inline Settings with NO extra windows!) + let stack = Stack::new(); + stack.set_transition_type(StackTransitionType::Crossfade); + stack.set_transition_duration(200); + + // ========================================== + // PAGE 1: HUD Quick Actions (Xbox / GPU Screen Recorder layout) + // ========================================== + let hud_page = Box::new(Orientation::Vertical, 16); + hud_page.set_margin_start(24); + hud_page.set_margin_end(24); + hud_page.set_margin_top(20); + hud_page.set_margin_bottom(20); + + // Header + let header_box = Box::new(Orientation::Horizontal, 10); + let title_label = Label::new(Some("vrec")); + title_label.style_context().add_class("hud-title"); + let badge_label = Label::new(Some("GPU ZERO-COPY")); + badge_label.style_context().add_class("badge-tag"); + let spacer = Box::new(Orientation::Horizontal, 0); + let close_btn = Button::with_label("✕"); + close_btn.style_context().add_class("close-btn"); + + header_box.pack_start(&title_label, false, false, 0); + header_box.pack_start(&badge_label, false, false, 6); + header_box.pack_start(&spacer, true, true, 0); + header_box.pack_start(&close_btn, false, false, 0); + + // Cards Row (4 Primary Action Cards) + let cards_box = Box::new(Orientation::Horizontal, 16); + cards_box.set_halign(gtk::Align::Center); + + // Card 1: Instant Replay + let replay_card = Box::new(Orientation::Vertical, 8); + replay_card.style_context().add_class("card-box"); + let replay_toggle_btn = Button::with_label(if config.replay_enabled { + "Instant Replay\n🟢 ACTIVE" + } else { + "Instant Replay\n⚪ OFF" }); - - replay_box.pack_start(&replay_btn, true, true, 0); - replay_box.pack_start(&replay_settings_btn, false, false, 0); + replay_toggle_btn.set_size_request(170, 75); + replay_toggle_btn.style_context().add_class("action-btn"); - let record_box = gtk::Box::new(gtk::Orientation::Vertical, 10); - let record_label = if config.record_enabled { "Recording\n🔴 ON" } else { "Recording\n⚪ OFF" }; - let record_btn = gtk::Button::with_label(record_label); - record_btn.set_size_request(200, 100); - let record_settings_btn = gtk::Button::with_label("⚙️ Record Settings"); - - let cfg_clone2 = config.clone(); - record_settings_btn.connect_clicked(move |_| { - settings_ui::open_record_settings(&cfg_clone2); - }); - - record_box.pack_start(&record_btn, true, true, 0); - record_box.pack_start(&record_settings_btn, false, false, 0); + let replay_save_btn = Button::with_label("💾 Save Replay"); + replay_save_btn.style_context().add_class("sub-btn"); + replay_card.pack_start(&replay_toggle_btn, true, true, 0); + replay_card.pack_start(&replay_save_btn, false, false, 0); - let settings_box = gtk::Box::new(gtk::Orientation::Vertical, 10); - let gen_settings_btn = gtk::Button::with_label("General Settings"); - gen_settings_btn.set_size_request(200, 50); - - let cfg_clone3 = config.clone(); - gen_settings_btn.connect_clicked(move |_| { - settings_ui::open_general_settings(&cfg_clone3); - }); + // Card 2: Normal Recording + let record_card = Box::new(Orientation::Vertical, 8); + record_card.style_context().add_class("card-box"); + let record_toggle_btn = Button::with_label("🔴 Record\nClick to Start"); + record_toggle_btn.set_size_request(170, 75); + record_toggle_btn.style_context().add_class("action-btn"); - let close_btn = gtk::Button::with_label("❌ Close Menu"); - close_btn.set_size_request(200, 50); - - let spacer = gtk::Box::new(gtk::Orientation::Vertical, 0); - settings_box.pack_start(&gen_settings_btn, false, false, 0); - settings_box.pack_start(&spacer, true, true, 0); - settings_box.pack_start(&close_btn, false, false, 0); + let record_status_sub = Label::new(Some("H.264 • 60 FPS")); + record_status_sub.style_context().add_class("sub-label"); + record_card.pack_start(&record_toggle_btn, true, true, 0); + record_card.pack_start(&record_status_sub, false, false, 0); - main_box.pack_start(&replay_box, true, true, 0); - main_box.pack_start(&record_box, true, true, 0); - main_box.pack_start(&settings_box, true, true, 0); + // Card 3: Microphone & Audio + let audio_card = Box::new(Orientation::Vertical, 8); + audio_card.style_context().add_class("card-box"); + let mic_toggle_btn = Button::with_label("🎙️ Mic / Audio\n🟢 LIVE"); + mic_toggle_btn.set_size_request(170, 75); + mic_toggle_btn.style_context().add_class("action-btn"); - window.add(&main_box); + let mic_status_sub = Label::new(Some("AAC • 48 kHz")); + mic_status_sub.style_context().add_class("sub-label"); + audio_card.pack_start(&mic_toggle_btn, true, true, 0); + audio_card.pack_start(&mic_status_sub, false, false, 0); - let window_clone = window.clone(); + // Card 4: Settings + let settings_card = Box::new(Orientation::Vertical, 8); + settings_card.style_context().add_class("card-box"); + let settings_open_btn = Button::with_label("⚙️ Settings\nPreferences"); + settings_open_btn.set_size_request(170, 75); + settings_open_btn.style_context().add_class("action-btn"); + + let shortcut_hint = Label::new(Some("Esc to Close")); + shortcut_hint.style_context().add_class("sub-label"); + settings_card.pack_start(&settings_open_btn, true, true, 0); + settings_card.pack_start(&shortcut_hint, false, false, 0); + + cards_box.pack_start(&replay_card, true, true, 0); + cards_box.pack_start(&record_card, true, true, 0); + cards_box.pack_start(&audio_card, true, true, 0); + cards_box.pack_start(&settings_card, true, true, 0); + + hud_page.pack_start(&header_box, false, false, 0); + hud_page.pack_start(&cards_box, true, true, 0); + + // ========================================== + // PAGE 2: Inline Settings Panel (Zero separate windows!) + // ========================================== + let settings_page = Box::new(Orientation::Vertical, 14); + settings_page.set_margin_start(30); + settings_page.set_margin_end(30); + settings_page.set_margin_top(20); + settings_page.set_margin_bottom(20); + + let settings_header = Box::new(Orientation::Horizontal, 12); + let back_btn = Button::with_label("⬅️ Back"); + back_btn.style_context().add_class("sub-btn"); + let settings_title = Label::new(Some("Recorder Settings")); + settings_title.style_context().add_class("hud-title"); + settings_header.pack_start(&back_btn, false, false, 0); + settings_header.pack_start(&settings_title, false, false, 0); + + let settings_grid = gtk::Grid::new(); + settings_grid.set_column_spacing(24); + settings_grid.set_row_spacing(12); + + // Replay Duration + let dur_lbl = Label::new(Some("Replay Duration:")); + dur_lbl.set_halign(gtk::Align::Start); + let dur_combo = ComboBoxText::new(); + dur_combo.append(Some("15"), "15 Seconds"); + dur_combo.append(Some("30"), "30 Seconds"); + dur_combo.append(Some("60"), "1 Minute"); + dur_combo.append(Some("120"), "2 Minutes"); + dur_combo.append(Some("300"), "5 Minutes"); + dur_combo.append(Some("600"), "10 Minutes"); + dur_combo.set_active_id(Some(&config.replay_duration_sec.to_string())); + + // Video Bitrate + let bit_lbl = Label::new(Some("Recording Quality:")); + bit_lbl.set_halign(gtk::Align::Start); + let bit_combo = ComboBoxText::new(); + bit_combo.append(Some("10000"), "Low (10 Mbps)"); + bit_combo.append(Some("15000"), "Medium (15 Mbps)"); + bit_combo.append(Some("25000"), "High (25 Mbps)"); + bit_combo.append(Some("50000"), "Ultra (50 Mbps)"); + bit_combo.set_active_id(Some(&config.record_bitrate_kbps.to_string())); + + // Autostart + let auto_lbl = Label::new(Some("Autostart on Boot:")); + auto_lbl.set_halign(gtk::Align::Start); + let auto_switch = Switch::new(); + auto_switch.set_active(config.autostart); + + // Hotkeys + let save_hk_lbl = Label::new(Some("Save Replay Hotkey:")); + save_hk_lbl.set_halign(gtk::Align::Start); + let save_hk_entry = Entry::new(); + save_hk_entry.set_text(&config.save_hotkey); + + let menu_hk_lbl = Label::new(Some("Menu Overlay Hotkey:")); + menu_hk_lbl.set_halign(gtk::Align::Start); + let menu_hk_entry = Entry::new(); + menu_hk_entry.set_text(&config.menu_hotkey); + + settings_grid.attach(&dur_lbl, 0, 0, 1, 1); + settings_grid.attach(&dur_combo, 1, 0, 1, 1); + settings_grid.attach(&bit_lbl, 0, 1, 1, 1); + settings_grid.attach(&bit_combo, 1, 1, 1, 1); + settings_grid.attach(&auto_lbl, 0, 2, 1, 1); + settings_grid.attach(&auto_switch, 1, 2, 1, 1); + settings_grid.attach(&save_hk_lbl, 0, 3, 1, 1); + settings_grid.attach(&save_hk_entry, 1, 3, 1, 1); + settings_grid.attach(&menu_hk_lbl, 0, 4, 1, 1); + settings_grid.attach(&menu_hk_entry, 1, 4, 1, 1); + + let save_settings_btn = Button::with_label("💾 Apply & Save Settings"); + save_settings_btn.style_context().add_class("apply-btn"); + + settings_page.pack_start(&settings_header, false, false, 0); + settings_page.pack_start(&settings_grid, true, true, 4); + settings_page.pack_start(&save_settings_btn, false, false, 4); + + stack.add_named(&hud_page, "hud"); + stack.add_named(&settings_page, "settings"); + window.add(&stack); + + // CSS Styling (Sleek Frosted Glass HUD) + let css_provider = CssProvider::new(); + let css = r#" + window { + background-color: rgba(18, 20, 26, 0.95); + border-radius: 20px; + border: 1px solid rgba(255, 255, 255, 0.12); + box-shadow: 0px 12px 40px rgba(0, 0, 0, 0.75); + color: #ffffff; + } + .hud-title { + font-size: 20px; + font-weight: 900; + color: #ffffff; + letter-spacing: 1px; + } + .badge-tag { + background-color: rgba(56, 189, 248, 0.2); + color: #38bdf8; + font-size: 10px; + font-weight: bold; + border-radius: 6px; + padding: 2px 8px; + } + .card-box { + background-color: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 14px; + padding: 10px; + } + .card-box:hover { + background-color: rgba(255, 255, 255, 0.07); + border-color: rgba(255, 255, 255, 0.18); + } + .action-btn { + background-color: transparent; + border: none; + color: #f1f5f9; + font-size: 15px; + font-weight: bold; + } + .action-btn:hover { + color: #38bdf8; + } + .recording-active { + color: #ef4444 !important; + animation: pulse 1s infinite alternate; + } + .sub-btn { + background-color: rgba(255, 255, 255, 0.08); + color: #cbd5e1; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + font-size: 12px; + font-weight: 600; + padding: 6px 12px; + } + .sub-btn:hover { + background-color: rgba(255, 255, 255, 0.16); + color: #ffffff; + } + .apply-btn { + background-color: #2563eb; + color: #ffffff; + border: none; + border-radius: 10px; + font-size: 14px; + font-weight: bold; + padding: 10px 20px; + } + .apply-btn:hover { + background-color: #1d4ed8; + } + .close-btn { + background-color: transparent; + color: #94a3b8; + border: none; + font-size: 18px; + font-weight: bold; + padding: 4px 8px; + } + .close-btn:hover { + color: #f43f5e; + } + .sub-label { + color: #64748b; + font-size: 11px; + font-weight: 500; + } + entry, combobox button { + background-color: rgba(30, 32, 40, 0.9); + color: #ffffff; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 8px; + padding: 6px 10px; + } + "#; + let _ = css_provider.load_from_data(css.as_bytes()); + if let Some(screen) = gdk::Screen::default() { + StyleContext::add_provider_for_screen( + &screen, + &css_provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); + } + + // Window Close Triggers + let window_close = window.clone(); close_btn.connect_clicked(move |_| { - window_clone.close(); + window_close.close(); }); let window_esc = window.clone(); @@ -167,61 +400,106 @@ pub fn show_menu_overlay() { } }); - let replay_btn_clone = replay_btn.clone(); - replay_btn.connect_clicked(move |_| { + // Stack Navigation + let stack_to_settings = stack.clone(); + settings_open_btn.connect_clicked(move |_| { + stack_to_settings.set_visible_child_name("settings"); + }); + + let stack_to_hud = stack.clone(); + back_btn.connect_clicked(move |_| { + stack_to_hud.set_visible_child_name("hud"); + }); + + // Save Settings Action + let stack_after_save = stack.clone(); + save_settings_btn.connect_clicked(move |_| { + let mut cfg = VrecConfig::load(); + if let Some(dur_str) = dur_combo.active_id() + && let Ok(dur) = dur_str.parse() { + cfg.replay_duration_sec = dur; + } + if let Some(bit_str) = bit_combo.active_id() + && let Ok(bit) = bit_str.parse() { + cfg.record_bitrate_kbps = bit; + cfg.replay_bitrate_kbps = bit; + } + cfg.autostart = auto_switch.is_active(); + cfg.save_hotkey = save_hk_entry.text().to_string(); + cfg.menu_hotkey = menu_hk_entry.text().to_string(); + let _ = cfg.save(); + VrecConfig::notify_daemon_reload(); + stack_after_save.set_visible_child_name("hud"); + }); + + // Action Handlers + let replay_lbl_btn = replay_toggle_btn.clone(); + replay_toggle_btn.connect_clicked(move |_| { let mut cfg = VrecConfig::load(); cfg.replay_enabled = !cfg.replay_enabled; let _ = cfg.save(); VrecConfig::notify_daemon_reload(); if cfg.replay_enabled { - replay_btn_clone.set_label("Instant Replay\n🟢 ON"); + replay_lbl_btn.set_label("Instant Replay\n🟢 ACTIVE"); } else { - replay_btn_clone.set_label("Instant Replay\n⚪ OFF"); + replay_lbl_btn.set_label("Instant Replay\n⚪ OFF"); } }); - let record_btn_clone = record_btn.clone(); - record_btn.connect_clicked(move |_| { - let mut cfg = VrecConfig::load(); - cfg.record_enabled = !cfg.record_enabled; - let _ = cfg.save(); - VrecConfig::notify_daemon_reload(); - if cfg.record_enabled { - record_btn_clone.set_label("Recording\n🔴 ON"); - let _ = crate::ipc::send_command(crate::ipc::Command::StartRecording); - } else { - record_btn_clone.set_label("Recording\n⚪ OFF"); - let _ = crate::ipc::send_command(crate::ipc::Command::StopRecording); - } + replay_save_btn.connect_clicked(|_| { + let _ = ipc::send_command(Command::SaveReplay); + show_notification_overlay(); }); + record_toggle_btn.connect_clicked(|_| { + let _ = ipc::send_command(Command::ToggleRecording); + }); - let css_provider = CssProvider::new(); - let css = r#" - window { - background-color: rgba(25, 25, 25, 0.95); - border-radius: 16px; - color: #ffffff; + mic_toggle_btn.connect_clicked(|_| { + let _ = ipc::send_command(Command::ToggleAudio); + }); + + // Periodic Live Status Query from Daemon (Syncs live recording state & timer!) + let rec_btn_sync = record_toggle_btn.clone(); + let mic_btn_sync = mic_toggle_btn.clone(); + let replay_btn_sync = replay_toggle_btn.clone(); + let is_running = Arc::new(AtomicBool::new(true)); + let is_running_clone = Arc::clone(&is_running); + + gtk::glib::timeout_add_local(Duration::from_millis(500), move || { + if !is_running_clone.load(Ordering::Relaxed) { + return gtk::glib::ControlFlow::Break; } - button { - background-color: rgba(50, 50, 50, 0.8); - color: white; - border-radius: 12px; - padding: 10px; - font-weight: bold; - font-size: 16px; - border: 1px solid rgba(100, 100, 100, 0.5); + + if let Ok(status) = ipc::query_status() { + if status.is_recording { + let mins = status.recording_duration_sec / 60; + let secs = status.recording_duration_sec % 60; + rec_btn_sync.set_label(&format!("⏹️ Recording\n⏱️ {:02}:{:02}", mins, secs)); + rec_btn_sync.style_context().add_class("recording-active"); + } else { + rec_btn_sync.set_label("🔴 Record\nClick to Start"); + rec_btn_sync.style_context().remove_class("recording-active"); + } + + if status.audio_muted { + mic_btn_sync.set_label("🔇 Mic / Audio\n⚪ MUTED"); + } else { + mic_btn_sync.set_label("🎙️ Mic / Audio\n🟢 LIVE"); + } + + if status.is_replay_active { + replay_btn_sync.set_label("Instant Replay\n🟢 ACTIVE"); + } else { + replay_btn_sync.set_label("Instant Replay\n⚪ OFF"); + } } - button:hover { - background-color: rgba(80, 80, 80, 0.9); - } - "#; - css_provider.load_from_data(css.as_bytes()).unwrap(); - StyleContext::add_provider_for_screen( - &gdk::Screen::default().unwrap(), - &css_provider, - gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, - ); + gtk::glib::ControlFlow::Continue + }); + + window.connect_destroy(move |_| { + is_running.store(false, Ordering::Relaxed); + }); window.show_all(); });