v0.1.4-pre.008
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: crates/ksp-app-config-desk/Cargo.toml
|
||||
# version: 3
|
||||
# version: 4
|
||||
|
||||
[package]
|
||||
name = "ksp-app-config-desk"
|
||||
@@ -29,6 +29,7 @@ ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||
serde.workspace = true
|
||||
tauri.workspace = true
|
||||
tauri-plugin-tracing.workspace = true
|
||||
tokio.workspace = true
|
||||
ts-rs.workspace = true
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-config-desk/README.md -->
|
||||
<!-- version: 6 -->
|
||||
<!-- version: 7 -->
|
||||
|
||||
# `ksp-app-config-desk`
|
||||
|
||||
@@ -21,7 +21,7 @@ Le gabarit desktop actif fournit maintenant :
|
||||
|
||||
- le verrou single-instance ;
|
||||
- l'assemblage Tauri centralisé dans `src/tauri.rs` ;
|
||||
- les fenêtres `splash` et `main` ;
|
||||
- les fenêtres `splash` et `main`, orchestrées par `tw_splash.rs` et `tw_main.rs` ;
|
||||
- le frontend Vanilla TypeScript + Vite sous `frontend/` ;
|
||||
- SCSS, Bootstrap et Font Awesome ;
|
||||
- SimpleBar + `resize-observer-polyfill` pour le scrolling du shell ;
|
||||
@@ -29,7 +29,8 @@ Le gabarit desktop actif fournit maintenant :
|
||||
- les ports dédiés `1430` pour Vite HTTP et `1431` pour le WebSocket de développement ;
|
||||
- la destination frontend externe commune à Tauri et Vite;
|
||||
- le bootstrap Config/Logging réel, un `AppState` durable et le premier contrat Tauri/TS-RS `get_app_snapshot`;
|
||||
- le bridge de logging frontend KSP `frontend_log.ts`, routé par `emit_frontend_log` vers des targets statiques whitelistés de `ksp-logging-lib`.
|
||||
- le bridge de logging frontend KSP `frontend_log.ts`, routé par `emit_frontend_log` vers des targets statiques whitelistés de `ksp-logging-lib` ;
|
||||
- le lifecycle configurable `splash -> main` avec navigation monofenêtre et instrumentation `debug`/`trace` des interactions frontend.
|
||||
|
||||
## Layout frontend
|
||||
|
||||
@@ -46,6 +47,7 @@ frontend/
|
||||
│ └── splash.scss
|
||||
├── ts/
|
||||
│ ├── frontend_log.ts
|
||||
│ ├── invoke.ts
|
||||
│ ├── main.ts
|
||||
│ └── splash.ts
|
||||
├── main.html
|
||||
@@ -110,6 +112,23 @@ Si le document Logging, sa résolution environnementale ou l'initialisation du r
|
||||
|
||||
Le `LoggingGuard` est conservé dans `AppState` pour préparer le hot reload ultérieur.
|
||||
|
||||
## Lifecycle des fenêtres
|
||||
|
||||
`tw_splash.rs` valide l'origine de `splash_frontend_ready`, ignore les readiness dupliquées et orchestre le passage à `main`. Les timings sont résolus par `ksp-config-lib` depuis :
|
||||
|
||||
```text
|
||||
KSP_DESK_SPLASH_MINIMUM_MS=1200
|
||||
KSP_DESK_SPLASH_FADE_MS=300
|
||||
```
|
||||
|
||||
Après la durée minimale, le splash reçoit son fade-out, `tw_main.rs` affiche/focalise `main`, puis le splash est détruit. Une valeur splash invalide ne bloque pas le manager : des timings de fallback sûrs restent en mémoire afin que la future surface `.env` puisse permettre la réparation.
|
||||
|
||||
Le shell principal expose les cinq routes de référence `Vue d'ensemble`, `Documents`, `Profils`, `Environnement / .env` et `Logging`. Les routes non encore fonctionnelles affichent un placeholder sans réimplémenter les services prévus par les prereleases suivantes.
|
||||
|
||||
## Traçabilité frontend
|
||||
|
||||
Les actions utilisateur significatives et changements d'état sont journalisés en `debug`; les opérations plus fines (rendu/remplacement DOM, étapes IPC, animations et événements fréquents) en `trace`. Le wrapper `invoke.ts` trace début/fin des commandes sans journaliser leurs arguments, afin de ne pas créer ultérieurement de fuite de valeurs sensibles.
|
||||
|
||||
## Police du splash
|
||||
|
||||
Le splash utilise `DOS_Amazigh.ttf` (`font-family: Dos Amazigh`) pour son titre, via le `@font-face` déclaré dans `frontend/sass/splash.scss`. Le répertoire `frontend/fonts/` documente la provenance et l'empreinte de l'asset de référence. Le lifecycle final du splash reste traité dans la tranche dédiée `pre.008`.
|
||||
Le splash utilise `DOS_Amazigh.ttf` (`font-family: Dos Amazigh`) pour son titre, via le `@font-face` déclaré dans `frontend/sass/splash.scss`. Le répertoire `frontend/fonts/` documente la provenance et l'empreinte de l'asset de référence. Le lifecycle du splash est désormais actif et utilise cette police pendant la transition vers la fenêtre principale.
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<!-- file: crates/ksp-app-config-desk/TODO.md -->
|
||||
<!-- version: 5 -->
|
||||
<!-- version: 6 -->
|
||||
|
||||
# TODO `ksp-app-config-desk`
|
||||
|
||||
Le gabarit frontend, le bootstrap Config/Logging, `AppState`, le `LoggingGuard` durable, les premiers DTO TS-RS et le bridge frontend logging KSP sont maintenant présents.
|
||||
Le gabarit frontend, le bootstrap Config/Logging, `AppState`, le `LoggingGuard` durable, les premiers DTO TS-RS, le bridge frontend logging KSP et le lifecycle `splash -> main` sont maintenant présents.
|
||||
|
||||
Les prochaines tranches doivent encore :
|
||||
|
||||
- finaliser le lifecycle splash configurable sans temporisation environnementale codée en dur ;
|
||||
- développer les panneaux Documents, Profils, Environnement/`.env`, Secrets et Logging ;
|
||||
- introduire DataTables au premier tableau qui exige tri, filtrage, sélection ou checkbox ;
|
||||
- démontrer le hot reload Logging et le panneau de test de routage.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-config-desk/USAGE.md -->
|
||||
<!-- version: 6 -->
|
||||
<!-- version: 7 -->
|
||||
|
||||
# Utilisation de `ksp-app-config-desk`
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
Le gabarit Rust/Tauri et le frontend Vite/TypeScript/SCSS sont présents. Le backend initialise désormais Config, le runtime Logging et `AppState`; les panneaux métier Config restent volontairement absents à ce stade.
|
||||
|
||||
La fenêtre `splash` est la fenêtre visible au démarrage. La fenêtre `main` est déjà construite et bundlée, mais son lifecycle d'affichage est finalisé dans la tranche splash/shell dédiée afin de ne pas introduire une temporisation codée en dur.
|
||||
La fenêtre `splash` est visible au démarrage. Après readiness du frontend et temporisation résolue par Config, l'application effectue le fade-out, affiche/focalise `main` puis détruit `splash`. La fenêtre principale expose immédiatement la navigation monofenêtre de référence.
|
||||
|
||||
## Première installation des dépendances frontend
|
||||
|
||||
@@ -106,6 +106,33 @@ Si cette configuration ne peut pas être utilisée, l'application doit rester d
|
||||
|
||||
La commande Tauri `get_app_snapshot` expose un état sûr du bootstrap. Ses types TypeScript sont générés par `cargo test -p ksp-app-config-desk` sous `frontend/ts/bindings/`.
|
||||
|
||||
## Lifecycle et timings du splash
|
||||
|
||||
Les variables communes sont :
|
||||
|
||||
```text
|
||||
KSP_DESK_SPLASH_MINIMUM_MS=1200
|
||||
KSP_DESK_SPLASH_FADE_MS=300
|
||||
```
|
||||
|
||||
La priorité est celle de Config : process > `.env` > fallback. Les valeurs sont lues uniquement via `ksp-config-lib`. Une valeur non numérique ou hors borne déclenche un fallback runtime en mémoire plutôt qu'un refus de démarrer Config Desk.
|
||||
|
||||
Le frontend `splash.ts` installe son listener puis appelle `splash_frontend_ready`. Le backend vérifie que l'appel provient réellement de la WebView `splash`; une readiness dupliquée (par exemple après reload Vite) est ignorée.
|
||||
|
||||
## Navigation principale
|
||||
|
||||
La barre principale contient :
|
||||
|
||||
```text
|
||||
Vue d'ensemble
|
||||
Documents
|
||||
Profils
|
||||
Environnement / .env
|
||||
Logging
|
||||
```
|
||||
|
||||
Seule la vue d'ensemble consomme déjà `get_app_snapshot`; les autres routes restent des placeholders jusqu'à leurs tranches fonctionnelles. Chaque clic de navigation est tracé en `debug` et chaque remplacement/rendu de section en `trace`. Les appels Tauri partagés utilisent `frontend/ts/invoke.ts`, qui journalise le début et la fin d'une commande sans journaliser ses arguments.
|
||||
|
||||
## Asset font du splash
|
||||
|
||||
La police bot3 de référence est `DOS_Amazigh.ttf`. Vérifier l'asset local contre l'empreinte documentée dans `frontend/fonts/README.md` ; `frontend/sass/splash.scss` l'applique déjà au titre du splash. La tranche `pre.008` reste consacrée au lifecycle et à la finition visuelle.
|
||||
La police bot3 de référence est `DOS_Amazigh.ttf`. Vérifier l'asset local contre l'empreinte documentée dans `frontend/fonts/README.md` ; `frontend/sass/splash.scss` l'applique au titre du splash.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-config-desk/frontend/main.html -->
|
||||
<!-- version: 1 -->
|
||||
<!-- version: 2 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -13,12 +13,19 @@
|
||||
|
||||
<body>
|
||||
<header class="app-header">
|
||||
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
||||
<div class="container-fluid px-4">
|
||||
<div class="navbar-brand d-flex align-items-center">
|
||||
<nav class="navbar h-100 py-0 bg-light text-dark">
|
||||
<div class="container-fluid px-4 flex-nowrap">
|
||||
<div class="navbar-brand d-flex align-items-center me-4">
|
||||
<img alt="KSP" src="imgs/logo.png" class="app-logo">
|
||||
<span class="ps-2 fs-4 fw-bold text-primary">KSP — <span class="badge text-bg-primary">Config Desk</span></span>
|
||||
</div>
|
||||
<div class="app-nav nav nav-pills flex-nowrap gap-1" aria-label="Navigation Config Desk">
|
||||
<button class="nav-link app-nav-button active" type="button" data-view="overview">Vue d'ensemble</button>
|
||||
<button class="nav-link app-nav-button" type="button" data-view="documents">Documents</button>
|
||||
<button class="nav-link app-nav-button" type="button" data-view="profiles">Profils</button>
|
||||
<button class="nav-link app-nav-button" type="button" data-view="environment">Environnement / .env</button>
|
||||
<button class="nav-link app-nav-button" type="button" data-view="logging">Logging</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
@@ -28,10 +35,30 @@
|
||||
<div class="container-fluid px-4 py-4">
|
||||
<div class="card app-shell-card mx-auto shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<h1 class="h3 mb-3">Configuration KSP</h1>
|
||||
<p class="text-body-secondary mb-4">
|
||||
Le shell desktop est prêt. Les panneaux Documents, Profils, Environnement et Logging seront ajoutés dans les prereleases suivantes.
|
||||
</p>
|
||||
<h1 id="viewTitle" class="h3 mb-2">Vue d'ensemble</h1>
|
||||
<p id="viewDescription" class="text-body-secondary mb-4">État sûr du bootstrap Config Desk et du runtime Logging actuellement actif.</p>
|
||||
|
||||
<section id="overviewPanel" aria-label="Vue d'ensemble">
|
||||
<dl class="row mb-4 app-overview-list">
|
||||
<dt class="col-sm-5">Version application</dt>
|
||||
<dd id="overviewVersion" class="col-sm-7 text-sm-end">—</dd>
|
||||
<dt class="col-sm-5">Descripteurs Config enregistrés</dt>
|
||||
<dd id="overviewDocuments" class="col-sm-7 text-sm-end">—</dd>
|
||||
<dt class="col-sm-5">Profil Logging actif</dt>
|
||||
<dd id="overviewLoggingProfile" class="col-sm-7 text-sm-end">—</dd>
|
||||
<dt class="col-sm-5">Génération Logging</dt>
|
||||
<dd id="overviewLoggingGeneration" class="col-sm-7 text-sm-end">—</dd>
|
||||
<dt class="col-sm-5 mb-0">Fallback Logging actif</dt>
|
||||
<dd id="overviewLoggingFallback" class="col-sm-7 text-sm-end mb-0">—</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section id="placeholderPanel" hidden aria-live="polite">
|
||||
<div class="alert alert-secondary mb-4" role="status">
|
||||
Cette route du shell est prête ; son contenu fonctionnel sera ajouté dans la prerelease prévue par le plan 0.1.4.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="alert alert-primary mb-0" role="status">
|
||||
<i class="fa-solid fa-desktop me-2" aria-hidden="true"></i>
|
||||
<span id="shellStatus" class="app-shell-status">Initialisation du frontend...</span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/frontend/sass/_app.scss
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
$app-header-height: 72px;
|
||||
$app-footer-height: 42px;
|
||||
@@ -58,3 +58,17 @@ body {
|
||||
.app-shell-status {
|
||||
font-family: var(--bs-font-monospace);
|
||||
}
|
||||
|
||||
.app-nav {
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.app-nav-button {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-overview-list dd {
|
||||
font-family: var(--bs-font-monospace);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/frontend/sass/splash.scss
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
@font-face {
|
||||
font-family: "Dos Amazigh";
|
||||
@@ -26,7 +26,7 @@ body {
|
||||
position: relative;
|
||||
width: 960px;
|
||||
height: 637px;
|
||||
opacity: 1;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#splash-image {
|
||||
@@ -62,4 +62,4 @@ body {
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
font-family: monospace;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
}
|
||||
|
||||
19
crates/ksp-app-config-desk/frontend/ts/invoke.ts
Normal file
19
crates/ksp-app-config-desk/frontend/ts/invoke.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// file: crates/ksp-app-config-desk/frontend/ts/invoke.ts
|
||||
// version: 1
|
||||
|
||||
//! Shared Tauri invoke wrapper with bounded frontend lifecycle tracing.
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { frontendDebug, frontendError, frontendTrace, type FrontendLogTargetId } from "./frontend_log";
|
||||
|
||||
export async function invokeKsp<T>(targetId: FrontendLogTargetId, command: string, args?: Record<string, unknown>): Promise<T> {
|
||||
frontendDebug(targetId, "Frontend IPC command requested", { command });
|
||||
try {
|
||||
const result = await invoke<T>(command, args);
|
||||
frontendTrace(targetId, "Frontend IPC command completed", { command });
|
||||
return result;
|
||||
} catch (caughtError) {
|
||||
frontendError(targetId, "Frontend IPC command failed", { command });
|
||||
throw caughtError;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,140 @@
|
||||
// file: crates/ksp-app-config-desk/frontend/ts/main.ts
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
import "bootstrap";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import "simplebar";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { frontendInfo, installFrontendConsoleBridge } from "./frontend_log";
|
||||
import type { AppSnapshotDto } from "./bindings/ksp_app_config_desk/dto_common/AppSnapshotDto.ts";
|
||||
import { frontendDebug, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log";
|
||||
import { invokeKsp } from "./invoke";
|
||||
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
installFrontendConsoleBridge("main");
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const windowLabel = getCurrentWindow().label;
|
||||
const status = document.querySelector<HTMLElement>("#shellStatus");
|
||||
if (status) {
|
||||
status.textContent = `Frontend chargé dans la fenêtre '${windowLabel}'.`;
|
||||
type ViewId = "overview" | "documents" | "profiles" | "environment" | "logging";
|
||||
|
||||
const viewCopy: Record<ViewId, { title: string; description: string }> = {
|
||||
overview: {
|
||||
title: "Vue d'ensemble",
|
||||
description: "État sûr du bootstrap Config Desk et du runtime Logging actuellement actif.",
|
||||
},
|
||||
documents: {
|
||||
title: "Documents",
|
||||
description: "Inventaire, diagnostics et réparation des documents Config — surface fonctionnelle prévue en pre.009.",
|
||||
},
|
||||
profiles: {
|
||||
title: "Profils",
|
||||
description: "Sélection et provenance des profils Config — surface fonctionnelle prévue en pre.010.",
|
||||
},
|
||||
environment: {
|
||||
title: "Environnement / .env",
|
||||
description: "Rapports et management des variables KSP/KSPB — surfaces fonctionnelles prévues en pre.011/pre.012.",
|
||||
},
|
||||
logging: {
|
||||
title: "Logging",
|
||||
description: "Édition des profils et hot reload du runtime Logging — surfaces fonctionnelles prévues en pre.014 à pre.017.",
|
||||
},
|
||||
};
|
||||
|
||||
function isViewId(value: string): value is ViewId {
|
||||
return Object.prototype.hasOwnProperty.call(viewCopy, value);
|
||||
}
|
||||
|
||||
function activateView(viewId: ViewId, source: "startup" | "user"): void {
|
||||
if (source === "user") {
|
||||
frontendDebug("main", "Main navigation activated", { viewId });
|
||||
}
|
||||
const copy = viewCopy[viewId];
|
||||
const title = document.querySelector<HTMLElement>("#viewTitle");
|
||||
const description = document.querySelector<HTMLElement>("#viewDescription");
|
||||
if (title) {
|
||||
title.textContent = copy.title;
|
||||
}
|
||||
if (description) {
|
||||
description.textContent = copy.description;
|
||||
}
|
||||
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
|
||||
const active = button.dataset.view === viewId;
|
||||
button.classList.toggle("active", active);
|
||||
button.setAttribute("aria-current", active ? "page" : "false");
|
||||
});
|
||||
const overview = document.querySelector<HTMLElement>("#overviewPanel");
|
||||
if (overview) {
|
||||
overview.hidden = viewId !== "overview";
|
||||
}
|
||||
const placeholder = document.querySelector<HTMLElement>("#placeholderPanel");
|
||||
if (placeholder) {
|
||||
placeholder.hidden = viewId === "overview";
|
||||
}
|
||||
frontendTrace("main", "Main view DOM updated", { viewId, source });
|
||||
}
|
||||
|
||||
function bindNavigation(): void {
|
||||
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
const requestedView = button.dataset.view;
|
||||
if (requestedView && isViewId(requestedView)) {
|
||||
activateView(requestedView, "user");
|
||||
}
|
||||
});
|
||||
});
|
||||
frontendTrace("main", "Main navigation handlers installed");
|
||||
}
|
||||
|
||||
function renderSnapshot(snapshot: AppSnapshotDto): void {
|
||||
const version = document.querySelector<HTMLElement>("#overviewVersion");
|
||||
const documents = document.querySelector<HTMLElement>("#overviewDocuments");
|
||||
const profile = document.querySelector<HTMLElement>("#overviewLoggingProfile");
|
||||
const generation = document.querySelector<HTMLElement>("#overviewLoggingGeneration");
|
||||
const fallback = document.querySelector<HTMLElement>("#overviewLoggingFallback");
|
||||
if (version) {
|
||||
version.textContent = snapshot.applicationVersion;
|
||||
}
|
||||
if (documents) {
|
||||
documents.textContent = snapshot.configDocumentCount.toString();
|
||||
}
|
||||
if (profile) {
|
||||
profile.textContent = snapshot.activeLoggingProfile ?? "fallback transitoire";
|
||||
}
|
||||
if (generation) {
|
||||
generation.textContent = snapshot.loggingGeneration.toString();
|
||||
}
|
||||
if (fallback) {
|
||||
fallback.textContent = snapshot.fallbackLoggingActive ? "oui" : "non";
|
||||
}
|
||||
frontendTrace("main", "Application snapshot rendered", {
|
||||
loggingGeneration: snapshot.loggingGeneration,
|
||||
fallbackLoggingActive: snapshot.fallbackLoggingActive,
|
||||
});
|
||||
}
|
||||
|
||||
async function loadSnapshot(): Promise<void> {
|
||||
const status = document.querySelector<HTMLElement>("#shellStatus");
|
||||
const snapshot = await invokeKsp<AppSnapshotDto>("main", "get_app_snapshot");
|
||||
renderSnapshot(snapshot);
|
||||
if (status) {
|
||||
status.textContent = "Shell principal prêt.";
|
||||
}
|
||||
frontendTrace("main", "Main shell status replaced", { status: "ready" });
|
||||
}
|
||||
|
||||
async function initializeMain(): Promise<void> {
|
||||
const windowLabel = getCurrentWindow().label;
|
||||
frontendInfo("main", "Config Desk main frontend loaded", { windowLabel });
|
||||
bindNavigation();
|
||||
activateView("overview", "startup");
|
||||
try {
|
||||
await loadSnapshot();
|
||||
} catch {
|
||||
const status = document.querySelector<HTMLElement>("#shellStatus");
|
||||
if (status) {
|
||||
status.textContent = "Le snapshot initial n'a pas pu être chargé.";
|
||||
}
|
||||
frontendTrace("main", "Main shell status replaced", { status: "snapshot_error" });
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
void initializeMain();
|
||||
});
|
||||
|
||||
@@ -1,16 +1,113 @@
|
||||
// file: crates/ksp-app-config-desk/frontend/ts/splash.ts
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { frontendInfo, installFrontendConsoleBridge } from "./frontend_log";
|
||||
import type { SplashOrderDto } from "./bindings/ksp_app_config_desk/splash/SplashOrderDto.ts";
|
||||
import { frontendDebug, frontendError, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log";
|
||||
import { invokeKsp } from "./invoke";
|
||||
|
||||
installFrontendConsoleBridge("splash");
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const windowLabel = getCurrentWindow().label;
|
||||
const defaultFadeDurationMs = 300;
|
||||
let activeOpacityFrame: number | null = null;
|
||||
|
||||
function normalizeDurationMs(value: number | null | undefined): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
||||
return defaultFadeDurationMs;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function easeInOut(progress: number): number {
|
||||
if (progress < 0.5) {
|
||||
return 2 * progress * progress;
|
||||
}
|
||||
return 1 - Math.pow(-2 * progress + 2, 2) / 2;
|
||||
}
|
||||
|
||||
async function animateOpacity(element: HTMLElement, fromOpacity: number, toOpacity: number, durationMs: number): Promise<void> {
|
||||
frontendTrace("splash", "Splash opacity animation started", { fromOpacity, toOpacity, durationMs });
|
||||
if (activeOpacityFrame !== null) {
|
||||
cancelAnimationFrame(activeOpacityFrame);
|
||||
activeOpacityFrame = null;
|
||||
}
|
||||
element.style.opacity = fromOpacity.toString();
|
||||
element.style.willChange = "opacity";
|
||||
await new Promise<void>(resolve => requestAnimationFrame(() => resolve()));
|
||||
await new Promise<void>(resolve => {
|
||||
const startedAt = performance.now();
|
||||
const opacityDelta = toOpacity - fromOpacity;
|
||||
const updateOpacity = (currentTime: number): void => {
|
||||
const elapsedMs = currentTime - startedAt;
|
||||
const rawProgress = Math.min(elapsedMs / durationMs, 1);
|
||||
element.style.opacity = (fromOpacity + opacityDelta * easeInOut(rawProgress)).toString();
|
||||
if (rawProgress >= 1) {
|
||||
element.style.opacity = toOpacity.toString();
|
||||
element.style.willChange = "auto";
|
||||
activeOpacityFrame = null;
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
activeOpacityFrame = requestAnimationFrame(updateOpacity);
|
||||
};
|
||||
activeOpacityFrame = requestAnimationFrame(updateOpacity);
|
||||
});
|
||||
frontendTrace("splash", "Splash opacity animation completed", { toOpacity, durationMs });
|
||||
}
|
||||
|
||||
function replaceStatus(message: string | null): void {
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
const status = document.querySelector<HTMLElement>("#splash-status");
|
||||
if (status) {
|
||||
status.textContent = `Frontend chargé dans la fenêtre '${windowLabel}'. Lifecycle complet prévu en pre.008.`;
|
||||
status.textContent = message;
|
||||
frontendTrace("splash", "Splash status replaced", { message });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSplashOrder(order: SplashOrderDto): Promise<void> {
|
||||
frontendTrace("splash", "Splash order received", { action: order.action });
|
||||
const container = document.querySelector<HTMLElement>("#splash-container");
|
||||
replaceStatus(order.message);
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
if (order.action === "fade_in") {
|
||||
await animateOpacity(container, 0, 1, normalizeDurationMs(order.durationMs));
|
||||
return;
|
||||
}
|
||||
if (order.action === "fade_out") {
|
||||
await animateOpacity(container, 1, 0, normalizeDurationMs(order.durationMs));
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeSplash(): Promise<void> {
|
||||
const windowLabel = getCurrentWindow().label;
|
||||
frontendInfo("splash", "Config Desk splash frontend loaded", { windowLabel });
|
||||
const container = document.querySelector<HTMLElement>("#splash-container");
|
||||
if (container) {
|
||||
container.style.opacity = "0";
|
||||
container.style.willChange = "opacity";
|
||||
frontendTrace("splash", "Splash container prepared for managed fade-in");
|
||||
}
|
||||
await listen<SplashOrderDto>("ksp-splash-order", event => {
|
||||
void handleSplashOrder(event.payload);
|
||||
});
|
||||
frontendDebug("splash", "Splash lifecycle listener installed");
|
||||
try {
|
||||
await invokeKsp<void>("splash", "splash_frontend_ready");
|
||||
} catch {
|
||||
replaceStatus("Le lifecycle du splash n'a pas pu démarrer.");
|
||||
if (container) {
|
||||
container.style.opacity = "1";
|
||||
container.style.willChange = "auto";
|
||||
}
|
||||
frontendError("splash", "Splash readiness command failed");
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
void initializeSplash();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/src/app_state.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Shared backend state owned by the Tauri application.
|
||||
|
||||
@@ -15,6 +15,8 @@ struct LoggingRuntimeState {
|
||||
pub(crate) struct AppState {
|
||||
config_management: ksp_config_lib::ConfigManagement,
|
||||
logging_runtime: std::sync::Mutex<LoggingRuntimeState>,
|
||||
splash_settings: crate::SplashSettings,
|
||||
splash_sequence_started: std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -30,6 +32,27 @@ impl AppState {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let splash_settings = crate::SplashSettings::load();
|
||||
let splash_settings = match splash_settings {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
error_domain = error.code().domain(),
|
||||
error_code = error.code().code(),
|
||||
"managed splash timings are invalid; using transient in-memory defaults"
|
||||
);
|
||||
crate::SplashSettings::fallback()
|
||||
},
|
||||
};
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
minimum_ms = splash_settings.minimum_ms(),
|
||||
fade_ms = splash_settings.fade_ms(),
|
||||
"resolved Config Desk splash timings"
|
||||
);
|
||||
return std::result::Result::Ok(Self {
|
||||
config_management,
|
||||
logging_runtime: std::sync::Mutex::new(LoggingRuntimeState {
|
||||
@@ -39,6 +62,8 @@ impl AppState {
|
||||
fallback_active: logging_startup.fallback_active,
|
||||
startup_diagnostic: logging_startup.startup_diagnostic,
|
||||
}),
|
||||
splash_settings,
|
||||
splash_sequence_started: std::sync::atomic::AtomicBool::new(false),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,4 +100,18 @@ impl AppState {
|
||||
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the resolved splash timings captured during application bootstrap.
|
||||
#[must_use]
|
||||
pub(crate) const fn splash_settings(&self) -> crate::SplashSettings {
|
||||
return self.splash_settings;
|
||||
}
|
||||
|
||||
/// Marks the one-shot splash lifecycle as started and reports whether this caller won the transition.
|
||||
pub(crate) fn begin_splash_sequence(&self) -> bool {
|
||||
return self
|
||||
.splash_sequence_started
|
||||
.compare_exchange(false, true, std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire)
|
||||
.is_ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/src/constants.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Application-owned tracing targets and domains.
|
||||
|
||||
@@ -15,3 +15,5 @@ pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-config-desk.fro
|
||||
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "config.bootstrap";
|
||||
/// Structured domain used by technical frontend events.
|
||||
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
|
||||
/// Structured domain used by Tauri window lifecycle operations.
|
||||
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/src/errors.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Application-local error codes for the configuration desktop shell.
|
||||
|
||||
@@ -15,3 +15,12 @@ pub(crate) const ERROR_CODE_APP_STATE_LOCK_FAILED: ksp_core_lib::ErrorCode = ksp
|
||||
pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_level_invalid");
|
||||
/// Frontend logging requested a target outside the application whitelist.
|
||||
pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_target_invalid");
|
||||
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
|
||||
pub(crate) const ERROR_CODE_SPLASH_SETTING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "splash_setting_invalid");
|
||||
/// Splash readiness was invoked from a window other than the splash window.
|
||||
pub(crate) const ERROR_CODE_SPLASH_ORIGIN_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "splash_origin_invalid");
|
||||
/// A required Tauri window is missing from the configured application runtime.
|
||||
pub(crate) const ERROR_CODE_TAURI_WINDOW_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "tauri_window_missing");
|
||||
/// A Tauri window show/focus/destroy/event operation failed.
|
||||
pub(crate) const ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("config_desk", "tauri_window_operation_failed");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/src/lib.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Tauri desktop application for managing and validating KSP configuration.
|
||||
|
||||
@@ -13,7 +13,10 @@ mod constants;
|
||||
mod dto_common;
|
||||
mod errors;
|
||||
mod frontend_logging;
|
||||
mod splash;
|
||||
mod tauri;
|
||||
mod tw_main;
|
||||
mod tw_splash;
|
||||
|
||||
/// Runs the KSP configuration desktop application.
|
||||
pub use self::tauri::run;
|
||||
@@ -23,6 +26,7 @@ pub(crate) use self::bootstrap::config_management;
|
||||
pub(crate) use self::bootstrap::initialize_logging;
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
|
||||
@@ -34,6 +38,16 @@ pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
|
||||
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
|
||||
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
|
||||
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
|
||||
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
|
||||
pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID;
|
||||
pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED;
|
||||
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING;
|
||||
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED;
|
||||
pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
|
||||
pub(crate) use self::frontend_logging::emit_frontend_log_event;
|
||||
pub(crate) use self::splash::SplashOrderDto;
|
||||
pub(crate) use self::splash::SplashSettings;
|
||||
pub(crate) use self::tw_main::WINDOW_LABEL_MAIN;
|
||||
pub(crate) use self::tw_main::show_and_focus as show_main_window;
|
||||
pub(crate) use self::tw_splash::WINDOW_LABEL_SPLASH;
|
||||
pub(crate) use self::tw_splash::frontend_ready as splash_frontend_ready_service;
|
||||
|
||||
138
crates/ksp-app-config-desk/src/splash.rs
Normal file
138
crates/ksp-app-config-desk/src/splash.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
// file: crates/ksp-app-config-desk/src/splash.rs
|
||||
// version: 1
|
||||
|
||||
//! Common splash settings and frontend event contracts for Config Desk.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
const ENV_SPLASH_MINIMUM_MS: &str = "KSP_DESK_SPLASH_MINIMUM_MS";
|
||||
const ENV_SPLASH_FADE_MS: &str = "KSP_DESK_SPLASH_FADE_MS";
|
||||
const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200;
|
||||
const DEFAULT_SPLASH_FADE_MS: u32 = 300;
|
||||
const MAX_SPLASH_MINIMUM_MS: u64 = 60_000;
|
||||
const MAX_SPLASH_FADE_MS: u32 = 10_000;
|
||||
|
||||
/// Runtime timings used by the common desk splash lifecycle.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct SplashSettings {
|
||||
minimum_ms: u64,
|
||||
fade_ms: u32,
|
||||
}
|
||||
|
||||
impl SplashSettings {
|
||||
/// Resolves splash timings through the Config-owned environment snapshot.
|
||||
pub(crate) fn load() -> ksp_core_lib::Result<Self> {
|
||||
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||
let environment = match environment {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let minimum = environment.resolve_variable(ENV_SPLASH_MINIMUM_MS, std::option::Option::Some("1200"));
|
||||
let minimum = match minimum {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let fade = environment.resolve_variable(ENV_SPLASH_FADE_MS, std::option::Option::Some("300"));
|
||||
let fade = match fade {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let minimum_ms = parse_u64_setting(ENV_SPLASH_MINIMUM_MS, minimum.value(), MAX_SPLASH_MINIMUM_MS);
|
||||
let minimum_ms = match minimum_ms {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let fade_ms = parse_u32_setting(ENV_SPLASH_FADE_MS, fade.value(), MAX_SPLASH_FADE_MS);
|
||||
let fade_ms = match fade_ms {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(Self { minimum_ms, fade_ms });
|
||||
}
|
||||
|
||||
/// Returns safe in-memory timings used when the managed environment cannot be resolved.
|
||||
#[must_use]
|
||||
pub(crate) const fn fallback() -> Self {
|
||||
return Self { minimum_ms: DEFAULT_SPLASH_MINIMUM_MS, fade_ms: DEFAULT_SPLASH_FADE_MS };
|
||||
}
|
||||
|
||||
/// Returns the minimum visible duration after splash frontend readiness.
|
||||
#[must_use]
|
||||
pub(crate) const fn minimum_ms(self) -> u64 {
|
||||
return self.minimum_ms;
|
||||
}
|
||||
|
||||
/// Returns the fade duration used for both fade-in and fade-out.
|
||||
#[must_use]
|
||||
pub(crate) const fn fade_ms(self) -> u32 {
|
||||
return self.fade_ms;
|
||||
}
|
||||
}
|
||||
|
||||
/// Command emitted by Rust to the splash frontend.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/splash/SplashOrderDto.ts")]
|
||||
pub(crate) struct SplashOrderDto {
|
||||
/// Splash action name.
|
||||
pub(crate) action: String,
|
||||
/// Optional status text.
|
||||
pub(crate) message: std::option::Option<String>,
|
||||
/// Optional animation duration in milliseconds.
|
||||
pub(crate) duration_ms: std::option::Option<u32>,
|
||||
}
|
||||
|
||||
impl SplashOrderDto {
|
||||
#[must_use]
|
||||
pub(crate) fn new(action: &str, message: std::option::Option<&str>, duration_ms: std::option::Option<u32>) -> Self {
|
||||
return Self { action: action.to_owned(), message: message.map(std::string::ToString::to_string), duration_ms };
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_u64_setting(variable_name: &str, value: &str, maximum: u64) -> ksp_core_lib::Result<u64> {
|
||||
let parsed = value.parse::<u64>();
|
||||
let parsed = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration is not a valid unsigned integer")
|
||||
.with_context("variable_name", variable_name)
|
||||
.with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
if parsed > maximum {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration exceeds the allowed bound")
|
||||
.with_context("variable_name", variable_name)
|
||||
.with_context("maximum_ms", maximum),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(parsed);
|
||||
}
|
||||
|
||||
fn parse_u32_setting(variable_name: &str, value: &str, maximum: u32) -> ksp_core_lib::Result<u32> {
|
||||
let parsed = value.parse::<u32>();
|
||||
let parsed = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration is not a valid unsigned integer")
|
||||
.with_context("variable_name", variable_name)
|
||||
.with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
if parsed > maximum {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration exceeds the allowed bound")
|
||||
.with_context("variable_name", variable_name)
|
||||
.with_context("maximum_ms", maximum),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(parsed);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/splash.rs"]
|
||||
mod tests;
|
||||
@@ -1,10 +1,8 @@
|
||||
// file: crates/ksp-app-config-desk/src/tauri.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! Tauri runtime assembly for the KSP configuration desktop application.
|
||||
|
||||
use tauri::Manager; // rust-rules: trait-import
|
||||
|
||||
/// Runs the configuration desktop application.
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
|
||||
@@ -38,16 +36,18 @@ fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<taur
|
||||
}
|
||||
|
||||
fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
return builder.invoke_handler(tauri::generate_handler![get_app_snapshot, emit_frontend_log]);
|
||||
return builder.invoke_handler(tauri::generate_handler![get_app_snapshot, emit_frontend_log, splash_frontend_ready]);
|
||||
}
|
||||
|
||||
fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
return builder.setup(|app| {
|
||||
if app.get_webview_window("splash").is_none() {
|
||||
return std::result::Result::Err(std::boxed::Box::new(std::io::Error::new(std::io::ErrorKind::NotFound, "splash window is missing")));
|
||||
let splash = crate::tw_splash::require_window(app);
|
||||
if let std::result::Result::Err(error) = splash {
|
||||
return std::result::Result::Err(std::boxed::Box::new(error));
|
||||
}
|
||||
if app.get_webview_window("main").is_none() {
|
||||
return std::result::Result::Err(std::boxed::Box::new(std::io::Error::new(std::io::ErrorKind::NotFound, "main window is missing")));
|
||||
let main = crate::tw_main::require_window(app);
|
||||
if let std::result::Result::Err(error) = main {
|
||||
return std::result::Result::Err(std::boxed::Box::new(error));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
});
|
||||
@@ -70,3 +70,16 @@ fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Resu
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn splash_frontend_ready(
|
||||
app: tauri::AppHandle,
|
||||
webview_window: tauri::WebviewWindow,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<(), crate::CommandErrorDto> {
|
||||
let result = crate::splash_frontend_ready_service(app, webview_window, &state).await;
|
||||
return match result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
};
|
||||
}
|
||||
|
||||
50
crates/ksp-app-config-desk/src/tw_main.rs
Normal file
50
crates/ksp-app-config-desk/src/tw_main.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
// file: crates/ksp-app-config-desk/src/tw_main.rs
|
||||
// version: 1
|
||||
|
||||
//! Tauri-window helpers for the Config Desk main window.
|
||||
|
||||
use tauri::Manager; // rust-rules: trait-import
|
||||
|
||||
pub(crate) const WINDOW_LABEL_MAIN: &str = "main";
|
||||
|
||||
pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
|
||||
let window = manager.get_webview_window(WINDOW_LABEL_MAIN);
|
||||
return match window {
|
||||
std::option::Option::Some(value) => std::result::Result::Ok(value),
|
||||
std::option::Option::None => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_MISSING, "Config Desk main window is missing")
|
||||
.with_context("window_label", WINDOW_LABEL_MAIN),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn show_and_focus(app: &tauri::AppHandle) -> ksp_core_lib::Result<()> {
|
||||
let window = require_window(app);
|
||||
let window = match window {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let shown = window.show();
|
||||
if let std::result::Result::Err(error) = shown {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot show Config Desk main window")
|
||||
.with_context("window_label", WINDOW_LABEL_MAIN)
|
||||
.with_source(error),
|
||||
);
|
||||
}
|
||||
let focused = window.set_focus();
|
||||
if let std::result::Result::Err(error) = focused {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot focus Config Desk main window")
|
||||
.with_context("window_label", WINDOW_LABEL_MAIN)
|
||||
.with_source(error),
|
||||
);
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
window_label = WINDOW_LABEL_MAIN,
|
||||
"main window shown and focused"
|
||||
);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
105
crates/ksp-app-config-desk/src/tw_splash.rs
Normal file
105
crates/ksp-app-config-desk/src/tw_splash.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
// file: crates/ksp-app-config-desk/src/tw_splash.rs
|
||||
// version: 1
|
||||
|
||||
//! Tauri-window lifecycle for the Config Desk splash window.
|
||||
|
||||
use tauri::Emitter; // rust-rules: trait-import
|
||||
use tauri::Manager; // rust-rules: trait-import
|
||||
|
||||
pub(crate) const WINDOW_LABEL_SPLASH: &str = "splash";
|
||||
const SPLASH_EVENT_NAME: &str = "ksp-splash-order";
|
||||
|
||||
pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
|
||||
let window = manager.get_webview_window(WINDOW_LABEL_SPLASH);
|
||||
return match window {
|
||||
std::option::Option::Some(value) => std::result::Result::Ok(value),
|
||||
std::option::Option::None => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_MISSING, "Config Desk splash window is missing")
|
||||
.with_context("window_label", WINDOW_LABEL_SPLASH),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) async fn frontend_ready(app: tauri::AppHandle, invoking_window: tauri::WebviewWindow, state: &crate::AppState) -> ksp_core_lib::Result<()> {
|
||||
if invoking_window.label() != WINDOW_LABEL_SPLASH {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_ORIGIN_INVALID, "Splash readiness may only originate from the splash window")
|
||||
.with_context("window_label", invoking_window.label()),
|
||||
);
|
||||
}
|
||||
if !state.begin_splash_sequence() {
|
||||
ksp_logging_lib::trace!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
window_label = WINDOW_LABEL_SPLASH,
|
||||
"duplicate splash frontend readiness ignored"
|
||||
);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
let settings = state.splash_settings();
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
minimum_ms = settings.minimum_ms(),
|
||||
fade_ms = settings.fade_ms(),
|
||||
"starting splash to main lifecycle"
|
||||
);
|
||||
let fade_in = emit_order(
|
||||
&invoking_window,
|
||||
crate::SplashOrderDto::new("fade_in", std::option::Option::Some("Initialisation de KSP Config Desk..."), std::option::Option::Some(settings.fade_ms())),
|
||||
);
|
||||
if let std::result::Result::Err(error) = fade_in {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(settings.minimum_ms())).await;
|
||||
let fade_out = emit_order(
|
||||
&invoking_window,
|
||||
crate::SplashOrderDto::new("fade_out", std::option::Option::Some("Initialisation terminée."), std::option::Option::Some(settings.fade_ms())),
|
||||
);
|
||||
if let std::result::Result::Err(error) = fade_out {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(u64::from(settings.fade_ms()))).await;
|
||||
let show_main = crate::show_main_window(&app);
|
||||
if let std::result::Result::Err(error) = show_main {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let destroyed = invoking_window.destroy();
|
||||
if let std::result::Result::Err(error) = destroyed {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot destroy Config Desk splash window")
|
||||
.with_context("window_label", WINDOW_LABEL_SPLASH)
|
||||
.with_source(error),
|
||||
);
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
window_label = WINDOW_LABEL_SPLASH,
|
||||
"splash window destroyed after main activation"
|
||||
);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn emit_order(window: &tauri::WebviewWindow, order: crate::SplashOrderDto) -> ksp_core_lib::Result<()> {
|
||||
let action = order.action.clone();
|
||||
let emitted = window.emit(SPLASH_EVENT_NAME, order);
|
||||
return match emitted {
|
||||
std::result::Result::Ok(()) => {
|
||||
ksp_logging_lib::trace!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
window_label = WINDOW_LABEL_SPLASH,
|
||||
action = action.as_str(),
|
||||
"splash order emitted"
|
||||
);
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot emit Config Desk splash order")
|
||||
.with_context("window_label", WINDOW_LABEL_SPLASH)
|
||||
.with_context("action", action)
|
||||
.with_source(error),
|
||||
),
|
||||
};
|
||||
}
|
||||
26
crates/ksp-app-config-desk/unit_tests/splash.rs
Normal file
26
crates/ksp-app-config-desk/unit_tests/splash.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
// file: crates/ksp-app-config-desk/unit_tests/splash.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn fallback_splash_settings_are_short_and_ordered() {
|
||||
let settings = crate::SplashSettings::fallback();
|
||||
assert_eq!(settings.minimum_ms(), 1200);
|
||||
assert_eq!(settings.fade_ms(), 300);
|
||||
assert!(settings.minimum_ms() >= u64::from(settings.fade_ms()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splash_order_contract_keeps_action_message_and_duration() {
|
||||
let order = crate::SplashOrderDto::new("fade_out", std::option::Option::Some("done"), std::option::Option::Some(300));
|
||||
assert_eq!(order.action, "fade_out");
|
||||
assert_eq!(order.message.as_deref(), std::option::Option::Some("done"));
|
||||
assert_eq!(order.duration_ms, std::option::Option::Some(300));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splash_duration_parser_rejects_invalid_or_unbounded_values() {
|
||||
let malformed = super::parse_u64_setting("KSP_DESK_SPLASH_MINIMUM_MS", "invalid", 60_000);
|
||||
let oversized = super::parse_u32_setting("KSP_DESK_SPLASH_FADE_MS", "10001", 10_000);
|
||||
assert!(malformed.is_err());
|
||||
assert!(oversized.is_err());
|
||||
}
|
||||
Reference in New Issue
Block a user