From 01e0671de904c2a84f0debf0516e24178bf03fac Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Wed, 30 Sep 2026 22:50:46 -0400 Subject: [PATCH] feat(lecteur): menu FAB partage /watch et /shorts - PlayerMenuComponent : FAB circulaire translucide (haut-gauche) + panneau deroulant, commun aux deux pages (position, style, open/close, clic exterieur) - /shorts : consolidation de tous les boutons (fournisseurs, langue, actions) dans le menu FAB ; suppression du rail a droite et de la rangee du haut - /watch : actions (cinema, qualite, j'aime, playlist, transcription, telechargement, partage, ouvrir) deplacees dans le menu FAB - AddToPlaylistComponent : variante menuItem pour s'integrer au menu - styles .pm-item/.pm-sep/.pm-section-label dans index.css --- index.css | 31 +++ .../player-menu/player-menu.component.ts | 65 +++++ .../add-to-playlist.component.ts | 14 +- .../shorts/watch-short.component.html | 226 ++++++++---------- .../shorts/watch-short.component.ts | 18 +- src/components/watch/watch.component.html | 179 ++++++-------- src/components/watch/watch.component.ts | 8 +- 7 files changed, 301 insertions(+), 240 deletions(-) create mode 100644 src/app/shared/components/player-menu/player-menu.component.ts diff --git a/index.css b/index.css index 1845a0c..b21c891 100644 --- a/index.css +++ b/index.css @@ -572,6 +572,37 @@ body { touch-action: none; } +/* --- Player FAB menu (shared /watch & /shorts) ---------------------------- */ +/* Items/coque du panneau du menu flottant : petits, translucides, harmonisés + avec les commandes overlay des players YouTube/Dailymotion. */ +.pm-section-label { + padding: 0.375rem 0.75rem 0.25rem; + font-size: 0.6875rem; /* 11px */ + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--muted); +} +.pm-item { + display: flex; + align-items: center; + gap: 0.625rem; + width: 100%; + padding: 0.5rem 0.75rem; + border-radius: 0.5rem; + font-size: 0.8125rem; /* 13px */ + color: var(--text); + text-align: left; + line-height: 1.25; + transition: background-color .15s ease; +} +.pm-item:hover { background-color: rgba(255, 255, 255, .08); } +.pm-item:disabled { opacity: .4; cursor: not-allowed; } +.pm-item svg { flex-shrink: 0; } +.pm-item .pm-item-label { flex: 1; min-width: 0; } +.pm-item .pm-item-hint { font-size: 0.6875rem; color: var(--muted); } +.pm-sep { height: 1px; margin: 0.375rem 0.5rem; background: rgba(255, 255, 255, .12); } + /* --- Drawer / overlay plumbing -------------------------------------------- */ /* Applied to while a full-screen drawer or modal owns the viewport. Note: deliberately NOT `touch-action: none` — touch-action is intersected up diff --git a/src/app/shared/components/player-menu/player-menu.component.ts b/src/app/shared/components/player-menu/player-menu.component.ts new file mode 100644 index 0000000..434ec9e --- /dev/null +++ b/src/app/shared/components/player-menu/player-menu.component.ts @@ -0,0 +1,65 @@ +import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, HostListener, Input, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +/** + * Bouton-menu flottant (FAB) partagé entre /watch et /shorts. + * + * Pastille circulaire translucide en haut à gauche du lecteur, façon + * commandes overlay des players YouTube/Dailymotion : elle ouvre un panneau + * déroulant qui reçoit son contenu par projection (`ng-content`) — chaque page + * fournit ses propres options, la coque (position, style, open/close, clic + * extérieur) est identique partout. + * + * Usage : ` …items… ` + * dans un conteneur `position: relative` (le `:host` se place en absolu). + */ +@Component({ + selector: 'app-player-menu', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule], + template: ` + + @if (open) { + + } + `, + styles: [ + `:host { position: absolute; top: 0.5rem; left: 0.5rem; z-index: 30; display: block; }`, + ], +}) +export class PlayerMenuComponent { + @Input() open = false; + @Output() openChange = new EventEmitter(); + + constructor(private el: ElementRef) {} + + toggle(): void { + this.openChange.emit(!this.open); + } + + close(): void { + if (this.open) this.openChange.emit(false); + } + + @HostListener('document:click', ['$event']) + onDocumentClick(event: Event): void { + if (!this.open) return; + const target = event.target as Node | null; + if (target && this.el.nativeElement.contains(target)) return; + this.close(); + } +} diff --git a/src/components/shared/components/add-to-playlist/add-to-playlist.component.ts b/src/components/shared/components/add-to-playlist/add-to-playlist.component.ts index 1d13b83..5c721b3 100644 --- a/src/components/shared/components/add-to-playlist/add-to-playlist.component.ts +++ b/src/components/shared/components/add-to-playlist/add-to-playlist.component.ts @@ -12,18 +12,22 @@ import { forkJoin } from 'rxjs'; // HttpClient sans intercepteurs (plus de Bearer/refresh -> 401). imports: [CommonModule], template: ` -
+
@if (open()) { @@ -115,6 +119,8 @@ export class AddToPlaylistComponent implements OnInit, OnDestroy { @Input() videoId: string = ''; @Input() title?: string; @Input() thumbnail?: string; + /** Rendu « élément de menu » (menu FAB du lecteur) : plein largeur, icon + label. */ + @Input() menuItem = false; open = signal(false); loading = signal(false); diff --git a/src/components/shorts/watch-short.component.html b/src/components/shorts/watch-short.component.html index 6485f40..d12aeff 100644 --- a/src/components/shorts/watch-short.component.html +++ b/src/components/shorts/watch-short.component.html @@ -10,66 +10,19 @@ - + - -
- - - - -
-
@@ -93,13 +46,102 @@ } @if (current(); as v) { + + + + + @for (p of providerButtons(); track p.id) { + + } + +
+ + + +
+ + + + + @if (watchLink()) { + + + Commentaires + + } + + @if (providerUrl()) { + + + Ouvrir chez le fournisseur + + } + @if (watchLink()) { + + + Ouvrir dans Watch + + } + @if (currentSource()) { +
Via le catalogue • {{ currentSource() }}
+ } + +
+ + YouTube, ou accès direct au lecteur 6 s sinon). --> @@ -108,76 +150,6 @@
Commandes du lecteur accessibles
} - -
- - -
- - @if (menuOpen()) { - - } -
-
- - -
-
- - J'aime -
-
- - - - Commentaires -
-
- - Partager -
-
- - Son -
-
-
@@ -233,9 +205,9 @@
diff --git a/src/components/shorts/watch-short.component.ts b/src/components/shorts/watch-short.component.ts index ec3f866..c7504a9 100644 --- a/src/components/shorts/watch-short.component.ts +++ b/src/components/shorts/watch-short.component.ts @@ -16,6 +16,7 @@ import { Video } from '../../models/video.model'; import { shortsFeedAccepts, isPlayable as isPlayablePolicy, type PlayableContext } from '../../app/shared/utils/section-policy'; import { SHORT_TO_LONG, type ProviderId } from '../../app/shared/providers/provider-ids'; import { dailymotionEmbedBase } from '../../app/shared/providers/dailymotion-player'; +import { PlayerMenuComponent } from '../../app/shared/components/player-menu/player-menu.component'; import { SearchService } from '../../app/search/search.service'; import { ChannelProviderFactory } from '../../app/core/providers/channel/channel-provider.factory'; import { @@ -87,7 +88,7 @@ const SHORTS_QUERIES: Record = { selector: 'app-watch-short', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [CommonModule, RouterLink], + imports: [CommonModule, RouterLink, PlayerMenuComponent], templateUrl: './watch-short.component.html' }) export class WatchShortComponent implements OnDestroy { @@ -274,6 +275,18 @@ export class WatchShortComponent implements OnDestroy { return this.langFilter() === 'profile' ? `Filtre actif : ${base} (profil)` : `Filtre actif : ${base}`; } catch { return 'Filtre de langues'; } }); + + /** Libellé court du mode de langue pour le menu FAB (« Français »…). */ + readonly langFilterShort = computed(() => { + switch (this.langFilter()) { + case 'profile': return 'Profil'; + case 'fr': return 'Français'; + case 'en': return 'Anglais'; + case 'fren': return 'FR + EN'; + case 'all': return 'Toutes'; + default: return this.langFilter(); + } + }); readonly current = computed