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
+8
View File
@@ -74,6 +74,7 @@ import {
isTauriRuntime,
} from "./io/projectFile";
import type { LockInfo } from "./io/projectFile";
import { checkForUpdate } from "./update/checkForUpdate";
import type {
PlanViewHandle,
PlanSelection,
@@ -541,6 +542,13 @@ export default function App() {
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
// localStorage + reconcile). Bei jeder Änderung zurückspeichern.
const layout = useStore((s) => s.layout);
+26 -3
View File
@@ -140,7 +140,7 @@ export const de = {
"menu.native.view": "Ansicht",
"menu.native.window": "Fenster",
"menu.native.panels": "Panels",
"menu.native.about": "Über Dossier",
"menu.native.about": "Über dossier",
"menu.native.quit": "Beenden",
// ── Layout-Menü (Topbar) ─────────────────────────────────────────────────
@@ -1318,12 +1318,22 @@ export const de = {
"exportSave.fmt.stl": "STL (3D-Mesh)",
"about.title": "Über dossier",
"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.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.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 ──────────────────────────────────────────────────
"topbar.settings": "Einstellungen",
@@ -1344,6 +1354,19 @@ export const de = {
"settings.referenceElevation": "Referenzhöhe EG (m ü. M.)",
"settings.referenceElevation.hint":
"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) ──────────────────────────────────
"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.window": "Window",
"menu.native.panels": "Panels",
"menu.native.about": "About Dossier",
"menu.native.about": "About dossier",
"menu.native.quit": "Quit",
// Layout menu.
@@ -1305,12 +1305,22 @@ export const en: Record<TranslationKey, string> = {
"exportSave.fmt.stl": "STL (3D mesh)",
"about.title": "About dossier",
"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.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.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.
"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.hint":
"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).
"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
// 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) —
// 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({
text: "Dossier",
text: "dossier",
items: [
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"),
}),
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, {
url: "index.html?window=context-import",
title: "Standort importieren — Dossier",
title: "Standort importieren — dossier",
width: 480,
height: 680,
minWidth: 400,
+1 -1
View File
@@ -51,7 +51,7 @@ export async function openDrawingLevelsWindowNative(): Promise<void> {
}
new WebviewWindow(DRAWING_LEVELS_WINDOW_LABEL, {
url: "index.html?window=drawing-levels",
title: "Zeichnungsebenen — Dossier",
title: "Zeichnungsebenen — dossier",
width: 340,
height: 560,
minWidth: 260,
+1 -1
View File
@@ -54,7 +54,7 @@ export async function openLayerSettingsWindowNative(code: string): Promise<void>
}
new WebviewWindow(LAYER_SETTINGS_WINDOW_LABEL, {
url: `index.html?window=layer-settings&code=${encodeURIComponent(code)}`,
title: "Ebeneneinstellungen — Dossier",
title: "Ebeneneinstellungen — dossier",
width: 360,
height: 420,
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)}` : "");
new WebviewWindow(RESOURCE_WINDOW_LABEL, {
url,
title: "Ressourcen — Dossier",
title: "Ressourcen — dossier",
width: 780,
height: 660,
minWidth: 480,
+1 -1
View File
@@ -52,7 +52,7 @@ export async function openSettingsWindowNative(): Promise<void> {
}
new WebviewWindow(SETTINGS_WINDOW_LABEL, {
url: "index.html?window=settings",
title: "Einstellungen — Dossier",
title: "Einstellungen — dossier",
width: 520,
height: 640,
minWidth: 420,
+46 -6
View File
@@ -630,9 +630,9 @@ body {
border: 1px solid var(--border, #3a3f47);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
color: var(--ink, #e6e8ea);
/* Der Dialog ist ein DOM-Kind der `.topbar` (white-space: nowrap) — ohne
dieses Reset liefe der mehrzeilige Beschreibungstext einzeilig aus dem
Dialog heraus. Hier wieder normalen Zeilenumbruch erzwingen. */
/* Defensiv gesetzt (unabhängig davon, ob ein Vorfahre white-space: nowrap
setzt) — ohne dieses Reset liefe der mehrzeilige Beschreibungstext
einzeilig aus dem Dialog heraus. */
white-space: normal;
}
.about-brand {
@@ -676,9 +676,15 @@ body {
line-height: 1.6;
color: var(--label, #c4c8cd);
}
.about-close {
appearance: none;
.about-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 18px;
}
.about-close,
.about-check-update {
appearance: none;
padding: 6px 14px;
border-radius: 6px;
border: 1px solid var(--border, #3a3f47);
@@ -687,9 +693,14 @@ body {
cursor: pointer;
font-size: 12px;
}
.about-close:hover {
.about-close:hover,
.about-check-update:hover:not(:disabled) {
background: var(--accent-dim, #33383f);
}
.about-check-update:disabled {
opacity: 0.6;
cursor: default;
}
.tag {
font-weight: 500;
@@ -5392,6 +5403,35 @@ body {
outline: none;
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 ──────────────────────────────────────────────
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 { ColorHexField } from "./ColorHexField";
import { LayoutMenu } from "./LayoutMenu";
import { APP_VERSION } from "./TopBar";
import type { RendererMode } from "./StatusBar";
import type { LayoutState } from "../panels/types";
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 {
/** Hell/Dunkel — Standard ist Hell (siehe theme/themeMode.ts). */
@@ -71,6 +87,22 @@ export function SettingsDialog({
onClose,
windowed = false,
}: 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(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
@@ -208,6 +240,97 @@ export function SettingsDialog({
/>
<div className="imp-note">{t("settings.referenceElevation.hint")}</div>
</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>
{!windowed && (
+38 -6
View File
@@ -10,6 +10,8 @@
import type { Project } from "../model/types";
import { t } from "../i18n";
import { checkForUpdate } from "../update/checkForUpdate";
import { getRunningVersion } from "../update/version";
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { Dropdown } from "./Dropdown";
@@ -1020,6 +1022,11 @@ export const APP_VERSION = "0.1.0";
* Wortmarke. Klick ausserhalb / Esc schliesst.
*/
function AboutDialog({ onClose }: { onClose: () => void }) {
const [checkingUpdate, setCheckingUpdate] = useState(false);
const [runningVersion, setRunningVersion] = useState<string | null>(null);
useEffect(() => {
void getRunningVersion().then(setRunningVersion);
}, []);
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
@@ -1028,7 +1035,14 @@ function AboutDialog({ onClose }: { onClose: () => void }) {
return () => document.removeEventListener("keydown", onKey);
}, [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-dialog"
@@ -1041,7 +1055,7 @@ function AboutDialog({ onClose }: { onClose: () => void }) {
dossier<span className="brand-dot">.</span>
</div>
<div className="about-version">
{t("about.version")} {APP_VERSION}
{t("about.version")} {runningVersion ?? APP_VERSION}
</div>
<p className="about-desc">{t("about.desc")}</p>
<div className="about-meta">
@@ -1059,11 +1073,29 @@ function AboutDialog({ onClose }: { onClose: () => void }) {
<li>LibreDWG — GPL-3.0</li>
<li>acadrust — MPL-2.0</li>
</ul>
<button type="button" className="about-close" onClick={onClose}>
{t("about.close")}
</button>
<div className="about-actions">
<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>,
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));
}