feat(sidebar): accordeon unique strict, Plus/Moins, scroll au survol
CI / build-and-test (push) Failing after 8m17s
CI / build-and-test (push) Failing after 8m17s
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user