From 417536e4c46d85f0cd5aab2993ea5d3253ce15b9 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Sat, 25 Jul 2026 19:45:35 +0200 Subject: [PATCH] v0.1.0-pre.046 --- kb-app-demo-desktop/Cargo.toml | 4 + kb-app-demo-desktop/frontend/main.html | 70 +++- kb-app-demo-desktop/frontend/sass/_app.scss | 385 ++++++++++++++++++ .../frontend/sass/_bootswatch.scss | 160 ++++++++ .../frontend/sass/_fontawesome.scss | 19 + .../frontend/sass/_simplebar.scss | 248 +++++++++++ .../frontend/sass/_variables.scss | 95 +++++ kb-app-demo-desktop/frontend/sass/main.scss | 10 + kb-app-demo-desktop/frontend/sass/splash.scss | 71 ++-- .../frontend/ts/frontend_log.ts | 97 +++++ kb-app-demo-desktop/frontend/ts/main.ts | 38 ++ kb-app-demo-desktop/src/app_state.rs | 119 ++++++ kb-app-demo-desktop/src/frontend_log.rs | 72 ++++ kb-app-demo-desktop/src/lib.rs | 16 +- kb-app-demo-desktop/src/main_window.rs | 68 ++++ kb-app-demo-desktop/src/splash.rs | 13 +- kb-app-demo-desktop/src/tauri.rs | 63 ++- 17 files changed, 1491 insertions(+), 57 deletions(-) create mode 100644 kb-app-demo-desktop/frontend/sass/_app.scss create mode 100644 kb-app-demo-desktop/frontend/sass/_bootswatch.scss create mode 100644 kb-app-demo-desktop/frontend/sass/_fontawesome.scss create mode 100644 kb-app-demo-desktop/frontend/sass/_simplebar.scss create mode 100644 kb-app-demo-desktop/frontend/sass/_variables.scss create mode 100644 kb-app-demo-desktop/frontend/sass/main.scss create mode 100644 kb-app-demo-desktop/frontend/ts/frontend_log.ts create mode 100644 kb-app-demo-desktop/frontend/ts/main.ts create mode 100644 kb-app-demo-desktop/src/app_state.rs create mode 100644 kb-app-demo-desktop/src/frontend_log.rs create mode 100644 kb-app-demo-desktop/src/main_window.rs diff --git a/kb-app-demo-desktop/Cargo.toml b/kb-app-demo-desktop/Cargo.toml index 845b103..459dd50 100644 --- a/kb-app-demo-desktop/Cargo.toml +++ b/kb-app-demo-desktop/Cargo.toml @@ -22,10 +22,14 @@ tauri-build.workspace = true [dependencies] fs2.workspace = true +kb-config = { path = "../kb-config" } kb-core = { path = "../kb-core" } +kb-logging = { path = "../kb-logging" } serde.workspace = true serde_json.workspace = true +rustls.workspace = true tauri.workspace = true +tauri-plugin-tracing.workspace = true tokio.workspace = true tracing.workspace = true ts-rs.workspace = true diff --git a/kb-app-demo-desktop/frontend/main.html b/kb-app-demo-desktop/frontend/main.html index fd5d845..3c1e11f 100644 --- a/kb-app-demo-desktop/frontend/main.html +++ b/kb-app-demo-desktop/frontend/main.html @@ -1,18 +1,58 @@ - - + + - - - - Khadhroony Bot3 - - - -
-

Khadhroony Bot3

-

La fenêtre splash de kb-app-demo-desktop est migrée.

-

La fenêtre principale sera complétée dans la prochaine tranche.

-
- + + + + Khadhroony Bot3 + + + +
+ +
+
+
+
+
+
+
+
+

README du projet

+

+ Cette fenêtre d'accueil charge automatiquement le fichier README depuis le backend Rust. +

+
Chargement du README...
+
+
+
+
+
+
+
+ + + diff --git a/kb-app-demo-desktop/frontend/sass/_app.scss b/kb-app-demo-desktop/frontend/sass/_app.scss new file mode 100644 index 0000000..fc42eb2 --- /dev/null +++ b/kb-app-demo-desktop/frontend/sass/_app.scss @@ -0,0 +1,385 @@ +// file: kb_app_demo/frontend/sass/_app.scss +// version: 5 + +$app-header-height: 48px; +$app-footer-height: 48px; +$sidenav-base-width: 260px; + +html, +body { + height: 100%; + overflow: hidden; +} + +body { + min-height: 100vh; +} + +#ibackground { + position: fixed; + top: 0; + left: 0; + min-width: 100%; + min-height: 100%; + width: auto; + height: auto; + z-index: 1; + object-fit: cover; +} + +#vbackground { + position: fixed; + top: 0; + left: 0; + min-width: 100%; + min-height: 100%; + width: auto; + height: auto; + z-index: 2; + object-fit: cover; +} + +video#vbackground:not([playsinline]) { + display: none; +} + +.app-header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: $app-header-height; + z-index: 1030; + + & nav { + border-top: none !important; + border-left: none !important; + border-right: none !important; + border-bottom: 3px $primary solid !important; + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; + border-bottom-left-radius: 15px 150px; + border-bottom-right-radius: 75px 25px; + } + +} + +.app-footer { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: $app-footer-height; + z-index: 1020; + border-style: solid; + border-width: 2px 0 0; + border-top-left-radius: 255px 25px; + border-top-right-radius: 25px 225px; +} + +.app-main { + position: relative; + height: calc(100vh - $app-header-height - $app-footer-height); // plein viewport + margin-top: $app-header-height; + margin-bottom: $app-footer-height; + box-sizing: border-box; + overflow: hidden; // pas de scroll ici + z-index: 500; +} + +.app-main-error { + position: relative; + height: 100vh; // plein viewport + padding-top: $app-header-height; + padding-bottom: $app-footer-height; + box-sizing: border-box; + overflow: hidden; // pas de scroll ici + background: linear-gradient(135deg, #9945ff 0%, #764ba2 100%); + color: white; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + z-index: 501; +} + +.app-main-error-content { + width: 100%; + + & h1 { + font-size: 4rem; + margin: 0; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); + } + + & p { + font-size: 1.5rem; + margin: 1rem 0 2rem; + } +} + +.app-logo { + height: 42px; + width: auto; + display: block; +} + +#app-dashbord { + display: flex; + height: 100%; //height: calc(100vh - var(--app-header-height) - var(--app-footer-height)); +} + +#app-dashbord-sidenav { + flex-basis: $sidenav-base-width; + flex-shrink: 0; + transition: transform 0.15s ease-in-out; + + &[dir="ltr"] { + transform: translateX(-$sidenav-base-width); + } + + &[dir="rtl"] { + transform: translateX($sidenav-base-width); + } +} + + +#app-dashbord-main { + position: relative; + display: flex; + flex-direction: column; + min-width: 0; + flex-grow: 1; + min-height: 100%; + + &[dir="ltr"] { + margin-left: -$sidenav-base-width; + } + + &[dir="rtl"] { + margin-right: -$sidenav-base-width; + } +} + + +// quand body a la classe .sidenav-toggled +.sidenav-toggled { + #app-dashbord-sidenav { + transform: translateX(0); + } + + #app-dashbord-main { + &:before { + content: ""; + display: block; + position: absolute; + inset: 0; + background: #000; + opacity: 0.5; + transition: opacity 0.3s ease-in-out; + } + } +} + +// breakpoint desktop : sidebar visible par défaut +@include media-breakpoint-up(lg) { + #app-dashbord-sidenav { + transform: translateX(0); + } + + #app-dashbord-main { + margin-left: 0; + margin-right: 0; + transition: margin 0.15s ease-in-out; + } + + .sidenav-toggled { + #app-dashbord-sidenav { + &[dir="ltr"] { + transform: translateX(-$sidenav-base-width); + } + + &[dir="rtl"] { + transform: translateX($sidenav-base-width); + } + } + + #app-dashbord-main { + &[dir="ltr"] { + margin-left: -$sidenav-base-width; + } + + &[dir="rtl"] { + margin-right: -$sidenav-base-width; + } + + &:before { + display: none; + } + } + } +} + +// ------------------------------------------------------------------ +// Sidebar (style SBAdmin-ish) +// ------------------------------------------------------------------ + +.app-sidebar.sidenav { + display: flex; + flex-direction: column; + height: 100%; + flex-wrap: nowrap; + background-color: rgb(97, 53, 131, 0.7); + color: #fff; +} + +.sidenav-menu { + flex-grow: 1; + + .nav { + flex-direction: column; + flex-wrap: nowrap; + } + + .sidenav-menu-heading { + padding: 1.5rem 1rem 0.75rem; + font-size: 0.75rem; + font-weight: bold; + text-transform: uppercase; + color: rgba(255, 255, 255, 0.5); + } + + .nav-link { + display: flex; + align-items: center; + padding: 0.75rem 1.25rem; + color: rgba(255, 255, 255, 0.8); + position: relative; + + &:hover, + &.active { + color: #fff; + background: rgba(255, 255, 255, 0.08); + } + + .nav-link-icon { + font-size: 0.9rem; + margin-right: 0.5rem; + } + + .sidenav-collapse-arrow { + margin-left: auto; + transition: transform 0.15s ease; + } + + &.collapsed { + .sidenav-collapse-arrow { + transform: rotate(-90deg); + } + } + } + + .sidenav-menu-nested { + margin-left: 1.5rem; + flex-direction: column; + } +} + + + +.osb-scrollable { + height: 100%; // occupe toute la hauteur de .app-main + max-height: 100%; // évite de dépasser + // Pour vérifier sans OverlayScrollbars : + // +} + +.hero { + padding: 2rem; + background: white; +} + +.modal { + z-index: 1080; +} + +.modal-backdrop { + z-index: -1; +} + +.voice-output-textarea { + min-height: 320px; + resize: vertical; + font-size: 1.05rem; + line-height: 1.5; +} + +#logs-card { + transition: opacity 0.15s ease-in-out; +} + +.app-readme-box, +.app-config-box { + max-height: 60vh; + overflow: auto; + word-break: break-word; + border: 1px solid var(--bs-border-color); + border-radius: 0.5rem; + background: var(--bs-body-bg); + padding: 1rem; +} + +.app-config-box { + font-size: 0.875rem; + white-space: pre-wrap; +} + +.app-json-viewer { + white-space: normal; +} + +.app-json-viewer andypf-json-viewer { + display: block; + min-width: 100%; +} + +.app-markdown-body { + line-height: 1.6; + + & h1, + & h2, + & h3, + & h4, + & h5, + & h6 { + margin-top: 1.5rem; + margin-bottom: 0.75rem; + } + + & h1:first-child, + & h2:first-child, + & h3:first-child { + margin-top: 0; + } + + & pre { + padding: 0.75rem; + overflow: auto; + border: 1px solid var(--bs-border-color); + border-radius: 0.5rem; + background: var(--bs-tertiary-bg); + } + + & code { + color: var(--bs-code-color); + } +} +.app-json-box { + max-height: 420px; + overflow: auto; + padding: 1rem; + border-radius: 0.5rem; + background-color: rgba(0, 0, 0, 0.04); + white-space: pre-wrap; +} diff --git a/kb-app-demo-desktop/frontend/sass/_bootswatch.scss b/kb-app-demo-desktop/frontend/sass/_bootswatch.scss new file mode 100644 index 0000000..90c26e9 --- /dev/null +++ b/kb-app-demo-desktop/frontend/sass/_bootswatch.scss @@ -0,0 +1,160 @@ +// file: kb_demo_app/frontend/sass/_bootswatch.scss +// version: 1 + +// Pulse 5.3.8 +// Bootswatch + + +// Variables + +// Buttons + +.btn { + + &:focus, + &:active, + &:active:focus, + &.active:focus { + outline: none; + } + + &-secondary { + color: $gray-900; + background-color: $white; + border-color: #ccc; + + &:hover { + color: $gray-900; + background-color: $gray-300; + border-color: $gray-500; + } + + &.disabled { + color: tint-color($gray-900, 5%); + background-color: $white; + border-color: tint-color(#ccc, 5%); + } + } + + &-warning { + color: $white; + } + + &-primary:focus { + box-shadow: 0 0 5px tint-color($primary, 10%); + } + + &-secondary:focus { + box-shadow: 0 0 5px $gray-400; + } + + &-success:focus { + box-shadow: 0 0 5px tint-color($success, 10%); + } + + &-info:focus { + box-shadow: 0 0 5px tint-color($info, 10%); + } + + &-warning:focus { + box-shadow: 0 0 5px tint-color($warning, 10%); + } + + &-danger:focus { + box-shadow: 0 0 5px tint-color($danger, 10%); + } + + &.disabled:focus { + box-shadow: none; + } +} + +// Tables + +.table .thead-dark th { + background-color: $secondary; + border-color: $table-border-color; +} + +.table-primary, +.table-secondary, +.table-success, +.table-warning, +.table-danger, +.table-info, +.table-light { + --#{$prefix}table-color: #{$body-color}; +} + +// Forms + +.form-control:focus { + box-shadow: 0 0 5px rgba(100, 65, 164, .4); +} + +// Navs + +.nav-tabs { + + .nav-link, + .nav-link.active { + border-width: 0 0 1px; + } + + .nav-link:hover, + .nav-link.active, + .nav-link.active:hover, + .nav-link.active:focus { + border-bottom: 1px solid $primary; + } + + .nav-item+.nav-item { + margin-left: 0; + } +} + +.breadcrumb { + &-item.active { + color: $gray-700; + } +} + +// Indicators + +.badge { + &.bg-light { + color: $dark; + } +} + +// Progress bars + +.progress { + height: 8px; +} + +// Containers + +.list-group { + &-item { + color: rgba(255, 255, 255, .8); + + &.active, + &:hover, + &:focus { + color: $white; + } + + &.active { + font-weight: 700; + + &:hover { + background-color: $list-group-hover-bg; + } + } + + &.disabled:hover { + color: $list-group-disabled-color; + } + } +} \ No newline at end of file diff --git a/kb-app-demo-desktop/frontend/sass/_fontawesome.scss b/kb-app-demo-desktop/frontend/sass/_fontawesome.scss new file mode 100644 index 0000000..a47e750 --- /dev/null +++ b/kb-app-demo-desktop/frontend/sass/_fontawesome.scss @@ -0,0 +1,19 @@ +// file: kb_demo_app/frontend/sass/_fontawesome.scss +// version: 1 + +//@use '@fortawesome/fontawesome-free/scss/variables' with ( +// // customizing $font-path - make sure it points to where your webfonts are stored in your project +// $font-path: '../webfonts', +//); +@use '@fortawesome/fontawesome-free/scss/variables' with ( + // use fonts from @fortawesome/fontawesome-free + $font-path: '@fortawesome/fontawesome-free/webfonts', +); +// load Font Awesome core +@use '@fortawesome/fontawesome-free/scss/fontawesome'; + +// load and make available Font Awesome helpers (mixins, functions, and variables) +@use '@fortawesome/fontawesome-free/scss/fa' as fa; +@use '@fortawesome/fontawesome-free/scss/brands' as fa-brands; +@use '@fortawesome/fontawesome-free/scss/regular' as fa-regular; +@use '@fortawesome/fontawesome-free/scss/solid' as fa-solid; \ No newline at end of file diff --git a/kb-app-demo-desktop/frontend/sass/_simplebar.scss b/kb-app-demo-desktop/frontend/sass/_simplebar.scss new file mode 100644 index 0000000..63b6a48 --- /dev/null +++ b/kb-app-demo-desktop/frontend/sass/_simplebar.scss @@ -0,0 +1,248 @@ +// file: kb_demo_app/frontend/sass/_simplebar.scss +// version: 1 + +/* Rtl support */ +[data-simplebar] { + position: relative; + flex-direction: column; + flex-wrap: wrap; + justify-content: flex-start; + align-content: flex-start; + align-items: flex-start; +} + +.simplebar-wrapper { + overflow: hidden; + width: inherit; + height: inherit; + max-width: inherit; + max-height: inherit; +} + +.simplebar-mask { + direction: inherit; + position: absolute; + overflow: hidden; + padding: 0; + margin: 0; + left: 0; + top: 0; + bottom: 0; + right: 0; + width: auto !important; + height: auto !important; +// z-index: 0; + inset: 0; +} + +.simplebar-offset { + direction: inherit !important; + box-sizing: inherit !important; + resize: none !important; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + padding: 0; + margin: 0; + -webkit-overflow-scrolling: touch; + inset: 0; +} + +.simplebar-content-wrapper { + direction: inherit; + box-sizing: border-box !important; + position: relative; + display: block; + height: 100%; + width: auto; + max-width: 100%; + max-height: 100%; + overflow: auto; + scrollbar-width: none; + -ms-overflow-style: none; + + &::-webkit-scrollbar { + display: none; + width: 0; + height: 0; + } +} + +.simplebar-hide-scrollbar { + &::-webkit-scrollbar { + display: none; + width: 0; + height: 0; + } + + position: fixed; + left: 0; + visibility: hidden; + overflow-y: scroll; + scrollbar-width: none; + -ms-overflow-style: none; +} + +.simplebar-content { + &:before { + content: ' '; + display: table; + } + + &:after { + content: ' '; + display: table; + } +} + +.simplebar-placeholder { + max-height: 100%; + max-width: 100%; + width: 100%; + pointer-events: none; +} + +.simplebar-height-auto-observer-wrapper { + box-sizing: inherit !important; + height: 100%; + width: 100%; + max-width: 1px; + position: relative; + float: left; + max-height: 1px; + overflow: hidden; +// z-index: -1; + padding: 0; + margin: 0; + pointer-events: none; + flex-grow: inherit; + flex-shrink: 0; + flex-basis: 0; +} + +.simplebar-height-auto-observer { + box-sizing: inherit; + display: block; + opacity: 0; + position: absolute; + top: 0; + left: 0; + height: 1000%; + width: 1000%; + min-height: 1px; + min-width: 1px; + overflow: hidden; + pointer-events: none; +// z-index: -1; +} + +.simplebar-track { +// z-index: 1; + position: absolute; + right: 0; + bottom: 0; + pointer-events: none; + overflow: hidden; +} + +[data-simplebar].simplebar-dragging { + pointer-events: none; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + .simplebar-content { + pointer-events: none; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + + .simplebar-track { + pointer-events: all; + } +} + +.simplebar-scrollbar { + position: absolute; + left: 0; + right: 0; + min-height: 10px; + + + &:before { + position: absolute; + content: ''; + background: black; + border-radius: 7px; + left: 2px; + right: 2px; + opacity: 0; + transition: opacity 0.2s 0.5s linear; + top: 2px; + bottom: 2px; + } +} + +.simplebar-scrollbar.simplebar-visible { + &:before { + opacity: 0.5; + transition-delay: 0s; + transition-duration: 0s; + } +} + +.simplebar-track.simplebar-vertical { + top: 0; + width: 11px; +} + +.simplebar-track.simplebar-horizontal { + left: 0; + height: 11px; + + .simplebar-scrollbar { + right: auto; + left: 0; + top: 0; + bottom: 0; + min-height: 0; + min-width: 10px; + width: auto; + } +} + +[data-simplebar-direction='rtl'] { + .simplebar-track.simplebar-vertical { + right: auto; + left: 0; + } +} + +.simplebar-dummy-scrollbar-size { + direction: rtl; + position: fixed; + opacity: 0; + visibility: hidden; + height: 500px; + width: 500px; + overflow-y: hidden; + overflow-x: scroll; + -ms-overflow-style: scrollbar !important; + + >div { + width: 200%; + height: 200%; + margin: 10px 0; + } +} + +.simplebar-hover { + cursor: pointer; +} \ No newline at end of file diff --git a/kb-app-demo-desktop/frontend/sass/_variables.scss b/kb-app-demo-desktop/frontend/sass/_variables.scss new file mode 100644 index 0000000..97db729 --- /dev/null +++ b/kb-app-demo-desktop/frontend/sass/_variables.scss @@ -0,0 +1,95 @@ +// file: kb_demo_app/frontend/sass/_variables.scss +// version: 1 + +// Pulse 5.3.8 +// Bootswatch + +$theme: "pulse" !default; + +// +// Color system +// + +$white: #fff !default; +$gray-100: #fafafa !default; +$gray-200: #f9f8fc !default; +$gray-300: #ededed !default; +$gray-400: #cbc8d0 !default; +$gray-500: #adb5bd !default; +$gray-600: #868e96 !default; +$gray-700: #444 !default; +$gray-800: #343a40 !default; +$gray-900: #17141f !default; +$black: #000 !default; + +$blue: #007bff !default; +$indigo: #6610f2 !default; +$purple: #593196 !default; +$pink: #e83e8c !default; +$red: #fc3939 !default; +$orange: #fd7e14 !default; +$yellow: #efa31d !default; +$green: #13b955 !default; +$teal: #20c997 !default; +$cyan: #009cdc !default; + +$primary: $purple !default; +$secondary: #a991d4 !default; +$success: $green !default; +$info: $cyan !default; +$warning: $yellow !default; +$danger: $red !default; +$light: $gray-200 !default; +$dark: $gray-900 !default; + +$min-contrast-ratio: 2.1 !default; + +// Options + +$enable-rounded: false !default; + +// Body + +$body-color: $gray-700 !default; + +// Links + +$link-hover-color: $primary !default; + +// Tables + +$table-color: initial !default; + +$table-border-color: rgba(0, 0, 0, .05) !default; + +// Forms + +$input-focus-border-color: $primary !default; + +// Dropdowns + +$dropdown-link-hover-color: $white !default; +$dropdown-link-hover-bg: $primary !default; + +// Navs + +$nav-tabs-border-color: $gray-300 !default; +$nav-tabs-link-hover-border-color: $primary !default; + +// Navbar + +$navbar-padding-y: 1.2rem !default; + +// Progress bars + +$progress-bg: $gray-300 !default; +$progress-bar-bg: $primary !default; + +// List group + +$list-group-bg: $gray-900 !default; +$list-group-border-color: transparent !default; +$list-group-hover-bg: lighten($list-group-bg, 10%) !default; +$list-group-active-color: $white !default; +$list-group-active-bg: $list-group-bg !default; +$list-group-disabled-color: lighten($list-group-bg, 30%) !default; \ No newline at end of file diff --git a/kb-app-demo-desktop/frontend/sass/main.scss b/kb-app-demo-desktop/frontend/sass/main.scss new file mode 100644 index 0000000..cebeba2 --- /dev/null +++ b/kb-app-demo-desktop/frontend/sass/main.scss @@ -0,0 +1,10 @@ +// file: kb_demo_app/frontend/sass/main.scss +// version: 1 + +@import "bootstrap/scss/functions"; +@import "variables"; +@import "fontawesome"; +@import "simplebar"; +@import "bootstrap/scss/bootstrap"; +@import "bootswatch"; +@import "app"; diff --git a/kb-app-demo-desktop/frontend/sass/splash.scss b/kb-app-demo-desktop/frontend/sass/splash.scss index a499c73..aad12f5 100644 --- a/kb-app-demo-desktop/frontend/sass/splash.scss +++ b/kb-app-demo-desktop/frontend/sass/splash.scss @@ -1,5 +1,5 @@ -// file: kb-app-demo-desktop/frontend/sass/splash.scss -// version: 2 +// file: kb_app_demo/frontend/sass/splash.scss +// version: 3 @font-face { font-family: 'Dos Amazigh'; @@ -7,36 +7,36 @@ font-weight: normal; font-style: normal; font-display: swap; + /* Assure un bon rendu pendant le chargement de la police */ } -html, body { - width: 100%; - height: 100%; margin: 0; padding: 0; background-color: transparent; - overflow: hidden; -} - -body { display: flex; justify-content: center; align-items: center; + height: 100vh; + overflow: hidden; font-family: Arial, sans-serif; } #splash-container { position: relative; width: 960px; + /* Largeur exacte de votre image */ height: 637px; + /* Hauteur exacte de votre image */ opacity: 0; + /* Sera animé par JavaScript. */ will-change: opacity; } #splash-image { position: absolute; - inset: 0; + top: 0; + left: 0; width: 100%; height: 100%; z-index: 1; @@ -48,34 +48,24 @@ body { left: 50%; transform: translate(-50%, -50%); font-family: 'Dos Amazigh', sans-serif; + /* Application de la police */ font-size: 96px; font-weight: bold; color: #fff; + /* Blanc pour contraste */ text-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + /* Ombre pour meilleure lisibilité */ z-index: 2; text-align: center; } -#debug-info, -#messages-container { - overflow-y: auto; - scrollbar-width: none; - -ms-overflow-style: none; -} - -#debug-info::-webkit-scrollbar, -#messages-container::-webkit-scrollbar { - display: none; - width: 0; - height: 0; -} - #debug-info { position: absolute; top: 0; left: 0; width: 50%; max-height: 30%; + overflow-y: auto; color: white; font-family: monospace; font-size: 12px; @@ -91,6 +81,7 @@ body { left: 0; width: 100%; max-height: 30%; + overflow-y: auto; background-color: rgba(0, 0, 0, 0.1); padding: 10px 10px 40px; z-index: 2; @@ -106,12 +97,30 @@ body { color: #fff; } -.splash-message.info { background-color: rgba(33, 150, 243, 0.2); } -.splash-message.warning { background-color: rgba(255, 152, 0, 0.2); } -.splash-message.error { background-color: rgba(244, 67, 54, 0.2); } -.splash-message.success { background-color: rgba(76, 175, 80, 0.2); } - @keyframes fadeIn { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(10px); + } + + to { + opacity: 1; + transform: translateY(0); + } } + +.splash-message.info { + background-color: rgba(33, 150, 243, 0.2); +} + +.splash-message.warning { + background-color: rgba(255, 152, 0, 0.2); +} + +.splash-message.error { + background-color: rgba(244, 67, 54, 0.2); +} + +.splash-message.success { + background-color: rgba(76, 175, 80, 0.2); +} \ No newline at end of file diff --git a/kb-app-demo-desktop/frontend/ts/frontend_log.ts b/kb-app-demo-desktop/frontend/ts/frontend_log.ts new file mode 100644 index 0000000..f8f4613 --- /dev/null +++ b/kb-app-demo-desktop/frontend/ts/frontend_log.ts @@ -0,0 +1,97 @@ +// file: kb-app-demo-desktop/frontend/ts/frontend_log.ts +// version: 1 + +//! Frontend logging helpers that preserve explicit tracing targets. + +import { invoke } from "@tauri-apps/api/core"; +import type { FrontendLogPayload } from "./bindings/kb_app_demo_desktop/frontend_log/FrontendLogPayload.ts"; + +type FrontendLogLevel = "trace" | "debug" | "info" | "warn" | "error"; + +type ConsoleMethod = (...items: unknown[]) => void; + +const originalConsole = { + trace: console.trace.bind(console), + debug: console.debug.bind(console), + log: console.log.bind(console), + info: console.info.bind(console), + warn: console.warn.bind(console), + error: console.error.bind(console), +}; + +function stringifyItem(item: unknown): string { + if (item instanceof Error) { + return item.stack ?? item.message; + } + if (typeof item === "string") { + return item; + } + try { + return JSON.stringify(item); + } catch (caughtError) { + return String(item); + } +} + +function formatMessage(items: unknown[]): string { + return items.map(item => stringifyItem(item)).join(" "); +} + +async function sendFrontendLog(level: FrontendLogLevel, target: string, message: string): Promise { + const payload: FrontendLogPayload = { + level, + target, + message, + }; + try { + await invoke("emit_frontend_log", { payload }); + } catch (caughtError) { + originalConsole.error("frontend tracing bridge failed", caughtError); + } +} + +export function frontendTrace(target: string, ...items: unknown[]): void { + const message = formatMessage(items); + originalConsole.trace(message); + void sendFrontendLog("trace", target, message); +} + +export function frontendDebug(target: string, ...items: unknown[]): void { + const message = formatMessage(items); + originalConsole.debug(message); + void sendFrontendLog("debug", target, message); +} + +export function frontendInfo(target: string, ...items: unknown[]): void { + const message = formatMessage(items); + originalConsole.info(message); + void sendFrontendLog("info", target, message); +} + +export function frontendWarn(target: string, ...items: unknown[]): void { + const message = formatMessage(items); + originalConsole.warn(message); + void sendFrontendLog("warn", target, message); +} + +export function frontendError(target: string, ...items: unknown[]): void { + const message = formatMessage(items); + originalConsole.error(message); + void sendFrontendLog("error", target, message); +} + +function buildConsoleBridge(level: FrontendLogLevel, target: string, original: ConsoleMethod): ConsoleMethod { + return (...items: unknown[]) => { + original(...items); + void sendFrontendLog(level, target, formatMessage(items)); + }; +} + +export function installFrontendConsoleBridge(target: string): void { + console.trace = buildConsoleBridge("trace", target, originalConsole.trace); + console.debug = buildConsoleBridge("debug", target, originalConsole.debug); + console.log = buildConsoleBridge("info", target, originalConsole.log); + console.info = buildConsoleBridge("info", target, originalConsole.info); + console.warn = buildConsoleBridge("warn", target, originalConsole.warn); + console.error = buildConsoleBridge("error", target, originalConsole.error); +} diff --git a/kb-app-demo-desktop/frontend/ts/main.ts b/kb-app-demo-desktop/frontend/ts/main.ts new file mode 100644 index 0000000..5f62656 --- /dev/null +++ b/kb-app-demo-desktop/frontend/ts/main.ts @@ -0,0 +1,38 @@ +// file: kb-app-demo-desktop/frontend/ts/main.ts +// version: 1 + +import * as bootstrap from "bootstrap"; +import MarkdownIt from "markdown-it"; +import ResizeObserver from "resize-observer-polyfill"; +import "simplebar"; +import { invoke } from "@tauri-apps/api/core"; +import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log.ts"; + +(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap; +(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver; + +const markdownRenderer = new MarkdownIt({ + html: false, + linkify: true, + typographer: true, +}); + +async function loadReadme(readmeContent: HTMLElement): Promise { + try { + const readme = await invoke("load_project_readme"); + readmeContent.innerHTML = markdownRenderer.render(readme); + } catch (caughtError) { + const message = caughtError instanceof Error ? caughtError.message : String(caughtError); + readmeContent.textContent = `Erreur pendant le chargement du README : ${message}`; + frontendError("kb-app-demo-desktop.frontend.main", `README loading failed: ${message}`); + } +} + +document.addEventListener("DOMContentLoaded", async () => { + installFrontendConsoleBridge("kb-app-demo-desktop.frontend.main"); + frontendDebug("kb-app-demo-desktop.frontend.main", "main window loaded"); + const readmeContent = document.querySelector("#readmeContent"); + if (readmeContent) { + await loadReadme(readmeContent); + } +}); diff --git a/kb-app-demo-desktop/src/app_state.rs b/kb-app-demo-desktop/src/app_state.rs new file mode 100644 index 0000000..ecb1b08 --- /dev/null +++ b/kb-app-demo-desktop/src/app_state.rs @@ -0,0 +1,119 @@ +// file: kb-app-demo-desktop/src/app_state.rs +// version: 1 + +//! Shared Tauri application state and startup initialization. + +/// Shared state managed by Tauri for the desktop demo application. +pub(crate) struct AppState { + config_path: std::string::String, + app_config: kb_config::AppConfig, + active_profile: kb_config::ProfileConfig, + logging_guard: std::sync::Mutex, +} + +impl crate::AppState { + /// Initializes configuration, logging and shared runtime state. + pub(crate) fn initialize() -> kb_core::Result { + let config_path = resolve_config_path(); + let app_config = match kb_config::read_config_json_file(&config_path) { + std::result::Result::Ok(config) => config, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let active_profile = match kb_config::active_profile(&app_config) { + std::result::Result::Ok(profile) => profile.clone(), + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let logging_config = convert_logging_config(&active_profile.logging); + let logging_guard = match kb_logging::init_logging(&logging_config) { + std::result::Result::Ok(guard) => guard, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(crate::AppState { + config_path: config_path.display().to_string(), + app_config, + active_profile, + logging_guard: std::sync::Mutex::new(logging_guard), + }); + } + + /// Returns the path used to load the configuration file. + pub(crate) fn config_path(&self) -> &str { + return self.config_path.as_str(); + } + + /// Returns the complete parsed application configuration. + pub(crate) fn app_config(&self) -> &kb_config::AppConfig { + return &self.app_config; + } + + /// Returns the active profile selected from the configuration. + pub(crate) fn active_profile(&self) -> &kb_config::ProfileConfig { + return &self.active_profile; + } + + /// Returns the number of logging routes held by the logging guard. + pub(crate) fn logging_route_count(&self) -> usize { + let lock_result = self.logging_guard.lock(); + let guard = match lock_result { + std::result::Result::Ok(guard) => guard, + std::result::Result::Err(_) => return 0, + }; + return guard.route_count(); + } +} + +fn convert_logging_config(config: &kb_config::LoggingConfig) -> kb_logging::LoggingConfig { + let mut targets = std::vec::Vec::::new(); + for target in &config.targets { + targets.push(kb_logging::LogTargetConfig { + name: target.name.clone(), + enabled: target.enabled, + sink: target.sink.clone(), + level: target.level.clone(), + path: target.path.clone(), + rotation: target.rotation.clone(), + format: target.format.clone(), + ansi: target.ansi, + targets: target.targets.clone(), + }); + } + let mut target_filters = std::vec::Vec::::new(); + for filter in &config.target_filters { + target_filters.push(kb_logging::LogTargetFilterConfig { + target: filter.target.clone(), + level: filter.level.clone(), + }); + } + return kb_logging::LoggingConfig { + default_level: config.default_level.clone(), + targets, + target_filters, + }; +} + +fn resolve_config_path() -> std::path::PathBuf { + let configured = std::env::var("KB_CONFIG_PATH").ok(); + return resolve_config_path_from_value(configured.as_deref()); +} + +fn resolve_config_path_from_value(value: std::option::Option<&str>) -> std::path::PathBuf { + if let std::option::Option::Some(configured) = value { + let trimmed = configured.trim(); + if !trimmed.is_empty() { + return std::path::PathBuf::from(trimmed); + } + } + return crate::workspace_root_dir().join("config/example.config.json"); +} + +#[cfg(test)] +mod tests { + #[test] + fn resolve_config_path_uses_workspace_example_by_default() { + let path = super::resolve_config_path_from_value(std::option::Option::None); + assert_eq!( + path.file_name().and_then(std::ffi::OsStr::to_str), + std::option::Option::Some("example.config.json") + ); + } +} diff --git a/kb-app-demo-desktop/src/frontend_log.rs b/kb-app-demo-desktop/src/frontend_log.rs new file mode 100644 index 0000000..5f68bcd --- /dev/null +++ b/kb-app-demo-desktop/src/frontend_log.rs @@ -0,0 +1,72 @@ +// file: kb-app-demo-desktop/src/frontend_log.rs +// version: 1 + +//! Frontend logging bridge used by Tauri WebView scripts. + +use ts_rs::TS; // rust-rules: derive-import + +/// Log payload sent by frontend scripts. +#[derive(Clone, Debug, serde::Deserialize, TS)] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_app_demo_desktop/frontend_log/FrontendLogPayload.ts" +)] +pub(crate) struct FrontendLogPayload { + /// Lowercase tracing level. + pub(crate) level: std::string::String, + /// Explicit frontend tracing target. + pub(crate) target: std::string::String, + /// Rendered frontend message. + pub(crate) message: std::string::String, +} + +pub(crate) fn emit_frontend_log(payload: crate::FrontendLogPayload) { + let target = normalize_frontend_target(payload.target.as_str()); + match payload.level.trim().to_ascii_lowercase().as_str() { + "trace" => { + tracing::trace!(target: crate::TRACING_TARGET, action = "frontend_log", frontend_target = target, "{}", payload.message) + }, + "debug" => { + tracing::debug!(target: crate::TRACING_TARGET, action = "frontend_log", frontend_target = target, "{}", payload.message) + }, + "warn" => { + tracing::warn!(target: crate::TRACING_TARGET, action = "frontend_log", frontend_target = target, "{}", payload.message) + }, + "error" => { + tracing::error!(target: crate::TRACING_TARGET, action = "frontend_log", frontend_target = target, "{}", payload.message) + }, + _ => { + tracing::info!(target: crate::TRACING_TARGET, action = "frontend_log", frontend_target = target, "{}", payload.message) + }, + } +} + +fn normalize_frontend_target(target: &str) -> std::string::String { + let trimmed = target.trim(); + if trimmed == "kb-app-demo-desktop.frontend.main" { + return trimmed.to_string(); + } + if trimmed == "kb-app-demo-desktop.frontend.splash" { + return trimmed.to_string(); + } + return "kb-app-demo-desktop.frontend".to_string(); +} + +#[cfg(test)] +mod tests { + #[test] + fn normalize_frontend_target_uses_default_for_unknown_target() { + assert_eq!( + super::normalize_frontend_target("other.frontend"), + "kb-app-demo-desktop.frontend" + ); + } + + #[test] + fn normalize_frontend_target_keeps_main_target() { + assert_eq!( + super::normalize_frontend_target("kb-app-demo-desktop.frontend.main"), + "kb-app-demo-desktop.frontend.main" + ); + } +} diff --git a/kb-app-demo-desktop/src/lib.rs b/kb-app-demo-desktop/src/lib.rs index bebd90e..8234534 100644 --- a/kb-app-demo-desktop/src/lib.rs +++ b/kb-app-demo-desktop/src/lib.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/lib.rs -// version: 2 +// version: 3 //! Tauri desktop demo application for `khadhroony-bot3`. @@ -7,13 +7,26 @@ #![deny(unreachable_pub)] #![warn(missing_docs)] +mod app_state; mod constants; +mod frontend_log; +mod main_window; mod splash; mod tauri; /// Runs the desktop demo application. pub use self::tauri::run; +/// Shared application state managed by Tauri. +pub(crate) use self::app_state::AppState; +/// Frontend logging payload. +pub(crate) use self::frontend_log::FrontendLogPayload; +/// Emits one normalized frontend log event. +pub(crate) use self::frontend_log::emit_frontend_log; +/// Loads the project README for the main window. +pub(crate) use self::main_window::load_project_readme; +/// Returns the workspace root directory. +pub(crate) use self::main_window::workspace_root_dir; /// Delay after fade-out before destroying the splash window. pub(crate) use self::splash::SPLASH_CLOSE_WAIT_MS; /// Splash fade duration. @@ -26,6 +39,7 @@ pub(crate) use self::splash::SplashOrder; pub(crate) use self::splash::emit_splash_order; /// Waits for the minimum splash duration. pub(crate) use self::splash::wait_until_minimum; + // Keep the canonical tracing target as the final facade export. /// Canonical tracing target for the desktop demo application. pub(crate) use self::constants::TRACING_TARGET; diff --git a/kb-app-demo-desktop/src/main_window.rs b/kb-app-demo-desktop/src/main_window.rs new file mode 100644 index 0000000..731d579 --- /dev/null +++ b/kb-app-demo-desktop/src/main_window.rs @@ -0,0 +1,68 @@ +// file: kb-app-demo-desktop/src/main_window.rs +// version: 1 + +//! Main-window helpers and workspace README loading. + +pub(crate) fn workspace_root_dir() -> std::path::PathBuf { + let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); + return match manifest_dir.parent() { + std::option::Option::Some(parent) => parent.to_path_buf(), + std::option::Option::None => manifest_dir, + }; +} + +pub(crate) fn load_project_readme() -> kb_core::Result { + let path = crate::workspace_root_dir().join("README.md"); + let content = match std::fs::read_to_string(&path) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(kb_core::Error::io(format!( + "cannot read project README '{}': {error}", + path.display() + ))); + }, + }; + return std::result::Result::Ok(strip_readme_metadata_comments(content.as_str())); +} + +fn strip_readme_metadata_comments(content: &str) -> std::string::String { + let mut output = std::string::String::new(); + let mut leading_metadata = true; + for line in content.lines() { + let trimmed = line.trim(); + if leading_metadata && is_readme_metadata_comment(trimmed) { + continue; + } + leading_metadata = false; + output.push_str(line); + output.push('\n'); + } + return output; +} + +fn is_readme_metadata_comment(line: &str) -> bool { + if line.starts_with("\n\n# Title\nBody\n"; + let cleaned = super::strip_readme_metadata_comments(content); + assert_eq!(cleaned, "# Title\nBody\n"); + } + + #[test] + fn strip_readme_metadata_comments_keeps_later_html_comments() { + let content = "\n# Title\n\nBody\n"; + let cleaned = super::strip_readme_metadata_comments(content); + assert_eq!(cleaned, "# Title\n\nBody\n"); + } +} diff --git a/kb-app-demo-desktop/src/splash.rs b/kb-app-demo-desktop/src/splash.rs index b69a608..2c424b0 100644 --- a/kb-app-demo-desktop/src/splash.rs +++ b/kb-app-demo-desktop/src/splash.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/splash.rs -// version: 1 +// version: 2 //! Splash-window payloads and startup sequencing helpers. @@ -15,7 +15,10 @@ pub(crate) const SPLASH_CLOSE_WAIT_MS: u64 = 3100; /// Command sent from Rust to the splash frontend. #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)] -#[ts(export, export_to = "../frontend/ts/bindings/kb_app_demo_desktop/splash/SplashOrder.ts")] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_app_demo_desktop/splash/SplashOrder.ts" +)] pub(crate) struct SplashOrder { /// Command name. pub(crate) order: std::string::String, @@ -77,8 +80,8 @@ mod tests { #[test] fn splash_timing_is_ordered() { - assert!(super::SPLASH_MINIMUM_MS > 0); - assert!(super::SPLASH_FADE_MS > 0); - assert!(super::SPLASH_CLOSE_WAIT_MS >= u64::from(super::SPLASH_FADE_MS)); + assert!(crate::SPLASH_MINIMUM_MS > 0); + assert!(crate::SPLASH_FADE_MS > 0); + assert!(crate::SPLASH_CLOSE_WAIT_MS >= u64::from(crate::SPLASH_FADE_MS)); } } diff --git a/kb-app-demo-desktop/src/tauri.rs b/kb-app-demo-desktop/src/tauri.rs index 3297abd..93e217e 100644 --- a/kb-app-demo-desktop/src/tauri.rs +++ b/kb-app-demo-desktop/src/tauri.rs @@ -1,13 +1,36 @@ // file: kb-app-demo-desktop/src/tauri.rs -// version: 1 +// version: 3 //! Tauri runtime assembly and private command wrappers. use tauri::Manager; // rust-rules: trait-import /// Runs the desktop demo application. +#[cfg_attr(mobile, tauri::mobile_entry_point)] +#[allow(clippy::question_mark_used)] pub fn run() -> kb_core::Result<()> { - let builder = tauri::Builder::default().setup(|app| { + let rustls_result = install_default_rustls_provider(); + if let std::result::Result::Err(error) = rustls_result { + return std::result::Result::Err(error); + } + let app_state = match crate::AppState::initialize() { + std::result::Result::Ok(state) => state, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + tracing::info!( + target: crate::TRACING_TARGET, + config_path = app_state.config_path(), + active_profile = app_state.active_profile().name.as_str(), + logging_routes = app_state.logging_route_count(), + "starting desktop demo application" + ); + let tracing_builder = tauri_plugin_tracing::Builder::new(); + let mut builder = tauri::Builder::default(); + builder = builder.manage(app_state); + builder = + builder.invoke_handler(tauri::generate_handler![emit_frontend_log, load_project_readme,]); + builder = builder.plugin(tracing_builder.build::()); + builder = builder.setup(|app| { let splash_window = match app.get_webview_window("splash") { std::option::Option::Some(window) => window, std::option::Option::None => { @@ -66,8 +89,38 @@ pub fn run() -> kb_core::Result<()> { let run_result = builder.run(tauri::generate_context!()); return match run_result { std::result::Result::Ok(()) => std::result::Result::Ok(()), - std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::tauri(format!( - "cannot run desktop demo application: {error:?}" - ))), + std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::tauri( + format!("cannot run desktop demo application: {error:?}"), + )), }; } + +fn install_default_rustls_provider() -> kb_core::Result<()> { + if rustls::crypto::CryptoProvider::get_default().is_some() { + return std::result::Result::Ok(()); + } + let provider_result = rustls::crypto::aws_lc_rs::default_provider().install_default(); + return match provider_result { + std::result::Result::Ok(()) => std::result::Result::Ok(()), + std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::invalid_state( + format!("cannot install default rustls crypto provider: {error:?}"), + )), + }; +} + +fn into_ipc_result(result: kb_core::Result) -> std::result::Result { + return match result { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(error) => std::result::Result::Err(error.to_string()), + }; +} + +#[tauri::command] +fn emit_frontend_log(payload: crate::FrontendLogPayload) { + crate::emit_frontend_log(payload); +} + +#[tauri::command] +fn load_project_readme() -> std::result::Result { + return into_ipc_result(crate::load_project_readme()); +}