diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f2ac728..c5989f5 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -477,6 +477,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.1" @@ -2080,7 +2086,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] @@ -2198,6 +2204,19 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -2730,6 +2749,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "muda" version = "0.17.2" @@ -2745,7 +2774,7 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation", "once_cell", - "png", + "png 0.17.16", "serde", "thiserror 2.0.18", "windows-sys 0.60.2", @@ -3454,6 +3483,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.11.0" @@ -3610,6 +3652,12 @@ dependencies = [ "psl-types", ] +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + [[package]] name = "quick-xml" version = "0.23.1" @@ -4845,6 +4893,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", + "image", "jni", "libc", "log", @@ -4912,7 +4961,7 @@ dependencies = [ "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", "semver", @@ -5587,7 +5636,7 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation", "once_cell", - "png", + "png 0.17.16", "serde", "thiserror 2.0.18", "windows-sys 0.60.2", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3323fd1..9e724f6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -10,7 +10,7 @@ rust-version = "1.77" tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2", features = [] } +tauri = { version = "2", features = ["tray-icon", "image-png"] } tauri-plugin-shell = "2" tauri-plugin-dialog = "2" tauri-plugin-opener = "2" diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index b357a9e..6d55e63 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -26,6 +26,7 @@ pub struct AppState { pub folder_monitor: Arc, pub remote_server: Arc, pub otp_broker: Arc, + pub shutdown_scheduler: Arc, pub rot_log_path: Mutex, pub upload_log_path: Mutex, pub always_on_top: PLMutex, @@ -107,8 +108,17 @@ pub async fn get_always_on_top(state: State<'_, AppState>) -> AppResult { } #[tauri::command] -pub async fn set_shutdown_after_finish(state: State<'_, AppState>, mode: String) -> AppResult<()> { - *state.shutdown_mode.lock() = mode; +pub async fn set_shutdown_after_finish( + app: AppHandle, + state: State<'_, AppState>, + mode: String, +) -> AppResult<()> { + *state.shutdown_mode.lock() = mode.clone(); + if mode != "nothing" { + state.shutdown_scheduler.schedule(app, mode); + } else { + state.shutdown_scheduler.cancel(); + } Ok(()) } @@ -120,6 +130,7 @@ pub async fn get_shutdown_after_finish(state: State<'_, AppState>) -> AppResult< #[tauri::command] pub async fn cancel_shutdown(state: State<'_, AppState>) -> AppResult<()> { *state.shutdown_mode.lock() = "nothing".into(); + state.shutdown_scheduler.cancel(); Ok(()) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4802034..561081d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -12,6 +12,8 @@ pub mod remote_server; pub mod updater; pub mod upload_log; pub mod otp; +pub mod tray; +pub mod shutdown; pub mod commands; use std::sync::{Arc, Mutex}; @@ -53,12 +55,20 @@ pub fn run() { let otp_broker = otp::OtpBroker::new(); manager.set_otp_broker(otp_broker.clone(), app.handle().clone()); + let shutdown_scheduler = shutdown::ShutdownScheduler::new(); + + // System tray (Windows/Linux/macOS). Non-fatal if it fails. + if let Err(e) = tray::install(app.handle()) { + tracing::warn!("tray install: {e}"); + } + app.manage(commands::AppState { config: store.clone(), uploads: manager, folder_monitor: fm.clone(), remote_server: remote.clone(), otp_broker, + shutdown_scheduler, rot_log_path: Mutex::new(data_dir.join("account-rotation.log")), upload_log_path: Mutex::new(data_dir.join("fileuploader.log")), always_on_top: parking_lot::Mutex::new(false), diff --git a/src-tauri/src/shutdown.rs b/src-tauri/src/shutdown.rs new file mode 100644 index 0000000..947b894 --- /dev/null +++ b/src-tauri/src/shutdown.rs @@ -0,0 +1,71 @@ +//! Shutdown-after-finish — emits a 60 second countdown to the renderer so +//! the user can cancel, then runs the chosen system command. +//! +//! Modes: "nothing" | "sleep" | "shutdown" | "restart". + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration; +use tauri::{AppHandle, Emitter}; + +pub struct ShutdownScheduler { + pending: Arc, + cancel: Arc, +} + +impl ShutdownScheduler { + pub fn new() -> Arc { + Arc::new(Self { + pending: Arc::new(AtomicBool::new(false)), + cancel: Arc::new(AtomicBool::new(false)), + }) + } + + pub fn cancel(&self) { + if self.pending.load(Ordering::Relaxed) { + self.cancel.store(true, Ordering::Relaxed); + } + } + + pub fn schedule(self: &Arc, app: AppHandle, mode: String) { + if mode == "nothing" || self.pending.load(Ordering::Relaxed) { return; } + self.pending.store(true, Ordering::Relaxed); + self.cancel.store(false, Ordering::Relaxed); + let pending = self.pending.clone(); + let cancel = self.cancel.clone(); + let mode_clone = mode.clone(); + tokio::spawn(async move { + for remaining in (0..=60u32).rev() { + if cancel.load(Ordering::Relaxed) { + let _ = app.emit("shutdown-countdown", + serde_json::json!({ "mode": mode_clone, "seconds": 0, "cancelled": true })); + pending.store(false, Ordering::Relaxed); + return; + } + let _ = app.emit("shutdown-countdown", + serde_json::json!({ "mode": mode_clone, "seconds": remaining })); + tokio::time::sleep(Duration::from_secs(1)).await; + } + if !cancel.load(Ordering::Relaxed) { + execute(&mode_clone); + app.exit(0); + } + pending.store(false, Ordering::Relaxed); + }); + } +} + +fn execute(mode: &str) { + #[cfg(target_os = "windows")] + { + use std::process::Command; + match mode { + "sleep" => { Command::new("rundll32.exe").args(["powrprof.dll,SetSuspendState", "0,1,0"]).spawn().ok(); } + "shutdown" => { Command::new("shutdown").args(["/s", "/t", "0"]).spawn().ok(); } + "restart" => { Command::new("shutdown").args(["/r", "/t", "0"]).spawn().ok(); } + _ => {} + } + } + #[cfg(not(target_os = "windows"))] + { let _ = mode; } +} diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs new file mode 100644 index 0000000..fbd53d3 --- /dev/null +++ b/src-tauri/src/tray.rs @@ -0,0 +1,42 @@ +//! System tray icon — matches v1's behavior: minimize-to-tray, left-click +//! restores window, right-click menu for show / hide / quit. + +use tauri::{ + menu::{Menu, MenuItem}, + tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, + AppHandle, Manager, +}; + +pub fn install(app: &AppHandle) -> Result<(), Box> { + let show = MenuItem::with_id(app, "show", "Anzeigen", true, None::<&str>)?; + let hide = MenuItem::with_id(app, "hide", "Ausblenden", true, None::<&str>)?; + let sep = tauri::menu::PredefinedMenuItem::separator(app)?; + let quit = MenuItem::with_id(app, "quit", "Beenden", true, None::<&str>)?; + let menu = Menu::with_items(app, &[&show, &hide, &sep, &quit])?; + + let _tray = TrayIconBuilder::with_id("main-tray") + .tooltip("Multi-Hoster-Upload") + .icon(app.default_window_icon().cloned().unwrap_or_else(|| { + // Fallback to a 1x1 transparent PNG — Tauri requires an image. + tauri::image::Image::new_owned(vec![0u8; 4], 1, 1) + })) + .menu(&menu) + .menu_on_left_click(false) + .on_menu_event(|app, event| match event.id.as_ref() { + "show" => { if let Some(w) = app.get_webview_window("main") { let _ = w.show(); let _ = w.set_focus(); } } + "hide" => { if let Some(w) = app.get_webview_window("main") { let _ = w.hide(); } } + "quit" => { app.exit(0); } + _ => {} + }) + .on_tray_icon_event(|tray, ev| { + if let TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } = ev { + let app = tray.app_handle(); + if let Some(w) = app.get_webview_window("main") { + let visible = w.is_visible().unwrap_or(true); + if visible { let _ = w.hide(); } else { let _ = w.show(); let _ = w.set_focus(); } + } + } + }) + .build(app)?; + Ok(()) +} diff --git a/src-tauri/src/updater.rs b/src-tauri/src/updater.rs index 13b34d9..0a51287 100644 --- a/src-tauri/src/updater.rs +++ b/src-tauri/src/updater.rs @@ -8,7 +8,7 @@ use semver::Version; use serde::{Deserialize, Serialize}; use std::time::Duration; -const REPO: &str = "Administrator/Multi-Hoster-Upload"; +const REPO: &str = "Administrator/Multi-Hoster-Upload-2"; const BASE: &str = "https://git.24-music.de/api/v1/repos"; #[derive(Serialize, Default, Clone, Debug)]