feat(06-02): add native desktop features — tray, window-state, autostart, version check

- Add GET /health/version public endpoint to API
- Extend Tauri with 4 plugins: notification, autostart, window-state, store
- Implement close-to-tray with prevent_close + prevent_exit (Pitfall 2)
- Tray menu with Oeffnen/Beenden (German labels)
- Async startup version check against server /health/version
- Generate Tessera-branded icons (yellow T on dark bg, OKLCH palette)
- Update capabilities for notification, autostart, window-state permissions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 14:01:12 +02:00
parent 4d94a254fd
commit c65ada0ba9
6 changed files with 1146 additions and 34 deletions
+1019 -14
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -13,5 +13,9 @@ tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = ["tray-icon"] }
tauri-plugin-store = "2"
tauri-plugin-notification = "2"
tauri-plugin-autostart = "2"
tauri-plugin-window-state = "2"
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
@@ -5,6 +5,14 @@
"windows": ["main"],
"permissions": [
"core:default",
"store:default"
"store:default",
"notification:default",
"notification:allow-is-permission-granted",
"notification:allow-request-permission",
"notification:allow-notify",
"autostart:allow-enable",
"autostart:allow-disable",
"autostart:allow-is-enabled",
"window-state:default"
]
}
+96 -13
View File
@@ -1,23 +1,34 @@
use tauri::Manager;
use tauri::{
Manager,
menu::{MenuBuilder, MenuItemBuilder},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
RunEvent, WindowEvent,
};
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_notification::NotificationExt;
use tauri_plugin_store::StoreExt;
/// Main entry point for the Tessera desktop application.
///
/// Registers plugins, checks for a stored server URL on startup,
/// and either navigates to the configured server or shows the
/// bundled setup page for first-run configuration.
#[derive(serde::Deserialize)]
struct VersionResponse {
version: String,
}
pub fn run() {
tauri::Builder::default()
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))
.setup(|app| {
// Attempt to read a previously stored server URL
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() {
// Navigate the main window to the configured server URL
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(parsed);
@@ -26,11 +37,83 @@ pub fn run() {
}
}
}
// If no server_url is stored, the WebView remains on the
// bundled setup page (frontendDist = ../src/setup.html)
// Tray menu
let open = MenuItemBuilder::with_id("open", "Oeffnen").build(app)?;
let quit = MenuItemBuilder::with_id("quit", "Beenden").build(app)?;
let menu = MenuBuilder::new(app)
.item(&open)
.separator()
.item(&quit)
.build()?;
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() {
"open" => {
if let Some(w) = app.get_webview_window("main") {
let _ = w.show();
let _ = w.set_focus();
}
}
"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.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();
tauri::async_runtime::spawn(async move {
let url = format!("{}/health/version", server_url.trim_end_matches('/'));
if let Ok(resp) = reqwest::get(&url).await {
if let Ok(info) = resp.json::<VersionResponse>().await {
if info.version != app_version {
let _ = app_handle
.notification()
.builder()
.title("Tessera Update")
.body("Eine neue Version ist verfuegbar.")
.show();
}
}
}
});
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running Tessera desktop application");
.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| {
if let RunEvent::ExitRequested { api, .. } = event {
api.prevent_exit();
}
});
}