Schnittebenen 2D↔3D Phase 1+2, native Fenster, Hell/Dunkel-Theme, SWISSIMAGE-Import
- Schnittebenen: 3D-Live-Schnitt folgt der gewählten Grundriss-Schnittlinie (section3dCutId/sectionPlaneFromLevel), Schalter "Im 3D schneiden" im Objekt-Info, Doppelklick auf Schnittlinie springt in 2D-Schnittansicht, unsichtbare Ebenen blenden ihre Führungslinie aus - Eigene native Tauri-Fenster für Kontext-Import/Zeichnungsebenen/ Ebenen-Einstellungen/Ressourcen/Einstellungen + klassische Menüleiste (AppMenuBar) neben der Wortmarke - Hell/Dunkel-Umschalter (Einstellungen → Darstellung), persistiert, flackerfrei vor erstem Render gesetzt - SWISSIMAGE-Luftbild-Import (swisstopo WMS) als Kontext-Hintergrundebene - UI-Politur: Werkzeug-Panel Symbole/Liste umschaltbar, Topbar-Quick-Access- Icons entfernt, Zahnrad→Einstellungen in Panel-Köpfen, Footerbar/ Snap-Marker/Maß-HUD auf helle Pillen-Sprache umgestellt - Neues Dachziegel-Material (RoofingTiles013A)
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
// Eigenständiger React-Root für das native Standort-Import-Fenster (siehe
|
||||
// contextImportWindow.ts für die Hauptfenster-Seite der Sync-Brücke). Von
|
||||
// main.tsx gerendert, wenn die URL `?window=context-import` trägt.
|
||||
//
|
||||
// Rendert `ContextImportDialog` UNVERÄNDERT (windowed=true) — der Dialog
|
||||
// selbst braucht keinerlei Host-/Store-Zugriff (reine `io/*`-Netzabrufe),
|
||||
// daher hier kein Proxy-Pattern nötig wie bei den anderen Fenstern: nur
|
||||
// `onImport`/`onSetGeoAnchor` laufen als einzelne Events ans Hauptfenster.
|
||||
//
|
||||
// Bezeichner englisch, UI-Text über i18n (CONVENTIONS.md).
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { ContextImportDialog } from "../ui/ContextImportDialog";
|
||||
import type { GeoAnchor } from "../ui/ContextImportDialog";
|
||||
import type { ContextImportSyncState } from "./contextImportWindow";
|
||||
import { t } from "../i18n";
|
||||
|
||||
export function ContextImportWindowApp() {
|
||||
const [state, setState] = useState<ContextImportSyncState | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
const unlisten: Array<() => void> = [];
|
||||
void (async () => {
|
||||
const { listen, emitTo } = await import("@tauri-apps/api/event");
|
||||
if (disposed) return;
|
||||
unlisten.push(
|
||||
await listen<ContextImportSyncState>("context-import-sync-state", (e) =>
|
||||
setState(e.payload),
|
||||
),
|
||||
);
|
||||
await emitTo("main", "context-import-window-ready").catch(() => {});
|
||||
})();
|
||||
return () => {
|
||||
disposed = true;
|
||||
unlisten.forEach((u) => u());
|
||||
};
|
||||
}, []);
|
||||
|
||||
const send = (name: string, ...args: unknown[]) => {
|
||||
void import("@tauri-apps/api/event").then(({ emitTo }) =>
|
||||
emitTo("main", "context-import-command", { name, args }).catch(() => {}),
|
||||
);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
void import("@tauri-apps/api/window").then(({ getCurrentWindow }) =>
|
||||
getCurrentWindow().close(),
|
||||
);
|
||||
};
|
||||
|
||||
if (!state) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100vh",
|
||||
color: "var(--ink-dim, #8a8a8a)",
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
{t("resources.loading")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextImportDialog
|
||||
onImport={(objs) => send("onImport", objs)}
|
||||
onClose={onClose}
|
||||
geoAnchor={state.geoAnchor}
|
||||
onSetGeoAnchor={(anchor: GeoAnchor | undefined) => send("onSetGeoAnchor", anchor)}
|
||||
windowed
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// Eigenständiger React-Root für das native Zeichnungsebenen-Fenster (siehe
|
||||
// drawingLevelsWindow.ts für die Hauptfenster-Seite der Sync-Brücke). Von
|
||||
// main.tsx gerendert, wenn die URL `?window=drawing-levels` trägt.
|
||||
//
|
||||
// Spiegelt die Kategorie-/Zeilen-Darstellung des Dock-Panels
|
||||
// (panels/DrawingLevelsPanel.tsx) — nutzt dessen exportierte `LevelRow` +
|
||||
// Kategorie-Konstanten direkt (keine Duplikation), aber NICHT `usePanelHost`
|
||||
// (das ist an den riesigen, docked-Panel-spezifischen PanelHostContext
|
||||
// gebunden); stattdessen der schmale Sync-/Command-Zustand von
|
||||
// drawingLevelsWindow.ts, analog SettingsWindowApp.tsx.
|
||||
//
|
||||
// Bezeichner englisch, UI-Text über i18n (CONVENTIONS.md).
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
LevelRow,
|
||||
CATEGORY_ORDER,
|
||||
CATEGORY_TITLE_KEY,
|
||||
CATEGORY_ADD_KEY,
|
||||
} from "../panels/DrawingLevelsPanel";
|
||||
import type { DrawingLevelsCommandHandlers, DrawingLevelsSyncState } from "./drawingLevelsWindow";
|
||||
import type { DrawingLevelKind } from "../model/types";
|
||||
import { t } from "../i18n";
|
||||
|
||||
type HandlerName = keyof DrawingLevelsCommandHandlers;
|
||||
|
||||
export function DrawingLevelsWindowApp() {
|
||||
const [state, setState] = useState<DrawingLevelsSyncState | null>(null);
|
||||
const [collapsed, setCollapsed] = useState<Record<DrawingLevelKind, boolean>>({
|
||||
floor: false,
|
||||
section: false,
|
||||
elevation: false,
|
||||
drawing: false,
|
||||
});
|
||||
const toggleCategory = (kind: DrawingLevelKind) =>
|
||||
setCollapsed((c) => ({ ...c, [kind]: !c[kind] }));
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
const unlisten: Array<() => void> = [];
|
||||
void (async () => {
|
||||
const { listen, emitTo } = await import("@tauri-apps/api/event");
|
||||
if (disposed) return;
|
||||
unlisten.push(
|
||||
await listen<DrawingLevelsSyncState>("drawing-levels-sync-state", (e) =>
|
||||
setState(e.payload),
|
||||
),
|
||||
);
|
||||
await emitTo("main", "drawing-levels-window-ready").catch(() => {});
|
||||
})();
|
||||
return () => {
|
||||
disposed = true;
|
||||
unlisten.forEach((u) => u());
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handlers = useMemo<DrawingLevelsCommandHandlers>(
|
||||
() =>
|
||||
new Proxy({} as DrawingLevelsCommandHandlers, {
|
||||
get(_target, prop: string) {
|
||||
return (...args: unknown[]) => {
|
||||
void import("@tauri-apps/api/event").then(({ emitTo }) =>
|
||||
emitTo("main", "drawing-levels-command", { name: prop as HandlerName, args }).catch(
|
||||
() => {},
|
||||
),
|
||||
);
|
||||
};
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
if (!state) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100vh",
|
||||
color: "var(--ink-dim, #8a8a8a)",
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
{t("resources.loading")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const onAddByKind: Record<DrawingLevelKind, () => void> = {
|
||||
floor: handlers.onAddFloor,
|
||||
section: handlers.onAddSection,
|
||||
elevation: handlers.onAddElevation,
|
||||
drawing: handlers.onAddDrawing,
|
||||
};
|
||||
|
||||
// Oberstes Geschoss zuoberst (wie das Dock-Panel/DOSSIER): Anzeige umkehren.
|
||||
const levelsTopDown = [...state.drawingLevels].reverse();
|
||||
|
||||
// Klick aufs Auge in „Alle anzeigen"/„Nur aktive" schaltet automatisch auf
|
||||
// „Ausgewählte" — dieselbe DOSSIER-Regel wie im Dock-Panel.
|
||||
const toggle = (id: string) => {
|
||||
if (state.displayMode === "all_force" || state.displayMode === "active") {
|
||||
handlers.onSetDisplayMode("all");
|
||||
}
|
||||
handlers.onToggleLevel(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
className="nav-group"
|
||||
style={{ height: "100vh", overflowY: "auto" }}
|
||||
>
|
||||
{/* Selber Kopf wie die übrigen vier Fenster (.res-head/.imp-head):
|
||||
44px, border-bottom, 76px Ampel-Abstand — kein Dock-Panel-Look mehr. */}
|
||||
<header className="res-head" data-tauri-drag-region style={{ paddingLeft: 88 }}>
|
||||
<span className="res-head-title">{t("nav.drawingLevels")}</span>
|
||||
</header>
|
||||
|
||||
{CATEGORY_ORDER.map((kind) => {
|
||||
const levelsOfKind = levelsTopDown.filter((l) => l.kind === kind);
|
||||
const isOpen = !collapsed[kind];
|
||||
return (
|
||||
<div className="level-cat" key={kind}>
|
||||
<header className="level-cat-head" onClick={() => toggleCategory(kind)}>
|
||||
<span className={`level-cat-chevron${isOpen ? " open" : ""}`} aria-hidden="true">
|
||||
▸
|
||||
</span>
|
||||
<span className="level-cat-title">{t(CATEGORY_TITLE_KEY[kind])}</span>
|
||||
<span
|
||||
className="level-cat-add"
|
||||
role="button"
|
||||
aria-label={t(CATEGORY_ADD_KEY[kind])}
|
||||
title={t(CATEGORY_ADD_KEY[kind])}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAddByKind[kind]();
|
||||
}}
|
||||
>
|
||||
+
|
||||
</span>
|
||||
</header>
|
||||
|
||||
{isOpen && (
|
||||
<div className="nav-list">
|
||||
{levelsOfKind.map((level) => (
|
||||
<LevelRow
|
||||
key={level.id}
|
||||
level={level}
|
||||
active={level.id === state.activeLevelId}
|
||||
mode={state.displayMode}
|
||||
onSelect={() => handlers.onSelectLevel(level.id)}
|
||||
onToggle={() => toggle(level.id)}
|
||||
// Rechtsklick-Kontextmenü bewusst kein Bestandteil des
|
||||
// Fensters (s. drawingLevelsWindow.ts Kopfkommentar).
|
||||
onContext={(e) => e.preventDefault()}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// Eigenständiger React-Root für das native Ebeneneinstellungs-Fenster (siehe
|
||||
// layerSettingsWindow.ts für die Hauptfenster-Seite der Sync-Brücke). Von
|
||||
// main.tsx gerendert, wenn die URL `?window=layer-settings` trägt.
|
||||
//
|
||||
// Dieselben Felder wie der bisherige `InlineEditor`-Popover in App.tsx
|
||||
// (`editor?.kind === "layer"`): Name, Farbe, Linienstärke, Schraffur-Auswahl
|
||||
// — siehe layerSettingsWindow.ts Kopfkommentar für den bewussten Scope-Schnitt
|
||||
// gegenüber der Rhino-Vorlage.
|
||||
//
|
||||
// Bezeichner englisch, UI-Text über i18n (CONVENTIONS.md).
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { EditorField } from "../ui/EditorField";
|
||||
import type { LayerSettingsCommandHandlers, LayerSettingsSyncState } from "./layerSettingsWindow";
|
||||
import { t } from "../i18n";
|
||||
|
||||
function initialCodeFromUrl(): string {
|
||||
return new URLSearchParams(window.location.search).get("code") ?? "";
|
||||
}
|
||||
|
||||
export function LayerSettingsWindowApp() {
|
||||
const [state, setState] = useState<LayerSettingsSyncState | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
const unlisten: Array<() => void> = [];
|
||||
void (async () => {
|
||||
const { listen, emitTo } = await import("@tauri-apps/api/event");
|
||||
if (disposed) return;
|
||||
unlisten.push(
|
||||
await listen<LayerSettingsSyncState | null>("layer-settings-sync-state", (e) =>
|
||||
setState(e.payload),
|
||||
),
|
||||
);
|
||||
// Beim Wechsel auf eine andere Ebene (erneutes Öffnen desselben
|
||||
// Fensters aus dem Kontextmenü) — nur der Titel/URL-Code als Hinweis;
|
||||
// der eigentliche State kommt weiterhin per Sync vom Hauptfenster.
|
||||
unlisten.push(await listen<string>("layer-settings-set-code", () => {}));
|
||||
await emitTo("main", "layer-settings-window-ready").catch(() => {});
|
||||
})();
|
||||
return () => {
|
||||
disposed = true;
|
||||
unlisten.forEach((u) => u());
|
||||
};
|
||||
}, []);
|
||||
|
||||
const patch = (p: Partial<{ name: string; color: string; lw: number; hatch: string | undefined }>) => {
|
||||
if (!state) return;
|
||||
void import("@tauri-apps/api/event").then(({ emitTo }) =>
|
||||
emitTo("main", "layer-settings-command", {
|
||||
name: "onPatchCategory" as keyof LayerSettingsCommandHandlers,
|
||||
args: [state.code, p],
|
||||
}).catch(() => {}),
|
||||
);
|
||||
};
|
||||
|
||||
const code = state?.code ?? initialCodeFromUrl();
|
||||
|
||||
if (!state) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100vh",
|
||||
color: "var(--ink-dim, #8a8a8a)",
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
{t("resources.loading")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
|
||||
<header
|
||||
className="imp-head"
|
||||
data-tauri-drag-region
|
||||
style={{ paddingLeft: 88 }}
|
||||
>
|
||||
<span className="imp-head-title">
|
||||
{t("editor.layerTitle", { code })}
|
||||
</span>
|
||||
</header>
|
||||
<div className="imp-body" style={{ overflowY: "auto" }}>
|
||||
<section className="imp-section">
|
||||
<EditorField label={t("editor.name")}>
|
||||
<input
|
||||
type="text"
|
||||
value={state.name}
|
||||
autoFocus
|
||||
onChange={(e) => patch({ name: e.target.value })}
|
||||
/>
|
||||
</EditorField>
|
||||
<EditorField label={t("editor.color")}>
|
||||
<input
|
||||
type="color"
|
||||
value={state.color}
|
||||
onChange={(e) => patch({ color: e.target.value })}
|
||||
/>
|
||||
</EditorField>
|
||||
<EditorField label={t("editor.lineWeight")}>
|
||||
<span className="editor-num">
|
||||
<input
|
||||
type="number"
|
||||
step={0.01}
|
||||
min={0.01}
|
||||
value={state.lw}
|
||||
onChange={(e) => patch({ lw: Number(e.target.value) })}
|
||||
/>
|
||||
<span className="editor-unit">{t("editor.unit.mm")}</span>
|
||||
</span>
|
||||
</EditorField>
|
||||
<EditorField label={t("editor.hatch")}>
|
||||
<span className="editor-hatch">
|
||||
<select
|
||||
value={state.hatchId ?? ""}
|
||||
onChange={(e) => patch({ hatch: e.target.value || undefined })}
|
||||
>
|
||||
<option value="">{t("resources.viewHatch.none")}</option>
|
||||
{state.hatches.map((h) => (
|
||||
<option key={h.id} value={h.id}>
|
||||
{h.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
</EditorField>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Eigenständiger React-Root für das native Ressourcen-Fenster (siehe
|
||||
// resourceWindow.ts für die Hauptfenster-Seite der Sync-Brücke). Von
|
||||
// main.tsx gerendert, wenn die URL `?window=resources` trägt — KEIN Teil des
|
||||
// normalen App-Baums (kein Zugriff auf den Haupt-Store, eigener Prozess).
|
||||
//
|
||||
// Projekt-Zustand kommt ausschliesslich per Tauri-Event vom Hauptfenster
|
||||
// („resource-sync-project"); Bearbeitungen laufen NICHT lokal, sondern werden
|
||||
// als „resource-command"-Events ans Hauptfenster geschickt, das die echten
|
||||
// Handler aufruft und die Änderung über den nächsten Sync-Push zurückspiegelt
|
||||
// — dasselbe Prinzip wie die native 2D/3D-Fensterspiegelung (nativeSync.ts),
|
||||
// nur bidirektional statt nur lesend.
|
||||
//
|
||||
// Bezeichner englisch, UI-Text über i18n (CONVENTIONS.md).
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ResourceManager } from "../ui/ResourceManager";
|
||||
import type { ResourceManagerHandlers, TabId } from "../ui/ResourceManager";
|
||||
import type { Project } from "../model/types";
|
||||
import { t } from "../i18n";
|
||||
|
||||
/** Alle Felder von ResourceManagerHandlers sind Funktionen — für den Proxy unten. */
|
||||
type HandlerName = keyof ResourceManagerHandlers;
|
||||
|
||||
/** Liest `?tab=…` aus der Fenster-URL (initialer Tab beim Öffnen aus einem Kontext). */
|
||||
function initialTabFromUrl(): TabId | undefined {
|
||||
const v = new URLSearchParams(window.location.search).get("tab");
|
||||
return (v as TabId) || undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Eigenständige Seite des Ressourcen-Fensters. Zeigt einen Lade-Hinweis, bis
|
||||
* der erste Projekt-Sync vom Hauptfenster eintrifft (i. d. R. Millisekunden).
|
||||
*/
|
||||
export function ResourceWindowApp() {
|
||||
const [project, setProject] = useState<Project | null>(null);
|
||||
const [tab, setTab] = useState<TabId | undefined>(() => initialTabFromUrl());
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
const unlisten: Array<() => void> = [];
|
||||
void (async () => {
|
||||
const { listen, emitTo } = await import("@tauri-apps/api/event");
|
||||
if (disposed) return;
|
||||
unlisten.push(
|
||||
await listen<Project>("resource-sync-project", (e) => setProject(e.payload)),
|
||||
);
|
||||
unlisten.push(await listen<TabId>("resource-set-tab", (e) => setTab(e.payload)));
|
||||
// Hauptfenster um den aktuellen Stand bitten — deckt den Fall ab, dass
|
||||
// dieses Fenster NACH dem letzten Projekt-Push gestartet ist.
|
||||
await emitTo("main", "resource-window-ready").catch(() => {});
|
||||
})();
|
||||
return () => {
|
||||
disposed = true;
|
||||
unlisten.forEach((u) => u());
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Jeder Handler-Aufruf wird zu EINEM „resource-command"-Event ans
|
||||
// Hauptfenster (Name + Argumente, beides JSON-serialisierbar — alle
|
||||
// ResourceManagerHandlers-Signaturen sind reine Datenpatches). Per Proxy
|
||||
// statt 27 einzeln ausgeschriebenen Weiterleitungen (Drift-Gefahr).
|
||||
const handlers = useMemo<ResourceManagerHandlers>(
|
||||
() =>
|
||||
new Proxy({} as ResourceManagerHandlers, {
|
||||
get(_target, prop: string) {
|
||||
return (...args: unknown[]) => {
|
||||
void import("@tauri-apps/api/event").then(({ emitTo }) =>
|
||||
emitTo("main", "resource-command", { name: prop as HandlerName, args }).catch(
|
||||
() => {},
|
||||
),
|
||||
);
|
||||
};
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const onClose = () => {
|
||||
void import("@tauri-apps/api/window").then(({ getCurrentWindow }) =>
|
||||
getCurrentWindow().close(),
|
||||
);
|
||||
};
|
||||
|
||||
if (!project) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100vh",
|
||||
color: "var(--ink-dim, #8a8a8a)",
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
{t("resources.loading")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ResourceManager
|
||||
project={project}
|
||||
handlers={handlers}
|
||||
initialTab={tab}
|
||||
onClose={onClose}
|
||||
windowed
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Eigenständiger React-Root für das native Einstellungs-Fenster (siehe
|
||||
// settingsWindow.ts für die Hauptfenster-Seite der Sync-Brücke). Von
|
||||
// main.tsx gerendert, wenn die URL `?window=settings` trägt. Gleiches Prinzip
|
||||
// wie ResourceWindowApp.tsx, nur mit dem schmaleren Settings-Zustand statt
|
||||
// des ganzen Projekts.
|
||||
//
|
||||
// Bezeichner englisch, UI-Text über i18n (CONVENTIONS.md).
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { SettingsDialog } from "../ui/SettingsDialog";
|
||||
import type { SettingsCommandHandlers, SettingsSyncState } from "./settingsWindow";
|
||||
import { t } from "../i18n";
|
||||
import { applyThemeMode } from "../theme/themeMode";
|
||||
|
||||
type HandlerName = keyof SettingsCommandHandlers;
|
||||
|
||||
export function SettingsWindowApp() {
|
||||
const [state, setState] = useState<SettingsSyncState | null>(null);
|
||||
|
||||
// Dieses Fenster ist ein eigenes Dokument — Hell/Dunkel muss hier separat
|
||||
// angewendet werden, sobald der synchronisierte Zustand eintrifft.
|
||||
useEffect(() => {
|
||||
if (state) applyThemeMode(state.themeMode);
|
||||
}, [state]);
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
const unlisten: Array<() => void> = [];
|
||||
void (async () => {
|
||||
const { listen, emitTo } = await import("@tauri-apps/api/event");
|
||||
if (disposed) return;
|
||||
unlisten.push(
|
||||
await listen<SettingsSyncState>("settings-sync-state", (e) => setState(e.payload)),
|
||||
);
|
||||
// Hauptfenster um den aktuellen Stand bitten (Race-Schutz: dieses
|
||||
// Fenster könnte nach dem letzten Sync-Push gestartet sein).
|
||||
await emitTo("main", "settings-window-ready").catch(() => {});
|
||||
})();
|
||||
return () => {
|
||||
disposed = true;
|
||||
unlisten.forEach((u) => u());
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Jeder Handler-Aufruf → EIN „settings-command"-Event ans Hauptfenster
|
||||
// (Name + Argumente), analog ResourceWindowApp.tsx.
|
||||
const handlers = useMemo<SettingsCommandHandlers>(
|
||||
() =>
|
||||
new Proxy({} as SettingsCommandHandlers, {
|
||||
get(_target, prop: string) {
|
||||
return (...args: unknown[]) => {
|
||||
void import("@tauri-apps/api/event").then(({ emitTo }) =>
|
||||
emitTo("main", "settings-command", { name: prop as HandlerName, args }).catch(
|
||||
() => {},
|
||||
),
|
||||
);
|
||||
};
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const onClose = () => {
|
||||
void import("@tauri-apps/api/window").then(({ getCurrentWindow }) =>
|
||||
getCurrentWindow().close(),
|
||||
);
|
||||
};
|
||||
|
||||
if (!state) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100vh",
|
||||
color: "var(--ink-dim, #8a8a8a)",
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
{t("resources.loading")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsDialog
|
||||
themeMode={state.themeMode}
|
||||
onThemeModeChange={handlers.onThemeModeChange}
|
||||
marqueeColor={state.marqueeColor}
|
||||
onMarqueeColorChange={handlers.onMarqueeColorChange}
|
||||
snapColor={state.snapColor}
|
||||
onSnapColorChange={handlers.onSnapColorChange}
|
||||
referenceElevationMasl={state.referenceElevationMasl}
|
||||
onReferenceElevationChange={handlers.onReferenceElevationChange}
|
||||
rendererMode={state.rendererMode}
|
||||
webGpuAvailable={state.webGpuAvailable}
|
||||
onRendererModeChange={handlers.onRendererModeChange}
|
||||
layout={state.layout}
|
||||
onApplyLayout={handlers.onApplyLayout}
|
||||
onClose={onClose}
|
||||
windowed
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
// Native macOS-Systemmenüleiste (oben am Bildschirm) — NICHT zu verwechseln mit
|
||||
// AppMenuBar (die In-App-Menüleiste in der TopBar). Nutzt die Tauri-v2-Menu-API
|
||||
// (@tauri-apps/api/menu), die einen echten macOS-App-Menü-Baum baut. No-op im
|
||||
// reinen Browser (kein Tauri) — s. isTauriRuntime.
|
||||
//
|
||||
// Aufbau: EIN Menü-Baum wird beim Mount EINMAL erzeugt (MenuItem.new ist async
|
||||
// und teuer genug, um es nicht bei jedem Render neu zu bauen); alle Aktionen
|
||||
// laufen über eine Ref auf die jeweils aktuellen Handler (klassisches Ref-
|
||||
// Pattern gegen veraltete Closures, wie `toolShortcutRef` in App.tsx). Enabled-
|
||||
// /Checked-Zustand (Undo/Redo/Löschen/Ansicht) wird in einem separaten Effekt
|
||||
// synchron zum React-State über `setEnabled`/`setChecked` nachgezogen.
|
||||
//
|
||||
// Bezeichner englisch, UI-Text über i18n (CONVENTIONS.md).
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { isTauriRuntime } from "../io/projectFile";
|
||||
import { t } from "../i18n";
|
||||
import { APP_VERSION } from "../ui/TopBar";
|
||||
import type { View3d, ViewType } from "../ui/TopBar";
|
||||
|
||||
/** Alle Aktionen, die die native Menüleiste auslösen kann. */
|
||||
export interface NativeAppMenuHandlers {
|
||||
onOpenProject: () => void;
|
||||
onSaveProject: () => void;
|
||||
onImport: () => void;
|
||||
onExportPdf: () => void;
|
||||
onExportDxf: () => void;
|
||||
onExportSchedule: () => void;
|
||||
onExportIfc: () => void;
|
||||
onExportObj: () => void;
|
||||
onExportStl: () => void;
|
||||
|
||||
onUndo: () => void;
|
||||
onRedo: () => void;
|
||||
onDeleteSelection: () => void;
|
||||
onMove: () => void;
|
||||
onMirror: () => void;
|
||||
onRotate: () => void;
|
||||
|
||||
onViewType: (v: ViewType) => void;
|
||||
onView3d: (v: View3d) => void;
|
||||
onReferenceLines: (v: boolean) => void;
|
||||
|
||||
onToggleResources: () => void;
|
||||
onOpenSettings: () => void;
|
||||
/** Macht das Panel mit dieser ID sichtbar (siehe `revealPanel` in panels/layout.ts). */
|
||||
onOpenPanel: (panelId: string) => void;
|
||||
/** Öffnet den Zeichnungsebenen-Manager als eigenes Fenster (zusätzlich zum Dock-Panel). */
|
||||
onOpenDrawingLevelsWindow: () => void;
|
||||
/** Öffnet die Ebeneneinstellungen für die aktuell aktive Ebene (activeCategoryCode). */
|
||||
onOpenLayerSettingsWindow: () => void;
|
||||
/** Öffnet den Standort-Import (swisstopo/OSM). */
|
||||
onOpenContextImportWindow: () => void;
|
||||
}
|
||||
|
||||
/** Ein Eintrag für „Fenster > Panels" — id + fertiges i18n-Label. */
|
||||
export interface NativeAppMenuPanel {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/** Ortsabhängiger Zustand, der Enabled-/Checked-Häkchen der Menüpunkte treibt. */
|
||||
export interface NativeAppMenuState {
|
||||
canUndo: boolean;
|
||||
canRedo: boolean;
|
||||
hasSelection: boolean;
|
||||
viewType: ViewType;
|
||||
view3d: View3d;
|
||||
referenceLines: boolean;
|
||||
resourcesOpen: boolean;
|
||||
}
|
||||
|
||||
const VIEW3D_IDS: View3d[] = [
|
||||
"front",
|
||||
"back",
|
||||
"side",
|
||||
"left",
|
||||
"top",
|
||||
"iso",
|
||||
"perspective",
|
||||
];
|
||||
|
||||
/**
|
||||
* Baut die native macOS-Menüleiste einmal auf und hält Enabled-/Checked-
|
||||
* Zustand synchron zum übergebenen `state`. `handlers` darf sich bei jedem
|
||||
* Render ändern (wird über eine Ref gelesen, kein Neu-Erzeugen der Menüpunkte).
|
||||
*/
|
||||
export function useNativeAppMenu(
|
||||
handlers: NativeAppMenuHandlers,
|
||||
state: NativeAppMenuState,
|
||||
panels: NativeAppMenuPanel[],
|
||||
): void {
|
||||
const handlersRef = useRef(handlers);
|
||||
handlersRef.current = handlers;
|
||||
// Panel-Liste ist beim Mount bekannt (Registry ist statisch) — als Ref nur,
|
||||
// damit der Aufbau-Effekt (deps: []) sie ohne Lint-Warnung lesen darf.
|
||||
const panelsRef = useRef(panels);
|
||||
panelsRef.current = panels;
|
||||
|
||||
// Menüpunkte, die Enabled-/Checked-Zustand nachgezogen bekommen — als Ref,
|
||||
// damit der Sync-Effekt unten sie ohne Neu-Aufbau erreicht.
|
||||
const itemsRef = useRef<{
|
||||
undo?: import("@tauri-apps/api/menu").MenuItem;
|
||||
redo?: import("@tauri-apps/api/menu").MenuItem;
|
||||
delete?: import("@tauri-apps/api/menu").MenuItem;
|
||||
floorplan?: import("@tauri-apps/api/menu").CheckMenuItem;
|
||||
perspective?: import("@tauri-apps/api/menu").CheckMenuItem;
|
||||
view3d: Map<View3d, import("@tauri-apps/api/menu").CheckMenuItem>;
|
||||
referenceLines?: import("@tauri-apps/api/menu").CheckMenuItem;
|
||||
resources?: import("@tauri-apps/api/menu").CheckMenuItem;
|
||||
}>({ view3d: new Map() });
|
||||
|
||||
// Baut den Menü-Baum EINMAL. Aktionen rufen ausschliesslich über
|
||||
// `handlersRef.current` — so bleibt der Baum stabil, während die
|
||||
// eigentlichen Callbacks (aus App.tsx, jeden Render neu) trotzdem aktuell
|
||||
// bleiben.
|
||||
useEffect(() => {
|
||||
if (!isTauriRuntime()) return;
|
||||
let disposed = false;
|
||||
void (async () => {
|
||||
const {
|
||||
Menu,
|
||||
Submenu,
|
||||
MenuItem,
|
||||
CheckMenuItem,
|
||||
PredefinedMenuItem,
|
||||
} = await import("@tauri-apps/api/menu");
|
||||
if (disposed) return;
|
||||
|
||||
const fileMenu = await Submenu.new({
|
||||
text: t("menu.native.file"),
|
||||
items: [
|
||||
await MenuItem.new({
|
||||
text: t("menu.native.open"),
|
||||
accelerator: "CmdOrCtrl+O",
|
||||
action: () => handlersRef.current.onOpenProject(),
|
||||
}),
|
||||
await MenuItem.new({
|
||||
text: t("menu.native.saveAs"),
|
||||
accelerator: "CmdOrCtrl+S",
|
||||
action: () => handlersRef.current.onSaveProject(),
|
||||
}),
|
||||
await MenuItem.new({
|
||||
text: t("menu.native.import"),
|
||||
action: () => handlersRef.current.onImport(),
|
||||
}),
|
||||
await Submenu.new({
|
||||
text: t("menu.native.export"),
|
||||
items: [
|
||||
await MenuItem.new({
|
||||
text: "PDF",
|
||||
action: () => handlersRef.current.onExportPdf(),
|
||||
}),
|
||||
await MenuItem.new({
|
||||
text: "DXF",
|
||||
action: () => handlersRef.current.onExportDxf(),
|
||||
}),
|
||||
await MenuItem.new({
|
||||
text: t("menu.native.exportSchedule"),
|
||||
action: () => handlersRef.current.onExportSchedule(),
|
||||
}),
|
||||
await MenuItem.new({
|
||||
text: "IFC",
|
||||
action: () => handlersRef.current.onExportIfc(),
|
||||
}),
|
||||
await MenuItem.new({
|
||||
text: "OBJ",
|
||||
action: () => handlersRef.current.onExportObj(),
|
||||
}),
|
||||
await MenuItem.new({
|
||||
text: "STL",
|
||||
action: () => handlersRef.current.onExportStl(),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const undoItem = await MenuItem.new({
|
||||
text: t("menu.edit.undo"),
|
||||
accelerator: "CmdOrCtrl+Z",
|
||||
action: () => handlersRef.current.onUndo(),
|
||||
});
|
||||
const redoItem = await MenuItem.new({
|
||||
text: t("menu.edit.redo"),
|
||||
accelerator: "CmdOrCtrl+Shift+Z",
|
||||
action: () => handlersRef.current.onRedo(),
|
||||
});
|
||||
const deleteItem = await MenuItem.new({
|
||||
text: t("menu.edit.delete"),
|
||||
action: () => handlersRef.current.onDeleteSelection(),
|
||||
});
|
||||
itemsRef.current.undo = undoItem;
|
||||
itemsRef.current.redo = redoItem;
|
||||
itemsRef.current.delete = deleteItem;
|
||||
|
||||
const editMenu = await Submenu.new({
|
||||
text: t("menu.native.edit"),
|
||||
items: [
|
||||
undoItem,
|
||||
redoItem,
|
||||
await PredefinedMenuItem.new({ item: "Separator" }),
|
||||
deleteItem,
|
||||
await PredefinedMenuItem.new({ item: "Separator" }),
|
||||
await MenuItem.new({
|
||||
text: t("menu.native.move"),
|
||||
action: () => handlersRef.current.onMove(),
|
||||
}),
|
||||
await MenuItem.new({
|
||||
text: t("menu.native.mirror"),
|
||||
action: () => handlersRef.current.onMirror(),
|
||||
}),
|
||||
await MenuItem.new({
|
||||
text: t("menu.native.rotate"),
|
||||
action: () => handlersRef.current.onRotate(),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const floorplanItem = await CheckMenuItem.new({
|
||||
text: t("menu.view.floorplan"),
|
||||
action: () => handlersRef.current.onViewType("grundriss"),
|
||||
});
|
||||
const perspectiveItem = await CheckMenuItem.new({
|
||||
text: t("menu.view.perspective"),
|
||||
action: () => handlersRef.current.onViewType("perspektive"),
|
||||
});
|
||||
itemsRef.current.floorplan = floorplanItem;
|
||||
itemsRef.current.perspective = perspectiveItem;
|
||||
|
||||
const view3dLabels: Record<View3d, string> = {
|
||||
front: t("view3d.front"),
|
||||
back: t("view3d.back"),
|
||||
side: t("view3d.side"),
|
||||
left: t("view3d.left"),
|
||||
top: t("view3d.top"),
|
||||
iso: t("view3d.iso"),
|
||||
isoFrontLeft: t("view3d.iso"),
|
||||
isoBackRight: t("view3d.iso"),
|
||||
isoBackLeft: t("view3d.iso"),
|
||||
perspective: t("view3d.perspective"),
|
||||
};
|
||||
const view3dItems: import("@tauri-apps/api/menu").CheckMenuItem[] = [];
|
||||
for (const id of VIEW3D_IDS) {
|
||||
const item = await CheckMenuItem.new({
|
||||
text: view3dLabels[id],
|
||||
action: () => handlersRef.current.onView3d(id),
|
||||
});
|
||||
itemsRef.current.view3d.set(id, item);
|
||||
view3dItems.push(item);
|
||||
}
|
||||
|
||||
const referenceLinesItem = await CheckMenuItem.new({
|
||||
text: t("menu.view.referenceLines"),
|
||||
action: () => handlersRef.current.onReferenceLines(!state.referenceLines),
|
||||
});
|
||||
itemsRef.current.referenceLines = referenceLinesItem;
|
||||
|
||||
const viewMenu = await Submenu.new({
|
||||
text: t("menu.native.view"),
|
||||
items: [
|
||||
floorplanItem,
|
||||
perspectiveItem,
|
||||
await PredefinedMenuItem.new({ item: "Separator" }),
|
||||
...view3dItems,
|
||||
await PredefinedMenuItem.new({ item: "Separator" }),
|
||||
referenceLinesItem,
|
||||
],
|
||||
});
|
||||
|
||||
const resourcesItem = await CheckMenuItem.new({
|
||||
text: t("menu.window.resources"),
|
||||
action: () => handlersRef.current.onToggleResources(),
|
||||
});
|
||||
itemsRef.current.resources = resourcesItem;
|
||||
|
||||
// Ein Menüpunkt je registriertem Panel (ToolsPanel/Attribute/Objekt-Info/
|
||||
// Zeichnungsebenen/Ebenen/Umgebung/Flächenbilanz/Element-Baum/Ausschnitte/
|
||||
// Layouts) — macht das jeweilige Panel sichtbar (`revealPanel`), egal ob es
|
||||
// gerade angedockt, schwebend oder gar nicht offen ist.
|
||||
const panelItems = await Promise.all(
|
||||
panelsRef.current.map((p) =>
|
||||
MenuItem.new({
|
||||
text: p.label,
|
||||
action: () => handlersRef.current.onOpenPanel(p.id),
|
||||
}),
|
||||
),
|
||||
);
|
||||
const panelsMenu = await Submenu.new({
|
||||
text: t("menu.native.panels"),
|
||||
items: panelItems,
|
||||
});
|
||||
|
||||
const windowMenu = await Submenu.new({
|
||||
text: t("menu.native.window"),
|
||||
items: [
|
||||
resourcesItem,
|
||||
await PredefinedMenuItem.new({ item: "Separator" }),
|
||||
panelsMenu,
|
||||
await MenuItem.new({
|
||||
text: t("menu.window.drawingLevels"),
|
||||
action: () => handlersRef.current.onOpenDrawingLevelsWindow(),
|
||||
}),
|
||||
await MenuItem.new({
|
||||
text: t("menu.window.layerSettings"),
|
||||
action: () => handlersRef.current.onOpenLayerSettingsWindow(),
|
||||
}),
|
||||
await MenuItem.new({
|
||||
text: t("menu.window.contextImport"),
|
||||
action: () => handlersRef.current.onOpenContextImportWindow(),
|
||||
}),
|
||||
await PredefinedMenuItem.new({ item: "Separator" }),
|
||||
await MenuItem.new({
|
||||
text: t("menu.window.settings"),
|
||||
action: () => handlersRef.current.onOpenSettings(),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// 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
|
||||
// „About") statt des eigenen In-App-Dialogs (AboutDialog in TopBar.tsx) —
|
||||
// an dieser Stelle erwartet macOS exakt diesen Eintrag, kein Custom-Dialog.
|
||||
const appMenu = await Submenu.new({
|
||||
text: "Dossier",
|
||||
items: [
|
||||
await PredefinedMenuItem.new({
|
||||
item: { About: { name: "Dossier", version: APP_VERSION } },
|
||||
text: t("menu.native.about"),
|
||||
}),
|
||||
await PredefinedMenuItem.new({ item: "Separator" }),
|
||||
await PredefinedMenuItem.new({ item: "Quit", text: t("menu.native.quit") }),
|
||||
],
|
||||
});
|
||||
|
||||
if (disposed) return;
|
||||
const menu = await Menu.new({
|
||||
items: [appMenu, fileMenu, editMenu, viewMenu, windowMenu],
|
||||
});
|
||||
await menu.setAsAppMenu();
|
||||
})().catch((e) => {
|
||||
console.debug("useNativeAppMenu: Aufbau fehlgeschlagen (kein Tauri-Menu?)", e);
|
||||
});
|
||||
return () => {
|
||||
disposed = true;
|
||||
};
|
||||
// Nur beim Mount aufbauen — Aktionen laufen über handlersRef, State-
|
||||
// Synchronisation über den Effekt unten. Ein Neu-Aufbau bei jedem Render
|
||||
// wäre teuer (mehrere async IPC-Aufrufe) und unnötig.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Enabled-/Checked-Zustand synchron zum React-State halten (billig, kein
|
||||
// Neu-Aufbau der Menüpunkte).
|
||||
useEffect(() => {
|
||||
if (!isTauriRuntime()) return;
|
||||
const items = itemsRef.current;
|
||||
void items.undo?.setEnabled(state.canUndo);
|
||||
void items.redo?.setEnabled(state.canRedo);
|
||||
void items.delete?.setEnabled(state.hasSelection);
|
||||
void items.floorplan?.setChecked(state.viewType === "grundriss");
|
||||
void items.perspective?.setChecked(state.viewType === "perspektive");
|
||||
for (const [id, item] of items.view3d) {
|
||||
void item.setChecked(state.view3d === id);
|
||||
void item.setEnabled(state.viewType === "perspektive");
|
||||
}
|
||||
void items.referenceLines?.setChecked(state.referenceLines);
|
||||
void items.resources?.setChecked(state.resourcesOpen);
|
||||
}, [
|
||||
state.canUndo,
|
||||
state.canRedo,
|
||||
state.hasSelection,
|
||||
state.viewType,
|
||||
state.view3d,
|
||||
state.referenceLines,
|
||||
state.resourcesOpen,
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Standort-Import ("Standort importieren…", geo.admin/swisstopo/OSM) als
|
||||
// EIGENES natives Tauri-Fenster statt Overlay-Dialog im Hauptfenster.
|
||||
//
|
||||
// Anders als Ressourcen/Einstellungen/Ebenen braucht dieser Dialog fast
|
||||
// keinen geteilten Zustand: der gesamte Netz-Abruf (Geocoding, swissBUILDINGS3D/
|
||||
// ALTI3D/SWISSIMAGE, OSM) läuft komplett INNERHALB von ContextImportDialog
|
||||
// selbst (reine `io/*`-Funktionen, kein Store-Zugriff) — ein eigenes Fenster
|
||||
// ist hier sogar naheliegender als beim Rest: ein langer Netz-Abruf blockiert
|
||||
// so nicht einmal gedanklich das Hauptfenster. Nur das ENDERGEBNIS
|
||||
// (fertige Kontext-Objekte + optionaler Georeferenzierungs-Bezug) muss zurück.
|
||||
//
|
||||
// Hauptfenster → Fenster: „context-import-sync-state" mit `{ geoAnchor }`.
|
||||
// Fenster → Hauptfenster: „context-import-command" mit `{name,args}` — wie
|
||||
// die übrigen Brücken (resourceWindow.ts etc.).
|
||||
//
|
||||
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { isTauriRuntime } from "../io/projectFile";
|
||||
import type { GeoAnchor } from "../ui/ContextImportDialog";
|
||||
import { BORDERLESS_WINDOW_CHROME } from "./windowChrome";
|
||||
|
||||
const CONTEXT_IMPORT_WINDOW_LABEL = "context-import";
|
||||
|
||||
export interface ContextImportSyncState {
|
||||
geoAnchor: GeoAnchor | undefined;
|
||||
}
|
||||
|
||||
export interface ContextImportCommandHandlers {
|
||||
onImport: (objs: unknown[]) => void;
|
||||
onSetGeoAnchor: (anchor: GeoAnchor | undefined) => void;
|
||||
}
|
||||
|
||||
/** Öffnet das native Standort-Import-Fenster (bzw. holt es nach vorne). */
|
||||
export async function openContextImportWindowNative(): Promise<void> {
|
||||
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
|
||||
const existing = await WebviewWindow.getByLabel(CONTEXT_IMPORT_WINDOW_LABEL);
|
||||
if (existing) {
|
||||
await existing.setFocus();
|
||||
return;
|
||||
}
|
||||
new WebviewWindow(CONTEXT_IMPORT_WINDOW_LABEL, {
|
||||
url: "index.html?window=context-import",
|
||||
title: "Standort importieren — Dossier",
|
||||
width: 480,
|
||||
height: 680,
|
||||
minWidth: 400,
|
||||
minHeight: 480,
|
||||
...BORDERLESS_WINDOW_CHROME,
|
||||
});
|
||||
}
|
||||
|
||||
/** Hauptfenster-Seite der Sync-Brücke. */
|
||||
export function useContextImportWindowHost(
|
||||
geoAnchor: GeoAnchor | undefined,
|
||||
handlers: ContextImportCommandHandlers,
|
||||
): void {
|
||||
const handlersRef = useRef(handlers);
|
||||
handlersRef.current = handlers;
|
||||
const geoAnchorRef = useRef(geoAnchor);
|
||||
geoAnchorRef.current = geoAnchor;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTauriRuntime()) return;
|
||||
let disposed = false;
|
||||
const unlisten: Array<() => void> = [];
|
||||
void (async () => {
|
||||
const { listen, emitTo } = await import("@tauri-apps/api/event");
|
||||
if (disposed) return;
|
||||
unlisten.push(
|
||||
await listen<{ name: string; args: unknown[] }>("context-import-command", (e) => {
|
||||
const fn = (handlersRef.current as unknown as Record<string, unknown>)[e.payload.name];
|
||||
if (typeof fn === "function") (fn as (...a: unknown[]) => void)(...e.payload.args);
|
||||
}),
|
||||
);
|
||||
unlisten.push(
|
||||
await listen("context-import-window-ready", () => {
|
||||
void emitTo(CONTEXT_IMPORT_WINDOW_LABEL, "context-import-sync-state", {
|
||||
geoAnchor: geoAnchorRef.current,
|
||||
}).catch(() => {});
|
||||
}),
|
||||
);
|
||||
})();
|
||||
return () => {
|
||||
disposed = true;
|
||||
unlisten.forEach((u) => u());
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTauriRuntime()) return;
|
||||
void (async () => {
|
||||
const { emitTo } = await import("@tauri-apps/api/event");
|
||||
await emitTo(CONTEXT_IMPORT_WINDOW_LABEL, "context-import-sync-state", {
|
||||
geoAnchor,
|
||||
}).catch(() => {});
|
||||
})();
|
||||
}, [geoAnchor]);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Zeichnungsebenen-Manager als EIGENES natives Tauri-Fenster — Referenz:
|
||||
// DOSSIER-Rhino-Plugin (`git.kgva.ch/karim/DOSSIER`, `src/DrawingLevelsApp.jsx`
|
||||
// + `rhino/layers_panel.py`), dort ein eigenständiges Rhino-Panel statt
|
||||
// angedockt. Hier: dieselbe Liste wie das bestehende Dock-Panel
|
||||
// (DrawingLevelsPanel.tsx), zusätzlich als Fenster öffenbar — das Dock-Panel
|
||||
// bleibt bestehen (bewusst additiv, nicht ersetzt, anders als beim
|
||||
// Ressourcen-Manager, der nie ein Dock-Panel war).
|
||||
//
|
||||
// Gleiches Sync-/Command-Prinzip wie settingsWindow.ts: ein expliziter, enger
|
||||
// Zustand (nicht das ganze Project) + eine {name,args}-Command-RPC.
|
||||
// **Bewusst NICHT unterstützt** (Scope-Entscheidung, s. PENDENZEN.md): das
|
||||
// Rechtsklick-Kontextmenü einer Zeile (Duplizieren/Löschen/Einstellungen…) —
|
||||
// das lebt heute als App-weites Overlay im HAUPTFENSTER; ein Klick im
|
||||
// Zeichnungsebenen-FENSTER kann es dort nicht sinnvoll positionieren. Bis das
|
||||
// eigens gelöst ist, bleibt Rechtsklick im Fenster ein No-op (Auswahl/
|
||||
// Sichtbarkeit/Hinzufügen funktionieren voll).
|
||||
//
|
||||
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { isTauriRuntime } from "../io/projectFile";
|
||||
import type { DrawingLevel } from "../model/types";
|
||||
import type { DisplayMode } from "../panels/types";
|
||||
import { BORDERLESS_WINDOW_CHROME } from "./windowChrome";
|
||||
|
||||
const DRAWING_LEVELS_WINDOW_LABEL = "drawing-levels";
|
||||
|
||||
export interface DrawingLevelsSyncState {
|
||||
drawingLevels: DrawingLevel[];
|
||||
activeLevelId: string;
|
||||
displayMode: DisplayMode;
|
||||
}
|
||||
|
||||
export interface DrawingLevelsCommandHandlers {
|
||||
onSelectLevel: (id: string) => void;
|
||||
onToggleLevel: (id: string) => void;
|
||||
onAddFloor: () => void;
|
||||
onAddSection: () => void;
|
||||
onAddElevation: () => void;
|
||||
onAddDrawing: () => void;
|
||||
onSetDisplayMode: (mode: DisplayMode) => void;
|
||||
}
|
||||
|
||||
/** Öffnet das native Zeichnungsebenen-Fenster (bzw. holt es nach vorne). */
|
||||
export async function openDrawingLevelsWindowNative(): Promise<void> {
|
||||
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
|
||||
const existing = await WebviewWindow.getByLabel(DRAWING_LEVELS_WINDOW_LABEL);
|
||||
if (existing) {
|
||||
await existing.setFocus();
|
||||
return;
|
||||
}
|
||||
new WebviewWindow(DRAWING_LEVELS_WINDOW_LABEL, {
|
||||
url: "index.html?window=drawing-levels",
|
||||
title: "Zeichnungsebenen — Dossier",
|
||||
width: 340,
|
||||
height: 560,
|
||||
minWidth: 260,
|
||||
minHeight: 300,
|
||||
...BORDERLESS_WINDOW_CHROME,
|
||||
});
|
||||
}
|
||||
|
||||
/** Hauptfenster-Seite der Sync-Brücke — Pendant zu useSettingsWindowHost. */
|
||||
export function useDrawingLevelsWindowHost(
|
||||
state: DrawingLevelsSyncState,
|
||||
handlers: DrawingLevelsCommandHandlers,
|
||||
): void {
|
||||
const handlersRef = useRef(handlers);
|
||||
handlersRef.current = handlers;
|
||||
const stateRef = useRef(state);
|
||||
stateRef.current = state;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTauriRuntime()) return;
|
||||
let disposed = false;
|
||||
const unlisten: Array<() => void> = [];
|
||||
void (async () => {
|
||||
const { listen, emitTo } = await import("@tauri-apps/api/event");
|
||||
if (disposed) return;
|
||||
unlisten.push(
|
||||
await listen<{ name: string; args: unknown[] }>("drawing-levels-command", (e) => {
|
||||
const fn = (handlersRef.current as unknown as Record<string, unknown>)[e.payload.name];
|
||||
if (typeof fn === "function") (fn as (...a: unknown[]) => void)(...e.payload.args);
|
||||
}),
|
||||
);
|
||||
unlisten.push(
|
||||
await listen("drawing-levels-window-ready", () => {
|
||||
void emitTo(
|
||||
DRAWING_LEVELS_WINDOW_LABEL,
|
||||
"drawing-levels-sync-state",
|
||||
stateRef.current,
|
||||
).catch(() => {});
|
||||
}),
|
||||
);
|
||||
})();
|
||||
return () => {
|
||||
disposed = true;
|
||||
unlisten.forEach((u) => u());
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTauriRuntime()) return;
|
||||
void (async () => {
|
||||
const { emitTo } = await import("@tauri-apps/api/event");
|
||||
await emitTo(DRAWING_LEVELS_WINDOW_LABEL, "drawing-levels-sync-state", state).catch(
|
||||
() => {},
|
||||
);
|
||||
})();
|
||||
}, [state]);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// Ebeneneinstellungen als EIGENES natives Tauri-Fenster — Referenz: DOSSIER-
|
||||
// Rhino-Plugin (`git.kgva.ch/karim/DOSSIER`, `rhino/layers_panel.py`
|
||||
// `_open_ebenen_settings`), das dort exakt so als „Satelliten-Fenster"
|
||||
// (Eto-Form, kein Docked-Panel) läuft. Ersetzt unter Tauri den bisherigen
|
||||
// `InlineEditor`-Popover (App.tsx, `editor?.kind === "layer"`); im reinen
|
||||
// Browser bleibt der Popover (kein OS-Fenster-Konzept dort).
|
||||
//
|
||||
// **Bewusst NUR die heute schon im Modell vorhandenen Felder** (Name/Farbe/
|
||||
// Linienstärke/Schraffur-Auswahl) — die Rhino-Version kennt zusätzlich eine
|
||||
// PRO-EBENE Schraffur-Skalierung/-Rotation/-Stiftstärke (`section.hatchScale`
|
||||
// etc.), die es in `LayerCategory` (model/types.ts) schlicht nicht gibt
|
||||
// (dort nur `hatch?: string`, eine Referenz auf die geteilte Hatch-Resource
|
||||
// ohne Per-Kategorie-Override). Das nachzurüsten wäre eine eigene
|
||||
// Modell-Erweiterung + generatePlan.ts-Verdrahtung, keine reine Fenster-
|
||||
// Frage — bewusst nicht mitgemacht, um hier nichts zu erfinden, das der Rest
|
||||
// der App nicht einlöst.
|
||||
//
|
||||
// Nur EIN Fenster gleichzeitig (Label fix "layer-settings", wie Rhinos
|
||||
// Satellit): erneutes Öffnen für eine andere Ebene schiebt nur den neuen Code
|
||||
// nach (kein Fenster-Zoo), analog dem Dropdown-Wechsel im Original.
|
||||
//
|
||||
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { isTauriRuntime } from "../io/projectFile";
|
||||
import { findCategory } from "../state/projectSlice";
|
||||
import type { HatchStyle, LayerCategory, Project } from "../model/types";
|
||||
import { BORDERLESS_WINDOW_CHROME } from "./windowChrome";
|
||||
|
||||
const LAYER_SETTINGS_WINDOW_LABEL = "layer-settings";
|
||||
|
||||
export interface LayerSettingsSyncState {
|
||||
code: string;
|
||||
name: string;
|
||||
color: string;
|
||||
lw: number;
|
||||
hatchId: string | undefined;
|
||||
hatches: { id: string; name: string }[];
|
||||
}
|
||||
|
||||
export interface LayerSettingsCommandHandlers {
|
||||
onPatchCategory: (code: string, patch: Partial<LayerCategory>) => void;
|
||||
}
|
||||
|
||||
/** Öffnet das native Ebeneneinstellungs-Fenster für `code` (bzw. schiebt den Code nach, falls schon offen). */
|
||||
export async function openLayerSettingsWindowNative(code: string): Promise<void> {
|
||||
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
|
||||
const existing = await WebviewWindow.getByLabel(LAYER_SETTINGS_WINDOW_LABEL);
|
||||
if (existing) {
|
||||
const { emitTo } = await import("@tauri-apps/api/event");
|
||||
await emitTo(LAYER_SETTINGS_WINDOW_LABEL, "layer-settings-set-code", code).catch(() => {});
|
||||
await existing.setFocus();
|
||||
return;
|
||||
}
|
||||
new WebviewWindow(LAYER_SETTINGS_WINDOW_LABEL, {
|
||||
url: `index.html?window=layer-settings&code=${encodeURIComponent(code)}`,
|
||||
title: "Ebeneneinstellungen — Dossier",
|
||||
width: 360,
|
||||
height: 420,
|
||||
minWidth: 300,
|
||||
minHeight: 340,
|
||||
...BORDERLESS_WINDOW_CHROME,
|
||||
});
|
||||
}
|
||||
|
||||
/** Baut den Sync-Zustand für eine Kategorie (bzw. `null`, wenn der Code nicht mehr existiert — z. B. gelöscht). */
|
||||
function stateFor(project: Project, code: string): LayerSettingsSyncState | null {
|
||||
const cat = findCategory(project.layers, code);
|
||||
if (!cat) return null;
|
||||
return {
|
||||
code: cat.code,
|
||||
name: cat.name,
|
||||
color: cat.color,
|
||||
lw: cat.lw,
|
||||
hatchId: cat.hatch,
|
||||
hatches: project.hatches.map((h: HatchStyle) => ({ id: h.id, name: h.name })),
|
||||
};
|
||||
}
|
||||
|
||||
/** Hauptfenster-Seite der Sync-Brücke. `code` = aktuell im Fenster editierte Ebene (folgt `openLayerSettingsWindowNative`-Aufrufen). */
|
||||
export function useLayerSettingsWindowHost(
|
||||
project: Project,
|
||||
code: string | null,
|
||||
onPatchCategory: (code: string, patch: Partial<LayerCategory>) => void,
|
||||
): void {
|
||||
const stateRef = useRef<LayerSettingsSyncState | null>(code ? stateFor(project, code) : null);
|
||||
stateRef.current = code ? stateFor(project, code) : null;
|
||||
const onPatchRef = useRef(onPatchCategory);
|
||||
onPatchRef.current = onPatchCategory;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTauriRuntime()) return;
|
||||
let disposed = false;
|
||||
const unlisten: Array<() => void> = [];
|
||||
void (async () => {
|
||||
const { listen, emitTo } = await import("@tauri-apps/api/event");
|
||||
if (disposed) return;
|
||||
unlisten.push(
|
||||
await listen<{ name: string; args: unknown[] }>("layer-settings-command", (e) => {
|
||||
if (e.payload.name === "onPatchCategory") {
|
||||
const [c, patch] = e.payload.args as [string, Partial<LayerCategory>];
|
||||
onPatchRef.current(c, patch);
|
||||
}
|
||||
}),
|
||||
);
|
||||
unlisten.push(
|
||||
await listen("layer-settings-window-ready", () => {
|
||||
void emitTo(
|
||||
LAYER_SETTINGS_WINDOW_LABEL,
|
||||
"layer-settings-sync-state",
|
||||
stateRef.current,
|
||||
).catch(() => {});
|
||||
}),
|
||||
);
|
||||
})();
|
||||
return () => {
|
||||
disposed = true;
|
||||
unlisten.forEach((u) => u());
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTauriRuntime()) return;
|
||||
void (async () => {
|
||||
const { emitTo } = await import("@tauri-apps/api/event");
|
||||
await emitTo(
|
||||
LAYER_SETTINGS_WINDOW_LABEL,
|
||||
"layer-settings-sync-state",
|
||||
stateRef.current,
|
||||
).catch(() => {});
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- stateRef.current ist bereits die aktuelle Ableitung
|
||||
}, [project, code]);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// Ressourcen-Manager als EIGENES natives Tauri-Fenster (statt schwebendes
|
||||
// Overlay im Hauptfenster) — Hauptfenster-Seite der Brücke.
|
||||
//
|
||||
// Ein natives Zweitfenster ist ein separater Webview-Prozesskontext: KEIN
|
||||
// gemeinsamer JS-Speicher, also kein direkter Store-Zugriff. Die Brücke
|
||||
// synchronisiert daher explizit über Tauri-Events (@tauri-apps/api/event):
|
||||
// • Hauptfenster → Ressourcen-Fenster: „resource-sync-project" mit dem
|
||||
// kompletten (JSON-serialisierbaren) Project bei jeder Änderung + einmal
|
||||
// sofort, sobald das Ressourcen-Fenster „resource-window-ready" meldet
|
||||
// (Race-Schutz: das Fenster könnte später starten als der erste Push).
|
||||
// • Ressourcen-Fenster → Hauptfenster: „resource-command" mit
|
||||
// `{ name, args }` — der Name eines ResourceManagerHandlers-Feldes plus
|
||||
// seine Argumente. Das Hauptfenster ruft den ECHTEN Handler auf (der
|
||||
// Projekt-Patches über setProject anwendet); die Antwort kommt implizit
|
||||
// über den nächsten „resource-sync-project"-Push zurück.
|
||||
// Das Gegenstück (Fenster-Seite: Proxy-Handlers + Projekt-State) lebt in
|
||||
// ResourceWindowApp.tsx.
|
||||
//
|
||||
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { isTauriRuntime } from "../io/projectFile";
|
||||
import type { Project } from "../model/types";
|
||||
import type { ResourceManagerHandlers, TabId } from "../ui/ResourceManager";
|
||||
import { BORDERLESS_WINDOW_CHROME } from "./windowChrome";
|
||||
|
||||
/** Fenster-Label des Ressourcen-Fensters (für emitTo/getByLabel). */
|
||||
const RESOURCE_WINDOW_LABEL = "resources";
|
||||
|
||||
/**
|
||||
* Öffnet das native Ressourcen-Fenster (bzw. holt es nach vorne, falls schon
|
||||
* offen). No-op-Fallback existiert NICHT hier — Aufrufer prüfen
|
||||
* `isTauriRuntime()` selbst und nutzen sonst das alte Overlay (App.tsx).
|
||||
*/
|
||||
export async function openResourceWindowNative(initialTab?: TabId): Promise<void> {
|
||||
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
|
||||
const existing = await WebviewWindow.getByLabel(RESOURCE_WINDOW_LABEL);
|
||||
if (existing) {
|
||||
if (initialTab) {
|
||||
const { emitTo } = await import("@tauri-apps/api/event");
|
||||
await emitTo(RESOURCE_WINDOW_LABEL, "resource-set-tab", initialTab).catch(() => {});
|
||||
}
|
||||
await existing.setFocus();
|
||||
return;
|
||||
}
|
||||
const url =
|
||||
"index.html?window=resources" + (initialTab ? `&tab=${encodeURIComponent(initialTab)}` : "");
|
||||
new WebviewWindow(RESOURCE_WINDOW_LABEL, {
|
||||
url,
|
||||
title: "Ressourcen — Dossier",
|
||||
width: 780,
|
||||
height: 660,
|
||||
minWidth: 480,
|
||||
minHeight: 360,
|
||||
...BORDERLESS_WINDOW_CHROME,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hauptfenster-Seite der Sync-Brücke: hält das Ressourcen-Fenster (falls
|
||||
* offen) mit dem aktuellen Projekt synchron und wendet dessen Befehle auf die
|
||||
* ECHTEN Handler an. No-op ausserhalb von Tauri. In App.tsx neben der
|
||||
* bestehenden `<ResourceManager>`-Einbettung aufrufen (dieselben
|
||||
* `resourceHandlers`).
|
||||
*/
|
||||
export function useResourceWindowHost(
|
||||
project: Project,
|
||||
handlers: ResourceManagerHandlers,
|
||||
): void {
|
||||
const handlersRef = useRef(handlers);
|
||||
handlersRef.current = handlers;
|
||||
const projectRef = useRef(project);
|
||||
projectRef.current = project;
|
||||
|
||||
// Befehle vom Ressourcen-Fenster entgegennehmen + auf die echten Handler
|
||||
// anwenden, UND bei „ready" (Fenster gerade erst gestartet) sofort das
|
||||
// aktuelle Projekt nachschieben.
|
||||
useEffect(() => {
|
||||
if (!isTauriRuntime()) return;
|
||||
let disposed = false;
|
||||
const unlisten: Array<() => void> = [];
|
||||
void (async () => {
|
||||
const { listen, emitTo } = await import("@tauri-apps/api/event");
|
||||
if (disposed) return;
|
||||
unlisten.push(
|
||||
await listen<{ name: string; args: unknown[] }>("resource-command", (e) => {
|
||||
const fn = (handlersRef.current as unknown as Record<string, unknown>)[e.payload.name];
|
||||
if (typeof fn === "function") (fn as (...a: unknown[]) => void)(...e.payload.args);
|
||||
}),
|
||||
);
|
||||
unlisten.push(
|
||||
await listen("resource-window-ready", () => {
|
||||
void emitTo(RESOURCE_WINDOW_LABEL, "resource-sync-project", projectRef.current).catch(
|
||||
() => {},
|
||||
);
|
||||
}),
|
||||
);
|
||||
})();
|
||||
return () => {
|
||||
disposed = true;
|
||||
unlisten.forEach((u) => u());
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Bei jeder Projektänderung ans (evtl. offene) Ressourcen-Fenster pushen.
|
||||
// `emitTo` auf ein nicht existierendes Fenster ist harmlos (kein Empfänger).
|
||||
useEffect(() => {
|
||||
if (!isTauriRuntime()) return;
|
||||
void (async () => {
|
||||
const { emitTo } = await import("@tauri-apps/api/event");
|
||||
await emitTo(RESOURCE_WINDOW_LABEL, "resource-sync-project", project).catch(() => {});
|
||||
})();
|
||||
}, [project]);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// Einstellungen als EIGENES natives Tauri-Fenster (statt Overlay-Dialog im
|
||||
// Hauptfenster) — Hauptfenster-Seite der Brücke. Gleiches Prinzip wie
|
||||
// resourceWindow.ts, aber schmaler: SettingsDialog braucht nur eine Handvoll
|
||||
// einzelner Felder (nicht das ganze Project), daher ein explizites
|
||||
// Sync-/Command-Paar statt eines generischen Proxys über 27 Handler.
|
||||
//
|
||||
// Hauptfenster → Einstellungs-Fenster: „settings-sync-state" mit
|
||||
// { marqueeColor, snapColor, referenceElevationMasl, rendererMode,
|
||||
// webGpuAvailable, layout } bei jeder Änderung + auf „settings-window-ready".
|
||||
// Einstellungs-Fenster → Hauptfenster: „settings-command" mit
|
||||
// { name, args } — dieselbe {name,args}-RPC-Form wie resource-command.
|
||||
//
|
||||
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { isTauriRuntime } from "../io/projectFile";
|
||||
import type { LayoutState } from "../panels/types";
|
||||
import type { RendererMode } from "../ui/StatusBar";
|
||||
import type { ThemeMode } from "../theme/themeMode";
|
||||
import { BORDERLESS_WINDOW_CHROME } from "./windowChrome";
|
||||
|
||||
const SETTINGS_WINDOW_LABEL = "settings";
|
||||
|
||||
/** Alles, was SettingsDialog zum Rendern braucht — wandert 1:1 rüber. */
|
||||
export interface SettingsSyncState {
|
||||
themeMode: ThemeMode;
|
||||
marqueeColor: string;
|
||||
snapColor: string;
|
||||
referenceElevationMasl: number | undefined;
|
||||
rendererMode: RendererMode;
|
||||
webGpuAvailable: boolean;
|
||||
layout: LayoutState;
|
||||
}
|
||||
|
||||
/** Alle Aktionen, die das Einstellungs-Fenster auslösen kann. */
|
||||
export interface SettingsCommandHandlers {
|
||||
onThemeModeChange: (mode: ThemeMode) => void;
|
||||
onMarqueeColorChange: (hex: string) => void;
|
||||
onSnapColorChange: (hex: string) => void;
|
||||
onReferenceElevationChange: (n: number | undefined) => void;
|
||||
onRendererModeChange: (mode: RendererMode) => void;
|
||||
onApplyLayout: (layout: LayoutState) => void;
|
||||
}
|
||||
|
||||
/** Öffnet das native Einstellungs-Fenster (bzw. holt es nach vorne). */
|
||||
export async function openSettingsWindowNative(): Promise<void> {
|
||||
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
|
||||
const existing = await WebviewWindow.getByLabel(SETTINGS_WINDOW_LABEL);
|
||||
if (existing) {
|
||||
await existing.setFocus();
|
||||
return;
|
||||
}
|
||||
new WebviewWindow(SETTINGS_WINDOW_LABEL, {
|
||||
url: "index.html?window=settings",
|
||||
title: "Einstellungen — Dossier",
|
||||
width: 520,
|
||||
height: 640,
|
||||
minWidth: 420,
|
||||
minHeight: 420,
|
||||
...BORDERLESS_WINDOW_CHROME,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hauptfenster-Seite der Sync-Brücke: hält das Einstellungs-Fenster (falls
|
||||
* offen) synchron zu `state` und wendet dessen Befehle auf `handlers` an.
|
||||
* No-op ausserhalb von Tauri.
|
||||
*/
|
||||
export function useSettingsWindowHost(
|
||||
state: SettingsSyncState,
|
||||
handlers: SettingsCommandHandlers,
|
||||
): void {
|
||||
const handlersRef = useRef(handlers);
|
||||
handlersRef.current = handlers;
|
||||
const stateRef = useRef(state);
|
||||
stateRef.current = state;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTauriRuntime()) return;
|
||||
let disposed = false;
|
||||
const unlisten: Array<() => void> = [];
|
||||
void (async () => {
|
||||
const { listen, emitTo } = await import("@tauri-apps/api/event");
|
||||
if (disposed) return;
|
||||
unlisten.push(
|
||||
await listen<{ name: string; args: unknown[] }>("settings-command", (e) => {
|
||||
const fn = (handlersRef.current as unknown as Record<string, unknown>)[e.payload.name];
|
||||
if (typeof fn === "function") (fn as (...a: unknown[]) => void)(...e.payload.args);
|
||||
}),
|
||||
);
|
||||
unlisten.push(
|
||||
await listen("settings-window-ready", () => {
|
||||
void emitTo(SETTINGS_WINDOW_LABEL, "settings-sync-state", stateRef.current).catch(
|
||||
() => {},
|
||||
);
|
||||
}),
|
||||
);
|
||||
})();
|
||||
return () => {
|
||||
disposed = true;
|
||||
unlisten.forEach((u) => u());
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTauriRuntime()) return;
|
||||
void (async () => {
|
||||
const { emitTo } = await import("@tauri-apps/api/event");
|
||||
await emitTo(SETTINGS_WINDOW_LABEL, "settings-sync-state", state).catch(() => {});
|
||||
})();
|
||||
}, [state]);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Gemeinsame Fenster-Chrome für native Nebenfenster (Ressourcen, Einstellungen,
|
||||
// künftige weitere) — spiegelt das Hauptfenster (tauri.conf.json: randlos, macOS-
|
||||
// Overlay-Titelleiste, eigener Drag-Header) statt der nativen OS-Titelzeile samt
|
||||
// Titeltext. Ohne das trägt jedes Nebenfenster ZWEI optisch übereinander
|
||||
// liegende „Kopfzeilen" (OS-Titel + unser eigener `res-head`/`.win-head`) — genau
|
||||
// das vom Nutzer bemängelte „wirkt am Header etwas weird".
|
||||
//
|
||||
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
|
||||
|
||||
import { LogicalPosition } from "@tauri-apps/api/dpi";
|
||||
|
||||
/** In `new WebviewWindow(label, { ...BORDERLESS_WINDOW_CHROME, ... })` mergen.
|
||||
* `trafficLightPosition` zentriert die macOS-Ampeln vertikal in unserer 40px-
|
||||
* Kopfzeile (sonst sitzen sie höher als der Titeltext). */
|
||||
export const BORDERLESS_WINDOW_CHROME = {
|
||||
titleBarStyle: "overlay" as const,
|
||||
hiddenTitle: true,
|
||||
trafficLightPosition: new LogicalPosition(20, 22),
|
||||
};
|
||||
Reference in New Issue
Block a user