feat(desktop): Verbundenen Server im Infobereich zeigen, Server-Adresse nachträglich änderbar
- Tray zeigt erste (gesperrte) Menüzeile "Verbunden mit {host}" und Tooltip "Tessera – {host}" (bzw. "nicht verbunden")
- Neuer Menüpunkt "Server-Adresse ändern…" navigiert zur gebündelten Setup-Seite (lokaler Ursprung, kein Capability-Eintrag nötig)
- setup.html erkennt per neuem Command get_server_url den Änderungsmodus (Feld vorbelegt, "Aktuell verbunden mit: …", Knopf "Abbrechen" → open_server)
- Nach save_server_url aktualisieren apply_server (Tooltip + Menüzeile) und spawn_version_check (aus setup herausgezogen, läuft neu gegen den neuen Server) ohne Neustart; Tray-Klick "update" liest die Adresse jetzt beim Klick aus dem Store
- Reine Helfer server_host, tray_labels, setup_page_url, parse_server_url mit 13 neuen Tests (RED zuerst: 13 Compile-Fehler auf die fehlenden Helfer, dann GREEN); 18 Rust-Tests gesamt, fmt/check/clippy sauber
- capabilities/default.json unverändert (T-JN2-01, kein remote-Block)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,20 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tauri::{
|
use tauri::{
|
||||||
menu::{CheckMenuItemBuilder, MenuBuilder, MenuItemBuilder},
|
menu::{CheckMenuItemBuilder, MenuBuilder, MenuItem, MenuItemBuilder},
|
||||||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||||
AppHandle, Manager, RunEvent, WindowEvent,
|
AppHandle, Manager, RunEvent, WindowEvent, Wry,
|
||||||
};
|
};
|
||||||
use tauri_plugin_autostart::{MacosLauncher, ManagerExt};
|
use tauri_plugin_autostart::{MacosLauncher, ManagerExt};
|
||||||
use tauri_plugin_notification::NotificationExt;
|
use tauri_plugin_notification::NotificationExt;
|
||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
use tauri_plugin_store::StoreExt;
|
use tauri_plugin_store::StoreExt;
|
||||||
|
|
||||||
|
/// Standardtext des Update-Eintrags im Tray-Menü, bevor eine neuere Version
|
||||||
|
/// gefunden wurde. `spawn_version_check` setzt den Eintrag nach einem
|
||||||
|
/// Serverwechsel hierauf zurück, damit kein Hinweis des alten Servers stehen
|
||||||
|
/// bleibt.
|
||||||
|
const UPDATE_ITEM_DEFAULT: &str = "Update herunterladen";
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
struct VersionResponse {
|
struct VersionResponse {
|
||||||
version: String,
|
version: String,
|
||||||
@@ -71,12 +77,147 @@ fn update_labels(version_changed: bool, version: &str, commit: &str) -> (String,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
/// Parst eine vom Nutzer eingegebene Server-Adresse und erlaubt nur http/https
|
||||||
async fn check_server(url: String) -> Result<String, String> {
|
/// -- dieselben Fehlertexte wie bisher inline in `check_server`. `check_server`,
|
||||||
let parsed = tauri::Url::parse(&url).map_err(|_| "Diese Adresse ist ungültig.".to_string())?;
|
/// `save_server_url` und `open_server` rufen ausschliesslich diesen Helfer;
|
||||||
|
/// das Verhalten bleibt unveraendert, das Duplikat ist weg.
|
||||||
|
fn parse_server_url(url: &str) -> Result<tauri::Url, String> {
|
||||||
|
let parsed = tauri::Url::parse(url).map_err(|_| "Diese Adresse ist ungültig.".to_string())?;
|
||||||
if parsed.scheme() != "http" && parsed.scheme() != "https" {
|
if parsed.scheme() != "http" && parsed.scheme() != "https" {
|
||||||
return Err("Es sind nur Adressen mit http oder https erlaubt.".to_string());
|
return Err("Es sind nur Adressen mit http oder https erlaubt.".to_string());
|
||||||
}
|
}
|
||||||
|
Ok(parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Liefert nur den Host der gespeicherten Server-Adresse (ohne Schema, ohne
|
||||||
|
/// Pfad) -- die Anzeige im Tray soll kurz bleiben. `host:port` nur, wenn
|
||||||
|
/// ausdruecklich ein Nicht-Standardport angegeben wurde (der `url`-Crate
|
||||||
|
/// laesst Standardports beim Parsen automatisch weg).
|
||||||
|
fn server_host(url: Option<&str>) -> Option<String> {
|
||||||
|
let url = url?;
|
||||||
|
if url.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let parsed = tauri::Url::parse(url).ok()?;
|
||||||
|
let host = parsed.host_str()?;
|
||||||
|
match parsed.port() {
|
||||||
|
Some(port) => Some(format!("{}:{}", host, port)),
|
||||||
|
None => Some(host.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Liefert (Tooltip, Menüzeile) für den Tray -- einzige Stelle, die beide
|
||||||
|
/// Texte aus der Server-Adresse ableitet. Gedankenstrich U+2013 wie in
|
||||||
|
/// `update_labels`.
|
||||||
|
fn tray_labels(url: Option<&str>) -> (String, String) {
|
||||||
|
match server_host(url) {
|
||||||
|
Some(host) => (
|
||||||
|
format!("Tessera – {}", host),
|
||||||
|
format!("Verbunden mit {}", host),
|
||||||
|
),
|
||||||
|
None => (
|
||||||
|
"Tessera – nicht verbunden".to_string(),
|
||||||
|
"Nicht verbunden".to_string(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spiegelt Tauris nicht öffentliche `tauri_protocol_url`
|
||||||
|
/// (tauri-2.11.3 manager/mod.rs Z. 339-346): WebView2 kennt kein eigenes
|
||||||
|
/// Schema, darum unter Windows/Android `http://tauri.localhost`, sonst
|
||||||
|
/// `tauri://localhost`. `useHttpsScheme` ist in tauri.conf.json nicht
|
||||||
|
/// gesetzt, darum bewusst `http`. Der `tauri dev`-Fall mit `devUrl` wird in
|
||||||
|
/// diesem Projekt nicht genutzt (CI baut Release) und ist hier bewusst nicht
|
||||||
|
/// abgebildet. `WebviewUrl::App` laesst sich nicht an `navigate` geben,
|
||||||
|
/// darum die feste Adresse.
|
||||||
|
fn setup_page_url(windows: bool) -> tauri::Url {
|
||||||
|
let base = if windows {
|
||||||
|
"http://tauri.localhost/setup.html"
|
||||||
|
} else {
|
||||||
|
"tauri://localhost/setup.html"
|
||||||
|
};
|
||||||
|
tauri::Url::parse(base).expect("setup_page_url: feste Adresse muss gueltig sein")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Liest die gespeicherte Server-Adresse aus dem Store -- einzige Lesestelle
|
||||||
|
/// (Start, `get_server_url`, `open_server`, Tray-Klick "update"); nur
|
||||||
|
/// nicht-leere Werte gelten als gespeichert.
|
||||||
|
fn stored_server_url(app: &AppHandle) -> Option<String> {
|
||||||
|
let store = app.store("config.json").ok()?;
|
||||||
|
let value = store.get("server_url")?;
|
||||||
|
let s = value.as_str()?.to_string();
|
||||||
|
if s.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Menü-Handles für die Auffrischung ohne Neustart (`apply_server`,
|
||||||
|
/// `spawn_version_check`). `MenuItem` ist Send + Sync, `app.manage` verlangt
|
||||||
|
/// das.
|
||||||
|
struct TrayItems {
|
||||||
|
connected: MenuItem<Wry>,
|
||||||
|
update: MenuItem<Wry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setzt Tooltip und die gesperrte "Verbunden mit ..."-Zeile aus derselben
|
||||||
|
/// Quelle -- die EINZIGE Stelle, die beide Anzeigen setzt (Start und
|
||||||
|
/// Wechsel), sonst laufen die drei Anzeigen auseinander. `set_tooltip` ist
|
||||||
|
/// unter Linux ein No-Op (Tauri-Doku); die Menüzeile bleibt dort die Anzeige.
|
||||||
|
/// Fehler werden wie im Bestand ignoriert.
|
||||||
|
fn apply_server(app: &AppHandle, url: Option<&str>) {
|
||||||
|
let (tooltip, line) = tray_labels(url);
|
||||||
|
if let Some(tray) = app.tray_by_id("main") {
|
||||||
|
let _ = tray.set_tooltip(Some(tooltip));
|
||||||
|
}
|
||||||
|
let _ = app.state::<TrayItems>().connected.set_text(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Versionsprüfung, aus `setup` herausgezogen, damit sie nach einem
|
||||||
|
/// Serverwechsel erneut laufen kann (async, non-blocking wie bisher). Setzt
|
||||||
|
/// den Update-Eintrag zuerst auf den Standardtext zurück -- nach einem
|
||||||
|
/// Wechsel darf kein Hinweis des alten Servers stehen bleiben.
|
||||||
|
fn spawn_version_check(app: AppHandle, server_url: String) {
|
||||||
|
let update_item = app.state::<TrayItems>().update.clone();
|
||||||
|
let _ = update_item.set_text(UPDATE_ITEM_DEFAULT);
|
||||||
|
let _ = update_item.set_enabled(false);
|
||||||
|
|
||||||
|
let app_version = env!("CARGO_PKG_VERSION").to_string();
|
||||||
|
let app_commit = env!("APP_COMMIT").to_string();
|
||||||
|
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
|
||||||
|
.notification()
|
||||||
|
.builder()
|
||||||
|
.title("Tessera-Update")
|
||||||
|
.body(body)
|
||||||
|
.show();
|
||||||
|
let _ = update_item.set_text(menu_text);
|
||||||
|
let _ = update_item.set_enabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn check_server(url: String) -> Result<String, String> {
|
||||||
|
parse_server_url(&url)?;
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.timeout(Duration::from_secs(8))
|
.timeout(Duration::from_secs(8))
|
||||||
@@ -106,13 +247,16 @@ async fn check_server(url: String) -> Result<String, String> {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn save_server_url(app: AppHandle, url: String) -> Result<(), String> {
|
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 parsed = parse_server_url(&url)?;
|
||||||
let normalized = parsed.as_str().to_string();
|
let normalized = parsed.as_str().to_string();
|
||||||
|
|
||||||
let store = app.store("config.json").map_err(|e| e.to_string())?;
|
let store = app.store("config.json").map_err(|e| e.to_string())?;
|
||||||
store.set("server_url", serde_json::json!(normalized));
|
store.set("server_url", serde_json::json!(normalized));
|
||||||
store.save().map_err(|e| e.to_string())?;
|
store.save().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
apply_server(&app, Some(&normalized));
|
||||||
|
spawn_version_check(app.clone(), normalized.clone());
|
||||||
|
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
let _ = window.navigate(with_desktop_marker(&parsed));
|
let _ = window.navigate(with_desktop_marker(&parsed));
|
||||||
}
|
}
|
||||||
@@ -120,6 +264,30 @@ fn save_server_url(app: AppHandle, url: String) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Liefert NUR den gespeicherten Wert `server_url` aus dem Store, nichts
|
||||||
|
/// anderes. Nur vom lokalen Ursprung aufrufbar (T-JN2-01): capabilities/
|
||||||
|
/// default.json hat keinen `remote`-Block, darum verweigert Tauri App-
|
||||||
|
/// Commands aus der Server-Seite (tauri-2.11.3 webview/mod.rs Z. 1819-1823).
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_server_url(app: AppHandle) -> Option<String> {
|
||||||
|
stored_server_url(&app)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigiert das Fenster `main` zur gespeicherten Server-Adresse -- Gegenstück
|
||||||
|
/// zum "Abbrechen"-Knopf der Setup-Seite im Änderungsmodus.
|
||||||
|
#[tauri::command]
|
||||||
|
fn open_server(app: AppHandle) -> Result<(), String> {
|
||||||
|
let stored = stored_server_url(&app)
|
||||||
|
.ok_or_else(|| "Es ist keine Server-Adresse gespeichert.".to_string())?;
|
||||||
|
let parsed = parse_server_url(&stored)?;
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
window
|
||||||
|
.navigate(with_desktop_marker(&parsed))
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
let app = tauri::Builder::default()
|
let app = tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_store::Builder::new().build())
|
.plugin(tauri_plugin_store::Builder::new().build())
|
||||||
@@ -130,31 +298,37 @@ pub fn run() {
|
|||||||
None,
|
None,
|
||||||
))
|
))
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.invoke_handler(tauri::generate_handler![check_server, save_server_url])
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
check_server,
|
||||||
|
save_server_url,
|
||||||
|
get_server_url,
|
||||||
|
open_server
|
||||||
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let store = app.store("config.json")?;
|
let server_url = stored_server_url(app.handle());
|
||||||
let server_url = store.get("server_url");
|
|
||||||
|
|
||||||
let mut url_for_check: Option<String> = None;
|
if let Some(url) = &server_url {
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
if let Some(url_value) = server_url {
|
if let Ok(parsed) = parse_server_url(url) {
|
||||||
if let Some(url_str) = url_value.as_str() {
|
let _ = window.navigate(with_desktop_marker(&parsed));
|
||||||
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.
|
// Tray menu: Verbunden mit ... (gesperrt) · — · Öffnen ·
|
||||||
// "update" bleibt gesperrt, bis die Versionspruefung eine neuere Version
|
// Server-Adresse ändern... · Update herunterladen · — ·
|
||||||
// findet; "autostart" spiegelt den tatsaechlichen Systemzustand beim Start.
|
// Autostart-Haken · — · Beenden.
|
||||||
|
// "connected" ist nicht anklickbar; "update" bleibt gesperrt, bis
|
||||||
|
// die Versionspruefung eine neuere Version findet; "autostart"
|
||||||
|
// spiegelt den tatsaechlichen Systemzustand beim Start.
|
||||||
|
let connected =
|
||||||
|
MenuItemBuilder::with_id("connected", tray_labels(server_url.as_deref()).1)
|
||||||
|
.enabled(false)
|
||||||
|
.build(app)?;
|
||||||
let open = MenuItemBuilder::with_id("open", "Öffnen").build(app)?;
|
let open = MenuItemBuilder::with_id("open", "Öffnen").build(app)?;
|
||||||
let update = MenuItemBuilder::with_id("update", "Update herunterladen")
|
let change_server =
|
||||||
|
MenuItemBuilder::with_id("change_server", "Server-Adresse ändern…").build(app)?;
|
||||||
|
let update = MenuItemBuilder::with_id("update", UPDATE_ITEM_DEFAULT)
|
||||||
.enabled(false)
|
.enabled(false)
|
||||||
.build(app)?;
|
.build(app)?;
|
||||||
let autostart_label = if cfg!(target_os = "windows") {
|
let autostart_label = if cfg!(target_os = "windows") {
|
||||||
@@ -168,7 +342,10 @@ pub fn run() {
|
|||||||
let quit = MenuItemBuilder::with_id("quit", "Beenden").build(app)?;
|
let quit = MenuItemBuilder::with_id("quit", "Beenden").build(app)?;
|
||||||
|
|
||||||
let menu = MenuBuilder::new(app)
|
let menu = MenuBuilder::new(app)
|
||||||
|
.item(&connected)
|
||||||
|
.separator()
|
||||||
.item(&open)
|
.item(&open)
|
||||||
|
.item(&change_server)
|
||||||
.item(&update)
|
.item(&update)
|
||||||
.separator()
|
.separator()
|
||||||
.item(&autostart)
|
.item(&autostart)
|
||||||
@@ -176,13 +353,17 @@ pub fn run() {
|
|||||||
.item(&quit)
|
.item(&quit)
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
let server_for_menu = url_for_check.clone();
|
app.manage(TrayItems {
|
||||||
|
connected: connected.clone(),
|
||||||
|
update: update.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
let autostart_for_menu = autostart.clone();
|
let autostart_for_menu = autostart.clone();
|
||||||
|
|
||||||
let _tray = TrayIconBuilder::new()
|
let _tray = TrayIconBuilder::with_id("main")
|
||||||
.icon(app.default_window_icon().unwrap().clone())
|
.icon(app.default_window_icon().unwrap().clone())
|
||||||
.menu(&menu)
|
.menu(&menu)
|
||||||
.tooltip("Tessera")
|
.tooltip(tray_labels(server_url.as_deref()).0)
|
||||||
.show_menu_on_left_click(false)
|
.show_menu_on_left_click(false)
|
||||||
.on_menu_event(move |app, event| match event.id().as_ref() {
|
.on_menu_event(move |app, event| match event.id().as_ref() {
|
||||||
"open" => {
|
"open" => {
|
||||||
@@ -193,8 +374,16 @@ pub fn run() {
|
|||||||
let _ = w.set_focus();
|
let _ = w.set_focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"change_server" => {
|
||||||
|
if let Some(w) = app.get_webview_window("main") {
|
||||||
|
let _ = w.navigate(setup_page_url(cfg!(windows)));
|
||||||
|
let _ = w.unminimize();
|
||||||
|
let _ = w.show();
|
||||||
|
let _ = w.set_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
"update" => {
|
"update" => {
|
||||||
if let Some(server) = &server_for_menu {
|
if let Some(server) = stored_server_url(app) {
|
||||||
let target = format!(
|
let target = format!(
|
||||||
"{}/settings/general/desktop",
|
"{}/settings/general/desktop",
|
||||||
server.trim_end_matches('/')
|
server.trim_end_matches('/')
|
||||||
@@ -240,40 +429,10 @@ pub fn run() {
|
|||||||
})
|
})
|
||||||
.build(app)?;
|
.build(app)?;
|
||||||
|
|
||||||
// Version check (async, non-blocking)
|
apply_server(app.handle(), server_url.as_deref());
|
||||||
if let Some(server_url) = url_for_check {
|
|
||||||
let app_handle = app.handle().clone();
|
if let Some(url) = server_url {
|
||||||
let app_version = env!("CARGO_PKG_VERSION").to_string();
|
spawn_version_check(app.handle().clone(), url);
|
||||||
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(())
|
Ok(())
|
||||||
@@ -346,4 +505,95 @@ mod tests {
|
|||||||
"Neuer Beta-Stand abc1234 verfügbar – Download über das Symbol im Infobereich."
|
"Neuer Beta-Stand abc1234 verfügbar – Download über das Symbol im Infobereich."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn server_host_mit_pfad_und_ohne_port() {
|
||||||
|
assert_eq!(
|
||||||
|
server_host(Some("https://tessera.ctl.de/")),
|
||||||
|
Some("tessera.ctl.de".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn server_host_mit_nicht_standard_port() {
|
||||||
|
assert_eq!(
|
||||||
|
server_host(Some("http://localhost:3000/")),
|
||||||
|
Some("localhost:3000".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn server_host_laesst_standardport_pfad_und_query_weg() {
|
||||||
|
assert_eq!(
|
||||||
|
server_host(Some("https://host:443/pfad?x=1")),
|
||||||
|
Some("host".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn server_host_none_bei_none() {
|
||||||
|
assert_eq!(server_host(None), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn server_host_none_bei_leerem_string() {
|
||||||
|
assert_eq!(server_host(Some("")), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn server_host_none_bei_unparsbarer_adresse() {
|
||||||
|
assert_eq!(server_host(Some("kein url")), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tray_labels_mit_host() {
|
||||||
|
let (tooltip, line) = tray_labels(Some("https://tessera.ctl.de/"));
|
||||||
|
assert_eq!(tooltip, "Tessera – tessera.ctl.de");
|
||||||
|
assert_eq!(line, "Verbunden mit tessera.ctl.de");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tray_labels_ohne_adresse() {
|
||||||
|
let (tooltip, line) = tray_labels(None);
|
||||||
|
assert_eq!(tooltip, "Tessera – nicht verbunden");
|
||||||
|
assert_eq!(line, "Nicht verbunden");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn setup_page_url_unter_windows() {
|
||||||
|
assert_eq!(
|
||||||
|
setup_page_url(true).as_str(),
|
||||||
|
"http://tauri.localhost/setup.html"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn setup_page_url_unter_linux() {
|
||||||
|
assert_eq!(
|
||||||
|
setup_page_url(false).as_str(),
|
||||||
|
"tauri://localhost/setup.html"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_server_url_gueltige_adresse() {
|
||||||
|
let parsed = parse_server_url("https://tessera.ctl.de").unwrap();
|
||||||
|
assert_eq!(parsed.as_str(), "https://tessera.ctl.de/");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_server_url_lehnt_fremdes_schema_ab() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_server_url("ftp://host"),
|
||||||
|
Err("Es sind nur Adressen mit http oder https erlaubt.".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_server_url_lehnt_unparsbare_adresse_ab() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_server_url("kein url"),
|
||||||
|
Err("Diese Adresse ist ungültig.".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,6 +140,25 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.current-server {
|
||||||
|
display: none;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: oklch(0.75 0 0);
|
||||||
|
margin: -20px 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.secondary {
|
||||||
|
margin-top: 12px;
|
||||||
|
background: transparent;
|
||||||
|
color: oklch(0.85 0 0);
|
||||||
|
border: 1px solid oklch(0.30 0.01 260);
|
||||||
|
}
|
||||||
|
|
||||||
|
button[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -155,7 +174,8 @@
|
|||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
<h1>Tessera</h1>
|
<h1>Tessera</h1>
|
||||||
<p class="subtitle">Desktop-App einrichten</p>
|
<p class="subtitle" id="subtitle">Desktop-App einrichten</p>
|
||||||
|
<p id="current-server" class="current-server"></p>
|
||||||
|
|
||||||
<label for="server-url">Adresse Ihres Tessera-Servers</label>
|
<label for="server-url">Adresse Ihres Tessera-Servers</label>
|
||||||
<p class="hint">Das ist die Adresse, unter der Sie Tessera auch im Browser öffnen.</p>
|
<p class="hint">Das ist die Adresse, unter der Sie Tessera auch im Browser öffnen.</p>
|
||||||
@@ -171,6 +191,7 @@
|
|||||||
<p id="info-msg" class="info-message"></p>
|
<p id="info-msg" class="info-message"></p>
|
||||||
|
|
||||||
<button id="connect-btn" type="button">Verbinden</button>
|
<button id="connect-btn" type="button">Verbinden</button>
|
||||||
|
<button id="cancel-btn" type="button" class="secondary" hidden>Abbrechen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
@@ -178,9 +199,12 @@
|
|||||||
|
|
||||||
const urlInput = document.getElementById('server-url');
|
const urlInput = document.getElementById('server-url');
|
||||||
const connectBtn = document.getElementById('connect-btn');
|
const connectBtn = document.getElementById('connect-btn');
|
||||||
|
const cancelBtn = document.getElementById('cancel-btn');
|
||||||
const errorMsg = document.getElementById('error-msg');
|
const errorMsg = document.getElementById('error-msg');
|
||||||
const warningMsg = document.getElementById('warning-msg');
|
const warningMsg = document.getElementById('warning-msg');
|
||||||
const infoMsg = document.getElementById('info-msg');
|
const infoMsg = document.getElementById('info-msg');
|
||||||
|
const subtitle = document.getElementById('subtitle');
|
||||||
|
const currentServer = document.getElementById('current-server');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate a server URL using the URL constructor.
|
* Validate a server URL using the URL constructor.
|
||||||
@@ -303,6 +327,50 @@
|
|||||||
|
|
||||||
// Clear error messages on input change
|
// Clear error messages on input change
|
||||||
urlInput.addEventListener('input', clearMessages);
|
urlInput.addEventListener('input', clearMessages);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aenderungsmodus: die Adresse steht bereits im Store (get_server_url
|
||||||
|
* liefert einen Wert). Feld vorbelegen, aktuelle Adresse anzeigen und
|
||||||
|
* den "Abbrechen"-Knopf einblenden. Der eingesetzte Wert kommt per
|
||||||
|
* textContent/value in die Seite, nie als HTML (T-JN2-05).
|
||||||
|
*/
|
||||||
|
function enterChangeMode(current) {
|
||||||
|
urlInput.value = current;
|
||||||
|
currentServer.textContent = 'Aktuell verbunden mit: ' + current;
|
||||||
|
currentServer.style.display = 'block';
|
||||||
|
subtitle.textContent = 'Server-Adresse ändern';
|
||||||
|
cancelBtn.hidden = false;
|
||||||
|
urlInput.focus();
|
||||||
|
urlInput.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelBtn.addEventListener('click', async () => {
|
||||||
|
clearMessages();
|
||||||
|
cancelBtn.disabled = true;
|
||||||
|
try {
|
||||||
|
await invoke('open_server');
|
||||||
|
} catch (err) {
|
||||||
|
showError(String(err));
|
||||||
|
cancelBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Beim Laden pruefen, ob bereits eine Server-Adresse gespeichert ist --
|
||||||
|
// dann ist dies der Aenderungsmodus, nicht der Erststart. Ohne
|
||||||
|
// gespeicherte Adresse (oder wenn der Aufruf fehlschlaegt) bleibt es
|
||||||
|
// beim bisherigen Erststart-Verhalten.
|
||||||
|
async function init() {
|
||||||
|
try {
|
||||||
|
const current = await invoke('get_server_url');
|
||||||
|
if (typeof current === 'string' && current) {
|
||||||
|
enterChangeMode(current);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Erststart-Verhalten: Feld bleibt leer, kein Aenderungsmodus.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user