From 22c1e5462cbd1f49bdaf51d25d7010ce5ba16556 Mon Sep 17 00:00:00 2001 From: 4gray Date: Tue, 24 Feb 2026 19:18:09 +0100 Subject: [PATCH] refactor: code style improvements --- .../video-player/video-player.component.html | 11 +- .../video-player/video-player.component.scss | 24 + .../video-player/video-player.component.ts | 2 + .../xtream-code-import.component.html | 10 +- .../external-player-info-dialog.component.ts | 7 +- .../grid-list/grid-list.component.scss | 74 ++- .../grid-list/grid-list.component.ts | 9 +- .../src/lib/xtream-category.interface.ts | 1 + .../playlist-switcher.component.html | 13 +- .../playlist-switcher.component.scss | 291 ++++------ .../playlist-switcher.component.ts | 534 +++++++++++++++++- .../src/lib/resizable/resizable.scss | 5 - .../sidebar/sidebar.component.html | 32 +- .../video-player/sidebar/sidebar.component.ts | 1 + 14 files changed, 759 insertions(+), 255 deletions(-) diff --git a/apps/web/src/app/home/video-player/video-player.component.html b/apps/web/src/app/home/video-player/video-player.component.html index 66f90c69e..0651e3a95 100644 --- a/apps/web/src/app/home/video-player/video-player.component.html +++ b/apps/web/src/app/home/video-player/video-player.component.html @@ -1,4 +1,8 @@ - + @defer { - + } @loading {

Loading...

} @placeholder { diff --git a/apps/web/src/app/home/video-player/video-player.component.scss b/apps/web/src/app/home/video-player/video-player.component.scss index 4d7ad9be5..e4448742f 100644 --- a/apps/web/src/app/home/video-player/video-player.component.scss +++ b/apps/web/src/app/home/video-player/video-player.component.scss @@ -1,3 +1,10 @@ +:host { + display: block; + width: 100%; + height: 100%; + min-height: 0; +} + .main-container { position: absolute; top: 0; @@ -7,6 +14,23 @@ overflow: hidden; // Prevent horizontal scrollbar from resize handle } +.main-container.main-container--workspace { + position: relative; + top: auto; + right: auto; + bottom: auto; + left: auto; + width: 100%; + height: 100%; + min-height: 0; +} + +.main-container.main-container--workspace .mat-drawer-content, +.main-container.main-container--workspace .drawer-content { + height: 100%; + min-height: 0; +} + .mat-drawer-content { background: #000; color: #fff; diff --git a/apps/web/src/app/home/video-player/video-player.component.ts b/apps/web/src/app/home/video-player/video-player.component.ts index a0bd82866..b4942a4fe 100644 --- a/apps/web/src/app/home/video-player/video-player.component.ts +++ b/apps/web/src/app/home/video-player/video-player.component.ts @@ -108,6 +108,8 @@ export class VideoPlayerComponent implements OnInit, OnDestroy { }; readonly isDesktop = !!window['electron']; + readonly isWorkspaceLayout = + this.activatedRoute.snapshot.data['layout'] === 'workspace'; sidebarView: SidebarView = 'CHANNELS'; diff --git a/apps/web/src/app/home/xtream-code-import/xtream-code-import.component.html b/apps/web/src/app/home/xtream-code-import/xtream-code-import.component.html index e08c7311b..be1996265 100644 --- a/apps/web/src/app/home/xtream-code-import/xtream-code-import.component.html +++ b/apps/web/src/app/home/xtream-code-import/xtream-code-import.component.html @@ -1,11 +1,11 @@
- + {{ 'HOME.XTREAM_PLAYLIST.TITLE' | translate }} - + {{ 'HOME.XTREAM_PLAYLIST.SERVER_URL' | translate }} @@ -21,13 +21,13 @@ }} {{ 'SETTINGS.EPG_URL_ERROR' | translate }} - + {{ 'HOME.XTREAM_PLAYLIST.USERNAME' | translate }} - + {{ 'HOME.XTREAM_PLAYLIST.PASSWORD' | translate }} @@ -53,7 +53,7 @@ - @if (connectionStatus){ + @if (connectionStatus) {
{{ getStatusIcon() }} diff --git a/apps/web/src/app/shared/components/external-player-info-dialog/external-player-info-dialog.component.ts b/apps/web/src/app/shared/components/external-player-info-dialog/external-player-info-dialog.component.ts index 7e3bcb30b..61f8d1eae 100644 --- a/apps/web/src/app/shared/components/external-player-info-dialog/external-player-info-dialog.component.ts +++ b/apps/web/src/app/shared/components/external-player-info-dialog/external-player-info-dialog.component.ts @@ -1,11 +1,10 @@ -import { Component, inject } from '@angular/core'; +import { Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { RouterLink } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; -import { PortalStore } from '../../../xtream/portal.store'; @Component({ template: ` @@ -68,13 +67,11 @@ import { PortalStore } from '../../../xtream/portal.store'; ], }) export class ExternalPlayerInfoDialogComponent { - readonly portalStore = inject(PortalStore); - openUrl(url: string) { window.open(url, '_blank'); } setVisibility(value: boolean) { - this.portalStore.setHideExternalInfoDialog(value); + localStorage.setItem('hideExternalInfoDialog', value.toString()); } } diff --git a/apps/web/src/app/shared/components/grid-list/grid-list.component.scss b/apps/web/src/app/shared/components/grid-list/grid-list.component.scss index 163f74a3c..d4e65bbdf 100644 --- a/apps/web/src/app/shared/components/grid-list/grid-list.component.scss +++ b/apps/web/src/app/shared/components/grid-list/grid-list.component.scss @@ -1,24 +1,26 @@ +// ─── Card base ─────────────────────────────────────────────────────────────── mat-card { overflow: hidden; - transition: all 0.9s cubic-bezier(0.25, 0.8, 0.25, 1); + transition: + transform 0.18s ease, + box-shadow 0.18s ease; box-shadow: none; + background: transparent; } mat-card:hover { + transform: translateY(-3px); box-shadow: - 0 14px 28px rgb(0 0 0 / 25%), - 0 10px 10px rgb(0 0 0 / 22%); + 0 8px 24px rgba(0, 0, 0, 0.35), + 0 2px 8px rgba(0, 0, 0, 0.2); } +// ─── Grid layout ───────────────────────────────────────────────────────────── .grid { - display: flex; - flex-wrap: wrap; - gap: 30px; - justify-content: center; mat-card { cursor: pointer; - width: 170px; + width: 100%; mat-card-content { padding: 0; @@ -28,59 +30,81 @@ mat-card:hover { .card-thumbnail-container { position: relative; width: 100%; - border-radius: 12px; + border-radius: 10px; overflow: hidden; + aspect-ratio: 2 / 3; // maintain poster ratio + background: var(--mat-sys-surface-container-high); } .stream-icon { width: 100%; - position: relative !important; - border-radius: 12px; + height: 100%; + object-fit: cover; + border-radius: 10px; display: block; - background-color: #f0f0f0; + // No hardcoded light bg — uses parent container bg + background: transparent; } .stream-icon-placeholder { width: 100%; - background: #999; - max-height: 300px; - min-height: 255px; + height: 100%; + min-height: 200px; + // Use graphite surface token, not hardcoded grey + background: var(--mat-sys-surface-container-high); display: flex; + flex-direction: column; justify-content: center; align-items: center; + gap: 8px; + color: var(--app-muted-color, var(--mat-sys-on-surface-variant)); + border-radius: 10px; &::after { content: 'No cover'; + font-size: 0.75rem; + font-weight: 500; + letter-spacing: 0.03em; } } .title { - word-break: break-all; + margin-top: 8px; + padding: 0 2px; + word-break: break-word; text-align: center; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; + font-size: 0.8rem; + line-height: 1.35; + color: var(--app-body-color, var(--mat-sys-on-surface-variant)); } } } +// ─── Rating badge ───────────────────────────────────────────────────────────── .rating { position: absolute; - top: 5px; + top: 6px; right: 6px; - background-color: #3bb33b; - color: #fff; - font-size: 0.8em; - border-radius: 3px; + // Use a semi-transparent dark pill, not hardcoded green + background: rgba(0, 0, 0, 0.65); + color: #4ade80; // green-400, readable across themes + font-size: 0.72rem; + font-weight: 600; + border-radius: 6px; display: flex; justify-content: center; align-items: center; - padding: 0 5px; + padding: 2px 5px; + gap: 2px; + backdrop-filter: blur(4px); mat-icon { - font-size: 16px; - height: 18px; - width: 18px; + font-size: 13px; + height: 14px; + width: 14px; } } diff --git a/apps/web/src/app/shared/components/grid-list/grid-list.component.ts b/apps/web/src/app/shared/components/grid-list/grid-list.component.ts index 7f5e9ef71..b09770071 100644 --- a/apps/web/src/app/shared/components/grid-list/grid-list.component.ts +++ b/apps/web/src/app/shared/components/grid-list/grid-list.component.ts @@ -11,9 +11,11 @@ import { WatchedBadgeComponent } from '../../../xtream-electron/shared/watched-b @Component({ selector: 'app-grid-list', - template: `
+ template: `
@if (isLoading()) { -
+
} @else { @@ -82,7 +84,7 @@ import { WatchedBadgeComponent } from '../../../xtream-electron/shared/watched-b } }
- @if (items()?.length > 0) { + @if (showPaginator() && items()?.length > 0) { (); readonly isLoading = input(); + readonly showPaginator = input(true); readonly itemClicked = output(); readonly pageChange = output(); diff --git a/libs/shared/interfaces/src/lib/xtream-category.interface.ts b/libs/shared/interfaces/src/lib/xtream-category.interface.ts index d5d74581c..8c4dbde93 100644 --- a/libs/shared/interfaces/src/lib/xtream-category.interface.ts +++ b/libs/shared/interfaces/src/lib/xtream-category.interface.ts @@ -3,4 +3,5 @@ export interface XtreamCategory { category_id: string; category_name: string; parent_id: number; + count?: number; } diff --git a/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.html b/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.html index d52ba7dd2..7db1824b6 100644 --- a/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.html +++ b/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.html @@ -1,14 +1,19 @@
+
+ playlist_play +
-
{{ currentTitle() }}
+
{{ displayTitle() }}
{{ subtitle() }}
@@ -18,7 +23,10 @@ @@ -30,6 +38,7 @@
search ; + +interface PlaylistRouteContext { + inWorkspace: boolean; + isWorkspaceDashboard: boolean; + provider: PlaylistRouteProvider | null; + playlistId: string | null; + section: string | null; +} + +const LAST_SECTION_STORAGE_KEY = 'playlist-switcher:last-sections'; +const LAST_ACTIVE_PLAYLIST_STORAGE_KEY = 'playlist-switcher:last-active-playlist-id'; +interface PlaylistSectionMemory { + providers: Partial>; + playlists: Record< + string, + { + provider: PlaylistSectionProvider; + section: string; + updatedAt: number; + } + >; +} +const XTREAM_SECTIONS = [ + 'live', + 'vod', + 'series', + 'favorites', + 'recent', + 'search', + 'recently-added', + 'downloads', +] as const; +const STALKER_SECTIONS = [ + 'itv', + 'vod', + 'series', + 'favorites', + 'recent', + 'search', + 'downloads', +] as const; + @Component({ selector: 'app-playlist-switcher', templateUrl: './playlist-switcher.component.html', @@ -39,9 +91,12 @@ import { PlaylistMeta } from 'shared-interfaces'; ], }) export class PlaylistSwitcherComponent { + private readonly document = inject(DOCUMENT); + private readonly destroyRef = inject(DestroyRef); private readonly router = inject(Router); private readonly store = inject(Store); private readonly portalStatusService = inject(PortalStatusService); + private focusSearchTimeoutId: ReturnType | null = null; /** Current playlist title to display */ readonly currentTitle = input.required(); @@ -52,6 +107,10 @@ export class PlaylistSwitcherComponent { readonly menuTrigger = viewChild.required('menuTrigger'); readonly playlistMenu = viewChild.required('playlistMenu'); + readonly triggerElement = viewChild.required>( + 'triggerElement' + ); + readonly searchInput = viewChild>('searchInput'); /** Signal for tracking menu open state */ readonly isMenuOpen = signal(false); @@ -61,6 +120,7 @@ export class PlaylistSwitcherComponent { /** All playlists from store */ readonly playlists = this.store.selectSignal(selectAllPlaylistsMeta); + readonly allPlaylistsLoaded = this.store.selectSignal(selectPlaylistsLoadingFlag); /** Filtered playlists based on search query, sorted by importDate (newest first) */ readonly filteredPlaylists = computed(() => { @@ -82,7 +142,33 @@ export class PlaylistSwitcherComponent { }); /** Current active playlist ID from route */ - readonly activePlaylistId = signal(null); + readonly routePlaylistId = signal(null); + readonly selectedPlaylistId = this.store.selectSignal(selectActivePlaylistId); + readonly activePlaylistId = computed(() => { + return this.routePlaylistId() ?? this.selectedPlaylistId() ?? null; + }); + readonly activePlaylist = computed(() => { + const playlistId = this.activePlaylistId(); + if (!playlistId) { + return null; + } + + return this.playlists().find((playlist) => playlist._id === playlistId) ?? null; + }); + readonly displayTitle = computed(() => { + if (!this.activePlaylistId()) { + return 'Select playlist'; + } + + return ( + this.currentTitle() || + this.activePlaylist()?.title || + this.activePlaylist()?.filename || + this.activePlaylist()?.url || + this.activePlaylist()?.portalUrl || + 'Untitled playlist' + ); + }); /** Portal statuses for Xtream playlists */ readonly portalStatuses = signal>(new Map()); @@ -90,6 +176,61 @@ export class PlaylistSwitcherComponent { constructor() { // Extract playlist ID from current route this.updateActivePlaylistFromRoute(this.router.url); + this.rememberSectionFromRoute(this.router.url); + effect(() => { + if (!this.allPlaylistsLoaded()) { + return; + } + const playlistIds = this.playlists().map((playlist) => playlist._id); + this.removeDeletedPlaylistsFromSectionMemory(playlistIds); + }); + effect(() => { + const allPlaylistsLoaded = this.allPlaylistsLoaded(); + const playlistIds = this.playlists().map((playlist) => playlist._id); + const playlistIdSet = new Set(playlistIds); + const selectedId = this.selectedPlaylistId(); + const routeId = this.routePlaylistId(); + const storedId = this.readPersistedActivePlaylistId(); + + // Before playlists are loaded, avoid stale cleanups. + // But still keep storage in sync with explicit runtime context. + if (!allPlaylistsLoaded) { + if (routeId) { + this.writePersistedActivePlaylistId(routeId); + return; + } + if (selectedId) { + this.writePersistedActivePlaylistId(selectedId); + } + return; + } + + // If route provides playlist context, keep storage in sync with URL state. + if (routeId && playlistIdSet.has(routeId)) { + this.writePersistedActivePlaylistId(routeId); + return; + } + + // Keep storage up-to-date with selected playlist state. + if (selectedId && playlistIdSet.has(selectedId)) { + this.writePersistedActivePlaylistId(selectedId); + return; + } + + // On routes without playlist in URL (for example dashboard), + // restore last known active playlist when store has none. + if (!routeId && !selectedId && storedId && playlistIdSet.has(storedId)) { + this.store.dispatch( + PlaylistActions.setActivePlaylist({ playlistId: storedId }) + ); + return; + } + + // Remove stale value if playlist no longer exists. + if (storedId && !playlistIdSet.has(storedId)) { + this.clearPersistedActivePlaylistId(); + } + }); // Listen for route changes this.router.events @@ -99,19 +240,55 @@ export class PlaylistSwitcherComponent { ) .subscribe((event: NavigationEnd) => { this.updateActivePlaylistFromRoute(event.urlAfterRedirects); + this.rememberSectionFromRoute(event.urlAfterRedirects); }); + + this.destroyRef.onDestroy(() => { + this.clearMenuOverlayWidth(); + this.clearSearchFocusTimeout(); + }); } private updateActivePlaylistFromRoute(url: string) { // Match routes like /playlists/:id, /xtreams/:id, /stalker/:id const match = url.match(/\/(playlists|xtreams|stalker)\/([^\/\?]+)/); if (match) { - this.activePlaylistId.set(match[2]); + this.routePlaylistId.set(match[2]); } else { - this.activePlaylistId.set(null); + this.routePlaylistId.set(null); } } + private rememberSectionFromRoute(url: string): void { + const routeContext = this.getRouteContext(url); + if ( + !routeContext.provider || + routeContext.provider === 'playlists' || + !routeContext.section + ) { + return; + } + + const normalized = this.normalizeSectionForProvider( + routeContext.section, + routeContext.provider + ); + if (!normalized) { + return; + } + + const memory = this.readSectionMemory(); + memory.providers[routeContext.provider] = normalized; + if (routeContext.playlistId) { + memory.playlists[routeContext.playlistId] = { + provider: routeContext.provider, + section: normalized, + updatedAt: Date.now(), + }; + } + this.writeSectionMemory(memory); + } + private async checkPortalStatuses(playlists: PlaylistMeta[]) { const statusPromises = playlists .filter( @@ -142,25 +319,358 @@ export class PlaylistSwitcherComponent { onMenuOpened() { this.isMenuOpen.set(true); + this.syncMenuOverlayWidthToTrigger(); + this.focusSearchField(); this.checkPortalStatuses(this.playlists()); } onMenuClosed() { this.isMenuOpen.set(false); + this.clearMenuOverlayWidth(); + this.clearSearchFocusTimeout(); this.searchQuery.set(''); } - selectPlaylist(playlist: PlaylistMeta) { - if (playlist.serverUrl) { - this.router.navigate(['xtreams', playlist._id]); - } else if (playlist.macAddress) { - this.router.navigate(['stalker', playlist._id]); - } else { - this.router.navigate(['playlists', playlist._id]); + private syncMenuOverlayWidthToTrigger() { + const triggerWidth = Math.round( + this.triggerElement().nativeElement.getBoundingClientRect().width + ); + this.document.documentElement.style.setProperty( + '--playlist-switcher-overlay-width', + `${triggerWidth}px` + ); + } + + private clearMenuOverlayWidth() { + this.document.documentElement.style.removeProperty( + '--playlist-switcher-overlay-width' + ); + } + + private focusSearchField() { + this.clearSearchFocusTimeout(); + this.focusSearchTimeoutId = setTimeout(() => { + const input = this.searchInput()?.nativeElement; + if (!input) { + return; + } + input.focus(); + input.select(); + this.focusSearchTimeoutId = null; + }, 0); + } + + private clearSearchFocusTimeout() { + if (this.focusSearchTimeoutId !== null) { + clearTimeout(this.focusSearchTimeoutId); + this.focusSearchTimeoutId = null; } + } + + selectPlaylist(playlist: PlaylistMeta) { + const routeContext = this.getRouteContext(this.router.url); + const provider = this.getProviderForPlaylist(playlist); + + this.setActivePlaylist(playlist._id); + + const targetCommands = this.getTargetCommands( + provider, + playlist._id, + routeContext + ); + if (targetCommands) { + this.router.navigate(targetCommands); + } + this.playlistSelected.emit(playlist._id); } + private setActivePlaylist(playlistId: string): void { + this.store.dispatch(PlaylistActions.setActivePlaylist({ playlistId })); + } + + private isWorkspaceRoute(url: string): boolean { + return /^\/workspace(?:\/|$)/.test(url); + } + + private getProviderForPlaylist(playlist: PlaylistMeta): PlaylistRouteProvider { + if (playlist.serverUrl) { + return 'xtreams'; + } + if (playlist.macAddress) { + return 'stalker'; + } + return 'playlists'; + } + + private getRouteContext(url: string): PlaylistRouteContext { + const inWorkspace = this.isWorkspaceRoute(url); + const isWorkspaceDashboard = + /^\/workspace(?:\/dashboard)?(?:\/)?(?:\?.*)?$/.test(url); + const match = url.match( + /^\/(?:workspace\/)?(playlists|xtreams|stalker)\/([^\/\?]+)(?:\/([^\/\?]+))?/ + ); + + return { + inWorkspace, + isWorkspaceDashboard, + provider: (match?.[1] as PlaylistRouteProvider | undefined) ?? null, + playlistId: match?.[2] ?? null, + section: match?.[3] ?? null, + }; + } + + private getTargetCommands( + provider: PlaylistRouteProvider, + playlistId: string, + routeContext: PlaylistRouteContext + ): string[] | null { + const prefix = routeContext.inWorkspace ? ['workspace'] : []; + + // In workspace routes without explicit provider context (for example + // dashboard and sources), selecting a source should only change active + // context and must not trigger navigation. + if (routeContext.inWorkspace && !routeContext.provider) { + return null; + } + + if (provider === 'playlists') { + return [...prefix, 'playlists', playlistId]; + } + + if (!this.supportsSectionNavigation(provider)) { + return [...prefix, provider, playlistId]; + } + + const section = this.resolveTargetSection( + provider, + routeContext.section, + playlistId + ); + return [...prefix, provider, playlistId, section]; + } + + private resolveTargetSection( + provider: PlaylistSectionProvider, + currentSection: string | null, + targetPlaylistId: string + ): string { + const fromCurrent = this.normalizeSectionForProvider(currentSection, provider); + if (fromCurrent) { + return fromCurrent; + } + + const memory = this.readSectionMemory(); + const playlistMemory = memory.playlists[targetPlaylistId]; + if (playlistMemory?.provider === provider) { + const normalizedPlaylistSection = this.normalizeSectionForProvider( + playlistMemory.section, + provider + ); + if (normalizedPlaylistSection) { + return normalizedPlaylistSection; + } + } + + const providerMemory = memory.providers[provider]; + const normalizedProviderSection = this.normalizeSectionForProvider( + providerMemory, + provider + ); + if (normalizedProviderSection) { + return normalizedProviderSection; + } + + return 'vod'; + } + + private supportsSectionNavigation(provider: PlaylistSectionProvider): boolean { + if (provider === 'xtreams') { + return Boolean(window.electron); + } + return true; + } + + private normalizeSectionForProvider( + section: string | null | undefined, + provider: PlaylistSectionProvider + ): string | null { + if (!section) { + return null; + } + + if (provider === 'xtreams') { + if (section === 'itv') { + return 'live'; + } + return XTREAM_SECTIONS.includes(section as (typeof XTREAM_SECTIONS)[number]) + ? section + : null; + } + + if (section === 'live') { + return 'itv'; + } + if (section === 'recently-added') { + return 'recent'; + } + return STALKER_SECTIONS.includes(section as (typeof STALKER_SECTIONS)[number]) + ? section + : null; + } + + private readSectionMemory(): PlaylistSectionMemory { + const empty: PlaylistSectionMemory = { providers: {}, playlists: {} }; + + try { + const raw = localStorage.getItem(LAST_SECTION_STORAGE_KEY); + if (!raw) { + return empty; + } + const parsed = JSON.parse(raw); + if (typeof parsed !== 'object' || parsed === null) { + return empty; + } + + const providers: Partial> = {}; + const playlists: PlaylistSectionMemory['playlists'] = {}; + + const parsedProviders = (parsed as Record)['providers']; + if (typeof parsedProviders === 'object' && parsedProviders !== null) { + const candidate = parsedProviders as Record; + if (typeof candidate['xtreams'] === 'string') { + providers.xtreams = candidate['xtreams']; + } + if (typeof candidate['stalker'] === 'string') { + providers.stalker = candidate['stalker']; + } + } else { + // Backward compatibility with previous format: + // { xtreams: "...", stalker: "..." } + const legacy = parsed as Record; + if (typeof legacy['xtreams'] === 'string') { + providers.xtreams = legacy['xtreams']; + } + if (typeof legacy['stalker'] === 'string') { + providers.stalker = legacy['stalker']; + } + } + + const parsedPlaylists = (parsed as Record)['playlists']; + if (typeof parsedPlaylists === 'object' && parsedPlaylists !== null) { + Object.entries(parsedPlaylists as Record).forEach( + ([playlistId, value]) => { + if ( + !value || + typeof value !== 'object' || + !playlistId + ) { + return; + } + + const row = value as Record; + const provider = row['provider']; + const section = row['section']; + const updatedAt = row['updatedAt']; + const isProviderValid = + provider === 'xtreams' || provider === 'stalker'; + if (!isProviderValid || typeof section !== 'string') { + return; + } + + playlists[playlistId] = { + provider, + section, + updatedAt: + typeof updatedAt === 'number' + ? updatedAt + : Date.now(), + }; + } + ); + } + + return { providers, playlists }; + } catch { + return empty; + } + } + + private writeSectionMemory(memory: PlaylistSectionMemory): void { + try { + localStorage.setItem(LAST_SECTION_STORAGE_KEY, JSON.stringify(memory)); + } catch { + // Ignore storage write failures (private mode/storage quota). + } + } + + private readPersistedActivePlaylistId(): string | null { + try { + const value = localStorage.getItem(LAST_ACTIVE_PLAYLIST_STORAGE_KEY); + return value && value.trim().length > 0 ? value : null; + } catch { + return null; + } + } + + private writePersistedActivePlaylistId(playlistId: string): void { + try { + localStorage.setItem(LAST_ACTIVE_PLAYLIST_STORAGE_KEY, playlistId); + } catch { + // Ignore storage write failures (private mode/storage quota). + } + } + + private clearPersistedActivePlaylistId(): void { + try { + localStorage.removeItem(LAST_ACTIVE_PLAYLIST_STORAGE_KEY); + } catch { + // Ignore storage write failures. + } + } + + private removeDeletedPlaylistsFromSectionMemory(existingIds: string[]): void { + const existingIdSet = new Set(existingIds); + const memory = this.readSectionMemory(); + const currentEntries = Object.entries(memory.playlists); + if (currentEntries.length === 0) { + return; + } + + const nextPlaylists: PlaylistSectionMemory['playlists'] = {}; + let hasChanges = false; + + currentEntries.forEach(([playlistId, entry]) => { + if (!existingIdSet.has(playlistId)) { + hasChanges = true; + return; + } + + const normalizedSection = this.normalizeSectionForProvider( + entry.section, + entry.provider + ); + if (!normalizedSection) { + hasChanges = true; + return; + } + + nextPlaylists[playlistId] = { + ...entry, + section: normalizedSection, + }; + }); + + if (!hasChanges) { + return; + } + + this.writeSectionMemory({ + providers: memory.providers, + playlists: nextPlaylists, + }); + } + getPlaylistIcon(playlist: PlaylistMeta): string { if (playlist.macAddress) { return 'dashboard'; diff --git a/libs/ui/components/src/lib/resizable/resizable.scss b/libs/ui/components/src/lib/resizable/resizable.scss index 1ae5be5a0..8c39ee0ae 100644 --- a/libs/ui/components/src/lib/resizable/resizable.scss +++ b/libs/ui/components/src/lib/resizable/resizable.scss @@ -57,10 +57,6 @@ body.resizing-active { background: rgba(255, 255, 255, 0.08); } - &:active::before { - background: rgba(255, 255, 255, 0.12); - } - // Position variants &--right { right: -6px; @@ -86,7 +82,6 @@ body.resizing-active { top: 0; bottom: 0; width: 1px; - background: rgba(255, 255, 255, 0.12); transition: all 0.2s ease; } diff --git a/libs/ui/components/src/lib/video-player/sidebar/sidebar.component.html b/libs/ui/components/src/lib/video-player/sidebar/sidebar.component.html index e5126a32e..1dabc5fd1 100644 --- a/libs/ui/components/src/lib/video-player/sidebar/sidebar.component.html +++ b/libs/ui/components/src/lib/video-player/sidebar/sidebar.component.html @@ -1,18 +1,20 @@ -
- +@if (showPlaylistHeader()) { +
+ - -
+ +
+} diff --git a/libs/ui/components/src/lib/video-player/sidebar/sidebar.component.ts b/libs/ui/components/src/lib/video-player/sidebar/sidebar.component.ts index 973e08aba..0b899a43d 100644 --- a/libs/ui/components/src/lib/video-player/sidebar/sidebar.component.ts +++ b/libs/ui/components/src/lib/video-player/sidebar/sidebar.component.ts @@ -26,6 +26,7 @@ import { ChannelListContainerComponent } from './../../channel-list-container/ch }) export class SidebarComponent { readonly channels = input([]); + readonly showPlaylistHeader = input(true); private readonly store = inject(Store); private readonly translate = inject(TranslateService);