diff --git a/Cargo.lock b/Cargo.lock index 948be91..e3ab737 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,7 +35,7 @@ checksum = "fb8421aaa9644a5faf26735f258b669b15f063313ef8f8e2bdb28912a1a6f111" dependencies = [ "enumflags2", "futures-util", - "getrandom", + "getrandom 0.4.3", "serde", "serde_repr", "tokio", @@ -253,6 +253,27 @@ version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + [[package]] name = "dispatch2" version = "0.3.1" @@ -522,6 +543,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -886,6 +918,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libredox" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d8f1ea3f21fd3405dcaf6c9b5c1630af9afc422d9073ea39c5f6d6c772e08ed" +dependencies = [ + "libc", +] + [[package]] name = "libspa" version = "0.10.1" @@ -1097,6 +1138,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-stream" version = "0.2.0" @@ -1276,6 +1323,17 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.20", +] + [[package]] name = "regex" version = "1.13.1" @@ -1562,7 +1620,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -1827,6 +1885,7 @@ version = "0.1.0" dependencies = [ "ashpd", "crossbeam-channel", + "dirs", "ffmpeg-next", "ffmpeg-sys-next", "gdk", diff --git a/Cargo.toml b/Cargo.toml index 38f42c1..19ee7b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,4 @@ serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" tokio = { version = "1.53.1", features = ["rt", "macros", "rt-multi-thread"] } xcb = { version = "1.7.1", features = ["shm", "xfixes"] } +dirs = "6.0.0" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..dbe1bd7 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,69 @@ +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct VrecConfig { + pub replay_enabled: bool, + pub replay_duration_sec: u32, + pub replay_bitrate_kbps: u32, + + pub record_enabled: bool, + pub record_bitrate_kbps: u32, + + pub autostart: bool, + pub language: String, + pub ui_color_theme: String, + pub save_hotkey: String, + pub menu_hotkey: String, +} + +impl Default for VrecConfig { + fn default() -> Self { + Self { + replay_enabled: true, + replay_duration_sec: 60, + replay_bitrate_kbps: 10000, + + record_enabled: false, + record_bitrate_kbps: 15000, + + autostart: false, + language: "en".to_string(), + ui_color_theme: "dark".to_string(), + save_hotkey: "Ctrl+Shift+R".to_string(), + menu_hotkey: "Alt+Z".to_string(), + } + } +} + +impl VrecConfig { + pub fn config_path() -> PathBuf { + let mut path = dirs::config_dir().unwrap_or_else(|| PathBuf::from(".")); + path.push("vrec"); + path.push("config.json"); + path + } + + pub fn load() -> Self { + let path = Self::config_path(); + if path.exists() { + if let Ok(data) = fs::read_to_string(path) { + if let Ok(config) = serde_json::from_str(&data) { + return config; + } + } + } + Self::default() + } + + pub fn save(&self) -> Result<(), std::io::Error> { + let path = Self::config_path(); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let data = serde_json::to_string_pretty(self)?; + fs::write(path, data)?; + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index e9af7f2..c9e19f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,3 +5,5 @@ pub mod muxer; pub mod hotkey; pub mod ipc; pub mod overlay; +pub mod config; +pub mod settings_ui; diff --git a/src/overlay.rs b/src/overlay.rs index 5077cb1..ebb0cb1 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -2,6 +2,8 @@ use gtk::prelude::*; use gtk::{Application, ApplicationWindow, Label, CssProvider, StyleContext}; use gtk_layer_shell::{LayerShell, Layer}; use std::time::Duration; +use crate::config::VrecConfig; +use crate::settings_ui; pub fn show_notification_overlay() { let app = Application::builder() @@ -21,7 +23,6 @@ pub fn show_notification_overlay() { window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 40); window.set_anchor(gtk_layer_shell::Edge::Top, true); - // Make it transparent and borderless let css_provider = CssProvider::new(); let css = r#" window { @@ -46,7 +47,6 @@ pub fn show_notification_overlay() { window.show_all(); - // Close after 2 seconds let window_clone = window.clone(); glib::timeout_add_local(Duration::from_secs(2), move || { window_clone.close(); @@ -63,6 +63,8 @@ pub fn show_menu_overlay() { .build(); app.connect_activate(|app| { + let config = VrecConfig::load(); + let window = ApplicationWindow::builder() .application(app) .default_width(700) @@ -76,7 +78,6 @@ pub fn show_menu_overlay() { window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 100); window.set_anchor(gtk_layer_shell::Edge::Top, true); - // Main layout let main_box = gtk::Box::new(gtk::Orientation::Horizontal, 20); main_box.set_margin_start(20); main_box.set_margin_end(20); @@ -84,26 +85,43 @@ pub fn show_menu_overlay() { main_box.set_margin_bottom(20); main_box.set_halign(gtk::Align::Center); - // 1. Replay Box let replay_box = gtk::Box::new(gtk::Orientation::Vertical, 10); - let replay_btn = gtk::Button::with_label("Instant Replay\n🟢 ON"); + 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); + }); + replay_box.pack_start(&replay_btn, true, true, 0); replay_box.pack_start(&replay_settings_btn, false, false, 0); - // 2. Record Box let record_box = gtk::Box::new(gtk::Orientation::Vertical, 10); - let record_btn = gtk::Button::with_label("Recording\n⚪ OFF"); + 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); - // 3. Settings Box 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); + }); + let close_btn = gtk::Button::with_label("❌ Close Menu"); close_btn.set_size_request(200, 50); @@ -123,7 +141,34 @@ pub fn show_menu_overlay() { window_clone.close(); }); - // Add CSS styling + // Toggle buttons logic (just UI for now, backend IPC will be needed to actually toggle daemon state) + let btn_cfg_1 = config.clone(); + let replay_btn_clone = replay_btn.clone(); + replay_btn.connect_clicked(move |_| { + let mut cfg = VrecConfig::load(); + cfg.replay_enabled = !cfg.replay_enabled; + cfg.save().unwrap(); + if cfg.replay_enabled { + replay_btn_clone.set_label("Instant Replay\n🟢 ON"); + } else { + replay_btn_clone.set_label("Instant Replay\n⚪ OFF"); + } + }); + + let btn_cfg_2 = config.clone(); + let record_btn_clone = record_btn.clone(); + record_btn.connect_clicked(move |_| { + let mut cfg = VrecConfig::load(); + cfg.record_enabled = !cfg.record_enabled; + cfg.save().unwrap(); + if cfg.record_enabled { + record_btn_clone.set_label("Recording\n🔴 ON"); + } else { + record_btn_clone.set_label("Recording\n⚪ OFF"); + } + }); + + let css_provider = CssProvider::new(); let css = r#" window { diff --git a/src/settings_ui.rs b/src/settings_ui.rs new file mode 100644 index 0000000..3664874 --- /dev/null +++ b/src/settings_ui.rs @@ -0,0 +1,147 @@ +use gtk::prelude::*; +use gtk::{Window, WindowType, Box, Orientation, Label, Entry, Button, Switch, ComboBoxText}; +use crate::config::VrecConfig; + +pub fn open_replay_settings(config_in: &VrecConfig) { + let window = Window::new(WindowType::Toplevel); + window.set_title("Replay Settings"); + window.set_default_size(300, 200); + + let vbox = Box::new(Orientation::Vertical, 10); + vbox.set_margin(15); + + let duration_label = Label::new(Some("Replay Duration (seconds):")); + let duration_entry = Entry::new(); + duration_entry.set_text(&config_in.replay_duration_sec.to_string()); + + let bitrate_label = Label::new(Some("Bitrate (kbps):")); + let bitrate_entry = Entry::new(); + bitrate_entry.set_text(&config_in.replay_bitrate_kbps.to_string()); + + let save_btn = Button::with_label("Save"); + + vbox.pack_start(&duration_label, false, false, 0); + vbox.pack_start(&duration_entry, false, false, 0); + vbox.pack_start(&bitrate_label, false, false, 0); + vbox.pack_start(&bitrate_entry, false, false, 0); + vbox.pack_start(&save_btn, false, false, 0); + + window.add(&vbox); + + let window_clone = window.clone(); + save_btn.connect_clicked(move |_| { + let mut cfg = VrecConfig::load(); + if let Ok(val) = duration_entry.text().parse() { + cfg.replay_duration_sec = val; + } + if let Ok(val) = bitrate_entry.text().parse() { + cfg.replay_bitrate_kbps = val; + } + let _ = cfg.save(); + window_clone.close(); + }); + + window.show_all(); +} + +pub fn open_record_settings(config_in: &VrecConfig) { + let window = Window::new(WindowType::Toplevel); + window.set_title("Record Settings"); + window.set_default_size(300, 150); + + let vbox = Box::new(Orientation::Vertical, 10); + vbox.set_margin(15); + + let bitrate_label = Label::new(Some("Recording Bitrate (kbps):")); + let bitrate_entry = Entry::new(); + bitrate_entry.set_text(&config_in.record_bitrate_kbps.to_string()); + + let save_btn = Button::with_label("Save"); + + vbox.pack_start(&bitrate_label, false, false, 0); + vbox.pack_start(&bitrate_entry, false, false, 0); + vbox.pack_start(&save_btn, false, false, 0); + + window.add(&vbox); + + let window_clone = window.clone(); + save_btn.connect_clicked(move |_| { + let mut cfg = VrecConfig::load(); + if let Ok(val) = bitrate_entry.text().parse() { + cfg.record_bitrate_kbps = val; + } + let _ = cfg.save(); + window_clone.close(); + }); + + window.show_all(); +} + +pub fn open_general_settings(config_in: &VrecConfig) { + let window = Window::new(WindowType::Toplevel); + window.set_title("General Settings"); + window.set_default_size(350, 300); + + let vbox = Box::new(Orientation::Vertical, 10); + vbox.set_margin(15); + + // Autostart + let hbox_auto = Box::new(Orientation::Horizontal, 10); + let auto_label = Label::new(Some("Autostart with system:")); + let auto_switch = Switch::new(); + auto_switch.set_active(config_in.autostart); + hbox_auto.pack_start(&auto_label, false, false, 0); + hbox_auto.pack_start(&auto_switch, false, false, 0); + + // Theme + let theme_label = Label::new(Some("UI Theme:")); + let theme_combo = ComboBoxText::new(); + theme_combo.append_text("dark"); + theme_combo.append_text("light"); + theme_combo.set_active(Some(if config_in.ui_color_theme == "dark" { 0 } else { 1 })); + + // Language + let lang_label = Label::new(Some("Language Code (e.g. en, fr):")); + let lang_entry = Entry::new(); + lang_entry.set_text(&config_in.language); + + // Hotkeys + let menu_hk_label = Label::new(Some("Menu Hotkey:")); + let menu_hk_entry = Entry::new(); + menu_hk_entry.set_text(&config_in.menu_hotkey); + + let save_hk_label = Label::new(Some("Save Replay Hotkey:")); + let save_hk_entry = Entry::new(); + save_hk_entry.set_text(&config_in.save_hotkey); + + let save_btn = Button::with_label("Save General Settings"); + + vbox.pack_start(&hbox_auto, false, false, 0); + vbox.pack_start(&theme_label, false, false, 0); + vbox.pack_start(&theme_combo, false, false, 0); + vbox.pack_start(&lang_label, false, false, 0); + vbox.pack_start(&lang_entry, false, false, 0); + vbox.pack_start(&menu_hk_label, false, false, 0); + vbox.pack_start(&menu_hk_entry, false, false, 0); + vbox.pack_start(&save_hk_label, false, false, 0); + vbox.pack_start(&save_hk_entry, false, false, 0); + vbox.pack_start(&save_btn, false, false, 0); + + window.add(&vbox); + + let window_clone = window.clone(); + save_btn.connect_clicked(move |_| { + let mut cfg = VrecConfig::load(); + cfg.autostart = auto_switch.is_active(); + if let Some(txt) = theme_combo.active_text() { + cfg.ui_color_theme = txt.to_string(); + } + cfg.language = lang_entry.text().to_string(); + cfg.menu_hotkey = menu_hk_entry.text().to_string(); + cfg.save_hotkey = save_hk_entry.text().to_string(); + let _ = cfg.save(); + window_clone.close(); + }); + + window.show_all(); +}