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 tauri::{
|
||||
menu::{CheckMenuItemBuilder, MenuBuilder, MenuItemBuilder},
|
||||
menu::{CheckMenuItemBuilder, MenuBuilder, MenuItem, MenuItemBuilder},
|
||||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||
AppHandle, Manager, RunEvent, WindowEvent,
|
||||
AppHandle, Manager, RunEvent, WindowEvent, Wry,
|
||||
};
|
||||
use tauri_plugin_autostart::{MacosLauncher, ManagerExt};
|
||||
use tauri_plugin_notification::NotificationExt;
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
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)]
|
||||
struct VersionResponse {
|
||||
version: String,
|
||||
@@ -71,12 +77,147 @@ fn update_labels(version_changed: bool, version: &str, commit: &str) -> (String,
|
||||
}
|
||||
}
|
||||
|
||||
#[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())?;
|
||||
/// Parst eine vom Nutzer eingegebene Server-Adresse und erlaubt nur http/https
|
||||
/// -- dieselben Fehlertexte wie bisher inline in `check_server`. `check_server`,
|
||||
/// `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" {
|
||||
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()
|
||||
.timeout(Duration::from_secs(8))
|
||||
@@ -106,13 +247,16 @@ async fn check_server(url: String) -> Result<String, String> {
|
||||
|
||||
#[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 parsed = parse_server_url(&url)?;
|
||||
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())?;
|
||||
|
||||
apply_server(&app, Some(&normalized));
|
||||
spawn_version_check(app.clone(), normalized.clone());
|
||||
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.navigate(with_desktop_marker(&parsed));
|
||||
}
|
||||
@@ -120,6 +264,30 @@ fn save_server_url(app: AppHandle, url: String) -> Result<(), String> {
|
||||
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() {
|
||||
let app = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
@@ -130,31 +298,37 @@ pub fn run() {
|
||||
None,
|
||||
))
|
||||
.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| {
|
||||
let store = app.store("config.json")?;
|
||||
let server_url = store.get("server_url");
|
||||
let server_url = stored_server_url(app.handle());
|
||||
|
||||
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(url) = &server_url {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
if let Ok(parsed) = url_str.parse::<tauri::Url>() {
|
||||
if let Ok(parsed) = parse_server_url(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.
|
||||
// Tray menu: Verbunden mit ... (gesperrt) · — · Öffnen ·
|
||||
// Server-Adresse ändern... · Update herunterladen · — ·
|
||||
// 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 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)
|
||||
.build(app)?;
|
||||
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 menu = MenuBuilder::new(app)
|
||||
.item(&connected)
|
||||
.separator()
|
||||
.item(&open)
|
||||
.item(&change_server)
|
||||
.item(&update)
|
||||
.separator()
|
||||
.item(&autostart)
|
||||
@@ -176,13 +353,17 @@ pub fn run() {
|
||||
.item(&quit)
|
||||
.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 _tray = TrayIconBuilder::new()
|
||||
let _tray = TrayIconBuilder::with_id("main")
|
||||
.icon(app.default_window_icon().unwrap().clone())
|
||||
.menu(&menu)
|
||||
.tooltip("Tessera")
|
||||
.tooltip(tray_labels(server_url.as_deref()).0)
|
||||
.show_menu_on_left_click(false)
|
||||
.on_menu_event(move |app, event| match event.id().as_ref() {
|
||||
"open" => {
|
||||
@@ -193,8 +374,16 @@ pub fn run() {
|
||||
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" => {
|
||||
if let Some(server) = &server_for_menu {
|
||||
if let Some(server) = stored_server_url(app) {
|
||||
let target = format!(
|
||||
"{}/settings/general/desktop",
|
||||
server.trim_end_matches('/')
|
||||
@@ -240,40 +429,10 @@ pub fn run() {
|
||||
})
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
apply_server(app.handle(), server_url.as_deref());
|
||||
|
||||
if let Some(url) = server_url {
|
||||
spawn_version_check(app.handle().clone(), url);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -346,4 +505,95 @@ mod tests {
|
||||
"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;
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
@@ -155,7 +174,8 @@
|
||||
</g>
|
||||
</svg>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<button id="connect-btn" type="button">Verbinden</button>
|
||||
<button id="cancel-btn" type="button" class="secondary" hidden>Abbrechen</button>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
@@ -178,9 +199,12 @@
|
||||
|
||||
const urlInput = document.getElementById('server-url');
|
||||
const connectBtn = document.getElementById('connect-btn');
|
||||
const cancelBtn = document.getElementById('cancel-btn');
|
||||
const errorMsg = document.getElementById('error-msg');
|
||||
const warningMsg = document.getElementById('warning-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.
|
||||
@@ -303,6 +327,50 @@
|
||||
|
||||
// Clear error messages on input change
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user