Some checks failed
Qualité & Sécurité / MegaLinter (PMD · Ruff · Bandit · gitleaks · hadolint) (push) Successful in 1m9s
Build & Push Images / build (brain) (push) Successful in 1m10s
Qualité & Sécurité / Trivy (CVE dépendances Maven / pip / npm) (push) Failing after 2m36s
Build & Push Images / build (web) (push) Successful in 1m48s
Build & Push Images / build-switcher (push) Successful in 20s
Build & Push Images / build (core) (push) Successful in 3m9s
Qualité & Sécurité / Web (ESLint · angular-eslint + sonarjs) (push) Successful in 38s
CI (Gitea Actions) : - Nouveau workflow quality.yml (séparé de ci.yml, non bloquant pour la release) : MegaLinter v9 (PMD, Ruff, Bandit, gitleaks, hadolint), ng lint (web) et Trivy (CVE des dépendances Maven/pip/npm), sur main + beta + PR - Trivy : préchargement du cache Maven avant le scan — sans ça Maven Central rate-limite l'IP du runner (429) en résolvant les BOMs du parent Spring Boot - upload-artifact v4 → v3 : l'API artifacts v4 n'est pas supportée par Gitea (corrige aussi l'upload du rapport Playwright de e2e.yml, cassé depuis toujours) - Ruleset PMD projet (java-pmd-ruleset.xml, auto-détecté par MegaLinter) orienté bugs réels, sans le style Lombok-hostile du défaut : 11 337 → 65 findings ; PMD en mode rapport (JAVA_PMD_DISABLE_ERRORS) le temps de purger ce backlog Web : - angular-eslint 21 + eslint-plugin-sonarjs : les règles Sonar vivent désormais dans ESLint (config web/eslint.config.js, réglages justifiés en commentaire) - tsconfig : skipLibCheck + types:[] + node_modules ré-exclu — répare les builds desktop Windows/Linux cassés par le conflit @types/eslint-scope vs ESLint 9 - Purge du backlog lint : 114 erreurs → 0 sur 66 fichiers (ngOnDestroy vides supprimés, any typés avec les vrais DTOs, outputs (close) → (closed), regex super-linéaires désamorcées, ternaires/fonctions imbriqués dépliés, intention documentée sur les catch/error volontairement vides) - Bug latent corrigé au passage : license.service.disconnect() émettait undefined au lieu de true en cas de succès (masqué par un double cast)
111 lines
4.0 KiB
TypeScript
111 lines
4.0 KiB
TypeScript
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
|
|
|
|
import { LucideAngularModule, X, Image as ImageIcon, ChevronLeft, ChevronRight } from 'lucide-angular';
|
|
import { TranslatePipe } from '@ngx-translate/core';
|
|
import { ImageService } from '../../services/image.service';
|
|
import { Image } from '../../services/image.model';
|
|
import { ImageLayout } from '../../services/template.model';
|
|
import { ImageUploaderComponent } from '../image-uploader/image-uploader.component';
|
|
|
|
/**
|
|
* Composant reutilisable de galerie d'images.
|
|
*
|
|
* Deux modes :
|
|
* - editable=false (defaut) : affichage en grille, clic sur une image ouvre
|
|
* un lightbox plein ecran pour zoomer.
|
|
* - editable=true : bouton "+ ajouter" en fin de grille (via app-image-uploader),
|
|
* chaque vignette a un bouton "X" pour la retirer.
|
|
*
|
|
* La galerie raisonne sur une liste d'IDs d'images (string[]). Elle ne stocke
|
|
* pas les objets Image eux-memes : les thumbs utilisent `imageService.contentUrl(id)`
|
|
* directement comme src. Le navigateur cache les binaires via Cache-Control immutable
|
|
* pose par le backend, donc aucune requete redondante.
|
|
*
|
|
* Usage :
|
|
* <app-image-gallery [imageIds]="scene.illustrationImageIds"></app-image-gallery>
|
|
* <app-image-gallery [imageIds]="tempIds" [editable]="true"
|
|
* (imageIdsChange)="tempIds = $event"></app-image-gallery>
|
|
*/
|
|
@Component({
|
|
selector: 'app-image-gallery',
|
|
imports: [LucideAngularModule, ImageUploaderComponent, TranslatePipe],
|
|
templateUrl: './image-gallery.component.html',
|
|
styleUrls: ['./image-gallery.component.scss']
|
|
})
|
|
export class ImageGalleryComponent {
|
|
readonly X = X;
|
|
readonly ImageIcon = ImageIcon;
|
|
readonly ChevronLeft = ChevronLeft;
|
|
readonly ChevronRight = ChevronRight;
|
|
|
|
/** IDs d'images a afficher. */
|
|
@Input() imageIds: string[] = [];
|
|
|
|
/** Mode edition : afficher le bouton d'ajout + les boutons de suppression. */
|
|
@Input() editable = false;
|
|
|
|
/**
|
|
* Variante de mise en page. Null/undefined = GALLERY (rendu historique).
|
|
* HERO : premiere image en banniere pleine largeur, suivantes en petit dessous.
|
|
* MASONRY : mosaique a hauteurs variables.
|
|
* CAROUSEL : defilement horizontal avec fleches.
|
|
*/
|
|
@Input() layout: ImageLayout | null | undefined = 'GALLERY';
|
|
|
|
/** Emet la nouvelle liste quand l'utilisateur ajoute/retire une image. */
|
|
@Output() imageIdsChange = new EventEmitter<string[]>();
|
|
|
|
/** ID de l'image actuellement ouverte en lightbox (null = ferme). */
|
|
lightboxId: string | null = null;
|
|
|
|
@ViewChild('carouselTrack') carouselTrack?: ElementRef<HTMLDivElement>;
|
|
|
|
constructor(private imageService: ImageService) {}
|
|
|
|
/** Layout effectif (null → GALLERY). */
|
|
get effectiveLayout(): ImageLayout {
|
|
return this.layout ?? 'GALLERY';
|
|
}
|
|
|
|
/** Premiere image (pour le layout HERO). */
|
|
get heroId(): string | null {
|
|
return this.imageIds[0] ?? null;
|
|
}
|
|
|
|
/** Images restantes apres la hero (pour le layout HERO). */
|
|
get restIds(): string[] {
|
|
return this.imageIds.slice(1);
|
|
}
|
|
|
|
scrollCarousel(direction: -1 | 1): void {
|
|
const el = this.carouselTrack?.nativeElement;
|
|
if (!el) return;
|
|
el.scrollBy({ left: direction * Math.max(240, el.clientWidth * 0.8), behavior: 'smooth' });
|
|
}
|
|
|
|
/** URL absolue du binaire d'une image. */
|
|
urlFor(id: string): string {
|
|
return this.imageService.contentUrl(id);
|
|
}
|
|
|
|
onUploaded(image: Image): void {
|
|
this.imageIdsChange.emit([...this.imageIds, image.id]);
|
|
}
|
|
|
|
remove(id: string, event: MouseEvent): void {
|
|
event.stopPropagation(); // Evite d'ouvrir le lightbox en cliquant sur X.
|
|
// On supprime aussi cote serveur pour ne pas laisser d'image orpheline.
|
|
// Best-effort : on n'attend pas le retour pour emettre la nouvelle liste.
|
|
this.imageService.delete(id).subscribe({ error: () => { /* best-effort : erreur ignorée volontairement */ } });
|
|
this.imageIdsChange.emit(this.imageIds.filter(i => i !== id));
|
|
}
|
|
|
|
openLightbox(id: string): void {
|
|
this.lightboxId = id;
|
|
}
|
|
|
|
closeLightbox(): void {
|
|
this.lightboxId = null;
|
|
}
|
|
}
|