Start-Sequenz ergänzt: Splash-Screen, dann Startbildschirm mit Neues Projekt/Projekt öffnen/Zuletzt geöffnet.
Zuletzt-geöffnet-Liste (state/recentProjects.ts) trägt sich beim Öffnen und beim Speichern automatisch ein, inkl. Öffnen über einen bekannten Pfad ohne Dialog (verwaiste Einträge fliegen raus, wenn die Datei fehlt). Der Splash wartet bewusst nicht auf den Update-Check — der läuft unabhängig im Hintergrund weiter, damit ein langsames Netz den Start nie verzögert.
This commit is contained in:
+35
@@ -210,6 +210,9 @@ import {
|
||||
// Seiteneffekt: registriert die zwei dockbaren Kern-Panels in der Registry.
|
||||
import { BUILTIN_PANEL_IDS } from "./panels/builtinPanels";
|
||||
import { deriveDrawings } from "./DrawingTabs";
|
||||
import { SplashScreen } from "./ui/SplashScreen";
|
||||
import { StartScreen } from "./ui/StartScreen";
|
||||
import { emptyProject } from "./model/sampleProject";
|
||||
|
||||
/**
|
||||
* Offenes Kontextmenü — genau eine Quelle. `x/y` sind Viewport-Pixel (clientX/Y).
|
||||
@@ -266,6 +269,13 @@ export default function App() {
|
||||
// umgeschaltet wird (alle Kinder lesen denselben modulweiten Zustand über t).
|
||||
useT();
|
||||
|
||||
// ── Boot-Sequenz: Splash → Startbildschirm (Neu/Öffnen/Zuletzt) → Editor ──
|
||||
// Wie bei den meisten CAD-Programmen (s. ui/SplashScreen.tsx/StartScreen.tsx).
|
||||
// Läuft NUR einmal pro Fensterstart; sobald "editor" erreicht ist, bleibt der
|
||||
// volle Editor bestehen (auch bei späteren File>Neu/Öffnen-Aktionen, die
|
||||
// weiterhin über das normale Menü laufen).
|
||||
const [bootPhase, setBootPhase] = useState<"splash" | "start" | "editor">("splash");
|
||||
|
||||
// ── Globaler Zustand aus dem Store (Slices: project/selection/view/layout) ─
|
||||
// Pro useStore-Aufruf EIN Feld selektieren (Referenz-Gleichheit, sonst
|
||||
// Endlosschleife). Actions leben ebenfalls im Zustand und sind referenzstabil.
|
||||
@@ -2158,6 +2168,7 @@ export default function App() {
|
||||
onExportObj,
|
||||
onExportStl,
|
||||
onOpenProject,
|
||||
openProjectAtPath,
|
||||
onProjectFileChosen,
|
||||
runImport,
|
||||
} = useProjectFileActions(
|
||||
@@ -4457,6 +4468,30 @@ export default function App() {
|
||||
if (f) openImportForFile(f);
|
||||
};
|
||||
|
||||
if (bootPhase === "splash") {
|
||||
return <SplashScreen onDone={() => setBootPhase("start")} />;
|
||||
}
|
||||
if (bootPhase === "start") {
|
||||
return (
|
||||
<StartScreen
|
||||
onNewProject={() => {
|
||||
setProject(emptyProject);
|
||||
setBootPhase("editor");
|
||||
}}
|
||||
onOpenProject={() => {
|
||||
onOpenProject();
|
||||
setBootPhase("editor");
|
||||
}}
|
||||
onOpenRecent={(path) => {
|
||||
void openProjectAtPath(path).then((found) => {
|
||||
if (!found) notify(t("start.recentMissing"), "error");
|
||||
});
|
||||
setBootPhase("editor");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="app"
|
||||
|
||||
@@ -1636,6 +1636,15 @@ export const de = {
|
||||
"layouts.size.widthMm": "Breite (mm)",
|
||||
"layouts.size.heightMm": "Höhe (mm)",
|
||||
"layouts.size.create": "Erstellen",
|
||||
|
||||
// Startbildschirm (nach dem Splash, vor dem Editor)
|
||||
"start.newProject": "Neues Projekt",
|
||||
"start.newProject.hint": "Leeres Projekt anlegen",
|
||||
"start.openProject": "Projekt öffnen …",
|
||||
"start.openProject.hint": "Bestehende .obp-Datei auswählen",
|
||||
"start.recent": "Zuletzt geöffnet",
|
||||
"start.recent.remove": "Aus der Liste entfernen",
|
||||
"start.recentMissing": "Datei nicht gefunden — aus der Liste entfernt.",
|
||||
} as const;
|
||||
|
||||
export type TranslationKey = keyof typeof de;
|
||||
|
||||
@@ -1623,4 +1623,13 @@ export const en: Record<TranslationKey, string> = {
|
||||
"layouts.size.widthMm": "Width (mm)",
|
||||
"layouts.size.heightMm": "Height (mm)",
|
||||
"layouts.size.create": "Create",
|
||||
|
||||
// Start screen (after the splash, before the editor)
|
||||
"start.newProject": "New project",
|
||||
"start.newProject.hint": "Create a blank project",
|
||||
"start.openProject": "Open project …",
|
||||
"start.openProject.hint": "Choose an existing .obp file",
|
||||
"start.recent": "Recently opened",
|
||||
"start.recent.remove": "Remove from list",
|
||||
"start.recentMissing": "File not found — removed from the list.",
|
||||
};
|
||||
|
||||
+28
-2
@@ -55,6 +55,26 @@ export async function openProjectFileNative(): Promise<OpenedProjectFile | null>
|
||||
return { path: selected, content };
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet eine Projektdatei über einen BEKANNTEN Pfad (kein Dialog) — für den
|
||||
* Startbildschirm/„Zuletzt geöffnet" (state/recentProjects.ts): der Pfad kommt
|
||||
* dort schon aus einem früheren Öffnen/Speichern. Nur unter Tauri sinnvoll.
|
||||
*
|
||||
* @returns `null` ohne Tauri oder wenn die Datei nicht mehr lesbar ist (z. B.
|
||||
* gelöscht/verschoben) — der Aufrufer sollte den Eintrag dann aus der
|
||||
* Zuletzt-geöffnet-Liste entfernen.
|
||||
*/
|
||||
export async function openProjectFileAtPath(path: string): Promise<OpenedProjectFile | null> {
|
||||
if (!isTauriRuntime()) return null;
|
||||
try {
|
||||
const { readTextFile } = await import("@tauri-apps/plugin-fs");
|
||||
const content = await readTextFile(path);
|
||||
return { path, content };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert das Projekt-JSON in eine `.obp`-Datei. Unter Tauri nativer
|
||||
* Speichern-Dialog mit Projekt-Filter, im Browser Blob-Download — beides via
|
||||
@@ -62,10 +82,16 @@ export async function openProjectFileNative(): Promise<OpenedProjectFile | null>
|
||||
*
|
||||
* @param json Fertig serialisiertes Projekt (JSON.stringify des ProjectSlice).
|
||||
* @param projectName `project.name` für den Standard-Dateinamen (Fallback "projekt").
|
||||
* @param onSavedPath Optionaler Callback mit dem gewählten Pfad (Zuletzt-
|
||||
* geöffnet-Liste, s. state/recentProjects.ts).
|
||||
*/
|
||||
export async function saveProjectFile(json: string, projectName: string): Promise<boolean> {
|
||||
export async function saveProjectFile(
|
||||
json: string,
|
||||
projectName: string,
|
||||
onSavedPath?: (path: string) => void,
|
||||
): Promise<boolean> {
|
||||
const filename = `${projectName || "projekt"}.${PROJECT_EXT}`;
|
||||
return saveTextFile(json, filename, "application/json", PROJECT_DIALOG_FILTERS);
|
||||
return saveTextFile(json, filename, "application/json", PROJECT_DIALOG_FILTERS, onSavedPath);
|
||||
}
|
||||
|
||||
// ── Lock gegen Doppelöffnung (nur Tauri) ────────────────────────────────────
|
||||
|
||||
@@ -26,6 +26,9 @@ export interface FileDialogFilter {
|
||||
* @param filters Optionale Datei-Endungsfilter fuer den nativen Dialog (z. B.
|
||||
* Projekt-Endung). Additiv/optional, damit bestehende Aufrufer ohne
|
||||
* Filter unveraendert funktionieren (Default-Verhalten: irgendeine Endung).
|
||||
* @param onSavedPath Optionaler Callback mit dem gewaehlten Pfad, NUR bei
|
||||
* erfolgreichem Tauri-Speichern (z. B. fuer die Zuletzt-geoeffnet-Liste,
|
||||
* s. state/recentProjects.ts) — additiv, aendert den Rueckgabewert nicht.
|
||||
* @returns `true` wenn geschrieben/geladen wurde, `false` bei Abbruch (Dialog
|
||||
* ohne Pfad). Bei einem Tauri-Fehler wird auf den Browser-Fallback
|
||||
* zurueckgefallen.
|
||||
@@ -35,6 +38,7 @@ export async function saveTextFile(
|
||||
defaultName: string,
|
||||
mime = "application/octet-stream",
|
||||
filters?: FileDialogFilter[],
|
||||
onSavedPath?: (path: string) => void,
|
||||
): Promise<boolean> {
|
||||
if (isTauri()) {
|
||||
try {
|
||||
@@ -43,6 +47,7 @@ export async function saveTextFile(
|
||||
if (!path) return false; // Nutzer hat abgebrochen
|
||||
const { writeTextFile } = await import("@tauri-apps/plugin-fs");
|
||||
await writeTextFile(path, content);
|
||||
onSavedPath?.(path);
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (!warnedFallback) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
// laufen. Die Slice tippt daher nur ihre eigenen Felder; `get()` liefert
|
||||
// RootState (siehe appStore.ts), wir greifen punktuell auf View-Felder zu.
|
||||
|
||||
import { emptyProject } from "../model/sampleProject";
|
||||
import { sampleProject } from "../model/sampleProject";
|
||||
import {
|
||||
flattenCategories,
|
||||
recomputeFloorElevations,
|
||||
@@ -363,10 +363,11 @@ export function createProjectSlice(
|
||||
return {
|
||||
...history,
|
||||
|
||||
// TEMPORÄR für manuellen Test (Nutzer-Wunsch): leeres Projekt statt Demo-
|
||||
// Haus. NICHT committen — vor dem nächsten echten Commit zurück auf
|
||||
// `sampleProject` stellen.
|
||||
project: emptyProject,
|
||||
// Store-Default beim Modul-Laden — sichtbar nur ganz kurz HINTER dem
|
||||
// Splash/Startbildschirm (App.tsx bootPhase: splash → start → editor),
|
||||
// bevor eine der drei Optionen (Neu/Öffnen/Zuletzt) das echte Projekt
|
||||
// setzt. Der konkrete Wert spielt daher praktisch keine Rolle mehr.
|
||||
project: sampleProject,
|
||||
|
||||
setProject,
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// Liste zuletzt geöffneter/gespeicherter Projektdateien für den Startbildschirm
|
||||
// (s. ui/StartScreen.tsx). Nur Pfade + Metadaten, lokal in localStorage — der
|
||||
// Dateiinhalt selbst wird erst beim Anklicken über den echten Pfad geladen
|
||||
// (io/projectFile.ts), damit die Liste nie mit dem Dateiinhalt auseinanderläuft.
|
||||
// Nur unter Tauri sinnvoll (echte Dateipfade); im Browser bleibt sie leer.
|
||||
|
||||
const STORAGE_KEY = "dossier.recentProjects";
|
||||
const MAX_ENTRIES = 8;
|
||||
|
||||
export interface RecentProjectEntry {
|
||||
path: string;
|
||||
name: string;
|
||||
/** ISO-Zeitstempel des letzten Öffnens/Speicherns. */
|
||||
openedAt: string;
|
||||
}
|
||||
|
||||
function readAll(): RecentProjectEntry[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return [];
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? (parsed as RecentProjectEntry[]) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function writeAll(entries: RecentProjectEntry[]): void {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
|
||||
}
|
||||
|
||||
/** Neueste zuerst. */
|
||||
export function getRecentProjects(): RecentProjectEntry[] {
|
||||
return readAll();
|
||||
}
|
||||
|
||||
/** Trägt einen Pfad als "gerade geöffnet/gespeichert" ein (an den Anfang,
|
||||
* Duplikate entfernt, auf MAX_ENTRIES gekappt). */
|
||||
export function addRecentProject(path: string, name: string): void {
|
||||
const rest = readAll().filter((e) => e.path !== path);
|
||||
const next = [{ path, name, openedAt: new Date().toISOString() }, ...rest].slice(
|
||||
0,
|
||||
MAX_ENTRIES,
|
||||
);
|
||||
writeAll(next);
|
||||
}
|
||||
|
||||
/** Entfernt einen Eintrag (z. B. wenn die Datei beim Öffnen nicht mehr existiert). */
|
||||
export function removeRecentProject(path: string): void {
|
||||
writeAll(readAll().filter((e) => e.path !== path));
|
||||
}
|
||||
@@ -21,12 +21,14 @@ import { saveTextFile } from "../io/saveFile";
|
||||
import {
|
||||
saveProjectFile,
|
||||
openProjectFileNative,
|
||||
openProjectFileAtPath,
|
||||
acquireProjectLock,
|
||||
releaseProjectLock,
|
||||
registerLockAutoRelease,
|
||||
isTauriRuntime,
|
||||
} from "../io/projectFile";
|
||||
import type { LockInfo } from "../io/projectFile";
|
||||
import { addRecentProject, removeRecentProject } from "./recentProjects";
|
||||
import type { ContextObject, Drawing2D, Project, Vec2 } from "../model/types";
|
||||
import type { PlanViewHandle } from "../plan/PlanView";
|
||||
|
||||
@@ -187,7 +189,9 @@ export function useProjectFileActions(
|
||||
// erlaubt beide Endungen).
|
||||
const onSaveProject = () => {
|
||||
const json = JSON.stringify(project, null, 2);
|
||||
void saveProjectFile(json, project.name);
|
||||
void saveProjectFile(json, project.name, (path) =>
|
||||
addRecentProject(path, project.name || "modell"),
|
||||
);
|
||||
};
|
||||
// Gibt den Lock der zuletzt geöffneten Projektdatei frei (falls wir ihn
|
||||
// halten) und vergisst den Pfad — vor dem Wechsel auf ein anderes Projekt.
|
||||
@@ -212,6 +216,7 @@ export function useProjectFileActions(
|
||||
setProject(parsed as Project);
|
||||
openedProjectPathRef.current = path;
|
||||
projectLockOwnedRef.current = lockOwned;
|
||||
addRecentProject(path, (parsed as Project).name || "modell");
|
||||
} catch (err) {
|
||||
console.error("Projekt-Import fehlgeschlagen:", err);
|
||||
}
|
||||
@@ -238,6 +243,24 @@ export function useProjectFileActions(
|
||||
setLockConflict({ path: opened.path, content: opened.content, info: lock.info });
|
||||
}
|
||||
};
|
||||
// Öffnen über einen BEKANNTEN Pfad (Startbildschirm/„Zuletzt geöffnet",
|
||||
// s. ui/StartScreen.tsx) — kein Dialog, sonst identischer Lock-Fluss wie
|
||||
// `openProjectViaNativeDialog`. Ist die Datei nicht mehr lesbar (gelöscht/
|
||||
// verschoben), wird der verwaiste Eintrag aus der Liste entfernt.
|
||||
const openProjectAtPath = async (path: string): Promise<boolean> => {
|
||||
const opened = await openProjectFileAtPath(path);
|
||||
if (!opened) {
|
||||
removeRecentProject(path);
|
||||
return false;
|
||||
}
|
||||
const lock = await acquireProjectLock(opened.path);
|
||||
if (lock.ok) {
|
||||
applyOpenedProjectFile(opened.path, opened.content, true);
|
||||
} else {
|
||||
setLockConflict({ path: opened.path, content: opened.content, info: lock.info });
|
||||
}
|
||||
return true;
|
||||
};
|
||||
// Datei speichern: unter Tauri nativer „Speichern unter"-Dialog (Ordner + Name),
|
||||
// im Browser Blob-Download-Fallback — die gesamte Logik (inkl. WKWebView-
|
||||
// Härtung: octet-stream-MIME + verzögertes revoke) steckt in saveTextFile.
|
||||
@@ -351,6 +374,7 @@ export function useProjectFileActions(
|
||||
releaseCurrentProjectLock,
|
||||
applyOpenedProjectFile,
|
||||
openProjectViaNativeDialog,
|
||||
openProjectAtPath,
|
||||
downloadTextFile,
|
||||
projectFileName,
|
||||
runExport,
|
||||
|
||||
+133
@@ -6880,3 +6880,136 @@ body {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Splash + Startbildschirm (App.tsx: bootPhase splash → start → editor) ──
|
||||
Immer dunkel (wie der Über-Dialog), unabhängig vom Hell/Dunkel-Theme —
|
||||
eigener Marken-Moment vor dem eigentlichen Editor. */
|
||||
.splash-screen,
|
||||
.start-screen {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #16181c;
|
||||
color: #e6e8ea;
|
||||
}
|
||||
.splash-brand,
|
||||
.start-brand {
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
text-transform: lowercase;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.start-brand {
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
.start-actions {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
}
|
||||
.start-action {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
min-width: 200px;
|
||||
padding: 16px 18px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #3a3f47;
|
||||
background: #23262b;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.12s, background 0.12s;
|
||||
}
|
||||
.start-action:hover {
|
||||
border-color: var(--accent, #0f766e);
|
||||
background: #292d33;
|
||||
}
|
||||
.start-action-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.start-action-hint {
|
||||
font-size: 11px;
|
||||
color: #9aa0a6;
|
||||
}
|
||||
.start-recent {
|
||||
margin-top: 36px;
|
||||
width: 420px;
|
||||
max-width: calc(100vw - 48px);
|
||||
}
|
||||
.start-recent-title {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: #9aa0a6;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.start-recent-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.start-recent-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.start-recent-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1px;
|
||||
min-width: 0;
|
||||
padding: 7px 10px;
|
||||
border-radius: 7px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
.start-recent-item:hover {
|
||||
background: #23262b;
|
||||
}
|
||||
.start-recent-name {
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
.start-recent-path {
|
||||
font-size: 10.5px;
|
||||
color: #767c83;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
.start-recent-remove {
|
||||
flex: 0 0 auto;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #767c83;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
.start-recent-remove:hover {
|
||||
background: #23262b;
|
||||
color: #e6e8ea;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Kurzer Marken-Splash beim Start, bevor der Startbildschirm (StartScreen)
|
||||
// erscheint. BEWUSST keine künstliche Wartezeit/„Ladebalken"-Inszenierung:
|
||||
// kommerzielle CAD-Programme brauchen beim Start oft mehrere Sekunden für
|
||||
// Lizenzserver-Handshakes, riesige Bauteil-/Vorlagenbibliotheken und
|
||||
// historisch gewachsene Plugin-Ketten — wir starten schnell, WEIL es nichts
|
||||
// dergleichen gibt, nicht obwohl wir etwas Echtes überspringen. Die einzige
|
||||
// Wartezeit hier ist eine kurze, feste Mindestdauer für die Markenwirkung
|
||||
// (sonst nur ein Flackern bei einem sehr schnellen Rechner) — kein Fake-Fortschritt.
|
||||
//
|
||||
// Der stille Update-Check (state/checkForUpdate.ts) läuft bewusst NICHT hier,
|
||||
// sondern unabhängig im Hintergrund weiter (App.tsx) — ein langsames/nicht
|
||||
// erreichbares Netz soll den Start nie verzögern.
|
||||
import { useEffect } from "react";
|
||||
|
||||
/** Mindest-Anzeigedauer (ms) — rein für die Markenwirkung, kein simulierter Ladevorgang. */
|
||||
const MIN_VISIBLE_MS = 500;
|
||||
|
||||
export function SplashScreen({ onDone }: { onDone: () => void }) {
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(onDone, MIN_VISIBLE_MS);
|
||||
return () => clearTimeout(timer);
|
||||
}, [onDone]);
|
||||
|
||||
return (
|
||||
<div className="splash-screen">
|
||||
<div className="splash-brand">
|
||||
dossier<span className="brand-dot">.</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Startbildschirm nach dem Splash (s. SplashScreen.tsx) — wie bei den meisten
|
||||
// CAD-Programmen: „Neues Projekt" / „Projekt öffnen…" plus eine Liste zuletzt
|
||||
// geöffneter/gespeicherter Projekte (state/recentProjects.ts, nur Tauri —
|
||||
// im Browser gibt es keine echten Dateipfade zum Wiederöffnen).
|
||||
import { useState } from "react";
|
||||
import { t } from "../i18n";
|
||||
import { isTauriRuntime } from "../io/projectFile";
|
||||
import {
|
||||
getRecentProjects,
|
||||
removeRecentProject,
|
||||
type RecentProjectEntry,
|
||||
} from "../state/recentProjects";
|
||||
|
||||
export function StartScreen({
|
||||
onNewProject,
|
||||
onOpenProject,
|
||||
onOpenRecent,
|
||||
}: {
|
||||
onNewProject: () => void;
|
||||
onOpenProject: () => void;
|
||||
onOpenRecent: (path: string) => void;
|
||||
}) {
|
||||
const [recent, setRecent] = useState<RecentProjectEntry[]>(() => getRecentProjects());
|
||||
const showRecent = isTauriRuntime() && recent.length > 0;
|
||||
|
||||
return (
|
||||
<div className="start-screen">
|
||||
<div className="start-brand">
|
||||
dossier<span className="brand-dot">.</span>
|
||||
</div>
|
||||
<div className="start-actions">
|
||||
<button type="button" className="start-action" onClick={onNewProject}>
|
||||
<span className="start-action-title">{t("start.newProject")}</span>
|
||||
<span className="start-action-hint">{t("start.newProject.hint")}</span>
|
||||
</button>
|
||||
<button type="button" className="start-action" onClick={onOpenProject}>
|
||||
<span className="start-action-title">{t("start.openProject")}</span>
|
||||
<span className="start-action-hint">{t("start.openProject.hint")}</span>
|
||||
</button>
|
||||
</div>
|
||||
{showRecent && (
|
||||
<div className="start-recent">
|
||||
<div className="start-recent-title">{t("start.recent")}</div>
|
||||
<ul className="start-recent-list">
|
||||
{recent.map((e) => (
|
||||
<li key={e.path} className="start-recent-row">
|
||||
<button
|
||||
type="button"
|
||||
className="start-recent-item"
|
||||
onClick={() => onOpenRecent(e.path)}
|
||||
title={e.path}
|
||||
>
|
||||
<span className="start-recent-name">{e.name}</span>
|
||||
<span className="start-recent-path">{e.path}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="start-recent-remove"
|
||||
aria-label={t("start.recent.remove")}
|
||||
title={t("start.recent.remove")}
|
||||
onClick={() => {
|
||||
removeRecentProject(e.path);
|
||||
setRecent(getRecentProjects());
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user