mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
refactor(runtime): gate settings external players by capability
This commit is contained in:
1 parent
b4fd93354e
commit
09e2e08fc1
9 files changed
+104
-27
No files matched your search
@@ -69,7 +69,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (isDesktop() && isExternalPlayerSelected()) {
|
||||
@if (supportsManagedExternalPlayers() && isExternalPlayerSelected()) {
|
||||
<div
|
||||
class="setting-item"
|
||||
data-test-id="external-player-double-click-setting"
|
||||
@@ -148,7 +148,7 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isDesktop() && form().value.player === 'mpv') {
|
||||
@if (supportsManagedExternalPlayers() && form().value.player === 'mpv') {
|
||||
<div class="setting-item">
|
||||
<div class="setting-item__meta">
|
||||
<h4>{{ 'SETTINGS.MPV_PLAYER_PATH_LABEL' | translate }}</h4>
|
||||
@@ -220,7 +220,7 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isDesktop() && form().value.player === 'vlc') {
|
||||
@if (supportsManagedExternalPlayers() && form().value.player === 'vlc') {
|
||||
<div class="setting-item">
|
||||
<div class="setting-item__meta">
|
||||
<h4>{{ 'SETTINGS.VLC_PLAYER_PATH_LABEL' | translate }}</h4>
|
||||
|
||||
@@ -58,9 +58,10 @@ describe('SettingsPlaybackSectionComponent', () => {
|
||||
fixture.componentRef.setInput('streamFormatEnum', StreamFormat);
|
||||
});
|
||||
|
||||
it('hides the external-player double-click option outside desktop builds', () => {
|
||||
it('hides the external-player double-click option when managed external players are unsupported', () => {
|
||||
fixture.componentRef.setInput('form', createForm(VideoPlayer.MPV));
|
||||
fixture.componentRef.setInput('isDesktop', false);
|
||||
fixture.componentRef.setInput('isDesktop', true);
|
||||
fixture.componentRef.setInput('supportsManagedExternalPlayers', false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
@@ -75,6 +76,7 @@ describe('SettingsPlaybackSectionComponent', () => {
|
||||
|
||||
it('hides the external-player double-click option for embedded players', () => {
|
||||
fixture.componentRef.setInput('isDesktop', true);
|
||||
fixture.componentRef.setInput('supportsManagedExternalPlayers', true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
@@ -85,10 +87,11 @@ describe('SettingsPlaybackSectionComponent', () => {
|
||||
});
|
||||
|
||||
it.each([VideoPlayer.MPV, VideoPlayer.VLC])(
|
||||
'labels the double-click option as external-player behavior on desktop for %s',
|
||||
'labels the double-click option as external-player behavior when managed players are supported for %s',
|
||||
(player) => {
|
||||
fixture.componentRef.setInput('form', createForm(player));
|
||||
fixture.componentRef.setInput('isDesktop', true);
|
||||
fixture.componentRef.setInput('supportsManagedExternalPlayers', true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
@@ -106,6 +109,7 @@ describe('SettingsPlaybackSectionComponent', () => {
|
||||
const form = createForm();
|
||||
fixture.componentRef.setInput('form', form);
|
||||
fixture.componentRef.setInput('isDesktop', true);
|
||||
fixture.componentRef.setInput('supportsManagedExternalPlayers', true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
@@ -147,6 +151,7 @@ describe('SettingsPlaybackSectionComponent', () => {
|
||||
it('shows MPV bundle guidance and the IINA executable tip for desktop MPV playback', () => {
|
||||
fixture.componentRef.setInput('form', createForm(VideoPlayer.MPV));
|
||||
fixture.componentRef.setInput('isDesktop', true);
|
||||
fixture.componentRef.setInput('supportsManagedExternalPlayers', true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toContain(
|
||||
@@ -162,9 +167,10 @@ describe('SettingsPlaybackSectionComponent', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('hides MPV path guidance and the IINA executable tip outside desktop builds', () => {
|
||||
it('hides MPV path guidance and the IINA executable tip when managed external players are unsupported', () => {
|
||||
fixture.componentRef.setInput('form', createForm(VideoPlayer.MPV));
|
||||
fixture.componentRef.setInput('isDesktop', false);
|
||||
fixture.componentRef.setInput('isDesktop', true);
|
||||
fixture.componentRef.setInput('supportsManagedExternalPlayers', false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).not.toContain(
|
||||
@@ -183,6 +189,7 @@ describe('SettingsPlaybackSectionComponent', () => {
|
||||
it('shows VLC bundle guidance without the IINA tip for desktop VLC playback', () => {
|
||||
fixture.componentRef.setInput('form', createForm(VideoPlayer.VLC));
|
||||
fixture.componentRef.setInput('isDesktop', true);
|
||||
fixture.componentRef.setInput('supportsManagedExternalPlayers', true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toContain(
|
||||
@@ -198,9 +205,10 @@ describe('SettingsPlaybackSectionComponent', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('hides VLC path guidance outside desktop builds', () => {
|
||||
it('hides VLC path guidance when managed external players are unsupported', () => {
|
||||
fixture.componentRef.setInput('form', createForm(VideoPlayer.VLC));
|
||||
fixture.componentRef.setInput('isDesktop', false);
|
||||
fixture.componentRef.setInput('isDesktop', true);
|
||||
fixture.componentRef.setInput('supportsManagedExternalPlayers', false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).not.toContain(
|
||||
@@ -215,6 +223,7 @@ describe('SettingsPlaybackSectionComponent', () => {
|
||||
|
||||
it('does not show external-player path guidance for embedded players', () => {
|
||||
fixture.componentRef.setInput('isDesktop', true);
|
||||
fixture.componentRef.setInput('supportsManagedExternalPlayers', true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).not.toContain(
|
||||
@@ -233,6 +242,7 @@ describe('SettingsPlaybackSectionComponent', () => {
|
||||
it('shows MPV command-line arguments only when MPV is selected', () => {
|
||||
fixture.componentRef.setInput('form', createForm(VideoPlayer.MPV));
|
||||
fixture.componentRef.setInput('isDesktop', true);
|
||||
fixture.componentRef.setInput('supportsManagedExternalPlayers', true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
@@ -258,6 +268,7 @@ describe('SettingsPlaybackSectionComponent', () => {
|
||||
it('shows VLC command-line arguments only when VLC is selected', () => {
|
||||
fixture.componentRef.setInput('form', createForm(VideoPlayer.VLC));
|
||||
fixture.componentRef.setInput('isDesktop', true);
|
||||
fixture.componentRef.setInput('supportsManagedExternalPlayers', true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
|
||||
@@ -45,6 +45,7 @@ export class SettingsPlaybackSectionComponent {
|
||||
readonly players = input.required<SettingsPlayerOption[]>();
|
||||
readonly streamFormatEnum = input.required<typeof StreamFormat>();
|
||||
readonly isDesktop = input(false);
|
||||
readonly supportsManagedExternalPlayers = input(false);
|
||||
readonly selectRecordingFolder = output<void>();
|
||||
|
||||
isExternalPlayerSelected(): boolean {
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
[players]="players()"
|
||||
[streamFormatEnum]="streamFormatEnum"
|
||||
[isDesktop]="isDesktop"
|
||||
[supportsManagedExternalPlayers]="supportsManagedExternalPlayers"
|
||||
(selectRecordingFolder)="selectRecordingFolder()"
|
||||
/>
|
||||
|
||||
|
||||
@@ -253,6 +253,8 @@ describe('SettingsComponent', () => {
|
||||
forceFetchEpg: jest.fn().mockResolvedValue({ success: true }),
|
||||
getAppVersion: jest.fn().mockResolvedValue('1.0.0'),
|
||||
getLocalIpAddresses: jest.fn().mockResolvedValue([]),
|
||||
openInMpv: jest.fn(),
|
||||
openInVlc: jest.fn(),
|
||||
platform: 'linux',
|
||||
saveFileDialog: jest.fn().mockResolvedValue('/tmp/backup.json'),
|
||||
setMpvPlayerPath: jest.fn().mockResolvedValue(undefined),
|
||||
@@ -472,8 +474,44 @@ describe('SettingsComponent', () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('hides external player path settings when the Electron bridge is incomplete', async () => {
|
||||
fixture.destroy();
|
||||
window.electron = {
|
||||
getAppVersion: jest.fn().mockResolvedValue('1.0.0'),
|
||||
platform: 'linux',
|
||||
updateSettings: jest.fn().mockResolvedValue(undefined),
|
||||
} as unknown as typeof window.electron;
|
||||
|
||||
const partialBridgeFixture =
|
||||
TestBed.createComponent(SettingsComponent);
|
||||
const partialBridgeComponent =
|
||||
partialBridgeFixture.componentInstance;
|
||||
partialBridgeComponent.checkAppVersion = jest.fn();
|
||||
partialBridgeComponent.fetchLocalIpAddresses = jest
|
||||
.fn()
|
||||
.mockResolvedValue(undefined);
|
||||
partialBridgeFixture.detectChanges();
|
||||
|
||||
expect(partialBridgeComponent.isDesktop).toBe(true);
|
||||
expect(
|
||||
partialBridgeComponent.supportsExternalPlayerPathSettings
|
||||
).toBe(false);
|
||||
expect(
|
||||
partialBridgeComponent
|
||||
.players()
|
||||
.some((player) => player.id === VideoPlayer.MPV)
|
||||
).toBe(false);
|
||||
expect(
|
||||
partialBridgeComponent
|
||||
.players()
|
||||
.some((player) => player.id === VideoPlayer.VLC)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('does not block settings initialization while embedded mpv support is pending', async () => {
|
||||
let resolveSupport: (value: EmbeddedMpvSupport) => void;
|
||||
let resolveSupport:
|
||||
| ((value: EmbeddedMpvSupport) => void)
|
||||
| undefined;
|
||||
window.electron = {
|
||||
...window.electron,
|
||||
getEmbeddedMpvSupport: jest.fn(
|
||||
@@ -495,8 +533,11 @@ describe('SettingsComponent', () => {
|
||||
).toBe(false);
|
||||
|
||||
if (!resolveSupport) {
|
||||
throw new Error('Expected embedded MPV support resolver');
|
||||
throw new Error(
|
||||
'Expected embedded MPV support probe to start'
|
||||
);
|
||||
}
|
||||
|
||||
resolveSupport({
|
||||
supported: true,
|
||||
platform: 'darwin',
|
||||
|
||||
@@ -137,6 +137,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
/** Flag that indicates whether the app runs in electron environment */
|
||||
readonly isDesktop = this.runtime.isElectron;
|
||||
readonly supportsExternalPlayerPathSettings =
|
||||
this.runtime.supportsExternalPlayerPathSettings;
|
||||
readonly embeddedMpvSupport = signal<EmbeddedMpvSupport | null>(null);
|
||||
readonly supportsEmbeddedMpv = computed(
|
||||
() => this.isDesktop && !!this.embeddedMpvSupport()?.supported
|
||||
@@ -156,13 +158,15 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...SETTINGS_OS_PLAYER_OPTIONS,
|
||||
...(this.supportsExternalPlayerPathSettings
|
||||
? SETTINGS_OS_PLAYER_OPTIONS
|
||||
: []),
|
||||
]);
|
||||
|
||||
/** Player options */
|
||||
readonly players = computed(() => [
|
||||
...SETTINGS_EMBEDDED_PLAYER_OPTIONS,
|
||||
...(this.isDesktop ? this.osPlayers() : []),
|
||||
...this.osPlayers(),
|
||||
]);
|
||||
|
||||
/** Current version of the app */
|
||||
@@ -481,7 +485,9 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (window.electron) {
|
||||
window.electron.updateSettings(settings);
|
||||
}
|
||||
|
||||
if (this.supportsExternalPlayerPathSettings && window.electron) {
|
||||
window.electron.setMpvPlayerPath(settings.mpvPlayerPath);
|
||||
window.electron.setVlcPlayerPath(settings.vlcPlayerPath);
|
||||
}
|
||||
|
||||
@@ -77,9 +77,10 @@ requires the complete downloads preload API surface used by `DownloadsService`,
|
||||
bridge, `supportsXtreamSectionNavigation` is available in PWA and in Electron
|
||||
when either the SQLite Xtream data source or the Xtream API transport is
|
||||
available, and
|
||||
`supportsManagedExternalPlayers` requires the MPV and VLC preload launch methods
|
||||
(`openInMpv` and `openInVlc`); a partial Electron bridge must not expose
|
||||
desktop-only actions in the PWA/shared UI.
|
||||
`supportsManagedExternalPlayers` requires the MPV and VLC preload launch and
|
||||
path-setting methods (`openInMpv`, `openInVlc`, `setMpvPlayerPath`, and
|
||||
`setVlcPlayerPath`); a partial Electron bridge must not expose desktop-only
|
||||
actions in the PWA/shared UI.
|
||||
|
||||
## Runtime Limitations
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ describe('RuntimeCapabilitiesService', () => {
|
||||
expect(service.supportsPortalActivityStorage).toBe(false);
|
||||
expect(service.supportsPlaylistRefresh).toBe(false);
|
||||
expect(service.supportsManagedExternalPlayers).toBe(false);
|
||||
expect(service.supportsExternalPlayerPathSettings).toBe(false);
|
||||
expect(service.supportsEmbeddedMpv).toBe(false);
|
||||
expect(service.supportsDesktopFileSave).toBe(false);
|
||||
expect(service.supportsRemoteControl).toBe(false);
|
||||
@@ -97,6 +98,8 @@ describe('RuntimeCapabilitiesService', () => {
|
||||
onPlaylistRefreshEvent: jest.fn(),
|
||||
openInMpv: jest.fn(),
|
||||
openInVlc: jest.fn(),
|
||||
setMpvPlayerPath: jest.fn(),
|
||||
setVlcPlayerPath: jest.fn(),
|
||||
prepareEmbeddedMpv: jest.fn(),
|
||||
saveFileDialog: jest.fn(),
|
||||
writeFile: jest.fn(),
|
||||
@@ -120,6 +123,7 @@ describe('RuntimeCapabilitiesService', () => {
|
||||
expect(service.supportsPortalActivityStorage).toBe(true);
|
||||
expect(service.supportsPlaylistRefresh).toBe(true);
|
||||
expect(service.supportsManagedExternalPlayers).toBe(true);
|
||||
expect(service.supportsExternalPlayerPathSettings).toBe(true);
|
||||
expect(service.supportsEmbeddedMpv).toBe(true);
|
||||
expect(service.supportsDesktopFileSave).toBe(true);
|
||||
expect(service.supportsRemoteControl).toBe(true);
|
||||
@@ -143,6 +147,7 @@ describe('RuntimeCapabilitiesService', () => {
|
||||
expect(service.supportsPortalActivityStorage).toBe(false);
|
||||
expect(service.supportsPlaylistRefresh).toBe(false);
|
||||
expect(service.supportsManagedExternalPlayers).toBe(false);
|
||||
expect(service.supportsExternalPlayerPathSettings).toBe(false);
|
||||
expect(service.supportsEmbeddedMpv).toBe(false);
|
||||
expect(service.supportsDesktopFileSave).toBe(false);
|
||||
expect(service.supportsRemoteControl).toBe(false);
|
||||
@@ -170,22 +175,27 @@ describe('RuntimeCapabilitiesService', () => {
|
||||
expect(service.supportsXtreamSqliteDataSource).toBe(false);
|
||||
});
|
||||
|
||||
it('requires both managed external player launch methods', () => {
|
||||
testWindow.electron = {
|
||||
openInMpv: jest.fn(),
|
||||
};
|
||||
|
||||
const service = new RuntimeCapabilitiesService();
|
||||
|
||||
expect(service.isElectron).toBe(true);
|
||||
expect(service.supportsManagedExternalPlayers).toBe(false);
|
||||
|
||||
it('decouples external player launch support from path-setting support', () => {
|
||||
testWindow.electron = {
|
||||
openInMpv: jest.fn(),
|
||||
openInVlc: jest.fn(),
|
||||
};
|
||||
|
||||
const service = new RuntimeCapabilitiesService();
|
||||
|
||||
expect(service.isElectron).toBe(true);
|
||||
expect(service.supportsManagedExternalPlayers).toBe(true);
|
||||
expect(service.supportsExternalPlayerPathSettings).toBe(false);
|
||||
|
||||
testWindow.electron = {
|
||||
openInMpv: jest.fn(),
|
||||
openInVlc: jest.fn(),
|
||||
setMpvPlayerPath: jest.fn(),
|
||||
setVlcPlayerPath: jest.fn(),
|
||||
};
|
||||
|
||||
expect(service.supportsManagedExternalPlayers).toBe(true);
|
||||
expect(service.supportsExternalPlayerPathSettings).toBe(true);
|
||||
});
|
||||
|
||||
it('requires the complete downloads preload surface', () => {
|
||||
|
||||
@@ -136,6 +136,12 @@ export class RuntimeCapabilitiesService {
|
||||
);
|
||||
}
|
||||
|
||||
get supportsExternalPlayerPathSettings(): boolean {
|
||||
return ['setMpvPlayerPath', 'setVlcPlayerPath'].every((methodName) =>
|
||||
this.hasElectronMethod(methodName)
|
||||
);
|
||||
}
|
||||
|
||||
get supportsEmbeddedMpv(): boolean {
|
||||
return this.hasElectronMethod('prepareEmbeddedMpv');
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user