feat(sidebar): add dialogue selection and copying

- Support multi-row selection with clean clipboard text
- Preserve selections across playback updates and clear them on source changes
This commit is contained in:
2026-09-06 22:36:04 -07:00
parent c14c690875
commit 97b8ecb8a3
24 changed files with 621 additions and 3 deletions
+16
View File
@@ -7,6 +7,10 @@ import type {
import { subtitleCueListSeekTime } from '../../core/services/subtitle-cue-navigation.js';
import type { ModalStateReader, RendererContext } from '../context';
import { syncOverlayMouseIgnoreState } from '../overlay-mouse-ignore.js';
import {
clearSubtitleSidebarSelection,
hasSubtitleSidebarSelection,
} from './subtitle-sidebar-selection.js';
import {
YOMITAN_POPUP_HIDDEN_EVENT,
YOMITAN_POPUP_SHOWN_EVENT,
@@ -209,6 +213,7 @@ export function createSubtitleSidebarModal(
let subtitleSidebarYomitanPopupVisible = false;
let subtitleSidebarPauseHeldByYomitanPopup = false;
let lastSubtitleSidebarLookupCueIndex = -1;
let subtitleSourceKey: string | null = null;
function restoreEmbeddedSidebarPassthrough(): void {
syncOverlayMouseIgnoreState(ctx);
@@ -469,6 +474,8 @@ export function createSubtitleSidebarModal(
): void {
if (
!ctx.state.subtitleSidebarAutoScroll ||
ctx.dom.subtitleSidebarList.dataset?.selecting === 'true' ||
hasSubtitleSidebarSelection(ctx.dom.subtitleSidebarList) ||
ctx.state.subtitleSidebarActiveCueIndex < 0 ||
(!force && ctx.state.subtitleSidebarActiveCueIndex === previousActiveCueIndex) ||
nowForUiTiming() < ctx.state.subtitleSidebarManualScrollUntilMs
@@ -565,8 +572,14 @@ export function createSubtitleSidebarModal(
async function refreshSnapshot(): Promise<SubtitleSidebarSnapshot> {
const snapshot = await window.electronAPI.getSubtitleSidebarSnapshot();
if (snapshot.sourceKey !== subtitleSourceKey) {
clearSubtitleSidebarSelection(ctx.dom.subtitleSidebarList);
lastSubtitleSidebarLookupCueIndex = -1;
subtitleSourceKey = snapshot.sourceKey;
}
applyConfig(snapshot);
if (!snapshot.config.enabled) {
clearSubtitleSidebarSelection(ctx.dom.subtitleSidebarList);
resumeSubtitleSidebarHoverPause();
clearSidebarInteractionState();
ctx.state.subtitleSidebarCues = [];
@@ -586,6 +599,7 @@ export function createSubtitleSidebarModal(
const cuesChanged = !subtitleCueListsEqual(ctx.state.subtitleSidebarCues, snapshot.cues);
if (cuesChanged) {
clearSubtitleSidebarSelection(ctx.dom.subtitleSidebarList);
ctx.state.subtitleSidebarCues = snapshot.cues;
if (ctx.state.subtitleSidebarModalOpen) {
renderCueList();
@@ -670,6 +684,7 @@ export function createSubtitleSidebarModal(
if (!ctx.state.subtitleSidebarModalOpen) {
return;
}
clearSubtitleSidebarSelection(ctx.dom.subtitleSidebarList);
resumeSubtitleSidebarHoverPause();
clearSidebarInteractionState();
ctx.state.subtitleSidebarModalOpen = false;
@@ -710,6 +725,7 @@ export function createSubtitleSidebarModal(
closeSubtitleSidebarModal();
});
ctx.dom.subtitleSidebarList.addEventListener('click', (event) => {
if (hasSubtitleSidebarSelection(ctx.dom.subtitleSidebarList)) return;
const target = event.target;
if (!(target instanceof Element)) {
return;