mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19:46:16 -08:00
feat(workspace): add embedded MPV to the command palette
Register an "Switch player to Embedded MPV" command in the Cmd+K command
palette so the embedded MPV player can be activated like the other players.
Visibility is gated on an async getEmbeddedMpvSupport() check, mirroring the
Settings dropdown so the command only appears when embedded MPV is usable.
Generalizes the per-command visibility flag from desktopOnly to a `requires`
discriminator ('none' | 'managed-external' | 'embedded-mpv').
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6373c798a1
commit
f7ad17298f
3 files changed
+152
-23
No files matched your search
+85
-6
@@ -46,10 +46,17 @@ describe('WorkspacePlayerCommandsContributor', () => {
|
||||
let viewCommands: ViewCommandsMock;
|
||||
let settingsStore: SettingsStoreMock;
|
||||
let snackBar: SnackBarMock;
|
||||
let runtime: { supportsManagedExternalPlayers: boolean };
|
||||
let runtime: {
|
||||
supportsManagedExternalPlayers: boolean;
|
||||
supportsEmbeddedMpv: boolean;
|
||||
};
|
||||
let translate: { instant: jest.Mock; onLangChange: ReturnType<typeof of> };
|
||||
|
||||
function bootstrap(options: { supportsManagedExternalPlayers: boolean }) {
|
||||
function bootstrap(options: {
|
||||
supportsManagedExternalPlayers: boolean;
|
||||
supportsEmbeddedMpv?: boolean;
|
||||
embeddedMpvSupportResult?: { supported: boolean } | null;
|
||||
}) {
|
||||
viewCommands = {
|
||||
registerCommand: jest.fn().mockReturnValue(() => undefined),
|
||||
commands: jest.fn().mockReturnValue([]),
|
||||
@@ -62,13 +69,23 @@ describe('WorkspacePlayerCommandsContributor', () => {
|
||||
runtime = {
|
||||
supportsManagedExternalPlayers:
|
||||
options.supportsManagedExternalPlayers,
|
||||
supportsEmbeddedMpv: options.supportsEmbeddedMpv ?? false,
|
||||
};
|
||||
|
||||
const electronStub = runtime.supportsEmbeddedMpv
|
||||
? {
|
||||
getEmbeddedMpvSupport: jest.fn().mockResolvedValue(
|
||||
options.embeddedMpvSupportResult ?? {
|
||||
supported: true,
|
||||
}
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
(window as unknown as { electron?: unknown }).electron = electronStub;
|
||||
translate = {
|
||||
instant: jest.fn(
|
||||
(key: string, params?: Record<string, string | number>) =>
|
||||
params?.['name']
|
||||
? `${key}:${params['name']}`
|
||||
: key
|
||||
params?.['name'] ? `${key}:${params['name']}` : key
|
||||
),
|
||||
onLangChange: of(null),
|
||||
};
|
||||
@@ -92,9 +109,10 @@ describe('WorkspacePlayerCommandsContributor', () => {
|
||||
|
||||
afterEach(() => {
|
||||
TestBed.resetTestingModule();
|
||||
delete (window as unknown as { electron?: unknown }).electron;
|
||||
});
|
||||
|
||||
it('registers all five player commands when running in Electron', () => {
|
||||
it('registers all six player commands when running in Electron', () => {
|
||||
bootstrap({ supportsManagedExternalPlayers: true });
|
||||
|
||||
const ids = getRegistered(viewCommands).map((c) => c.id);
|
||||
@@ -102,6 +120,7 @@ describe('WorkspacePlayerCommandsContributor', () => {
|
||||
'switch-player-videojs',
|
||||
'switch-player-html5',
|
||||
'switch-player-artplayer',
|
||||
'switch-player-embedded-mpv',
|
||||
'switch-player-mpv',
|
||||
'switch-player-vlc',
|
||||
]);
|
||||
@@ -122,6 +141,66 @@ describe('WorkspacePlayerCommandsContributor', () => {
|
||||
expect(visibilityById['switch-player-vlc']).toBe(false);
|
||||
});
|
||||
|
||||
it('hides embedded MPV when it is unsupported', () => {
|
||||
bootstrap({
|
||||
supportsManagedExternalPlayers: true,
|
||||
supportsEmbeddedMpv: false,
|
||||
});
|
||||
|
||||
const embedded = getRegistered(viewCommands).find(
|
||||
(c) => c.id === 'switch-player-embedded-mpv'
|
||||
);
|
||||
expect(resolveBoolean(embedded?.visible)).toBe(false);
|
||||
});
|
||||
|
||||
it('shows embedded MPV once support resolves to supported', async () => {
|
||||
bootstrap({
|
||||
supportsManagedExternalPlayers: true,
|
||||
supportsEmbeddedMpv: true,
|
||||
embeddedMpvSupportResult: { supported: true },
|
||||
});
|
||||
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
const embedded = getRegistered(viewCommands).find(
|
||||
(c) => c.id === 'switch-player-embedded-mpv'
|
||||
);
|
||||
expect(resolveBoolean(embedded?.visible)).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps embedded MPV hidden when support resolves to unsupported', async () => {
|
||||
bootstrap({
|
||||
supportsManagedExternalPlayers: true,
|
||||
supportsEmbeddedMpv: true,
|
||||
embeddedMpvSupportResult: { supported: false },
|
||||
});
|
||||
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
const embedded = getRegistered(viewCommands).find(
|
||||
(c) => c.id === 'switch-player-embedded-mpv'
|
||||
);
|
||||
expect(resolveBoolean(embedded?.visible)).toBe(false);
|
||||
});
|
||||
|
||||
it('switches to embedded MPV on run', () => {
|
||||
bootstrap({
|
||||
supportsManagedExternalPlayers: true,
|
||||
supportsEmbeddedMpv: true,
|
||||
});
|
||||
|
||||
const embedded = getRegistered(viewCommands).find(
|
||||
(c) => c.id === 'switch-player-embedded-mpv'
|
||||
);
|
||||
embedded?.run({ query: '' });
|
||||
|
||||
expect(settingsStore.updateSettings).toHaveBeenCalledWith({
|
||||
player: VideoPlayer.EmbeddedMpv,
|
||||
});
|
||||
});
|
||||
|
||||
it('marks the active player command as disabled', () => {
|
||||
bootstrap({ supportsManagedExternalPlayers: true });
|
||||
settingsStore.player.set(VideoPlayer.MPV);
|
||||
|
||||
+57
-12
@@ -1,4 +1,4 @@
|
||||
import { DestroyRef, Injectable, inject } from '@angular/core';
|
||||
import { DestroyRef, Injectable, inject, signal } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
@@ -8,13 +8,15 @@ import {
|
||||
import { RuntimeCapabilitiesService, SettingsStore } from '@iptvnator/services';
|
||||
import { VideoPlayer } from '@iptvnator/shared/interfaces';
|
||||
|
||||
type PlayerCommandRequirement = 'none' | 'managed-external' | 'embedded-mpv';
|
||||
|
||||
interface PlayerCommandDefinition {
|
||||
id: string;
|
||||
player: VideoPlayer;
|
||||
icon: string;
|
||||
nameKey: string;
|
||||
keywords: readonly string[];
|
||||
desktopOnly: boolean;
|
||||
requires: PlayerCommandRequirement;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
@@ -25,7 +27,7 @@ const PLAYER_COMMAND_DEFS: readonly PlayerCommandDefinition[] = [
|
||||
icon: 'play_circle',
|
||||
nameKey: 'SETTINGS.PLAYER_VIDEOJS',
|
||||
keywords: ['player', 'videojs', 'video.js'],
|
||||
desktopOnly: false,
|
||||
requires: 'none',
|
||||
priority: 90,
|
||||
},
|
||||
{
|
||||
@@ -34,7 +36,7 @@ const PLAYER_COMMAND_DEFS: readonly PlayerCommandDefinition[] = [
|
||||
icon: 'play_circle',
|
||||
nameKey: 'SETTINGS.PLAYER_HTML5',
|
||||
keywords: ['player', 'html5'],
|
||||
desktopOnly: false,
|
||||
requires: 'none',
|
||||
priority: 91,
|
||||
},
|
||||
{
|
||||
@@ -43,17 +45,26 @@ const PLAYER_COMMAND_DEFS: readonly PlayerCommandDefinition[] = [
|
||||
icon: 'play_circle',
|
||||
nameKey: 'SETTINGS.PLAYER_ARTPLAYER',
|
||||
keywords: ['player', 'artplayer', 'art'],
|
||||
desktopOnly: false,
|
||||
requires: 'none',
|
||||
priority: 92,
|
||||
},
|
||||
{
|
||||
id: 'switch-player-embedded-mpv',
|
||||
player: VideoPlayer.EmbeddedMpv,
|
||||
icon: 'play_circle',
|
||||
nameKey: 'SETTINGS.PLAYER_EMBEDDED_MPV',
|
||||
keywords: ['player', 'embedded', 'mpv', 'native'],
|
||||
requires: 'embedded-mpv',
|
||||
priority: 93,
|
||||
},
|
||||
{
|
||||
id: 'switch-player-mpv',
|
||||
player: VideoPlayer.MPV,
|
||||
icon: 'play_circle_outline',
|
||||
nameKey: 'SETTINGS.PLAYER_MPV',
|
||||
keywords: ['player', 'mpv', 'external'],
|
||||
desktopOnly: true,
|
||||
priority: 93,
|
||||
requires: 'managed-external',
|
||||
priority: 94,
|
||||
},
|
||||
{
|
||||
id: 'switch-player-vlc',
|
||||
@@ -61,8 +72,8 @@ const PLAYER_COMMAND_DEFS: readonly PlayerCommandDefinition[] = [
|
||||
icon: 'play_circle_outline',
|
||||
nameKey: 'SETTINGS.PLAYER_VLC',
|
||||
keywords: ['player', 'vlc', 'external'],
|
||||
desktopOnly: true,
|
||||
priority: 94,
|
||||
requires: 'managed-external',
|
||||
priority: 95,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -74,12 +85,15 @@ export class WorkspacePlayerCommandsContributor {
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly runtime = inject(RuntimeCapabilitiesService);
|
||||
private readonly embeddedMpvSupported = signal(false);
|
||||
|
||||
constructor() {
|
||||
const unregisters = PLAYER_COMMAND_DEFS.map((def) =>
|
||||
this.viewCommands.registerCommand(this.toContribution(def))
|
||||
);
|
||||
|
||||
void this.loadEmbeddedMpvSupport();
|
||||
|
||||
this.destroyRef.onDestroy(() => {
|
||||
for (const unregister of unregisters) {
|
||||
unregister();
|
||||
@@ -87,6 +101,26 @@ export class WorkspacePlayerCommandsContributor {
|
||||
});
|
||||
}
|
||||
|
||||
private async loadEmbeddedMpvSupport(): Promise<void> {
|
||||
if (
|
||||
!this.runtime.supportsEmbeddedMpv ||
|
||||
typeof window === 'undefined' ||
|
||||
!window.electron?.getEmbeddedMpvSupport
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const support = await window.electron.getEmbeddedMpvSupport();
|
||||
this.embeddedMpvSupported.set(!!support?.supported);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'Failed to verify embedded MPV support for the command palette.',
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private toContribution(
|
||||
def: PlayerCommandDefinition
|
||||
): WorkspaceCommandContribution {
|
||||
@@ -96,7 +130,8 @@ export class WorkspacePlayerCommandsContributor {
|
||||
icon: def.icon,
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.SWITCH_PLAYER_LABEL',
|
||||
labelParams: () => ({ name: this.translate.instant(def.nameKey) }),
|
||||
descriptionKey: 'WORKSPACE.SHELL.COMMANDS.SWITCH_PLAYER_DESCRIPTION',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.SWITCH_PLAYER_DESCRIPTION',
|
||||
descriptionParams: () => ({
|
||||
name: this.translate.instant(def.nameKey),
|
||||
}),
|
||||
@@ -105,13 +140,23 @@ export class WorkspacePlayerCommandsContributor {
|
||||
this.translate.instant(def.nameKey).toLowerCase(),
|
||||
],
|
||||
priority: def.priority,
|
||||
visible: () =>
|
||||
!def.desktopOnly || this.runtime.supportsManagedExternalPlayers,
|
||||
visible: () => this.isVisible(def),
|
||||
enabled: () => this.settingsStore.player() !== def.player,
|
||||
run: () => this.activate(def),
|
||||
};
|
||||
}
|
||||
|
||||
private isVisible(def: PlayerCommandDefinition): boolean {
|
||||
switch (def.requires) {
|
||||
case 'managed-external':
|
||||
return this.runtime.supportsManagedExternalPlayers;
|
||||
case 'embedded-mpv':
|
||||
return this.embeddedMpvSupported();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private activate(def: PlayerCommandDefinition): void {
|
||||
void this.settingsStore.updateSettings({ player: def.player });
|
||||
|
||||
|
||||
Reference in new issue
Block a user