Files
iptvnator/libs/services/src/lib/settings-store.defaults.ts
T
4grayandClaude Fable 5.1 dad9217363 fix(settings): follow an inconclusive Embedded MPV answer to a final decision
The settings store checked a saved Embedded MPV selection once. After an
inconclusive answer it kept the selection and never looked again, so when
mpv turned out to be really missing the player stayed on Embedded MPV
instead of falling back to the default one.

The store now follows the answer with `watchEmbeddedMpvSupport()` until it
is final and only then decides. It acts on an answer only while Embedded
MPV is still the saved player, so a player picked meanwhile, also while
the first answer was pending, is never overwritten.

The watch backs off from 3 s to 30 s between rechecks, so a login shell
that never answers does not keep the app polling at the first rate, and it
no longer schedules a recheck after its answer handler stopped it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 20:03:43 +02:00

167 lines
5.3 KiB
TypeScript

import {
DEFAULT_DASHBOARD_RAILS_SETTINGS,
DEFAULT_PARENTAL_LOCK_RELOCK_MINUTES,
DEFAULT_TMDB_SETTINGS,
Language,
Settings,
StartupBehavior,
StreamFormat,
Theme,
VideoPlayer,
watchEmbeddedMpvSupport,
} from '@iptvnator/shared/interfaces';
/** Defaults and boot-time helpers of `SettingsStore`, split out for size. */
export const DEFAULT_SETTINGS: Settings = {
player: VideoPlayer.VideoJs,
webPlayerSharedControls: true,
playerAmbientMode: false,
playerUpNextRail: true,
playerUpNextCard: true,
fullscreenChannelPanel: true,
vodAutoFailover: false,
m3uVodDetails: true,
streamFormat: StreamFormat.AutoStreamFormat,
openStreamOnDoubleClick: false,
language: Language.ENGLISH,
showCaptions: false,
showDashboard: true,
startupBehavior: StartupBehavior.FirstView,
startupWindowMode: 'normal',
updateChannel: 'stable',
showExternalPlaybackBar: true,
stripCountryPrefix: false,
theme: Theme.SystemTheme,
mpvPlayerPath: '',
mpvPlayerArguments: '',
mpvReuseInstance: false,
vlcPlayerPath: '',
vlcPlayerArguments: '',
vlcReuseInstance: false,
remoteControl: false,
remoteControlPort: 8765,
epgUrl: [],
downloadFolder: '',
recordingFolder: '',
embeddedMpvFrameCopy: false,
embeddedMpvExtraOptions: '',
embeddedMpvAutoReconnect: true,
portalConnectivityGuard: true,
coverSize: 'medium',
showCoverTitles: true,
epgViewMode: 'timeline',
epgOffsetMinutes: 0,
dashboardRails: DEFAULT_DASHBOARD_RAILS_SETTINGS,
preferUploadedEpgOverXtream: false,
trustedPrivateNetworkEpgUrls: [],
trustedInsecureTlsHosts: [],
tmdb: DEFAULT_TMDB_SETTINGS,
parentalLockEnabled: false,
parentalLockRelockMinutes: DEFAULT_PARENTAL_LOCK_RELOCK_MINUTES,
};
/**
* Which half of the settings persistence round-trip failed, if any.
*
* Settings live in the renderer's IndexedDB, which can be unavailable for
* reasons the app cannot control (a second instance holding the Chromium
* storage lock, a corrupted profile, storage blocked by security software).
* Both failures used to be swallowed: `updateSettings` patches the in-memory
* state before persisting, so a failed write still looked applied until the
* next restart (issue #1156). Recording the failure lets the settings UI say
* so instead of pretending the change stuck.
*/
export type SettingsStorageFailure = 'load' | 'save';
export interface SettingsStorageState {
storageFailure: SettingsStorageFailure | null;
}
let embeddedMpvPrepareScheduled = false;
export function scheduleEmbeddedMpvPrepare(): void {
if (
embeddedMpvPrepareScheduled ||
typeof window === 'undefined' ||
!window.electron?.prepareEmbeddedMpv
) {
return;
}
embeddedMpvPrepareScheduled = true;
const prepare = () => {
void window.electron
.prepareEmbeddedMpv?.()
.then((support) => {
if (!support?.supported) {
embeddedMpvPrepareScheduled = false;
}
})
.catch((error) => {
embeddedMpvPrepareScheduled = false;
console.warn('Failed to prepare embedded MPV.', error);
});
};
const idleWindow = window as typeof window & {
requestIdleCallback?: (
callback: IdleRequestCallback,
options?: IdleRequestOptions
) => number;
};
if (idleWindow.requestIdleCallback) {
idleWindow.requestIdleCallback(prepare, { timeout: 5000 });
} else {
window.setTimeout(prepare, 2000);
}
}
/**
* Checks a saved Embedded MPV selection against this machine: schedules the
* idle prepare when it is supported, and calls `fallBack` when it is not or
* when the check itself fails. An inconclusive answer is no verdict: the
* selection stays and the answer is followed until it is final. Nothing is
* done once `isSaved` turns false, because the user picked another player
* meanwhile. Resolves when the first answer has been handled.
*/
export function verifySavedEmbeddedMpvPlayer(
isSaved: () => boolean,
fallBack: () => Promise<void>
): Promise<void> {
const electron =
typeof window === 'undefined' ? undefined : window.electron;
if (!electron?.getEmbeddedMpvSupport) {
return fallBack();
}
return new Promise<void>((handled, failed) => {
const stop = watchEmbeddedMpvSupport(
() => electron.getEmbeddedMpvSupport(),
(support) => {
if (!isSaved()) {
stop();
handled();
} else if (support.supported) {
scheduleEmbeddedMpvPrepare();
handled();
} else if (support.inconclusive) {
handled();
} else {
fallBack().then(handled, failed);
}
},
(error) => {
console.warn(
'Failed to verify embedded MPV support; reverting to the default inline player.',
error
);
if (isSaved()) {
fallBack().then(handled, failed);
} else {
handled();
}
}
);
});
}