Fix Windows overlay transparency, global hotkeys, toast duplication, and file dialogs
Some checks are pending
Release & Packaging / Build Linux Packages (.tar.gz, .deb, .rpm, .AppImage) (push) Waiting to run
Release & Packaging / Build Windows Binaries (.exe) (push) Waiting to run
Release & Packaging / Publish GitHub Release (push) Blocked by required conditions
Some checks are pending
Release & Packaging / Build Linux Packages (.tar.gz, .deb, .rpm, .AppImage) (push) Waiting to run
Release & Packaging / Build Windows Binaries (.exe) (push) Waiting to run
Release & Packaging / Publish GitHub Release (push) Blocked by required conditions
This commit is contained in:
parent
3d5496d4ab
commit
0749bb2e61
8 changed files with 201 additions and 73 deletions
22
.github/workflows/release.yml
vendored
22
.github/workflows/release.yml
vendored
|
|
@ -143,6 +143,8 @@ jobs:
|
|||
cp target/release/scythe-daemon.exe dist/bundle/
|
||||
cp target/release/scythe-ui.exe dist/bundle/
|
||||
python3 packaging/windows/bundle_deps.py dist/bundle
|
||||
strip dist/bundle/*.exe || true
|
||||
strip --strip-unneeded dist/bundle/*.dll || true
|
||||
|
||||
- name: Package Windows Standalone (.exe), Installer, and Zip
|
||||
shell: pwsh
|
||||
|
|
@ -181,19 +183,19 @@ jobs:
|
|||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.ref_name != 'main' && github.ref_name || 'v0.2.1' }}
|
||||
name: "Scythe ${{ github.ref_name != 'main' && github.ref_name || 'v0.2.1' }}"
|
||||
tag_name: ${{ github.ref_name != 'main' && github.ref_name || 'v0.2.2' }}
|
||||
name: "Scythe ${{ github.ref_name != 'main' && github.ref_name || 'v0.2.2' }}"
|
||||
body: |
|
||||
High-performance GPU hardware screen recorder and instant replay overlay for Linux and Windows.
|
||||
|
||||
### What's New in v0.2.1
|
||||
- **Windows Setup Installer (`scythe-setup.exe`)**: Fixed DXGI adapter initialization and DirectX 11 hardware capture pipeline on Windows. Full Windows NSIS installer with desktop/start menu integration.
|
||||
- **System Autostart Options**: Toggle automatic startup on system login for both the overlay HUD and background replay buffer.
|
||||
- **Clean Sequential Trimmer Exports**: Cleaned export naming to `clip_trimmed.mp4`, `clip_trimmed_1.mp4` avoiding repetitive suffixes.
|
||||
- **Settings UI Organization**: Dedicated grouped sections for System & Startup, Storage & File Naming with real-time pattern preview, and hotkey defaults reset.
|
||||
- **Auto-Update System**: Integrated background release checker with voluntary update notifications in the HUD and dedicated settings manager.
|
||||
- **Standardized Datetime Filenames**: Clips and replays now save with clear human-readable timestamps (`Replay-HH-MM-SS_DD-MM-YYYY.mp4` / `Recording-HH-MM-SS_DD-MM-YYYY.mp4`).
|
||||
- **NVIDIA ShadowPlay Toast Overlay**: Sleek, non-intrusive floating toast notifications in the top-right corner for Replay Saved, Recording Started/Stopped, and Mouse Cursor toggle.
|
||||
### What's New in v0.2.2
|
||||
- **Windows Overlay Transparency**: Fixed DWM opacity issue where the overlay showed an opaque black screen instead of a translucent frosted background.
|
||||
- **Windows Global Hotkeys**: Added dedicated Win32 message pump so global hotkeys (`Alt+Z`, `Ctrl+Shift+R`, etc.) register and trigger reliably across all Windows applications and games.
|
||||
- **Dynamic Hotkey Reloading**: Changes to hotkey bindings in Settings take effect immediately in the background without needing a restart.
|
||||
- **Fixed Notification Duplication & Hang**: Eliminated duplicate in-HUD toast instances and fixed toast freeze issues by ensuring isolated processes with auto-closing watchdog timers.
|
||||
- **Working Folder Options**: Fixed 'Open' and 'Change' folder dialogs on Windows with top-most STA PowerShell picker and visible Explorer launching.
|
||||
- **Zero-Latency UI Buttons & Key Recording**: All daemon communication from UI buttons is now asynchronous, and key listening repaints with 0ms delay.
|
||||
- **Reduced Windows Installer Size**: Enabled Solid LZMA compression in NSIS and stripped symbols from Windows binaries and DLLs.
|
||||
|
||||
### Packages
|
||||
- Windows: `scythe-setup.exe` (Official installer with Start Menu & Desktop shortcuts)
|
||||
|
|
|
|||
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -3700,7 +3700,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "scythe"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"ashpd",
|
||||
"cpal",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "scythe"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
!include "MUI2.nsh"
|
||||
!include "FileFunc.nsh"
|
||||
|
||||
SetCompressor /SOLID lzma
|
||||
SetCompressorDictSize 64
|
||||
|
||||
Name "Scythe"
|
||||
OutFile "..\..\dist\scythe-setup.exe"
|
||||
InstallDir "$LOCALAPPDATA\Programs\scythe"
|
||||
|
|
|
|||
|
|
@ -218,15 +218,12 @@ fn ensure_hotkeys_running() {
|
|||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
unsafe {
|
||||
use windows::Win32::Foundation::{CloseHandle, GetLastError, WIN32_ERROR};
|
||||
use windows::Win32::System::Threading::CreateMutexW;
|
||||
use windows::Win32::Foundation::CloseHandle;
|
||||
use windows::Win32::System::Threading::{OpenMutexW, SYNCHRONIZATION_ACCESS_RIGHTS};
|
||||
|
||||
if let Ok(handle) = CreateMutexW(None, false, windows::core::w!("Global\\scythe_hotkeys_single_instance")) {
|
||||
if GetLastError() == WIN32_ERROR(183) { // ERROR_ALREADY_EXISTS
|
||||
let _ = CloseHandle(handle);
|
||||
return;
|
||||
}
|
||||
if let Ok(handle) = OpenMutexW(SYNCHRONIZATION_ACCESS_RIGHTS(0x00100000), false, windows::core::w!("Global\\scythe_hotkeys_single_instance")) {
|
||||
let _ = CloseHandle(handle);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -318,7 +315,7 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
let title = args.get(2).map(|s| s.as_str()).unwrap_or("SCYTHE");
|
||||
let subtitle = args.get(3).map(|s| s.as_str()).unwrap_or("");
|
||||
let icon_name = args.get(4).map(|s| s.as_str()).unwrap_or("info");
|
||||
show_shadowplay_toast(title, subtitle, ToastIcon::from_name(icon_name));
|
||||
scythe::overlay_egui::run_egui_toast(title, subtitle, ToastIcon::from_name(icon_name));
|
||||
return Ok(());
|
||||
}
|
||||
"--menu" => {
|
||||
|
|
@ -463,35 +460,108 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
return Ok(());
|
||||
}
|
||||
};
|
||||
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);
|
||||
let hotkey_cursor = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F10);
|
||||
|
||||
let is_on_hyprland = scythe::hyprland_binds::is_hyprland();
|
||||
if !is_on_hyprland {
|
||||
let _ = manager.register(hotkey_menu);
|
||||
}
|
||||
let _ = manager.register(hotkey_save);
|
||||
let _ = manager.register(hotkey_record);
|
||||
let _ = manager.register(hotkey_cursor);
|
||||
let mut current_config = scythe::config::ScytheConfig::load();
|
||||
let mut registered_hotkeys: Vec<HotKey> = Vec::new();
|
||||
|
||||
println!("Listening for global hotkeys (Alt+Z for overlay, Ctrl+Shift+R for replay, Ctrl+Shift+F9 for recording, Ctrl+Shift+F10 for cursor)...");
|
||||
let register_all = |mgr: &GlobalHotKeyManager, cfg: &scythe::config::ScytheConfig, reg: &mut Vec<HotKey>| -> (Option<u32>, Option<u32>, Option<u32>, Option<u32>) {
|
||||
for hk in reg.drain(..) {
|
||||
let _ = mgr.unregister(hk);
|
||||
}
|
||||
let is_on_hyprland = scythe::hyprland_binds::is_hyprland();
|
||||
let m_menu = scythe::hotkey::parse_hotkey(&cfg.menu_hotkey).or_else(|| Some(HotKey::new(Some(Modifiers::ALT), Code::KeyZ)));
|
||||
let m_save = scythe::hotkey::parse_hotkey(&cfg.save_hotkey).or_else(|| Some(HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyR)));
|
||||
let m_rec = scythe::hotkey::parse_hotkey(&cfg.record_hotkey).or_else(|| Some(HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F9)));
|
||||
let m_cur = scythe::hotkey::parse_hotkey(&cfg.cursor_hotkey).or_else(|| Some(HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F10)));
|
||||
|
||||
let receiver = GlobalHotKeyEvent::receiver();
|
||||
loop {
|
||||
if let Ok(event) = receiver.try_recv() {
|
||||
if event.id == hotkey_menu.id() {
|
||||
ensure_daemon_running();
|
||||
let _ = get_ui_cmd().arg("--menu").spawn();
|
||||
} else if event.id == hotkey_save.id() {
|
||||
let _ = send_with_notification(Command::SaveReplay, "INSTANT REPLAY", "Saved to Videos", ToastIcon::Replay);
|
||||
} else if event.id == hotkey_record.id() {
|
||||
let _ = handle_toggle_recording();
|
||||
} else if event.id == hotkey_cursor.id() {
|
||||
let _ = handle_toggle_cursor();
|
||||
let mut id_menu = None;
|
||||
let mut id_save = None;
|
||||
let mut id_rec = None;
|
||||
let mut id_cur = None;
|
||||
|
||||
if !is_on_hyprland {
|
||||
if let Some(hk) = m_menu {
|
||||
id_menu = Some(hk.id());
|
||||
let _ = mgr.register(hk);
|
||||
reg.push(hk);
|
||||
}
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
if let Some(hk) = m_save {
|
||||
id_save = Some(hk.id());
|
||||
let _ = mgr.register(hk);
|
||||
reg.push(hk);
|
||||
}
|
||||
if let Some(hk) = m_rec {
|
||||
id_rec = Some(hk.id());
|
||||
let _ = mgr.register(hk);
|
||||
reg.push(hk);
|
||||
}
|
||||
if let Some(hk) = m_cur {
|
||||
id_cur = Some(hk.id());
|
||||
let _ = mgr.register(hk);
|
||||
reg.push(hk);
|
||||
}
|
||||
(id_menu, id_save, id_rec, id_cur)
|
||||
};
|
||||
|
||||
let (mut id_menu, mut id_save, mut id_rec, mut id_cur) = register_all(&manager, ¤t_config, &mut registered_hotkeys);
|
||||
|
||||
println!("Listening for global hotkeys (Overlay: {}, Replay: {}, Record: {}, Cursor: {})...",
|
||||
current_config.menu_hotkey, current_config.save_hotkey, current_config.record_hotkey, current_config.cursor_hotkey);
|
||||
|
||||
let receiver = GlobalHotKeyEvent::receiver();
|
||||
let mut last_config_check = std::time::Instant::now();
|
||||
|
||||
loop {
|
||||
#[cfg(target_os = "windows")]
|
||||
unsafe {
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
PeekMessageW, TranslateMessage, DispatchMessageW, MSG, PM_REMOVE,
|
||||
};
|
||||
let mut msg = MSG::default();
|
||||
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
}
|
||||
|
||||
if last_config_check.elapsed() > std::time::Duration::from_millis(800) {
|
||||
last_config_check = std::time::Instant::now();
|
||||
let new_cfg = scythe::config::ScytheConfig::load();
|
||||
if new_cfg.menu_hotkey != current_config.menu_hotkey
|
||||
|| new_cfg.save_hotkey != current_config.save_hotkey
|
||||
|| new_cfg.record_hotkey != current_config.record_hotkey
|
||||
|| new_cfg.cursor_hotkey != current_config.cursor_hotkey
|
||||
{
|
||||
current_config = new_cfg;
|
||||
let (im, is, ir, ic) = register_all(&manager, ¤t_config, &mut registered_hotkeys);
|
||||
id_menu = im;
|
||||
id_save = is;
|
||||
id_rec = ir;
|
||||
id_cur = ic;
|
||||
}
|
||||
}
|
||||
|
||||
while let Ok(event) = receiver.try_recv() {
|
||||
if Some(event.id) == id_menu {
|
||||
std::thread::spawn(|| {
|
||||
ensure_daemon_running();
|
||||
let _ = get_ui_cmd().arg("--menu").spawn();
|
||||
});
|
||||
} else if Some(event.id) == id_save {
|
||||
std::thread::spawn(|| {
|
||||
let _ = send_with_notification(Command::SaveReplay, "INSTANT REPLAY", "Saved to Videos", ToastIcon::Replay);
|
||||
});
|
||||
} else if Some(event.id) == id_rec {
|
||||
std::thread::spawn(|| {
|
||||
let _ = handle_toggle_recording();
|
||||
});
|
||||
} else if Some(event.id) == id_cur {
|
||||
std::thread::spawn(|| {
|
||||
let _ = handle_toggle_cursor();
|
||||
});
|
||||
}
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -291,7 +291,9 @@ X-GNOME-Autostart-enabled=true\n";
|
|||
}
|
||||
|
||||
pub fn notify_daemon_reload() {
|
||||
let _ = crate::ipc::send_command(crate::ipc::Command::ReloadConfig);
|
||||
std::thread::spawn(|| {
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ pub fn show_notification(message: &str) {
|
|||
pub fn show_shadowplay_toast(title: &str, subtitle: &str, icon: ToastIcon) {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
crate::overlay_egui::run_egui_toast(title, subtitle, icon);
|
||||
spawn_toast(title, subtitle, icon);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -209,10 +209,10 @@ fn open_folder(path: &std::path::Path) {
|
|||
std::thread::spawn(move || {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
let _ = std::process::Command::new("explorer")
|
||||
.arg(&p)
|
||||
.creation_flags(0x08000000)
|
||||
let p_str = p.to_string_lossy().replace('/', "\\");
|
||||
let _ = std::fs::create_dir_all(&p_str);
|
||||
let _ = std::process::Command::new("explorer.exe")
|
||||
.arg(&p_str)
|
||||
.spawn();
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
|
|
@ -234,17 +234,22 @@ fn pick_folder(current_dir: &str, tx: Sender<String>, is_active: Arc<AtomicBool>
|
|||
std::thread::spawn(move || {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
let clean_cur = cur.replace('/', "\\").replace('\'', "''");
|
||||
let script = format!(
|
||||
"[System.Reflection.Assembly]::LoadWithPartialName(\x27System.windows.forms\x27) | Out-Null; \
|
||||
"Add-Type -AssemblyName System.Windows.Forms; \
|
||||
$f = New-Object System.Windows.Forms.FolderBrowserDialog; \
|
||||
$f.SelectedPath = \x27{}\x27; \
|
||||
if ($f.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {{ Write-Host $f.SelectedPath }}",
|
||||
cur.replace("\x27", "\x27\x27")
|
||||
$f.Description = 'Select Recordings Directory'; \
|
||||
$f.SelectedPath = '{}'; \
|
||||
$f.ShowNewFolderButton = $true; \
|
||||
$top = New-Object System.Windows.Forms.Form; \
|
||||
$top.TopMost = $true; \
|
||||
if ($f.ShowDialog($top) -eq [System.Windows.Forms.DialogResult]::OK) {{ \
|
||||
Write-Output $f.SelectedPath \
|
||||
}}",
|
||||
clean_cur
|
||||
);
|
||||
if let Ok(out) = std::process::Command::new("powershell")
|
||||
.args(["-NoProfile", "-WindowStyle", "Hidden", "-Command", &script])
|
||||
.creation_flags(0x08000000)
|
||||
if let Ok(out) = std::process::Command::new("powershell.exe")
|
||||
.args(["-NoProfile", "-STA", "-Command", &script])
|
||||
.output()
|
||||
{
|
||||
let sel = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
|
|
@ -252,6 +257,7 @@ fn pick_folder(current_dir: &str, tx: Sender<String>, is_active: Arc<AtomicBool>
|
|||
let _ = tx.send(sel);
|
||||
}
|
||||
}
|
||||
flag.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
|
|
@ -295,6 +301,13 @@ fn pick_folder(current_dir: &str, tx: Sender<String>, is_active: Arc<AtomicBool>
|
|||
});
|
||||
}
|
||||
|
||||
// Asynchronous daemon command dispatcher to avoid blocking the egui render loop
|
||||
fn async_send_command(cmd: Command) {
|
||||
std::thread::spawn(move || {
|
||||
let _ = ipc::send_command(cmd);
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to render mechanical keyboard keycap badges
|
||||
#[allow(dead_code)]
|
||||
fn render_keycap(ui: &mut egui::Ui, text: &str) {
|
||||
|
|
@ -790,7 +803,7 @@ impl ScytheOverlayApp {
|
|||
if let Ok(s) = ipc::query_status() {
|
||||
let _ = status_tx.send(s);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
std::thread::sleep(Duration::from_millis(150));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -1018,9 +1031,8 @@ impl ScytheOverlayApp {
|
|||
self.replay_dropdown_open = false;
|
||||
}
|
||||
if render_menu_item(ui, "Save Replay", accent, true) {
|
||||
let _ = ipc::send_command(Command::SaveReplay);
|
||||
async_send_command(Command::SaveReplay);
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
|
@ -1067,13 +1079,11 @@ impl ScytheOverlayApp {
|
|||
render_dropdown_menu(ui, card_w, accent, |ui| {
|
||||
let rec_toggle_text = if is_recording { "Stop Recording" } else { "Start Recording" };
|
||||
if render_menu_item(ui, rec_toggle_text, accent, true) {
|
||||
let _ = ipc::send_command(Command::ToggleRecording);
|
||||
async_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;
|
||||
}
|
||||
|
|
@ -1261,7 +1271,7 @@ impl ScytheOverlayApp {
|
|||
self.show_cursor = cur;
|
||||
self.config.show_cursor = cur;
|
||||
let _ = self.config.save();
|
||||
let _ = ipc::send_command(Command::ToggleCursor);
|
||||
async_send_command(Command::ToggleCursor);
|
||||
self.show_hud_notification(
|
||||
"MOUSE CURSOR",
|
||||
if cur { "Visible in recording" } else { "Hidden from recording" },
|
||||
|
|
@ -2108,12 +2118,20 @@ impl ScytheOverlayApp {
|
|||
}
|
||||
|
||||
impl eframe::App for ScytheOverlayApp {
|
||||
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
|
||||
[0.0, 0.0, 0.0, 0.0]
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
self.poll_async_events();
|
||||
self.mic_vu = (self.mic_vu * 0.94).max(0.0);
|
||||
self.sys_vu = (self.sys_vu * 0.94).max(0.0);
|
||||
self.anim_time += 0.033;
|
||||
ctx.request_repaint_after(Duration::from_millis(50));
|
||||
if self.listening_keybind.is_some() || self.hud_notification.is_some() {
|
||||
ctx.request_repaint();
|
||||
} else {
|
||||
ctx.request_repaint_after(Duration::from_millis(33));
|
||||
}
|
||||
|
||||
// Auto-position and size to monitor on launch
|
||||
if !self.initial_pos_set {
|
||||
|
|
@ -2242,14 +2260,24 @@ impl eframe::App for ScytheOverlayApp {
|
|||
}
|
||||
|
||||
pub fn run_egui_overlay() {
|
||||
#[cfg(target_os = "windows")]
|
||||
let (screen_w, screen_h): (f32, f32) = unsafe {
|
||||
use windows::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_CXSCREEN, SM_CYSCREEN};
|
||||
let w = GetSystemMetrics(SM_CXSCREEN) as f32;
|
||||
let h = GetSystemMetrics(SM_CYSCREEN) as f32;
|
||||
if w > 100.0 && h > 100.0 { (w, h) } else { (1920.0, 1080.0) }
|
||||
};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let (screen_w, screen_h): (f32, f32) = (1920.0, 1080.0);
|
||||
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_title("Scythe")
|
||||
.with_app_id("scythe-overlay")
|
||||
.with_position([0.0, 0.0])
|
||||
.with_inner_size([1920.0, 1080.0])
|
||||
.with_maximized(true)
|
||||
.with_resizable(true)
|
||||
.with_inner_size([screen_w, screen_h])
|
||||
.with_maximized(false)
|
||||
.with_resizable(false)
|
||||
.with_decorations(false)
|
||||
.with_transparent(true)
|
||||
.with_always_on_top(),
|
||||
|
|
@ -2275,6 +2303,10 @@ pub struct ShadowPlayToastApp {
|
|||
}
|
||||
|
||||
impl eframe::App for ShadowPlayToastApp {
|
||||
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
|
||||
[0.0, 0.0, 0.0, 0.0]
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
let elapsed = self.created_at.elapsed().as_secs_f32();
|
||||
let total_dur = self.duration.as_secs_f32();
|
||||
|
|
@ -2282,7 +2314,7 @@ impl eframe::App for ShadowPlayToastApp {
|
|||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
std::process::exit(0);
|
||||
}
|
||||
ctx.request_repaint();
|
||||
ctx.request_repaint_after(Duration::from_millis(16));
|
||||
|
||||
let slide_x = if elapsed < 0.35 {
|
||||
let t = (elapsed / 0.35).min(1.0);
|
||||
|
|
@ -2392,13 +2424,26 @@ 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 = 340.0;
|
||||
let toast_h = 64.0;
|
||||
let toast_w: f32 = 340.0;
|
||||
let toast_h: f32 = 64.0;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let screen_w: f32 = unsafe {
|
||||
use windows::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_CXSCREEN};
|
||||
let w = GetSystemMetrics(SM_CXSCREEN) as f32;
|
||||
if w > 100.0 { w } else { 1920.0 }
|
||||
};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let screen_w: f32 = 1920.0;
|
||||
|
||||
let pos_x = (screen_w - toast_w - 24.0).max(10.0);
|
||||
let pos_y = 24.0;
|
||||
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_title("Scythe Notification")
|
||||
.with_app_id("scythe-toast")
|
||||
.with_position([pos_x, pos_y])
|
||||
.with_inner_size([toast_w, toast_h])
|
||||
.with_decorations(false)
|
||||
.with_transparent(true)
|
||||
|
|
@ -2416,6 +2461,12 @@ pub fn run_egui_toast(title: &str, subtitle: &str, icon: crate::overlay::ToastIc
|
|||
duration: Duration::from_millis(2800),
|
||||
};
|
||||
|
||||
// Watchdog thread to guarantee exit after duration
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(Duration::from_millis(3200));
|
||||
std::process::exit(0);
|
||||
});
|
||||
|
||||
let _ = eframe::run_native(
|
||||
"scythe-toast",
|
||||
options,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue