import { Component, HostBinding, inject, OnInit } from '@angular/core'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Router, RouterOutlet } from '@angular/router'; import { Actions, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import { EpgService } from '@iptvnator/epg/data-access'; import { WORKSPACE_SHELL_ACTIONS } from '@iptvnator/workspace/shell/util'; import { EpgProgressPanelComponent } from '@iptvnator/ui/epg/progress-panel'; import { PlaylistActions, selectAllPlaylistsMeta } from 'm3u-state'; import { filter, take } from 'rxjs'; import { DataService } from 'services'; import { AppConfig } from '../environments/environment'; import { AUTO_UPDATE_PLAYLISTS, Language, OPEN_FILE, Settings, STORE_KEY, Theme, } from 'shared-interfaces'; import { SettingsService } from './services/settings.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', imports: [EpgProgressPanelComponent, RouterOutlet], }) export class AppComponent implements OnInit { @HostBinding('class.macos-platform') get isMacOS() { return ( window.electron && navigator.platform.toLowerCase().includes('mac') ); } private actions$ = inject(Actions); private dataService = inject(DataService); private epgService = inject(EpgService); private snackBar = inject(MatSnackBar); private router = inject(Router); private store = inject(Store); private translate = inject(TranslateService); private settingsService = inject(SettingsService); private readonly workspaceShellActions = inject(WORKSPACE_SHELL_ACTIONS); private readonly defaultUiLang: Language = AppConfig.product?.defaultLanguage ?? (AppConfig.defaultTranslateLang === 'es' ? Language.SPANISH : Language.ENGLISH); constructor() { if ( ((this.dataService.isElectron && this.dataService?.remote?.process.platform === 'linux') || this.dataService?.remote?.process.platform === 'win32') && this.dataService.remote.process.argv.length > 2 ) { const filePath = this.dataService.remote.process.argv.find( (filepath) => filepath.endsWith('.m3u') || filepath.endsWith('.m3u8') ); if (filePath) { const filePathsArray = filePath.split('/'); const fileName = filePathsArray[filePathsArray.length - 1]; this.dataService.sendIpcEvent(OPEN_FILE, { filePath, fileName, }); } } if (window.electron) { document.addEventListener('keydown', (event) => { if (event.ctrlKey || event.metaKey) { if (event.key === 'f') { event.preventDefault(); this.workspaceShellActions.openGlobalSearch(); } else if (event.key === 'r') { event.preventDefault(); this.workspaceShellActions.openGlobalRecent(); } } }); } if (AppConfig.product?.indexHtmlLang) { document.documentElement.lang = AppConfig.product.indexHtmlLang; } if (AppConfig.product?.appDisplayName) { document.title = AppConfig.product.appDisplayName; } } ngOnInit() { this.store.dispatch(PlaylistActions.loadPlaylists()); /** English is the translation fallback (see TranslateModule defaultLanguage / useDefaultLang). */ this.translate.setDefaultLang(Language.ENGLISH); this.initSettings(); this.triggerAutoUpdatePlaylists(); } /** * Reads the settings object from local storage and initializes the * application based on them */ initSettings(): void { this.settingsService .getValueFromLocalStorage(STORE_KEY.Settings) .subscribe((settings: Settings) => { if (settings && Object.keys(settings).length > 0) { // No need to send settings to Electron on init // Settings are stored in IndexedDB and loaded by the settings store // Only specific Electron settings (MPV/VLC paths) are sent when changed in settings component this.translate.use( settings.language ?? this.defaultUiLang ); // Fetch EPG if URLs are configured (only fetch stale data) if ( window.electron && settings.epgUrl?.length > 0 && settings.epgUrl?.some((u) => u !== '') ) { this.fetchStaleEpgData(settings.epgUrl); } if (settings.theme) { this.settingsService.changeTheme(settings.theme); } else { this.detectDarkMode(); } } else { this.translate.use(this.defaultUiLang); this.detectDarkMode(); } }); } /** * Applies the operating system color scheme when no explicit theme is set */ detectDarkMode(): void { this.settingsService.changeTheme(Theme.SystemTheme); } /** * Navigate to the specified route * @param route route to navigate to */ navigateToRoute(route: string) { this.router.navigateByUrl(route); } /** * Fetches EPG data only for URLs that have stale or missing data. * Data is considered fresh if updated within the last 12 hours. */ private async fetchStaleEpgData(urls: string[]): Promise { try { const result = await window.electron.checkEpgFreshness(urls, 12); if (result.freshUrls.length > 0) { console.log( `EPG: ${result.freshUrls.length} source(s) already fresh, skipping fetch` ); // Show snackbar if all EPG sources are fresh (no stale URLs) if (result.staleUrls.length === 0) { this.snackBar.open( this.translate.instant('EPG.UP_TO_DATE'), this.translate.instant('CLOSE'), { duration: 3000 } ); } } if (result.staleUrls.length > 0) { console.log( `EPG: Fetching ${result.staleUrls.length} stale source(s)` ); this.epgService.fetchEpg(result.staleUrls); } } catch (error) { console.error('Error checking EPG freshness, fetching all:', error); // Fallback: fetch all URLs if freshness check fails this.epgService.fetchEpg(urls); } } /** * Triggers auto-update for playlists that have autoRefresh enabled */ private triggerAutoUpdatePlaylists(): void { // Wait for playlists to be loaded successfully this.actions$ .pipe( ofType(PlaylistActions.loadPlaylistsSuccess), take(1) // Only trigger once on app startup ) .subscribe(() => { // Get all playlists from store this.store .select(selectAllPlaylistsMeta) .pipe( take(1), filter((playlists) => playlists.length > 0) ) .subscribe((playlists) => { // Filter playlists with autoRefresh enabled const playlistsToUpdate = playlists.filter( (playlist) => playlist.autoRefresh === true ); // Trigger auto-update if there are playlists to update if (playlistsToUpdate.length > 0) { console.log( `Auto-updating ${playlistsToUpdate.length} playlist(s) on startup` ); this.dataService.sendIpcEvent( AUTO_UPDATE_PLAYLISTS, playlistsToUpdate ); } }); }); } }