mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-19 05:16:27 -07:00
feat(sidebar): add dialogue selection and copying (#238)
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import type { ElectronAPI, SubtitleSidebarSnapshot } from '../../types';
|
||||
import { SUBTITLE_DEFAULT_CONFIG } from '../../config/definitions/defaults-subtitle';
|
||||
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,
|
||||
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'
|
||||
| 'copySubtitleSidebarSelection'
|
||||
| 'getOverlayLayer'
|
||||
| 'sendMpvCommand'
|
||||
| 'setIgnoreMouseEvents'
|
||||
>,
|
||||
});
|
||||
const ctx = {
|
||||
dom: resolveRendererDom(),
|
||||
state: createRendererState(),
|
||||
platform: resolvePlatformInfo(),
|
||||
};
|
||||
const modal = createSubtitleSidebarModal(ctx, {
|
||||
modalStateReader: { isAnyModalOpen: () => false },
|
||||
});
|
||||
modal.wireDomEvents();
|
||||
wireSubtitleSidebarSelection(ctx);
|
||||
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 {
|
||||
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(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user