Files
SubMiner/src/renderer/modals/subtitle-sidebar-selection.electron-fixture.ts
T

166 lines
6.2 KiB
TypeScript

import type { ElectronAPI, SubtitleSidebarSnapshot } from '../../types';
import { SUBTITLE_DEFAULT_CONFIG } from '../../config/definitions/defaults-subtitle';
import { CORE_DEFAULT_CONFIG } from '../../config/definitions/defaults-core';
import { createKeyboardHandlers } from '../handlers/keyboard';
import { createRendererState } from '../state';
import { resolveRendererDom } from '../utils/dom';
import { resolvePlatformInfo } from '../utils/platform';
import { createSubtitleSidebarModal } from './subtitle-sidebar';
import {
getSubtitleSidebarSelection,
wireSubtitleSidebarSelection,
} from './subtitle-sidebar-selection';
export async function setup() {
const commands: unknown[] = [];
const snapshot: SubtitleSidebarSnapshot = {
sourceKey: 'episode-1:track-1',
cues: [
{ text: '最初の台詞', startTime: 0, endTime: 1 },
{ text: '同じ台詞\n二行目', startTime: 1, endTime: 2 },
{ text: '同じ台詞', startTime: 2, endTime: 3 },
...Array.from({ length: 30 }, (_, i) => ({
text: `後の台詞${i}`,
startTime: i + 3,
endTime: i + 4,
})),
],
currentSubtitle: { text: '最初の台詞', startTime: 0, endTime: 1 },
currentTimeSec: 0,
config: {
...SUBTITLE_DEFAULT_CONFIG.subtitleSidebar,
enabled: true,
layout: 'overlay',
pauseVideoOnHover: false,
autoScroll: true,
css: {},
},
};
Object.defineProperty(window, 'electronAPI', {
value: {
getSubtitleSidebarSnapshot: async () => snapshot,
getSessionBindings: async () => [
{
sourcePath: 'keybindings[0].key',
originalKey: 'Space',
key: { code: 'Space', modifiers: [] },
actionType: 'mpv-command',
command: ['cycle', 'pause'],
},
],
getConfiguredShortcuts: async () => CORE_DEFAULT_CONFIG.shortcuts,
getStatsToggleKey: async () => 'Backquote',
getMarkWatchedKey: async () => '',
copySubtitleSidebarSelection: async (text) => {
if (!('copyTestSelection' in window) || typeof window.copyTestSelection !== 'function')
throw new Error('Missing test clipboard bridge');
window.copyTestSelection(text);
},
getOverlayLayer: () => 'visible',
sendMpvCommand: (command) => {
commands.push(command);
},
setIgnoreMouseEvents: () => {},
} satisfies Pick<
ElectronAPI,
| 'getSubtitleSidebarSnapshot'
| 'getSessionBindings'
| 'getConfiguredShortcuts'
| 'getStatsToggleKey'
| 'getMarkWatchedKey'
| 'copySubtitleSidebarSelection'
| 'getOverlayLayer'
| 'sendMpvCommand'
| 'setIgnoreMouseEvents'
>,
});
const ctx = {
dom: resolveRendererDom(),
state: createRendererState(),
platform: resolvePlatformInfo(),
};
const modal = createSubtitleSidebarModal(ctx, {
modalStateReader: { isAnyModalOpen: () => false },
});
modal.wireDomEvents();
wireSubtitleSidebarSelection(ctx);
const keyboard = createKeyboardHandlers(ctx, {
handleRuntimeOptionsKeydown: () => false,
handleCharacterDictionaryKeydown: () => false,
handleSubsyncKeydown: () => false,
handleKikuKeydown: () => false,
handleJimakuKeydown: () => false,
handleTsukihimeKeydown: () => false,
handleYoutubePickerKeydown: () => false,
handleMediaTimingReviewKeydown: () => false,
handlePlaylistBrowserKeydown: () => false,
handleControllerSelectKeydown: () => false,
handleControllerDebugKeydown: () => false,
handleSessionHelpKeydown: () => false,
handleChangelogKeydown: () => false,
openSessionHelpModal: () => {},
getPlaybackPaused: async () => false,
});
await keyboard.setupMpvInputForwarding();
await modal.openSubtitleSidebarModal();
const list = ctx.dom.subtitleSidebarList;
list.style.height = '180px';
list.style.overflowY = 'auto';
const selection = window.getSelection();
if (!selection) throw new Error('Native selection unavailable');
const textNode = (index: number) => {
const node = list.children[index]?.querySelector('.subtitle-sidebar-text')?.firstChild;
if (!node) throw new Error(`Missing cue ${index}`);
return node;
};
const select = (backward = false) => {
const start = textNode(0);
const end = textNode(2);
selection.setBaseAndExtent(backward ? end : start, 2, backward ? start : end, 2);
document.dispatchEvent(new Event('selectionchange'));
return getSubtitleSidebarSelection(list);
};
// This is the same competing action as the renderer's current-subtitle shortcut.
let fallbackCopies = 0;
document.addEventListener('keydown', (event) => {
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'c') fallbackCopies += 1;
});
return {
takeCommands: () => commands.splice(0),
cueFocused: () => document.activeElement?.matches('.subtitle-sidebar-item') ?? false,
focusCue: () => list.querySelector<HTMLElement>('.subtitle-sidebar-item')?.focus(),
select,
selected: () => getSubtitleSidebarSelection(list),
buttonVisible: () => !ctx.dom.subtitleSidebarCopy.hidden,
fallbackCopies: () => fallbackCopies,
dragPoints: () =>
[0, 2].map((index) => {
const range = document.createRange();
range.setStart(textNode(index), 2);
range.collapse(true);
const rect = range.getBoundingClientRect();
return { x: Math.round(rect.x), y: Math.round(rect.y + rect.height / 2) };
}),
clickCopy: () => ctx.dom.subtitleSidebarCopy.click(),
clickCue: () => {
const before = commands.length;
list.children[0]?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
return commands.length - before;
},
updatePlayback: async () => {
list.scrollTop = 0;
snapshot.currentTimeSec = 25;
snapshot.currentSubtitle = { text: '後の台詞22', startTime: 25, endTime: 26 };
await modal.refreshSubtitleSidebarSnapshot();
return list.scrollTop;
},
changeSource: async () => {
snapshot.sourceKey = 'episode-2:track-1';
await modal.refreshSubtitleSidebarSnapshot();
return getSubtitleSidebarSelection(list);
},
clear: () => selection.removeAllRanges(),
close: () => modal.closeSubtitleSidebarModal(),
};
}