diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml
index a9eafa7..4493b16 100644
--- a/.github/workflows/desktop-release.yml
+++ b/.github/workflows/desktop-release.yml
@@ -249,7 +249,7 @@ jobs:
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.ver.outputs.tag }}
- files: core/target/dist-out/*.AppImage
+ files: core/target/dist-out/DM_Loremind*.AppImage
fail_on_unmatched_files: true
# BETA uniquement : artefact PRIVE (pas de release publique).
@@ -258,5 +258,5 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: loremind-beta-${{ steps.ver.outputs.version }}-appimage
- path: core/target/dist-out/*.AppImage
+ path: core/target/dist-out/DM_Loremind*.AppImage
retention-days: 90
diff --git a/installers/desktop/build-linux.sh b/installers/desktop/build-linux.sh
index 8803a01..fa813ab 100644
--- a/installers/desktop/build-linux.sh
+++ b/installers/desktop/build-linux.sh
@@ -235,7 +235,9 @@ chmod +x "$APPDIR/AppRun"
# e) appimagetool (telecharge si absent). --appimage-extract-and-run : evite
# d'exiger FUSE (absent des runners CI).
-TOOL="$OUT_DIR/appimagetool-x86_64.AppImage"
+# /!\ HORS de $OUT_DIR : sinon ce .AppImage (l'outil) serait pris pour un livrable
+# et attache a la release a cote du vrai DM_Loremind-*.AppImage.
+TOOL="$CORE_DIR/target/appimagetool-x86_64.AppImage"
if [[ ! -x "$TOOL" ]]; then
curl -fsSL -o "$TOOL" \
https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
diff --git a/web/src/app/app.component.html b/web/src/app/app.component.html
index 2e0305d..29a255c 100644
--- a/web/src/app/app.component.html
+++ b/web/src/app/app.component.html
@@ -20,3 +20,6 @@
+
+
+
diff --git a/web/src/app/app.component.ts b/web/src/app/app.component.ts
index d49295f..66d9756 100644
--- a/web/src/app/app.component.ts
+++ b/web/src/app/app.component.ts
@@ -6,6 +6,7 @@ import { SecondarySidebarComponent } from './shared/secondary-sidebar/secondary-
import { GlobalSearchComponent } from './shared/global-search/global-search.component';
import { UpdateBannerComponent } from './shared/update-banner/update-banner.component';
import { ConfirmDialogHostComponent } from './shared/confirm-dialog/confirm-dialog-host.component';
+import { FirstRunLanguageComponent } from './shared/first-run-language/first-run-language.component';
import { LayoutService } from './services/layout.service';
import { GlobalSearchService } from './services/global-search.service';
import { VersionCheckerService } from './services/version-checker.service';
@@ -19,6 +20,7 @@ import { VersionCheckerService } from './services/version-checker.service';
GlobalSearchComponent,
UpdateBannerComponent,
ConfirmDialogHostComponent,
+ FirstRunLanguageComponent,
AsyncPipe
],
templateUrl: './app.component.html',
diff --git a/web/src/app/services/language.service.ts b/web/src/app/services/language.service.ts
index 8d73612..091734b 100644
--- a/web/src/app/services/language.service.ts
+++ b/web/src/app/services/language.service.ts
@@ -88,6 +88,14 @@ export class LanguageService {
return this.defaultLang;
}
+ /**
+ * Vrai si une langue a déjà été choisie explicitement (mémorisée en localStorage).
+ * Faux au tout premier lancement → on propose alors l'écran de choix de langue.
+ */
+ hasExplicitChoice(): boolean {
+ return this.read() !== null;
+ }
+
private read(): string | null {
try {
return localStorage.getItem(STORAGE_KEY);
diff --git a/web/src/app/shared/first-run-language/first-run-language.component.html b/web/src/app/shared/first-run-language/first-run-language.component.html
new file mode 100644
index 0000000..58ce620
--- /dev/null
+++ b/web/src/app/shared/first-run-language/first-run-language.component.html
@@ -0,0 +1,17 @@
+@if (visible) {
+
+
+

+
Bienvenue · Welcome
+
Choisissez votre langue · Choose your language
+
+ @for (opt of options; track opt.code) {
+
+ }
+
+
+
+}
diff --git a/web/src/app/shared/first-run-language/first-run-language.component.scss b/web/src/app/shared/first-run-language/first-run-language.component.scss
new file mode 100644
index 0000000..5372ccc
--- /dev/null
+++ b/web/src/app/shared/first-run-language/first-run-language.component.scss
@@ -0,0 +1,76 @@
+.flo-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 10000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 1.5rem;
+ background: rgba(10, 10, 20, 0.82);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+}
+
+.flo-card {
+ width: 100%;
+ max-width: 420px;
+ padding: 2.5rem 2rem;
+ text-align: center;
+ background: #12121f;
+ border: 1px solid rgba(108, 99, 255, 0.25);
+ border-radius: 18px;
+ box-shadow: 0 30px 80px -30px rgba(0, 0, 0, 0.8);
+}
+
+.flo-logo {
+ width: 64px;
+ height: 64px;
+ border-radius: 14px;
+ object-fit: contain;
+}
+
+.flo-title {
+ margin: 1rem 0 0.3rem;
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: #fff;
+}
+
+.flo-sub {
+ margin: 0 0 1.8rem;
+ font-size: 0.95rem;
+ color: #9ca3af;
+}
+
+.flo-buttons {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.flo-btn {
+ display: flex;
+ align-items: center;
+ gap: 0.9rem;
+ width: 100%;
+ padding: 0.9rem 1.1rem;
+ font-size: 1.05rem;
+ font-weight: 600;
+ color: #e0e0e0;
+ background: rgba(108, 99, 255, 0.08);
+ border: 1px solid rgba(108, 99, 255, 0.25);
+ border-radius: 12px;
+ cursor: pointer;
+ transition: background 0.15s, border-color 0.15s, transform 0.12s;
+}
+
+.flo-btn:hover {
+ background: rgba(108, 99, 255, 0.18);
+ border-color: rgba(108, 99, 255, 0.5);
+ transform: translateY(-1px);
+}
+
+.flo-flag {
+ font-size: 1.5rem;
+ line-height: 1;
+}
diff --git a/web/src/app/shared/first-run-language/first-run-language.component.ts b/web/src/app/shared/first-run-language/first-run-language.component.ts
new file mode 100644
index 0000000..661d267
--- /dev/null
+++ b/web/src/app/shared/first-run-language/first-run-language.component.ts
@@ -0,0 +1,35 @@
+import { Component, inject } from '@angular/core';
+import { LanguageService } from '../../services/language.service';
+
+/**
+ * Écran de choix de langue au TOUT PREMIER lancement (overlay one-shot).
+ *
+ * Ne s'affiche que si aucune langue n'a encore été choisie explicitement
+ * (cf. LanguageService.hasExplicitChoice). Le choix est mémorisé en localStorage
+ * via LanguageService.use() → aux lancements suivants l'overlay ne réapparaît pas,
+ * et le sélecteur de langue habituel prend le relais.
+ *
+ * Textes volontairement BILINGUES / neutres (l'utilisateur n'a pas encore choisi).
+ */
+@Component({
+ selector: 'app-first-run-language',
+ standalone: true,
+ templateUrl: './first-run-language.component.html',
+ styleUrls: ['./first-run-language.component.scss'],
+})
+export class FirstRunLanguageComponent {
+ private readonly language = inject(LanguageService);
+
+ visible = !this.language.hasExplicitChoice();
+
+ /** Libellés NATIFs (indépendants de la langue courante). */
+ readonly options: ReadonlyArray<{ code: string; native: string; flag: string }> = [
+ { code: 'fr', native: 'Français', flag: '🇫🇷' },
+ { code: 'en', native: 'English', flag: '🇬🇧' },
+ ];
+
+ choose(code: string): void {
+ this.language.use(code); // applique + mémorise
+ this.visible = false;
+ }
+}