mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
* feat(ui): add custom title bar window controls for Windows and Linux Hide the native title bar on win32/linux (titleBarStyle: 'hidden', frame untouched so native resize borders and snapping keep working) and render minimize / maximize-restore / close buttons in the renderer, mirroring the existing macOS traffic-light setup. - New WINDOW:* IPC contract (minimize, toggle-maximize, close, get-state) handled in window.events.ts, resolved from the sender WebContents; close goes through win.close() so window-bounds persistence still runs. - WINDOW:STATE_CHANGED pushed on maximize/unmaximize/fullscreen so the maximize/restore glyph stays correct for OS-triggered changes; controls hide while fullscreen. - WindowControlsComponent mounts once in app-root as a manual popover so it stays in the browser top layer above CDK overlays (dialogs, multi-EPG) - same behavior as macOS traffic lights. - Theme-aware via CSS vars (--app-on-surface, --app-hover-overlay); Windows-red close hover. Drag regions get right padding through a body-level frameless-platform class. - Gated by RuntimeCapabilitiesService.usesCustomWindowControls; PWA and macOS never mount the controls. Includes unit specs for the component and IPC handlers, an Electron E2E suite (window-controls.e2e.ts), and a window-chrome section in docs/architecture/workspace-shell.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(linux): upgrade Electron to 41 for frameless window decorations on Wayland With the native title bar hidden, Linux windows lost the WM-drawn shadow and rounded corners. Electron draws client-side decorations only on native Wayland, and frameless-window CSD (GTK drop shadow + extended resize boundaries) landed in Electron 41 - before that, frameless windows render as plain rectangles. - electron ^39.8.5 -> ^41.7.2 (Wayland auto-detected since 38.2; X11 sessions remain undecorated, matching other frameless Electron apps; Windows keeps its DWM shadow and rounded corners). - better-sqlite3 pinned to exactly 12.9.0: the last release shipping prebuilt binaries for both Node 20 (ABI 115, Jest) and Electron 41 (ABI 145, runtime). 12.10.0 dropped the Node 20 prebuilds, forcing a from-source build that fails without a C++ toolchain. - pnpm override node-abi 3.85.0 -> 3.92.0 so electron-builder install-app-deps can map Electron 41 to ABI 145. Reviewed Electron 40/41 breaking changes: only the renderer clipboard deprecation, which this app does not use. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(e2e): address review feedback and window-managerless Linux CI - Skip the three window-manager-dependent E2E assertions (maximize toggle, main-process state sync, minimize) on Linux CI: GitHub's ubuntu runners drive Electron under xvfb without a window manager, so maximize/minimize state never materializes there. Windows CI and local Linux/macOS runs keep the coverage. - WINDOW:TOGGLE_MAXIMIZE now returns the requested state instead of re-reading isMaximized() right after the call, which races on Linux window managers where maximize()/unmaximize() complete asynchronously; the WINDOW:STATE_CHANGED push stays authoritative. - Skip attaching window-state push listeners on macOS, where the custom controls never mount and the IPC traffic had no subscriber. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): gate custom window controls on the full bridge surface Include getWindowState and onWindowStateChange in the usesCustomWindowControls capability check — the controls rely on both for initial state and for keeping the maximize/restore glyph in sync with OS-triggered changes, so a partial bridge should not mount them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
282 lines
8.2 KiB
TypeScript
282 lines
8.2 KiB
TypeScript
import { Injectable } from '@angular/core';
|
|
import { ElectronBridgeApi } from '@iptvnator/shared/interfaces';
|
|
|
|
export type RuntimeEnvironment = 'electron' | 'pwa';
|
|
|
|
type RuntimeElectronBridge = Partial<ElectronBridgeApi>;
|
|
|
|
const playbackPositionStorageMethods = [
|
|
'dbSavePlaybackPosition',
|
|
'dbGetPlaybackPosition',
|
|
'dbGetSeriesPlaybackPositions',
|
|
'dbGetRecentPlaybackPositions',
|
|
'dbGetAllPlaybackPositions',
|
|
'dbClearAllPlaybackPositions',
|
|
'dbClearPlaybackPosition',
|
|
];
|
|
|
|
@Injectable({ providedIn: 'root' })
|
|
export class RuntimeCapabilitiesService {
|
|
get environment(): RuntimeEnvironment {
|
|
return this.isElectron ? 'electron' : 'pwa';
|
|
}
|
|
|
|
get isElectron(): boolean {
|
|
return !!this.electronBridge;
|
|
}
|
|
|
|
get isPwa(): boolean {
|
|
return !this.isElectron;
|
|
}
|
|
|
|
get platform(): string | undefined {
|
|
const platform = this.electronBridge?.['platform'];
|
|
return typeof platform === 'string' ? platform : undefined;
|
|
}
|
|
|
|
get isMacOS(): boolean {
|
|
return this.platform === 'darwin';
|
|
}
|
|
|
|
get isWindows(): boolean {
|
|
return this.platform === 'win32';
|
|
}
|
|
|
|
get isLinux(): boolean {
|
|
return this.platform === 'linux';
|
|
}
|
|
|
|
/**
|
|
* True when the window has no native title bar and the renderer must
|
|
* draw its own window-management buttons (Windows/Linux). macOS keeps
|
|
* the native traffic lights instead.
|
|
*/
|
|
get usesCustomWindowControls(): boolean {
|
|
return (
|
|
(this.isWindows || this.isLinux) &&
|
|
[
|
|
'minimizeWindow',
|
|
'toggleMaximizeWindow',
|
|
'closeWindow',
|
|
'getWindowState',
|
|
'onWindowStateChange',
|
|
].every((methodName) => this.hasElectronMethod(methodName))
|
|
);
|
|
}
|
|
|
|
get supportsEpg(): boolean {
|
|
return (
|
|
this.supportsEpgImport &&
|
|
this.supportsEpgProgramLookup &&
|
|
this.supportsEpgSourceFreshness &&
|
|
this.supportsEpgDataManagement &&
|
|
this.supportsEpgChannelBrowser &&
|
|
this.supportsEpgProgramSearch
|
|
);
|
|
}
|
|
|
|
get supportsEpgImport(): boolean {
|
|
return this.hasElectronMethod('fetchEpg');
|
|
}
|
|
|
|
get supportsEpgProgress(): boolean {
|
|
return this.hasElectronMethod('onEpgProgress');
|
|
}
|
|
|
|
get supportsEpgProgramLookup(): boolean {
|
|
return this.hasElectronMethod('getChannelPrograms');
|
|
}
|
|
|
|
get supportsEpgCurrentProgramBatch(): boolean {
|
|
return this.hasElectronMethod('getCurrentProgramsBatch');
|
|
}
|
|
|
|
get supportsEpgChannelMetadata(): boolean {
|
|
return this.hasElectronMethod('getEpgChannelMetadata');
|
|
}
|
|
|
|
get supportsEpgSourceFreshness(): boolean {
|
|
return this.hasElectronMethod('checkEpgFreshness');
|
|
}
|
|
|
|
get supportsEpgDataManagement(): boolean {
|
|
return (
|
|
this.hasElectronMethod('forceFetchEpg') &&
|
|
this.hasElectronMethod('clearEpgData')
|
|
);
|
|
}
|
|
|
|
get supportsEpgChannelBrowser(): boolean {
|
|
return this.hasElectronMethod('getEpgChannelsByRange');
|
|
}
|
|
|
|
get supportsEpgProgramSearch(): boolean {
|
|
return this.hasElectronMethod('searchEpgPrograms');
|
|
}
|
|
|
|
get supportsSqlite(): boolean {
|
|
return [
|
|
'dbDeleteAllPlaylists',
|
|
'dbDeletePlaylist',
|
|
'dbGetAppPlaylist',
|
|
'dbGetAppPlaylists',
|
|
'dbGetAppState',
|
|
'dbSetAppState',
|
|
'dbUpsertAppPlaylist',
|
|
'dbUpsertAppPlaylists',
|
|
].every((methodName) => this.hasElectronMethod(methodName));
|
|
}
|
|
|
|
get supportsXtreamSqliteDataSource(): boolean {
|
|
return [
|
|
'dbGetPlaylist',
|
|
'dbCreatePlaylist',
|
|
'dbUpdatePlaylist',
|
|
'dbDeletePlaylist',
|
|
'dbHasCategories',
|
|
'dbGetCategories',
|
|
'dbSaveCategories',
|
|
'dbGetAllCategories',
|
|
'dbUpdateCategoryVisibility',
|
|
'dbHasContent',
|
|
'dbGetContent',
|
|
'dbSaveContent',
|
|
'dbGetAppState',
|
|
'dbSetAppState',
|
|
'dbSearchContent',
|
|
'dbGetFavorites',
|
|
'dbAddFavorite',
|
|
'dbRemoveFavorite',
|
|
'dbIsFavorite',
|
|
'dbGetRecentItems',
|
|
'dbAddRecentItem',
|
|
'dbRemoveRecentItem',
|
|
'dbClearPlaylistRecentItems',
|
|
'dbGetContentByXtreamId',
|
|
...playbackPositionStorageMethods,
|
|
'dbDeleteXtreamContent',
|
|
'dbRestoreXtreamUserData',
|
|
].every((methodName) => this.hasElectronMethod(methodName));
|
|
}
|
|
|
|
get supportsPlaybackPositionStorage(): boolean {
|
|
return playbackPositionStorageMethods.every((methodName) =>
|
|
this.hasElectronMethod(methodName)
|
|
);
|
|
}
|
|
|
|
get supportsPlaybackPositionUpdates(): boolean {
|
|
return this.hasElectronMethod('onPlaybackPositionUpdate');
|
|
}
|
|
|
|
get supportsDownloads(): boolean {
|
|
return [
|
|
'downloadsStart',
|
|
'downloadsCancel',
|
|
'downloadsRetry',
|
|
'downloadsRemove',
|
|
'downloadsGetList',
|
|
'downloadsGet',
|
|
'downloadsGetDefaultFolder',
|
|
'downloadsSelectFolder',
|
|
'downloadsRevealFile',
|
|
'downloadsPlayFile',
|
|
'downloadsClearCompleted',
|
|
'onDownloadsUpdate',
|
|
].every((methodName) => this.hasElectronMethod(methodName));
|
|
}
|
|
|
|
get supportsPortalActivityStorage(): boolean {
|
|
return [
|
|
'dbGetRecentlyViewed',
|
|
'dbClearRecentlyViewed',
|
|
'dbGetAllGlobalFavorites',
|
|
'dbGetGlobalRecentlyAdded',
|
|
'dbAddFavorite',
|
|
'dbRemoveFavorite',
|
|
'dbGetFavorites',
|
|
'dbReorderGlobalFavorites',
|
|
'dbGetRecentItems',
|
|
'dbAddRecentItem',
|
|
'dbClearPlaylistRecentItems',
|
|
'dbRemoveRecentItem',
|
|
'dbRemoveRecentItemsBatch',
|
|
'dbGetContentByXtreamId',
|
|
].every((methodName) => this.hasElectronMethod(methodName));
|
|
}
|
|
|
|
get supportsAppStateStorage(): boolean {
|
|
return ['dbGetAppState', 'dbSetAppState'].every((methodName) =>
|
|
this.hasElectronMethod(methodName)
|
|
);
|
|
}
|
|
|
|
get supportsStalkerPlaylistSqliteSync(): boolean {
|
|
return ['dbGetPlaylist', 'dbCreatePlaylist'].every((methodName) =>
|
|
this.hasElectronMethod(methodName)
|
|
);
|
|
}
|
|
|
|
get supportsPlaylistRefresh(): boolean {
|
|
return [
|
|
'refreshPlaylist',
|
|
'cancelPlaylistRefresh',
|
|
'onPlaylistRefreshEvent',
|
|
].every((methodName) => this.hasElectronMethod(methodName));
|
|
}
|
|
|
|
get supportsManagedExternalPlayers(): boolean {
|
|
return ['openInMpv', 'openInVlc'].every((methodName) =>
|
|
this.hasElectronMethod(methodName)
|
|
);
|
|
}
|
|
|
|
get supportsExternalPlayerPathSettings(): boolean {
|
|
return ['setMpvPlayerPath', 'setVlcPlayerPath'].every((methodName) =>
|
|
this.hasElectronMethod(methodName)
|
|
);
|
|
}
|
|
|
|
get supportsEmbeddedMpv(): boolean {
|
|
return this.hasElectronMethod('prepareEmbeddedMpv');
|
|
}
|
|
|
|
get supportsDesktopFileSave(): boolean {
|
|
return (
|
|
this.hasElectronMethod('saveFileDialog') &&
|
|
this.hasElectronMethod('writeFile')
|
|
);
|
|
}
|
|
|
|
get supportsRemoteControl(): boolean {
|
|
return (
|
|
this.hasElectronMethod('updateRemoteControlStatus') &&
|
|
this.hasElectronMethod('onChannelChange') &&
|
|
this.hasElectronMethod('onRemoteControlCommand')
|
|
);
|
|
}
|
|
|
|
get supportsXtreamSectionNavigation(): boolean {
|
|
return (
|
|
this.isPwa ||
|
|
this.supportsXtreamSqliteDataSource ||
|
|
this.hasElectronMethod('xtreamRequest')
|
|
);
|
|
}
|
|
|
|
private hasElectronMethod(methodName: string): boolean {
|
|
const bridge = this.electronBridge as
|
|
| Record<string, unknown>
|
|
| undefined;
|
|
return typeof bridge?.[methodName] === 'function';
|
|
}
|
|
|
|
private get electronBridge(): RuntimeElectronBridge | undefined {
|
|
if (typeof window === 'undefined') {
|
|
return undefined;
|
|
}
|
|
|
|
return window.electron;
|
|
}
|
|
}
|