228 lines
8.5 KiB
TypeScript
228 lines
8.5 KiB
TypeScript
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<void> {
|
|
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
|
|
);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|