From 1780bb0e53f207458ddb8bfcf0ece09cf92285a5 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 8 Mar 2026 18:55:18 +0100 Subject: [PATCH] refactor: remove 'on-air' widget and related functionality from dashboard --- .claude/settings.local.json | 71 ++--- .../src/app/api/main.preload.ts | 2 - .../src/app/events/database/epg-db.events.ts | 60 ---- apps/web/src/assets/i18n/en.json | 11 - global.d.ts | 4 - .../src/lib/dashboard-widget.model.ts | 10 - .../lib/dashboard-widget-host.component.html | 3 - .../lib/dashboard-widget-host.component.ts | 2 - .../lib/widgets/on-air-widget.component.html | 130 -------- .../lib/widgets/on-air-widget.component.scss | 279 ------------------ .../lib/widgets/on-air-widget.component.ts | 196 ------------ 11 files changed, 36 insertions(+), 732 deletions(-) delete mode 100644 libs/workspace/dashboard/ui/src/lib/widgets/on-air-widget.component.html delete mode 100644 libs/workspace/dashboard/ui/src/lib/widgets/on-air-widget.component.scss delete mode 100644 libs/workspace/dashboard/ui/src/lib/widgets/on-air-widget.component.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 54bd330b6..795acc939 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,37 +1,38 @@ { - "permissions": { - "allow": [ - "Bash(tree:*)", - "WebSearch", - "WebFetch(domain:www.electronjs.org)", - "Bash(npm run build:frontend:*)", - "Bash(cat:*)", - "Bash(npx asar:*)", - "Bash(npm run package:app:*)", - "Bash(npm run build:backend:*)", - "Bash(pnpm add:*)", - "WebFetch(domain:github.com)", - "WebFetch(domain:www.npmjs.com)", - "Skill(example-skills:frontend-design)", - "Bash(npm run test:frontend:*)", - "Bash(nx lint:*)", - "Bash(npx tsc:*)", - "Bash(find:*)", - "Bash(xargs sed:*)", - "Skill(frontend-design)", - "mcp__angular-cli__get_best_practices", - "mcp__nx-mcp__nx_workspace", - "Bash(grep:*)", - "Bash(npx nx lint:*)", - "Bash(npx nx build:*)", - "Bash(npx eslint:*)", - "Bash(npx nx run:*)" - ], - "deny": [], - "ask": [] - }, - "enableAllProjectMcpServers": true, - "enabledMcpjsonServers": [ - "nx-mcp" - ] + "permissions": { + "allow": [ + "Bash(tree:*)", + "WebSearch", + "WebFetch(domain:www.electronjs.org)", + "Bash(npm run build:frontend:*)", + "Bash(cat:*)", + "Bash(npx asar:*)", + "Bash(npm run package:app:*)", + "Bash(npm run build:backend:*)", + "Bash(pnpm add:*)", + "WebFetch(domain:github.com)", + "WebFetch(domain:www.npmjs.com)", + "Skill(example-skills:frontend-design)", + "Bash(npm run test:frontend:*)", + "Bash(nx lint:*)", + "Bash(npx tsc:*)", + "Bash(find:*)", + "Bash(xargs sed:*)", + "Skill(frontend-design)", + "mcp__angular-cli__get_best_practices", + "mcp__nx-mcp__nx_workspace", + "Bash(grep:*)", + "Bash(npx nx lint:*)", + "Bash(npx nx build:*)", + "Bash(npx eslint:*)", + "Bash(npx nx run:*)", + "Bash(lsof:*)", + "Bash(xargs kill:*)", + "Bash(curl:*)" + ], + "deny": [], + "ask": [] + }, + "enableAllProjectMcpServers": true, + "enabledMcpjsonServers": ["nx-mcp"] } diff --git a/apps/electron-backend/src/app/api/main.preload.ts b/apps/electron-backend/src/app/api/main.preload.ts index 384fc3ca3..6a722195c 100644 --- a/apps/electron-backend/src/app/api/main.preload.ts +++ b/apps/electron-backend/src/app/api/main.preload.ts @@ -177,8 +177,6 @@ contextBridge.exposeInMainWorld('electron', { ipcRenderer.invoke('EPG_CHECK_FRESHNESS', { urls, maxAgeHours }), searchEpgPrograms: (searchTerm: string, limit?: number) => ipcRenderer.invoke('EPG_DB_SEARCH_PROGRAMS', searchTerm, limit), - getNowPlayingPrograms: (options?: { category?: string; limit?: number }) => - ipcRenderer.invoke('EPG_DB_GET_NOW_PLAYING', options ?? {}), setMpvPlayerPath: (mpvPlayerPath: string) => ipcRenderer.invoke('SET_MPV_PLAYER_PATH', mpvPlayerPath), setVlcPlayerPath: (vlcPlayerPath: string) => diff --git a/apps/electron-backend/src/app/events/database/epg-db.events.ts b/apps/electron-backend/src/app/events/database/epg-db.events.ts index b3850c805..1b7edb4fb 100644 --- a/apps/electron-backend/src/app/events/database/epg-db.events.ts +++ b/apps/electron-backend/src/app/events/database/epg-db.events.ts @@ -329,66 +329,6 @@ ipcMain.handle('EPG_DB_CLEAR_ALL', async () => { } }); -/** - * Get currently airing programs across all channels (batch, for On Air Now widget) - * Returns programs where start <= now <= stop, joined with channel info. - */ -ipcMain.handle( - 'EPG_DB_GET_NOW_PLAYING', - async ( - _event, - { category, limit = 25 }: { category?: string; limit?: number } = {} - ) => { - try { - const db = await getDatabase(); - const now = new Date().toISOString(); - - // Build base query using Drizzle ORM (consistent with other handlers) - // stop >= now is highly selective with idx_epg_programs_stop, - // filtering out all past programs before checking start <= now. - const results = await db - .select({ - channel_id: schema.epgPrograms.channelId, - title: schema.epgPrograms.title, - description: schema.epgPrograms.description, - category: schema.epgPrograms.category, - start: schema.epgPrograms.start, - stop: schema.epgPrograms.stop, - channel_name: schema.epgChannels.displayName, - channel_icon: schema.epgChannels.iconUrl, - }) - .from(schema.epgPrograms) - .leftJoin( - schema.epgChannels, - eq(schema.epgPrograms.channelId, schema.epgChannels.id) - ) - .where( - category && category !== 'all' - ? and( - lte(schema.epgPrograms.start, now), - gte(schema.epgPrograms.stop, now), - sql`LOWER(${schema.epgPrograms.category}) LIKE LOWER(${'%' + category + '%'})` - ) - : and( - lte(schema.epgPrograms.start, now), - gte(schema.epgPrograms.stop, now) - ) - ) - .orderBy(schema.epgChannels.displayName) - .limit(limit); - - return results; - } catch (error) { - console.error( - loggerLabel, - 'Error getting now playing programs:', - error - ); - throw error; - } - } -); - /** * Get EPG statistics */ diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index e9184ae20..c274dec8f 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -587,17 +587,6 @@ "TYPE_LIVE": "Live", "TYPE_MOVIE": "Movie", "TYPE_SERIES": "Series", - "ON_AIR_EYEBROW": "On Air", - "ON_AIR_TITLE": "Currently Airing", - "ON_AIR_EMPTY": "No EPG data available", - "ON_AIR_EMPTY_HINT": "Load an EPG source in Settings to see what's on right now.", - "ON_AIR_EMPTY_FILTER": "No programs match this category.", - "ON_AIR_ADD_EPG": "Add EPG Source", - "ON_AIR_CAT_ALL": "All", - "ON_AIR_CAT_SPORTS": "Sports", - "ON_AIR_CAT_NEWS": "News", - "ON_AIR_CAT_MOVIES": "Movies", - "ON_AIR_CAT_KIDS": "Kids", "MIN_LEFT": "min left" } } diff --git a/global.d.ts b/global.d.ts index 48e76361b..114f8f671 100644 --- a/global.d.ts +++ b/global.d.ts @@ -75,10 +75,6 @@ declare global { searchTerm: string, limit?: number ) => Promise; - getNowPlayingPrograms: (options?: { - category?: string; - limit?: number; - }) => Promise; updateSettings: (settings: any) => Promise; getAiSettings: () => Promise<{ aiProvider: string; diff --git a/libs/workspace/dashboard/data-access/src/lib/dashboard-widget.model.ts b/libs/workspace/dashboard/data-access/src/lib/dashboard-widget.model.ts index 6f2a0cc12..a3e5a5ad3 100644 --- a/libs/workspace/dashboard/data-access/src/lib/dashboard-widget.model.ts +++ b/libs/workspace/dashboard/data-access/src/lib/dashboard-widget.model.ts @@ -1,7 +1,6 @@ export type DashboardWidgetType = | 'source-stats' | 'continue-watching' - | 'on-air' | 'recently-watched' | 'global-favorites'; @@ -76,15 +75,6 @@ export const DEFAULT_DASHBOARD_WIDGETS: DashboardWidgetConfig[] = [ enabled: true, order: 2, }, - { - id: 'on-air', - type: 'on-air', - title: 'On Air Now', - description: 'Live programs currently airing across your EPG channels.', - size: 'full', - enabled: true, - order: 3, - }, { id: 'recently-watched', type: 'recently-watched', diff --git a/libs/workspace/dashboard/ui/src/lib/dashboard-widget-host.component.html b/libs/workspace/dashboard/ui/src/lib/dashboard-widget-host.component.html index 03d1f063f..356aacd5d 100644 --- a/libs/workspace/dashboard/ui/src/lib/dashboard-widget-host.component.html +++ b/libs/workspace/dashboard/ui/src/lib/dashboard-widget-host.component.html @@ -5,9 +5,6 @@ @case ('continue-watching') { } - @case ('on-air') { - - } @case ('recently-watched') { } diff --git a/libs/workspace/dashboard/ui/src/lib/dashboard-widget-host.component.ts b/libs/workspace/dashboard/ui/src/lib/dashboard-widget-host.component.ts index 89ee50298..d42e805e4 100644 --- a/libs/workspace/dashboard/ui/src/lib/dashboard-widget-host.component.ts +++ b/libs/workspace/dashboard/ui/src/lib/dashboard-widget-host.component.ts @@ -3,7 +3,6 @@ import { TranslatePipe } from '@ngx-translate/core'; import { DashboardWidgetConfig } from 'workspace-dashboard-data-access'; import { ContinueWatchingWidgetComponent } from './widgets/continue-watching-widget.component'; import { GlobalFavoritesWidgetComponent } from './widgets/global-favorites-widget.component'; -import { OnAirWidgetComponent } from './widgets/on-air-widget.component'; import { RecentlyWatchedWidgetComponent } from './widgets/recently-watched-widget.component'; import { SourceStatsWidgetComponent } from './widgets/source-stats-widget.component'; @@ -12,7 +11,6 @@ import { SourceStatsWidgetComponent } from './widgets/source-stats-widget.compon imports: [ ContinueWatchingWidgetComponent, GlobalFavoritesWidgetComponent, - OnAirWidgetComponent, RecentlyWatchedWidgetComponent, SourceStatsWidgetComponent, TranslatePipe, diff --git a/libs/workspace/dashboard/ui/src/lib/widgets/on-air-widget.component.html b/libs/workspace/dashboard/ui/src/lib/widgets/on-air-widget.component.html deleted file mode 100644 index 01ee3ab2b..000000000 --- a/libs/workspace/dashboard/ui/src/lib/widgets/on-air-widget.component.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - @for (cat of categoryKeys; track cat) { - - {{ categoryTranslationKey(cat) | translate }} - - } - - - - @if (isLoading()) { -
- @for (_ of [1, 2, 3, 4]; track $index) { -
-
-
-
-
-
-
- } -
- } @else if (filtered().length > 0) { -
- @for (item of filtered(); track $index) { -
- - - - - {{ item.channel_name ?? item.channel_id }} - @if (item.category) { - · {{ item.category }} - } - - {{ item.title }} - - - - - {{ item.minutesLeft }} min - - -
- } -
- } @else { -
- schedule -

- {{ 'WORKSPACE.DASHBOARD.ON_AIR_EMPTY' | translate }} -

-

- {{ - (items().length > 0 - ? 'WORKSPACE.DASHBOARD.ON_AIR_EMPTY_FILTER' - : 'WORKSPACE.DASHBOARD.ON_AIR_EMPTY_HINT' - ) | translate - }} -

- @if (!items().length) { - - add - {{ 'WORKSPACE.DASHBOARD.ON_AIR_ADD_EPG' | translate }} - - } -
- } -
diff --git a/libs/workspace/dashboard/ui/src/lib/widgets/on-air-widget.component.scss b/libs/workspace/dashboard/ui/src/lib/widgets/on-air-widget.component.scss deleted file mode 100644 index e60eef4b2..000000000 --- a/libs/workspace/dashboard/ui/src/lib/widgets/on-air-widget.component.scss +++ /dev/null @@ -1,279 +0,0 @@ -@use 'sass:color'; - -:host { - --on-air-accent: oklch(62% 0.20 200); - --on-air-accent-muted: oklch(62% 0.20 200 / 0.15); - --on-air-pulse: oklch(62% 0.22 160); - - display: block; - height: 100%; -} - -/* ─── Category filter ───────────────────────────────────────────────── */ -.category-filter { - ::ng-deep .mat-button-toggle { - font-size: 0.75rem; - font-weight: 500; - } - ::ng-deep .mat-button-toggle-checked { - color: var(--on-air-accent); - } -} - -/* ─── Channel list ──────────────────────────────────────────────────── */ -.on-air-list { - display: flex; - flex-direction: column; - gap: 2px; - container-type: inline-size; -} - -/* ─── Row item ──────────────────────────────────────────────────────── */ -.on-air-item { - display: grid; - grid-template-columns: 44px 1fr; - gap: 10px; - align-items: center; - padding: 8px 6px; - border-radius: 10px; - border: 1px solid transparent; - transition: transform 0.2s ease, border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease; - cursor: default; - - &:hover { - transform: translateY(-2px); - background: var(--on-air-accent-muted); - border-color: color-mix(in srgb, var(--on-air-accent) 30%, var(--mat-sys-outline-variant) 70%); - box-shadow: 0 4px 12px -4px rgba(0, 0, 0, 0.15); - } -} - -/* ─── Avatar button ─────────────────────────────────────────────────── */ -.on-air-item__avatar { - display: grid; - place-items: center; - position: relative; - width: 40px; - height: 40px; - border-radius: 50%; - background: var(--on-air-accent-muted); - color: var(--on-air-accent); - overflow: hidden; - flex-shrink: 0; - // Reset button defaults - border: none; - padding: 0; - cursor: pointer; - transition: filter 0.2s ease; - - mat-icon { - font-size: 20px; - width: 20px; - height: 20px; - } - - &:hover { - filter: brightness(1.2); - } - - &--img img { - width: 100%; - height: 100%; - object-fit: contain; - } -} - -/* Small always-visible info badge over channel logo */ -.on-air-item__avatar-badge { - position: absolute; - bottom: -1px; - right: -1px; - width: 16px; - height: 16px; - border-radius: 50%; - background: var(--on-air-accent); - color: #fff; - display: grid; - place-items: center; - border: 1.5px solid var(--mat-sys-surface-container); - - mat-icon { - font-size: 10px; - width: 10px; - height: 10px; - } -} - -/* ─── Body ──────────────────────────────────────────────────────────── */ -.on-air-item__body { - display: flex; - flex-direction: column; - gap: 3px; - min-width: 0; -} - -.on-air-item__channel { - display: flex; - align-items: center; - gap: 5px; - font-size: 0.7rem; - font-weight: 600; - letter-spacing: 0.04em; - text-transform: uppercase; - opacity: 0.6; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.on-air-item__category { - opacity: 0.75; - font-weight: 400; - text-transform: none; - letter-spacing: 0; -} - -.on-air-item__title { - font-size: 0.875rem; - font-weight: 500; - line-height: 1.25; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -/* ─── Progress row ──────────────────────────────────────────────────── */ -.on-air-item__progress-row { - display: flex; - align-items: center; - gap: 8px; - margin-top: 2px; -} - -.on-air-item__bar-track { - flex: 1; - height: 4px; - border-radius: 2px; - background: var(--on-air-accent-muted); - overflow: hidden; -} - -.on-air-item__bar-fill { - display: block; - height: 100%; - border-radius: 2px; - background: var(--on-air-accent); - transition: width 0.4s ease; - min-width: 3px; -} - -.on-air-item__time-left { - font-size: 0.7rem; - opacity: 0.55; - white-space: nowrap; -} - -/* ─── Live pulse dot ────────────────────────────────────────────────── */ -.on-air-item__pulse { - display: inline-block; - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--on-air-pulse); - flex-shrink: 0; - animation: pulse 2s ease-in-out infinite; -} - -@keyframes pulse { - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.4; transform: scale(0.7); } -} - -/* ─── Loading skeleton ──────────────────────────────────────────────── */ -.on-air-skeleton { - display: flex; - flex-direction: column; - gap: 8px; -} - -.skeleton-row { - display: grid; - grid-template-columns: 44px 1fr; - gap: 10px; - align-items: center; - padding: 8px 6px; -} - -.skeleton-avatar { - width: 40px; - height: 40px; - border-radius: 50%; - background: var(--mat-sys-outline-variant, rgba(128, 128, 128, 0.2)); - animation: shimmer 1.5s ease-in-out infinite; -} - -.skeleton-body { - display: flex; - flex-direction: column; - gap: 6px; -} - -.skeleton-line { - height: 11px; - border-radius: 6px; - background: var(--mat-sys-outline-variant, rgba(128, 128, 128, 0.2)); - animation: shimmer 1.5s ease-in-out infinite; - - &--wide { width: 80%; } - &--narrow { width: 50%; } -} - -@keyframes shimmer { - 0%, 100% { opacity: 0.5; } - 50% { opacity: 1; } -} - -/* ─── Empty state ───────────────────────────────────────────────────── */ -.on-air-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - gap: 10px; - padding: 40px 24px; - border-radius: 14px; - border: 1.5px dashed var(--on-air-accent-muted); - background: radial-gradient(ellipse at 50% 0%, var(--on-air-accent-muted) 0%, transparent 70%); -} - -.on-air-empty__icon { - width: 52px; - height: 52px; - font-size: 28px; - line-height: 52px; - text-align: center; - border-radius: 50%; - background: var(--on-air-accent-muted); - color: var(--on-air-accent); -} - -.on-air-empty__title { - margin: 0; - font-weight: 600; - font-size: 0.95rem; -} - -.on-air-empty__hint { - margin: 0; - font-size: 0.8rem; - opacity: 0.6; - max-width: 28ch; - line-height: 1.5; -} - -.on-air-empty__action { - margin-top: 6px; - color: var(--on-air-accent); - border-color: var(--on-air-accent); -} diff --git a/libs/workspace/dashboard/ui/src/lib/widgets/on-air-widget.component.ts b/libs/workspace/dashboard/ui/src/lib/widgets/on-air-widget.component.ts deleted file mode 100644 index 7f7e5271b..000000000 --- a/libs/workspace/dashboard/ui/src/lib/widgets/on-air-widget.component.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - NgZone, - OnDestroy, - OnInit, - computed, - inject, - signal, -} from '@angular/core'; -import { MatButtonModule } from '@angular/material/button'; -import { MatButtonToggleModule } from '@angular/material/button-toggle'; -import { MatDialog } from '@angular/material/dialog'; -import { MatIcon } from '@angular/material/icon'; -import { MatTooltip } from '@angular/material/tooltip'; -import { RouterLink } from '@angular/router'; -import { TranslatePipe } from '@ngx-translate/core'; -import { EpgItemDescriptionComponent } from 'components'; -import { DashboardWidgetShellComponent } from './dashboard-widget-shell.component'; - -export interface NowPlayingItem { - channel_id: string; - title: string; - description: string | null; - category: string | null; - start: string; - stop: string; - channel_name: string | null; - channel_icon: string | null; -} - -/** NowPlayingItem enriched with pre-computed display values */ -export interface NowPlayingItemView extends NowPlayingItem { - progressPercent: number; - minutesLeft: number; - initial: string; -} - -export type OnAirCategoryKey = 'all' | 'sports' | 'news' | 'movies' | 'kids'; - -@Component({ - selector: 'app-on-air-widget', - imports: [ - MatButtonModule, - MatButtonToggleModule, - MatIcon, - MatTooltip, - RouterLink, - DashboardWidgetShellComponent, - TranslatePipe, - ], - templateUrl: './on-air-widget.component.html', - styleUrl: './on-air-widget.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class OnAirWidgetComponent implements OnInit, OnDestroy { - private readonly zone = inject(NgZone); - private readonly dialog = inject(MatDialog); - - readonly items = signal([]); - readonly isLoading = signal(true); - readonly selectedCategory = signal('all'); - - readonly categoryKeys: OnAirCategoryKey[] = [ - 'all', - 'sports', - 'news', - 'movies', - 'kids', - ]; - - private readonly categoryKeywords: Record = { - all: [], - sports: [ - 'sport', - 'football', - 'soccer', - 'basketball', - 'tennis', - 'f1', - 'cricket', - 'rugby', - ], - news: ['news', 'nachrichten', 'info', 'actualité', 'noticias', 'haber'], - movies: ['movie', 'film', 'cinema', 'kino', 'cine', 'films'], - kids: [ - 'kids', - 'children', - 'child', - 'cartoon', - 'animation', - 'junior', - 'enfants', - ], - }; - - readonly filtered = computed(() => { - const cat = this.selectedCategory(); - if (cat === 'all') return this.items(); - const keywords = this.categoryKeywords[cat]; - return this.items().filter((item) => - keywords.some((kw) => - (item.category ?? '').toLowerCase().includes(kw) - ) - ); - }); - - private refreshTimer: ReturnType | null = null; - - ngOnInit(): void { - void this.loadItems(); - // Run timer outside Angular Zone — prevents zone-triggered CD on every tick. - // Signal updates inside loadItems() propagate to the view via Angular's - // signal reactivity without needing zone.run(). - this.zone.runOutsideAngular(() => { - this.refreshTimer = setInterval( - () => void this.loadItems(), - 60_000 - ); - }); - } - - ngOnDestroy(): void { - if (this.refreshTimer !== null) { - clearInterval(this.refreshTimer); - } - } - - setCategory(cat: OnAirCategoryKey): void { - this.selectedCategory.set(cat); - } - - categoryTranslationKey(cat: OnAirCategoryKey): string { - return `WORKSPACE.DASHBOARD.ON_AIR_CAT_${cat.toUpperCase()}`; - } - - openInfo(item: NowPlayingItemView): void { - this.dialog.open(EpgItemDescriptionComponent, { - data: { - channel: item.channel_id, - channel_name: item.channel_name, - title: item.title, - desc: item.description, - category: item.category, - start: item.start, - stop: item.stop, - episodeNum: null, - rating: null, - iconUrl: item.channel_icon ?? null, - }, - width: '480px', - maxWidth: '96vw', - }); - } - - private async loadItems(): Promise { - if (!window.electron) { - this.isLoading.set(false); - return; - } - try { - const data = await window.electron.getNowPlayingPrograms({ - limit: 25, - }); - const now = Date.now(); - const enriched: NowPlayingItemView[] = ( - data as NowPlayingItem[] - ).map((item) => { - const start = new Date(item.start).getTime(); - const stop = new Date(item.stop).getTime(); - const range = stop - start; - return { - ...item, - progressPercent: - range > 0 - ? Math.round( - Math.min( - 100, - Math.max(0, ((now - start) / range) * 100) - ) - ) - : 0, - minutesLeft: Math.max(0, Math.round((stop - now) / 60_000)), - initial: (item.channel_name ?? item.channel_id ?? '?') - .charAt(0) - .toUpperCase(), - }; - }); - this.items.set(enriched); - } catch { - this.items.set([]); - } finally { - this.isLoading.set(false); - } - } -}