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).
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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" }
|
||||||
|
|||||||
@@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 879 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 788 B |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -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| {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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" }),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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. */
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||