Add autostart options, clear settings sections, and formatted video filenames
This commit is contained in:
parent
900b7a9629
commit
8751e49727
4 changed files with 565 additions and 156 deletions
|
|
@ -25,6 +25,14 @@ ln -sf "$BIN_DIR/scythe-daemon" "$BIN_DIR/vrec-daemon"
|
|||
ln -sf "$BIN_DIR/scythe-ui" "$BIN_DIR/vrec-ui"
|
||||
ln -sf "$BIN_DIR/scythe-ui" "$BIN_DIR/vrec"
|
||||
|
||||
if pgrep -x scythe-daemon > /dev/null; then
|
||||
echo "Restarting active scythe-daemon engine..."
|
||||
pkill -x scythe-daemon || true
|
||||
sleep 0.5
|
||||
nohup "$BIN_DIR/scythe-daemon" > /dev/null 2>&1 &
|
||||
disown
|
||||
fi
|
||||
|
||||
echo "[3/4] Creating application launcher..."
|
||||
cat > "$APP_DIR/scythe.desktop" << EOF
|
||||
[Desktop Entry]
|
||||
|
|
|
|||
130
src/config.rs
130
src/config.rs
|
|
@ -54,6 +54,10 @@ pub struct ScytheConfig {
|
|||
|
||||
#[serde(default)]
|
||||
pub autostart: bool,
|
||||
#[serde(default)]
|
||||
pub autostart_replay: bool,
|
||||
#[serde(default)]
|
||||
pub autostart_overlay: bool,
|
||||
#[serde(default = "default_lang")]
|
||||
pub language: String,
|
||||
#[serde(default = "default_theme")]
|
||||
|
|
@ -92,6 +96,8 @@ impl Default for ScytheConfig {
|
|||
system_volume: default_system_volume(),
|
||||
|
||||
autostart: false,
|
||||
autostart_replay: false,
|
||||
autostart_overlay: false,
|
||||
language: "en".to_string(),
|
||||
ui_color_theme: "dark".to_string(),
|
||||
save_hotkey: "Ctrl+Shift+R".to_string(),
|
||||
|
|
@ -147,7 +153,10 @@ impl ScytheConfig {
|
|||
pub fn load() -> Self {
|
||||
let path = Self::config_path();
|
||||
if let Ok(content) = fs::read_to_string(&path)
|
||||
&& let Ok(cfg) = serde_json::from_str(&content) {
|
||||
&& let Ok(mut cfg) = serde_json::from_str::<Self>(&content) {
|
||||
if cfg.autostart && !cfg.autostart_replay {
|
||||
cfg.autostart_replay = true;
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
let legacy = Self::legacy_config_path();
|
||||
|
|
@ -157,6 +166,9 @@ impl ScytheConfig {
|
|||
if cfg.output_directory.ends_with("/Videos/vrec") || cfg.output_directory.ends_with("\\Videos\\vrec") {
|
||||
cfg.output_directory = Self::default_output_directory();
|
||||
}
|
||||
if cfg.autostart && !cfg.autostart_replay {
|
||||
cfg.autostart_replay = true;
|
||||
}
|
||||
let _ = cfg.save();
|
||||
return cfg;
|
||||
}
|
||||
|
|
@ -180,14 +192,109 @@ impl ScytheConfig {
|
|||
let _ = fs::write(legacy, &json);
|
||||
}
|
||||
|
||||
self.sync_autostart();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn sync_autostart(&self) {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let home = match std::env::var("HOME") {
|
||||
Ok(h) => PathBuf::from(h),
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let autostart_dir = home.join(".config").join("autostart");
|
||||
let _ = fs::create_dir_all(&autostart_dir);
|
||||
|
||||
// 1. Replay daemon autostart entry
|
||||
let daemon_desktop = autostart_dir.join("scythe-daemon.desktop");
|
||||
if self.autostart_replay {
|
||||
let content = "[Desktop Entry]\n\
|
||||
Type=Application\n\
|
||||
Name=Scythe Replay Engine\n\
|
||||
Comment=Scythe Background Screen Recorder & Instant Replay Daemon\n\
|
||||
Icon=media-record\n\
|
||||
Exec=scythe-daemon\n\
|
||||
Terminal=false\n\
|
||||
Hidden=false\n\
|
||||
X-GNOME-Autostart-enabled=true\n";
|
||||
let _ = fs::write(&daemon_desktop, content);
|
||||
} else {
|
||||
let _ = fs::remove_file(&daemon_desktop);
|
||||
}
|
||||
|
||||
// 2. Overlay autostart entry
|
||||
let overlay_desktop = autostart_dir.join("scythe-overlay.desktop");
|
||||
if self.autostart_overlay {
|
||||
let content = "[Desktop Entry]\n\
|
||||
Type=Application\n\
|
||||
Name=Scythe Overlay\n\
|
||||
Comment=Scythe Screen Recorder HUD Overlay\n\
|
||||
Icon=media-record\n\
|
||||
Exec=scythe-ui --menu\n\
|
||||
Terminal=false\n\
|
||||
Hidden=false\n\
|
||||
X-GNOME-Autostart-enabled=true\n";
|
||||
let _ = fs::write(&overlay_desktop, content);
|
||||
} else {
|
||||
let _ = fs::remove_file(&overlay_desktop);
|
||||
}
|
||||
|
||||
// 3. Hyprland execs.lua integration if present
|
||||
let hypr_execs_lua = home.join(".config").join("hypr").join("hyprland").join("execs.lua");
|
||||
if hypr_execs_lua.exists() {
|
||||
if let Ok(mut text) = fs::read_to_string(&hypr_execs_lua) {
|
||||
let mut modified = false;
|
||||
|
||||
let daemon_cmd_pattern = "scythe-daemon";
|
||||
let daemon_cmd_line = " hl.exec_cmd(\"scythe-daemon\")\n";
|
||||
if self.autostart_replay {
|
||||
if !text.contains(daemon_cmd_pattern) {
|
||||
if let Some(pos) = text.rfind("end)") {
|
||||
text.insert_str(pos, daemon_cmd_line);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
} else if text.contains(daemon_cmd_pattern) {
|
||||
text = text.lines()
|
||||
.filter(|l| !l.contains(daemon_cmd_pattern))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n") + "\n";
|
||||
modified = true;
|
||||
}
|
||||
|
||||
let overlay_cmd_pattern = "scythe-ui --menu";
|
||||
let overlay_cmd_line = " hl.exec_cmd(\"scythe-ui --menu\")\n";
|
||||
if self.autostart_overlay {
|
||||
if !text.contains(overlay_cmd_pattern) {
|
||||
if let Some(pos) = text.rfind("end)") {
|
||||
text.insert_str(pos, overlay_cmd_line);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
} else if text.contains(overlay_cmd_pattern) {
|
||||
text = text.lines()
|
||||
.filter(|l| !l.contains(overlay_cmd_pattern))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n") + "\n";
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if modified {
|
||||
let _ = fs::write(&hypr_execs_lua, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notify_daemon_reload() {
|
||||
let _ = crate::ipc::send_command(crate::ipc::Command::ReloadConfig);
|
||||
}
|
||||
|
||||
/// Formats video filenames with informative local timestamps, e.g. "Replay-18-30-00-05-09-2026.mp4"
|
||||
/// Formats video filenames with informative local timestamps, e.g. "Replay-18-30-00_05-09-2026.mp4"
|
||||
pub fn format_video_filename(prefix: &str, ext: &str) -> String {
|
||||
unsafe {
|
||||
let t = libc::time(std::ptr::null_mut());
|
||||
|
|
@ -204,7 +311,7 @@ impl ScytheConfig {
|
|||
let month = tm.tm_mon + 1;
|
||||
let year = tm.tm_year + 1900;
|
||||
|
||||
format!("{}-{:02}-{:02}-{:02}-{:02}-{:02}-{:04}.{}", prefix, hour, min, sec, day, month, year, ext)
|
||||
format!("{}-{:02}-{:02}-{:02}_{:02}-{:02}-{:04}.{}", prefix, hour, min, sec, day, month, year, ext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -239,12 +346,19 @@ mod tests {
|
|||
let name = ScytheConfig::format_video_filename("Replay", "mp4");
|
||||
assert!(name.starts_with("Replay-"));
|
||||
assert!(name.ends_with(".mp4"));
|
||||
// Check pattern Replay-HH-MM-SS-DD-MM-YYYY.mp4 (7 parts separated by hyphen)
|
||||
// Check pattern Replay-HH-MM-SS_DD-MM-YYYY.mp4
|
||||
let without_ext = name.strip_suffix(".mp4").unwrap();
|
||||
let parts: Vec<&str> = without_ext.split('-').collect();
|
||||
assert_eq!(parts.len(), 7, "Filename parts mismatch: {}", name);
|
||||
assert_eq!(parts[0], "Replay");
|
||||
for part in &parts[1..] {
|
||||
let top_parts: Vec<&str> = without_ext.split('_').collect();
|
||||
assert_eq!(top_parts.len(), 2, "Filename underscore separation mismatch: {}", name);
|
||||
let time_parts: Vec<&str> = top_parts[0].split('-').collect();
|
||||
assert_eq!(time_parts.len(), 4, "Time parts mismatch: {}", top_parts[0]);
|
||||
assert_eq!(time_parts[0], "Replay");
|
||||
for part in &time_parts[1..] {
|
||||
assert!(part.chars().all(|c| c.is_ascii_digit()));
|
||||
}
|
||||
let date_parts: Vec<&str> = top_parts[1].split('-').collect();
|
||||
assert_eq!(date_parts.len(), 3, "Date parts mismatch: {}", top_parts[1]);
|
||||
for part in &date_parts {
|
||||
assert!(part.chars().all(|c| c.is_ascii_digit()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use gtk::prelude::*;
|
||||
use gtk::{
|
||||
Application, ApplicationWindow, Box, Button, ComboBoxText, CssProvider,
|
||||
DrawingArea, Entry, Label, LevelBar, Orientation, Revealer, RevealerTransitionType, Scale,
|
||||
DrawingArea, Entry, Fixed, Label, LevelBar, Orientation, Revealer, RevealerTransitionType, Scale,
|
||||
ScrolledWindow, SpinButton, Stack, StackTransitionType, StyleContext, Switch,
|
||||
};
|
||||
#[cfg(target_os = "linux")]
|
||||
|
|
@ -9,7 +9,7 @@ use gtk_layer_shell::{Layer, LayerShell};
|
|||
use std::f64::consts::PI;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
use crate::config::ScytheConfig;
|
||||
use crate::ipc::{self, Command};
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ pub fn show_shadowplay_toast(title: &str, subtitle: &str, icon: ToastIcon) {
|
|||
return;
|
||||
}
|
||||
|
||||
let app_id = format!("com.scythe.toast.{}", std::process::id());
|
||||
let app_id = format!("com.scythe.toast.p{}", std::process::id());
|
||||
let app = Application::builder()
|
||||
.application_id(&app_id)
|
||||
.flags(gtk::gio::ApplicationFlags::NON_UNIQUE)
|
||||
|
|
@ -130,8 +130,8 @@ pub fn show_shadowplay_toast(title: &str, subtitle: &str, icon: ToastIcon) {
|
|||
app.connect_activate(move |app| {
|
||||
let window = ApplicationWindow::builder()
|
||||
.application(app)
|
||||
.default_width(330)
|
||||
.default_height(58)
|
||||
.default_width(340)
|
||||
.default_height(64)
|
||||
.build();
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
|
|
@ -156,7 +156,7 @@ pub fn show_shadowplay_toast(title: &str, subtitle: &str, icon: ToastIcon) {
|
|||
if let Some(display) = gdk::Display::default()
|
||||
&& let Some(mon) = display.primary_monitor().or_else(|| display.monitor(0)) {
|
||||
let geom = mon.geometry();
|
||||
let x = geom.x() + geom.width() - 330 - 24;
|
||||
let x = geom.x() + geom.width() - 340 - 24;
|
||||
let y = geom.y() + 24;
|
||||
window.move_(x, y);
|
||||
}
|
||||
|
|
@ -184,7 +184,7 @@ pub fn show_shadowplay_toast(title: &str, subtitle: &str, icon: ToastIcon) {
|
|||
box-shadow: none !important;
|
||||
}}
|
||||
.shadowplay-toast {{
|
||||
background-color: rgba(11, 11, 13, 0.96);
|
||||
background-color: rgba(12, 13, 17, 0.82);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-left: 3.5px solid {accent};
|
||||
border-radius: 0px !important;
|
||||
|
|
@ -321,13 +321,40 @@ pub fn show_shadowplay_toast(title: &str, subtitle: &str, icon: ToastIcon) {
|
|||
|
||||
hbox.pack_start(&icon_area, false, false, 0);
|
||||
hbox.pack_start(&vbox, true, true, 0);
|
||||
window.add(&hbox);
|
||||
|
||||
let fixed = Fixed::new();
|
||||
fixed.set_size_request(340, 64);
|
||||
fixed.put(&hbox, 340, 5);
|
||||
window.add(&fixed);
|
||||
window.show_all();
|
||||
|
||||
let window_clone = window.clone();
|
||||
gtk::glib::timeout_add_local(Duration::from_millis(2600), move || {
|
||||
window_clone.close();
|
||||
gtk::glib::ControlFlow::Break
|
||||
let hbox_clone = hbox.clone();
|
||||
let fixed_clone = fixed.clone();
|
||||
let start_time = Instant::now();
|
||||
|
||||
gtk::glib::timeout_add_local(Duration::from_millis(16), move || {
|
||||
let elapsed = start_time.elapsed().as_secs_f32();
|
||||
if elapsed >= 2.80 {
|
||||
window_clone.close();
|
||||
return gtk::glib::ControlFlow::Break;
|
||||
}
|
||||
|
||||
let slide_x = if elapsed < 0.35 {
|
||||
let t = (elapsed / 0.35).min(1.0);
|
||||
let ease = 1.0 - (1.0 - t).powi(3);
|
||||
(1.0 - ease) * 340.0
|
||||
} else if elapsed < 2.40 {
|
||||
0.0
|
||||
} else {
|
||||
let t = ((elapsed - 2.40) / 0.40).min(1.0);
|
||||
let ease = t.powi(3);
|
||||
ease * 340.0
|
||||
};
|
||||
|
||||
let target_x = (slide_x + 10.0).round() as i32;
|
||||
fixed_clone.move_(&hbox_clone, target_x, 5);
|
||||
gtk::glib::ControlFlow::Continue
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -73,10 +73,12 @@ fn trim_clip(
|
|||
let stem = input_path.file_stem().unwrap_or_default().to_string_lossy();
|
||||
let ext = input_path.extension().unwrap_or_default().to_string_lossy();
|
||||
let parent = input_path.parent().unwrap_or_else(|| std::path::Path::new("."));
|
||||
|
||||
let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs();
|
||||
let out_name = format!("{}_trim_{}.{}", stem, now, ext);
|
||||
let out_path = parent.join(&out_name);
|
||||
let mut out_path = parent.join(format!("{}_trimmed.{}", stem, ext));
|
||||
let mut counter = 1;
|
||||
while out_path.exists() {
|
||||
out_path = parent.join(format!("{}_trimmed_{}.{}", stem, counter, ext));
|
||||
counter += 1;
|
||||
}
|
||||
|
||||
let mut cmd = std::process::Command::new("ffmpeg");
|
||||
cmd.args([
|
||||
|
|
@ -338,7 +340,7 @@ fn render_keycap_button(
|
|||
)
|
||||
} else {
|
||||
(
|
||||
Color32::from_rgba_unmultiplied(16, 16, 19, 230),
|
||||
Color32::from_rgba_unmultiplied(18, 20, 26, 195),
|
||||
Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 28)),
|
||||
Color32::from_rgb(220, 220, 225),
|
||||
text.to_string(),
|
||||
|
|
@ -424,72 +426,53 @@ fn draw_replay_icon(painter: &egui::Painter, center: egui::Pos2, radius: f32, is
|
|||
Color32::from_rgb(150, 150, 155)
|
||||
};
|
||||
|
||||
// 1. Sleek circular track arc curving counter-clockwise (rewind)
|
||||
let arc_radius = radius * 0.95;
|
||||
let start_angle = -PI * 0.70; // top-left
|
||||
let end_angle = PI * 0.90; // bottom-left
|
||||
let steps = 36;
|
||||
// 1. Sleek circular track arc (sweeping ~290 degrees counter-clockwise for rewind effect)
|
||||
let arc_radius = radius * 0.92;
|
||||
let start_angle = -PI * 0.65; // top-left
|
||||
let end_angle = PI * 0.95; // bottom-left
|
||||
let steps = 40;
|
||||
let mut points = Vec::with_capacity(steps + 1);
|
||||
for i in 0..=steps {
|
||||
let t = i as f32 / steps as f32;
|
||||
let angle = start_angle + t * (end_angle - start_angle);
|
||||
points.push(center + Vec2::new(angle.cos() * arc_radius, angle.sin() * arc_radius));
|
||||
}
|
||||
let stroke = Stroke::new(2.2_f32, color);
|
||||
let stroke = Stroke::new(2.4_f32, color);
|
||||
for w in points.windows(2) {
|
||||
painter.line_segment([w[0], w[1]], stroke);
|
||||
}
|
||||
|
||||
// 2. Crisp sharp arrow head at start of arc pointing counter-clockwise
|
||||
// 2. Crisp directional arrow head pointing counter-clockwise
|
||||
let tip = points[0];
|
||||
let tangent = Vec2::new(-start_angle.sin(), start_angle.cos()).normalized();
|
||||
let normal = Vec2::new(start_angle.cos(), start_angle.sin()).normalized();
|
||||
let p_back_1 = tip - tangent * 7.0 + normal * 4.5;
|
||||
let p_back_2 = tip - tangent * 7.0 - normal * 4.5;
|
||||
let p_back_1 = tip - tangent * 7.5 + normal * 4.5;
|
||||
let p_back_2 = tip - tangent * 7.5 - normal * 4.5;
|
||||
painter.add(egui::Shape::convex_polygon(
|
||||
vec![tip, p_back_1, p_back_2],
|
||||
color,
|
||||
Stroke::NONE,
|
||||
));
|
||||
|
||||
// 3. Central dual rewind chevrons <<
|
||||
let chev_h = radius * 0.38;
|
||||
let chev_w = radius * 0.26;
|
||||
// Left chevron
|
||||
let c1_tip = center + Vec2::new(-chev_w * 1.1, 0.0);
|
||||
let c1_top = center + Vec2::new(-chev_w * 0.05, -chev_h);
|
||||
let c1_bot = center + Vec2::new(-chev_w * 0.05, chev_h);
|
||||
// 3. Crisp centered solid Play triangle ▶ (optical centering slightly right)
|
||||
let tri_r = radius * 0.42;
|
||||
let tri_offset = Vec2::new(tri_r * 0.18, 0.0);
|
||||
let p1 = center + tri_offset + Vec2::new(tri_r, 0.0);
|
||||
let p2 = center + tri_offset + Vec2::new(-tri_r * 0.65, -tri_r * 0.75);
|
||||
let p3 = center + tri_offset + Vec2::new(-tri_r * 0.65, tri_r * 0.75);
|
||||
painter.add(egui::Shape::convex_polygon(
|
||||
vec![c1_tip, c1_top, c1_bot],
|
||||
color,
|
||||
Stroke::NONE,
|
||||
));
|
||||
// Right chevron
|
||||
let c2_tip = center + Vec2::new(chev_w * 0.15, 0.0);
|
||||
let c2_top = center + Vec2::new(chev_w * 1.20, -chev_h);
|
||||
let c2_bot = center + Vec2::new(chev_w * 1.20, chev_h);
|
||||
painter.add(egui::Shape::convex_polygon(
|
||||
vec![c2_tip, c2_top, c2_bot],
|
||||
vec![p1, p2, p3],
|
||||
color,
|
||||
Stroke::NONE,
|
||||
));
|
||||
}
|
||||
|
||||
fn draw_record_icon(painter: &egui::Painter, center: egui::Pos2, radius: f32, is_recording: bool, anim_time: f32) {
|
||||
fn draw_record_icon(painter: &egui::Painter, center: egui::Pos2, radius: f32, is_recording: bool, _anim_time: f32) {
|
||||
let half = radius * 0.88;
|
||||
let arm = radius * 0.36;
|
||||
|
||||
if is_recording {
|
||||
let pulse = (anim_time * 5.0).sin() * 0.5 + 0.5;
|
||||
let red_bright = Color32::from_rgb(239, 68, 68);
|
||||
let red_glow = Color32::from_rgba_unmultiplied(239, 68, 68, (35.0 + pulse * 50.0) as u8);
|
||||
|
||||
// Soft pulsing ambient halo around viewfinder
|
||||
painter.rect_filled(
|
||||
egui::Rect::from_center_size(center, Vec2::splat(half * 2.2 + pulse * 4.0)),
|
||||
CornerRadius::ZERO,
|
||||
red_glow,
|
||||
);
|
||||
|
||||
// Viewfinder corner brackets (Red)
|
||||
let stroke = Stroke::new(2.2_f32, red_bright);
|
||||
|
|
@ -506,8 +489,8 @@ fn draw_record_icon(painter: &egui::Painter, center: egui::Pos2, radius: f32, is
|
|||
painter.line_segment([center + Vec2::new(half - arm, half), center + Vec2::new(half, half)], stroke);
|
||||
painter.line_segment([center + Vec2::new(half, half), center + Vec2::new(half, half - arm)], stroke);
|
||||
|
||||
// Center recording core (pulsing)
|
||||
let core_r = radius * 0.40 + pulse * 1.5;
|
||||
// Center recording core (completely static, solid and crisp, no bobbing/pulsing)
|
||||
let core_r = radius * 0.42;
|
||||
painter.circle_filled(center, core_r, red_bright);
|
||||
} else {
|
||||
let frame_color = Color32::from_rgb(150, 150, 155);
|
||||
|
|
@ -585,11 +568,11 @@ fn render_action_card(
|
|||
let hovered = response.hovered();
|
||||
|
||||
let bg = if dropdown_open {
|
||||
Color32::from_rgba_unmultiplied(22, 22, 26, 250)
|
||||
Color32::from_rgba_unmultiplied(22, 24, 30, 220)
|
||||
} else if hovered {
|
||||
Color32::from_rgba_unmultiplied(18, 18, 22, 245)
|
||||
Color32::from_rgba_unmultiplied(18, 20, 26, 210)
|
||||
} else {
|
||||
Color32::from_rgba_unmultiplied(11, 11, 13, 235)
|
||||
Color32::from_rgba_unmultiplied(12, 13, 17, 195)
|
||||
};
|
||||
|
||||
let border = if dropdown_open {
|
||||
|
|
@ -615,36 +598,36 @@ fn render_action_card(
|
|||
egui::pos2(rect.center().x, rect.top() + 24.0),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
title,
|
||||
FontId::proportional(12.0),
|
||||
FontId::monospace(14.5),
|
||||
if is_active { accent } else { Color32::WHITE },
|
||||
);
|
||||
|
||||
// Centered Vector Icon
|
||||
let icon_center = egui::pos2(rect.center().x, rect.top() + 85.0);
|
||||
// Centered vector icon
|
||||
let icon_center = egui::pos2(rect.center().x, rect.top() + 84.0);
|
||||
draw_icon(painter, icon_center);
|
||||
|
||||
// Status label
|
||||
// Status subtitle (e.g. "01:23" or "Not recording" / "Buffer 60s")
|
||||
painter.text(
|
||||
egui::pos2(rect.center().x, rect.top() + 138.0),
|
||||
egui::pos2(rect.center().x, rect.bottom() - 32.0),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
status_text,
|
||||
FontId::proportional(11.5),
|
||||
status_color,
|
||||
);
|
||||
|
||||
// Subtitle label
|
||||
// Secondary hint
|
||||
painter.text(
|
||||
egui::pos2(rect.center().x, rect.top() + 158.0),
|
||||
egui::pos2(rect.center().x, rect.bottom() - 14.0),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
sub_text,
|
||||
FontId::proportional(10.0),
|
||||
Color32::from_rgb(130, 130, 136),
|
||||
Color32::from_rgb(120, 120, 126),
|
||||
);
|
||||
|
||||
response.clicked()
|
||||
}
|
||||
|
||||
// Attached Squared Dropdown Menu Container (Pixel-perfect width and flush full-bleed items)
|
||||
// Sleek Squared Dropdown Action Menu Container
|
||||
fn render_dropdown_menu(
|
||||
ui: &mut egui::Ui,
|
||||
card_width: f32,
|
||||
|
|
@ -653,7 +636,7 @@ fn render_dropdown_menu(
|
|||
) {
|
||||
ui.add_space(4.0);
|
||||
egui::Frame::NONE
|
||||
.fill(Color32::from_rgba_unmultiplied(11, 11, 13, 252))
|
||||
.fill(Color32::from_rgba_unmultiplied(12, 13, 17, 210))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 180)))
|
||||
.corner_radius(CornerRadius::ZERO)
|
||||
.inner_margin(Margin::ZERO)
|
||||
|
|
@ -706,7 +689,7 @@ fn render_menu_item(ui: &mut egui::Ui, label: &str, accent: Color32, is_last: bo
|
|||
// Section card helper for Settings view
|
||||
fn render_section_card(ui: &mut egui::Ui, header: &str, accent: Color32, add_contents: impl FnOnce(&mut egui::Ui)) {
|
||||
egui::Frame::NONE
|
||||
.fill(Color32::from_rgba_unmultiplied(14, 14, 17, 230))
|
||||
.fill(Color32::from_rgba_unmultiplied(16, 18, 24, 185))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 16)))
|
||||
.corner_radius(CornerRadius::ZERO)
|
||||
.inner_margin(Margin::symmetric(16_i8, 12_i8))
|
||||
|
|
@ -747,7 +730,6 @@ pub struct ScytheOverlayApp {
|
|||
trim_status_msg: Option<(String, Instant)>,
|
||||
clip_duration_sec: f32,
|
||||
anim_time: f32,
|
||||
status_msg: Option<(String, Instant)>,
|
||||
status_rx: Receiver<DaemonStatus>,
|
||||
folder_tx: Sender<String>,
|
||||
folder_rx: Receiver<String>,
|
||||
|
|
@ -762,6 +744,18 @@ pub struct ScytheOverlayApp {
|
|||
update_status: Arc<std::sync::Mutex<crate::updater::UpdateStatus>>,
|
||||
update_dismissed: bool,
|
||||
auto_check_updates: bool,
|
||||
autostart_replay: bool,
|
||||
autostart_overlay: bool,
|
||||
hud_notification: Option<HudNotification>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct HudNotification {
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub icon: crate::overlay::ToastIcon,
|
||||
pub start_time: Instant,
|
||||
pub duration_secs: f32,
|
||||
}
|
||||
|
||||
pub type VrecOverlayApp = ScytheOverlayApp;
|
||||
|
|
@ -808,6 +802,8 @@ impl ScytheOverlayApp {
|
|||
let replay_sec_input_str = replay_sec.to_string();
|
||||
|
||||
let auto_check_updates = config.auto_check_updates;
|
||||
let autostart_replay = config.autostart_replay;
|
||||
let autostart_overlay = config.autostart_overlay;
|
||||
let update_status = Arc::new(std::sync::Mutex::new(crate::updater::UpdateStatus::Idle));
|
||||
if auto_check_updates {
|
||||
crate::updater::spawn_update_check(update_status.clone());
|
||||
|
|
@ -837,7 +833,6 @@ impl ScytheOverlayApp {
|
|||
trim_status_msg: None,
|
||||
clip_duration_sec: 0.0,
|
||||
anim_time: 0.0,
|
||||
status_msg: None,
|
||||
status_rx,
|
||||
folder_tx,
|
||||
folder_rx,
|
||||
|
|
@ -852,9 +847,22 @@ impl ScytheOverlayApp {
|
|||
update_status,
|
||||
update_dismissed: false,
|
||||
auto_check_updates,
|
||||
autostart_replay,
|
||||
autostart_overlay,
|
||||
hud_notification: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_hud_notification(&mut self, title: &str, subtitle: &str, icon: crate::overlay::ToastIcon) {
|
||||
self.hud_notification = Some(HudNotification {
|
||||
title: title.to_string(),
|
||||
subtitle: subtitle.to_string(),
|
||||
icon,
|
||||
start_time: Instant::now(),
|
||||
duration_secs: 2.8,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn accent_color(&self) -> Color32 {
|
||||
resolve_accent_color(&self.config.accent_color)
|
||||
}
|
||||
|
|
@ -934,31 +942,12 @@ impl ScytheOverlayApp {
|
|||
|
||||
ui.allocate_new_ui(egui::UiBuilder::new().max_rect(hud_rect), |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
// Sleek status toast if present
|
||||
if let Some((msg, ts)) = &self.status_msg {
|
||||
if ts.elapsed() < Duration::from_secs(3) {
|
||||
egui::Frame::NONE
|
||||
.fill(Color32::from_rgba_unmultiplied(10, 10, 12, 245))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 180)))
|
||||
.corner_radius(CornerRadius::ZERO)
|
||||
.inner_margin(Margin::symmetric(14_i8, 5_i8))
|
||||
.show(ui, |ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(msg)
|
||||
.font(FontId::monospace(11.0))
|
||||
.strong()
|
||||
.color(accent),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Voluntary, non-intrusive Update Notification Banner
|
||||
let cur_update = self.update_status.lock().ok().map(|g| g.clone()).unwrap_or_default();
|
||||
if let crate::updater::UpdateStatus::Available(ref info) = cur_update {
|
||||
if !self.update_dismissed {
|
||||
egui::Frame::NONE
|
||||
.fill(Color32::from_rgba_unmultiplied(12, 12, 15, 250))
|
||||
.fill(Color32::from_rgba_unmultiplied(13, 14, 18, 210))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 170)))
|
||||
.corner_radius(CornerRadius::ZERO)
|
||||
.inner_margin(Margin::symmetric(14_i8, 7_i8))
|
||||
|
|
@ -1030,7 +1019,7 @@ impl ScytheOverlayApp {
|
|||
}
|
||||
if render_menu_item(ui, "Save Replay", accent, true) {
|
||||
let _ = ipc::send_command(Command::SaveReplay);
|
||||
self.status_msg = Some(("Replay Saved!".to_string(), Instant::now()));
|
||||
self.show_hud_notification("INSTANT REPLAY", "Saved to Videos", crate::overlay::ToastIcon::Replay);
|
||||
crate::overlay::spawn_toast("INSTANT REPLAY", "Saved to Videos", crate::overlay::ToastIcon::Replay);
|
||||
self.replay_dropdown_open = false;
|
||||
}
|
||||
|
|
@ -1080,8 +1069,10 @@ impl ScytheOverlayApp {
|
|||
if render_menu_item(ui, rec_toggle_text, accent, true) {
|
||||
let _ = ipc::send_command(Command::ToggleRecording);
|
||||
if is_recording {
|
||||
self.show_hud_notification("RECORDING", "Recording saved", crate::overlay::ToastIcon::Save);
|
||||
crate::overlay::spawn_toast("RECORDING", "Recording saved", crate::overlay::ToastIcon::Save);
|
||||
} else {
|
||||
self.show_hud_notification("RECORDING", "Recording started", crate::overlay::ToastIcon::Record);
|
||||
crate::overlay::spawn_toast("RECORDING", "Recording started", crate::overlay::ToastIcon::Record);
|
||||
}
|
||||
self.record_dropdown_open = false;
|
||||
|
|
@ -1133,8 +1124,8 @@ impl ScytheOverlayApp {
|
|||
fn render_settings_view(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||
let screen_w = ui.available_width();
|
||||
let screen_h = ui.available_height();
|
||||
let modal_w = 640.0_f32;
|
||||
let modal_h = (screen_h - 50.0).clamp(660.0, 880.0);
|
||||
let modal_w = 680.0_f32;
|
||||
let modal_h = (screen_h - 40.0).clamp(660.0, 920.0);
|
||||
let left_pad = ((screen_w - modal_w) / 2.0).max(10.0);
|
||||
let top_pad = ((screen_h - modal_h) / 2.0).max(15.0);
|
||||
let accent = self.accent_color();
|
||||
|
|
@ -1144,7 +1135,7 @@ impl ScytheOverlayApp {
|
|||
|
||||
ui.allocate_new_ui(egui::UiBuilder::new().max_rect(modal_rect), |ui| {
|
||||
egui::Frame::NONE
|
||||
.fill(Color32::from_rgba_unmultiplied(11, 11, 13, 252))
|
||||
.fill(Color32::from_rgba_unmultiplied(12, 13, 17, 210))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 160)))
|
||||
.corner_radius(CornerRadius::ZERO)
|
||||
.inner_margin(Margin::symmetric(24_i8, 20_i8))
|
||||
|
|
@ -1201,7 +1192,64 @@ impl ScytheOverlayApp {
|
|||
.auto_shrink([false, false])
|
||||
.show(ui, |ui| {
|
||||
// -----------------------------------------------------------------
|
||||
// SECTION 1: DISPLAY & CAPTURE
|
||||
// SECTION 1: SYSTEM & STARTUP
|
||||
// -----------------------------------------------------------------
|
||||
render_section_card(ui, "SYSTEM & STARTUP", accent, |ui| {
|
||||
// Autostart Instant Replay
|
||||
ui.horizontal(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.label(egui::RichText::new("Autostart Instant Replay").size(12.5).strong().color(Color32::WHITE));
|
||||
ui.label(egui::RichText::new("Automatically launch the background recording engine on login so replay is always ready.").size(10.5).color(Color32::from_rgb(150, 150, 155)));
|
||||
});
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let mut ar = self.autostart_replay;
|
||||
if toggle_switch(ui, &mut ar, accent).changed() {
|
||||
self.autostart_replay = ar;
|
||||
self.config.autostart_replay = ar;
|
||||
self.config.autostart = ar;
|
||||
let _ = self.config.save();
|
||||
if ar {
|
||||
let _ = std::process::Command::new("scythe-daemon").spawn();
|
||||
}
|
||||
self.show_hud_notification(
|
||||
"AUTOSTART REPLAY",
|
||||
if ar { "Enabled: Starts on login" } else { "Disabled" },
|
||||
crate::overlay::ToastIcon::Info,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.separator();
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Autostart HUD Overlay
|
||||
ui.horizontal(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.label(egui::RichText::new("Autostart HUD Overlay").size(12.5).strong().color(Color32::WHITE));
|
||||
ui.label(egui::RichText::new("Automatically open the HUD overlay menu when logging into your desktop session.").size(10.5).color(Color32::from_rgb(150, 150, 155)));
|
||||
});
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let mut ao = self.autostart_overlay;
|
||||
if toggle_switch(ui, &mut ao, accent).changed() {
|
||||
self.autostart_overlay = ao;
|
||||
self.config.autostart_overlay = ao;
|
||||
let _ = self.config.save();
|
||||
self.show_hud_notification(
|
||||
"AUTOSTART OVERLAY",
|
||||
if ao { "Enabled: Opens on login" } else { "Disabled" },
|
||||
crate::overlay::ToastIcon::Info,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// SECTION 2: DISPLAY & CAPTURE
|
||||
// -----------------------------------------------------------------
|
||||
render_section_card(ui, "DISPLAY & CAPTURE", accent, |ui| {
|
||||
// Record Mouse Cursor
|
||||
|
|
@ -1214,7 +1262,11 @@ impl ScytheOverlayApp {
|
|||
self.config.show_cursor = cur;
|
||||
let _ = self.config.save();
|
||||
let _ = ipc::send_command(Command::ToggleCursor);
|
||||
self.status_msg = Some((format!("Cursor {}", if cur { "Visible" } else { "Hidden" }), Instant::now()));
|
||||
self.show_hud_notification(
|
||||
"MOUSE CURSOR",
|
||||
if cur { "Visible in recording" } else { "Hidden from recording" },
|
||||
crate::overlay::ToastIcon::Cursor,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1419,18 +1471,40 @@ impl ScytheOverlayApp {
|
|||
.color(accent),
|
||||
);
|
||||
}
|
||||
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
if squared_button(ui, "Reset Hotkeys to Defaults", false, accent) {
|
||||
let old_menu = self.config.menu_hotkey.clone();
|
||||
let old_save = self.config.save_hotkey.clone();
|
||||
let old_rec = self.config.record_hotkey.clone();
|
||||
let old_cur = self.config.cursor_hotkey.clone();
|
||||
crate::hyprland_binds::unbind_hotkey(&old_menu);
|
||||
crate::hyprland_binds::unbind_hotkey(&old_save);
|
||||
crate::hyprland_binds::unbind_hotkey(&old_rec);
|
||||
crate::hyprland_binds::unbind_hotkey(&old_cur);
|
||||
self.config.menu_hotkey = "Alt+Z".to_string();
|
||||
self.config.save_hotkey = "Ctrl+Shift+R".to_string();
|
||||
self.config.record_hotkey = "Ctrl+Shift+F9".to_string();
|
||||
self.config.cursor_hotkey = "Ctrl+Shift+F10".to_string();
|
||||
let _ = self.config.save();
|
||||
crate::hyprland_binds::register_hyprland_binds(&self.config);
|
||||
crate::config::ScytheConfig::notify_daemon_reload();
|
||||
self.show_hud_notification("KEYBINDS", "Restored default hotkeys", crate::overlay::ToastIcon::Info);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// SECTION 4: STORAGE & DESTINATION
|
||||
// SECTION 5: STORAGE & FILE NAMING
|
||||
// -----------------------------------------------------------------
|
||||
render_section_card(ui, "STORAGE & DESTINATION", accent, |ui| {
|
||||
render_section_card(ui, "STORAGE & FILE NAMING", accent, |ui| {
|
||||
ui.label(egui::RichText::new("Save Folder:").size(12.0).strong().color(Color32::WHITE));
|
||||
ui.add_space(3.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::TextEdit::singleline(&mut self.output_dir).desired_width(320.0));
|
||||
ui.add(egui::TextEdit::singleline(&mut self.output_dir).desired_width(340.0));
|
||||
if squared_button(ui, "Change", false, accent) {
|
||||
pick_folder(&self.output_dir, self.folder_tx.clone(), self.folder_picking_active.clone());
|
||||
}
|
||||
|
|
@ -1438,6 +1512,29 @@ impl ScytheOverlayApp {
|
|||
open_folder(&ScytheConfig::expand_tilde(&self.output_dir));
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.separator();
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.label(egui::RichText::new("Default File Naming Format:").size(12.0).strong().color(Color32::WHITE));
|
||||
ui.add_space(3.0);
|
||||
let sample_replay = ScytheConfig::format_video_filename("Replay", "mp4");
|
||||
let sample_record = ScytheConfig::format_video_filename("Recording", "mp4");
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("Template:").size(11.0).color(Color32::from_rgb(150, 150, 155)));
|
||||
ui.label(egui::RichText::new("Prefix-Timestamp_Date_Month_Year.mp4").size(11.0).font(FontId::monospace(11.0)).color(accent));
|
||||
});
|
||||
ui.add_space(2.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("Instant Replay:").size(11.0).color(Color32::from_rgb(150, 150, 155)));
|
||||
ui.label(egui::RichText::new(&sample_replay).size(11.0).font(FontId::monospace(11.0)).color(Color32::WHITE));
|
||||
});
|
||||
ui.add_space(2.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("Manual Record:").size(11.0).color(Color32::from_rgb(150, 150, 155)));
|
||||
ui.label(egui::RichText::new(&sample_record).size(11.0).font(FontId::monospace(11.0)).color(Color32::WHITE));
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
|
@ -1482,7 +1579,7 @@ impl ScytheOverlayApp {
|
|||
if ui.add(btn).clicked() {
|
||||
self.config.accent_color = id.to_string();
|
||||
let _ = self.config.save();
|
||||
self.status_msg = Some((format!("Accent: {}", name), Instant::now()));
|
||||
self.show_hud_notification("THEME ACCENT", &format!("Selected: {}", name), crate::overlay::ToastIcon::Info);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -1566,41 +1663,75 @@ impl ScytheOverlayApp {
|
|||
|
||||
ui.add_space(14.0);
|
||||
|
||||
// APPLY & SAVE BUTTON
|
||||
let apply_btn = egui::Button::new(
|
||||
egui::RichText::new("APPLY & SAVE SETTINGS")
|
||||
.font(FontId::monospace(13.0))
|
||||
.strong()
|
||||
.color(Color32::from_rgb(11, 18, 4)),
|
||||
)
|
||||
.fill(accent)
|
||||
.stroke(Stroke::NONE)
|
||||
.corner_radius(CornerRadius::ZERO)
|
||||
.min_size(Vec2::new(ui.available_width(), 40.0));
|
||||
// BOTTOM ACTIONS (RESET DEFAULTS + APPLY & SAVE)
|
||||
ui.horizontal(|ui| {
|
||||
let reset_btn = egui::Button::new(
|
||||
egui::RichText::new("RESET DEFAULTS")
|
||||
.font(FontId::monospace(11.5))
|
||||
.color(Color32::from_rgb(180, 180, 190)),
|
||||
)
|
||||
.fill(Color32::from_rgba_unmultiplied(255, 255, 255, 14))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 28)))
|
||||
.corner_radius(CornerRadius::ZERO)
|
||||
.min_size(Vec2::new(135.0, 38.0));
|
||||
|
||||
if ui.add(apply_btn).clicked() {
|
||||
self.config.show_cursor = self.show_cursor;
|
||||
self.config.fps = self.target_fps;
|
||||
self.config.record_bitrate_kbps = self.bitrate_mbps * 1000;
|
||||
self.config.replay_bitrate_kbps = self.bitrate_mbps * 1000;
|
||||
self.config.video_codec = self.video_codec.clone();
|
||||
self.config.replay_duration_sec = self.replay_sec;
|
||||
self.config.output_directory = self.output_dir.clone();
|
||||
self.config.mic_volume = self.mic_volume_pct as f32 / 100.0;
|
||||
self.config.system_volume = self.system_volume_pct as f32 / 100.0;
|
||||
self.config.audio_mode = match self.audio_mode_idx {
|
||||
1 => "mic".to_string(),
|
||||
2 => "both".to_string(),
|
||||
3 => "muted".to_string(),
|
||||
_ => "system".to_string(),
|
||||
};
|
||||
self.config.auto_check_updates = self.auto_check_updates;
|
||||
let _ = self.config.save();
|
||||
crate::hyprland_binds::register_hyprland_binds(&self.config);
|
||||
crate::config::ScytheConfig::notify_daemon_reload();
|
||||
self.status_msg = Some(("Settings Saved & Applied!".to_string(), Instant::now()));
|
||||
self.switch_view(ShadowPlayView::MainHud, ctx);
|
||||
}
|
||||
if ui.add(reset_btn).clicked() {
|
||||
let def = ScytheConfig::default();
|
||||
self.target_fps = def.fps;
|
||||
self.fps_input_str = def.fps.to_string();
|
||||
self.bitrate_mbps = def.record_bitrate_kbps / 1000;
|
||||
self.bitrate_input_str = self.bitrate_mbps.to_string();
|
||||
self.replay_sec = def.replay_duration_sec;
|
||||
self.replay_sec_input_str = def.replay_duration_sec.to_string();
|
||||
self.video_codec = def.video_codec;
|
||||
self.show_cursor = def.show_cursor;
|
||||
self.mic_volume_pct = (def.mic_volume * 100.0) as u32;
|
||||
self.system_volume_pct = (def.system_volume * 100.0) as u32;
|
||||
self.autostart_replay = def.autostart_replay;
|
||||
self.autostart_overlay = def.autostart_overlay;
|
||||
self.show_hud_notification("DEFAULTS", "Settings restored to defaults", crate::overlay::ToastIcon::Info);
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
let apply_btn = egui::Button::new(
|
||||
egui::RichText::new("APPLY & SAVE SETTINGS")
|
||||
.font(FontId::monospace(12.5))
|
||||
.strong()
|
||||
.color(Color32::from_rgb(11, 18, 4)),
|
||||
)
|
||||
.fill(accent)
|
||||
.stroke(Stroke::NONE)
|
||||
.corner_radius(CornerRadius::ZERO)
|
||||
.min_size(Vec2::new(ui.available_width(), 38.0));
|
||||
|
||||
if ui.add(apply_btn).clicked() {
|
||||
self.config.show_cursor = self.show_cursor;
|
||||
self.config.fps = self.target_fps;
|
||||
self.config.record_bitrate_kbps = self.bitrate_mbps * 1000;
|
||||
self.config.replay_bitrate_kbps = self.bitrate_mbps * 1000;
|
||||
self.config.video_codec = self.video_codec.clone();
|
||||
self.config.replay_duration_sec = self.replay_sec;
|
||||
self.config.output_directory = self.output_dir.clone();
|
||||
self.config.mic_volume = self.mic_volume_pct as f32 / 100.0;
|
||||
self.config.system_volume = self.system_volume_pct as f32 / 100.0;
|
||||
self.config.audio_mode = match self.audio_mode_idx {
|
||||
1 => "mic".to_string(),
|
||||
2 => "both".to_string(),
|
||||
3 => "muted".to_string(),
|
||||
_ => "system".to_string(),
|
||||
};
|
||||
self.config.auto_check_updates = self.auto_check_updates;
|
||||
self.config.autostart_replay = self.autostart_replay;
|
||||
self.config.autostart_overlay = self.autostart_overlay;
|
||||
self.config.autostart = self.autostart_replay;
|
||||
let _ = self.config.save();
|
||||
crate::hyprland_binds::register_hyprland_binds(&self.config);
|
||||
crate::config::ScytheConfig::notify_daemon_reload();
|
||||
self.show_hud_notification("SETTINGS", "Settings Saved & Applied!", crate::overlay::ToastIcon::Info);
|
||||
self.switch_view(ShadowPlayView::MainHud, ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1619,7 +1750,7 @@ impl ScytheOverlayApp {
|
|||
|
||||
ui.allocate_new_ui(egui::UiBuilder::new().max_rect(modal_rect), |ui| {
|
||||
egui::Frame::NONE
|
||||
.fill(Color32::from_rgba_unmultiplied(11, 11, 13, 252))
|
||||
.fill(Color32::from_rgba_unmultiplied(12, 13, 17, 210))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 160)))
|
||||
.corner_radius(CornerRadius::ZERO)
|
||||
.inner_margin(Margin::symmetric(20_i8, 16_i8))
|
||||
|
|
@ -1865,6 +1996,115 @@ impl ScytheOverlayApp {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn render_slide_notification(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||
let accent = self.accent_color();
|
||||
if let Some(notif) = &self.hud_notification {
|
||||
let elapsed = notif.start_time.elapsed().as_secs_f32();
|
||||
if elapsed < notif.duration_secs {
|
||||
ctx.request_repaint(); // 60 FPS animation
|
||||
|
||||
let slide_x = if elapsed < 0.35 {
|
||||
let t = (elapsed / 0.35).min(1.0);
|
||||
let ease = 1.0 - (1.0 - t).powi(3);
|
||||
(1.0 - ease) * 360.0
|
||||
} else if elapsed < notif.duration_secs - 0.40 {
|
||||
0.0
|
||||
} else {
|
||||
let t = ((elapsed - (notif.duration_secs - 0.40)) / 0.40).min(1.0);
|
||||
let ease = t.powi(3);
|
||||
ease * 360.0
|
||||
};
|
||||
|
||||
let screen_w = ui.available_width();
|
||||
let card_w = 320.0;
|
||||
let card_h = 56.0;
|
||||
let toast_rect = egui::Rect::from_min_size(
|
||||
egui::pos2(screen_w - card_w - 24.0 + slide_x, 24.0),
|
||||
egui::vec2(card_w, card_h),
|
||||
);
|
||||
|
||||
let painter = ui.painter();
|
||||
|
||||
// Drop shadow
|
||||
painter.rect_filled(
|
||||
toast_rect.translate(Vec2::new(0.0, 4.0)),
|
||||
CornerRadius::ZERO,
|
||||
Color32::from_rgba_unmultiplied(0, 0, 0, 120),
|
||||
);
|
||||
|
||||
// Translucent dark slate toast card
|
||||
painter.rect(
|
||||
toast_rect,
|
||||
CornerRadius::ZERO,
|
||||
Color32::from_rgba_unmultiplied(12, 13, 17, 210),
|
||||
Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 28)),
|
||||
egui::StrokeKind::Inside,
|
||||
);
|
||||
|
||||
// Left vertical accent bar
|
||||
let line_color = if notif.icon == crate::overlay::ToastIcon::Record {
|
||||
Color32::from_rgb(239, 68, 68)
|
||||
} else {
|
||||
accent
|
||||
};
|
||||
let accent_line = egui::Rect::from_min_size(toast_rect.min, Vec2::new(3.5, toast_rect.height()));
|
||||
painter.rect_filled(accent_line, CornerRadius::ZERO, line_color);
|
||||
|
||||
// Left icon area (32x32 at center-left)
|
||||
let icon_center = egui::pos2(toast_rect.left() + 24.0, toast_rect.center().y);
|
||||
match notif.icon {
|
||||
crate::overlay::ToastIcon::Replay => {
|
||||
draw_replay_icon(painter, icon_center, 9.5, true, accent);
|
||||
}
|
||||
crate::overlay::ToastIcon::Record => {
|
||||
painter.circle_filled(icon_center, 5.5, Color32::from_rgb(239, 68, 68));
|
||||
painter.circle_stroke(icon_center, 9.5, Stroke::new(1.2_f32, Color32::from_rgba_unmultiplied(239, 68, 68, 120)));
|
||||
}
|
||||
crate::overlay::ToastIcon::Save => {
|
||||
let c = icon_center;
|
||||
let p1 = c + Vec2::new(-5.5, 0.0);
|
||||
let p2 = c + Vec2::new(-1.5, 4.0);
|
||||
let p3 = c + Vec2::new(5.5, -4.0);
|
||||
painter.line_segment([p1, p2], Stroke::new(2.2_f32, accent));
|
||||
painter.line_segment([p2, p3], Stroke::new(2.2_f32, accent));
|
||||
}
|
||||
crate::overlay::ToastIcon::Cursor => {
|
||||
let c = icon_center;
|
||||
painter.text(c, egui::Align2::CENTER_CENTER, "⯈", FontId::proportional(14.0), accent);
|
||||
}
|
||||
crate::overlay::ToastIcon::Error => {
|
||||
let c = icon_center;
|
||||
let red = Color32::from_rgb(239, 68, 68);
|
||||
painter.line_segment([c + Vec2::new(-4.0, -4.0), c + Vec2::new(4.0, 4.0)], Stroke::new(2.0_f32, red));
|
||||
painter.line_segment([c + Vec2::new(4.0, -4.0), c + Vec2::new(-4.0, 4.0)], Stroke::new(2.0_f32, red));
|
||||
}
|
||||
crate::overlay::ToastIcon::Info => {
|
||||
painter.circle_filled(icon_center, 5.0, accent);
|
||||
}
|
||||
}
|
||||
|
||||
// Text stack (Title + Subtitle)
|
||||
let text_left = toast_rect.left() + 48.0;
|
||||
painter.text(
|
||||
egui::pos2(text_left, toast_rect.top() + 19.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
¬if.title,
|
||||
FontId::monospace(12.0),
|
||||
Color32::WHITE,
|
||||
);
|
||||
painter.text(
|
||||
egui::pos2(text_left, toast_rect.top() + 37.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
¬if.subtitle,
|
||||
FontId::proportional(10.5),
|
||||
Color32::from_rgb(160, 160, 168),
|
||||
);
|
||||
} else {
|
||||
self.hud_notification = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for ScytheOverlayApp {
|
||||
|
|
@ -1948,7 +2188,7 @@ impl eframe::App for ScytheOverlayApp {
|
|||
let _ = self.config.save();
|
||||
crate::hyprland_binds::register_hyprland_binds(&self.config);
|
||||
crate::config::ScytheConfig::notify_daemon_reload();
|
||||
self.status_msg = Some((format!("Bound {}: {}", action_name, combo), Instant::now()));
|
||||
self.show_hud_notification("KEYBIND", &format!("Bound {}: {}", action_name, combo), crate::overlay::ToastIcon::Info);
|
||||
self.listening_keybind = None;
|
||||
}
|
||||
}
|
||||
|
|
@ -1990,10 +2230,13 @@ impl eframe::App for ScytheOverlayApp {
|
|||
// Background screen darkening scrim (rgba 0, 0, 0, 115 gives ~45% dimming of background)
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame::NONE.fill(Color32::from_rgba_unmultiplied(0, 0, 0, 115)))
|
||||
.show(ctx, |ui| match self.current_view {
|
||||
ShadowPlayView::MainHud => self.render_main_hud(ctx, ui),
|
||||
ShadowPlayView::Settings => self.render_settings_view(ctx, ui),
|
||||
ShadowPlayView::Gallery => self.render_gallery_view(ctx, ui),
|
||||
.show(ctx, |ui| {
|
||||
match self.current_view {
|
||||
ShadowPlayView::MainHud => self.render_main_hud(ctx, ui),
|
||||
ShadowPlayView::Settings => self.render_settings_view(ctx, ui),
|
||||
ShadowPlayView::Gallery => self.render_gallery_view(ctx, ui),
|
||||
}
|
||||
self.render_slide_notification(ctx, ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -2033,11 +2276,25 @@ pub struct ShadowPlayToastApp {
|
|||
|
||||
impl eframe::App for ShadowPlayToastApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
if self.created_at.elapsed() >= self.duration {
|
||||
let elapsed = self.created_at.elapsed().as_secs_f32();
|
||||
let total_dur = self.duration.as_secs_f32();
|
||||
if elapsed >= total_dur {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
std::process::exit(0);
|
||||
}
|
||||
ctx.request_repaint_after(Duration::from_millis(50));
|
||||
ctx.request_repaint();
|
||||
|
||||
let slide_x = if elapsed < 0.35 {
|
||||
let t = (elapsed / 0.35).min(1.0);
|
||||
let ease = 1.0 - (1.0 - t).powi(3);
|
||||
(1.0 - ease) * 340.0
|
||||
} else if elapsed < total_dur - 0.40 {
|
||||
0.0
|
||||
} else {
|
||||
let t = ((elapsed - (total_dur - 0.40)) / 0.40).min(1.0);
|
||||
let ease = t.powi(3);
|
||||
ease * 340.0
|
||||
};
|
||||
|
||||
let mut visuals = egui::Visuals::dark();
|
||||
visuals.panel_fill = Color32::TRANSPARENT;
|
||||
|
|
@ -2047,7 +2304,10 @@ impl eframe::App for ShadowPlayToastApp {
|
|||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame::NONE.fill(Color32::TRANSPARENT))
|
||||
.show(ctx, |ui| {
|
||||
let (rect, _) = ui.allocate_exact_size(Vec2::new(320.0, 56.0), egui::Sense::hover());
|
||||
let rect = egui::Rect::from_min_size(
|
||||
egui::pos2(slide_x + 10.0, 4.0),
|
||||
Vec2::new(320.0, 56.0),
|
||||
);
|
||||
let painter = ui.painter();
|
||||
|
||||
// Drop shadow
|
||||
|
|
@ -2061,7 +2321,7 @@ impl eframe::App for ShadowPlayToastApp {
|
|||
painter.rect(
|
||||
rect,
|
||||
CornerRadius::ZERO,
|
||||
Color32::from_rgba_unmultiplied(10, 10, 12, 248),
|
||||
Color32::from_rgba_unmultiplied(12, 13, 17, 210),
|
||||
Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 28)),
|
||||
egui::StrokeKind::Inside,
|
||||
);
|
||||
|
|
@ -2114,7 +2374,7 @@ impl eframe::App for ShadowPlayToastApp {
|
|||
egui::pos2(text_left, rect.top() + 19.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
&self.title,
|
||||
FontId::proportional(12.0),
|
||||
FontId::monospace(12.0),
|
||||
Color32::WHITE,
|
||||
);
|
||||
painter.text(
|
||||
|
|
@ -2132,8 +2392,8 @@ pub fn run_egui_toast(title: &str, subtitle: &str, icon: crate::overlay::ToastIc
|
|||
let cfg = ScytheConfig::load();
|
||||
let accent = resolve_accent_color(&cfg.accent_color);
|
||||
|
||||
let toast_w = 320.0;
|
||||
let toast_h = 56.0;
|
||||
let toast_w = 340.0;
|
||||
let toast_h = 64.0;
|
||||
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
|
|
@ -2153,7 +2413,7 @@ pub fn run_egui_toast(title: &str, subtitle: &str, icon: crate::overlay::ToastIc
|
|||
icon,
|
||||
accent,
|
||||
created_at: Instant::now(),
|
||||
duration: Duration::from_millis(2600),
|
||||
duration: Duration::from_millis(2800),
|
||||
};
|
||||
|
||||
let _ = eframe::run_native(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue