feat(ui): menu du compte refondu (icône + titre + sous-titre) et version officielle
CI / build-and-test (push) Successful in 15m4s
CI / build-and-test (push) Successful in 15m4s
Refonte du panneau du bouton avatar (haut à droite du header) : lignes « icône + titre gras + sous-titre gris » pour Thème (thème appliqué, pastilles dépliées au clic), Administration, API (openapi.json), Préférences, Guide d'utilisation, À propos (version + tagline + code source) et Sessions ; Déconnexion isolée après séparateur et pied de page « Version ». Ajout de src/app/version.ts (APP_VERSION, alignée sur le tag semver du registre : 1.0.59 pour cette livraison) affichée dans le menu. Clés i18n FR + EN ajoutées des deux côtés, ligne README « Menu du compte refondu » dans le roadmap.
This commit is contained in:
@@ -274,6 +274,7 @@ Légende : **✅ livré** (câblé de bout en bout) · **🟡 partiellement livr
|
||||
* ✅ **Import/Export playlists (JSON)** — `GET /playlists/export` (dump complet + items, format `newtube-playlists-v1`), `POST /playlists/import` (création des listes manquantes, dédoublonnage par titre au réimport, compteurs détaillés ; bornes 200 listes / 5 000 vidéos par liste) ; boutons **[Exporter] [Importer]** sur `/#/library/playlists`.
|
||||
* ✅ **Page Administration** (`/#/admin`, entrée « Administration » de la section Informations dans la barre latérale) — `/healthz` + `/api/providers/metrics` rendus lisibles : statut API, mode YouTube, version et santé yt-dlp, clés actives / bannies (+ suffixes), anti-ban (cookies / PO token / proxy), cache de recherche par provider, compteurs 1 h par provider, quota YouTube du jour. Au passage : `healthz` servi aussi sous `/proxy/api/healthz` (le chemin que le front utilise en prod). *Journaux (logs) et exposition Prometheus restent à faire — voir Observabilité.*
|
||||
* ✅ **Cache TTL par provider** — `server/env-ttl.mjs` (`<BASE>_<PROVIDER>` → `<BASE>` → défaut, valeurs invalides ignorées, jamais de TTL nul) branché sur les caches **details** (`DETAILS_CACHE_TTL_MS_<P>`) et **transcripts** (`TRANSCRIPT_CACHE_TTL_<P>`) en plus de la recherche (`SEARCH_CACHE_TTL_MS_<P>`). Le cache **suggest** reste global par conception : sa clé agrégée couvre plusieurs providers d'un coup, un TTL par provider demanderait de scinder le cache. Tests : `npm run test:cache`
|
||||
* ✅ **Menu du compte refondu** (bouton avatar, haut à droite du header) — panneau sombre à lignes « icône + titre gras + sous-titre gris » : **Thème** (thème réellement appliqué en sous-titre, pastilles dépliées au clic), **Administration** (`/#/admin`), **API** (`/proxy/api/openapi.json` dans un nouvel onglet), **Préférences** (`/#/account/preferences`), **Guide d'utilisation** (`/#/info/utilisation`), **À propos** (version, tagline, lien code source), **Sessions** (`/#/account/sessions`), **Déconnexion** ; pied de page **Version** = version officielle de l'application (`src/app/version.ts`, alignée sur le tag semver publié par `docker/deploy-img.sh`). Clés i18n déclarées des deux côtés (FR + EN)
|
||||
|
||||
### 🟡 Partiellement livré
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Version officielle de l'application NewTube, affichée dans le menu du compte
|
||||
* (header) et le pied de page « À propos ».
|
||||
*
|
||||
* Source unique côté front : elle suit le tag semver publié par
|
||||
* `docker/deploy-img.sh` (le registre incrémente le patch à chaque déploiement).
|
||||
* Dernier tag publié avant cette livraison : 1.0.58 → cette livraison sort en
|
||||
* 1.0.59. Bumper cette valeur à chaque livraison.
|
||||
*/
|
||||
export const APP_VERSION = '1.0.59';
|
||||
@@ -105,18 +105,140 @@
|
||||
<span class="avatar-circle rounded-full" [ngClass]="avatarClasses()">{{ avatarLetter() }}</span>
|
||||
</button>
|
||||
@if (userMenuOpen()) {
|
||||
<!-- Panneau sombre : icône + titre gras + sous-titre gris par ligne,
|
||||
action rouge séparée, pied de page « Version » sur une ligne. -->
|
||||
<div class="absolute right-0 mt-2 w-[min(20rem,calc(100vw-1rem))] overflow-hidden rounded-xl border border-slate-700 bg-slate-900 shadow-xl z-50" role="menu">
|
||||
<div class="border-b border-slate-700 px-4 py-3">
|
||||
<div class="truncate font-semibold text-slate-100">{{ user()?.username || user()?.email }}</div>
|
||||
<div class="truncate text-xs text-slate-400">{{ user()?.email }}</div>
|
||||
<div class="flex items-center gap-3 border-b border-slate-700 px-4 py-3">
|
||||
<span class="avatar-circle rounded-full shrink-0" [ngClass]="avatarClasses()">{{ avatarLetter() }}</span>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate text-sm font-semibold text-slate-100">{{ user()?.username || user()?.email }}</span>
|
||||
<span class="block truncate text-xs text-slate-400">{{ user()?.email }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="py-1">
|
||||
<a routerLink="/account/preferences" (click)="closeUserMenu()" role="menuitem" class="block px-4 py-3 text-sm text-slate-200 hover:bg-slate-800">{{ 'menu.preferences' | t }}</a>
|
||||
<div class="px-4 py-2 text-xs uppercase tracking-wide text-slate-500">{{ 'menu.appearance' | t }}</div>
|
||||
<div class="px-3 pb-3">
|
||||
<ng-container *ngTemplateOutlet="themeSwatches"></ng-container>
|
||||
</div>
|
||||
<button type="button" (click)="onLogout(); closeUserMenu()" role="menuitem" class="w-full px-4 py-3 text-left text-sm text-red-300 hover:bg-red-900/30">{{ 'menu.logout' | t }}</button>
|
||||
<!-- Thème -->
|
||||
<button type="button" (click)="menuThemeOpen.set(!menuThemeOpen())" role="menuitem"
|
||||
[attr.aria-expanded]="menuThemeOpen()"
|
||||
class="flex w-full items-center gap-3 px-4 py-2.5 text-left hover:bg-slate-800 focus-ring">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0 text-slate-400" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm0-8a3 3 0 1 0 0 6 3 3 0 0 0 0-6z"/>
|
||||
</svg>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate text-sm font-semibold text-slate-100">{{ 'menu.theme' | t }}</span>
|
||||
<span class="block truncate text-xs text-slate-400">{{ themeLabel() | t }}</span>
|
||||
</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0 text-slate-400 transition-transform" [class.rotate-180]="menuThemeOpen()"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
@if (menuThemeOpen()) {
|
||||
<div class="px-3 pb-3">
|
||||
<ng-container *ngTemplateOutlet="themeSwatches"></ng-container>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Administration -->
|
||||
<a routerLink="/admin" (click)="closeUserMenu()" role="menuitem"
|
||||
class="flex w-full items-center gap-3 px-4 py-2.5 text-left hover:bg-slate-800 focus-ring">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0 text-slate-400" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12 2 4 5v6c0 5.55 3.84 10.74 8 12 4.16-1.26 8-6.45 8-12V5l-8-2zm0 2.18L18 6.3v4.7c0 4.52-3.02 8.76-6 9.79-2.98-1.03-6-5.27-6-9.79V6.3l6-2.12z"/>
|
||||
</svg>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate text-sm font-semibold text-slate-100">{{ 'nav.admin' | t }}</span>
|
||||
<span class="block truncate text-xs text-slate-400">{{ 'menu.admin_sub' | t }}</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<!-- API -->
|
||||
<a [href]="apiDocUrl()" target="_blank" rel="noopener noreferrer" (click)="closeUserMenu()" role="menuitem"
|
||||
class="flex w-full items-center gap-3 px-4 py-2.5 text-left hover:bg-slate-800 focus-ring">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
|
||||
</svg>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate text-sm font-semibold text-slate-100">{{ 'menu.api' | t }}</span>
|
||||
<span class="block truncate text-xs text-slate-400">{{ 'menu.api_sub' | t }}</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<!-- Paramètres -->
|
||||
<a routerLink="/account/preferences" (click)="closeUserMenu()" role="menuitem"
|
||||
class="flex w-full items-center gap-3 px-4 py-2.5 text-left hover:bg-slate-800 focus-ring">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0 text-slate-400" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 15.5A3.5 3.5 0 1 1 15.5 12 3.5 3.5 0 0 1 12 15.5Zm7.43-2.53a7.6 7.6 0 0 0 0-1.94l2.05-1.58a.5.5 0 0 0 .12-.64l-1.94-3.36a.5.5 0 0 0-.6-.22l-2.43.98a7.3 7.3 0 0 0-1.67-.97l-.37-2.58A.49.49 0 0 0 14 2h-4a.49.49 0 0 0-.49.42l-.37 2.58a7.7 7.7 0 0 0-1.67.97l-2.43-.98a.5.5 0 0 0-.6.22L2.07 8.58a.5.5 0 0 0 .12.64l2.05 1.58a7.9 7.9 0 0 0 0 1.94l-2.05 1.58a.5.5 0 0 0-.12.64l1.94 3.36a.5.5 0 0 0 .6.22l2.43-.98c.52.4 1.08.73 1.67.97l.37 2.58A.49.49 0 0 0 10 22h4a.49.49 0 0 0 .49-.42l.37-2.58c.59-.24 1.15-.57 1.67-.97l2.43.98a.5.5 0 0 0 .6-.22l1.94-3.36a.5.5 0 0 0-.12-.64Z"/></svg>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate text-sm font-semibold text-slate-100">{{ 'menu.preferences' | t }}</span>
|
||||
<span class="block truncate text-xs text-slate-400">{{ 'menu.settings_sub' | t }}</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<!-- Guide d'utilisation -->
|
||||
<a routerLink="/info/utilisation" (click)="closeUserMenu()" role="menuitem"
|
||||
class="flex w-full items-center gap-3 px-4 py-2.5 text-left hover:bg-slate-800 focus-ring">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
|
||||
</svg>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate text-sm font-semibold text-slate-100">{{ 'menu.guide' | t }}</span>
|
||||
<span class="block truncate text-xs text-slate-400">{{ 'menu.guide_sub' | t }}</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<!-- À propos -->
|
||||
<button type="button" (click)="menuAboutOpen.set(!menuAboutOpen())" role="menuitem"
|
||||
[attr.aria-expanded]="menuAboutOpen()"
|
||||
class="flex w-full items-center gap-3 px-4 py-2.5 text-left hover:bg-slate-800 focus-ring">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0 text-slate-400" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12 3l1.9 4.6L18.5 9.5l-4.6 1.9L12 16l-1.9-4.6L5.5 9.5l4.6-1.9L12 3Zm7 8 .9 2.1L22 14l-2.1.9L19 17l-.9-2.1L16 14l2.1-.9L19 11Z"/>
|
||||
</svg>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate text-sm font-semibold text-slate-100">{{ 'menu.about' | t }}</span>
|
||||
<span class="block truncate text-xs text-slate-400">{{ 'menu.about_sub' | t }}</span>
|
||||
</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0 text-slate-400 transition-transform" [class.rotate-180]="menuAboutOpen()"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
@if (menuAboutOpen()) {
|
||||
<div class="px-4 pb-3 pt-1 text-xs leading-relaxed text-slate-400">
|
||||
<p class="font-semibold text-slate-200">NewTube {{ appVersion }}</p>
|
||||
<p>{{ 'about.tagline' | t }}</p>
|
||||
<a href="https://git.dracodev.net/Projets/NewTube" target="_blank" rel="noopener noreferrer"
|
||||
class="mt-1 inline-block text-slate-300 underline underline-offset-2 hover:text-slate-100 focus-ring">{{ 'about.source' | t }}</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Sessions -->
|
||||
<a routerLink="/account/sessions" (click)="closeUserMenu()" role="menuitem"
|
||||
class="flex w-full items-center gap-3 px-4 py-2.5 text-left hover:bg-slate-800 focus-ring">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>
|
||||
</svg>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate text-sm font-semibold text-slate-100">{{ 'menu.sessions' | t }}</span>
|
||||
<span class="block truncate text-xs text-slate-400">{{ 'menu.sessions_sub' | t }}</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Déconnexion -->
|
||||
<div class="border-t border-slate-700 py-1">
|
||||
<button type="button" (click)="onLogout(); closeUserMenu()" role="menuitem"
|
||||
class="flex w-full items-center gap-3 px-4 py-2.5 text-left text-red-300 hover:bg-red-900/30 focus-ring">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>
|
||||
</svg>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate text-sm font-semibold">{{ 'menu.logout' | t }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Pied de page : version officielle -->
|
||||
<div class="flex items-center gap-2 border-t border-slate-700 bg-slate-800/40 px-4 py-2.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M20.59 13.41 12 22l-9-9V3h10l7.59 7.59a2 2 0 0 1 0 2.82Z"/><line x1="7" y1="7" x2="7.01" y2="7"/>
|
||||
</svg>
|
||||
<span class="text-xs font-semibold text-slate-200">{{ 'menu.version' | t }}</span>
|
||||
<span class="ml-auto rounded bg-slate-700/60 px-2 py-0.5 text-xs font-medium text-slate-200">NewTube {{ appVersion }}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { HistoryService, SearchHistoryItem } from '../../services/history.servic
|
||||
import { SearchBoxComponent } from '../search/search-box.component';
|
||||
import { filtersToQueryParams, type SearchFilters } from '../../app/search/filters';
|
||||
import { ALL_PROVIDER_IDS_CSV, type ProviderId } from '../../app/shared/providers/provider-ids';
|
||||
import { APP_VERSION } from '../../app/version';
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
@@ -316,6 +317,15 @@ export class HeaderComponent implements AfterViewInit {
|
||||
authError = signal<string | null>(null);
|
||||
userMenuOpen = signal(false);
|
||||
|
||||
// --- Menu du compte (header, desktop) ------------------------------------
|
||||
// Deux lignes dépliables : la pastille de thème et le bloc « À propos ».
|
||||
readonly menuThemeOpen = signal(false);
|
||||
readonly menuAboutOpen = signal(false);
|
||||
/** Version officielle affichée dans le pied de page du menu. */
|
||||
readonly appVersion = APP_VERSION;
|
||||
/** Sous-titre de la ligne « Thème » : le thème réellement appliqué. */
|
||||
readonly themeLabel = computed(() => 'theme.' + this.resolveThemeSlug(this.currentTheme()));
|
||||
|
||||
// --- Mobile chrome -------------------------------------------------------
|
||||
// Slide-down search sheet (phones). Desktop keeps the search box inline, so
|
||||
// the same <app-search-box> is only *positioned* differently by CSS.
|
||||
@@ -659,11 +669,20 @@ export class HeaderComponent implements AfterViewInit {
|
||||
toggleUserMenu() {
|
||||
this.userMenuOpen.update(v => !v);
|
||||
if (this.userMenuOpen()) {
|
||||
// Repli propre à chaque ouverture : aucun panneau déplié de la session précédente.
|
||||
this.menuThemeOpen.set(false);
|
||||
this.menuAboutOpen.set(false);
|
||||
this.moreMenuOpen.set(false);
|
||||
this.mobileSearchOpen.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
/** Doc OpenAPI : même règle /proxy/api que les services (dev sur 4200 → proxy Angular). */
|
||||
apiDocUrl(): string {
|
||||
const port = typeof window !== 'undefined' ? (window.location.port || '') : '';
|
||||
return `${port && port !== '4000' ? '/proxy/api' : '/api'}/openapi.json`;
|
||||
}
|
||||
|
||||
@HostListener('document:click', ['$event'])
|
||||
onDocumentClick(ev: MouseEvent) {
|
||||
if (this.userMenuOpen()) {
|
||||
|
||||
@@ -28,6 +28,19 @@ const EN: Record<string, string> = {
|
||||
'menu.close': 'Close',
|
||||
'menu.navigation': 'Navigation',
|
||||
'menu.theme': 'Theme',
|
||||
'menu.admin_sub': 'Health & provider metrics',
|
||||
'menu.api': 'API',
|
||||
'menu.api_sub': 'OpenAPI documentation',
|
||||
'menu.settings_sub': 'Application preferences',
|
||||
'menu.guide': 'Usage guide',
|
||||
'menu.guide_sub': 'Open help',
|
||||
'menu.about': 'About',
|
||||
'menu.about_sub': 'Version, credits & info',
|
||||
'menu.sessions': 'Sessions',
|
||||
'menu.sessions_sub': 'Connected devices',
|
||||
'menu.version': 'Version',
|
||||
'about.tagline': 'Multi-provider video aggregator',
|
||||
'about.source': 'Source code',
|
||||
'theme.system': 'System',
|
||||
'theme.black': 'Black',
|
||||
'theme.dark': 'Dark',
|
||||
@@ -224,6 +237,19 @@ const FR: Record<string, string> = {
|
||||
'menu.close': 'Fermer',
|
||||
'menu.navigation': 'Navigation',
|
||||
'menu.theme': 'Thème',
|
||||
'menu.admin_sub': 'Santé & métriques des fournisseurs',
|
||||
'menu.api': 'API',
|
||||
'menu.api_sub': 'Documentation OpenAPI',
|
||||
'menu.settings_sub': 'Paramètres de l\'application',
|
||||
'menu.guide': 'Guide d\'utilisation',
|
||||
'menu.guide_sub': 'Ouvrir l\'aide',
|
||||
'menu.about': 'À propos',
|
||||
'menu.about_sub': 'Version, crédits & infos',
|
||||
'menu.sessions': 'Sessions',
|
||||
'menu.sessions_sub': 'Appareils connectés',
|
||||
'menu.version': 'Version',
|
||||
'about.tagline': 'Agrégateur vidéo multi-fournisseurs',
|
||||
'about.source': 'Code source',
|
||||
'theme.system': 'Système',
|
||||
'theme.black': 'Noir',
|
||||
'theme.dark': 'Sombre',
|
||||
|
||||
Reference in New Issue
Block a user