mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-30 17:40:53 -07:00
Merge remote-tracking branch 'origin/main' into add-hachidori-backend
This commit is contained in:
@@ -260,7 +260,7 @@ export function createKeyboardHandlers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (binding.actionType === 'mpv-command') {
|
if (binding.actionType === 'mpv-command') {
|
||||||
dispatchConfiguredMpvCommand(binding.command, {
|
void dispatchConfiguredMpvCommand(binding.command, {
|
||||||
getPlaybackPaused: options.getPlaybackPaused,
|
getPlaybackPaused: options.getPlaybackPaused,
|
||||||
sendMpvCommand: (command) => window.electronAPI.sendMpvCommand(command),
|
sendMpvCommand: (command) => window.electronAPI.sendMpvCommand(command),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -143,11 +143,13 @@ function withFakeDom(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type HarnessOptions = { failActions?: boolean };
|
type HarnessOptions = { failActions?: boolean; bindings?: CompiledSessionBinding[] };
|
||||||
|
|
||||||
function createHarness(options: HarnessOptions) {
|
function createHarness(options: HarnessOptions) {
|
||||||
const mpvCommands: (string | number)[][] = [];
|
const mpvCommands: (string | number)[][] = [];
|
||||||
const sessionActions: string[] = [];
|
const sessionActions: string[] = [];
|
||||||
|
/** IPC in send order; main destroys the modal window on `closed`, dropping anything after it. */
|
||||||
|
const ipcLog: string[] = [];
|
||||||
const define = (key: string, value: unknown) =>
|
const define = (key: string, value: unknown) =>
|
||||||
Object.defineProperty(globalThis, key, { configurable: true, writable: true, value });
|
Object.defineProperty(globalThis, key, { configurable: true, writable: true, value });
|
||||||
|
|
||||||
@@ -163,15 +165,20 @@ function createHarness(options: HarnessOptions) {
|
|||||||
electronAPI: {
|
electronAPI: {
|
||||||
focusMainWindow: async () => {},
|
focusMainWindow: async () => {},
|
||||||
setIgnoreMouseEvents: () => {},
|
setIgnoreMouseEvents: () => {},
|
||||||
notifyOverlayModalClosed: () => {},
|
notifyOverlayModalClosed: () => ipcLog.push('closed'),
|
||||||
getSessionBindings: async () => SESSION_BINDINGS,
|
getSessionBindings: async () => options.bindings ?? SESSION_BINDINGS,
|
||||||
getSubtitleStyle: async () => ({}),
|
getSubtitleStyle: async () => ({}),
|
||||||
getMarkWatchedKey: async () => null,
|
getMarkWatchedKey: async () => null,
|
||||||
getSubtitleSidebarSnapshot: async () => ({ config: { toggleKey: null } }),
|
getSubtitleSidebarSnapshot: async () => ({ config: { toggleKey: null } }),
|
||||||
getRuntimeOptions: async () => [],
|
getRuntimeOptions: async () => [],
|
||||||
sendMpvCommand: (command: (string | number)[]) => mpvCommands.push(command),
|
getPlaybackPaused: async () => true,
|
||||||
|
sendMpvCommand: (command: (string | number)[]) => {
|
||||||
|
mpvCommands.push(command);
|
||||||
|
ipcLog.push(`mpv:${command.join(' ')}`);
|
||||||
|
},
|
||||||
dispatchSessionAction: async (actionId: string) => {
|
dispatchSessionAction: async (actionId: string) => {
|
||||||
sessionActions.push(actionId);
|
sessionActions.push(actionId);
|
||||||
|
ipcLog.push(`action:${actionId}`);
|
||||||
if (options.failActions) throw new Error('boom');
|
if (options.failActions) throw new Error('boom');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -239,7 +246,7 @@ function createHarness(options: HarnessOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rows = () => dom.sessionHelpContent.querySelectorAll('.session-help-item');
|
const rows = () => dom.sessionHelpContent.querySelectorAll('.session-help-item');
|
||||||
return { dom, state, open, pressEnter, rows, mpvCommands, sessionActions };
|
return { dom, state, open, pressEnter, rows, mpvCommands, sessionActions, ipcLog };
|
||||||
}
|
}
|
||||||
|
|
||||||
test('session help rows carry runnable commands except numeric and self-opening actions', () => {
|
test('session help rows carry runnable commands except numeric and self-opening actions', () => {
|
||||||
@@ -294,11 +301,39 @@ test(
|
|||||||
|
|
||||||
harness.pressEnter();
|
harness.pressEnter();
|
||||||
|
|
||||||
assert.deepEqual(harness.sessionActions, ['toggleStatsOverlay']);
|
assert.deepEqual(harness.ipcLog, ['action:toggleStatsOverlay', 'closed']);
|
||||||
assert.equal(harness.state.sessionHelpModalOpen, false);
|
assert.equal(harness.state.sessionHelpModalOpen, false);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'session help sends a paused subtitle seek before closing',
|
||||||
|
withFakeDom(
|
||||||
|
async (harness) => {
|
||||||
|
await harness.open(true);
|
||||||
|
// Playback rows live on another tab; a filter searches every tab.
|
||||||
|
harness.dom.sessionHelpFilter.value = 'next subtitle';
|
||||||
|
harness.dom.sessionHelpFilter.dispatch('input', {});
|
||||||
|
|
||||||
|
harness.dom.sessionHelpContent.dispatch('dblclick', { target: harness.rows()[0] });
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
assert.deepEqual(harness.ipcLog, ['mpv:sub-seek 1', 'mpv:set_property pause yes', 'closed']);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bindings: [
|
||||||
|
{
|
||||||
|
sourcePath: 'keybindings[0].key',
|
||||||
|
originalKey: 'Shift+KeyL',
|
||||||
|
key: { code: 'KeyL', modifiers: ['shift'] },
|
||||||
|
actionType: 'mpv-command',
|
||||||
|
command: ['sub-seek', 1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'session help ignores Enter when no video is playing',
|
'session help ignores Enter when no video is playing',
|
||||||
withFakeDom(async (harness) => {
|
withFakeDom(async (harness) => {
|
||||||
|
|||||||
@@ -309,26 +309,27 @@ export function createSessionHelpModal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes help first so a command that opens another modal (or needs mpv focus)
|
* Sends the command before closing help. On Linux and Windows, main destroys the
|
||||||
* lands after help has released the overlay.
|
* dedicated modal window once its last modal closes, so IPC sent after the close
|
||||||
|
* is dropped. Main still handles the command first, so a command that opens another
|
||||||
|
* modal keeps the modal window alive.
|
||||||
*/
|
*/
|
||||||
function runSelectedCommand(): void {
|
function runSelectedCommand(): void {
|
||||||
if (!commandsEnabled) return;
|
if (!commandsEnabled) return;
|
||||||
const command = visibleRows[ctx.state.sessionHelpSelectedIndex]?.command;
|
const command = visibleRows[ctx.state.sessionHelpSelectedIndex]?.command;
|
||||||
if (!command) return;
|
if (!command) return;
|
||||||
|
|
||||||
closeSessionHelpModal();
|
|
||||||
if (command.actionType === 'mpv-command') {
|
if (command.actionType === 'mpv-command') {
|
||||||
dispatchConfiguredMpvCommand(command.command, {
|
void dispatchConfiguredMpvCommand(command.command, {
|
||||||
getPlaybackPaused: () => window.electronAPI.getPlaybackPaused(),
|
getPlaybackPaused: () => window.electronAPI.getPlaybackPaused(),
|
||||||
sendMpvCommand: (mpvCommand) => window.electronAPI.sendMpvCommand(mpvCommand),
|
sendMpvCommand: (mpvCommand) => window.electronAPI.sendMpvCommand(mpvCommand),
|
||||||
});
|
}).finally(closeSessionHelpModal);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Help is already closed, so surface failures on the mpv OSD.
|
const dispatched = window.electronAPI.dispatchSessionAction(command.actionId, command.payload);
|
||||||
void window.electronAPI
|
closeSessionHelpModal();
|
||||||
.dispatchSessionAction(command.actionId, command.payload)
|
// Help is closed by the time this settles, so surface failures on the mpv OSD.
|
||||||
.catch((error: unknown) => {
|
void dispatched.catch((error: unknown) => {
|
||||||
console.error(`Session help could not run ${command.actionId}`, error);
|
console.error(`Session help could not run ${command.actionId}`, error);
|
||||||
window.electronAPI.sendMpvCommand(['show-text', 'Command failed to run', '3000']);
|
window.electronAPI.sendMpvCommand(['show-text', 'Command failed to run', '3000']);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ function isSubtitleSeekCommand(command: MpvCommand): command is [string, number]
|
|||||||
/**
|
/**
|
||||||
* Sends a configured mpv command. Subtitle seeks re-pause afterwards unless playback
|
* Sends a configured mpv command. Subtitle seeks re-pause afterwards unless playback
|
||||||
* is known to be running, so stepping lines from a paused video stays paused.
|
* is known to be running, so stepping lines from a paused video stays paused.
|
||||||
|
* Resolves once every command has been sent; it never rejects.
|
||||||
*/
|
*/
|
||||||
export function dispatchConfiguredMpvCommand(
|
export function dispatchConfiguredMpvCommand(
|
||||||
command: MpvCommand,
|
command: MpvCommand,
|
||||||
@@ -14,14 +15,14 @@ export function dispatchConfiguredMpvCommand(
|
|||||||
getPlaybackPaused: () => Promise<boolean | null>;
|
getPlaybackPaused: () => Promise<boolean | null>;
|
||||||
sendMpvCommand: (command: MpvCommand) => void;
|
sendMpvCommand: (command: MpvCommand) => void;
|
||||||
},
|
},
|
||||||
): void {
|
): Promise<void> {
|
||||||
if (!isSubtitleSeekCommand(command)) {
|
if (!isSubtitleSeekCommand(command)) {
|
||||||
deps.sendMpvCommand(command);
|
deps.sendMpvCommand(command);
|
||||||
return;
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The fallback only covers a failed pause lookup, so a failed re-pause never resends the seek.
|
// The fallback only covers a failed pause lookup, so a failed re-pause never resends the seek.
|
||||||
void deps
|
return deps
|
||||||
.getPlaybackPaused()
|
.getPlaybackPaused()
|
||||||
.then(
|
.then(
|
||||||
(paused) => {
|
(paused) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user