feat(sidebar): accordeon unique strict, Plus/Moins, scroll au survol
CI / build-and-test (push) Failing after 8m17s

This commit is contained in:
2026-09-26 11:20:54 -04:00
parent 49c3b5dfdf
commit 1b0a7eb3ce
4 changed files with 272 additions and 137 deletions
+23
View File
@@ -209,3 +209,26 @@ body {
/* Hide scrollbars on horizontal lists where we still want scrolling */
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
/* --- Sidebar: scrollbar visible au survol / focus uniquement --------------- */
.sidebar-scroll {
overflow-y: hidden;
scrollbar-width: thin;
scrollbar-color: #334155 transparent;
scrollbar-gutter: stable;
}
.sidebar-scroll:hover,
.sidebar-scroll:focus,
.sidebar-scroll:focus-within {
overflow-y: auto;
}
.sidebar-scroll::-webkit-scrollbar { width: 6px; }
.sidebar-scroll::-webkit-scrollbar-track { background: transparent; }
.sidebar-scroll::-webkit-scrollbar-thumb { background: #334155; border-radius: 9999px; }
@media (hover: none) {
/* tactile / mobile : toujours scrollable */
.sidebar-scroll { overflow-y: auto; }
}
@media (prefers-reduced-motion: reduce) {
.sidebar-scroll { scroll-behavior: auto; }
}
+168 -135
View File
@@ -1,4 +1,4 @@
<nav class="h-full w-full bg-slate-900 md:bg-transparent text-slate-200 p-3 md:p-0 overflow-y-auto">
<nav tabindex="0" class="sidebar-scroll h-full w-full bg-slate-900 md:bg-transparent text-slate-200 p-3 md:p-0">
<div class="space-y-6">
<!-- Brand (small only) -->
<div class="md:hidden flex items-center gap-2 px-3 py-2">
@@ -6,7 +6,7 @@
<span class="text-xl font-bold">NewTube</span>
</div>
<!-- Main -->
<!-- Main : toujours visible, hors accordeon -->
<div>
<ul class="space-y-1">
<li>
@@ -33,161 +33,194 @@
<!-- Vous -->
@if (isLoggedIn()) {
<div>
<div class="px-3 py-2 text-xs uppercase tracking-wide text-slate-400" [class.hidden]="collapsed">{{ 'nav.you' | t }}</div>
<ul class="space-y-1">
<li>
<a routerLink="/library/subscriptions" routerLinkActive="bg-slate-800" class="flex items-center px-3 py-2 rounded hover:bg-slate-800 transition" [ngClass]="{ 'gap-0 justify-center': collapsed, 'gap-3': !collapsed }" [attr.title]="collapsed ? ('nav.subscriptions' | t) : null">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24" fill="currentColor"><path d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2zm0 2v12h16V6H4zm3 3h10v2H7V9zm0 4h7v2H7v-2z"/></svg>
<span [class.hidden]="collapsed">{{ 'nav.subscriptions' | t }}</span>
</a>
</li>
<li>
<a routerLink="/account/history" routerLinkActive="bg-slate-800" class="flex items-center px-3 py-2 rounded hover:bg-slate-800 transition" [ngClass]="{ 'gap-0 justify-center': collapsed, 'gap-3': !collapsed }" [attr.title]="collapsed ? ('nav.history' | t) : null">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24" fill="currentColor"><path d="M12 8a1 1 0 0 1 1 1v3.38l2.32 1.34a1 1 0 1 1-1 1.74l-2.82-1.63A1 1 0 0 1 11 13V9a1 1 0 0 1 1-1zm0-6a10 10 0 1 0 10 10 10.011 10.011 0 0 0-10-10z"/></svg>
<span [class.hidden]="collapsed">{{ 'nav.history' | t }}</span>
</a>
</li>
<li>
<a routerLink="/library/liked" routerLinkActive="bg-slate-800" class="flex items-center px-3 py-2 rounded hover:bg-slate-800 transition" [ngClass]="{ 'gap-0 justify-center': collapsed, 'gap-3': !collapsed }" [attr.title]="collapsed ? ('nav.liked' | t) : null">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24" fill="currentColor"><path d="M12 21s-6.716-4.248-9.193-6.725A6 6 0 0 1 12 5.414 6 6 0 0 1 21.193 14.275C18.716 16.752 12 21 12 21z"/></svg>
<span [class.hidden]="collapsed">{{ 'nav.liked' | t }}</span>
</a>
</li>
<li>
<a routerLink="/library/downloads" routerLinkActive="bg-slate-800" class="flex items-center px-3 py-2 rounded hover:bg-slate-800 transition" [ngClass]="{ 'gap-0 justify-center': collapsed, 'gap-3': !collapsed }" [attr.title]="collapsed ? ('nav.downloads' | t) : null">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24" fill="currentColor"><path d="M12 3v10.17l3.59-3.58L17 11l-5 5-5-5 1.41-1.41L12 13.17V3h0zm-7 15h14v2H5v-2z"/></svg>
<span [class.hidden]="collapsed">{{ 'nav.downloads' | t }}</span>
</a>
</li>
</ul>
<button type="button" (click)="toggleSection('you')" [attr.aria-expanded]="isOpen('you')" aria-controls="sidebar-section-you" class="flex w-full items-center justify-between px-3 py-2 text-xs uppercase tracking-wide text-slate-400 hover:text-slate-200 transition" [class.hidden]="collapsed">
<span>{{ 'nav.you' | t }}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 transition-transform" [class.rotate-180]="isOpen('you')" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 10l5 5 5-5z"/></svg>
</button>
<div id="sidebar-section-you" [hidden]="!collapsed && !isOpen('you')">
<ul class="space-y-1">
<li>
<a routerLink="/library/subscriptions" routerLinkActive="bg-slate-800" class="flex items-center px-3 py-2 rounded hover:bg-slate-800 transition" [ngClass]="{ 'gap-0 justify-center': collapsed, 'gap-3': !collapsed }" [attr.title]="collapsed ? ('nav.subscriptions' | t) : null">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24" fill="currentColor"><path d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2zm0 2v12h16V6H4zm3 3h10v2H7V9zm0 4h7v2H7v-2z"/></svg>
<span [class.hidden]="collapsed">{{ 'nav.subscriptions' | t }}</span>
</a>
</li>
<li>
<a routerLink="/account/history" routerLinkActive="bg-slate-800" class="flex items-center px-3 py-2 rounded hover:bg-slate-800 transition" [ngClass]="{ 'gap-0 justify-center': collapsed, 'gap-3': !collapsed }" [attr.title]="collapsed ? ('nav.history' | t) : null">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24" fill="currentColor"><path d="M12 8a1 1 0 0 1 1 1v3.38l2.32 1.34a1 1 0 1 1-1 1.74l-2.82-1.63A1 1 0 0 1 11 13V9a1 1 0 0 1 1-1zm0-6a10 10 0 1 0 10 10 10.011 10.011 0 0 0-10-10z"/></svg>
<span [class.hidden]="collapsed">{{ 'nav.history' | t }}</span>
</a>
</li>
<li>
<a routerLink="/library/liked" routerLinkActive="bg-slate-800" class="flex items-center px-3 py-2 rounded hover:bg-slate-800 transition" [ngClass]="{ 'gap-0 justify-center': collapsed, 'gap-3': !collapsed }" [attr.title]="collapsed ? ('nav.liked' | t) : null">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24" fill="currentColor"><path d="M12 21s-6.716-4.248-9.193-6.725A6 6 0 0 1 12 5.414 6 6 0 0 1 21.193 14.275C18.716 16.752 12 21 12 21z"/></svg>
<span [class.hidden]="collapsed">{{ 'nav.liked' | t }}</span>
</a>
</li>
<li>
<a routerLink="/library/downloads" routerLinkActive="bg-slate-800" class="flex items-center px-3 py-2 rounded hover:bg-slate-800 transition" [ngClass]="{ 'gap-0 justify-center': collapsed, 'gap-3': !collapsed }" [attr.title]="collapsed ? ('nav.downloads' | t) : null">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24" fill="currentColor"><path d="M12 3v10.17l3.59-3.58L17 11l-5 5-5-5 1.41-1.41L12 13.17V3h0zm-7 15h14v2H5v-2z"/></svg>
<span [class.hidden]="collapsed">{{ 'nav.downloads' | t }}</span>
</a>
</li>
</ul>
</div>
</div>
<!-- Groupes d'abonnements (façon PocketTube) -->
<div *ngIf="!collapsed && subscriptionGroups().length > 0">
<div class="px-3 py-2 text-xs uppercase tracking-wide text-slate-400">{{ 'nav.groups' | t : 'Groupes' }}</div>
<ul class="space-y-1">
<li *ngFor="let group of subscriptionGroups()">
<a [routerLink]="['/library/subscriptions']" [queryParams]="{ group: group.id, tab: 'channels' }" routerLinkActive="bg-slate-800" class="flex items-center px-3 py-2 rounded hover:bg-slate-800 transition gap-3">
<span class="flex h-6 w-6 items-center justify-center rounded text-base" [style.background-color]="group.color + '26'">{{ group.icon }}</span>
<span class="flex-1 truncate text-sm">{{ group.name }}</span>
<span class="text-xs text-slate-500">{{ group.channelCount }}</span>
</a>
</li>
</ul>
<button type="button" (click)="toggleSection('groups')" [attr.aria-expanded]="isOpen('groups')" aria-controls="sidebar-section-groups" class="flex w-full items-center justify-between px-3 py-2 text-xs uppercase tracking-wide text-slate-400 hover:text-slate-200 transition">
<span>{{ 'nav.groups' | t }} <span class="text-slate-500">({{ subscriptionGroups().length }})</span></span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 transition-transform" [class.rotate-180]="isOpen('groups')" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 10l5 5 5-5z"/></svg>
</button>
<div id="sidebar-section-groups" [hidden]="!isOpen('groups')">
<ul class="space-y-1">
<li *ngFor="let group of displayedGroups()">
<a [routerLink]="['/library/subscriptions']" [queryParams]="{ group: group.id, tab: 'channels' }" routerLinkActive="bg-slate-800" class="flex items-center px-3 py-2 rounded hover:bg-slate-800 transition gap-3">
<span class="flex h-6 w-6 items-center justify-center rounded text-base" [style.background-color]="group.color + '26'">{{ group.icon }}</span>
<span class="flex-1 truncate text-sm">{{ group.name }}</span>
<span class="text-xs text-slate-500">{{ group.channelCount }}</span>
</a>
</li>
</ul>
<button *ngIf="subscriptionGroups().length > 5" type="button" class="flex items-center gap-2 px-3 py-2 text-sm text-slate-300 hover:text-white" (click)="toggleGroups()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 transition-transform" [class.rotate-180]="showAllGroups()" viewBox="0 0 24 24" fill="currentColor"><path d="M7 10l5 5 5-5z"/></svg>
<span>{{ showAllGroups() ? ('common.less' | t) : ('common.more' | t) }}</span>
</button>
</div>
</div>
}
<!-- Fournisseurs -->
<div>
<div class="px-3 py-2 text-xs uppercase tracking-wide text-slate-400" [class.hidden]="collapsed">Fournisseurs</div>
<ul class="space-y-1">
<!-- All providers except PeerTube (order handled in TS so PT is last) -->
<ng-container *ngFor="let p of providersList()">
<li *ngIf="p.id !== 'peertube'">
<a [routerLink]="['/p', p.id, 't', 'trending']" class="flex items-center px-3 py-2 rounded hover:bg-slate-800 transition" [ngClass]="{ 'gap-0 justify-center': collapsed, 'gap-3': !collapsed }" [attr.title]="collapsed ? (p.label) : null">
<span class="h-6 w-6 flex items-center justify-center">
<ng-container [ngSwitch]="p.id">
<!-- YouTube -->
<svg *ngSwitchCase="'youtube'" class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<rect x="3" y="6" width="18" height="12" rx="3" class="text-red-600" fill="currentColor"/>
<polygon points="10,9 16,12 10,15" fill="white"/>
</svg>
<!-- Dailymotion -->
<svg *ngSwitchCase="'dailymotion'" class="h-5 w-5 text-blue-500" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<circle cx="12" cy="12" r="10" fill="currentColor"/>
<polygon points="11,8 16,12 11,16" fill="white"/>
</svg>
<!-- Twitch -->
<svg *ngSwitchCase="'twitch'" class="h-5 w-5 text-purple-500" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M4 3h16v11l-4 4h-3l-2 2H9v-2H6V3zm3 2v9h6l2-2h3V5H7z" fill="currentColor"/>
</svg>
<!-- Rumble -->
<svg *ngSwitchCase="'rumble'" class="h-5 w-5 text-green-500" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="10" fill="currentColor"/>
<polygon points="10,8 16,12 10,16" fill="white"/>
</svg>
<!-- Odysee -->
<svg *ngSwitchCase="'odysee'" class="h-5 w-5 text-pink-500" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="10" fill="currentColor"/>
<circle cx="12" cy="12" r="5" fill="white"/>
</svg>
<!-- Fallback generic play icon -->
<svg *ngSwitchDefault class="h-5 w-5 text-slate-300" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<polygon points="9,7 17,12 9,17" fill="currentColor"/>
</svg>
</ng-container>
</span>
<span [class.hidden]="collapsed">{{ p.label }}</span>
</a>
</li>
</ng-container>
<button type="button" (click)="toggleSection('providers')" [attr.aria-expanded]="isOpen('providers')" aria-controls="sidebar-section-providers" class="flex w-full items-center justify-between px-3 py-2 text-xs uppercase tracking-wide text-slate-400 hover:text-slate-200 transition" [class.hidden]="collapsed">
<span>{{ 'nav.providers' | t }} <span class="text-slate-500">({{ providersList().length }})</span></span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 transition-transform" [class.rotate-180]="isOpen('providers')" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 10l5 5 5-5z"/></svg>
</button>
<div id="sidebar-section-providers" [hidden]="!collapsed && !isOpen('providers')">
<ul class="space-y-1">
<!-- All providers except PeerTube (order handled in TS so PT is last) -->
<ng-container *ngFor="let p of providersList()">
<li *ngIf="p.id !== 'peertube'">
<a [routerLink]="['/p', p.id, 't', 'trending']" class="flex items-center px-3 py-2 rounded hover:bg-slate-800 transition" [ngClass]="{ 'gap-0 justify-center': collapsed, 'gap-3': !collapsed }" [attr.title]="collapsed ? (p.label) : null">
<span class="h-6 w-6 flex items-center justify-center">
<ng-container [ngSwitch]="p.id">
<!-- YouTube -->
<svg *ngSwitchCase="'youtube'" class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<rect x="3" y="6" width="18" height="12" rx="3" class="text-red-600" fill="currentColor"/>
<polygon points="10,9 16,12 10,15" fill="white"/>
</svg>
<!-- Dailymotion -->
<svg *ngSwitchCase="'dailymotion'" class="h-5 w-5 text-blue-500" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<circle cx="12" cy="12" r="10" fill="currentColor"/>
<polygon points="11,8 16,12 11,16" fill="white"/>
</svg>
<!-- Twitch -->
<svg *ngSwitchCase="'twitch'" class="h-5 w-5 text-purple-500" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M4 3h16v11l-4 4h-3l-2 2H9v-2H6V3zm3 2v9h6l2-2h3V5H7z" fill="currentColor"/>
</svg>
<!-- Rumble -->
<svg *ngSwitchCase="'rumble'" class="h-5 w-5 text-green-500" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="10" fill="currentColor"/>
<polygon points="10,8 16,12 10,16" fill="white"/>
</svg>
<!-- Odysee -->
<svg *ngSwitchCase="'odysee'" class="h-5 w-5 text-pink-500" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="10" fill="currentColor"/>
<circle cx="12" cy="12" r="5" fill="white"/>
</svg>
<!-- Fallback generic play icon -->
<svg *ngSwitchDefault class="h-5 w-5 text-slate-300" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<polygon points="9,7 17,12 9,17" fill="currentColor"/>
</svg>
</ng-container>
</span>
<span [class.hidden]="collapsed">{{ p.label }}</span>
</a>
</li>
</ng-container>
<!-- PeerTube group with instances as sub-items -->
<ng-container *ngFor="let p of providersList()">
<li [ngClass]="{ 'mt-2': true }" *ngIf="p.id === 'peertube'">
<a [routerLink]="['/p', 'peertube', 't', 'trending']" class="flex items-center px-3 py-2 rounded hover:bg-slate-800 transition" [ngClass]="{ 'gap-0 justify-center': collapsed, 'gap-3': !collapsed }" [attr.title]="collapsed ? (p.label) : null">
<span class="h-6 w-6 flex items-center justify-center">
<!-- PeerTube logo style (orange play) -->
<svg class="h-5 w-5 text-orange-500" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="10" fill="currentColor"/>
<polygon points="10,8 16,12 10,16" fill="white"/>
</svg>
</span>
<span [class.hidden]="collapsed">{{ p.label }}</span>
</a>
<!-- Sub-list of instances (visible when not collapsed) -->
<ul class="ml-8 mt-1 space-y-1" [class.hidden]="collapsed">
<li *ngFor="let inst of peerTubeInstances()">
<a [routerLink]="['/p', 'peertube', 't', 'trending']"
(click)="$event.preventDefault(); selectPeerTubeInstance(inst)"
class="flex items-center px-3 py-1.5 rounded hover:bg-slate-800 transition gap-2">
<svg class="h-4 w-4 text-orange-500" viewBox="0 0 24 24" aria-hidden="true">
<!-- PeerTube group with instances as sub-items -->
<ng-container *ngFor="let p of providersList()">
<li [ngClass]="{ 'mt-2': true }" *ngIf="p.id === 'peertube'">
<a [routerLink]="['/p', 'peertube', 't', 'trending']" class="flex items-center px-3 py-2 rounded hover:bg-slate-800 transition" [ngClass]="{ 'gap-0 justify-center': collapsed, 'gap-3': !collapsed }" [attr.title]="collapsed ? (p.label) : null">
<span class="h-6 w-6 flex items-center justify-center">
<!-- PeerTube logo style (orange play) -->
<svg class="h-5 w-5 text-orange-500" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="10" fill="currentColor"/>
<polygon points="10,8 16,12 10,16" fill="white"/>
</svg>
<span class="text-xs" [ngClass]="{ 'text-sky-400': inst === activePeerTubeInstance(), 'text-slate-300': inst !== activePeerTubeInstance() }">{{ inst }}</span>
</a>
</li>
</ul>
</li>
</ng-container>
</ul>
</span>
<span [class.hidden]="collapsed">{{ p.label }}</span>
</a>
<!-- Sub-list of instances (visible when not collapsed) -->
<ul class="ml-8 mt-1 space-y-1" [class.hidden]="collapsed">
<li *ngFor="let inst of displayedInstances()">
<a [routerLink]="['/p', 'peertube', 't', 'trending']"
(click)="$event.preventDefault(); selectPeerTubeInstance(inst)"
class="flex items-center px-3 py-1.5 rounded hover:bg-slate-800 transition gap-2">
<svg class="h-4 w-4 text-orange-500" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="10" fill="currentColor"/>
<polygon points="10,8 16,12 10,16" fill="white"/>
</svg>
<span class="text-xs" [ngClass]="{ 'text-sky-400': inst === activePeerTubeInstance(), 'text-slate-300': inst !== activePeerTubeInstance() }">{{ inst }}</span>
</a>
</li>
</ul>
<button *ngIf="!collapsed && peerTubeInstances().length > 3" type="button" class="ml-8 flex items-center gap-2 px-3 py-2 text-sm text-slate-300 hover:text-white" (click)="toggleInstances()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 transition-transform" [class.rotate-180]="showAllInstances()" viewBox="0 0 24 24" fill="currentColor"><path d="M7 10l5 5 5-5z"/></svg>
<span>{{ showAllInstances() ? ('common.less' | t) : ('common.more' | t) }}</span>
</button>
</li>
</ng-container>
</ul>
</div>
</div>
<!-- Explorer (themes list with more/less) -->
<div>
<div class="px-3 py-2 text-xs uppercase tracking-wide text-slate-400" [class.hidden]="collapsed">{{ 'nav.explore' | t }}</div>
<ul class="space-y-1">
<li *ngFor="let t of displayedThemes()">
<a [routerLink]="['/t', t.slug]" class="flex items-center px-3 py-2 rounded hover:bg-slate-800 transition" [ngClass]="{ 'gap-0 justify-center': collapsed, 'gap-3': !collapsed }" [attr.title]="collapsed ? (t.label) : null">
<span class="h-6 w-6 flex items-center justify-center text-lg">{{ t.emoji }}</span>
<span [class.hidden]="collapsed">{{ t.label }}</span>
</a>
</li>
</ul>
<button class="flex items-center gap-2 px-3 py-2 text-sm text-slate-300 hover:text-white" (click)="toggleThemes()" [class.hidden]="collapsed">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor"><path d="M12 5l7 7-7 7-1.41-1.41L15.17 13H5v-2h10.17l-4.58-4.59L12 5z"/></svg>
<span>{{ showAllThemes() ? ('common.less' | t) : ('common.more' | t) }}</span>
<button type="button" (click)="toggleSection('explore')" [attr.aria-expanded]="isOpen('explore')" aria-controls="sidebar-section-explore" class="flex w-full items-center justify-between px-3 py-2 text-xs uppercase tracking-wide text-slate-400 hover:text-slate-200 transition" [class.hidden]="collapsed">
<span>{{ 'nav.explore' | t }} <span class="text-slate-500">({{ themes.themes().length }})</span></span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 transition-transform" [class.rotate-180]="isOpen('explore')" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 10l5 5 5-5z"/></svg>
</button>
<div id="sidebar-section-explore" [hidden]="!collapsed && !isOpen('explore')">
<ul class="space-y-1">
<li *ngFor="let t of displayedThemes()">
<a [routerLink]="['/t', t.slug]" class="flex items-center px-3 py-2 rounded hover:bg-slate-800 transition" [ngClass]="{ 'gap-0 justify-center': collapsed, 'gap-3': !collapsed }" [attr.title]="collapsed ? (t.label) : null">
<span class="h-6 w-6 flex items-center justify-center text-lg">{{ t.emoji }}</span>
<span [class.hidden]="collapsed">{{ t.label }}</span>
</a>
</li>
</ul>
<button type="button" class="flex items-center gap-2 px-3 py-2 text-sm text-slate-300 hover:text-white" (click)="toggleThemes()" [class.hidden]="collapsed">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 transition-transform" [class.rotate-180]="showAllThemes()" viewBox="0 0 24 24" fill="currentColor"><path d="M7 10l5 5 5-5z"/></svg>
<span>{{ showAllThemes() ? ('common.less' | t) : ('common.more' | t) }}</span>
</button>
</div>
</div>
<!-- Informations -->
<div>
<div class="px-3 py-2 text-xs uppercase tracking-wide text-slate-400" [class.hidden]="collapsed">{{ 'nav.info' | t : 'Informations' }}</div>
<ul class="space-y-1">
<li>
<a routerLink="/info/utilisation" routerLinkActive="bg-slate-800" class="flex items-center px-3 py-2 rounded hover:bg-slate-800 transition" [ngClass]="{ 'gap-0 justify-center': collapsed, 'gap-3': !collapsed }" [attr.title]="collapsed ? ('nav.usage' | t : 'Utilisation') : null">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2a10 10 0 1 0 10 10A10.011 10.011 0 0 0 12 2zm1 15h-2v-6h2zm0-8h-2V7h2z"/></svg>
<span [class.hidden]="collapsed">{{ 'nav.usage' | t : 'Utilisation' }}</span>
</a>
</li>
<li>
<a href="https://git.dracodev.net/Projets/NewTube" target="_blank" rel="noopener noreferrer" class="flex items-center px-3 py-2 rounded hover:bg-slate-800 transition" [ngClass]="{ 'gap-0 justify-center': collapsed, 'gap-3': !collapsed }" [attr.title]="collapsed ? 'Gitea' : null">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2a10 10 0 1 0 10 10A10.011 10.011 0 0 0 12 2zm1 14.59V20h-2v-3.41a4.994 4.994 0 0 1-4-4.9V7h2v4.69A3 3 0 0 0 12 14a3 3 0 0 0 3-2.31V7h2v4.69a4.994 4.994 0 0 1-4 4.9z"/></svg>
<span [class.hidden]="collapsed">Gitea</span>
</a>
</li>
</ul>
<button type="button" (click)="toggleSection('info')" [attr.aria-expanded]="isOpen('info')" aria-controls="sidebar-section-info" class="flex w-full items-center justify-between px-3 py-2 text-xs uppercase tracking-wide text-slate-400 hover:text-slate-200 transition" [class.hidden]="collapsed">
<span>{{ 'nav.info' | t }}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 transition-transform" [class.rotate-180]="isOpen('info')" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 10l5 5 5-5z"/></svg>
</button>
<div id="sidebar-section-info" [hidden]="!collapsed && !isOpen('info')">
<ul class="space-y-1">
<li>
<a routerLink="/info/utilisation" routerLinkActive="bg-slate-800" class="flex items-center px-3 py-2 rounded hover:bg-slate-800 transition" [ngClass]="{ 'gap-0 justify-center': collapsed, 'gap-3': !collapsed }" [attr.title]="collapsed ? ('nav.usage' | t) : null">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2a10 10 0 1 0 10 10A10.011 10.011 0 0 0 12 2zm1 15h-2v-6h2zm0-8h-2V7h2z"/></svg>
<span [class.hidden]="collapsed">{{ 'nav.usage' | t }}</span>
</a>
</li>
<li>
<a href="https://git.dracodev.net/Projets/NewTube" target="_blank" rel="noopener noreferrer" class="flex items-center px-3 py-2 rounded hover:bg-slate-800 transition" [ngClass]="{ 'gap-0 justify-center': collapsed, 'gap-3': !collapsed }" [attr.title]="collapsed ? 'Gitea' : null">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2a10 10 0 1 0 10 10A10.011 10.011 0 0 0 12 2zm1 14.59V20h-2v-3.41a4.994 4.994 0 0 1-4-4.9V7h2v4.69A3 3 0 0 0 12 14a3 3 0 0 0 3-2.31V7h2v4.69a4.994 4.994 0 0 1-4 4.9z"/></svg>
<span [class.hidden]="collapsed">Gitea</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</nav>
+75 -2
View File
@@ -1,12 +1,14 @@
import { ChangeDetectionStrategy, Component, computed, effect, inject, Input } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, effect, inject, Input, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink, RouterLinkActive } from '@angular/router';
import { NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router';
import { InstanceService } from '../../services/instance.service';
import { ThemesService } from '../../services/themes.service';
import { AuthService } from '../../services/auth.service';
import { SubscriptionsService } from '../../services/subscriptions.service';
import { TranslatePipe } from '../../pipes/translate.pipe';
export type SidebarSectionId = 'you' | 'groups' | 'providers' | 'explore' | 'info';
@Component({
selector: 'app-sidebar',
standalone: true,
@@ -18,6 +20,7 @@ export class SidebarComponent {
private instances = inject(InstanceService);
private auth = inject(AuthService);
private subs = inject(SubscriptionsService);
private router = inject(Router, { optional: true });
themes = inject(ThemesService);
// Check if user is logged in
@@ -30,6 +33,13 @@ export class SidebarComponent {
this.subs.listGroups().subscribe();
}
});
// Auto-ouvre la section contenant la route active (accordeon unique strict).
try {
this.openFromUrl(this.router?.url, false);
this.router?.events.subscribe(e => {
if (e instanceof NavigationEnd) this.openFromUrl(e.urlAfterRedirects || e.url, true);
});
} catch {}
}
provider = computed(() => this.instances.selectedProvider());
@@ -47,6 +57,69 @@ export class SidebarComponent {
displayedThemes = computed(() => this.showAllThemes() ? this.themes.themes() : this.themes.themes().slice(0, 8));
toggleThemes() { this.themes.toggleShowAll(); }
// --- Accordeon unique strict (une seule section ouverte a la fois) ---
private static readonly OPEN_SECTION_KEY = 'newtube.sidebar.openSection';
private static readonly GROUP_LIMIT = 5;
private static readonly INSTANCE_LIMIT = 3;
openSection = signal<SidebarSectionId | null>((() => {
try {
const raw = localStorage.getItem(SidebarComponent.OPEN_SECTION_KEY) as SidebarSectionId | null;
if (raw === 'you' || raw === 'groups' || raw === 'providers' || raw === 'explore' || raw === 'info') return raw;
} catch {}
return 'providers';
})());
isOpen(id: SidebarSectionId): boolean {
return this.openSection() === id;
}
toggleSection(id: SidebarSectionId): void {
const next = this.openSection() === id ? null : id;
this.openSection.set(next);
try {
if (next) localStorage.setItem(SidebarComponent.OPEN_SECTION_KEY, next);
else localStorage.removeItem(SidebarComponent.OPEN_SECTION_KEY);
} catch {}
}
/** Ouvre automatiquement la section contenant la route active.
Routes non mappees (/shorts, /watch, /search...) : ne touche a rien,
le choix manuel persiste s'applique. */
private openFromUrl(url: string | undefined, persist = true): void {
if (!url) return;
const pathOnly = url.split('?')[0].split('#')[0] || '/';
let next: SidebarSectionId | null = null;
if (pathOnly.startsWith('/library/subscriptions') && url.includes('group')) next = 'groups';
else if (pathOnly.startsWith('/library') || pathOnly.startsWith('/account')) next = 'you';
else if (pathOnly.startsWith('/p/')) next = 'providers';
else if (pathOnly === '/' || pathOnly === '' || pathOnly.startsWith('/t/')) next = 'explore';
else if (pathOnly.startsWith('/info')) next = 'info';
else return; // /shorts, /watch, /search... : ne touche a rien
if (this.collapsed) return; // en mode reduit les sections restent a plat (icones)
if (this.openSection() !== next) {
this.openSection.set(next);
try {
if (persist && next) localStorage.setItem(SidebarComponent.OPEN_SECTION_KEY, next);
} catch {}
}
}
// --- Plus/Moins pour les longues listes ---
showAllGroups = signal<boolean>(false);
displayedGroups = computed(() => {
const groups = this.subscriptionGroups();
return this.showAllGroups() ? groups : groups.slice(0, SidebarComponent.GROUP_LIMIT);
});
toggleGroups() { this.showAllGroups.update(v => !v); }
showAllInstances = signal<boolean>(false);
displayedInstances = computed(() => {
const list = this.peerTubeInstances();
return this.showAllInstances() ? list : list.slice(0, SidebarComponent.INSTANCE_LIMIT);
});
toggleInstances() { this.showAllInstances.update(v => !v); }
selectPeerTubeInstance(inst: string) {
if (!inst) return;
this.instances.setActivePeerTubeInstance(inst);
+6
View File
@@ -54,6 +54,9 @@ const EN: Record<string, string> = {
'nav.liked': 'Liked videos',
'nav.downloads': 'Downloads',
'nav.explore': 'Explore',
'nav.providers': 'Providers',
'nav.info': 'Information',
'nav.usage': 'Usage',
'explore.music': 'Music',
'explore.gaming': 'Gaming',
'explore.news': 'News',
@@ -193,6 +196,9 @@ const FR: Record<string, string> = {
'nav.liked': 'Vidéos que vous aimez',
'nav.downloads': 'Téléchargements',
'nav.explore': 'Explorer',
'nav.providers': 'Fournisseurs',
'nav.info': 'Informations',
'nav.usage': 'Utilisation',
'explore.music': 'Musique',
'explore.gaming': 'Jeux vidéo',
'explore.news': 'Actualités',