Add system tray, shutdown-after-finish scheduler, repoint updater

- src/tray.rs: system tray with show/hide/quit menu, left-click
    toggles main window visibility (minimize-to-tray parity with v1).
  - src/shutdown.rs: 60s countdown with per-second 'shutdown-countdown'
    event; sleep/shutdown/restart via rundll32/shutdown on Windows,
    cancel-aware.
  - cancel_shutdown + set_shutdown_after_finish commands now drive the
    scheduler so the renderer's existing countdown UI works unchanged.
  - Cargo features + tray-icon + image-png added.
  - Updater pointed at new Gitea repo Administrator/Multi-Hoster-Upload-2.
This commit is contained in:
Claude 2026-04-20 18:32:59 +02:00
parent 100bda60cd
commit 2958dca282
7 changed files with 191 additions and 8 deletions

57
src-tauri/Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -26,6 +26,7 @@ pub struct AppState {
pub folder_monitor: Arc<crate::folder_monitor::FolderMonitor>,
pub remote_server: Arc<crate::remote_server::RemoteServer>,
pub otp_broker: Arc<crate::otp::OtpBroker>,
pub shutdown_scheduler: Arc<crate::shutdown::ShutdownScheduler>,
pub rot_log_path: Mutex<PathBuf>,
pub upload_log_path: Mutex<PathBuf>,
pub always_on_top: PLMutex<bool>,
@ -107,8 +108,17 @@ pub async fn get_always_on_top(state: State<'_, AppState>) -> AppResult<bool> {
}
#[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(())
}

View File

@ -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),

71
src-tauri/src/shutdown.rs Normal file
View File

@ -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<AtomicBool>,
cancel: Arc<AtomicBool>,
}
impl ShutdownScheduler {
pub fn new() -> Arc<Self> {
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<Self>, 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; }
}

42
src-tauri/src/tray.rs Normal file
View File

@ -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<dyn std::error::Error>> {
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(())
}

View File

@ -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)]