Files
iptvnator/apps/web/src/app/services/epg-progress.service.ts
T
4gray 99b5ad161f feat(ui): improve info overlay and add EPG header search styles
Update info-overlay template to read simplified EPG fields and
avoid indexing into arrays for title, icon and description so bindings
are resilient and clearer:
- Use epgProgram?.iconUrl instead of epgProgram?.icon?.[0]?.src
- Use epgProgram?.title instead of epgProgram?.title?.[0]?.value
- Use epgProgram?.desc instead of epgProgram?.desc?.[0]?.value
- Guard description rendering with epgProgram?.desc

Adjust multi-epg header layout and add search UI styles to support
desktop window chrome and draggable regions:
- Add left padding to #epg-navigation for macOS traffic lights, enable
  backdrop drag via -webkit-app-region: drag and set z-index to keep
  header above content.
- Exclude interactive elements (buttons, search inputs) from drag by
  applying -webkit-app-region: no-drag to maintain native interactions.
- Add extensive styles for a channel search container (search input,
  icons, clear button, filter count, animations) and scaffold a program
  search container.

These changes improve robustness of EPG data rendering and add styled
search controls while ensuring proper window dragging behavior on
desktop builds.
2026-01-02 17:22:09 +01:00

93 lines
2.8 KiB
TypeScript

import { Injectable, signal, computed } from '@angular/core';
export interface EpgImportProgress {
url: string;
status: 'queued' | 'loading' | 'complete' | 'error';
stats?: { totalChannels: number; totalPrograms: number };
error?: string;
queuePosition?: number;
}
/**
* Service to track EPG import progress across multiple sources.
* Uses signals for reactive state management.
*/
@Injectable({ providedIn: 'root' })
export class EpgProgressService {
private _imports = signal<Map<string, EpgImportProgress>>(new Map());
private _initialized = false;
/** All current import progress entries */
readonly imports = computed(() => Array.from(this._imports().values()));
/** Whether any imports are currently loading */
readonly hasActiveImports = computed(() =>
this.imports().some((i) => i.status === 'loading')
);
/** Count of active imports */
readonly activeCount = computed(() =>
this.imports().filter((i) => i.status === 'loading').length
);
/** Queued imports waiting to be processed */
readonly queuedImports = computed(() =>
this.imports()
.filter((i) => i.status === 'queued')
.sort((a, b) => (a.queuePosition ?? 0) - (b.queuePosition ?? 0))
);
/** Count of queued imports */
readonly queuedCount = computed(() => this.queuedImports().length);
/** Whether the panel should be visible */
readonly isVisible = computed(() => this.imports().length > 0);
constructor() {
this.initializeListener();
}
private initializeListener(): void {
if (this._initialized) return;
this._initialized = true;
// Listen for EPG progress updates from Electron
if (window.electron?.onEpgProgress) {
window.electron.onEpgProgress((data) => {
this.updateProgress(data);
});
}
}
private updateProgress(progress: EpgImportProgress): void {
this._imports.update((current) => {
const updated = new Map(current);
updated.set(progress.url, progress);
return updated;
});
// Auto-remove completed/error entries after delay
if (progress.status === 'complete' || progress.status === 'error') {
setTimeout(() => this.removeImport(progress.url), 5000);
}
}
private removeImport(url: string): void {
this._imports.update((current) => {
const updated = new Map(current);
updated.delete(url);
return updated;
});
}
/** Manually dismiss an import notification */
dismiss(url: string): void {
this.removeImport(url);
}
/** Dismiss all notifications */
dismissAll(): void {
this._imports.set(new Map());
}
}