feat(18-04): Task 1 — Kommandos, Versionsprüfung und Tray für Update/Autostart

- tauri-plugin-opener@2 hinzugefügt (Legitimität in RESEARCH geprüft)
- Neue Rust-Kommandos check_server/save_server_url für die Erststart-Seite,
  Adressen laufen über api_url() ausschließlich über /api-proxy/*
- Versionsprüfung gegen /api-proxy/desktop/latest statt /health/version,
  schaltet den Tray-Eintrag "Update herunterladen" frei und benennt ihn um
- Tray-Menü: Öffnen · Update herunterladen · Autostart-Haken · Beenden,
  echte Umlaute, Update-Eintrag öffnet die Download-Seite im Systembrowser
- capabilities/default.json: opener:allow-open-url für http/https
- cargo check und cargo clippy grün

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-16 16:36:53 +02:00
parent 82312ef691
commit 8b130fddbd
8 changed files with 652 additions and 14 deletions
+128 -11
View File
@@ -1,11 +1,12 @@
use std::time::Duration;
use tauri::{
Manager,
menu::{MenuBuilder, MenuItemBuilder},
menu::{CheckMenuItemBuilder, MenuBuilder, MenuItemBuilder},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
RunEvent, WindowEvent,
AppHandle, Manager, RunEvent, WindowEvent,
};
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_autostart::{MacosLauncher, ManagerExt};
use tauri_plugin_notification::NotificationExt;
use tauri_plugin_opener::OpenerExt;
use tauri_plugin_store::StoreExt;
#[derive(serde::Deserialize)]
@@ -13,12 +14,77 @@ struct VersionResponse {
version: String,
}
#[derive(serde::Deserialize)]
struct DesktopLatest {
version: 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)
}
#[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(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");
@@ -38,27 +104,71 @@ pub fn run() {
}
}
// Tray menu
let open = MenuItemBuilder::with_id("open", "Oeffnen").build(app)?;
// 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(|app, event| match event.id().as_ref() {
.on_menu_event(move |app, event| match event.id().as_ref() {
"open" => {
if let Some(w) = app.get_webview_window("main") {
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);
}
@@ -83,17 +193,24 @@ pub fn run() {
if let Some(server_url) = url_for_check {
let app_handle = app.handle().clone();
let app_version = env!("CARGO_PKG_VERSION").to_string();
let update_item = update.clone();
tauri::async_runtime::spawn(async move {
let url = format!("{}/health/version", server_url.trim_end_matches('/'));
let url = api_url(&server_url, "/desktop/latest");
if let Ok(resp) = reqwest::get(&url).await {
if let Ok(info) = resp.json::<VersionResponse>().await {
if let Ok(info) = resp.json::<DesktopLatest>().await {
if info.version != app_version {
let _ = app_handle
.notification()
.builder()
.title("Tessera Update")
.body("Eine neue Version ist verfuegbar.")
.title("Tessera-Update")
.body(format!(
"Neue Version {} verfügbar – Download über das Symbol im Infobereich.",
info.version
))
.show();
let _ = update_item
.set_text(format!("Version {} herunterladen", info.version));
let _ = update_item.set_enabled(true);
}
}
}