fix: delegate multi-line digit selection to visible overlay (#78)

This commit is contained in:
2026-05-24 00:39:23 -07:00
committed by GitHub
parent c02edc90cc
commit da3c971ee6
62 changed files with 1822 additions and 209 deletions
+62 -15
View File
@@ -32,7 +32,7 @@ type YoutubeFlowDeps = {
sendMpvCommand: (command: Array<string | number>) => void;
requestMpvProperty: (name: string) => Promise<unknown>;
refreshCurrentSubtitle: (text: string) => void;
refreshSubtitleSidebarSource?: (sourcePath: string) => Promise<void>;
refreshSubtitleSidebarSource?: (sourcePath: string, mediaPath?: string) => Promise<void>;
startTokenizationWarmups: () => Promise<void>;
waitForTokenizationReady: () => Promise<void>;
waitForAnkiReady: () => Promise<void>;
@@ -42,9 +42,12 @@ type YoutubeFlowDeps = {
focusOverlayWindow: () => void;
showMpvOsd: (text: string) => void;
reportSubtitleFailure: (message: string) => void;
notifyPrimarySubtitleLoaded?: () => void;
warn: (message: string) => void;
log: (message: string) => void;
getYoutubeOutputDir: () => string;
createSubtitleTempDir?: () => Promise<string>;
cleanupSubtitleTempDirs?: (dirs: string[]) => void;
};
type YoutubeFlowSession = {
@@ -349,7 +352,9 @@ async function injectDownloadedSubtitles(
}
let trackListRaw: unknown = await deps.requestMpvProperty('track-list');
let primaryTrackId: number | null = primarySelection.existingTrackId;
let primaryTrackId: number | null = primarySelection.injectedPath
? null
: primarySelection.existingTrackId;
let secondaryTrackId: number | null = secondarySelection?.existingTrackId ?? null;
for (let attempt = 0; attempt < 12; attempt += 1) {
if (attempt > 0 || primarySelection.injectedPath || secondarySelection?.injectedPath) {
@@ -423,6 +428,53 @@ async function injectDownloadedSubtitles(
export function createYoutubeFlowRuntime(deps: YoutubeFlowDeps) {
let activeSession: YoutubeFlowSession | null = null;
const activeSubtitleTempDirs = new Set<string>();
const cleanupSubtitleTempDirs = (): void => {
const dirs = [...activeSubtitleTempDirs];
if (dirs.length === 0) {
return;
}
if (!deps.cleanupSubtitleTempDirs) {
activeSubtitleTempDirs.clear();
return;
}
deps.cleanupSubtitleTempDirs(dirs);
for (const dir of dirs) {
activeSubtitleTempDirs.delete(dir);
}
};
const cleanupSubtitleTempDirsForNextLoad = (): void => {
try {
cleanupSubtitleTempDirs();
} catch (error) {
deps.warn(
`Failed to cleanup YouTube subtitle temp files: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
};
const prepareSubtitleOutputDir = async (fallbackOutputDir: string): Promise<string> => {
if (!deps.createSubtitleTempDir || !deps.cleanupSubtitleTempDirs) {
return fallbackOutputDir;
}
cleanupSubtitleTempDirsForNextLoad();
try {
const tempDir = await deps.createSubtitleTempDir();
activeSubtitleTempDirs.add(tempDir);
return tempDir;
} catch (error) {
deps.warn(
`Failed to create YouTube subtitle temp dir; using configured output dir: ${
error instanceof Error ? error.message : String(error)
}`,
);
return fallbackOutputDir;
}
};
const acquireSelectedTracks = async (input: {
targetUrl: string;
@@ -567,6 +619,7 @@ export function createYoutubeFlowRuntime(deps: YoutubeFlowDeps) {
osdProgress.setMessage('Downloading subtitles...');
}
try {
const outputDir = await prepareSubtitleOutputDir(input.outputDir);
let initialTrackListRaw: unknown = null;
let existingPrimaryTrackId: number | null = null;
let existingSecondaryTrackId: number | null = null;
@@ -602,19 +655,11 @@ export function createYoutubeFlowRuntime(deps: YoutubeFlowDeps) {
let primaryInjectedPath: string | null = null;
let secondaryInjectedPath: string | null = null;
if (existingPrimaryTrackId !== null) {
primarySidebarPath = (
await deps.acquireYoutubeSubtitleTrack({
targetUrl: input.url,
outputDir: input.outputDir,
track: input.primaryTrack,
})
).path;
} else if (existingSecondaryTrackId !== null || !input.secondaryTrack) {
if (existingSecondaryTrackId !== null || !input.secondaryTrack) {
primaryInjectedPath = (
await deps.acquireYoutubeSubtitleTrack({
targetUrl: input.url,
outputDir: input.outputDir,
outputDir,
track: input.primaryTrack,
})
).path;
@@ -622,7 +667,7 @@ export function createYoutubeFlowRuntime(deps: YoutubeFlowDeps) {
} else {
const acquired = await acquireSelectedTracks({
targetUrl: input.url,
outputDir: input.outputDir,
outputDir,
primaryTrack: input.primaryTrack,
secondaryTrack: existingSecondaryTrackId === null ? input.secondaryTrack : null,
secondaryFailureLabel: input.secondaryFailureLabel,
@@ -641,7 +686,7 @@ export function createYoutubeFlowRuntime(deps: YoutubeFlowDeps) {
secondaryInjectedPath = (
await deps.acquireYoutubeSubtitleTrack({
targetUrl: input.url,
outputDir: input.outputDir,
outputDir,
track: input.secondaryTrack,
})
).path;
@@ -685,8 +730,9 @@ export function createYoutubeFlowRuntime(deps: YoutubeFlowDeps) {
if (!refreshedActiveSubtitle) {
return false;
}
deps.notifyPrimarySubtitleLoaded?.();
try {
await deps.refreshSubtitleSidebarSource?.(primarySidebarPath);
await deps.refreshSubtitleSidebarSource?.(primarySidebarPath, input.url);
} catch (error) {
deps.warn(
`Failed to refresh parsed subtitle cues for sidebar: ${
@@ -877,5 +923,6 @@ export function createYoutubeFlowRuntime(deps: YoutubeFlowDeps) {
resolveActivePicker,
cancelActivePicker,
hasActiveSession: () => Boolean(activeSession),
cleanupSubtitleTempDirs,
};
}