Mise en place de l'import / export des données pour pouvoir sauvegarder les lores / campagnes

This commit is contained in:
2026-06-18 09:49:34 +02:00
parent 7dfa9c3655
commit 72fe5e6215
19 changed files with 1775 additions and 10 deletions

View File

@@ -112,7 +112,7 @@
<label for="onemin-key">{{ 'settings.apiKey.label' | translate }}</label>
<input
id="onemin-key"
type="password"
type="password" autocomplete="new-password" data-1p-ignore data-lpignore="true" data-bwignore
[(ngModel)]="oneminApiKeyInput"
[placeholder]="(settings.onemin_api_key_set ? 'settings.apiKey.placeholderSet' : 'settings.apiKey.placeholderOnemin') | translate">
@if (settings.onemin_api_key_set) {
@@ -149,7 +149,7 @@
<label for="openrouter-key">{{ 'settings.apiKey.label' | translate }}</label>
<input
id="openrouter-key"
type="password"
type="password" autocomplete="new-password" data-1p-ignore data-lpignore="true" data-bwignore
[(ngModel)]="openrouterApiKeyInput"
[placeholder]="(settings.openrouter_api_key_set ? 'settings.apiKey.placeholderSet' : 'settings.apiKey.placeholderOpenrouter') | translate">
@if (settings.openrouter_api_key_set) {
@@ -183,7 +183,7 @@
<label for="mistral-key">{{ 'settings.apiKey.label' | translate }}</label>
<input
id="mistral-key"
type="password"
type="password" autocomplete="new-password" data-1p-ignore data-lpignore="true" data-bwignore
[(ngModel)]="mistralApiKeyInput"
[placeholder]="(settings.mistral_api_key_set ? 'settings.apiKey.placeholderSet' : 'settings.apiKey.placeholderMistral') | translate">
@if (settings.mistral_api_key_set) {
@@ -213,7 +213,7 @@
<label for="gemini-key">{{ 'settings.apiKey.label' | translate }}</label>
<input
id="gemini-key"
type="password"
type="password" autocomplete="new-password" data-1p-ignore data-lpignore="true" data-bwignore
[(ngModel)]="geminiApiKeyInput"
[placeholder]="(settings.gemini_api_key_set ? 'settings.apiKey.placeholderSet' : 'settings.apiKey.placeholderGemini') | translate">
@if (settings.gemini_api_key_set) {
@@ -285,7 +285,7 @@
<span class="key-state">{{ 'settings.apiKey.configured' | translate }}</span>
}
</label>
<input id="emb-mistral-key" type="password" [(ngModel)]="mistralApiKeyInput"
<input id="emb-mistral-key" type="password" autocomplete="new-password" data-1p-ignore data-lpignore="true" data-bwignore [(ngModel)]="mistralApiKeyInput"
[placeholder]="(settings.mistral_api_key_set ? 'settings.apiKey.placeholderSet' : 'settings.apiKey.placeholderMistral') | translate">
@if (settings.mistral_api_key_set) {
<label class="checkbox">
@@ -372,6 +372,33 @@
<!-- Mises a jour conteneurs + licence Patreon + canal beta (sous-composant) -->
<app-settings-updates-section></app-settings-updates-section>
<!-- Sauvegarde / Restauration des donnees (export & import) -->
<section class="card">
<h2>{{ 'settings.data.title' | translate }}</h2>
<p class="hint" [innerHTML]="'settings.data.hint' | translate"></p>
<div class="actions">
<button type="button" class="btn-secondary" (click)="exportData()" [disabled]="exporting || importing">
<span>{{ (exporting ? 'settings.data.exporting' : 'settings.data.exportBtn') | translate }}</span>
</button>
<button type="button" class="btn-secondary" (click)="importInput.click()" [disabled]="exporting || importing">
<span>{{ (importing ? 'settings.data.importing' : 'settings.data.importBtn') | translate }}</span>
</button>
<input #importInput type="file" accept=".zip,application/zip" hidden (change)="onImportFileSelected($event)">
</div>
@if (dataError) {
<div class="alert alert-error">
<lucide-icon [img]="AlertCircle" [size]="16"></lucide-icon>
<span>{{ dataError }}</span>
</div>
}
@if (dataMessage) {
<div class="alert alert-success">
<lucide-icon [img]="Check" [size]="16"></lucide-icon>
<span>{{ dataMessage }}</span>
</div>
}
</section>
@if (settings) {
<div class="actions">
<button class="btn-primary" (click)="save()" [disabled]="saving">

View File

@@ -4,7 +4,7 @@ import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { LucideAngularModule, ArrowLeft, RefreshCw, Save, Check, AlertCircle, Plus } from 'lucide-angular';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { SettingsService, AppSettings, AppSettingsUpdate, OneMinModelGroup, OpenRouterModel, MistralModel, GeminiModel } from '../services/settings.service';
import { SettingsService, AppSettings, AppSettingsUpdate, OneMinModelGroup, OpenRouterModel, MistralModel, GeminiModel, ImportResult } from '../services/settings.service';
import { LayoutService } from '../services/layout.service';
import { UpdatesSectionComponent } from './updates-section/updates-section.component';
import { OllamaModelManagerComponent } from './ollama-model-manager/ollama-model-manager.component';
@@ -332,6 +332,60 @@ export class SettingsComponent implements OnInit {
this.router.navigate(['/lore']);
}
// --- Sauvegarde / Restauration des donnees --------------------------------
exporting = false;
importing = false;
dataMessage = '';
dataError = '';
/** Telecharge l'export complet (zip) via un lien temporaire. */
exportData(): void {
this.exporting = true;
this.dataMessage = '';
this.dataError = '';
this.settingsService.exportData().subscribe({
next: (blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'loremind-export.zip';
a.click();
URL.revokeObjectURL(url);
this.exporting = false;
},
error: (err) => {
this.dataError = this.extractError(err, this.translate.instant('settings.data.exportError'));
this.exporting = false;
}
});
}
/** Déclenché par le sélecteur de fichier : importe le zip choisi (fusion). */
onImportFileSelected(event: Event): void {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
input.value = ''; // permet de re-sélectionner le même fichier
if (!file) return;
if (!confirm(this.translate.instant('settings.data.importConfirm'))) return;
this.importing = true;
this.dataMessage = '';
this.dataError = '';
this.settingsService.importData(file).subscribe({
next: (result: ImportResult) => {
const total = Object.values(result.created).reduce((a, b) => a + b, 0);
this.dataMessage = this.translate.instant('settings.data.importSuccess', { count: total });
this.importing = false;
},
error: (err) => {
this.dataError = this.extractError(err, this.translate.instant('settings.data.importError'));
this.importing = false;
}
});
}
private extractError(err: any, fallback: string): string {
if (err?.error?.detail) return String(err.error.detail);
if (err?.message) return err.message;