feat(shell): startup window mode, --fullscreen switch and F11 toggle (#1514)

Settings > General gains "Window on startup" (normal / maximized /
fullscreen), Electron only, mirrored into the main-process config by
SETTINGS_UPDATE and applied at the next window creation. `--fullscreen`
forces one fullscreen launch (consumed by the first window). F11 toggles
OS-level fullscreen through WINDOW:TOGGLE_FULLSCREEN — the exit path on
Windows/Linux where the title bar is hidden — and is skipped while the
player owns document.fullscreenElement.

attachWindowStateEvents tracks native and HTML fullscreen as two flags,
since Electron leaves only the HTML state when the window was already
natively fullscreen. macOS ignores the constructor `fullscreen` option on a
hidden window, so ready-to-show repeats the request after show(). Toggles
are decided by an observe-only, event-fed tracker
(native-fullscreen-transitions.ts), never against isFullScreen().

Closes #1455

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 authored and GitHub committed 2026-09-03 19:49:41 +02:00
1 parent 078bd07d94
commit fa9084fca3
57 files changed
+1650 -8

No files matched your search

@@ -270,6 +270,27 @@ describe('RuntimeCapabilitiesService', () => {
expect(service.supportsXtreamSqliteDataSource).toBe(false);
});
it('offers the startup window mode only with both the settings mirror and the F11 toggle', () => {
testWindow.electron = undefined;
const service = new RuntimeCapabilitiesService();
expect(service.supportsStartupWindowMode).toBe(false);
// The mirror alone would let a user pick fullscreen with no way out
// on Windows/Linux, where the title bar is hidden.
testWindow.electron = { updateSettings: jest.fn() };
expect(service.supportsStartupWindowMode).toBe(false);
testWindow.electron = { toggleFullScreenWindow: jest.fn() };
expect(service.supportsStartupWindowMode).toBe(false);
testWindow.electron = {
updateSettings: jest.fn(),
toggleFullScreenWindow: jest.fn(),
};
expect(service.supportsStartupWindowMode).toBe(true);
});
it('decouples external player launch support from path-setting support', () => {
testWindow.electron = {
openInMpv: jest.fn(),
@@ -69,6 +69,20 @@ export class RuntimeCapabilitiesService {
);
}
/**
* Desktop window mode at launch (normal / maximized / fullscreen) plus
* the F11 toggle. The mode reaches the main process through
* `updateSettings`, and the toggle is what makes a fullscreen launch
* escapable on Windows/Linux, where the title bar is hidden — so the
* setting is offered only when both halves exist.
*/
get supportsStartupWindowMode(): boolean {
return (
this.hasElectronMethod('updateSettings') &&
this.hasElectronMethod('toggleFullScreenWindow')
);
}
get supportsEpg(): boolean {
return (
this.supportsEpgImport &&
@@ -146,6 +146,36 @@ describe('SettingsStore dashboard rail settings', () => {
expect(store.getSettings().stripCountryPrefix).toBe(false);
});
it('defaults the startup window mode to normal when the stored field is missing', async () => {
storedSettings = {};
const store = injector.get(SettingsStore);
await store.loadSettings();
expect(store.getSettings().startupWindowMode).toBe('normal');
});
it('restores a persisted fullscreen startup window mode', async () => {
storedSettings = { startupWindowMode: 'fullscreen' };
const store = injector.get(SettingsStore);
await store.loadSettings();
expect(store.getSettings().startupWindowMode).toBe('fullscreen');
});
it('normalizes a persisted unknown startup window mode to normal', async () => {
storedSettings = {
startupWindowMode:
'kiosk' as unknown as Settings['startupWindowMode'],
};
const store = injector.get(SettingsStore);
await store.loadSettings();
expect(store.getSettings().startupWindowMode).toBe('normal');
});
it('defaults ambient player mode to false when the stored field is missing', async () => {
storedSettings = {};
const store = injector.get(SettingsStore);
@@ -22,6 +22,7 @@ import {
Theme,
VideoPlayer,
normalizeDashboardRailsSettings,
normalizeStartupWindowMode,
} from '@iptvnator/shared/interfaces';
const DEFAULT_SETTINGS: Settings = {
@@ -37,6 +38,7 @@ const DEFAULT_SETTINGS: Settings = {
showCaptions: false,
showDashboard: true,
startupBehavior: StartupBehavior.FirstView,
startupWindowMode: 'normal',
showExternalPlaybackBar: true,
stripCountryPrefix: false,
theme: Theme.SystemTheme,
@@ -240,6 +242,9 @@ export const SettingsStore = signalStore(
showCaptions: store.showCaptions(),
showDashboard: store.showDashboard(),
startupBehavior: store.startupBehavior(),
startupWindowMode: normalizeStartupWindowMode(
store.startupWindowMode?.()
),
showExternalPlaybackBar:
store.showExternalPlaybackBar?.() ??
DEFAULT_SETTINGS.showExternalPlaybackBar,