533 lines
20 KiB
Rust
533 lines
20 KiB
Rust
use gtk::prelude::*;
|
|
use gtk::{
|
|
Application, ApplicationWindow, Box, Button, ComboBoxText, CssProvider, Entry, Label,
|
|
Orientation, Stack, StackTransitionType, StyleContext, Switch,
|
|
};
|
|
#[cfg(target_os = "linux")]
|
|
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::ipc::{self, Command};
|
|
|
|
pub fn show_notification_overlay() {
|
|
#[cfg(unix)]
|
|
if std::env::var("WAYLAND_DISPLAY").is_err() && std::env::var("DISPLAY").is_err() {
|
|
return;
|
|
}
|
|
if gtk::init().is_err() {
|
|
return;
|
|
}
|
|
|
|
let app = Application::builder()
|
|
.application_id("com.vrec.notification")
|
|
.build();
|
|
|
|
app.connect_activate(|app| {
|
|
let window = ApplicationWindow::builder()
|
|
.application(app)
|
|
.default_width(320)
|
|
.default_height(70)
|
|
.build();
|
|
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
window.init_layer_shell();
|
|
window.set_layer(Layer::Overlay);
|
|
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);
|
|
}
|
|
|
|
#[cfg(not(target_os = "linux"))]
|
|
{
|
|
window.set_decorated(false);
|
|
window.set_keep_above(true);
|
|
window.set_skip_taskbar_hint(true);
|
|
window.set_position(gtk::WindowPosition::Center);
|
|
}
|
|
|
|
let css_provider = CssProvider::new();
|
|
let css = r#"
|
|
window {
|
|
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: 17px;
|
|
padding: 12px 24px;
|
|
}
|
|
"#;
|
|
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();
|
|
gtk::glib::timeout_add_local(Duration::from_secs(2), move || {
|
|
window_clone.close();
|
|
gtk::glib::ControlFlow::Break
|
|
});
|
|
});
|
|
|
|
app.run_with_args(&[] as &[&str]);
|
|
}
|
|
|
|
pub fn show_menu_overlay() {
|
|
#[cfg(unix)]
|
|
if std::env::var("WAYLAND_DISPLAY").is_err() && std::env::var("DISPLAY").is_err() {
|
|
eprintln!("Error: No display server detected (WAYLAND_DISPLAY and DISPLAY are unset).");
|
|
return;
|
|
}
|
|
if gtk::init().is_err() {
|
|
eprintln!("Error: Failed to connect to display server.");
|
|
return;
|
|
}
|
|
|
|
let app = Application::builder()
|
|
.application_id("com.vrec.hud")
|
|
.build();
|
|
|
|
app.connect_activate(|app| {
|
|
let config = VrecConfig::load();
|
|
|
|
let window = ApplicationWindow::builder()
|
|
.application(app)
|
|
.default_width(820)
|
|
.default_height(220)
|
|
.build();
|
|
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
window.init_layer_shell();
|
|
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_anchor(gtk_layer_shell::Edge::Top, true);
|
|
}
|
|
|
|
#[cfg(not(target_os = "linux"))]
|
|
{
|
|
window.set_decorated(false);
|
|
window.set_keep_above(true);
|
|
window.set_skip_taskbar_hint(true);
|
|
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);
|
|
|
|
// ==========================================
|
|
// 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_toggle_btn.set_size_request(170, 75);
|
|
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_save_btn, false, false, 0);
|
|
|
|
// 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 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);
|
|
|
|
// 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");
|
|
|
|
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);
|
|
|
|
// 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_close.close();
|
|
});
|
|
|
|
let window_esc = window.clone();
|
|
window.connect_key_press_event(move |_, key| {
|
|
if key.keyval() == gdk::keys::constants::Escape {
|
|
window_esc.close();
|
|
gtk::glib::Propagation::Stop
|
|
} else {
|
|
gtk::glib::Propagation::Proceed
|
|
}
|
|
});
|
|
|
|
// 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_lbl_btn.set_label("Instant Replay\n🟢 ACTIVE");
|
|
} else {
|
|
replay_lbl_btn.set_label("Instant Replay\n⚪ OFF");
|
|
}
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
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;
|
|
}
|
|
|
|
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");
|
|
}
|
|
}
|
|
gtk::glib::ControlFlow::Continue
|
|
});
|
|
|
|
window.connect_destroy(move |_| {
|
|
is_running.store(false, Ordering::Relaxed);
|
|
});
|
|
|
|
window.show_all();
|
|
});
|
|
|
|
app.run_with_args(&[] as &[&str]);
|
|
}
|