feat(ui): Redesign HUD to GPU Screen Recorder style, numeric inputs for duration/bitrate/fps, audio mode toggling, and auto-daemon recovery
This commit is contained in:
parent
e0096b0f86
commit
7a15b7f8ec
6 changed files with 753 additions and 349 deletions
|
|
@ -12,8 +12,42 @@ use std::io::{Read, Write};
|
|||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn ensure_wayland_env() {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let runtime_dir = env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| format!("/run/user/{}", unsafe { libc::getuid() }));
|
||||
unsafe {
|
||||
if env::var("WAYLAND_DISPLAY").is_err() {
|
||||
if let Ok(entries) = std::fs::read_dir(&runtime_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if name.starts_with("wayland-") && !name.ends_with(".lock") {
|
||||
env::set_var("WAYLAND_DISPLAY", &name);
|
||||
println!("Auto-detected Wayland display: {}", name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if env::var("DBUS_SESSION_BUS_ADDRESS").is_err() {
|
||||
let bus_path = format!("{}/bus", runtime_dir);
|
||||
if std::path::Path::new(&bus_path).exists() {
|
||||
env::set_var("DBUS_SESSION_BUS_ADDRESS", format!("unix:path={}", bus_path));
|
||||
}
|
||||
}
|
||||
if env::var("XDG_CURRENT_DESKTOP").is_err() {
|
||||
env::set_var("XDG_CURRENT_DESKTOP", "Hyprland");
|
||||
}
|
||||
if env::var("XDG_SESSION_TYPE").map(|s| s == "tty" || s.is_empty()).unwrap_or(true) && env::var("WAYLAND_DISPLAY").is_ok() {
|
||||
env::set_var("XDG_SESSION_TYPE", "wayland");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
ensure_wayland_env();
|
||||
let session_type = env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "x11".to_string());
|
||||
println!("Detected session type: {}", session_type);
|
||||
|
||||
|
|
@ -65,7 +99,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
let (mux_tx, mux_rx) = bounded::<Vec<Packet>>(1);
|
||||
let (audio_tx, audio_rx) = bounded::<Vec<f32>>(100);
|
||||
let initial_config = vrec::config::VrecConfig::load();
|
||||
let audio_info = vrec::capture::audio::AudioCapture::new_with_device(audio_tx, Some(&initial_config.audio_device)).ok();
|
||||
let audio_info = vrec::capture::audio::AudioCapture::new_with_device_and_mode(
|
||||
audio_tx,
|
||||
Some(&initial_config.audio_device),
|
||||
&initial_config.audio_mode,
|
||||
).ok();
|
||||
|
||||
let is_recording_state = Arc::new(AtomicBool::new(false));
|
||||
let record_start_state = Arc::new(AtomicU64::new(0));
|
||||
|
|
@ -306,11 +344,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
} else {
|
||||
0
|
||||
};
|
||||
let cfg = vrec::config::VrecConfig::load();
|
||||
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),
|
||||
audio_mode: cfg.audio_mode,
|
||||
};
|
||||
if let Ok(resp) = serde_json::to_vec(&status) {
|
||||
let len_resp = (resp.len() as u32).to_le_bytes();
|
||||
|
|
@ -318,6 +358,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
let _ = stream.write_all(&resp);
|
||||
}
|
||||
},
|
||||
Command::CycleAudioMode => {
|
||||
let mut cfg = vrec::config::VrecConfig::load();
|
||||
cfg.audio_mode = match cfg.audio_mode.as_str() {
|
||||
"system" => "mic",
|
||||
"mic" => "both",
|
||||
"both" => "muted",
|
||||
_ => "system",
|
||||
}.to_string();
|
||||
let _ = cfg.save();
|
||||
println!("Audio mode cycled to: {}", cfg.audio_mode);
|
||||
},
|
||||
Command::StopDaemon => {
|
||||
println!("StopDaemon requested: Finalizing active recordings...");
|
||||
let _ = cmd_tx.send(Command::StopRecording);
|
||||
|
|
|
|||
|
|
@ -3,20 +3,72 @@ use vrec::overlay::{show_menu_overlay, show_notification};
|
|||
use std::env;
|
||||
use global_hotkey::{GlobalHotKeyManager, hotkey::{HotKey, Modifiers, Code}, GlobalHotKeyEvent};
|
||||
|
||||
fn ensure_wayland_env() {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let runtime_dir = env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| format!("/run/user/{}", unsafe { libc::getuid() }));
|
||||
unsafe {
|
||||
if env::var("WAYLAND_DISPLAY").is_err() {
|
||||
if let Ok(entries) = std::fs::read_dir(&runtime_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if name.starts_with("wayland-") && !name.ends_with(".lock") {
|
||||
env::set_var("WAYLAND_DISPLAY", &name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if env::var("DBUS_SESSION_BUS_ADDRESS").is_err() {
|
||||
let bus_path = format!("{}/bus", runtime_dir);
|
||||
if std::path::Path::new(&bus_path).exists() {
|
||||
env::set_var("DBUS_SESSION_BUS_ADDRESS", format!("unix:path={}", bus_path));
|
||||
}
|
||||
}
|
||||
if env::var("XDG_CURRENT_DESKTOP").is_err() {
|
||||
env::set_var("XDG_CURRENT_DESKTOP", "Hyprland");
|
||||
}
|
||||
if env::var("XDG_SESSION_TYPE").map(|s| s == "tty" || s.is_empty()).unwrap_or(true) && env::var("WAYLAND_DISPLAY").is_ok() {
|
||||
env::set_var("XDG_SESSION_TYPE", "wayland");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_daemon_running() {
|
||||
if query_status().is_err() {
|
||||
println!("vrec-daemon not running. Auto-launching vrec-daemon...");
|
||||
let _ = std::process::Command::new("vrec-daemon")
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn();
|
||||
for _ in 0..15 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
if query_status().is_ok() {
|
||||
println!("vrec-daemon successfully connected.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_with_notification(cmd: Command, success_msg: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
ensure_daemon_running();
|
||||
match send_command(cmd) {
|
||||
Ok(()) => {
|
||||
show_notification(success_msg);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
show_notification("Error: vrec-daemon is not running");
|
||||
show_notification("Error: failed to connect to vrec-daemon");
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_toggle_recording() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
ensure_daemon_running();
|
||||
let status = query_status().ok();
|
||||
let is_rec = status.as_ref().map(|s| s.is_recording).unwrap_or(false);
|
||||
match send_command(Command::ToggleRecording) {
|
||||
|
|
@ -29,13 +81,15 @@ fn handle_toggle_recording() -> Result<(), Box<dyn std::error::Error + Send + Sy
|
|||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
show_notification("Error: vrec-daemon is not running");
|
||||
show_notification("Error: failed to connect to vrec-daemon");
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
ensure_wayland_env();
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() > 1 {
|
||||
match args[1].as_str() {
|
||||
|
|
@ -43,6 +97,7 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
return send_with_notification(Command::SaveReplay, "Replay saved");
|
||||
}
|
||||
"--menu" => {
|
||||
ensure_daemon_running();
|
||||
show_menu_overlay();
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -98,55 +153,29 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
println!("X11 session detected.");
|
||||
}
|
||||
|
||||
let manager = GlobalHotKeyManager::new().map_err(|e| format!("Failed to init GlobalHotKeyManager: {:?}", e))?;
|
||||
let config = vrec::config::VrecConfig::load();
|
||||
let save_hotkey = vrec::hotkey::parse_hotkey(&config.save_hotkey)
|
||||
.unwrap_or_else(|| HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyR));
|
||||
let menu_hotkey = vrec::hotkey::parse_hotkey(&config.menu_hotkey)
|
||||
.unwrap_or_else(|| HotKey::new(Some(Modifiers::ALT), Code::KeyZ));
|
||||
let record_hotkey = vrec::hotkey::parse_hotkey(&config.record_hotkey)
|
||||
.unwrap_or_else(|| HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F9));
|
||||
|
||||
let mut registered_count = 0;
|
||||
if let Err(e) = manager.register(save_hotkey) {
|
||||
eprintln!("Warning: Failed to register save hotkey ({}): {}", config.save_hotkey, e);
|
||||
} else {
|
||||
println!("Registered save replay hotkey: {}", config.save_hotkey);
|
||||
registered_count += 1;
|
||||
}
|
||||
let manager = GlobalHotKeyManager::new()?;
|
||||
let hotkey_menu = HotKey::new(Some(Modifiers::ALT), Code::KeyZ);
|
||||
let hotkey_save = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyR);
|
||||
let hotkey_record = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F9);
|
||||
|
||||
if let Err(e) = manager.register(menu_hotkey) {
|
||||
eprintln!("Warning: Failed to register menu hotkey ({}): {}", config.menu_hotkey, e);
|
||||
} else {
|
||||
println!("Registered menu overlay hotkey: {}", config.menu_hotkey);
|
||||
registered_count += 1;
|
||||
}
|
||||
let _ = manager.register(hotkey_menu);
|
||||
let _ = manager.register(hotkey_save);
|
||||
let _ = manager.register(hotkey_record);
|
||||
|
||||
if let Err(e) = manager.register(record_hotkey) {
|
||||
eprintln!("Warning: Failed to register record hotkey ({}): {}", config.record_hotkey, e);
|
||||
} else {
|
||||
println!("Registered toggle recording hotkey: {}", config.record_hotkey);
|
||||
registered_count += 1;
|
||||
}
|
||||
|
||||
if registered_count == 0 {
|
||||
eprintln!("Note: Running without global hotkeys. You can bind `vrec-ui --menu`, `vrec-ui --record`, and `vrec-ui --save` to compositor shortcuts.");
|
||||
}
|
||||
println!("Listening for global hotkeys (Alt+Z for overlay, Ctrl+Shift+R for replay, Ctrl+Shift+F9 for recording)...");
|
||||
|
||||
let receiver = GlobalHotKeyEvent::receiver();
|
||||
loop {
|
||||
if let Ok(event) = receiver.recv()
|
||||
&& event.state == global_hotkey::HotKeyState::Pressed {
|
||||
if event.id == save_hotkey.id() {
|
||||
println!("Save replay hotkey pressed. Triggering SaveReplay...");
|
||||
let _ = send_with_notification(Command::SaveReplay, "Replay saved");
|
||||
} else if event.id == menu_hotkey.id() {
|
||||
println!("Menu hotkey pressed. Opening Overlay Menu...");
|
||||
show_menu_overlay();
|
||||
} else if event.id == record_hotkey.id() {
|
||||
println!("Record hotkey pressed. Toggling Recording...");
|
||||
let _ = handle_toggle_recording();
|
||||
}
|
||||
if let Ok(event) = receiver.try_recv() {
|
||||
if event.id == hotkey_menu.id() {
|
||||
ensure_daemon_running();
|
||||
show_menu_overlay();
|
||||
} else if event.id == hotkey_save.id() {
|
||||
let _ = send_with_notification(Command::SaveReplay, "Replay saved");
|
||||
} else if event.id == hotkey_record.id() {
|
||||
let _ = handle_toggle_recording();
|
||||
}
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use crossbeam_channel::Sender;
|
||||
use std::collections::VecDeque;
|
||||
use std::thread;
|
||||
|
||||
pub struct AudioCapture {
|
||||
_stream: cpal::Stream,
|
||||
_streams: Vec<cpal::Stream>,
|
||||
}
|
||||
|
||||
pub fn get_device_name(dev: &cpal::Device) -> String {
|
||||
|
|
@ -27,75 +29,168 @@ pub fn list_input_devices() -> Vec<String> {
|
|||
|
||||
impl AudioCapture {
|
||||
pub fn new(sender: Sender<Vec<f32>>) -> Result<(Self, u32, u16), Box<dyn std::error::Error + Send + Sync>> {
|
||||
Self::new_with_device(sender, None)
|
||||
Self::new_with_device_and_mode(sender, None, "system")
|
||||
}
|
||||
|
||||
pub fn new_with_device(
|
||||
sender: Sender<Vec<f32>>,
|
||||
device_name: Option<&str>,
|
||||
) -> Result<(Self, u32, u16), Box<dyn std::error::Error + Send + Sync>> {
|
||||
Self::new_with_device_and_mode(sender, device_name, "system")
|
||||
}
|
||||
|
||||
pub fn new_with_device_and_mode(
|
||||
sender: Sender<Vec<f32>>,
|
||||
device_name: Option<&str>,
|
||||
audio_mode: &str,
|
||||
) -> Result<(Self, u32, u16), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let host = cpal::default_host();
|
||||
let device = if let Some(target) = device_name
|
||||
&& target != "default"
|
||||
&& !target.trim().is_empty() {
|
||||
if let Ok(mut devs) = host.input_devices() {
|
||||
devs.find(|d| get_device_name(d) == target)
|
||||
.or_else(|| host.default_input_device())
|
||||
.ok_or("Audio device not found")?
|
||||
} else {
|
||||
host.default_input_device().ok_or("No audio input device available")?
|
||||
|
||||
if audio_mode == "muted" {
|
||||
println!("Audio capture mode: MUTED (No audio recorded)");
|
||||
return Ok((Self { _streams: Vec::new() }, 48000, 2));
|
||||
}
|
||||
|
||||
// Helper to find monitor device (system sound)
|
||||
let find_system_device = || -> Option<cpal::Device> {
|
||||
if let Ok(devs) = host.input_devices() {
|
||||
for d in devs {
|
||||
let n = get_device_name(&d).to_lowercase();
|
||||
if n.contains("monitor") {
|
||||
return Some(d);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// When default is selected, prefer desktop audio monitor if available so gameplay sound is recorded
|
||||
if let Ok(mut devs) = host.input_devices() {
|
||||
devs.find(|d| {
|
||||
let n = get_device_name(d).to_lowercase();
|
||||
n.contains("monitor")
|
||||
})
|
||||
.or_else(|| host.default_input_device())
|
||||
.ok_or("No audio input device available")?
|
||||
} else {
|
||||
host.default_input_device().ok_or("No audio input device available")?
|
||||
}
|
||||
};
|
||||
|
||||
let dev_name = get_device_name(&device);
|
||||
let config = device.default_input_config()?;
|
||||
let sample_rate = config.sample_rate();
|
||||
let channels = config.channels();
|
||||
|
||||
println!("Audio capture initialized on [{}]: {} Hz, {} channels", dev_name, sample_rate, channels);
|
||||
|
||||
let err_fn = |err| eprintln!("Audio stream error: {}", err);
|
||||
let stream_config: cpal::StreamConfig = config.into();
|
||||
|
||||
let stream = match config.sample_format() {
|
||||
cpal::SampleFormat::F32 => {
|
||||
device.build_input_stream(
|
||||
stream_config,
|
||||
move |data: &[f32], _: &_| {
|
||||
let _ = sender.try_send(data.to_vec());
|
||||
},
|
||||
err_fn,
|
||||
None,
|
||||
)?
|
||||
}
|
||||
cpal::SampleFormat::I16 => {
|
||||
device.build_input_stream(
|
||||
stream_config,
|
||||
move |data: &[i16], _: &_| {
|
||||
let f32_data: Vec<f32> = data.iter().map(|&s| s as f32 / i16::MAX as f32).collect();
|
||||
let _ = sender.try_send(f32_data);
|
||||
},
|
||||
err_fn,
|
||||
None,
|
||||
)?
|
||||
}
|
||||
_ => return Err("Unsupported audio format".into()),
|
||||
host.default_input_device()
|
||||
};
|
||||
|
||||
stream.play()?;
|
||||
|
||||
Ok((Self { _stream: stream }, sample_rate, channels))
|
||||
|
||||
// Helper to find microphone device
|
||||
let find_mic_device = || -> Option<cpal::Device> {
|
||||
if let Some(target) = device_name
|
||||
&& target != "default"
|
||||
&& !target.trim().is_empty() {
|
||||
if let Ok(devs) = host.input_devices() {
|
||||
for d in devs {
|
||||
if get_device_name(&d) == target {
|
||||
return Some(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(devs) = host.input_devices() {
|
||||
for d in devs {
|
||||
let n = get_device_name(&d).to_lowercase();
|
||||
if !n.contains("monitor") {
|
||||
return Some(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
host.default_input_device()
|
||||
};
|
||||
|
||||
// Helper to build a stream for a device
|
||||
let build_stream = |dev: &cpal::Device, tx: Sender<Vec<f32>>| -> Result<(cpal::Stream, u32, u16), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let config = dev.default_input_config()?;
|
||||
let sample_rate = config.sample_rate();
|
||||
let channels = config.channels();
|
||||
let format = config.sample_format();
|
||||
let err_fn = |err| eprintln!("Audio stream error: {}", err);
|
||||
let stream_config: cpal::StreamConfig = config.into();
|
||||
|
||||
let stream = match format {
|
||||
cpal::SampleFormat::F32 => {
|
||||
dev.build_input_stream(
|
||||
stream_config,
|
||||
move |data: &[f32], _: &_| {
|
||||
let _ = tx.try_send(data.to_vec());
|
||||
},
|
||||
err_fn,
|
||||
None,
|
||||
)?
|
||||
}
|
||||
cpal::SampleFormat::I16 => {
|
||||
dev.build_input_stream(
|
||||
stream_config,
|
||||
move |data: &[i16], _: &_| {
|
||||
let f32_data: Vec<f32> = data.iter().map(|&s| s as f32 / i16::MAX as f32).collect();
|
||||
let _ = tx.try_send(f32_data);
|
||||
},
|
||||
err_fn,
|
||||
None,
|
||||
)?
|
||||
}
|
||||
_ => return Err("Unsupported audio format".into()),
|
||||
};
|
||||
stream.play()?;
|
||||
Ok((stream, sample_rate, channels))
|
||||
};
|
||||
|
||||
match audio_mode {
|
||||
"mic" => {
|
||||
let dev = find_mic_device().ok_or("No microphone device found")?;
|
||||
let name = get_device_name(&dev);
|
||||
let (stream, sr, ch) = build_stream(&dev, sender)?;
|
||||
println!("Audio capture mode: MICROPHONE ONLY [{}] ({} Hz, {} ch)", name, sr, ch);
|
||||
Ok((Self { _streams: vec![stream] }, sr, ch))
|
||||
}
|
||||
"both" => {
|
||||
let sys_dev = find_system_device().ok_or("No system audio monitor found")?;
|
||||
let mic_dev = find_mic_device().ok_or("No microphone found")?;
|
||||
|
||||
let sys_name = get_device_name(&sys_dev);
|
||||
let mic_name = get_device_name(&mic_dev);
|
||||
|
||||
let (sys_tx, sys_rx) = crossbeam_channel::bounded::<Vec<f32>>(100);
|
||||
let (mic_tx, mic_rx) = crossbeam_channel::bounded::<Vec<f32>>(100);
|
||||
|
||||
let (sys_stream, sr, ch) = build_stream(&sys_dev, sys_tx)?;
|
||||
let (mic_stream, _, _) = build_stream(&mic_dev, mic_tx)?;
|
||||
|
||||
let out_tx = sender;
|
||||
thread::spawn(move || {
|
||||
let mut sys_q: VecDeque<f32> = VecDeque::with_capacity(16384);
|
||||
let mut mic_q: VecDeque<f32> = VecDeque::with_capacity(16384);
|
||||
|
||||
loop {
|
||||
while let Ok(chunk) = sys_rx.try_recv() {
|
||||
sys_q.extend(chunk);
|
||||
}
|
||||
while let Ok(chunk) = mic_rx.try_recv() {
|
||||
mic_q.extend(chunk);
|
||||
}
|
||||
|
||||
let avail = sys_q.len().min(mic_q.len());
|
||||
if avail >= 480 {
|
||||
let mut mixed = Vec::with_capacity(avail);
|
||||
for _ in 0..avail {
|
||||
let s = sys_q.pop_front().unwrap_or(0.0);
|
||||
let m = mic_q.pop_front().unwrap_or(0.0);
|
||||
mixed.push((s + m * 0.9).clamp(-1.0, 1.0));
|
||||
}
|
||||
let _ = out_tx.try_send(mixed);
|
||||
} else if sys_q.len() > 4800 {
|
||||
let chunk: Vec<f32> = sys_q.drain(..960.min(sys_q.len())).collect();
|
||||
let _ = out_tx.try_send(chunk);
|
||||
} else if mic_q.len() > 4800 {
|
||||
let chunk: Vec<f32> = mic_q.drain(..960.min(mic_q.len())).collect();
|
||||
let _ = out_tx.try_send(chunk);
|
||||
}
|
||||
|
||||
thread::sleep(std::time::Duration::from_millis(4));
|
||||
}
|
||||
});
|
||||
|
||||
println!("Audio capture mode: BOTH (System [{}] + Mic [{}]) ({} Hz, {} ch)", sys_name, mic_name, sr, ch);
|
||||
Ok((Self { _streams: vec![sys_stream, mic_stream] }, sr, ch))
|
||||
}
|
||||
_ => {
|
||||
// "system" is default
|
||||
let dev = find_system_device().ok_or("No audio device available")?;
|
||||
let name = get_device_name(&dev);
|
||||
let (stream, sr, ch) = build_stream(&dev, sender)?;
|
||||
println!("Audio capture mode: SYSTEM SOUNDS ONLY [{}] ({} Hz, {} ch)", name, sr, ch);
|
||||
Ok((Self { _streams: vec![stream] }, sr, ch))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,24 +2,55 @@ use serde::{Deserialize, Serialize};
|
|||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn default_true() -> bool { true }
|
||||
fn default_replay_duration() -> u32 { 60 }
|
||||
fn default_replay_bitrate() -> u32 { 18000 }
|
||||
fn default_record_bitrate() -> u32 { 20000 }
|
||||
fn default_fps() -> u32 { 60 }
|
||||
fn default_video_codec() -> String { "h264".to_string() }
|
||||
fn default_audio_device() -> String { "default".to_string() }
|
||||
fn default_audio_mode() -> String { "system".to_string() }
|
||||
fn default_lang() -> String { "en".to_string() }
|
||||
fn default_theme() -> String { "dark".to_string() }
|
||||
fn default_save_hotkey() -> String { "Ctrl+Shift+R".to_string() }
|
||||
fn default_menu_hotkey() -> String { "Alt+Z".to_string() }
|
||||
fn default_record_hotkey() -> String { "Ctrl+Shift+F9".to_string() }
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct VrecConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub replay_enabled: bool,
|
||||
#[serde(default = "default_replay_duration")]
|
||||
pub replay_duration_sec: u32,
|
||||
#[serde(default = "default_replay_bitrate")]
|
||||
pub replay_bitrate_kbps: u32,
|
||||
|
||||
#[serde(default)]
|
||||
pub record_enabled: bool,
|
||||
#[serde(default = "default_record_bitrate")]
|
||||
pub record_bitrate_kbps: u32,
|
||||
#[serde(default = "default_fps")]
|
||||
pub fps: u32,
|
||||
#[serde(default = "default_video_codec")]
|
||||
pub video_codec: String,
|
||||
#[serde(default = "VrecConfig::default_output_directory")]
|
||||
pub output_directory: String,
|
||||
#[serde(default = "default_audio_device")]
|
||||
pub audio_device: String,
|
||||
#[serde(default = "default_audio_mode")]
|
||||
pub audio_mode: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub autostart: bool,
|
||||
#[serde(default = "default_lang")]
|
||||
pub language: String,
|
||||
#[serde(default = "default_theme")]
|
||||
pub ui_color_theme: String,
|
||||
#[serde(default = "default_save_hotkey")]
|
||||
pub save_hotkey: String,
|
||||
#[serde(default = "default_menu_hotkey")]
|
||||
pub menu_hotkey: String,
|
||||
#[serde(default = "default_record_hotkey")]
|
||||
pub record_hotkey: String,
|
||||
}
|
||||
|
||||
|
|
@ -36,6 +67,7 @@ impl Default for VrecConfig {
|
|||
video_codec: "h264".to_string(),
|
||||
output_directory: Self::default_output_directory(),
|
||||
audio_device: "default".to_string(),
|
||||
audio_mode: "system".to_string(),
|
||||
|
||||
autostart: false,
|
||||
language: "en".to_string(),
|
||||
|
|
@ -61,52 +93,31 @@ impl VrecConfig {
|
|||
path
|
||||
}
|
||||
|
||||
pub fn resolve_save_path(&self, filename: &str) -> String {
|
||||
let mut p = PathBuf::from(&self.output_directory);
|
||||
p.push(filename);
|
||||
p.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
pub fn load() -> Self {
|
||||
let path = Self::config_path();
|
||||
if path.exists()
|
||||
&& let Ok(data) = fs::read_to_string(&path)
|
||||
&& let Ok(config) = serde_json::from_str(&data) {
|
||||
return config;
|
||||
if let Ok(content) = fs::read_to_string(&path) {
|
||||
if let Ok(cfg) = serde_json::from_str(&content) {
|
||||
return cfg;
|
||||
}
|
||||
Self::default()
|
||||
}
|
||||
let default_cfg = Self::default();
|
||||
let _ = default_cfg.save();
|
||||
default_cfg
|
||||
}
|
||||
|
||||
pub fn resolve_save_path(&self, filename: &str) -> String {
|
||||
let p = std::path::Path::new(&self.output_directory);
|
||||
let _ = fs::create_dir_all(p);
|
||||
p.join(filename).to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
pub fn save(&self) -> Result<(), std::io::Error> {
|
||||
pub fn save(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let path = Self::config_path();
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let _ = fs::create_dir_all(&self.output_directory);
|
||||
let data = serde_json::to_string_pretty(self)?;
|
||||
fs::write(path, data)?;
|
||||
let _ = self.update_autostart();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_autostart(&self) -> Result<(), std::io::Error> {
|
||||
let mut autostart_dir = dirs::config_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
autostart_dir.push("autostart");
|
||||
let desktop_file = autostart_dir.join("vrec.desktop");
|
||||
|
||||
if self.autostart {
|
||||
fs::create_dir_all(&autostart_dir)?;
|
||||
let content = "[Desktop Entry]\n\
|
||||
Type=Application\n\
|
||||
Name=vrec Screen Recorder\n\
|
||||
Comment=Hardware-accelerated screen recorder and instant replay\n\
|
||||
Exec=sh -c \"vrec-daemon & vrec-ui\"\n\
|
||||
Terminal=false\n\
|
||||
Categories=AudioVideo;Recorder;\n";
|
||||
fs::write(desktop_file, content)?;
|
||||
} else if desktop_file.exists() {
|
||||
let _ = fs::remove_file(desktop_file);
|
||||
}
|
||||
let json = serde_json::to_string_pretty(self)?;
|
||||
fs::write(path, json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -122,13 +133,14 @@ mod tests {
|
|||
#[test]
|
||||
fn test_config_defaults_and_json() {
|
||||
let cfg = VrecConfig::default();
|
||||
assert!(cfg.replay_enabled);
|
||||
assert_eq!(cfg.replay_duration_sec, 60);
|
||||
assert_eq!(cfg.replay_bitrate_kbps, 18000);
|
||||
assert_eq!(cfg.fps, 60);
|
||||
assert!(cfg.output_directory.contains("vrec"));
|
||||
assert_eq!(cfg.video_codec, "h264");
|
||||
assert_eq!(cfg.audio_mode, "system");
|
||||
let json = serde_json::to_string(&cfg).unwrap();
|
||||
let parsed: VrecConfig = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed.menu_hotkey, "Alt+Z");
|
||||
assert_eq!(parsed.save_hotkey, "Ctrl+Shift+R");
|
||||
assert_eq!(parsed.replay_duration_sec, 60);
|
||||
assert_eq!(parsed.audio_mode, "system");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ pub enum Command {
|
|||
StopRecording,
|
||||
ToggleRecording,
|
||||
ToggleAudio,
|
||||
CycleAudioMode,
|
||||
ReloadConfig,
|
||||
StopDaemon,
|
||||
ShowOverlay,
|
||||
|
|
@ -19,6 +20,12 @@ pub struct DaemonStatus {
|
|||
pub recording_duration_sec: u64,
|
||||
pub is_replay_active: bool,
|
||||
pub audio_muted: bool,
|
||||
#[serde(default = "default_audio_mode_str")]
|
||||
pub audio_mode: String,
|
||||
}
|
||||
|
||||
fn default_audio_mode_str() -> String {
|
||||
"system".to_string()
|
||||
}
|
||||
|
||||
pub fn send_command(cmd: Command) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
|
|
|
|||
610
src/overlay.rs
610
src/overlay.rs
|
|
@ -1,7 +1,7 @@
|
|||
use gtk::prelude::*;
|
||||
use gtk::{
|
||||
Application, ApplicationWindow, Box, Button, ComboBoxText, CssProvider, Entry, Label,
|
||||
Orientation, Stack, StackTransitionType, StyleContext, Switch,
|
||||
Orientation, SpinButton, Stack, StackTransitionType, StyleContext, Switch,
|
||||
};
|
||||
#[cfg(target_os = "linux")]
|
||||
use gtk_layer_shell::{Layer, LayerShell};
|
||||
|
|
@ -32,8 +32,8 @@ pub fn show_notification(message: &str) {
|
|||
app.connect_activate(move |app| {
|
||||
let window = ApplicationWindow::builder()
|
||||
.application(app)
|
||||
.default_width(340)
|
||||
.default_height(70)
|
||||
.default_width(320)
|
||||
.default_height(60)
|
||||
.build();
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
|
|
@ -41,7 +41,7 @@ pub fn show_notification(message: &str) {
|
|||
window.init_layer_shell();
|
||||
window.set_layer(Layer::Overlay);
|
||||
window.set_namespace("vrec-notification");
|
||||
window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 35);
|
||||
window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 30);
|
||||
window.set_anchor(gtk_layer_shell::Edge::Top, true);
|
||||
}
|
||||
|
||||
|
|
@ -56,16 +56,16 @@ pub fn show_notification(message: &str) {
|
|||
let css_provider = CssProvider::new();
|
||||
let css = r#"
|
||||
window {
|
||||
background-color: rgba(18, 20, 26, 0.95);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
box-shadow: 0px 8px 30px rgba(0, 0, 0, 0.7);
|
||||
background-color: #12141a;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0px 8px 32px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
label {
|
||||
color: #ffffff;
|
||||
font-weight: 800;
|
||||
font-size: 16px;
|
||||
padding: 12px 28px;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
padding: 10px 24px;
|
||||
}
|
||||
"#;
|
||||
let _ = css_provider.load_from_data(css.as_bytes());
|
||||
|
|
@ -111,8 +111,8 @@ pub fn show_menu_overlay() {
|
|||
|
||||
let window = ApplicationWindow::builder()
|
||||
.application(app)
|
||||
.default_width(860)
|
||||
.default_height(470)
|
||||
.default_width(820)
|
||||
.default_height(480)
|
||||
.build();
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
|
|
@ -121,7 +121,7 @@ pub fn show_menu_overlay() {
|
|||
window.set_layer(Layer::Overlay);
|
||||
window.set_namespace("vrec-hud");
|
||||
window.set_keyboard_interactivity(true);
|
||||
window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 70);
|
||||
window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 50);
|
||||
window.set_anchor(gtk_layer_shell::Edge::Top, true);
|
||||
}
|
||||
|
||||
|
|
@ -133,25 +133,24 @@ pub fn show_menu_overlay() {
|
|||
window.set_position(gtk::WindowPosition::Center);
|
||||
}
|
||||
|
||||
// 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);
|
||||
stack.set_transition_duration(180);
|
||||
|
||||
// ==========================================
|
||||
// PAGE 1: HUD Quick Actions (Xbox / GPU Screen Recorder layout)
|
||||
// PAGE 1: HUD Quick Actions (GPU Screen Recorder Style)
|
||||
// ==========================================
|
||||
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);
|
||||
hud_page.set_margin_top(18);
|
||||
hud_page.set_margin_bottom(18);
|
||||
|
||||
// Header
|
||||
// Header Bar
|
||||
let header_box = Box::new(Orientation::Horizontal, 10);
|
||||
let title_label = Label::new(Some("vrec"));
|
||||
let title_label = Label::new(Some("VREC"));
|
||||
title_label.style_context().add_class("hud-title");
|
||||
let badge_label = Label::new(Some("GPU ZERO-COPY"));
|
||||
let badge_label = Label::new(Some("GPU ACCELERATED"));
|
||||
badge_label.style_context().add_class("badge-tag");
|
||||
let spacer = Box::new(Orientation::Horizontal, 0);
|
||||
let close_btn = Button::with_label("✕");
|
||||
|
|
@ -162,61 +161,120 @@ pub fn show_menu_overlay() {
|
|||
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);
|
||||
// 4 Action Cards Row
|
||||
let cards_box = Box::new(Orientation::Horizontal, 14);
|
||||
cards_box.set_halign(gtk::Align::Center);
|
||||
|
||||
// Card 1: Instant Replay
|
||||
// Card 1: 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\nEnabled"
|
||||
} else {
|
||||
"Instant Replay\nDisabled"
|
||||
});
|
||||
replay_toggle_btn.set_size_request(170, 75);
|
||||
let replay_hdr = Box::new(Orientation::Horizontal, 6);
|
||||
let replay_title = Label::new(Some("REPLAY"));
|
||||
replay_title.style_context().add_class("card-header-label");
|
||||
let replay_pill = Label::new(Some(if config.replay_enabled { "ACTIVE" } else { "OFF" }));
|
||||
replay_pill.style_context().add_class("status-pill");
|
||||
replay_pill.style_context().add_class(if config.replay_enabled { "pill-active" } else { "pill-idle" });
|
||||
let replay_sp = Box::new(Orientation::Horizontal, 0);
|
||||
replay_hdr.pack_start(&replay_title, false, false, 0);
|
||||
replay_hdr.pack_start(&replay_sp, true, true, 0);
|
||||
replay_hdr.pack_start(&replay_pill, false, false, 0);
|
||||
|
||||
let replay_toggle_btn = Button::with_label(if config.replay_enabled { "Disable Replay" } else { "Enable Replay" });
|
||||
replay_toggle_btn.set_size_request(165, 45);
|
||||
replay_toggle_btn.style_context().add_class("action-btn");
|
||||
|
||||
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_hdr, false, false, 0);
|
||||
replay_card.pack_start(&replay_toggle_btn, true, true, 4);
|
||||
replay_card.pack_start(&replay_save_btn, false, false, 0);
|
||||
|
||||
// Card 2: Normal Recording
|
||||
// Card 2: Record
|
||||
let record_card = Box::new(Orientation::Vertical, 8);
|
||||
record_card.style_context().add_class("card-box");
|
||||
let record_toggle_btn = Button::with_label("Record\nStart");
|
||||
record_toggle_btn.set_size_request(170, 75);
|
||||
let record_hdr = Box::new(Orientation::Horizontal, 6);
|
||||
let record_title = Label::new(Some("RECORD"));
|
||||
record_title.style_context().add_class("card-header-label");
|
||||
let record_pill = Label::new(Some("IDLE"));
|
||||
record_pill.style_context().add_class("status-pill");
|
||||
record_pill.style_context().add_class("pill-idle");
|
||||
let record_sp = Box::new(Orientation::Horizontal, 0);
|
||||
record_hdr.pack_start(&record_title, false, false, 0);
|
||||
record_hdr.pack_start(&record_sp, true, true, 0);
|
||||
record_hdr.pack_start(&record_pill, false, false, 0);
|
||||
|
||||
let record_toggle_btn = Button::with_label("Start Record");
|
||||
record_toggle_btn.set_size_request(165, 45);
|
||||
record_toggle_btn.style_context().add_class("action-btn");
|
||||
|
||||
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);
|
||||
let record_format_sub = Label::new(Some(&format!("{} • {} FPS", config.video_codec.to_uppercase(), config.fps)));
|
||||
record_format_sub.style_context().add_class("sub-label");
|
||||
record_card.pack_start(&record_hdr, false, false, 0);
|
||||
record_card.pack_start(&record_toggle_btn, true, true, 4);
|
||||
record_card.pack_start(&record_format_sub, false, false, 0);
|
||||
|
||||
// Card 3: Microphone & Audio
|
||||
// Card 3: Audio Mode
|
||||
let audio_card = Box::new(Orientation::Vertical, 8);
|
||||
audio_card.style_context().add_class("card-box");
|
||||
let mic_toggle_btn = Button::with_label("Microphone\nActive");
|
||||
mic_toggle_btn.set_size_request(170, 75);
|
||||
mic_toggle_btn.style_context().add_class("action-btn");
|
||||
let audio_hdr = Box::new(Orientation::Horizontal, 6);
|
||||
let audio_title = Label::new(Some("AUDIO"));
|
||||
audio_title.style_context().add_class("card-header-label");
|
||||
let audio_pill = Label::new(Some(match config.audio_mode.as_str() {
|
||||
"mic" => "MIC ONLY",
|
||||
"both" => "SYSTEM + MIC",
|
||||
"muted" => "MUTED",
|
||||
_ => "SYSTEM ONLY",
|
||||
}));
|
||||
audio_pill.style_context().add_class("status-pill");
|
||||
audio_pill.style_context().add_class(match config.audio_mode.as_str() {
|
||||
"mic" => "pill-cyan",
|
||||
"both" => "pill-purple",
|
||||
"muted" => "pill-recording",
|
||||
_ => "pill-active",
|
||||
});
|
||||
let audio_sp = Box::new(Orientation::Horizontal, 0);
|
||||
audio_hdr.pack_start(&audio_title, false, false, 0);
|
||||
audio_hdr.pack_start(&audio_sp, true, true, 0);
|
||||
audio_hdr.pack_start(&audio_pill, false, false, 0);
|
||||
|
||||
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 audio_cycle_btn = Button::with_label(match config.audio_mode.as_str() {
|
||||
"mic" => "Microphone Only",
|
||||
"both" => "System + Mic",
|
||||
"muted" => "Muted",
|
||||
_ => "System Audio Only",
|
||||
});
|
||||
audio_cycle_btn.set_size_request(165, 45);
|
||||
audio_cycle_btn.style_context().add_class("action-btn");
|
||||
|
||||
let audio_hint_sub = Label::new(Some("Click to Cycle Mode"));
|
||||
audio_hint_sub.style_context().add_class("sub-label");
|
||||
audio_card.pack_start(&audio_hdr, false, false, 0);
|
||||
audio_card.pack_start(&audio_cycle_btn, true, true, 4);
|
||||
audio_card.pack_start(&audio_hint_sub, false, false, 0);
|
||||
|
||||
// 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\nConfigure");
|
||||
settings_open_btn.set_size_request(170, 75);
|
||||
let settings_hdr = Box::new(Orientation::Horizontal, 6);
|
||||
let settings_title_hdr = Label::new(Some("PREFERENCES"));
|
||||
settings_title_hdr.style_context().add_class("card-header-label");
|
||||
let settings_pill = Label::new(Some("TUNING"));
|
||||
settings_pill.style_context().add_class("status-pill");
|
||||
settings_pill.style_context().add_class("pill-idle");
|
||||
let settings_sp = Box::new(Orientation::Horizontal, 0);
|
||||
settings_hdr.pack_start(&settings_title_hdr, false, false, 0);
|
||||
settings_hdr.pack_start(&settings_sp, true, true, 0);
|
||||
settings_hdr.pack_start(&settings_pill, false, false, 0);
|
||||
|
||||
let settings_open_btn = Button::with_label("Open Settings");
|
||||
settings_open_btn.set_size_request(165, 45);
|
||||
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);
|
||||
let settings_hint_sub = Label::new(Some("Esc to Close"));
|
||||
settings_hint_sub.style_context().add_class("sub-label");
|
||||
settings_card.pack_start(&settings_hdr, false, false, 0);
|
||||
settings_card.pack_start(&settings_open_btn, true, true, 4);
|
||||
settings_card.pack_start(&settings_hint_sub, false, false, 0);
|
||||
|
||||
cards_box.pack_start(&replay_card, true, true, 0);
|
||||
cards_box.pack_start(&record_card, true, true, 0);
|
||||
|
|
@ -227,75 +285,90 @@ pub fn show_menu_overlay() {
|
|||
hud_page.pack_start(&cards_box, true, true, 0);
|
||||
|
||||
// ==========================================
|
||||
// PAGE 2: Inline Settings Panel (Zero separate windows!)
|
||||
// PAGE 2: Fine-Tuned Settings Panel (Numeric inputs for Time, Quality, FPS)
|
||||
// ==========================================
|
||||
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_page = Box::new(Orientation::Vertical, 12);
|
||||
settings_page.set_margin_start(24);
|
||||
settings_page.set_margin_end(24);
|
||||
settings_page.set_margin_top(16);
|
||||
settings_page.set_margin_bottom(16);
|
||||
|
||||
let settings_header = Box::new(Orientation::Horizontal, 12);
|
||||
let back_btn = Button::with_label("Back");
|
||||
let back_btn = Button::with_label("Back to HUD");
|
||||
back_btn.style_context().add_class("sub-btn");
|
||||
let settings_title = Label::new(Some("Recorder Settings"));
|
||||
let settings_title = Label::new(Some("Recorder Settings & Quality Tuning"));
|
||||
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);
|
||||
settings_grid.set_column_spacing(20);
|
||||
settings_grid.set_row_spacing(10);
|
||||
|
||||
// Replay Duration
|
||||
let dur_lbl = Label::new(Some("Replay Duration:"));
|
||||
// 1. Replay Duration (Seconds) - Direct numeric spin button + preset pills!
|
||||
let dur_lbl = Label::new(Some("Replay Buffer (Seconds):"));
|
||||
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()));
|
||||
let dur_box = Box::new(Orientation::Horizontal, 6);
|
||||
let dur_spin = SpinButton::with_range(5.0, 1800.0, 5.0);
|
||||
dur_spin.set_value(config.replay_duration_sec as f64);
|
||||
dur_box.pack_start(&dur_spin, false, false, 0);
|
||||
|
||||
let p_30s = Button::with_label("30s"); p_30s.style_context().add_class("preset-btn");
|
||||
let p_60s = Button::with_label("60s"); p_60s.style_context().add_class("preset-btn");
|
||||
let p_2m = Button::with_label("2m"); p_2m.style_context().add_class("preset-btn");
|
||||
let p_5m = Button::with_label("5m"); p_5m.style_context().add_class("preset-btn");
|
||||
let s_clone = dur_spin.clone(); p_30s.connect_clicked(move |_| s_clone.set_value(30.0));
|
||||
let s_clone = dur_spin.clone(); p_60s.connect_clicked(move |_| s_clone.set_value(60.0));
|
||||
let s_clone = dur_spin.clone(); p_2m.connect_clicked(move |_| s_clone.set_value(120.0));
|
||||
let s_clone = dur_spin.clone(); p_5m.connect_clicked(move |_| s_clone.set_value(300.0));
|
||||
dur_box.pack_start(&p_30s, false, false, 0);
|
||||
dur_box.pack_start(&p_60s, false, false, 0);
|
||||
dur_box.pack_start(&p_2m, false, false, 0);
|
||||
dur_box.pack_start(&p_5m, false, false, 0);
|
||||
|
||||
// Video Bitrate
|
||||
let bit_lbl = Label::new(Some("Recording Quality:"));
|
||||
// 2. Video Quality / Bitrate (Mbps) - Direct numeric spin button + preset pills!
|
||||
let bit_lbl = Label::new(Some("Video Bitrate (Mbps):"));
|
||||
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()));
|
||||
let bit_box = Box::new(Orientation::Horizontal, 6);
|
||||
let bit_spin = SpinButton::with_range(2.0, 100.0, 1.0);
|
||||
bit_spin.set_value((config.record_bitrate_kbps / 1000) as f64);
|
||||
bit_box.pack_start(&bit_spin, false, false, 0);
|
||||
|
||||
// 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);
|
||||
let b_10m = Button::with_label("10M"); b_10m.style_context().add_class("preset-btn");
|
||||
let b_20m = Button::with_label("20M"); b_20m.style_context().add_class("preset-btn");
|
||||
let b_30m = Button::with_label("30M"); b_30m.style_context().add_class("preset-btn");
|
||||
let b_50m = Button::with_label("50M"); b_50m.style_context().add_class("preset-btn");
|
||||
let b_clone = bit_spin.clone(); b_10m.connect_clicked(move |_| b_clone.set_value(10.0));
|
||||
let b_clone = bit_spin.clone(); b_20m.connect_clicked(move |_| b_clone.set_value(20.0));
|
||||
let b_clone = bit_spin.clone(); b_30m.connect_clicked(move |_| b_clone.set_value(30.0));
|
||||
let b_clone = bit_spin.clone(); b_50m.connect_clicked(move |_| b_clone.set_value(50.0));
|
||||
bit_box.pack_start(&b_10m, false, false, 0);
|
||||
bit_box.pack_start(&b_20m, false, false, 0);
|
||||
bit_box.pack_start(&b_30m, false, false, 0);
|
||||
bit_box.pack_start(&b_50m, false, false, 0);
|
||||
|
||||
// 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);
|
||||
|
||||
// Framerate Selector (Crucial for low-end PCs: 30 FPS cuts GPU load by 50%)
|
||||
let fps_lbl = Label::new(Some("Framerate (FPS):"));
|
||||
// 3. Framerate (FPS) - Direct numeric spin button + preset pills!
|
||||
let fps_lbl = Label::new(Some("Target Framerate (FPS):"));
|
||||
fps_lbl.set_halign(gtk::Align::Start);
|
||||
let fps_combo = ComboBoxText::new();
|
||||
fps_combo.append(Some("30"), "30 FPS (Low-End PC)");
|
||||
fps_combo.append(Some("60"), "60 FPS (Smooth / Gaming)");
|
||||
fps_combo.append(Some("120"), "120 FPS (High-End)");
|
||||
fps_combo.set_active_id(Some(&config.fps.to_string()));
|
||||
let fps_box = Box::new(Orientation::Horizontal, 6);
|
||||
let fps_spin = SpinButton::with_range(15.0, 240.0, 1.0);
|
||||
fps_spin.set_value(config.fps as f64);
|
||||
fps_box.pack_start(&fps_spin, false, false, 0);
|
||||
|
||||
// Video Codec Selector
|
||||
let f_30 = Button::with_label("30"); f_30.style_context().add_class("preset-btn");
|
||||
let f_60 = Button::with_label("60"); f_60.style_context().add_class("preset-btn");
|
||||
let f_120 = Button::with_label("120"); f_120.style_context().add_class("preset-btn");
|
||||
let f_144 = Button::with_label("144"); f_144.style_context().add_class("preset-btn");
|
||||
let f_clone = fps_spin.clone(); f_30.connect_clicked(move |_| f_clone.set_value(30.0));
|
||||
let f_clone = fps_spin.clone(); f_60.connect_clicked(move |_| f_clone.set_value(60.0));
|
||||
let f_clone = fps_spin.clone(); f_120.connect_clicked(move |_| f_clone.set_value(120.0));
|
||||
let f_clone = fps_spin.clone(); f_144.connect_clicked(move |_| f_clone.set_value(144.0));
|
||||
fps_box.pack_start(&f_30, false, false, 0);
|
||||
fps_box.pack_start(&f_60, false, false, 0);
|
||||
fps_box.pack_start(&f_120, false, false, 0);
|
||||
fps_box.pack_start(&f_144, false, false, 0);
|
||||
|
||||
// 4. Video Codec
|
||||
let codec_lbl = Label::new(Some("Video Codec:"));
|
||||
codec_lbl.set_halign(gtk::Align::Start);
|
||||
let codec_combo = ComboBoxText::new();
|
||||
|
|
@ -304,8 +377,18 @@ pub fn show_menu_overlay() {
|
|||
codec_combo.append(Some("av1"), "AV1 (Next-Gen High Quality)");
|
||||
codec_combo.set_active_id(Some(&config.video_codec));
|
||||
|
||||
// Audio Input Device Selector
|
||||
let audio_dev_lbl = Label::new(Some("Audio Device:"));
|
||||
// 5. Audio Source Mode (System Only vs Mic Only vs Both!)
|
||||
let audio_mode_lbl = Label::new(Some("Audio Track Mode:"));
|
||||
audio_mode_lbl.set_halign(gtk::Align::Start);
|
||||
let audio_mode_combo = ComboBoxText::new();
|
||||
audio_mode_combo.append(Some("system"), "System Sounds Only (Game / Desktop)");
|
||||
audio_mode_combo.append(Some("mic"), "Microphone Only");
|
||||
audio_mode_combo.append(Some("both"), "Both (System Sounds + Microphone Merged)");
|
||||
audio_mode_combo.append(Some("muted"), "Muted (No Audio)");
|
||||
audio_mode_combo.set_active_id(Some(&config.audio_mode));
|
||||
|
||||
// 6. Audio Device
|
||||
let audio_dev_lbl = Label::new(Some("Audio Hardware Device:"));
|
||||
audio_dev_lbl.set_halign(gtk::Align::Start);
|
||||
let audio_dev_combo = ComboBoxText::new();
|
||||
audio_dev_combo.append(Some("default"), "Default Device");
|
||||
|
|
@ -314,39 +397,58 @@ pub fn show_menu_overlay() {
|
|||
}
|
||||
audio_dev_combo.set_active_id(Some(&config.audio_device));
|
||||
|
||||
// Save Folder
|
||||
// 7. Save Folder
|
||||
let dir_lbl = Label::new(Some("Save Folder:"));
|
||||
dir_lbl.set_halign(gtk::Align::Start);
|
||||
let dir_entry = Entry::new();
|
||||
dir_entry.set_text(&config.output_directory);
|
||||
|
||||
// 8. 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 record_hk_lbl = Label::new(Some("Toggle Record Hotkey:"));
|
||||
record_hk_lbl.set_halign(gtk::Align::Start);
|
||||
let record_hk_entry = Entry::new();
|
||||
record_hk_entry.set_text(&config.record_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);
|
||||
|
||||
// 9. Autostart
|
||||
let auto_lbl = Label::new(Some("Autostart on Login:"));
|
||||
auto_lbl.set_halign(gtk::Align::Start);
|
||||
let auto_switch = Switch::new();
|
||||
auto_switch.set_active(config.autostart);
|
||||
|
||||
settings_grid.attach(&dur_lbl, 0, 0, 1, 1);
|
||||
settings_grid.attach(&dur_combo, 1, 0, 1, 1);
|
||||
settings_grid.attach(&dur_box, 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(&bit_box, 1, 1, 1, 1);
|
||||
settings_grid.attach(&fps_lbl, 0, 2, 1, 1);
|
||||
settings_grid.attach(&fps_combo, 1, 2, 1, 1);
|
||||
settings_grid.attach(&fps_box, 1, 2, 1, 1);
|
||||
settings_grid.attach(&codec_lbl, 0, 3, 1, 1);
|
||||
settings_grid.attach(&codec_combo, 1, 3, 1, 1);
|
||||
settings_grid.attach(&audio_dev_lbl, 0, 4, 1, 1);
|
||||
settings_grid.attach(&audio_dev_combo, 1, 4, 1, 1);
|
||||
settings_grid.attach(&dir_lbl, 0, 5, 1, 1);
|
||||
settings_grid.attach(&dir_entry, 1, 5, 1, 1);
|
||||
settings_grid.attach(&auto_lbl, 0, 6, 1, 1);
|
||||
settings_grid.attach(&auto_switch, 1, 6, 1, 1);
|
||||
settings_grid.attach(&audio_mode_lbl, 0, 4, 1, 1);
|
||||
settings_grid.attach(&audio_mode_combo, 1, 4, 1, 1);
|
||||
settings_grid.attach(&audio_dev_lbl, 0, 5, 1, 1);
|
||||
settings_grid.attach(&audio_dev_combo, 1, 5, 1, 1);
|
||||
settings_grid.attach(&dir_lbl, 0, 6, 1, 1);
|
||||
settings_grid.attach(&dir_entry, 1, 6, 1, 1);
|
||||
settings_grid.attach(&save_hk_lbl, 0, 7, 1, 1);
|
||||
settings_grid.attach(&save_hk_entry, 1, 7, 1, 1);
|
||||
settings_grid.attach(&record_hk_lbl, 0, 8, 1, 1);
|
||||
settings_grid.attach(&record_hk_entry, 1, 8, 1, 1);
|
||||
settings_grid.attach(&menu_hk_lbl, 0, 9, 1, 1);
|
||||
settings_grid.attach(&menu_hk_entry, 1, 9, 1, 1);
|
||||
settings_grid.attach(&auto_lbl, 0, 10, 1, 1);
|
||||
settings_grid.attach(&auto_switch, 1, 10, 1, 1);
|
||||
|
||||
let save_settings_btn = Button::with_label("Apply & Save");
|
||||
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);
|
||||
|
|
@ -357,75 +459,133 @@ pub fn show_menu_overlay() {
|
|||
stack.add_named(&settings_page, "settings");
|
||||
window.add(&stack);
|
||||
|
||||
// CSS Styling (Sleek Frosted Glass HUD)
|
||||
// CSS Styling (Brushed Stealth HUD Theme)
|
||||
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;
|
||||
background-color: #12141a;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.85);
|
||||
color: #f1f5f9;
|
||||
}
|
||||
.hud-title {
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
letter-spacing: 1px;
|
||||
letter-spacing: 1.5px;
|
||||
}
|
||||
.badge-tag {
|
||||
background-color: rgba(56, 189, 248, 0.2);
|
||||
background-color: rgba(56, 189, 248, 0.12);
|
||||
color: #38bdf8;
|
||||
border: 1px solid rgba(56, 189, 248, 0.3);
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
font-weight: 700;
|
||||
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;
|
||||
background-color: #181b23;
|
||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
min-width: 170px;
|
||||
}
|
||||
.card-box:hover {
|
||||
background-color: rgba(255, 255, 255, 0.07);
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
background-color: #1e222d;
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
.card-header-label {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #94a3b8;
|
||||
letter-spacing: 0.8px;
|
||||
}
|
||||
.status-pill {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.pill-active {
|
||||
background-color: rgba(16, 185, 129, 0.15);
|
||||
color: #10b981;
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
.pill-idle {
|
||||
background-color: rgba(148, 163, 184, 0.12);
|
||||
color: #94a3b8;
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
.pill-recording {
|
||||
background-color: rgba(239, 68, 68, 0.2);
|
||||
color: #ef4444;
|
||||
border: 1px solid rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
.pill-cyan {
|
||||
background-color: rgba(6, 182, 212, 0.15);
|
||||
color: #06b6d4;
|
||||
border: 1px solid rgba(6, 182, 212, 0.3);
|
||||
}
|
||||
.pill-purple {
|
||||
background-color: rgba(168, 85, 247, 0.15);
|
||||
color: #a855f7;
|
||||
border: 1px solid rgba(168, 85, 247, 0.3);
|
||||
}
|
||||
.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;
|
||||
background-color: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.action-btn:hover {
|
||||
background-color: #2563eb;
|
||||
border-color: #3b82f6;
|
||||
color: #ffffff;
|
||||
}
|
||||
.recording-active {
|
||||
background-color: rgba(239, 68, 68, 0.25) !important;
|
||||
border-color: #ef4444 !important;
|
||||
color: #ef4444 !important;
|
||||
}
|
||||
.sub-btn {
|
||||
background-color: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 6px;
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 6px 12px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
.sub-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.16);
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
color: #ffffff;
|
||||
}
|
||||
.preset-btn {
|
||||
background-color: #1e222d;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 6px;
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
.preset-btn:hover {
|
||||
background-color: rgba(37, 99, 235, 0.2);
|
||||
border-color: #3b82f6;
|
||||
color: #ffffff;
|
||||
}
|
||||
.apply-btn {
|
||||
background-color: #2563eb;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
padding: 10px 24px;
|
||||
}
|
||||
.apply-btn:hover {
|
||||
background-color: #1d4ed8;
|
||||
|
|
@ -434,7 +594,7 @@ pub fn show_menu_overlay() {
|
|||
background-color: transparent;
|
||||
color: #94a3b8;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
|
@ -446,12 +606,26 @@ pub fn show_menu_overlay() {
|
|||
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);
|
||||
entry, spinbutton, combobox button {
|
||||
background-color: #171a22;
|
||||
color: #f1f5f9;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
entry:focus, spinbutton:focus, combobox button:focus {
|
||||
border-color: #3b82f6;
|
||||
background-color: #1c202a;
|
||||
}
|
||||
spinbutton button {
|
||||
background-color: transparent;
|
||||
color: #94a3b8;
|
||||
border: none;
|
||||
}
|
||||
spinbutton button:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
"#;
|
||||
let _ = css_provider.load_from_data(css.as_bytes());
|
||||
|
|
@ -494,29 +668,25 @@ pub fn show_menu_overlay() {
|
|||
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;
|
||||
}
|
||||
if let Some(fps_str) = fps_combo.active_id()
|
||||
&& let Ok(fps) = fps_str.parse() {
|
||||
cfg.fps = fps;
|
||||
}
|
||||
cfg.replay_duration_sec = dur_spin.value() as u32;
|
||||
let bit_mbps = bit_spin.value() as u32;
|
||||
cfg.record_bitrate_kbps = bit_mbps * 1000;
|
||||
cfg.replay_bitrate_kbps = bit_mbps * 1000;
|
||||
cfg.fps = fps_spin.value() as u32;
|
||||
|
||||
if let Some(c) = codec_combo.active_id() {
|
||||
cfg.video_codec = c.to_string();
|
||||
}
|
||||
if let Some(m) = audio_mode_combo.active_id() {
|
||||
cfg.audio_mode = m.to_string();
|
||||
}
|
||||
if let Some(dev) = audio_dev_combo.active_id() {
|
||||
cfg.audio_device = dev.to_string();
|
||||
}
|
||||
let dir_val = dir_entry.text().to_string();
|
||||
if !dir_val.trim().is_empty() {
|
||||
cfg.output_directory = dir_val;
|
||||
}
|
||||
if let Some(dev) = audio_dev_combo.active_id() {
|
||||
cfg.audio_device = dev.to_string();
|
||||
}
|
||||
cfg.autostart = auto_switch.is_active();
|
||||
cfg.save_hotkey = save_hk_entry.text().to_string();
|
||||
cfg.menu_hotkey = menu_hk_entry.text().to_string();
|
||||
|
|
@ -528,15 +698,22 @@ pub fn show_menu_overlay() {
|
|||
|
||||
// Action Handlers
|
||||
let replay_lbl_btn = replay_toggle_btn.clone();
|
||||
let replay_pill_clone = replay_pill.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_lbl_btn.set_label("Instant Replay\nEnabled");
|
||||
replay_lbl_btn.set_label("Disable Replay");
|
||||
replay_pill_clone.set_label("ACTIVE");
|
||||
replay_pill_clone.style_context().remove_class("pill-idle");
|
||||
replay_pill_clone.style_context().add_class("pill-active");
|
||||
} else {
|
||||
replay_lbl_btn.set_label("Instant Replay\nDisabled");
|
||||
replay_lbl_btn.set_label("Enable Replay");
|
||||
replay_pill_clone.set_label("OFF");
|
||||
replay_pill_clone.style_context().remove_class("pill-active");
|
||||
replay_pill_clone.style_context().add_class("pill-idle");
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -549,14 +726,53 @@ pub fn show_menu_overlay() {
|
|||
let _ = ipc::send_command(Command::ToggleRecording);
|
||||
});
|
||||
|
||||
mic_toggle_btn.connect_clicked(|_| {
|
||||
let _ = ipc::send_command(Command::ToggleAudio);
|
||||
// Audio Source Mode Quick-Cycle
|
||||
let audio_pill_clone = audio_pill.clone();
|
||||
let audio_btn_clone = audio_cycle_btn.clone();
|
||||
audio_cycle_btn.connect_clicked(move |_| {
|
||||
let mut cfg = VrecConfig::load();
|
||||
let next_mode = match cfg.audio_mode.as_str() {
|
||||
"system" => "mic",
|
||||
"mic" => "both",
|
||||
"both" => "muted",
|
||||
_ => "system",
|
||||
};
|
||||
cfg.audio_mode = next_mode.to_string();
|
||||
let _ = cfg.save();
|
||||
VrecConfig::notify_daemon_reload();
|
||||
|
||||
audio_pill_clone.style_context().remove_class("pill-active");
|
||||
audio_pill_clone.style_context().remove_class("pill-cyan");
|
||||
audio_pill_clone.style_context().remove_class("pill-purple");
|
||||
audio_pill_clone.style_context().remove_class("pill-recording");
|
||||
|
||||
match next_mode {
|
||||
"mic" => {
|
||||
audio_pill_clone.set_label("MIC ONLY");
|
||||
audio_pill_clone.style_context().add_class("pill-cyan");
|
||||
audio_btn_clone.set_label("Microphone Only");
|
||||
}
|
||||
"both" => {
|
||||
audio_pill_clone.set_label("SYSTEM + MIC");
|
||||
audio_pill_clone.style_context().add_class("pill-purple");
|
||||
audio_btn_clone.set_label("System + Mic");
|
||||
}
|
||||
"muted" => {
|
||||
audio_pill_clone.set_label("MUTED");
|
||||
audio_pill_clone.style_context().add_class("pill-recording");
|
||||
audio_btn_clone.set_label("Muted");
|
||||
}
|
||||
_ => {
|
||||
audio_pill_clone.set_label("SYSTEM ONLY");
|
||||
audio_pill_clone.style_context().add_class("pill-active");
|
||||
audio_btn_clone.set_label("System Audio Only");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Periodic Live Status Query from Daemon (Syncs live recording state & timer!)
|
||||
// Periodic Status Sync (Timer & Live States)
|
||||
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 rec_pill_sync = record_pill.clone();
|
||||
let is_running = Arc::new(AtomicBool::new(true));
|
||||
let is_running_clone = Arc::clone(&is_running);
|
||||
|
||||
|
|
@ -569,23 +785,17 @@ pub fn show_menu_overlay() {
|
|||
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.set_label("Stop Recording");
|
||||
rec_btn_sync.style_context().add_class("recording-active");
|
||||
rec_pill_sync.set_label(&format!("REC {:02}:{:02}", mins, secs));
|
||||
rec_pill_sync.style_context().remove_class("pill-idle");
|
||||
rec_pill_sync.style_context().add_class("pill-recording");
|
||||
} else {
|
||||
rec_btn_sync.set_label("Record\nStart");
|
||||
rec_btn_sync.set_label("Start Record");
|
||||
rec_btn_sync.style_context().remove_class("recording-active");
|
||||
}
|
||||
|
||||
if status.audio_muted {
|
||||
mic_btn_sync.set_label("Microphone\nMuted");
|
||||
} else {
|
||||
mic_btn_sync.set_label("Microphone\nActive");
|
||||
}
|
||||
|
||||
if status.is_replay_active {
|
||||
replay_btn_sync.set_label("Instant Replay\nEnabled");
|
||||
} else {
|
||||
replay_btn_sync.set_label("Instant Replay\nDisabled");
|
||||
rec_pill_sync.set_label("IDLE");
|
||||
rec_pill_sync.style_context().remove_class("pill-recording");
|
||||
rec_pill_sync.style_context().add_class("pill-idle");
|
||||
}
|
||||
}
|
||||
gtk::glib::ControlFlow::Continue
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue