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:
parent
100bda60cd
commit
2958dca282
57
src-tauri/Cargo.lock
generated
57
src-tauri/Cargo.lock
generated
@ -477,6 +477,12 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder-lite"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@ -2080,7 +2086,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
|
checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"png",
|
"png 0.17.16",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2198,6 +2204,19 @@ dependencies = [
|
|||||||
"icu_properties",
|
"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]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.9.3"
|
version = "1.9.3"
|
||||||
@ -2730,6 +2749,16 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "muda"
|
name = "muda"
|
||||||
version = "0.17.2"
|
version = "0.17.2"
|
||||||
@ -2745,7 +2774,7 @@ dependencies = [
|
|||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"png",
|
"png 0.17.16",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
@ -3454,6 +3483,19 @@ dependencies = [
|
|||||||
"miniz_oxide",
|
"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]]
|
[[package]]
|
||||||
name = "polling"
|
name = "polling"
|
||||||
version = "3.11.0"
|
version = "3.11.0"
|
||||||
@ -3610,6 +3652,12 @@ dependencies = [
|
|||||||
"psl-types",
|
"psl-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pxfm"
|
||||||
|
version = "0.1.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.23.1"
|
version = "0.23.1"
|
||||||
@ -4845,6 +4893,7 @@ dependencies = [
|
|||||||
"gtk",
|
"gtk",
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"http",
|
"http",
|
||||||
|
"image",
|
||||||
"jni",
|
"jni",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
@ -4912,7 +4961,7 @@ dependencies = [
|
|||||||
"ico",
|
"ico",
|
||||||
"json-patch",
|
"json-patch",
|
||||||
"plist",
|
"plist",
|
||||||
"png",
|
"png 0.17.16",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"semver",
|
"semver",
|
||||||
@ -5587,7 +5636,7 @@ dependencies = [
|
|||||||
"objc2-core-graphics",
|
"objc2-core-graphics",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"png",
|
"png 0.17.16",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
|
|||||||
@ -10,7 +10,7 @@ rust-version = "1.77"
|
|||||||
tauri-build = { version = "2", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = ["tray-icon", "image-png"] }
|
||||||
tauri-plugin-shell = "2"
|
tauri-plugin-shell = "2"
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
|
|||||||
@ -26,6 +26,7 @@ pub struct AppState {
|
|||||||
pub folder_monitor: Arc<crate::folder_monitor::FolderMonitor>,
|
pub folder_monitor: Arc<crate::folder_monitor::FolderMonitor>,
|
||||||
pub remote_server: Arc<crate::remote_server::RemoteServer>,
|
pub remote_server: Arc<crate::remote_server::RemoteServer>,
|
||||||
pub otp_broker: Arc<crate::otp::OtpBroker>,
|
pub otp_broker: Arc<crate::otp::OtpBroker>,
|
||||||
|
pub shutdown_scheduler: Arc<crate::shutdown::ShutdownScheduler>,
|
||||||
pub rot_log_path: Mutex<PathBuf>,
|
pub rot_log_path: Mutex<PathBuf>,
|
||||||
pub upload_log_path: Mutex<PathBuf>,
|
pub upload_log_path: Mutex<PathBuf>,
|
||||||
pub always_on_top: PLMutex<bool>,
|
pub always_on_top: PLMutex<bool>,
|
||||||
@ -107,8 +108,17 @@ pub async fn get_always_on_top(state: State<'_, AppState>) -> AppResult<bool> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn set_shutdown_after_finish(state: State<'_, AppState>, mode: String) -> AppResult<()> {
|
pub async fn set_shutdown_after_finish(
|
||||||
*state.shutdown_mode.lock() = mode;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,6 +130,7 @@ pub async fn get_shutdown_after_finish(state: State<'_, AppState>) -> AppResult<
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn cancel_shutdown(state: State<'_, AppState>) -> AppResult<()> {
|
pub async fn cancel_shutdown(state: State<'_, AppState>) -> AppResult<()> {
|
||||||
*state.shutdown_mode.lock() = "nothing".into();
|
*state.shutdown_mode.lock() = "nothing".into();
|
||||||
|
state.shutdown_scheduler.cancel();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,8 @@ pub mod remote_server;
|
|||||||
pub mod updater;
|
pub mod updater;
|
||||||
pub mod upload_log;
|
pub mod upload_log;
|
||||||
pub mod otp;
|
pub mod otp;
|
||||||
|
pub mod tray;
|
||||||
|
pub mod shutdown;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
@ -53,12 +55,20 @@ pub fn run() {
|
|||||||
let otp_broker = otp::OtpBroker::new();
|
let otp_broker = otp::OtpBroker::new();
|
||||||
manager.set_otp_broker(otp_broker.clone(), app.handle().clone());
|
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 {
|
app.manage(commands::AppState {
|
||||||
config: store.clone(),
|
config: store.clone(),
|
||||||
uploads: manager,
|
uploads: manager,
|
||||||
folder_monitor: fm.clone(),
|
folder_monitor: fm.clone(),
|
||||||
remote_server: remote.clone(),
|
remote_server: remote.clone(),
|
||||||
otp_broker,
|
otp_broker,
|
||||||
|
shutdown_scheduler,
|
||||||
rot_log_path: Mutex::new(data_dir.join("account-rotation.log")),
|
rot_log_path: Mutex::new(data_dir.join("account-rotation.log")),
|
||||||
upload_log_path: Mutex::new(data_dir.join("fileuploader.log")),
|
upload_log_path: Mutex::new(data_dir.join("fileuploader.log")),
|
||||||
always_on_top: parking_lot::Mutex::new(false),
|
always_on_top: parking_lot::Mutex::new(false),
|
||||||
|
|||||||
71
src-tauri/src/shutdown.rs
Normal file
71
src-tauri/src/shutdown.rs
Normal 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
42
src-tauri/src/tray.rs
Normal 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(())
|
||||||
|
}
|
||||||
@ -8,7 +8,7 @@ use semver::Version;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::time::Duration;
|
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";
|
const BASE: &str = "https://git.24-music.de/api/v1/repos";
|
||||||
|
|
||||||
#[derive(Serialize, Default, Clone, Debug)]
|
#[derive(Serialize, Default, Clone, Debug)]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user