From 5c2cf3f345fda9914dea04ef3bd7b250f458ea81 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 6 Sep 2026 13:25:37 +0200 Subject: [PATCH] refactor(epg): remove the multi-EPG overlay and the channel-range IPC Co-Authored-By: Claude Fable 5.1 --- .../src/app/events/epg-query.service.ts | 48 - apps/web/src/styles.scss | 6 +- .../window-controls.component.ts | 4 +- libs/ui/epg/src/index.ts | 2 - .../epg-item-description.component.ts | 4 +- .../multi-epg-container.component.html | 277 ------ .../multi-epg-container.component.scss | 916 ------------------ .../multi-epg-container.component.spec.ts | 180 ---- .../multi-epg-container.component.ts | 557 ----------- .../src/lib/multi-epg/overlay-ref.token.ts | 9 - tools/eslint/max-lines-baseline.mjs | 1 - 11 files changed, 5 insertions(+), 1999 deletions(-) delete mode 100644 libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.html delete mode 100644 libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.scss delete mode 100644 libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.spec.ts delete mode 100644 libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.ts delete mode 100644 libs/ui/epg/src/lib/multi-epg/overlay-ref.token.ts diff --git a/apps/electron-backend/src/app/events/epg-query.service.ts b/apps/electron-backend/src/app/events/epg-query.service.ts index 20f9c60df..c91bc567e 100644 --- a/apps/electron-backend/src/app/events/epg-query.service.ts +++ b/apps/electron-backend/src/app/events/epg-query.service.ts @@ -400,54 +400,6 @@ export class EpgQueryService { } } - async getChannelsByRange( - skip: number, - limit: number - ): Promise< - Array<{ - id: string; - displayName: string; - iconUrl: string | null; - programs: EpgProgram[]; - }> - > { - try { - const db = await getDatabase(); - const channels = await db - .select({ - id: schema.epgChannels.id, - displayName: schema.epgChannels.displayName, - iconUrl: schema.epgChannels.iconUrl, - }) - .from(schema.epgChannels) - .orderBy(schema.epgChannels.displayName) - .offset(skip) - .limit(limit); - - return Promise.all( - channels.map(async (channel) => { - const programs = await db - .select() - .from(schema.epgPrograms) - .where(eq(schema.epgPrograms.channelId, channel.id)) - .orderBy(schema.epgPrograms.start); - - return { - ...channel, - programs: programs.map(toEpgProgramFromRow), - }; - }) - ); - } catch (error) { - epgLogger.error( - this.loggerLabel, - 'Error getting channels by range:', - error - ); - return []; - } - } - private normalizeChannelLookupKeys(channelIds: string[]): string[] { return Array.from( new Set( diff --git a/apps/web/src/styles.scss b/apps/web/src/styles.scss index e6e8659ad..8b326b5a0 100644 --- a/apps/web/src/styles.scss +++ b/apps/web/src/styles.scss @@ -98,15 +98,11 @@ app-root.macos-platform .current-playlist { // Windows/Linux frameless title bar: reserve room in top-aligned drag // regions for the renderer-drawn window controls (3 × 46px buttons). -// Body-level class so it also reaches cdk-overlay content (multi-EPG). +// Body-level class so it also reaches cdk-overlay content (dialogs, menus). body.frameless-platform .workspace-header { padding-right: 150px; } -body.frameless-platform #epg-navigation { - padding-right: 150px; -} - // ─── Base ───────────────────────────────────────────────────────────────────── // Native UI (scrollbars, form controls, popups) must follow the app theme, // not the OS preference. Chromium colors unstyled/native scrollbars from the diff --git a/libs/ui/components/src/lib/window-controls/window-controls.component.ts b/libs/ui/components/src/lib/window-controls/window-controls.component.ts index 861c06ef1..dee3e0d77 100644 --- a/libs/ui/components/src/lib/window-controls/window-controls.component.ts +++ b/libs/ui/components/src/lib/window-controls/window-controls.component.ts @@ -15,7 +15,7 @@ import { ElectronBridgeWindowState } from '@iptvnator/shared/interfaces'; * native traffic lights instead. * * Rendered once in the app root so the buttons stay reachable above - * full-window content such as the multi-EPG overlay and dialog backdrops — + * full-window content such as dialog backdrops — * mirroring how the macOS traffic lights float above everything. CDK * overlays render as popovers in the browser top layer (above any * z-index), so the host is itself a manual popover and re-enters the top @@ -91,7 +91,7 @@ export class WindowControlsComponent implements AfterViewInit { /** * Re-enter the top layer after any other popover opens: top-layer * elements paint in insertion order, so hiding and re-showing puts the - * controls back above freshly opened CDK overlays (dialogs, multi-EPG). + * controls back above freshly opened CDK overlays (dialogs, menus). */ private readonly onDocumentToggle = (event: Event): void => { const newState = (event as { newState?: string }).newState; diff --git a/libs/ui/epg/src/index.ts b/libs/ui/epg/src/index.ts index 6a5544417..4bdb5bff2 100644 --- a/libs/ui/epg/src/index.ts +++ b/libs/ui/epg/src/index.ts @@ -28,5 +28,3 @@ export * from './lib/epg-guide/epg-guide-layout.util'; export * from './lib/epg-guide/epg-guide-preferences'; export * from './lib/epg-guide/epg-guide.component'; export * from './lib/epg-guide/epg-guide-now-playing.component'; -export * from './lib/multi-epg/multi-epg-container.component'; -export * from './lib/multi-epg/overlay-ref.token'; diff --git a/libs/ui/epg/src/lib/epg-item-description/epg-item-description.component.ts b/libs/ui/epg/src/lib/epg-item-description/epg-item-description.component.ts index d405b1273..a6e024588 100644 --- a/libs/ui/epg/src/lib/epg-item-description/epg-item-description.component.ts +++ b/libs/ui/epg/src/lib/epg-item-description/epg-item-description.component.ts @@ -79,8 +79,8 @@ export class EpgItemDescriptionComponent { this.archiveUnavailableNote = this.dialogData.archiveUnavailableNote ?? false; // Opened imperatively from six surfaces (timeline, list, channel rows, - // the multi-EPG grid and its search), so the dialog reads the display - // offset itself instead of trusting every opener to forward it. + // the programme guide grid and its search), so the dialog reads the + // display offset itself instead of trusting every opener to forward it. const offsetMinutes = this.settingsStore.resolvedEpgOffsetMinutes(); this.startMs = getProgramTimeMs( this.epgProgram.start, diff --git a/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.html b/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.html deleted file mode 100644 index 7231f4238..000000000 --- a/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.html +++ /dev/null @@ -1,277 +0,0 @@ -
- - -
- {{ selectedDayDate() | date: 'MMMM d, EEEE' : undefined : currentLocale() }} -
- - - - -
- @if (isSearchExpanded()) { - - {{ filteredChannels().length }} / {{ channels().length }} - } - -
- - -
- @if (isProgramSearchOpen()) { -
- manage_search - - @if (programSearchQuery()) { - - } - @if (isSearchingPrograms()) { -
- } -
- @if (programSearchResults().length > 0) { -
- @for (program of programSearchResults(); track program.start) { - - } -
- } - @if (programSearchQuery().length >= 2 && programSearchResults().length === 0 && !isSearchingPrograms()) { -
- search_off - No programs found -
- } - } - -
-
-
- - @for (item of filteredChannels(); track item.id; let i = $index) { - - - - - -
- @if (getChannelIcon(item)) { - - } - {{ getChannelName(item) }} -
-
-
-
- } -
-
- - - @for (hour of timeHeader; track hour) { - - - - {{ hour }}:00 - - - - - } - - - @for (item of filteredChannels(); track item.id; let i = $index) { - - @for (program of item.programs; track trackByProgram($index, program)) { - - - -
{{ program.title }}
-
-
- } -
- } - - - - - - {{ currentTimeLabel() }} - -
-
-
- - -@if (filteredChannels().length === 0 && channelFilter()) { -
- search_off -

No channels match "{{ channelFilter() }}"

- -
-} diff --git a/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.scss b/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.scss deleted file mode 100644 index 94bf9af24..000000000 --- a/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.scss +++ /dev/null @@ -1,916 +0,0 @@ -// Color palette — every value resolves through the app's theme tokens so -// Multi-EPG looks like part of IPTVnator instead of a separate broadcast -// utility. Previously hard-coded #0a0a0f / #12121a / #1a1a24 made the grid -// switch to near-black while the rest of the app sat on --app-content-bg, -// which the UX audit called out as the screen "visually detaching" from -// the rest of the app. -$bg-deep: var(--app-content-bg); -$bg-surface: var(--app-widget-bg); -$bg-elevated: var(--app-widget-header-bg); -$bg-hover: var(--app-card-hover-bg); -$border-subtle: var(--app-widget-header-border); -$border-medium: var(--app-separator); - -// Accent colors — unified with the rest of the app: -// cyan stays on the "now" indicator only; blue is the primary; the old -// $8b5cf6 purple and $00d4aa teal are dropped per the UX audit. -$accent-primary: #5cd6ff; // Cyan — reserved for "now" indicator -$accent-now-glow: rgba(92, 214, 255, 0.45); // Cyan glow for the now-line -$accent-secondary: var(--app-selection-color); // Blue — programs -$accent-tertiary: var(--app-selection-color); // Blue — highlights -$accent-warning: #fbbf24; - -// Text colors — derived from app heading/body so light and dark themes -// stay in sync without a separate `.light-theme` override at the bottom -// (which was duplicating responsibility with the same token chain). -$text-primary: var(--app-heading-color); -$text-secondary: var(--app-body-color); -$text-muted: var(--app-muted-color); - -// Gradients -$gradient-program: linear-gradient( - 135deg, - rgba(79, 142, 255, 0.15) 0%, - rgba(79, 142, 255, 0.05) 100% -); -$gradient-program-hover: linear-gradient( - 135deg, - rgba(79, 142, 255, 0.3) 0%, - rgba(79, 142, 255, 0.15) 100% -); -$gradient-header: linear-gradient( - 180deg, - rgba(92, 214, 255, 0.08) 0%, - transparent 100% -); - -// Base container -.parent { - display: flex; - background: $bg-deep; - color: $text-primary; - font-family: Roboto, 'Helvetica Neue', sans-serif; - position: relative; - overflow-y: auto; - height: calc(100vh - 56px); - - // Custom scrollbar for vertical scroll - &::-webkit-scrollbar { - width: 8px; - } - - &::-webkit-scrollbar-track { - background: $bg-surface; - } - - &::-webkit-scrollbar-thumb { - background: rgba(47, 123, 255, 0.4); - border-radius: 4px; - - &:hover { - background: rgba(47, 123, 255, 0.6); - } - } -} - -// Navigation toolbar - Frosted glass effect -#epg-navigation { - display: flex; - align-items: center; - padding: 12px 16px 12px 80px; // Left padding for macOS traffic lights - background: rgba(18, 18, 26, 0.85); - backdrop-filter: blur(20px); - border-bottom: 1px solid $border-medium; - gap: 8px; - position: relative; - z-index: 100; - -webkit-app-region: drag; - - // Subtle glow at bottom - &::after { - content: ''; - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 1px; - background: linear-gradient( - 90deg, - transparent 0%, - rgba(92, 214, 255, 0.3) 20%, - rgba(47, 123, 255, 0.3) 80%, - transparent 100% - ); - } - - // Button styling - exclude from drag region - button { - font-family: Roboto, 'Helvetica Neue', sans-serif; - font-weight: 500; - letter-spacing: 0.02em; - transition: all 0.2s ease; - -webkit-app-region: no-drag; - - &:not([disabled]):hover { - background: rgba(47, 123, 255, 0.15); - } - - &[disabled] { - opacity: 0.3; - } - - mat-icon { - font-size: 20px; - } - } -} - -// Search container - exclude from drag region -.search-container { - display: flex; - align-items: center; - gap: 8px; - margin-left: auto; - -webkit-app-region: no-drag; - - .channel-search { - display: flex; - align-items: center; - gap: 8px; - width: 200px; - height: 36px; - padding: 0 12px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid $border-subtle; - border-radius: 8px; - transition: all 0.2s ease; - - &:focus-within { - border-color: rgba(47, 123, 255, 0.5); - background: rgba(255, 255, 255, 0.08); - } - - .search-icon { - font-size: 18px; - width: 18px; - height: 18px; - color: $text-muted; - flex-shrink: 0; - } - - input { - flex: 1; - background: transparent; - border: none; - outline: none; - font-size: 13px; - font-family: Roboto, 'Helvetica Neue', sans-serif; - color: $text-primary; - min-width: 0; - - &::placeholder { - color: $text-muted; - } - } - - .clear-btn { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - padding: 0; - background: transparent; - border: none; - border-radius: 50%; - cursor: pointer; - color: $text-muted; - transition: all 0.15s ease; - flex-shrink: 0; - - &:hover { - background: rgba(255, 255, 255, 0.1); - color: $text-primary; - } - - mat-icon { - font-size: 16px; - width: 16px; - height: 16px; - } - } - } - - .filter-count { - font-size: 11px; - color: $text-muted; - font-family: 'JetBrains Mono', monospace; - white-space: nowrap; - } - - &.expanded { - .channel-search { - animation: slideIn 0.2s ease-out; - } - } -} - -@keyframes slideIn { - from { - opacity: 0; - width: 0; - transform: translateX(20px); - } - to { - opacity: 1; - width: 200px; - transform: translateX(0); - } -} - -// Program search container -.program-search-container { - display: flex; - align-items: center; - gap: 8px; - position: relative; - -webkit-app-region: no-drag; - - .program-search-input { - display: flex; - align-items: center; - gap: 8px; - width: 220px; - height: 36px; - padding: 0 12px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid $border-subtle; - border-radius: 8px; - transition: all 0.2s ease; - - &:focus-within { - border-color: rgba(47, 123, 255, 0.5); - background: rgba(255, 255, 255, 0.08); - } - - .search-icon { - font-size: 18px; - width: 18px; - height: 18px; - color: $accent-secondary; - flex-shrink: 0; - } - - input { - flex: 1; - background: transparent; - border: none; - outline: none; - font-size: 13px; - font-family: Roboto, 'Helvetica Neue', sans-serif; - color: $text-primary; - min-width: 0; - - &::placeholder { - color: $text-muted; - } - } - - .clear-btn { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - padding: 0; - background: transparent; - border: none; - border-radius: 50%; - cursor: pointer; - color: $text-muted; - transition: all 0.15s ease; - flex-shrink: 0; - - &:hover { - background: rgba(255, 255, 255, 0.1); - color: $text-primary; - } - - mat-icon { - font-size: 16px; - width: 16px; - height: 16px; - } - } - - .search-spinner { - width: 16px; - height: 16px; - border: 2px solid $border-subtle; - border-top-color: $accent-secondary; - border-radius: 50%; - animation: spin 0.8s linear infinite; - flex-shrink: 0; - } - } - - .program-search-results { - position: fixed; - top: 60px; - right: 16px; - width: 320px; - max-height: 400px; - overflow-y: auto; - background: $bg-elevated; - border: 1px solid $border-medium; - border-radius: 8px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); - z-index: 10000; - - &::-webkit-scrollbar { - width: 6px; - } - - &::-webkit-scrollbar-track { - background: transparent; - } - - &::-webkit-scrollbar-thumb { - background: rgba(47, 123, 255, 0.3); - border-radius: 3px; - } - - .search-result-item { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - padding: 12px 16px; - background: transparent; - border: none; - border-bottom: 1px solid $border-subtle; - cursor: pointer; - text-align: left; - transition: background 0.15s ease; - - &:last-child { - border-bottom: none; - } - - &:hover { - background: $bg-hover; - - .result-arrow { - opacity: 1; - transform: translateX(0); - } - } - - .result-info { - display: flex; - flex-direction: column; - gap: 4px; - overflow: hidden; - } - - .result-title { - font-size: 13px; - font-weight: 500; - color: $text-primary; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .result-meta { - display: flex; - gap: 8px; - font-size: 11px; - color: $text-muted; - } - - .result-time { - color: $accent-primary; - font-family: 'JetBrains Mono', monospace; - } - - .result-date { - color: $text-muted; - } - - .result-arrow { - font-size: 18px; - width: 18px; - height: 18px; - color: $accent-secondary; - opacity: 0; - transform: translateX(-4px); - transition: all 0.15s ease; - } - } - } - - .program-search-empty { - position: fixed; - top: 60px; - right: 16px; - width: 220px; - padding: 24px 16px; - background: $bg-elevated; - border: 1px solid $border-medium; - border-radius: 8px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); - z-index: 10000; - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - color: $text-muted; - - mat-icon { - font-size: 24px; - width: 24px; - height: 24px; - opacity: 0.5; - } - - span { - font-size: 12px; - } - } - - &.open { - .program-search-input { - animation: slideIn 0.2s ease-out; - } - } -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -@keyframes highlightPulse { - 0%, 100% { - filter: drop-shadow(0 0 8px rgba(92, 214, 255, 0.6)); - } - 50% { - filter: drop-shadow(0 0 20px rgba(92, 214, 255, 0.9)) - drop-shadow(0 0 40px rgba(92, 214, 255, 0.4)); - } -} - -// No results state -.no-results { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - display: flex; - flex-direction: column; - align-items: center; - gap: 16px; - color: $text-muted; - text-align: center; - z-index: 100; - - mat-icon { - font-size: 48px; - width: 48px; - height: 48px; - opacity: 0.5; - } - - p { - margin: 0; - font-size: 14px; - } - - button { - color: $accent-primary; - } -} - -// Today's date display -.today-date { - flex: 1; - font-family: 'JetBrains Mono', monospace; - font-size: 14px; - font-weight: 500; - color: $accent-primary; - text-align: center; - padding: 0 24px; - letter-spacing: 0.05em; - text-transform: uppercase; - - // Glowing text effect - text-shadow: 0 0 20px rgba(92, 214, 255, 0.3); -} - -// Channel column — bumped from 140px to 180px per the UX audit so long -// channel names ("13th Street Universal HD", "ARD alpha", etc.) stop being -// truncated to 5 characters. -#channels-column { - width: 180px; - flex: none; - border-right: 1px solid $border-medium; - background: $bg-surface; - position: relative; - z-index: 10; - min-height: calc(100vh - 6px); - - // Subtle shadow for depth - box-shadow: 4px 0 20px rgba(0, 0, 0, 0.3); - - .channel { - cursor: pointer; - transition: all 0.15s ease; - - rect { - fill: $bg-surface; - stroke: $border-subtle; - transition: fill 0.15s ease; - } - - &:hover rect { - fill: $bg-elevated; - } - - &.active rect { - fill: rgba(47, 123, 255, 0.2); - stroke: rgba(47, 123, 255, 0.4); - } - - &.active .channel-name { - color: $accent-secondary; - font-weight: 600; - } - } - - .channel-name { - color: $text-primary; - padding: 8px 12px; - font-size: 12.5px; - font-weight: 500; - display: flex; - align-items: center; - gap: 10px; - height: 100%; - overflow: hidden; - letter-spacing: 0.01em; - line-height: 1.2; - - img { - // Smaller logo (24px) per audit recommendation so the channel - // name has room to breathe in the 180px column. - width: 24px !important; - height: 24px !important; - margin-right: 0 !important; - flex: 0 0 auto; - border-radius: 4px; - background: rgba(255, 255, 255, 0.05); - padding: 2px; - object-fit: contain; - } - - span { - // Allow up to two lines so "13th Street Universal HD" and - // similar long names wrap instead of disappearing into an - // ellipsis after 5 characters. - min-width: 0; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; - word-break: break-word; - } - } - - image { - object-fit: contain; - background: $bg-elevated; - } -} - -// EPG container -#epg-container { - width: calc(100vw - 180px); - overflow-x: scroll; - overflow-y: clip; - background: $bg-deep; - min-height: calc(100vh - 6px); - - // Custom scrollbar - &::-webkit-scrollbar { - height: 8px; - } - - &::-webkit-scrollbar-track { - background: $bg-surface; - } - - &::-webkit-scrollbar-thumb { - background: rgba(47, 123, 255, 0.4); - border-radius: 4px; - - &:hover { - background: rgba(47, 123, 255, 0.6); - } - } -} - -// EPG SVG container -#epg-svg { - // Time header styling - g > rect[fill='#000'] { - fill: $bg-surface !important; - } - - g > text { - font-family: 'JetBrains Mono', monospace !important; - font-size: 11px !important; - font-weight: 500; - fill: $text-secondary !important; - letter-spacing: 0.1em; - } - - // Hour separator vertical lines - .hour-separator { - stroke: $border-subtle; - stroke-width: 1px; - pointer-events: none; - } - - // Program blocks - .program { - cursor: pointer; - - rect { - fill: $bg-elevated; - stroke: $border-subtle; - rx: 4; - ry: 4; - transition: all 0.15s ease; - } - - &:hover rect { - fill: $bg-hover; - stroke: rgba(47, 123, 255, 0.4); - filter: drop-shadow(0 0 8px rgba(47, 123, 255, 0.2)); - } - - // Highlighted program (from search) - &.highlighted rect { - fill: rgba(92, 214, 255, 0.25); - stroke: $accent-primary; - stroke-width: 2px; - animation: highlightPulse 1s ease-in-out 3; - } - - &.highlighted .program-title { - color: $accent-primary; - font-weight: 600; - } - } - - // Active channel row highlighting - .active-row .program rect { - fill: rgba(47, 123, 255, 0.15); - stroke: rgba(47, 123, 255, 0.3); - } - - .active-row .program:hover rect { - fill: rgba(47, 123, 255, 0.25); - } - - .program-title { - color: $text-primary; - padding: 6px 10px; - font-size: 11px; - font-weight: 400; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - pointer-events: none; - letter-spacing: 0.01em; - line-height: 1.4; - - // Subtle gradient text for depth - display: flex; - align-items: center; - height: 100%; - } - - // Current time line - glowing effect - line[stroke='red'] { - stroke: $accent-primary !important; - stroke-width: 2px; - filter: drop-shadow(0 0 8px rgba(92, 214, 255, 0.6)) - drop-shadow(0 0 16px rgba(92, 214, 255, 0.3)); - - // Animated pulse - animation: timePulse 2s ease-in-out infinite; - } - - // Now-line time badge — a small filled pill sitting on top of the time - // header that reads "HH:MM". Keeps the cyan visual language of the line - // itself so the relationship is unmistakable. - .now-label { - pointer-events: none; - - rect { - fill: $accent-primary; - stroke: none; - filter: drop-shadow(0 0 6px $accent-now-glow); - } - - text { - fill: var(--app-shell-bg, #0d0f12); - font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, - monospace; - font-size: 10.5px; - font-weight: 700; - letter-spacing: 0.04em; - } - } - - // Currently-airing program — a thin cyan border on every program block - // whose time window contains the now-line. Audit recommended this to - // make "what's airing right now" scannable without finding the line - // first then tracing it down each row. - .program.is-now { - rect { - stroke: $accent-primary; - stroke-width: 1.5px; - } - } -} - -// Pulse animation for time line -@keyframes timePulse { - 0%, - 100% { - opacity: 1; - filter: drop-shadow(0 0 8px rgba(92, 214, 255, 0.6)) - drop-shadow(0 0 16px rgba(92, 214, 255, 0.3)); - } - 50% { - opacity: 0.8; - filter: drop-shadow(0 0 12px rgba(92, 214, 255, 0.8)) - drop-shadow(0 0 24px rgba(92, 214, 255, 0.4)); - } -} - -// Legacy classes cleanup -.program-item { - cursor: pointer; -} - -rect { - stroke: $border-subtle; - cursor: pointer; -} - -.channel > rect { - stroke: $border-subtle; -} - -.program-name, -.channel-name { - -webkit-line-clamp: 2; - height: 32px; - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-box-orient: vertical; - padding: 6px 8px; -} - -#current-time-line { - stroke-width: 2px; - stroke: $accent-primary; -} - -// Dark theme override (already dark, but ensure consistency) -::ng-deep { - .dark-theme { - #epg-navigation { - background: rgba(18, 18, 26, 0.85); - } - } - - // Light theme adjustments - .light-theme { - #epg-navigation { - background: rgba(255, 255, 255, 0.9); - border-bottom-color: rgba(0, 0, 0, 0.1); - - .today-date { - color: #059669; - text-shadow: none; - } - } - - .parent { - background: #f8fafc; - - &::before { - background-image: - linear-gradient(rgba(0, 0, 0, 0.03) 1px, transparent 1px), - linear-gradient( - 90deg, - rgba(0, 0, 0, 0.03) 1px, - transparent 1px - ); - } - } - - #channels-column { - background: #ffffff; - border-right-color: rgba(0, 0, 0, 0.1); - box-shadow: 4px 0 20px rgba(0, 0, 0, 0.05); - - .channel rect { - fill: #ffffff; - stroke: rgba(0, 0, 0, 0.08); - } - - .channel:hover rect { - fill: #f1f5f9; - } - - .channel-name { - color: #1e293b; - } - } - - #epg-container { - background: #f8fafc; - } - - #epg-svg { - g > rect[fill='#000'] { - fill: #e2e8f0 !important; - } - - g > text { - fill: #64748b !important; - } - - .program { - rect { - fill: #ffffff; - stroke: rgba(0, 0, 0, 0.08); - } - - &:hover rect { - fill: #f1f5f9; - stroke: rgba(47, 123, 255, 0.4); - } - } - - .program-title { - color: #1e293b; - } - - line[stroke='red'] { - stroke: $accent-primary !important; - filter: drop-shadow(0 0 6px $accent-now-glow); - } - } - } -} - -// Responsive adjustments -@media (max-width: 768px) { - #epg-navigation { - padding-left: 70px; // Slightly reduced for smaller screens - } - - #channels-column { - width: 140px; - - .channel-name { - font-size: 11.5px; - padding: 6px 8px; - gap: 8px; - - img { - width: 20px !important; - height: 20px !important; - } - } - } - - #epg-container { - width: calc(100vw - 140px); - } - - .today-date { - font-size: 12px; - padding: 0 12px; - } -} diff --git a/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.spec.ts b/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.spec.ts deleted file mode 100644 index 13d709d27..000000000 --- a/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.spec.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { signal } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { OverlayRef } from '@angular/cdk/overlay'; -import { MatDialog } from '@angular/material/dialog'; -import { TranslateService } from '@ngx-translate/core'; -import { of } from 'rxjs'; -import { EpgRuntimeBridgeService } from '@iptvnator/epg/data-access'; -import { SettingsStore } from '@iptvnator/services'; -import { EpgProgram } from '@iptvnator/shared/interfaces'; -import { - MultiEpgContainerComponent, - isSelectedEpgDayToday, -} from './multi-epg-container.component'; -import { COMPONENT_OVERLAY_REF } from './overlay-ref.token'; - -describe('isSelectedEpgDayToday', () => { - it('returns true only when the selected EPG day is the actual current day', () => { - const now = new Date('2026-05-21T20:00:00.000Z'); - - expect(isSelectedEpgDayToday('20260521', now)).toBe(true); - expect(isSelectedEpgDayToday('20260520', now)).toBe(false); - expect(isSelectedEpgDayToday('20260522', now)).toBe(false); - }); -}); - -function programAt(start: Date, minutes: number, title: string): EpgProgram { - return { - start: start.toISOString(), - stop: new Date(start.getTime() + minutes * 60_000).toISOString(), - channel: 'c1', - title, - desc: null, - category: null, - }; -} - -describe('MultiEpgContainerComponent runtime gates', () => { - let fixture: ComponentFixture; - let component: MultiEpgContainerComponent; - let epgBridge: Partial; - const epgOffsetMinutes = signal(0); - - beforeEach(async () => { - epgOffsetMinutes.set(0); - epgBridge = { - searchPrograms: jest.fn().mockResolvedValue([]), - supportsGuide: false, - supportsProgramSearch: false, - }; - - await TestBed.configureTestingModule({ - imports: [MultiEpgContainerComponent], - providers: [ - { provide: MatDialog, useValue: { open: jest.fn() } }, - { - provide: COMPONENT_OVERLAY_REF, - useValue: { detach: jest.fn() }, - }, - { - provide: OverlayRef, - useValue: { detach: jest.fn() }, - }, - { - provide: EpgRuntimeBridgeService, - useValue: epgBridge, - }, - { - provide: SettingsStore, - useValue: { resolvedEpgOffsetMinutes: epgOffsetMinutes }, - }, - { - provide: TranslateService, - useValue: { - currentLang: 'en', - defaultLang: 'en', - onLangChange: of(null), - }, - }, - ], - }) - .overrideComponent(MultiEpgContainerComponent, { - set: { template: '' }, - }) - .compileComponents(); - - fixture = TestBed.createComponent(MultiEpgContainerComponent); - component = fixture.componentInstance; - }); - - afterEach(() => { - fixture.destroy(); - jest.restoreAllMocks(); - jest.useRealTimers(); - }); - - it('lays programmes out in display time when an EPG offset is set', () => { - const morning = new Date(); - morning.setHours(10, 0, 0, 0); - const late = new Date(); - late.setHours(23, 30, 0, 0); - component.originalEpgData.set([ - { - id: 'c1', - displayName: 'Channel 1', - programs: [ - programAt(morning, 60, 'Morning'), - programAt(late, 60, 'Late'), - ], - } as unknown as ReturnType< - typeof component.originalEpgData - >[number], - ]); - - epgOffsetMinutes.set(90); - - // 23:30 + 90 min lands on the next day and leaves today's column; - // 10:00 + 90 min is drawn under 11:30. - const programs = component.channels()[0].programs; - expect(programs.map((program) => program.title)).toEqual(['Morning']); - expect(programs[0].startPosition).toBe(11.5 * component.hourWidth()); - expect(programs[0].width).toBe(component.hourWidth()); - }); - - it('does not request EPG channel ranges when the EPG bridge cannot browse channels', async () => { - jest.spyOn(console, 'warn').mockImplementation(() => undefined); - - await component.requestPrograms(); - - expect(component.isLoading()).toBe(false); - }); - - // This legacy grid's `EPG_GET_CHANNELS_BY_RANGE` read was removed in - // Task 2 (it never carried real data past this point regardless — see - // the temporary `Promise.resolve([])` in `requestPrograms()`); the whole - // component is deleted in Task 10. - it.skip('requests EPG channel ranges through the EPG runtime bridge', async () => { - epgBridge.supportsGuide = true; - - await component.requestPrograms(); - - expect(component.isLoading()).toBe(false); - }); - - it('does not search EPG programs when the EPG bridge cannot search programs', () => { - jest.useFakeTimers(); - - component.onProgramSearchInput({ - target: { value: 'news' }, - } as unknown as Event); - jest.advanceTimersByTime(600); - - expect(epgBridge.searchPrograms).not.toHaveBeenCalled(); - expect(component.isSearchingPrograms()).toBe(false); - expect(component.programSearchResults()).toEqual([]); - }); - - it('searches EPG programs through the EPG runtime bridge', async () => { - jest.useFakeTimers(); - const results = [ - { - channelId: 'channel-1', - start: '2026-05-22T10:00:00.000Z', - stop: '2026-05-22T11:00:00.000Z', - title: 'News', - }, - ]; - epgBridge.searchPrograms = jest.fn().mockResolvedValue(results); - epgBridge.supportsProgramSearch = true; - - component.onProgramSearchInput({ - target: { value: 'news' }, - } as unknown as Event); - jest.advanceTimersByTime(500); - await Promise.resolve(); - - expect(epgBridge.searchPrograms).toHaveBeenCalledWith('news', 20); - expect(component.programSearchResults()).toEqual(results); - expect(component.isSearchingPrograms()).toBe(false); - }); -}); diff --git a/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.ts b/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.ts deleted file mode 100644 index 39f37a488..000000000 --- a/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.ts +++ /dev/null @@ -1,557 +0,0 @@ -import { DatePipe } from '@angular/common'; -import { OverlayRef } from '@angular/cdk/overlay'; - -import { - AfterViewInit, - ChangeDetectionStrategy, - Component, - computed, - ElementRef, - inject, - Input, - OnDestroy, - OnInit, - signal, - viewChild, -} from '@angular/core'; -import { toSignal } from '@angular/core/rxjs-interop'; -import { MatButtonModule } from '@angular/material/button'; -import { MatDialog } from '@angular/material/dialog'; -import { MatIcon } from '@angular/material/icon'; -import { MatTooltip } from '@angular/material/tooltip'; -import { normalizeDateLocale } from '@iptvnator/pipes'; -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { addDays, differenceInMinutes, format, parse, subDays } from 'date-fns'; -import { Observable, Subscription, startWith } from 'rxjs'; -import { - Channel, - EpgChannel, - epgDisplayTimeMs, - EpgProgram, -} from '@iptvnator/shared/interfaces'; -import { EpgRuntimeBridgeService } from '@iptvnator/epg/data-access'; -import { SettingsStore } from '@iptvnator/services'; -import { getProgramTimeMs } from '../epg-program.utils'; -import { EpgItemDescriptionComponent } from '../epg-item-description/epg-item-description.component'; -import { COMPONENT_OVERLAY_REF } from './overlay-ref.token'; - -// TODO(Task 10): this legacy multi-EPG view is being replaced by the -// host-agnostic `app-epg-guide` grid; `EPG_GET_CHANNELS_BY_RANGE` is gone, -// so this component is temporarily patched to keep typecheck green. -interface MultiEpgChannel { - id: string; - displayName: string; - iconUrl: string | null; - programs: EpgProgram[]; -} - -interface EnrichedProgram extends EpgProgram { - startDate: Date; - stopDate: Date; - startPosition: number; - width: number; -} - -interface EnrichedChannel extends MultiEpgChannel { - programs: EnrichedProgram[]; -} - -interface ProgramSearchResult extends EpgProgram { - channel_id?: string; - channelName?: string | null; - channel_name?: string | null; - display_name?: string | null; -} - -export function isSelectedEpgDayToday( - selectedDay: string, - now: Date = new Date() -): boolean { - return selectedDay === format(now, 'yyyyMMdd'); -} - -@Component({ - imports: [DatePipe, MatButtonModule, MatIcon, MatTooltip, TranslatePipe], - selector: 'app-multi-epg-container', - templateUrl: './multi-epg-container.component.html', - styleUrls: ['./multi-epg-container.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class MultiEpgContainerComponent - implements OnInit, AfterViewInit, OnDestroy -{ - readonly epgContainer = viewChild.required('epgContainer'); - - @Input() set playlistChannels(value: Observable) { - if (this.playlistChannelsSubscription) { - this.playlistChannelsSubscription.unsubscribe(); - } - - if (value) { - this.playlistChannelsSubscription = value.subscribe(() => { - this.channelsLowerRange = 0; - this.originalEpgData.set([]); - this.isLastPage.set(false); - this.initializeVisibleChannels(); - this.requestPrograms(); - }); - } - } - - @Input() activeChannelId: string | null = null; - - // Signals - readonly hourWidth = signal(150); - readonly today = signal(format(new Date(), 'yyyyMMdd')); - readonly originalEpgData = signal([]); - readonly isLoading = signal(false); - readonly isLastPage = signal(false); - readonly channelFilter = signal(''); - readonly isSearchExpanded = signal(false); - - // Program search signals - readonly isProgramSearchOpen = signal(false); - readonly programSearchQuery = signal(''); - readonly programSearchResults = signal([]); - readonly isSearchingPrograms = signal(false); - readonly highlightedProgramKey = signal(null); - private searchDebounceTimer: ReturnType | null = null; - private readonly translate = inject(TranslateService); - private readonly languageTick = toSignal( - this.translate.onLangChange.pipe(startWith(null)), - { initialValue: null } - ); - readonly currentLocale = computed(() => { - this.languageTick(); - return normalizeDateLocale( - this.translate.currentLang || this.translate.defaultLang - ); - }); - readonly selectedDayDate = computed(() => - parse(this.today(), 'yyyyMMdd', new Date()) - ); - - // Computed signal for enriched channels - automatically updates when dependencies change - readonly channels = computed(() => this.enrichProgramData()); - - // Computed signal for filtered channels based on search term - readonly filteredChannels = computed(() => { - const filter = this.channelFilter().toLowerCase().trim(); - const allChannels = this.channels(); - - if (!filter) { - return allChannels; - } - - return allChannels.filter((channel) => { - const name = this.getChannelName(channel).toLowerCase(); - return name.includes(filter); - }); - }); - - // Computed signal for current time line position - readonly currentTimeLine = computed(() => { - const now = new Date(); - return (now.getHours() + now.getMinutes() / 60) * this.hourWidth(); - }); - - // "16:32"-style label rendered as a badge above the now-line. Recomputes - // on the same 60-second tick that drives `currentTimeLine` (both depend - // on hourWidth's update pulse from `ngOnInit`). - readonly currentTimeLabel = computed(() => { - // Subscribe to the same tick source as currentTimeLine. - this.hourWidth(); - const now = new Date(); - return `${now.getHours().toString().padStart(2, '0')}:${now - .getMinutes() - .toString() - .padStart(2, '0')}`; - }); - - // Constants - readonly timeHeader = Array.from({ length: 24 }, (_, i) => i); - readonly barHeight = 50; - - // Pagination state - visibleChannels = 20; - channelsLowerRange = 0; - channelsUpperRange = this.visibleChannels; - totalChannels = 0; - - private dateCache = new Map(); - private interval!: ReturnType; - private playlistChannelsSubscription?: Subscription; - - private readonly dialog = inject(MatDialog); - private readonly overlayRef = inject(COMPONENT_OVERLAY_REF); - private readonly epgBridge = inject(EpgRuntimeBridgeService); - private readonly settingsStore = inject(SettingsStore); - /** - * Attached to an overlay without host inputs, so the grid reads the EPG - * display offset itself (`epg-display-offset.util.ts`, display form). - */ - private readonly epgOffsetMinutes = - this.settingsStore.resolvedEpgOffsetMinutes; - - ngOnInit() { - // Update current time line every minute - this.interval = setInterval(() => { - // Force recomputation by updating hourWidth to same value - // This triggers the computed signal to recalculate - this.hourWidth.update((v) => v); - }, 60000); - } - - ngAfterViewInit(): void { - this.initializeVisibleChannels(); - this.scrollToCurrentTime(); - } - - private scrollToCurrentTime(): void { - const timeNow = new Date(); - const scrollPosition = - (timeNow.getHours() + timeNow.getMinutes() / 60) * this.hourWidth(); - - requestAnimationFrame(() => { - const container = document.getElementById('epg-container'); - if (container) { - container.scrollTo( - scrollPosition < 1000 ? 0 : scrollPosition - 150, - 0 - ); - } - }); - } - - private initializeVisibleChannels(): void { - const epgContainer = this.epgContainer(); - if (epgContainer) { - const containerHeight = epgContainer.nativeElement.offsetHeight; - const calculatedVisibleChannels = Math.floor( - (containerHeight - this.barHeight) / this.barHeight - ); - - this.visibleChannels = Math.max( - 10, - Math.min(calculatedVisibleChannels, 20) - ); - this.channelsUpperRange = this.visibleChannels; - } - } - - ngOnDestroy(): void { - clearInterval(this.interval); - - if (this.playlistChannelsSubscription) { - this.playlistChannelsSubscription.unsubscribe(); - } - - if (this.searchDebounceTimer) { - clearTimeout(this.searchDebounceTimer); - } - } - - trackByIndex(index: number): number { - return index; - } - - trackByProgram(_: number, program: EnrichedProgram): string { - return `${program.start}|${program?.title?.toString() ?? ''}`; - } - - /** - * Returns true when the now-line falls within this program's rendered - * span — used to add a `.is-now` class so the cyan border lights up - * every airing program at once. The template reads `currentTimeLine()` - * (a signal) here so the highlight refreshes on the same 60-second tick - * as the line itself. - */ - isProgramAiringNow(program: EnrichedProgram): boolean { - const nowX = this.currentTimeLine(); - if (!isSelectedEpgDayToday(this.today())) { - return false; - } - return ( - nowX >= program.startPosition && - nowX <= program.startPosition + program.width - ); - } - - async requestPrograms(): Promise { - if (!this.epgBridge.supportsGuide) { - console.warn('Multi-EPG not available in this runtime'); - return; - } - - if (this.isLoading() || this.isLastPage()) { - return; - } - - this.isLoading.set(true); - - try { - const response = await Promise.resolve( - [] as MultiEpgChannel[] - ); - - if (response && Array.isArray(response)) { - // Append new data to existing data - this.originalEpgData.update((data) => [...data, ...response]); - - // Update isLastPage based on the number of channels received - this.isLastPage.set(response.length < this.visibleChannels); - - // Update range for next fetch - this.channelsLowerRange += response.length; - } - } catch (error) { - console.error('Error fetching EPG data:', error); - } finally { - this.isLoading.set(false); - } - } - - onScroll(event: Event): void { - const target = event.target as HTMLElement; - const scrollTop = target.scrollTop; - const scrollHeight = target.scrollHeight; - const clientHeight = target.clientHeight; - - // Load more when user scrolls to within 200px of the bottom - if (scrollHeight - scrollTop - clientHeight < 200) { - this.requestPrograms(); - } - } - - private getCachedDate(dateStr: string): Date { - let date = this.dateCache.get(dateStr); - if (!date) { - date = new Date(dateStr); - this.dateCache.set(dateStr, date); - } - return date; - } - - /** The cached raw date shifted into display time. */ - private getDisplayDate(dateStr: string, offsetMinutes: number): Date { - const raw = this.getCachedDate(dateStr); - return offsetMinutes === 0 - ? raw - : new Date(epgDisplayTimeMs(raw.getTime(), offsetMinutes)); - } - - /** Display-time epoch ms of a search-result boundary for the date pipe. */ - programDisplayMs(program: EpgProgram, side: 'start' | 'stop'): number { - return side === 'start' - ? getProgramTimeMs( - program.start, - program.startTimestamp, - this.epgOffsetMinutes() - ) - : getProgramTimeMs( - program.stop, - program.stopTimestamp, - this.epgOffsetMinutes() - ); - } - - private enrichProgramData(): EnrichedChannel[] { - const hourWidthValue = this.hourWidth(); - const todayValue = this.today(); - const data = this.originalEpgData(); - // Day filter and x-position both work in display time, so a shifted - // programme lands in the right day column under the right hour. - const offsetMinutes = this.epgOffsetMinutes(); - - return data.map((channel) => { - const filteredPrograms = (channel.programs || []) - .filter((item: EpgProgram) => { - const itemDate = format( - this.getDisplayDate(item.start, offsetMinutes), - 'yyyyMMdd' - ); - return itemDate === todayValue; - }) - .map((program: EpgProgram) => { - const startDate = this.getDisplayDate( - program.start, - offsetMinutes - ); - const stopDate = this.getDisplayDate( - program.stop, - offsetMinutes - ); - const startPosition = - (startDate.getHours() + startDate.getMinutes() / 60) * - hourWidthValue; - const duration = differenceInMinutes(stopDate, startDate); - const width = (duration * hourWidthValue) / 60; - - return { - ...program, - startDate, - stopDate, - startPosition, - width, - }; - }); - - return { - ...channel, - programs: filteredPrograms, - }; - }); - } - - /** - * Get display name from EpgChannel - */ - getChannelName(channel: EpgChannel | MultiEpgChannel): string { - if (typeof channel.displayName === 'string') { - return channel.displayName; - } - if ( - Array.isArray(channel.displayName) && - channel.displayName.length > 0 - ) { - return channel.displayName[0].value; - } - return ''; - } - - /** - * Get icon from EpgChannel - */ - getChannelIcon(channel: EpgChannel | MultiEpgChannel): string { - if ('iconUrl' in channel && channel.iconUrl) { - return channel.iconUrl; - } - if ( - 'icon' in channel && - Array.isArray(channel.icon) && - channel.icon.length > 0 - ) { - return channel.icon[0].src; - } - return ''; - } - - zoomIn(): void { - if (this.hourWidth() >= 800) return; - this.hourWidth.update((v) => v + 50); - } - - zoomOut(): void { - if (this.hourWidth() <= 50) return; - this.hourWidth.update((v) => v - 50); - } - - toggleSearch(): void { - this.isSearchExpanded.update((v) => !v); - if (!this.isSearchExpanded()) { - this.channelFilter.set(''); - } - } - - clearFilter(): void { - this.channelFilter.set(''); - } - - onFilterInput(event: Event): void { - const input = event.target as HTMLInputElement; - this.channelFilter.set(input.value); - } - - switchDay(direction: 'prev' | 'next'): void { - const currentDate = parse(this.today(), 'yyyyMMdd', new Date()); - this.today.set( - direction === 'prev' - ? format(subDays(currentDate, 1), 'yyyyMMdd') - : format(addDays(currentDate, 1), 'yyyyMMdd') - ); - } - - showDescription(program: EpgProgram): void { - this.dialog.open(EpgItemDescriptionComponent, { - width: '600px', - data: program, - }); - } - - // Program search methods - toggleProgramSearch(): void { - this.isProgramSearchOpen.update((v) => !v); - if (!this.isProgramSearchOpen()) { - this.programSearchQuery.set(''); - this.programSearchResults.set([]); - } - } - - onProgramSearchInput(event: Event): void { - const input = event.target as HTMLInputElement; - const query = input.value.trim(); - this.programSearchQuery.set(query); - - // Clear previous debounce timer - if (this.searchDebounceTimer) { - clearTimeout(this.searchDebounceTimer); - } - - if (query.length < 2) { - this.programSearchResults.set([]); - this.isSearchingPrograms.set(false); - return; - } - - if (!this.epgBridge.supportsProgramSearch) { - this.programSearchResults.set([]); - this.isSearchingPrograms.set(false); - return; - } - - // Debounce search by 500ms - show spinner only when search actually starts - this.searchDebounceTimer = setTimeout(async () => { - this.isSearchingPrograms.set(true); - try { - const results = await this.epgBridge.searchPrograms(query, 20); - this.programSearchResults.set( - (results as ProgramSearchResult[]) || [] - ); - } catch (error) { - console.error('Error searching programs:', error); - this.programSearchResults.set([]); - } finally { - this.isSearchingPrograms.set(false); - } - }, 500); - } - - clearProgramSearch(): void { - this.programSearchQuery.set(''); - this.programSearchResults.set([]); - } - - showProgramDetails(program: ProgramSearchResult): void { - // Get channel id (DB returns channel_id, EPG uses channel) - const channelId = program.channel_id || program.channel; - - // Find channel name - const channel = this.channels().find((ch) => ch.id === channelId); - const channelName = channel ? this.getChannelName(channel) : null; - - // Open the program details dialog with channel name - this.dialog.open(EpgItemDescriptionComponent, { - width: '800px', - data: { ...program, channelName }, - }); - } - - getProgramKey(program: ProgramSearchResult): string { - const channelId = program.channel_id || program.channel; - return `${channelId}|${program.start}`; - } - - close() { - this.overlayRef.detach(); - } -} diff --git a/libs/ui/epg/src/lib/multi-epg/overlay-ref.token.ts b/libs/ui/epg/src/lib/multi-epg/overlay-ref.token.ts deleted file mode 100644 index db768fed1..000000000 --- a/libs/ui/epg/src/lib/multi-epg/overlay-ref.token.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { OverlayRef } from '@angular/cdk/overlay'; -import { InjectionToken } from '@angular/core'; - -/** - * Injection token for providing OverlayRef to dynamically created overlay components - */ -export const COMPONENT_OVERLAY_REF = new InjectionToken( - 'COMPONENT_OVERLAY_REF' -); diff --git a/tools/eslint/max-lines-baseline.mjs b/tools/eslint/max-lines-baseline.mjs index c609d81dc..1a44727aa 100644 --- a/tools/eslint/max-lines-baseline.mjs +++ b/tools/eslint/max-lines-baseline.mjs @@ -60,7 +60,6 @@ export const maxLinesBaseline = [ 'libs/shared/interfaces/src/lib/electron-api.interface.ts', 'libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts', 'libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.ts', - 'libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.ts', 'libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.ts', 'libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.spec.ts', 'libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.ts',