feat(library): liste « À regarder plus tard » (watch later)
CI / build-and-test (push) Successful in 14m55s
CI / build-and-test (push) Successful in 14m55s
Nouvelle liste dans la section Vous de la barre latérale, avec un bouton horloge a cote du coeur sur toutes les cartes (accueil, themes, provider- themes, recherche). - db : les 4 fonctions de listes (like/unlike/isLiked/list) prennent un paramètre `tag` — la table `tags` sert déjà de mot-clés génériques, donc zéro migration. - api : les 4 routes likes sont générées par registerVideoTagRoutes(prefix, tagName) ; /api/user/watch-later = même fabrique, même enrichissement yt-dlp, même middleware cookie-aware. Entrée OpenAPI ajoutée. - front : LikesService prend un VideoTag, like-button un @Input() list (icône horloge, ambre quand enregistré, libellés a11y dédiés) — un seul composant pour les deux boutons, pas de copie. - cartes de recherche : la grille partagée n'avait AUCUN bouton ; cœur + horloge ajoutés, masqués sur les lives et cartes chaîne (coin bas-gauche déjà occupé par le compteur de spectateurs / pas des vidéos). - route /library/watch-later = LikedComponent via data.list (titre, icône, placeholder et état vide pilotés par la route), clés nav.watchLater FR/EN. Tests : npm run test:api 41/41 (nouveau test watch-later : ajout, statut, indépendance vis-à-vis des likes, liste, retrait, 400, 401) + 21 autres suites vertes. Build OK, instance locale 4200 rebuildée. Sondage Playwright desktop+mobile : 12/14 (2 faux positifs du sondage : accordéon replié sur une route hors /library, 3×401 console pendant la phase déconnectée).
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, inject, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { LikesService } from '../../../../services/likes.service';
|
||||
import { LikesService, VideoTag } from '../../../../services/likes.service';
|
||||
import { finalize } from 'rxjs';
|
||||
import { AuthService } from '../../../../services/auth.service';
|
||||
|
||||
@@ -14,13 +14,16 @@ import { AuthService } from '../../../../services/auth.service';
|
||||
@if (isAuthenticated()) {
|
||||
<button
|
||||
(click)="onClick($event)"
|
||||
[class.text-red-500]="isLiked()"
|
||||
[class.text-red-500]="isLiked() && list === 'like'"
|
||||
[class.text-amber-400]="isLiked() && list === 'watch-later'"
|
||||
[class.text-slate-400]="!isLiked()"
|
||||
[class.hover\:text-red-500]="!isLiked()"
|
||||
[class.hover\:text-red-500]="!isLiked() && list === 'like'"
|
||||
[class.hover\:text-amber-400]="!isLiked() && list === 'watch-later'"
|
||||
[disabled]="isLoading()"
|
||||
class="flex h-11 w-11 items-center justify-center rounded-full transition-colors duration-200 hover:bg-slate-700/50 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500/60"
|
||||
[attr.aria-label]="isLiked() ? 'Retirer des vidéos aimées' : 'Ajouter aux vidéos aimées'"
|
||||
[attr.aria-label]="label"
|
||||
[attr.aria-pressed]="isLiked()"
|
||||
[attr.title]="label"
|
||||
>
|
||||
@if (isLoading()) {
|
||||
<svg class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
@@ -36,11 +39,9 @@ import { AuthService } from '../../../../services/auth.service';
|
||||
fill="currentColor"
|
||||
[attr.aria-hidden]="true"
|
||||
>
|
||||
<path
|
||||
[attr.d]="isLiked() ? 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z' : 'M16.5 3c-1.74 0-3.41.81-4.5 2.09C10.91 3.81 9.24 3 7.5 3 4.42 3 2 5.42 2 8.5c0 3.78 3.4 6.86 8.55 11.54L12 21.35l1.45-1.32C18.6 15.36 22 12.28 22 8.5 22 5.42 19.58 3 16.5 3zm-4.4 15.55l-.1.1-.1-.1C7.14 14.24 4 11.39 4 8.5 4 6.5 5.5 5 7.5 5c1.54 0 3.04.99 3.57 2.36h1.87C13.46 5.99 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5 0 2.89-3.14 5.74-7.9 10.05z'"
|
||||
/>
|
||||
<path [attr.d]="iconPath" />
|
||||
</svg>
|
||||
<span class="sr-only">{{ isLiked() ? 'Retirer des vidéos aimées' : 'Ajouter aux vidéos aimées' }}</span>
|
||||
<span class="sr-only">{{ label }}</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
@@ -83,6 +84,8 @@ export class LikeButtonComponent implements OnInit, OnDestroy, OnChanges {
|
||||
private likesService = inject(LikesService);
|
||||
private auth = inject(AuthService);
|
||||
|
||||
/** Liste pilotée par ce bouton : 'like' (cœur) ou 'watch-later' (horloge). */
|
||||
@Input() list: VideoTag = 'like';
|
||||
@Input() provider = 'youtube';
|
||||
@Input() title?: string;
|
||||
@Input() thumbnail?: string;
|
||||
@@ -100,6 +103,27 @@ export class LikeButtonComponent implements OnInit, OnDestroy, OnChanges {
|
||||
get videoId(): string {
|
||||
return this._videoId;
|
||||
}
|
||||
|
||||
/** Libellé a11y (aria-label / title / sr-only) selon la liste et l'état. */
|
||||
get label(): string {
|
||||
if (this.list === 'watch-later') {
|
||||
return this.isLiked()
|
||||
? 'Retirer de « À regarder plus tard »'
|
||||
: 'Ajouter à « À regarder plus tard »';
|
||||
}
|
||||
return this.isLiked() ? 'Retirer des vidéos aimées' : 'Ajouter aux vidéos aimées';
|
||||
}
|
||||
|
||||
get iconPath(): string {
|
||||
if (this.list === 'watch-later') {
|
||||
// Horloge (Material « schedule ») : identique dans les deux états,
|
||||
// l'état se lit à la couleur (ambre quand la vidéo est enregistrée).
|
||||
return 'M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.23.2.8-1.3-4.5-2.7z';
|
||||
}
|
||||
return this.isLiked()
|
||||
? 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z'
|
||||
: 'M16.5 3c-1.74 0-3.41.81-4.5 2.09C10.91 3.81 9.24 3 7.5 3 4.42 3 2 5.42 2 8.5c0 3.78 3.4 6.86 8.55 11.54L12 21.35l1.45-1.32C18.6 15.36 22 12.28 22 8.5 22 5.42 19.58 3 16.5 3zm-4.4 15.55l-.1.1-.1-.1C7.14 14.24 4 11.39 4 8.5 4 6.5 5.5 5 7.5 5c1.54 0 3.04.99 3.57 2.36h1.87C13.46 5.99 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5 0 2.89-3.14 5.74-7.9 10.05z';
|
||||
}
|
||||
|
||||
isLiked = signal<boolean>(false);
|
||||
isLoading = signal<boolean>(true);
|
||||
@@ -126,7 +150,8 @@ export class LikeButtonComponent implements OnInit, OnDestroy, OnChanges {
|
||||
this.isLoading.set(false);
|
||||
return;
|
||||
}
|
||||
if (changes['videoId'] && !changes['videoId'].firstChange) {
|
||||
if ((changes['videoId'] && !changes['videoId'].firstChange) ||
|
||||
(changes['list'] && !changes['list'].firstChange)) {
|
||||
this.checkLikeStatus();
|
||||
}
|
||||
}
|
||||
@@ -157,7 +182,7 @@ export class LikeButtonComponent implements OnInit, OnDestroy, OnChanges {
|
||||
|
||||
this.isLoading.set(true);
|
||||
try {
|
||||
this.likesService.isLiked(this.provider, this.videoId)
|
||||
this.likesService.isLiked(this.provider, this.videoId, this.list)
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
if (!this.destroyRef) {
|
||||
@@ -193,7 +218,7 @@ export class LikeButtonComponent implements OnInit, OnDestroy, OnChanges {
|
||||
|
||||
try {
|
||||
if (this.isLiked()) {
|
||||
this.likesService.unlike(this.provider, this.videoId)
|
||||
this.likesService.unlike(this.provider, this.videoId, this.list)
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
if (!this.destroyRef) {
|
||||
@@ -217,8 +242,8 @@ export class LikeButtonComponent implements OnInit, OnDestroy, OnChanges {
|
||||
} else {
|
||||
const cleanTitle = (this.title || '').trim();
|
||||
const cleanThumb = (this.thumbnail || '').trim();
|
||||
try { console.debug('[LikeButton] like payload', { provider: this.provider, videoId: this.videoId, title: cleanTitle, hasThumb: !!cleanThumb }); } catch {}
|
||||
this.likesService.like(this.provider, this.videoId, cleanTitle || undefined, cleanThumb || undefined)
|
||||
try { console.debug('[LikeButton] tag payload', { list: this.list, provider: this.provider, videoId: this.videoId, title: cleanTitle, hasThumb: !!cleanThumb }); } catch {}
|
||||
this.likesService.like(this.provider, this.videoId, cleanTitle || undefined, cleanThumb || undefined, this.list)
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
if (!this.destroyRef) {
|
||||
|
||||
Reference in New Issue
Block a user