diff --git a/apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.html b/apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.html index ee6a402d9..f585dbfd1 100644 --- a/apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.html +++ b/apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.html @@ -46,61 +46,27 @@ -->
@for (item of itvChannels(); track item.id ?? $index) { -
- -
- {{ - item.o_name || item.name - }} - @if (currentPrograms.get(item.id)) { -
-
- {{ currentPrograms.get(item.id) }} -
- @if (currentProgramsProgress.has(item.id)) { -
- {{ - programTimings.get(item.id)?.start - | date: 'HH:mm' - }} -
-
-
- {{ - programTimings.get(item.id)?.end - | date: 'HH:mm' - }} -
- } -
- } -
- -
+ } @if (isLoadingMore()) { diff --git a/apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.scss b/apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.scss index d60056f52..06f8d2a8c 100644 --- a/apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.scss +++ b/apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.scss @@ -33,23 +33,38 @@ display: flex; flex-direction: column; overflow: hidden; + min-height: 0; } .video-player { - flex: 1; - height: 50%; + flex: 1 1 0; + min-height: 0; + overflow: hidden; + display: flex; } .epg { - flex: 1; - height: 50%; + flex: 1 1 0; + min-height: 0; overflow: hidden; } +.video-player > app-web-player-view { + flex: 1; + min-height: 0; +} + .epg-content { height: 100%; - overflow-y: auto; - padding-bottom: 20px; + min-height: 0; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.epg-content > app-epg-view { + flex: 1; + min-height: 0; } .no-channel-selected { @@ -62,7 +77,6 @@ gap: 10px; } -// Channel list .search-panel { padding: 8px 12px; flex-shrink: 0; @@ -91,125 +105,10 @@ flex: 1; overflow-y: auto; overflow-x: hidden; - padding: 4px 0; -} - -.channel-item { - display: flex; - align-items: center; - padding: 8px; - margin: 2px 4px; - border-radius: 4px; - cursor: pointer; - transition: background-color 0.2s ease; - min-height: 48px; - box-sizing: border-box; - gap: 8px; - width: calc(100% - 8px); - min-width: 0; - overflow: hidden; - - &:hover { - background-color: color-mix(in srgb, var(--mat-sys-on-surface) 8%, transparent); - - .title { - color: var(--mat-sys-primary); - } - } - - &.active { - background-color: color-mix(in srgb, var(--mat-sys-primary) 16%, transparent); - - .title { - color: var(--mat-sys-primary); - } - } -} - -.stream-icon { - width: 32px; - height: 32px; - border-radius: 4px; - object-fit: cover; - flex-shrink: 0; - background: color-mix(in srgb, var(--mat-sys-on-surface) 5%, transparent); -} - -.channel-info { - flex: 1 1 0; - min-width: 0; - max-width: 100%; + padding: 4px 8px 12px; display: flex; flex-direction: column; - overflow: hidden; - - .title { - font-weight: 600; - font-size: 14px; - transition: color 0.2s ease; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .current-program { - font-size: 12px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - opacity: 0.8; - } - - .progress-container { - display: flex; - align-items: center; - gap: 8px; - font-size: 11px; - opacity: 0.6; - - span { - flex-shrink: 0; - } - - .progress-bar { - flex: 1; - height: 3px; - background: color-mix(in srgb, var(--mat-sys-on-surface) 10%, transparent); - border-radius: 2px; - overflow: hidden; - min-width: 20px; - - .progress { - height: 100%; - background: var(--mat-sys-primary); - transition: width 1s linear; - } - } - } -} - -.favorite-btn { - background: none; - border: none; - padding: 8px; - cursor: pointer; - color: inherit; - opacity: 0.6; - transition: opacity 0.2s ease; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - - &:hover { - opacity: 1; - } - - mat-icon { - font-size: 20px; - width: 20px; - height: 20px; - } + gap: 2px; } .loading-indicator { @@ -224,43 +123,9 @@ padding: 8px 0; } -// Container queries for responsive behavior @container (max-width: 350px) { - .channel-item { - gap: 6px; - padding: 6px; - } - - .stream-icon { - width: 28px; - height: 28px; - } - - .favorite-btn { - padding: 4px; - - mat-icon { - font-size: 18px; - width: 18px; - height: 18px; - } - } -} - -@container (max-width: 300px) { - .stream-icon { - width: 24px; - height: 24px; - } - - .favorite-btn { - display: none; - } -} - -@container (max-width: 260px) { - .stream-icon { - display: none; + .channels-list { + padding-inline: 6px; } } diff --git a/apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.ts b/apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.ts index efa45fb0d..770a3f052 100644 --- a/apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.ts +++ b/apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.ts @@ -11,7 +11,6 @@ import { viewChild, } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { DatePipe } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { MatButton, MatIconButton } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -21,9 +20,13 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSnackBar } from '@angular/material/snack-bar'; import { ActivatedRoute } from '@angular/router'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { PlaylistSwitcherComponent, ResizableDirective } from 'components'; -import { PlaylistsService } from 'services'; -import { EpgItem } from 'shared-interfaces'; +import { + ChannelListItemComponent, + PlaylistSwitcherComponent, + ResizableDirective, +} from 'components'; +import { PlaylistsService, StalkerSessionService } from 'services'; +import { EpgItem, EpgProgram, PlaylistMeta } from 'shared-interfaces'; import { EpgViewComponent, WebPlayerViewComponent } from 'shared-portals'; import { SettingsStore } from '../../services/settings-store.service'; import { PlayerService } from '../../services/player.service'; @@ -54,7 +57,7 @@ import { ], imports: [ CategoryViewComponent, - DatePipe, + ChannelListItemComponent, EpgViewComponent, FormsModule, MatButton, @@ -77,10 +80,14 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy { readonly stalkerStore = inject(StalkerStore); private readonly settingsStore = inject(SettingsStore); private readonly playlistService = inject(PlaylistsService); + private readonly stalkerSession = inject(StalkerSessionService); private readonly playerService = inject(PlayerService); private readonly snackBar = inject(MatSnackBar); private readonly translate = inject(TranslateService); private readonly logger = createLogger('StalkerLiveStream'); + private readonly stalkerMagUserAgent = + 'Mozilla/5.0 (QtEmbedded; U; Linux; C) AppleWebKit/533.3 (KHTML, like Gecko) MAG250'; + private readonly stalkerStreamUserAgent = 'KSPlayer'; /** Categories */ readonly categories = this.stalkerStore.getCategoryResource; @@ -98,6 +105,8 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy { /** Search */ readonly searchString = signal(''); + readonly selectedChannelId = this.stalkerStore.selectedItvId; + protected readonly normalizeStalkerEntityId = normalizeStalkerEntityId; /** Player */ readonly player = this.settingsStore.player; @@ -111,9 +120,8 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy { private epgChannelId: number | string | null = null; /** Channel list EPG preview */ - readonly currentPrograms = new Map(); + readonly epgPreviewPrograms = new Map(); readonly currentProgramsProgress = new Map(); - readonly programTimings = new Map(); private readonly requestedEpgChannels = new Set(); private readonly cdr = inject(ChangeDetectorRef); private readonly ngZone = inject(NgZone); @@ -255,6 +263,13 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy { this.stalkerStore.setSelectedCategory(null); } + isSelectedChannel(item: StalkerItvChannel): boolean { + return ( + this.selectedChannelId() === + this.normalizeStalkerEntityId(item.id) + ); + } + async playChannel(item: StalkerItvChannel) { this.stalkerStore.setSelectedItem(item); @@ -275,15 +290,36 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy { if (isEmbeddedPlayer) { this.streamUrl = url; } else { + const playlist = this.currentPlaylist(); + const portalOrigin = this.getPortalOrigin(playlist); + const crossOriginStream = this.isCrossOriginStream( + playlist, + url + ); + const playbackHeaders = + this.buildExternalPlaybackHeaders(playlist, url); + const playbackUserAgent = + playbackHeaders['User-Agent'] || + playlist?.userAgent || + this.stalkerMagUserAgent; + const playbackReferer = crossOriginStream + ? undefined + : playlist?.referrer || portalOrigin; + const playbackOrigin = crossOriginStream + ? undefined + : playlist?.origin || portalOrigin; this.playerService.openPlayer( url, item.o_name || item.name, item.logo, true, true, - this.currentPlaylist()?.userAgent, - this.currentPlaylist()?.referrer, - this.currentPlaylist()?.origin + playbackUserAgent, + playbackReferer, + playbackOrigin, + undefined, + undefined, + playbackHeaders ); } @@ -404,7 +440,10 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy { if (items.length > 0) { const program = items[0]; const id = normalizeStalkerEntityId(channelId); - this.currentPrograms.set(id, program.title); + this.epgPreviewPrograms.set( + id, + this.toPreviewProgram(program, id) + ); const now = Date.now() / 1000; const start = parseInt(program.start_timestamp, 10); @@ -414,10 +453,8 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy { const progress = ((now - start) / (end - start)) * 100; this.currentProgramsProgress.set(id, progress); - this.programTimings.set(id, { - start: start * 1000, - end: end * 1000, - }); + } else { + this.currentProgramsProgress.delete(id); } } } catch { @@ -426,9 +463,8 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy { } private clearEpgPreviewMaps() { - this.currentPrograms.clear(); + this.epgPreviewPrograms.clear(); this.currentProgramsProgress.clear(); - this.programTimings.clear(); this.requestedEpgChannels.clear(); } @@ -472,6 +508,105 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy { } } + private getPortalOrigin( + playlist: PlaylistMeta | undefined | null + ): string | undefined { + const portalUrl = playlist?.portalUrl; + if (!portalUrl) return undefined; + try { + return new URL(portalUrl).origin; + } catch { + return undefined; + } + } + + private buildExternalPlaybackHeaders( + playlist: PlaylistMeta | undefined | null, + streamUrl?: string + ): Record { + if (!playlist?.macAddress) { + return {}; + } + + if (this.isCrossOriginStream(playlist, streamUrl)) { + return { + 'User-Agent': this.stalkerStreamUserAgent, + Accept: '*/*', + Range: 'bytes=0-', + Connection: 'keep-alive', + 'Icy-MetaData': '1', + }; + } + + const cookieParts = [ + `mac=${playlist.macAddress}`, + 'stb_lang=en_US@rg=dezzzz', + 'timezone=Europe/Berlin', + ]; + if (playlist.stalkerSerialNumber) { + cookieParts.push( + `__cfduid=${playlist.stalkerSerialNumber.toLowerCase()}e030245495acd6ebfc1` + ); + } + + const headers: Record = { + Cookie: cookieParts.join('; '), + 'X-User-Agent': this.stalkerMagUserAgent, + SN: playlist.stalkerSerialNumber || '', + }; + + const token = this.stalkerSession.getCachedToken(playlist._id); + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + const origin = this.getPortalOrigin(playlist); + if (origin) { + headers['Origin'] = origin; + headers['Referer'] = origin; + } + + return Object.entries(headers).reduce>( + (acc, [name, value]) => { + if (value?.trim()) { + acc[name] = value; + } + return acc; + }, + {} + ); + } + + private isCrossOriginStream( + playlist: PlaylistMeta | undefined | null, + streamUrl?: string + ): boolean { + const portalOrigin = this.getPortalOrigin(playlist); + if (!portalOrigin || !streamUrl) { + return false; + } + + try { + return new URL(streamUrl).origin !== portalOrigin; + } catch { + return false; + } + } + + private toPreviewProgram( + item: EpgItem, + channelId: string | number + ): EpgProgram { + return { + start: item.start, + stop: item.stop || item.end, + channel: String(channelId), + title: item.title, + desc: item.description || null, + category: null, + }; + } + private handleRemoteChannelChange(direction: 'up' | 'down'): void { const activeItem = this.stalkerStore.selectedItem(); if (!activeItem?.id) { diff --git a/apps/web/src/app/xtream-electron/live-stream-layout/live-stream-layout.component.scss b/apps/web/src/app/xtream-electron/live-stream-layout/live-stream-layout.component.scss index f8b2963b5..7450a5b04 100644 --- a/apps/web/src/app/xtream-electron/live-stream-layout/live-stream-layout.component.scss +++ b/apps/web/src/app/xtream-electron/live-stream-layout/live-stream-layout.component.scss @@ -39,23 +39,38 @@ display: flex; flex-direction: column; overflow: hidden; + min-height: 0; } .video-player { - flex: 1; - height: 50%; + flex: 1 1 0; + min-height: 0; + overflow: hidden; + display: flex; } .epg { - flex: 1; - height: 50%; + flex: 1 1 0; + min-height: 0; overflow: hidden; } +.video-player > app-web-player-view { + flex: 1; + min-height: 0; +} + .epg-content { height: 100%; - overflow-y: auto; - padding-bottom: 20px; + min-height: 0; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.epg-content > app-epg-view { + flex: 1; + min-height: 0; } .sidebar-header { diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.html b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.html index 396d95ee0..9a90a725d 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.html +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.html @@ -12,78 +12,84 @@ drag_indicator }
- @if (logo()) { - - } +
{{ name() }}
@if (showEpg()) { -
-
- @if (epgProgram(); as program) { -
- {{ program.title }} -
-
- {{ formatTime(program.start) }} - - {{ formatTime(program.stop) }} -
-
-
-
- } @else { -
- {{ 'EPG.NO_PROGRAM_INFO' | translate }} -
- } + @if (epgProgram(); as program) { +
+ {{ program.title }}
-
+
+ {{ + formatTime(program.start) + }} +
+
+
+ {{ + formatTime(program.stop) + }} +
+ } @else { +
+ {{ 'EPG.NO_PROGRAM_INFO' | translate }} +
+ } }
- -
- @if (showEpg() && epgProgram(); as program) { - - } - @if (showFavoriteButton()) { - - } -
+ @if ( + (showProgramInfoButton() && showEpg() && epgProgram()) || + showFavoriteButton() + ) { +
+ @if (showProgramInfoButton() && showEpg() && epgProgram(); as program) { + + } + @if (showFavoriteButton()) { + + } +
+ }
diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.scss b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.scss index 8f131b5ae..82ba34e29 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.scss +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.scss @@ -1,295 +1,301 @@ -// Main channel list item container +:host { + display: block; + width: 100%; + container-type: inline-size; +} + .channel-list-item { width: 100%; + min-height: 68px; display: flex; align-items: center; - height: 68px; - padding: 8px 12px; - border-radius: 4px; - cursor: pointer; - transition: background-color 0.2s ease; gap: 12px; + padding: 8px 10px 8px 12px; + border: 1px solid transparent; + border-radius: 12px; + background: transparent; + color: inherit; + cursor: pointer; box-sizing: border-box; - min-width: 0; overflow: hidden; + transition: + background 0.15s ease, + border-color 0.15s ease, + box-shadow 0.18s ease, + transform 0.18s ease, + color 0.15s ease; &:hover { - background-color: rgba(0, 0, 0, 0.04); + background: color-mix(in srgb, currentColor 8%, transparent); } &.active { - background-color: rgba(0, 0, 0, 0.08); + background: linear-gradient( + 135deg, + var(--app-selection-surface-strong), + var(--app-selection-surface) + ); + border-color: var(--app-selection-border); + color: var(--app-selection-color); + box-shadow: + inset 0 0 0 1px var(--app-selection-border), + 0 12px 20px -18px var(--app-selection-glow); + transform: translateY(-1px); } - // Compact mode when EPG is not shown &.compact { - height: 48px; - padding: 6px 12px; + min-height: 52px; + padding-block: 7px; } } -// Dark theme support -::ng-deep .dark-theme { - .channel-list-item { - &:hover { - background-color: rgba(255, 255, 255, 0.04); - } - - &.active { - background-color: rgba(255, 255, 255, 0.08); - } - } -} - -// Drag handle icon .drag-icon { + flex-shrink: 0; cursor: move; - margin-right: 4px; - opacity: 0.5; + opacity: 0.46; } -// Main content area .channel-content { + min-width: 0; + flex: 1; display: flex; align-items: center; - flex: 1; gap: 12px; - min-width: 0; // Allow text truncation } -// Channel logo -.channel-logo { - width: 42px; - height: 42px; - border-radius: 4px; - object-fit: contain; +.channel-logo-shell { + position: relative; + width: 44px; + height: 44px; flex-shrink: 0; + border-radius: 10px; + display: grid; + place-items: center; + overflow: hidden; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0)), + rgba(127, 127, 127, 0.12); + box-shadow: + inset 0 0 0 1px rgba(255, 255, 255, 0.08), + inset 0 1px 1px rgba(0, 0, 0, 0.15); - // Smaller logo in compact mode .compact & { - width: 32px; - height: 32px; + width: 38px; + height: 38px; } } -// Channel details (name + EPG) -.channel-details { - display: flex; - flex-direction: column; - gap: 2px; - flex: 1; - min-width: 0; // Allow text truncation - overflow: visible; -} - -// Channel name -.channel-name { - font-family: 'DM Sans', sans-serif; - font-weight: 500; - font-size: 0.9rem; - line-height: 1.3; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - min-width: 0; - max-width: 100%; -} - -// EPG container - holds EPG content and info button -.epg-container { - display: flex; - align-items: center; - gap: 8px; - width: 100%; - overflow: visible; -} - -// EPG content area -.epg-content { - display: flex; - flex-direction: column; - gap: 2px; - flex: 1; - min-width: 0; - overflow: hidden; // Allow text truncation within content -} - -// EPG title (program name) -.epg-title { - font-family: - -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', - Arial, sans-serif; - font-size: 0.8rem; - font-weight: 500; - opacity: 0.85; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - line-height: 1.2; - min-width: 0; - max-width: 100%; -} - -// EPG timeline container -.epg-timeline { - display: flex; - flex-direction: column; - gap: 2px; - min-width: 0; - max-width: 100%; -} - -// EPG time display -.epg-time { - font-family: 'DM Sans', sans-serif; - font-size: 0.7rem; - opacity: 0.6; - letter-spacing: 0.02em; - line-height: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - min-width: 0; - max-width: 100%; -} - -// EPG placeholder (when no data available) -.epg-placeholder { - font-family: 'DM Sans', sans-serif; - font-size: 0.75rem; +.channel-logo-fallback { + position: absolute; + inset: 0; + margin: auto; + width: 18px; + height: 18px; + font-size: 18px; opacity: 0.5; - font-style: italic; +} + +.channel-logo { + position: relative; + z-index: 1; + width: 100%; + height: 100%; + object-fit: contain; + padding: 6px; + box-sizing: border-box; +} + +.channel-details { + min-width: 0; + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + gap: 3px; +} + +.channel-name { + min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - min-width: 0; - max-width: 100%; + font-size: 0.96rem; + font-weight: 600; + letter-spacing: 0.01em; + line-height: 1.2; +} + +.epg-title { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 0.82rem; + line-height: 1.2; + opacity: 0.84; +} + +.epg-timeline { + min-width: 0; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 10px; +} + +.epg-time { + font-size: 0.75rem; + line-height: 1; + opacity: 0.72; + font-variant-numeric: tabular-nums; + white-space: nowrap; } -// Progress track .epg-progress-track { position: relative; width: 100%; - height: 3px; - background: rgba(128, 128, 128, 0.2); - border-radius: 2px; + height: 6px; overflow: hidden; + border-radius: 999px; + background-color: rgba(127, 127, 127, 0.28); + background-image: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.12), + rgba(255, 255, 255, 0) + ); + box-shadow: + inset 0 0 0 1px rgba(255, 255, 255, 0.12), + inset 0 1px 1px rgba(0, 0, 0, 0.18); } -// Progress fill .epg-progress-fill { position: absolute; - left: 0; - top: 0; - height: 100%; - background: linear-gradient(90deg, #4f46e5 0%, #7c3aed 50%, #ec4899 100%); - border-radius: 2px; - transition: width 0.5s ease-out; - box-shadow: 0 0 6px rgba(124, 58, 237, 0.4); + inset-block: 0; + inset-inline-start: 0; + border-radius: inherit; + background-color: var(--app-selection-color); + background-image: linear-gradient( + 90deg, + rgba(255, 255, 255, 0.18), + rgba(255, 255, 255, 0) + ); + box-shadow: + 0 0 0 1px color-mix(in srgb, var(--app-selection-border) 82%, transparent), + 0 0 10px -2px var(--app-selection-glow); + transition: width 0.35s ease; } -// Favorite button -.favorite-button { - flex-shrink: 0; +.epg-placeholder { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 0.78rem; + line-height: 1.2; + opacity: 0.52; } -// Program info button -.program-info-button { - flex-shrink: 0; - opacity: 0.7; - - &:hover { - opacity: 1; - } -} - -// Action buttons column (info + favorite stacked vertically) .action-buttons { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; flex-shrink: 0; - margin-left: auto; - gap: 0; + display: flex; + align-items: center; + gap: 2px; + margin-inline-start: auto; button { - width: 32px; - height: 32px; - line-height: 32px; - padding: 0; + width: 34px; + height: 34px; + border-radius: 10px; + color: inherit; + opacity: 0.76; + transition: + opacity 0.15s ease, + background-color 0.15s ease, + transform 0.15s ease; - .mat-icon { - font-size: 20px; - width: 20px; - height: 20px; - line-height: 20px; + &:hover { + opacity: 1; + background: color-mix(in srgb, currentColor 10%, transparent); + transform: translateY(-1px); } } + + mat-icon { + width: 20px; + height: 20px; + font-size: 20px; + } } -// ============================================================================ -// Container queries for responsive sidebar resizing -// ============================================================================ +.channel-list-item.active { + .epg-title, + .epg-time, + .epg-placeholder, + .drag-icon, + .action-buttons button, + .action-buttons mat-icon { + color: inherit; + } +} -@container (max-width: 350px) { +@container (max-width: 360px) { .channel-list-item { - gap: 8px; - padding: 6px 8px; + gap: 10px; + padding-inline: 10px 8px; } - .channel-logo { - width: 36px; - height: 36px; + .channel-logo-shell { + width: 40px; + height: 40px; } - // Hide EPG timeline (progress bar + times) but keep program title .epg-timeline { - display: none; + gap: 8px; } - .epg-placeholder { - display: none; + .action-buttons { + gap: 0; + } +} + +@container (max-width: 310px) { + .epg-timeline { + grid-template-columns: 1fr; + gap: 5px; } - // Hide info button, keep only favorite + .epg-time:last-child { + display: none; + } +} + +@container (max-width: 270px) { + .channel-list-item { + min-height: 52px; + gap: 8px; + padding-inline: 8px 6px; + } + + .channel-logo-shell { + width: 34px; + height: 34px; + } + + .epg-title, + .epg-timeline, + .epg-placeholder, .program-info-button { display: none; } } -@container (max-width: 280px) { - .channel-logo { - width: 28px; - height: 28px; - } - - // Hide all EPG content - .epg-container { - display: none; - } - - .channel-list-item { - height: 48px; - - &.compact { - height: 40px; - } - } - - // Hide action buttons +@container (max-width: 220px) { + .channel-logo-shell, .action-buttons { display: none; } -} -@container (max-width: 200px) { - .channel-logo { - display: none; - } - - .channel-list-item { - gap: 4px; - padding: 4px 6px; + .channel-content { + gap: 0; } } diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.ts b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.ts index d1f9889d9..09463e600 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.ts +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.ts @@ -3,9 +3,11 @@ import { NgStyle } from '@angular/common'; import { ChangeDetectionStrategy, Component, + effect, inject, input, output, + signal, } from '@angular/core'; import { MatIconButton } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; @@ -31,11 +33,13 @@ import { EpgItemDescriptionComponent } from '../../epg-list/epg-item-description }) export class ChannelListItemComponent { private readonly dialog = inject(MatDialog); + private readonly logoFailed = signal(false); readonly isDraggable = input(false); - readonly logo = input.required(); + readonly logo = input(''); readonly name = input(''); readonly showFavoriteButton = input(false); + readonly showProgramInfoButton = input(true); readonly isFavorite = input(false); readonly selected = input(false); readonly showEpg = input(true); @@ -46,6 +50,13 @@ export class ChannelListItemComponent { readonly clicked = output(); readonly favoriteToggled = output(); + constructor() { + effect(() => { + this.logo(); + this.logoFailed.set(false); + }); + } + /** * Formats time for display (HH:mm) */ @@ -69,4 +80,21 @@ export class ChannelListItemComponent { data: program, }); } + + onFavoriteClick(event: MouseEvent): void { + event.stopPropagation(); + this.favoriteToggled.emit(event); + } + + showLogoFallback(): boolean { + return !this.logo() || this.logoFailed(); + } + + onLogoError(event: Event): void { + this.logoFailed.set(true); + (event.target as HTMLImageElement | null)?.style.setProperty( + 'display', + 'none' + ); + } } diff --git a/libs/ui/components/src/lib/vjs-player/vjs-player.component.scss b/libs/ui/components/src/lib/vjs-player/vjs-player.component.scss index 89e7e58b5..99c83a210 100644 --- a/libs/ui/components/src/lib/vjs-player/vjs-player.component.scss +++ b/libs/ui/components/src/lib/vjs-player/vjs-player.component.scss @@ -1,7 +1,15 @@ +:host { + display: block; + width: 100%; + height: 100%; + min-height: 0; + overflow: hidden; +} + .video-js { position: relative !important; width: 100% !important; - height: calc(100vh - 65px); + height: 100% !important; } /* Hide the captions settings item from the captions menu. */ @@ -16,9 +24,3 @@ .video-js .vjs-menu-button-popup .vjs-menu { z-index: 10; } - -@media only screen and (max-width: 599px) { - .video-js { - height: calc(50vh - 65px) !important; - } -} diff --git a/libs/ui/shared-portals/src/lib/epg-view/epg-view.component.html b/libs/ui/shared-portals/src/lib/epg-view/epg-view.component.html index 0c904885c..422853cc7 100644 --- a/libs/ui/shared-portals/src/lib/epg-view/epg-view.component.html +++ b/libs/ui/shared-portals/src/lib/epg-view/epg-view.component.html @@ -1,37 +1,49 @@
{{ 'EPG.TITLE' | translate }}
- + +
@for (item of epgItems; track $index) { - -
{{ item.title }}
-
- {{ item.description }} -
-
- {{ item.start | date: 'HH:mm' }} - - {{ item.stop ?? item.end | date: 'HH:mm (dd.MM.yyyy)' }} -
- - - - @if (isCurrentProgram(item)) { -
-
+
+ +
{{ item.title }}
+ + @if (item.description) { +
+ {{ item.description }}
} -
+ + @if (isCurrentProgram(item)) { +
+
+
+
+
+ } + } @if (epgItems?.length === 0) { - +
{{ 'EPG.EPG_NOT_AVAILABLE_CHANNEL_TITLE' | translate }} - +
} - +
diff --git a/libs/ui/shared-portals/src/lib/epg-view/epg-view.component.scss b/libs/ui/shared-portals/src/lib/epg-view/epg-view.component.scss index 710d413a3..54ba6824a 100644 --- a/libs/ui/shared-portals/src/lib/epg-view/epg-view.component.scss +++ b/libs/ui/shared-portals/src/lib/epg-view/epg-view.component.scss @@ -1,33 +1,192 @@ -.mat-mdc-list-item.current-program { - background-color: rgba(29, 185, 84, 0.2); - - .title { - color: #1db954; - } -} - -.title { - font-weight: bold; +:host { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + overflow: hidden; } .epg-title { margin: 0; - font-size: 1.2rem; - font-weight: 500; - padding: 16px; + padding: 14px 16px 10px; + font-size: 1.02rem; + font-weight: 620; + letter-spacing: 0.01em; + flex-shrink: 0; + position: sticky; + top: 0; + z-index: 2; + background: + linear-gradient(180deg, rgba(18, 24, 38, 0.98), rgba(18, 24, 38, 0.94)), + rgba(12, 16, 24, 0.96); + border-bottom: 1px solid rgba(255, 255, 255, 0.06); } -.progress-container { - width: 100%; - height: 3px; - background: rgba(255, 255, 255, 0.1); - margin-top: 8px; - border-radius: 2px; - overflow: hidden; - - .progress { - height: 100%; - background: #1db954; - transition: width 1s linear; +.epg-list { + flex: 1; + min-height: 0; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 6px; + padding: 10px 10px 22px; + box-sizing: border-box; + scrollbar-gutter: stable; +} + +.epg-item { + display: flex; + flex-direction: column; + gap: 10px; + padding: 10px 14px 16px; + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 14px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0)), + rgba(127, 127, 127, 0.045); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.04), + 0 10px 20px -20px rgba(0, 0, 0, 0.35); + transition: + background 0.15s ease, + border-color 0.15s ease, + box-shadow 0.18s ease, + transform 0.18s ease; + + &:hover { + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0)), + rgba(127, 127, 127, 0.065); + transform: translateY(-1px); } -} \ No newline at end of file + + &.current-program { + background: linear-gradient( + 135deg, + var(--app-selection-surface-strong), + var(--app-selection-surface) + ); + border-color: var(--app-selection-border); + box-shadow: + inset 0 0 0 1px var(--app-selection-border), + 0 12px 20px -18px var(--app-selection-glow); + + .epg-item__title, + .epg-item__time, + .epg-item__description, + .epg-item__info { + color: var(--app-selection-color); + } + } +} + +.epg-item__meta { + display: flex; + align-items: center; + gap: 8px; +} + +.epg-item__time { + font-size: 0.78rem; + line-height: 1; + font-weight: 600; + letter-spacing: 0.02em; + font-variant-numeric: tabular-nums; + opacity: 0.76; +} + +.epg-item__badge { + padding: 2px 8px; + border-radius: 999px; + font-size: 0.68rem; + line-height: 1.4; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + background: color-mix(in srgb, var(--app-selection-color) 18%, transparent); + color: var(--app-selection-color); + border: 1px solid color-mix(in srgb, var(--app-selection-border) 86%, transparent); +} + +.epg-item__info { + margin-inline-start: auto; + width: 34px; + height: 34px; + border-radius: 10px; + color: inherit; + opacity: 0.76; + transition: + opacity 0.15s ease, + background-color 0.15s ease, + transform 0.15s ease; + + &:hover { + opacity: 1; + background: color-mix(in srgb, currentColor 10%, transparent); + transform: translateY(-1px); + } +} + +.epg-item__title { + font-size: 0.94rem; + line-height: 1.35; + font-weight: 560; + letter-spacing: 0.005em; +} + +.epg-item__description { + font-size: 0.82rem; + line-height: 1.4; + opacity: 0.74; + display: -webkit-box; + overflow: hidden; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.epg-item__progress { + padding-top: 4px; +} + +.epg-progress-track { + position: relative; + width: 100%; + height: 6px; + overflow: hidden; + border-radius: 999px; + background-color: rgba(127, 127, 127, 0.28); + background-image: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.12), + rgba(255, 255, 255, 0) + ); + box-shadow: + inset 0 0 0 1px rgba(255, 255, 255, 0.12), + inset 0 1px 1px rgba(0, 0, 0, 0.18); +} + +.epg-progress-fill { + position: absolute; + inset-block: 0; + inset-inline-start: 0; + border-radius: inherit; + background-color: var(--app-selection-color); + background-image: linear-gradient( + 90deg, + rgba(255, 255, 255, 0.18), + rgba(255, 255, 255, 0) + ); + box-shadow: + 0 0 0 1px color-mix(in srgb, var(--app-selection-border) 82%, transparent), + 0 0 10px -2px var(--app-selection-glow); + transition: width 0.35s ease; +} + +.epg-empty { + padding: 22px; + border: 1px dashed rgba(255, 255, 255, 0.12); + border-radius: 14px; + text-align: center; + font-size: 0.88rem; + opacity: 0.66; +} diff --git a/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.scss b/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.scss index 2158f469b..4a9e1a447 100644 --- a/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.scss +++ b/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.scss @@ -2,13 +2,16 @@ :host { width: 100%; + height: 100%; + min-height: 0; + display: block; } .itv-container { display: grid; // Use auto for sidebar to respect the directive's width grid-template-columns: auto 1fr 1fr; - grid-template-rows: 50% 50%; + grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); gap: 0 0; grid-auto-flow: row; grid-template-areas: @@ -21,7 +24,7 @@ display: grid; // Use auto for sidebar to respect the directive's width grid-template-columns: auto 1fr 1fr; - grid-template-rows: 1fr 1fr 1fr; + grid-template-rows: repeat(3, minmax(0, 1fr)); gap: 0 0; grid-auto-flow: row; grid-template-areas: @@ -44,9 +47,23 @@ flex-direction: column; } -.video-player { grid-area: video-player; } +.video-player { + grid-area: video-player; + min-height: 0; + overflow: hidden; + display: flex; +} -.epg { grid-area: epg; } +.video-player > app-web-player-view { + flex: 1; + min-height: 0; +} + +.epg { + grid-area: epg; + min-height: 0; + overflow: hidden; +} .search-panel { margin-top: 10px; @@ -61,10 +78,17 @@ } .epg-content { - height: calc(50vh - 20px); - overflow-y: auto; + height: 100%; + min-height: 0; + overflow: hidden; + display: flex; + flex-direction: column; border-right: #111 1px solid; - padding-bottom: 20px; +} + +.epg-content > app-epg-view { + flex: 1; + min-height: 0; } .sidebar-content { @@ -175,4 +199,3 @@ } } } - diff --git a/libs/ui/shared-portals/src/lib/web-player-view/web-player-view.component.scss b/libs/ui/shared-portals/src/lib/web-player-view/web-player-view.component.scss index f06df8dbd..621bc4511 100644 --- a/libs/ui/shared-portals/src/lib/web-player-view/web-player-view.component.scss +++ b/libs/ui/shared-portals/src/lib/web-player-view/web-player-view.component.scss @@ -1,12 +1,25 @@ -app-vjs-player > .video-js { - height: 50vh !important; +:host { + display: block; + width: 100%; + height: 100%; + min-height: 0; + overflow: hidden; + background: #000; } -app-html-video-player > #video-player { - height: 50vh !important; +app-vjs-player, +app-html-video-player, +app-art-player { + display: block; + width: 100%; + height: 100%; + min-height: 0; + overflow: hidden; } +app-vjs-player > .video-js, +app-html-video-player > #video-player, app-art-player > .artplayer-container { width: 100% !important; - height: 300px !important; + height: 100% !important; }