mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -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
@@ -184,11 +184,16 @@ Command palette behavior is shell-owned but view-extensible:
|
||||
visibility — a navigation command like `Open sources` is invisible while
|
||||
the user is on `/workspace/sources` but the id stays in storage so it
|
||||
reappears in the recent section after navigating away.
|
||||
7. Five "Switch player to X" commands are registered globally by
|
||||
`WorkspacePlayerCommandsContributor`. The MPV/VLC entries are visible only
|
||||
in Electron, and the entry matching the current `SettingsStore.player()`
|
||||
value is disabled. The new player setting applies to the next playback
|
||||
session; an existing stream is not re-mounted.
|
||||
7. Six "Switch player to X" commands are registered globally by
|
||||
`WorkspacePlayerCommandsContributor` (VideoJS, HTML5, ArtPlayer, Embedded
|
||||
MPV, MPV, VLC). Each command carries a `requires` flag gating its
|
||||
visibility: the MPV/VLC ("managed-external") entries are visible only when
|
||||
`RuntimeCapabilitiesService.supportsManagedExternalPlayers` is true, and the
|
||||
Embedded MPV ("embedded-mpv") entry is visible only after an async
|
||||
`window.electron.getEmbeddedMpvSupport()` check resolves to `supported`
|
||||
(mirroring the Settings dropdown gate). The entry matching the current
|
||||
`SettingsStore.player()` value is disabled. The new player setting applies
|
||||
to the next playback session; an existing stream is not re-mounted.
|
||||
|
||||
Keyboard shortcut help is shell-owned:
|
||||
|
||||
|
||||
+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