5bdabf558b
- with_desktop_marker haengt desktop=1 an beide Navigationen (save_server_url, Startnavigation im setup); der Store-Wert server_url bleibt ohne Parameter - update_labels liefert Menue-/Benachrichtigungstext: bei Versionswechsel wie bisher, bei gleicher Version (Beta-Kanal) nennt der Text den Commit-Stempel - mod tests deckt beide Helfer ab (fmt/check/clippy/test --lib gruen) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
350 lines
14 KiB
Rust
350 lines
14 KiB
Rust
use std::time::Duration;
|
||
use tauri::{
|
||
menu::{CheckMenuItemBuilder, MenuBuilder, MenuItemBuilder},
|
||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||
AppHandle, Manager, RunEvent, WindowEvent,
|
||
};
|
||
use tauri_plugin_autostart::{MacosLauncher, ManagerExt};
|
||
use tauri_plugin_notification::NotificationExt;
|
||
use tauri_plugin_opener::OpenerExt;
|
||
use tauri_plugin_store::StoreExt;
|
||
|
||
#[derive(serde::Deserialize)]
|
||
struct VersionResponse {
|
||
version: String,
|
||
}
|
||
|
||
#[derive(serde::Deserialize)]
|
||
struct DesktopLatest {
|
||
version: String,
|
||
channel: String,
|
||
commit: String,
|
||
}
|
||
|
||
/// Baut die Adresse eines API-Pfads aus der gespeicherten Server-Adresse.
|
||
/// Die API ist vom Client nur ueber den Web-Ursprung erreichbar
|
||
/// (Next.js-Rewrite `/api-proxy/*`, siehe 18-01) -- niemals direkt unter dem
|
||
/// Web-Hostnamen. Dies ist die einzige Stelle, an der der Rewrite-Praefix
|
||
/// steht.
|
||
fn api_url(server: &str, path: &str) -> String {
|
||
format!("{}/api-proxy{}", server.trim_end_matches('/'), path)
|
||
}
|
||
|
||
/// Haengt das Query-Paar `desktop=1` an einen Klon der Adresse an und gibt
|
||
/// den Klon zurueck -- das Original bleibt unveraendert. Der Parameter geht
|
||
/// NUR in die Navigation, niemals in den gespeicherten Server-Wert
|
||
/// (`server_url` im Store bleibt die reine Adresse); daraus liest die
|
||
/// Next.js-Middleware (apps/web/src/middleware.ts) das Cookie
|
||
/// `tessera_desktop`, an dem der Web-Client den Desktop-Client erkennt. Die
|
||
/// Tray-URL "Update herunterladen" bekommt den Parameter bewusst NICHT --
|
||
/// sie oeffnet im System-Browser, dort muessen die Download-Links sichtbar
|
||
/// bleiben.
|
||
fn with_desktop_marker(url: &tauri::Url) -> tauri::Url {
|
||
let mut marked = url.clone();
|
||
marked.query_pairs_mut().append_pair("desktop", "1");
|
||
marked
|
||
}
|
||
|
||
/// Liefert (Menuetext, Benachrichtigungstext) fuer den Update-Hinweis. Der
|
||
/// Beta-Kanal (main) vergibt jedem Commit dieselbe X.Y.Z-Version (D-07,
|
||
/// desktop-collect.sh) -- hat sich nur der Commit geaendert, waere der reine
|
||
/// Versionstext ("Version 1.1.0 herunterladen") verwirrend, weil der Client
|
||
/// bereits genau diese Version hat. Der zweite Zweig nennt darum den
|
||
/// Commit-Stempel statt der unveraenderten Version.
|
||
fn update_labels(version_changed: bool, version: &str, commit: &str) -> (String, String) {
|
||
if version_changed {
|
||
(
|
||
format!("Version {} herunterladen", version),
|
||
format!(
|
||
"Neue Version {} verfügbar – Download über das Symbol im Infobereich.",
|
||
version
|
||
),
|
||
)
|
||
} else {
|
||
(
|
||
"Neuen Beta-Stand herunterladen".to_string(),
|
||
format!(
|
||
"Neuer Beta-Stand {} verfügbar – Download über das Symbol im Infobereich.",
|
||
commit
|
||
),
|
||
)
|
||
}
|
||
}
|
||
|
||
#[tauri::command]
|
||
async fn check_server(url: String) -> Result<String, String> {
|
||
let parsed = tauri::Url::parse(&url).map_err(|_| "Diese Adresse ist ungültig.".to_string())?;
|
||
if parsed.scheme() != "http" && parsed.scheme() != "https" {
|
||
return Err("Es sind nur Adressen mit http oder https erlaubt.".to_string());
|
||
}
|
||
|
||
let client = reqwest::Client::builder()
|
||
.timeout(Duration::from_secs(8))
|
||
.build()
|
||
.map_err(|e| e.to_string())?;
|
||
|
||
let resp = client
|
||
.get(api_url(&url, "/health/version"))
|
||
.send()
|
||
.await
|
||
.map_err(|_| "Unter dieser Adresse antwortet kein Tessera-Server.".to_string())?;
|
||
|
||
if !resp.status().is_success() {
|
||
return Err(format!(
|
||
"Der Server antwortete mit Status {}.",
|
||
resp.status().as_u16()
|
||
));
|
||
}
|
||
|
||
let info = resp
|
||
.json::<VersionResponse>()
|
||
.await
|
||
.map_err(|_| "Unter dieser Adresse antwortet kein Tessera-Server.".to_string())?;
|
||
|
||
Ok(info.version)
|
||
}
|
||
|
||
#[tauri::command]
|
||
fn save_server_url(app: AppHandle, url: String) -> Result<(), String> {
|
||
let parsed = tauri::Url::parse(&url).map_err(|_| "Diese Adresse ist ungültig.".to_string())?;
|
||
let normalized = parsed.as_str().to_string();
|
||
|
||
let store = app.store("config.json").map_err(|e| e.to_string())?;
|
||
store.set("server_url", serde_json::json!(normalized));
|
||
store.save().map_err(|e| e.to_string())?;
|
||
|
||
if let Some(window) = app.get_webview_window("main") {
|
||
let _ = window.navigate(with_desktop_marker(&parsed));
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
pub fn run() {
|
||
let app = tauri::Builder::default()
|
||
.plugin(tauri_plugin_store::Builder::new().build())
|
||
.plugin(tauri_plugin_notification::init())
|
||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||
.plugin(tauri_plugin_autostart::init(
|
||
MacosLauncher::LaunchAgent,
|
||
None,
|
||
))
|
||
.plugin(tauri_plugin_opener::init())
|
||
.invoke_handler(tauri::generate_handler![check_server, save_server_url])
|
||
.setup(|app| {
|
||
let store = app.store("config.json")?;
|
||
let server_url = store.get("server_url");
|
||
|
||
let mut url_for_check: Option<String> = None;
|
||
|
||
if let Some(url_value) = server_url {
|
||
if let Some(url_str) = url_value.as_str() {
|
||
if !url_str.is_empty() {
|
||
url_for_check = Some(url_str.to_string());
|
||
if let Some(window) = app.get_webview_window("main") {
|
||
if let Ok(parsed) = url_str.parse::<tauri::Url>() {
|
||
let _ = window.navigate(with_desktop_marker(&parsed));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Tray menu: Öffnen · Update herunterladen · — · Autostart-Haken · — · Beenden.
|
||
// "update" bleibt gesperrt, bis die Versionspruefung eine neuere Version
|
||
// findet; "autostart" spiegelt den tatsaechlichen Systemzustand beim Start.
|
||
let open = MenuItemBuilder::with_id("open", "Öffnen").build(app)?;
|
||
let update = MenuItemBuilder::with_id("update", "Update herunterladen")
|
||
.enabled(false)
|
||
.build(app)?;
|
||
let autostart_label = if cfg!(target_os = "windows") {
|
||
"Mit Windows starten"
|
||
} else {
|
||
"Beim Anmelden starten"
|
||
};
|
||
let autostart = CheckMenuItemBuilder::with_id("autostart", autostart_label)
|
||
.checked(app.autolaunch().is_enabled().unwrap_or(false))
|
||
.build(app)?;
|
||
let quit = MenuItemBuilder::with_id("quit", "Beenden").build(app)?;
|
||
|
||
let menu = MenuBuilder::new(app)
|
||
.item(&open)
|
||
.item(&update)
|
||
.separator()
|
||
.item(&autostart)
|
||
.separator()
|
||
.item(&quit)
|
||
.build()?;
|
||
|
||
let server_for_menu = url_for_check.clone();
|
||
let autostart_for_menu = autostart.clone();
|
||
|
||
let _tray = TrayIconBuilder::new()
|
||
.icon(app.default_window_icon().unwrap().clone())
|
||
.menu(&menu)
|
||
.tooltip("Tessera")
|
||
.show_menu_on_left_click(false)
|
||
.on_menu_event(move |app, event| match event.id().as_ref() {
|
||
"open" => {
|
||
if let Some(w) = app.get_webview_window("main") {
|
||
// Minimiertes Fenster (z. B. per Win+D) zuerst wiederherstellen
|
||
let _ = w.unminimize();
|
||
let _ = w.show();
|
||
let _ = w.set_focus();
|
||
}
|
||
}
|
||
"update" => {
|
||
if let Some(server) = &server_for_menu {
|
||
let target = format!(
|
||
"{}/settings/general/desktop",
|
||
server.trim_end_matches('/')
|
||
);
|
||
let _ = app.opener().open_url(target, None::<&str>);
|
||
}
|
||
}
|
||
"autostart" => {
|
||
let mgr = app.autolaunch();
|
||
let currently_on = mgr.is_enabled().unwrap_or(false);
|
||
let toggled = if currently_on {
|
||
mgr.disable()
|
||
} else {
|
||
mgr.enable()
|
||
};
|
||
match toggled {
|
||
Ok(()) => {
|
||
let _ = autostart_for_menu.set_checked(!currently_on);
|
||
}
|
||
Err(_) => {
|
||
let _ = autostart_for_menu.set_checked(currently_on);
|
||
}
|
||
}
|
||
}
|
||
"quit" => {
|
||
app.exit(0);
|
||
}
|
||
_ => {}
|
||
})
|
||
.on_tray_icon_event(|tray, event| {
|
||
if let TrayIconEvent::Click {
|
||
button: MouseButton::Left,
|
||
button_state: MouseButtonState::Up,
|
||
..
|
||
} = event
|
||
{
|
||
if let Some(w) = tray.app_handle().get_webview_window("main") {
|
||
let _ = w.unminimize();
|
||
let _ = w.show();
|
||
let _ = w.set_focus();
|
||
}
|
||
}
|
||
})
|
||
.build(app)?;
|
||
|
||
// Version check (async, non-blocking)
|
||
if let Some(server_url) = url_for_check {
|
||
let app_handle = app.handle().clone();
|
||
let app_version = env!("CARGO_PKG_VERSION").to_string();
|
||
let app_commit = env!("APP_COMMIT").to_string();
|
||
let update_item = update.clone();
|
||
tauri::async_runtime::spawn(async move {
|
||
let url = api_url(&server_url, "/desktop/latest");
|
||
if let Ok(resp) = reqwest::get(&url).await {
|
||
if let Ok(info) = resp.json::<DesktopLatest>().await {
|
||
// Beta-Kanal (main) vergibt jedem Commit dieselbe X.Y.Z-Version
|
||
// (D-07, desktop-collect.sh) -- ohne den Commit-Vergleich saehe
|
||
// ein Beta-Client zwischen zwei Freigabe-Tags nie einen neueren
|
||
// Bau (WR-02, Code-Review Phase 18). Fuer den Live-Kanal bleibt
|
||
// es beim reinen Versionsvergleich. Die Texte fuer Menue und
|
||
// Benachrichtigung liefert `update_labels`.
|
||
let version_changed = info.version != app_version;
|
||
let is_newer = version_changed
|
||
|| (info.channel == "beta" && info.commit != app_commit);
|
||
if is_newer {
|
||
let (menu_text, body) =
|
||
update_labels(version_changed, &info.version, &info.commit);
|
||
let _ = app_handle
|
||
.notification()
|
||
.builder()
|
||
.title("Tessera-Update")
|
||
.body(body)
|
||
.show();
|
||
let _ = update_item.set_text(menu_text);
|
||
let _ = update_item.set_enabled(true);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
Ok(())
|
||
})
|
||
.on_window_event(|window, event| {
|
||
if let WindowEvent::CloseRequested { api, .. } = event {
|
||
let _ = window.hide();
|
||
api.prevent_close();
|
||
}
|
||
})
|
||
.build(tauri::generate_context!())
|
||
.expect("error while building Tessera desktop application");
|
||
|
||
app.run(|_app, event| {
|
||
// Fenster schliessen setzt code auf None -- App laeuft dann im
|
||
// Infobereich weiter (siehe on_window_event). Der Tray-Eintrag
|
||
// "Beenden" ruft dagegen app.exit(0), das liefert code: Some --
|
||
// dieser Fall muss durchgelassen werden, sonst bleibt der Prozess
|
||
// samt Tray-Symbol haengen.
|
||
if let RunEvent::ExitRequested {
|
||
code: None, api, ..
|
||
} = event
|
||
{
|
||
api.prevent_exit();
|
||
}
|
||
});
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn with_desktop_marker_ohne_pfad_haengt_query_an() {
|
||
let url = tauri::Url::parse("https://tessera.example.com").unwrap();
|
||
let marked = with_desktop_marker(&url);
|
||
assert_eq!(marked.as_str(), "https://tessera.example.com/?desktop=1");
|
||
}
|
||
|
||
#[test]
|
||
fn with_desktop_marker_mit_vorhandenem_query_haengt_an() {
|
||
let url = tauri::Url::parse("https://host/app?x=1").unwrap();
|
||
let marked = with_desktop_marker(&url);
|
||
assert_eq!(marked.as_str(), "https://host/app?x=1&desktop=1");
|
||
}
|
||
|
||
#[test]
|
||
fn with_desktop_marker_laesst_original_unveraendert() {
|
||
let url = tauri::Url::parse("https://host/app").unwrap();
|
||
let _ = with_desktop_marker(&url);
|
||
assert_eq!(url.query(), None);
|
||
}
|
||
|
||
#[test]
|
||
fn update_labels_bei_versionswechsel() {
|
||
let (menu_text, body) = update_labels(true, "1.2.0", "abc1234");
|
||
assert_eq!(menu_text, "Version 1.2.0 herunterladen");
|
||
assert_eq!(
|
||
body,
|
||
"Neue Version 1.2.0 verfügbar – Download über das Symbol im Infobereich."
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn update_labels_bei_gleicher_version_beta_commit() {
|
||
let (menu_text, body) = update_labels(false, "1.1.0", "abc1234");
|
||
assert_eq!(menu_text, "Neuen Beta-Stand herunterladen");
|
||
assert_eq!(
|
||
body,
|
||
"Neuer Beta-Stand abc1234 verfügbar – Download über das Symbol im Infobereich."
|
||
);
|
||
}
|
||
}
|