Selbst-Update-Funktion ergänzt (Tauri Updater-Plugin) + App-Branding

"dossier" statt "Dossier".

Prüft beim Start still gegen ein Gitea-Release-Manifest (latest.json),
bietet im Settings-/About-Dialog einen manuellen Check sowie einen
Versionsverlauf mit Rollback-Download (versions.json, eigener schlanker
Index neben dem Updater-Manifest). Dafür: tauri-plugin-updater/process/
http/shell + zugehörige Capabilities, neue App-Icons (auch für die
plattformspezifischen Bundle-Formate), sowie ein kleingeschriebener
Produktname samt erweitertem nativen Info-Fenster (macOS-About).
This commit is contained in:
2026-07-31 17:03:23 +02:00
parent be7a47c785
commit 63dba0592c
38 changed files with 1299 additions and 35 deletions
+43 -2
View File
@@ -1,17 +1,22 @@
{ {
"name": "cad", "name": "dossier",
"version": "0.0.0", "version": "0.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cad", "name": "dossier",
"version": "0.0.0", "version": "0.0.0",
"license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@mlightcad/libredwg-web": "^0.7.7", "@mlightcad/libredwg-web": "^0.7.7",
"@tauri-apps/api": "^2.11.1", "@tauri-apps/api": "^2.11.1",
"@tauri-apps/plugin-dialog": "^2.7.1", "@tauri-apps/plugin-dialog": "^2.7.1",
"@tauri-apps/plugin-fs": "^2.5.1", "@tauri-apps/plugin-fs": "^2.5.1",
"@tauri-apps/plugin-http": "^2.5.9",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-shell": "^2.3.5",
"@tauri-apps/plugin-updater": "^2.10.1",
"delaunator": "^5.1.0", "delaunator": "^5.1.0",
"dxf-parser": "^1.1.2", "dxf-parser": "^1.1.2",
"jspdf": "^4.2.1", "jspdf": "^4.2.1",
@@ -1862,6 +1867,42 @@
"@tauri-apps/api": "^2.11.0" "@tauri-apps/api": "^2.11.0"
} }
}, },
"node_modules/@tauri-apps/plugin-http": {
"version": "2.5.9",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-http/-/plugin-http-2.5.9.tgz",
"integrity": "sha512-lCiY0+vs4HvIUSvZrBs8TC3TiCB0MOPRmiUjTq4prW7SlcJE2jdLeT6KBsJrT9Tlplufl7W1pY6SFAO3gCWxDA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.11.0"
}
},
"node_modules/@tauri-apps/plugin-process": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.3.1.tgz",
"integrity": "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.8.0"
}
},
"node_modules/@tauri-apps/plugin-shell": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz",
"integrity": "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.10.1"
}
},
"node_modules/@tauri-apps/plugin-updater": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.10.1.tgz",
"integrity": "sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.10.1"
}
},
"node_modules/@tweenjs/tween.js": { "node_modules/@tweenjs/tween.js": {
"version": "23.1.3", "version": "23.1.3",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
+4
View File
@@ -29,6 +29,10 @@
"@tauri-apps/api": "^2.11.1", "@tauri-apps/api": "^2.11.1",
"@tauri-apps/plugin-dialog": "^2.7.1", "@tauri-apps/plugin-dialog": "^2.7.1",
"@tauri-apps/plugin-fs": "^2.5.1", "@tauri-apps/plugin-fs": "^2.5.1",
"@tauri-apps/plugin-http": "^2.5.9",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-shell": "^2.3.5",
"@tauri-apps/plugin-updater": "^2.10.1",
"delaunator": "^5.1.0", "delaunator": "^5.1.0",
"dxf-parser": "^1.1.2", "dxf-parser": "^1.1.2",
"jspdf": "^4.2.1", "jspdf": "^4.2.1",
+778 -2
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -39,6 +39,10 @@ tauri-build = { version = "2", features = [] }
tauri = { version = "2", features = [] } tauri = { version = "2", features = [] }
tauri-plugin-dialog = "2" tauri-plugin-dialog = "2"
tauri-plugin-fs = "2" tauri-plugin-fs = "2"
tauri-plugin-updater = "2"
tauri-plugin-process = "2"
tauri-plugin-http = "2"
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
geometry = { path = "geometry" } geometry = { path = "geometry" }
+11 -2
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default", "identifier": "default",
"description": "Basis-Permissions + Fenstersteuerung fuer die randlose (decorations:false) Titelleiste: Minimieren/Maximieren/Schliessen + Ziehen der eigenen Titelleiste (data-tauri-drag-region). Menu: fuer die native macOS-Systemmenueleiste (src/native/appMenu.ts). Webview/Event: fuer die nativen Ressourcen-/Einstellungs-/Zeichnungsebenen-/Ebeneneinstellungs-Fenster + ihre Sync-Bruecken (src/native/*Window.ts) - gilt fuer alle Fensterlabels, da jedes Events senden/empfangen muss.", "description": "Basis-Permissions + Fenstersteuerung fuer die randlose (decorations:false) Titelleiste: Minimieren/Maximieren/Schliessen + Ziehen der eigenen Titelleiste (data-tauri-drag-region). Menu: fuer die native macOS-Systemmenueleiste (src/native/appMenu.ts). Webview/Event: fuer die nativen Ressourcen-/Einstellungs-/Zeichnungsebenen-/Ebeneneinstellungs-Fenster + ihre Sync-Bruecken (src/native/*Window.ts) - gilt fuer alle Fensterlabels, da jedes Events senden/empfangen muss. Dialog/Fs: nativer Oeffnen- UND Speichern-Dialog fuer Projektdateien (.obp/.json) plus Lesen/Schreiben der gewaehlten Datei (io/projectFile.ts, io/saveFile.ts) - open+read fuer Projekt laden, save+write fuer Projekt/Export speichern. Updater/Process: Selbst-Update-Check beim Start (src/update/checkForUpdate.ts) + Neustart nach Installation. Http: Versionsverlauf (versions.json vom Gitea-Release) ohne Browser-CORS, Scope auf den eigenen Gitea-Host begrenzt. Shell:allow-open: oeffnet den DMG-Download einer aelteren Version im System-Browser (Rollback).",
"windows": ["main", "resources", "settings", "drawing-levels", "layer-settings", "context-import"], "windows": ["main", "resources", "settings", "drawing-levels", "layer-settings", "context-import"],
"permissions": [ "permissions": [
"core:default", "core:default",
@@ -16,7 +16,16 @@
"core:webview:allow-create-webview-window", "core:webview:allow-create-webview-window",
"core:menu:default", "core:menu:default",
"core:event:default", "core:event:default",
"dialog:allow-open",
"dialog:allow-save", "dialog:allow-save",
"fs:allow-write-text-file" "fs:allow-read-text-file",
"fs:allow-write-text-file",
"updater:default",
"process:allow-restart",
{
"identifier": "http:default",
"allow": [{ "url": "https://git.openbureau.ch/*" }]
},
"shell:allow-open"
] ]
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+12
View File
@@ -72,6 +72,18 @@ pub fn run() {
// im Tauri-Fenster einen echten Save-Dialog zeigen statt still zu laden. // im Tauri-Fenster einen echten Save-Dialog zeigen statt still zu laden.
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
// Selbst-Update: prueft beim Start gegen `endpoints` aus tauri.conf.json
// (Gitea-Release-Asset `latest.json`), Installation via JS-Seite
// (@tauri-apps/plugin-updater). `tauri_plugin_process` liefert `relaunch()`
// fuer den Neustart nach der Installation.
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init())
// HTTP-Requests aus der Webview (Versionsverlauf, versions.json vom
// selben Gitea-Release) ohne Browser-CORS-Einschraenkung; Shell: oeffnet
// den DMG-Download einer aelteren Version im System-Browser (Rollback,
// s. src/update/checkForUpdate.ts).
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_shell::init())
// Haelt die pro Instanz offen gelockten Projektdatei-Handles (lock.rs). // Haelt die pro Instanz offen gelockten Projektdatei-Handles (lock.rs).
.manage(lock::LockState::default()) .manage(lock::LockState::default())
.setup(|_app| { .setup(|_app| {
+18 -3
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Dossier", "productName": "dossier",
"version": "0.1.0", "version": "0.1.0",
"identifier": "ch.dossier.cad", "identifier": "ch.dossier.cad",
"build": { "build": {
@@ -11,7 +11,7 @@
"windows": [ "windows": [
{ {
"label": "main", "label": "main",
"title": "Dossier", "title": "dossier",
"width": 1400, "width": 1400,
"height": 900, "height": 900,
"dragDropEnabled": false, "dragDropEnabled": false,
@@ -27,6 +27,21 @@
"bundle": { "bundle": {
"active": true, "active": true,
"targets": "all", "targets": "all",
"icon": ["icons/icon.png"] "createUpdaterArtifacts": true,
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
},
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEYyRDk3NkZGODQwRUJBNTQKUldSVXVnNkUvM2JaOHV2QWp0L05qZnpEUmt2TkN3ZWJBeFQwb1I0cExWNEpuZWU0UjZiazdQNU0K",
"endpoints": [
"https://git.openbureau.ch/karim/DOSSIER-STANDALONE/releases/download/latest/latest.json"
]
}
} }
} }
+8
View File
@@ -74,6 +74,7 @@ import {
isTauriRuntime, isTauriRuntime,
} from "./io/projectFile"; } from "./io/projectFile";
import type { LockInfo } from "./io/projectFile"; import type { LockInfo } from "./io/projectFile";
import { checkForUpdate } from "./update/checkForUpdate";
import type { import type {
PlanViewHandle, PlanViewHandle,
PlanSelection, PlanSelection,
@@ -541,6 +542,13 @@ export default function App() {
return () => setDxfImportTrigger(null); return () => setDxfImportTrigger(null);
}, []); }, []);
// Selbst-Update: einmalig beim Start prüfen (still — meldet sich nur bei
// einem gefundenen Update oder einem echten Fehler, s. checkForUpdate.ts).
// No-op ausserhalb von Tauri (Vite-Dev-Server im Browser).
useEffect(() => {
checkForUpdate(true);
}, []);
// Panel-Layout (Docks/Tabs/Größen) — aus dem Store-Slice (Init lädt aus // Panel-Layout (Docks/Tabs/Größen) — aus dem Store-Slice (Init lädt aus
// localStorage + reconcile). Bei jeder Änderung zurückspeichern. // localStorage + reconcile). Bei jeder Änderung zurückspeichern.
const layout = useStore((s) => s.layout); const layout = useStore((s) => s.layout);
+26 -3
View File
@@ -140,7 +140,7 @@ export const de = {
"menu.native.view": "Ansicht", "menu.native.view": "Ansicht",
"menu.native.window": "Fenster", "menu.native.window": "Fenster",
"menu.native.panels": "Panels", "menu.native.panels": "Panels",
"menu.native.about": "Über Dossier", "menu.native.about": "Über dossier",
"menu.native.quit": "Beenden", "menu.native.quit": "Beenden",
// ── Layout-Menü (Topbar) ───────────────────────────────────────────────── // ── Layout-Menü (Topbar) ─────────────────────────────────────────────────
@@ -1318,12 +1318,22 @@ export const de = {
"exportSave.fmt.stl": "STL (3D-Mesh)", "exportSave.fmt.stl": "STL (3D-Mesh)",
"about.title": "Über dossier", "about.title": "Über dossier",
"about.version": "Version", "about.version": "Version",
"about.desc": "Open-Source-CAAD (Computer Aided Architecture Design) — schöne, normgerechte Pläne aus einem semantischen Gebäudemodell. Als Desktop-App (vollständige Fassung) und im Browser zugänglich.", "about.desc": "Open Source Computer Aided Architecture Design — 2D-Vektordesign sowie normgerechte Pläne aus einem semantischen Gebäudemodell.",
"about.copyright": "© 2026 Karim Gabriele Varano", "about.copyright": "© 2026 Karim Gabriele Varano",
"about.license": "Lizenz: AGPL-3.0-or-later", "about.license": "Lizenz: AGPL-3.0-or-later",
"about.suite": "Teil der openbureau-Suite", "about.suite": "Teil von openbureau",
"about.licenses": "Open-Source-Lizenzen (Drittkomponenten)", "about.licenses": "Open-Source-Lizenzen (Drittkomponenten)",
"about.close": "Schließen", "about.close": "Schließen",
"about.checkUpdate": "Nach Updates suchen",
"about.checkingUpdate": "Suche nach Updates …",
"update.title": "Update",
"update.upToDate": "Du hast bereits die aktuellste Version.",
"update.availableBody": "Version {version} ist verfügbar. Jetzt herunterladen und installieren? Die App startet danach neu.",
"update.installNow": "Installieren",
"update.installLater": "Später",
"update.checkFailed": "Update-Prüfung fehlgeschlagen: {error}",
"update.installFailed": "Installation fehlgeschlagen: {error}",
// ── Einstellungs-Fenster ────────────────────────────────────────────────── // ── Einstellungs-Fenster ──────────────────────────────────────────────────
"topbar.settings": "Einstellungen", "topbar.settings": "Einstellungen",
@@ -1344,6 +1354,19 @@ export const de = {
"settings.referenceElevation": "Referenzhöhe EG (m ü. M.)", "settings.referenceElevation": "Referenzhöhe EG (m ü. M.)",
"settings.referenceElevation.hint": "settings.referenceElevation.hint":
"Nur gespeichert — wird noch nicht verwendet. Folgt für Terrain-Draping/reale Höhen relativ zum Erdgeschoss.", "Nur gespeichert — wird noch nicht verwendet. Folgt für Terrain-Draping/reale Höhen relativ zum Erdgeschoss.",
"settings.section.update": "Update",
"settings.update.autoCheck": "Automatisch nach Updates suchen",
"settings.update.snooze.active": "Aktiv",
"settings.update.snooze.1day": "1 Tag pausieren",
"settings.update.snooze.1week": "1 Woche pausieren",
"settings.update.snooze.1month": "1 Monat pausieren",
"settings.update.snoozedUntil": "Pausiert bis {date}. Manuelles Suchen funktioniert weiterhin.",
"settings.update.versionHistory": "Frühere Versionen",
"settings.update.versionHistory.hint":
"Öffnet den Download im Browser — Installation bleibt manuell (ersetzt die aktuelle Version, wie beim ersten Installieren).",
"settings.update.loadingHistory": "Lade Versionsliste …",
"settings.update.noHistory": "Keine früheren Versionen verfügbar.",
"settings.update.download": "Herunterladen",
// ── Drag & Drop (App-weiter Datei-Drop) ────────────────────────────────── // ── Drag & Drop (App-weiter Datei-Drop) ──────────────────────────────────
"drop.hint": "DXF/DWG-Datei hier ablegen", "drop.hint": "DXF/DWG-Datei hier ablegen",
+26 -3
View File
@@ -138,7 +138,7 @@ export const en: Record<TranslationKey, string> = {
"menu.native.view": "View", "menu.native.view": "View",
"menu.native.window": "Window", "menu.native.window": "Window",
"menu.native.panels": "Panels", "menu.native.panels": "Panels",
"menu.native.about": "About Dossier", "menu.native.about": "About dossier",
"menu.native.quit": "Quit", "menu.native.quit": "Quit",
// Layout menu. // Layout menu.
@@ -1305,12 +1305,22 @@ export const en: Record<TranslationKey, string> = {
"exportSave.fmt.stl": "STL (3D mesh)", "exportSave.fmt.stl": "STL (3D mesh)",
"about.title": "About dossier", "about.title": "About dossier",
"about.version": "Version", "about.version": "Version",
"about.desc": "Open-source CAAD (Computer Aided Architecture Design) — beautiful, standard-compliant drawings from a semantic building model. Available as a desktop app (full version) and in the browser.", "about.desc": "Open Source Computer Aided Architecture Design — 2D vector design plus standards-compliant drawings from a semantic building model.",
"about.copyright": "© 2026 Karim Gabriele Varano", "about.copyright": "© 2026 Karim Gabriele Varano",
"about.license": "License: AGPL-3.0-or-later", "about.license": "License: AGPL-3.0-or-later",
"about.suite": "Part of the openbureau suite", "about.suite": "Part of openbureau",
"about.licenses": "Open-source licenses (third-party)", "about.licenses": "Open-source licenses (third-party)",
"about.close": "Close", "about.close": "Close",
"about.checkUpdate": "Check for updates",
"about.checkingUpdate": "Checking for updates …",
"update.title": "Update",
"update.upToDate": "You already have the latest version.",
"update.availableBody": "Version {version} is available. Download and install now? The app will restart afterwards.",
"update.installNow": "Install",
"update.installLater": "Later",
"update.checkFailed": "Update check failed: {error}",
"update.installFailed": "Installation failed: {error}",
// Settings dialog. // Settings dialog.
"topbar.settings": "Settings", "topbar.settings": "Settings",
@@ -1331,6 +1341,19 @@ export const en: Record<TranslationKey, string> = {
"settings.referenceElevation": "Ground floor reference elevation (m a.s.l.)", "settings.referenceElevation": "Ground floor reference elevation (m a.s.l.)",
"settings.referenceElevation.hint": "settings.referenceElevation.hint":
"Stored only — not used yet. Terrain draping / real-world elevations relative to the ground floor will follow.", "Stored only — not used yet. Terrain draping / real-world elevations relative to the ground floor will follow.",
"settings.section.update": "Update",
"settings.update.autoCheck": "Check for updates automatically",
"settings.update.snooze.active": "Active",
"settings.update.snooze.1day": "Pause for 1 day",
"settings.update.snooze.1week": "Pause for 1 week",
"settings.update.snooze.1month": "Pause for 1 month",
"settings.update.snoozedUntil": "Paused until {date}. Manual checks still work.",
"settings.update.versionHistory": "Earlier versions",
"settings.update.versionHistory.hint":
"Opens the download in your browser — installation stays manual (replaces the current version, same as the first install).",
"settings.update.loadingHistory": "Loading version list …",
"settings.update.noHistory": "No earlier versions available.",
"settings.update.download": "Download",
// Drag & drop (app-wide file drop). // Drag & drop (app-wide file drop).
"drop.hint": "Drop a DXF/DWG file here", "drop.hint": "Drop a DXF/DWG file here",
+16 -3
View File
@@ -319,14 +319,27 @@ export function useNativeAppMenu(
// macOS: der allererste Eintrag ist das App-Menü (Über/Beenden). Ohne ihn // macOS: der allererste Eintrag ist das App-Menü (Über/Beenden). Ohne ihn
// rutscht „Datei" an die Stelle, an der macOS sonst den App-Namen zeigt. // rutscht „Datei" an die Stelle, an der macOS sonst den App-Namen zeigt.
// „Über Dossier" nutzt das NATIVE macOS-Info-Fenster (PredefinedMenuItem // „Über dossier" nutzt das NATIVE macOS-Info-Fenster (PredefinedMenuItem
// „About") statt des eigenen In-App-Dialogs (AboutDialog in TopBar.tsx) — // „About") statt des eigenen In-App-Dialogs (AboutDialog in TopBar.tsx) —
// an dieser Stelle erwartet macOS exakt diesen Eintrag, kein Custom-Dialog. // an dieser Stelle erwartet macOS exakt diesen Eintrag, kein Custom-Dialog.
// Metadaten spiegeln den In-App-Dialog: kleingeschriebener Name „dossier",
// Beschreibung + Lizenz/Suite als `credits` (macOS zeigt sie als Textblock
// unter der Version), Copyright unten. Das Icon zieht macOS aus dem Bundle
// (tauri.conf.json bundle.icon → icon.icns). Auf macOS NICHT unterstützt
// und daher weggelassen: authors/comments/license/website.
const appMenu = await Submenu.new({ const appMenu = await Submenu.new({
text: "Dossier", text: "dossier",
items: [ items: [
await PredefinedMenuItem.new({ await PredefinedMenuItem.new({
item: { About: { name: "Dossier", version: APP_VERSION } }, item: {
About: {
name: "dossier",
version: APP_VERSION,
shortVersion: APP_VERSION,
copyright: t("about.copyright"),
credits: `${t("about.desc")}\n\n${t("about.license")} · ${t("about.suite")}`,
},
},
text: t("menu.native.about"), text: t("menu.native.about"),
}), }),
await PredefinedMenuItem.new({ item: "Separator" }), await PredefinedMenuItem.new({ item: "Separator" }),
+1 -1
View File
@@ -41,7 +41,7 @@ export async function openContextImportWindowNative(): Promise<void> {
} }
new WebviewWindow(CONTEXT_IMPORT_WINDOW_LABEL, { new WebviewWindow(CONTEXT_IMPORT_WINDOW_LABEL, {
url: "index.html?window=context-import", url: "index.html?window=context-import",
title: "Standort importieren — Dossier", title: "Standort importieren — dossier",
width: 480, width: 480,
height: 680, height: 680,
minWidth: 400, minWidth: 400,
+1 -1
View File
@@ -51,7 +51,7 @@ export async function openDrawingLevelsWindowNative(): Promise<void> {
} }
new WebviewWindow(DRAWING_LEVELS_WINDOW_LABEL, { new WebviewWindow(DRAWING_LEVELS_WINDOW_LABEL, {
url: "index.html?window=drawing-levels", url: "index.html?window=drawing-levels",
title: "Zeichnungsebenen — Dossier", title: "Zeichnungsebenen — dossier",
width: 340, width: 340,
height: 560, height: 560,
minWidth: 260, minWidth: 260,
+1 -1
View File
@@ -54,7 +54,7 @@ export async function openLayerSettingsWindowNative(code: string): Promise<void>
} }
new WebviewWindow(LAYER_SETTINGS_WINDOW_LABEL, { new WebviewWindow(LAYER_SETTINGS_WINDOW_LABEL, {
url: `index.html?window=layer-settings&code=${encodeURIComponent(code)}`, url: `index.html?window=layer-settings&code=${encodeURIComponent(code)}`,
title: "Ebeneneinstellungen — Dossier", title: "Ebeneneinstellungen — dossier",
width: 360, width: 360,
height: 420, height: 420,
minWidth: 300, minWidth: 300,
+1 -1
View File
@@ -47,7 +47,7 @@ export async function openResourceWindowNative(initialTab?: TabId): Promise<void
"index.html?window=resources" + (initialTab ? `&tab=${encodeURIComponent(initialTab)}` : ""); "index.html?window=resources" + (initialTab ? `&tab=${encodeURIComponent(initialTab)}` : "");
new WebviewWindow(RESOURCE_WINDOW_LABEL, { new WebviewWindow(RESOURCE_WINDOW_LABEL, {
url, url,
title: "Ressourcen — Dossier", title: "Ressourcen — dossier",
width: 780, width: 780,
height: 660, height: 660,
minWidth: 480, minWidth: 480,
+1 -1
View File
@@ -52,7 +52,7 @@ export async function openSettingsWindowNative(): Promise<void> {
} }
new WebviewWindow(SETTINGS_WINDOW_LABEL, { new WebviewWindow(SETTINGS_WINDOW_LABEL, {
url: "index.html?window=settings", url: "index.html?window=settings",
title: "Einstellungen — Dossier", title: "Einstellungen — dossier",
width: 520, width: 520,
height: 640, height: 640,
minWidth: 420, minWidth: 420,
+46 -6
View File
@@ -630,9 +630,9 @@ body {
border: 1px solid var(--border, #3a3f47); border: 1px solid var(--border, #3a3f47);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5); box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
color: var(--ink, #e6e8ea); color: var(--ink, #e6e8ea);
/* Der Dialog ist ein DOM-Kind der `.topbar` (white-space: nowrap) — ohne /* Defensiv gesetzt (unabhängig davon, ob ein Vorfahre white-space: nowrap
dieses Reset liefe der mehrzeilige Beschreibungstext einzeilig aus dem setzt) — ohne dieses Reset liefe der mehrzeilige Beschreibungstext
Dialog heraus. Hier wieder normalen Zeilenumbruch erzwingen. */ einzeilig aus dem Dialog heraus. */
white-space: normal; white-space: normal;
} }
.about-brand { .about-brand {
@@ -676,9 +676,15 @@ body {
line-height: 1.6; line-height: 1.6;
color: var(--label, #c4c8cd); color: var(--label, #c4c8cd);
} }
.about-close { .about-actions {
appearance: none; display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 18px; margin-top: 18px;
}
.about-close,
.about-check-update {
appearance: none;
padding: 6px 14px; padding: 6px 14px;
border-radius: 6px; border-radius: 6px;
border: 1px solid var(--border, #3a3f47); border: 1px solid var(--border, #3a3f47);
@@ -687,9 +693,14 @@ body {
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 12px;
} }
.about-close:hover { .about-close:hover,
.about-check-update:hover:not(:disabled) {
background: var(--accent-dim, #33383f); background: var(--accent-dim, #33383f);
} }
.about-check-update:disabled {
opacity: 0.6;
cursor: default;
}
.tag { .tag {
font-weight: 500; font-weight: 500;
@@ -5392,6 +5403,35 @@ body {
outline: none; outline: none;
box-shadow: 0 0 0 2px var(--accent-border); box-shadow: 0 0 0 2px var(--accent-border);
} }
.settings-subsection {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid var(--line);
}
.settings-select {
font-family: inherit;
font-size: 13px;
padding: 6px 9px;
border: none;
border-radius: 7px;
background: var(--field-bg);
color: var(--ink);
}
.settings-select:focus {
outline: none;
box-shadow: 0 0 0 2px var(--accent-border);
}
.settings-history-row {
display: flex;
align-items: center;
gap: 8px;
}
.settings-history-row .settings-select {
flex: 1;
}
/* ── App-weites Drop-Feedback ────────────────────────────────────────────── /* ── App-weites Drop-Feedback ──────────────────────────────────────────────
Dezenter Rahmen + Hinweis, solange eine Datei über die App gezogen wird. */ Dezenter Rahmen + Hinweis, solange eine Datei über die App gezogen wird. */
+123
View File
@@ -14,9 +14,25 @@ import { t } from "../i18n";
import { ACCENT_SWATCHES } from "../theme/accents"; import { ACCENT_SWATCHES } from "../theme/accents";
import { ColorHexField } from "./ColorHexField"; import { ColorHexField } from "./ColorHexField";
import { LayoutMenu } from "./LayoutMenu"; import { LayoutMenu } from "./LayoutMenu";
import { APP_VERSION } from "./TopBar";
import type { RendererMode } from "./StatusBar"; import type { RendererMode } from "./StatusBar";
import type { LayoutState } from "../panels/types"; import type { LayoutState } from "../panels/types";
import type { ThemeMode } from "../theme/themeMode"; import type { ThemeMode } from "../theme/themeMode";
import { checkForUpdate, getUpdateSnoozeUntil, setUpdateSnooze } from "../update/checkForUpdate";
import { getRunningVersion } from "../update/version";
import {
fetchVersionHistory,
openVersionDownload,
type VersionEntry,
} from "../update/versionHistory";
/** Pausier-Optionen fuer den stillen Start-Check (s. checkForUpdate.ts). */
const SNOOZE_OPTIONS = [
{ id: "active", ms: null },
{ id: "1day", ms: 24 * 60 * 60 * 1000 },
{ id: "1week", ms: 7 * 24 * 60 * 60 * 1000 },
{ id: "1month", ms: 30 * 24 * 60 * 60 * 1000 },
] as const;
export interface SettingsDialogProps { export interface SettingsDialogProps {
/** Hell/Dunkel — Standard ist Hell (siehe theme/themeMode.ts). */ /** Hell/Dunkel — Standard ist Hell (siehe theme/themeMode.ts). */
@@ -71,6 +87,22 @@ export function SettingsDialog({
onClose, onClose,
windowed = false, windowed = false,
}: SettingsDialogProps) { }: SettingsDialogProps) {
const [checkingUpdate, setCheckingUpdate] = useState(false);
const [runningVersion, setRunningVersion] = useState<string | null>(null);
const [snoozeUntil, setSnoozeUntilState] = useState<number | null>(() => getUpdateSnoozeUntil());
const [versionHistory, setVersionHistory] = useState<VersionEntry[] | null>(null);
const [selectedTag, setSelectedTag] = useState<string>("");
useEffect(() => {
void getRunningVersion().then(setRunningVersion);
}, []);
useEffect(() => {
void fetchVersionHistory().then((entries) => {
setVersionHistory(entries);
if (entries.length > 0) setSelectedTag(entries[0].tag);
});
}, []);
useEffect(() => { useEffect(() => {
const onKey = (e: KeyboardEvent) => { const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose(); if (e.key === "Escape") onClose();
@@ -208,6 +240,97 @@ export function SettingsDialog({
/> />
<div className="imp-note">{t("settings.referenceElevation.hint")}</div> <div className="imp-note">{t("settings.referenceElevation.hint")}</div>
</section> </section>
<section className="imp-section">
<div className="imp-section-title">{t("settings.section.update")}</div>
<div className="imp-note">
{t("about.version")} {runningVersion ?? APP_VERSION}
</div>
<button
type="button"
className="imp-btn"
disabled={checkingUpdate}
onClick={async () => {
setCheckingUpdate(true);
try {
await checkForUpdate(false);
} finally {
setCheckingUpdate(false);
}
}}
>
{checkingUpdate ? t("about.checkingUpdate") : t("about.checkUpdate")}
</button>
<div className="settings-subsection">
<label className="settings-field-label" htmlFor="settings-update-snooze">
{t("settings.update.autoCheck")}
</label>
<select
id="settings-update-snooze"
className="settings-select"
value={snoozeUntil == null ? "active" : String(snoozeUntil)}
onChange={(e) => {
const opt = SNOOZE_OPTIONS.find((o) => o.id === e.target.value);
const until = opt?.ms == null ? null : Date.now() + opt.ms;
setUpdateSnooze(until);
setSnoozeUntilState(until);
}}
>
{SNOOZE_OPTIONS.map((o) => (
<option key={o.id} value={o.id}>
{t(`settings.update.snooze.${o.id}` as const)}
</option>
))}
</select>
{snoozeUntil != null && (
<div className="imp-note">
{t("settings.update.snoozedUntil", {
date: new Date(snoozeUntil).toLocaleDateString(),
})}
</div>
)}
</div>
<div className="settings-subsection">
<label className="settings-field-label" htmlFor="settings-update-history">
{t("settings.update.versionHistory")}
</label>
{versionHistory == null ? (
<div className="imp-note">{t("settings.update.loadingHistory")}</div>
) : versionHistory.length === 0 ? (
<div className="imp-note">{t("settings.update.noHistory")}</div>
) : (
<>
<div className="settings-history-row">
<select
id="settings-update-history"
className="settings-select"
value={selectedTag}
onChange={(e) => setSelectedTag(e.target.value)}
>
{versionHistory.map((v) => (
<option key={v.tag} value={v.tag}>
{v.version} — {v.pub_date.slice(0, 10)}
</option>
))}
</select>
<button
type="button"
className="imp-btn"
onClick={() => {
const entry = versionHistory.find((v) => v.tag === selectedTag);
if (entry) void openVersionDownload(entry);
}}
>
{t("settings.update.download")}
</button>
</div>
<div className="imp-note">{t("settings.update.versionHistory.hint")}</div>
</>
)}
</div>
</section>
</div> </div>
{!windowed && ( {!windowed && (
+38 -6
View File
@@ -10,6 +10,8 @@
import type { Project } from "../model/types"; import type { Project } from "../model/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { checkForUpdate } from "../update/checkForUpdate";
import { getRunningVersion } from "../update/version";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { Dropdown } from "./Dropdown"; import { Dropdown } from "./Dropdown";
@@ -1020,6 +1022,11 @@ export const APP_VERSION = "0.1.0";
* Wortmarke. Klick ausserhalb / Esc schliesst. * Wortmarke. Klick ausserhalb / Esc schliesst.
*/ */
function AboutDialog({ onClose }: { onClose: () => void }) { function AboutDialog({ onClose }: { onClose: () => void }) {
const [checkingUpdate, setCheckingUpdate] = useState(false);
const [runningVersion, setRunningVersion] = useState<string | null>(null);
useEffect(() => {
void getRunningVersion().then(setRunningVersion);
}, []);
useEffect(() => { useEffect(() => {
const onKey = (e: KeyboardEvent) => { const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose(); if (e.key === "Escape") onClose();
@@ -1028,7 +1035,14 @@ function AboutDialog({ onClose }: { onClose: () => void }) {
return () => document.removeEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey);
}, [onClose]); }, [onClose]);
return ( // Per Portal an document.body statt als DOM-Kind der .topbar rendern: die
// .topbar bildet (position:relative + z-index) einen eigenen Stacking-
// Context, der das hohe z-index:1000 von .about-overlay einsperrt — andere
// z-index:2-Elemente ausserhalb (z. B. das Lineal-Overlay im Grundriss)
// konnten dadurch trotzdem durch den abgedunkelten Hintergrund "durch-
// scheinen". Als Portal-Kind von body liegt der Dialog in keinem fremden
// Stacking-Context mehr.
return createPortal(
<div className="about-overlay" onMouseDown={onClose}> <div className="about-overlay" onMouseDown={onClose}>
<div <div
className="about-dialog" className="about-dialog"
@@ -1041,7 +1055,7 @@ function AboutDialog({ onClose }: { onClose: () => void }) {
dossier<span className="brand-dot">.</span> dossier<span className="brand-dot">.</span>
</div> </div>
<div className="about-version"> <div className="about-version">
{t("about.version")} {APP_VERSION} {t("about.version")} {runningVersion ?? APP_VERSION}
</div> </div>
<p className="about-desc">{t("about.desc")}</p> <p className="about-desc">{t("about.desc")}</p>
<div className="about-meta"> <div className="about-meta">
@@ -1059,11 +1073,29 @@ function AboutDialog({ onClose }: { onClose: () => void }) {
<li>LibreDWG — GPL-3.0</li> <li>LibreDWG — GPL-3.0</li>
<li>acadrust — MPL-2.0</li> <li>acadrust — MPL-2.0</li>
</ul> </ul>
<button type="button" className="about-close" onClick={onClose}> <div className="about-actions">
{t("about.close")} <button
</button> type="button"
className="about-check-update"
disabled={checkingUpdate}
onClick={async () => {
setCheckingUpdate(true);
try {
await checkForUpdate(false);
} finally {
setCheckingUpdate(false);
}
}}
>
{checkingUpdate ? t("about.checkingUpdate") : t("about.checkUpdate")}
</button>
<button type="button" className="about-close" onClick={onClose}>
{t("about.close")}
</button>
</div>
</div> </div>
</div> </div>,
document.body,
); );
} }
+81
View File
@@ -0,0 +1,81 @@
// Selbst-Update: fragt `endpoints` aus tauri.conf.json ab (Gitea-Release-Asset
// `latest.json`, per `/releases/latest/download/` immer der aktuellste Release
// dieses Repos — die URL selbst muss bei neuen Versionen nicht angepasst
// werden). Nur unter Tauri aktiv; im reinen Browser/Vite-Dev-Server gibt es
// weder die Plugins noch etwas zu aktualisieren.
import { ask, message } from "@tauri-apps/plugin-dialog";
import { t } from "../i18n";
import { isTauriRuntime } from "../io/projectFile";
/** localStorage-Key fuer die Update-Pause (s. `getUpdateSnoozeUntil`). */
const SNOOZE_KEY = "dossier.updateSnoozeUntil";
/** Ende der aktuellen Update-Pause (ms epoch), `null` wenn keine aktiv/abgelaufen. */
export function getUpdateSnoozeUntil(): number | null {
const raw = localStorage.getItem(SNOOZE_KEY);
if (!raw) return null;
const n = Number(raw);
if (!Number.isFinite(n) || n <= Date.now()) return null;
return n;
}
/** Pausiert den STILLEN Start-Check bis `untilMs` (epoch); `null` hebt die Pause auf.
* Der manuelle Check (Button in Settings/About) ignoriert die Pause immer —
* das ist eine explizite Nutzeraktion. */
export function setUpdateSnooze(untilMs: number | null): void {
if (untilMs == null) localStorage.removeItem(SNOOZE_KEY);
else localStorage.setItem(SNOOZE_KEY, String(untilMs));
}
/**
* Prueft auf eine neue Version. `silent=true` (Start-Check) meldet sich nur
* bei einem GEFUNDENEN Update oder einem echten Fehler — nicht bei „aktuell",
* damit der App-Start nicht bei jedem Start einen Dialog zeigt. Respektiert
* ausserdem eine aktive Update-Pause (s. `setUpdateSnooze`). Der manuelle
* Aufruf (About-/Settings-Dialog, `silent=false`) meldet in jedem Fall ein
* Ergebnis und ignoriert die Pause.
*/
export async function checkForUpdate(silent: boolean): Promise<void> {
if (!isTauriRuntime()) return;
if (silent && getUpdateSnoozeUntil() != null) return;
const { check } = await import("@tauri-apps/plugin-updater");
let update: Awaited<ReturnType<typeof check>>;
try {
update = await check();
} catch (err) {
if (!silent) {
await message(t("update.checkFailed", { error: String(err) }), {
title: t("update.title"),
kind: "error",
});
}
return;
}
if (!update) {
if (!silent) {
await message(t("update.upToDate"), {
title: t("update.title"),
kind: "info",
});
}
return;
}
const install = await ask(t("update.availableBody", { version: update.version }), {
title: t("update.title"),
kind: "info",
okLabel: t("update.installNow"),
cancelLabel: t("update.installLater"),
});
if (!install) return;
try {
await update.downloadAndInstall();
} catch (err) {
await message(t("update.installFailed", { error: String(err) }), {
title: t("update.title"),
kind: "error",
});
return;
}
const { relaunch } = await import("@tauri-apps/plugin-process");
await relaunch();
}
+12
View File
@@ -0,0 +1,12 @@
// Tatsaechlich laufende App-Version (aus Tauris eigenen Metadaten, NICHT der
// manuell gepflegte `APP_VERSION`-String in ui/TopBar.tsx) — vermeidet, dass
// die Anzeige von der echten Installation abweicht, falls jemand beim
// Versionsbump eine der beiden Stellen vergisst.
import { isTauriRuntime } from "../io/projectFile";
/** `null` ausserhalb von Tauri (Vite-Dev-Server im Browser, keine Installation). */
export async function getRunningVersion(): Promise<string | null> {
if (!isTauriRuntime()) return null;
const { getVersion } = await import("@tauri-apps/api/app");
return getVersion();
}
+48
View File
@@ -0,0 +1,48 @@
// Versionsverlauf fuer den manuellen Rollback (Settings → Update): Tauris
// Updater-Plugin kennt nur „neuere Version installieren", kein Downgrade-
// Manifest mit Auswahl. Statt das Plugin zu verbiegen, pflegen wir einen
// eigenen, schlanken Index `versions.json` (liegt auf demselben Gitea-
// Release „latest" wie `latest.json`, s. checkForUpdate.ts) und bieten pro
// Eintrag nur den DMG-Download im System-Browser an — Installation bleibt
// der gewohnte manuelle Schritt (DMG oeffnen, App in Applications ersetzen).
import { isTauriRuntime } from "../io/projectFile";
export interface VersionEntry {
version: string;
/** Git-Tag des Releases, z. B. "v0.1.0". */
tag: string;
/** ISO-Datum. */
pub_date: string;
notes: string;
/** Dateiname der DMG innerhalb dieses Release-Tags. */
dmg: string;
}
const VERSIONS_URL =
"https://git.openbureau.ch/karim/DOSSIER-STANDALONE/releases/download/latest/versions.json";
/** Leeres Array ausserhalb von Tauri oder bei Netzwerk-/Parsefehlern (keine
* Wurf-Semantik hier — der Aufrufer zeigt einfach eine leere Liste). */
export async function fetchVersionHistory(): Promise<VersionEntry[]> {
if (!isTauriRuntime()) return [];
try {
const { fetch: tauriFetch } = await import("@tauri-apps/plugin-http");
const res = await tauriFetch(VERSIONS_URL);
if (!res.ok) return [];
const data = (await res.json()) as VersionEntry[];
return Array.isArray(data) ? data : [];
} catch {
return [];
}
}
export function downloadUrlFor(entry: VersionEntry): string {
return `https://git.openbureau.ch/karim/DOSSIER-STANDALONE/releases/download/${entry.tag}/${entry.dmg}`;
}
/** Oeffnet den DMG-Download im System-Standardbrowser (kein In-App-Download —
* Installation bleibt manuell). */
export async function openVersionDownload(entry: VersionEntry): Promise<void> {
const { open } = await import("@tauri-apps/plugin-shell");
await open(downloadUrlFor(entry));
}