feat(themes): interrupteur global bande des themes + masquee sur watch
CI / build-and-test (push) Failing after 8m18s
CI / build-and-test (push) Failing after 8m18s
- ThemesService: signal barVisible persiste (localStorage), toggle global - header: bouton oeil afficher/masquer la bande (aria-pressed, FR/EN) - app: affichage = page autorisee ET interrupteur; allowlist stricte (accueil + /t/... uniquement, chemin normalise) -> jamais sur /watch - i18n: cles themes.showBar/hideBar FR/EN
This commit is contained in:
+10
-4
@@ -6,6 +6,7 @@ import { SidebarComponent } from './components/sidebar/sidebar.component';
|
|||||||
import { ThemesNavComponent } from './components/themes/themes-nav.component';
|
import { ThemesNavComponent } from './components/themes/themes-nav.component';
|
||||||
import { I18nService } from './services/i18n.service';
|
import { I18nService } from './services/i18n.service';
|
||||||
import { AuthService } from './services/auth.service';
|
import { AuthService } from './services/auth.service';
|
||||||
|
import { ThemesService } from './services/themes.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@@ -18,6 +19,7 @@ export class AppComponent {
|
|||||||
private i18n = inject(I18nService);
|
private i18n = inject(I18nService);
|
||||||
private auth = inject(AuthService);
|
private auth = inject(AuthService);
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
|
private themesSvc = inject(ThemesService);
|
||||||
sidebarOpen = signal(false);
|
sidebarOpen = signal(false);
|
||||||
sidebarCollapsed = signal(false);
|
sidebarCollapsed = signal(false);
|
||||||
private _showThemesBar = signal(false);
|
private _showThemesBar = signal(false);
|
||||||
@@ -59,9 +61,12 @@ export class AppComponent {
|
|||||||
|
|
||||||
private updateShowThemesBar() {
|
private updateShowThemesBar() {
|
||||||
try {
|
try {
|
||||||
const url = this.router.url || '/';
|
// Liste blanche stricte : accueil + pages de thèmes uniquement.
|
||||||
const isHome = url === '/' || url.startsWith('/?');
|
// /watch, /search, /shorts, /library, /channel... : jamais de bande.
|
||||||
const isTheme = url.startsWith('/t/') || /^\/p\/[^/]+\/t\//.test(url);
|
const raw = this.router.url || '/';
|
||||||
|
const pathOnly = raw.split('?')[0].split('#')[0] || '/';
|
||||||
|
const isHome = pathOnly === '/' || pathOnly === '';
|
||||||
|
const isTheme = pathOnly.startsWith('/t/') || /^\/p\/[^/]+\/t\//.test(pathOnly);
|
||||||
this._showThemesBar.set(isHome || isTheme);
|
this._showThemesBar.set(isHome || isTheme);
|
||||||
} catch {
|
} catch {
|
||||||
this._showThemesBar.set(false);
|
this._showThemesBar.set(false);
|
||||||
@@ -69,5 +74,6 @@ export class AppComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Template helpers
|
// Template helpers
|
||||||
showThemesBar() { return this._showThemesBar(); }
|
// Visible = page autorisée ET interrupteur global du header (persisté).
|
||||||
|
showThemesBar() { return this._showThemesBar() && this.themesSvc.barVisible(); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,17 @@
|
|||||||
|
|
||||||
<!-- Right: user -->
|
<!-- Right: user -->
|
||||||
<div class="justify-self-end flex items-center gap-3 pr-3 md:pr-4">
|
<div class="justify-self-end flex items-center gap-3 pr-3 md:pr-4">
|
||||||
|
<!-- Toggle global de la bande des thèmes -->
|
||||||
|
<button (click)="toggleThemesBar()"
|
||||||
|
[attr.aria-pressed]="themesBarVisible()"
|
||||||
|
[attr.title]="themesBarVisible() ? ('themes.hideBar' | t : 'Masquer la bande des thèmes') : ('themes.showBar' | t : 'Afficher la bande des thèmes')"
|
||||||
|
aria-label="Afficher ou masquer la bande des thèmes"
|
||||||
|
class="p-2 rounded-full text-slate-200 hover:bg-slate-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-400"
|
||||||
|
[class.opacity-40]="!themesBarVisible()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" 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>
|
||||||
|
</button>
|
||||||
<!-- Provider context indicator placed between search and user area -->
|
<!-- Provider context indicator placed between search and user area -->
|
||||||
<div *ngIf="providerContextLabel()" class="flex items-center gap-1 text-xs">
|
<div *ngIf="providerContextLabel()" class="flex items-center gap-1 text-xs">
|
||||||
<span class="inline-flex items-center px-2 py-0.5 rounded border shadow-sm gap-1"
|
<span class="inline-flex items-center px-2 py-0.5 rounded border shadow-sm gap-1"
|
||||||
|
|||||||
@@ -178,6 +178,12 @@ export class HeaderComponent {
|
|||||||
return p?.label || ctx;
|
return p?.label || ctx;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Bande des thèmes : interrupteur global (persisté dans ThemesService)
|
||||||
|
themesBarVisible = this.themes.barVisible;
|
||||||
|
toggleThemesBar(): void {
|
||||||
|
this.themes.toggleBarVisible();
|
||||||
|
}
|
||||||
|
|
||||||
// Theme management (global/local)
|
// Theme management (global/local)
|
||||||
themeOptions = ['system', 'light', 'dark', 'black', 'blue'];
|
themeOptions = ['system', 'light', 'dark', 'black', 'blue'];
|
||||||
currentTheme = signal<string>((() => {
|
currentTheme = signal<string>((() => {
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ const EN: Record<string, string> = {
|
|||||||
'themes.title': 'Themes',
|
'themes.title': 'Themes',
|
||||||
'themes.show': 'Show',
|
'themes.show': 'Show',
|
||||||
'themes.hide': 'Hide',
|
'themes.hide': 'Hide',
|
||||||
|
'themes.showBar': 'Show the themes bar',
|
||||||
|
'themes.hideBar': 'Hide the themes bar',
|
||||||
// Common
|
// Common
|
||||||
'common.error': 'An error occurred.',
|
'common.error': 'An error occurred.',
|
||||||
'empty.noItems': 'No items.',
|
'empty.noItems': 'No items.',
|
||||||
@@ -210,6 +212,8 @@ const FR: Record<string, string> = {
|
|||||||
'themes.title': 'Thèmes',
|
'themes.title': 'Thèmes',
|
||||||
'themes.show': 'Afficher',
|
'themes.show': 'Afficher',
|
||||||
'themes.hide': 'Masquer',
|
'themes.hide': 'Masquer',
|
||||||
|
'themes.showBar': 'Afficher la bande des thèmes',
|
||||||
|
'themes.hideBar': 'Masquer la bande des thèmes',
|
||||||
// Common
|
// Common
|
||||||
'common.error': 'Une erreur est survenue.',
|
'common.error': 'Une erreur est survenue.',
|
||||||
'empty.noItems': 'Aucun élément.',
|
'empty.noItems': 'Aucun élément.',
|
||||||
|
|||||||
@@ -16,6 +16,25 @@ export class ThemesService {
|
|||||||
// Controls whether the themes nav should show all themes or only a subset (e.g., 8)
|
// Controls whether the themes nav should show all themes or only a subset (e.g., 8)
|
||||||
readonly showAll = signal<boolean>(false);
|
readonly showAll = signal<boolean>(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interrupteur global de la bande des thèmes (bouton du header).
|
||||||
|
* Persisté : '0' = masquée partout, toute autre valeur = visible
|
||||||
|
* (sous réserve des règles de route : jamais sur /watch, etc.).
|
||||||
|
*/
|
||||||
|
private static readonly BAR_VISIBLE_KEY = 'newtube.themesBarVisible';
|
||||||
|
readonly barVisible = signal<boolean>((() => {
|
||||||
|
try { return localStorage.getItem(ThemesService.BAR_VISIBLE_KEY) !== '0'; } catch { return true; }
|
||||||
|
})());
|
||||||
|
|
||||||
|
setBarVisible(v: boolean): void {
|
||||||
|
this.barVisible.set(!!v);
|
||||||
|
try { localStorage.setItem(ThemesService.BAR_VISIBLE_KEY, v ? '1' : '0'); } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleBarVisible(): void {
|
||||||
|
this.setBarVisible(!this.barVisible());
|
||||||
|
}
|
||||||
|
|
||||||
constructor(private i18n: I18nService) {}
|
constructor(private i18n: I18nService) {}
|
||||||
|
|
||||||
themes() { return this.list(); }
|
themes() { return this.list(); }
|
||||||
|
|||||||
Reference in New Issue
Block a user