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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user