mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-11-10 04:46:07 -08:00
118 lines
4.8 KiB
TypeScript
118 lines
4.8 KiB
TypeScript
import type { IpcMain, WebContents } from 'electron';
|
|
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
|
|
import {
|
|
parseSubtitleSelectionRequest,
|
|
type SubtitleSelectionState,
|
|
} from '../../shared/subtitle-selection';
|
|
import { openOverlayHostedModal, retryOverlayModalOpen } from './overlay-hosted-modal-open';
|
|
|
|
interface SelectionMpvClient {
|
|
connected: boolean;
|
|
requestProperty: (name: string) => Promise<unknown>;
|
|
request: (command: unknown[]) => Promise<{ error?: string }>;
|
|
}
|
|
|
|
export function openSubtitleSelectionModal(
|
|
deps: Parameters<typeof openOverlayHostedModal>[0] & Parameters<typeof retryOverlayModalOpen>[0],
|
|
): Promise<boolean> {
|
|
return retryOverlayModalOpen(deps, {
|
|
modal: 'subtitle-selection',
|
|
timeoutMs: 1500,
|
|
retryWarning: 'Subtitle selection modal did not acknowledge opening; retrying.',
|
|
sendOpen: () =>
|
|
openOverlayHostedModal(deps, {
|
|
channel: IPC_CHANNELS.event.subtitleSelectionOpen,
|
|
modal: 'subtitle-selection',
|
|
preferModalWindow: true,
|
|
}),
|
|
});
|
|
}
|
|
|
|
export function createSubtitleSelectionRuntime(deps: {
|
|
isEnabled: () => boolean;
|
|
getMpvClient: () => SelectionMpvClient | null;
|
|
}) {
|
|
function getClient(): SelectionMpvClient {
|
|
if (!deps.isEnabled()) throw new Error('Enable subtitle selection in Settings first.');
|
|
const client = deps.getMpvClient();
|
|
if (!client?.connected) throw new Error('Connect to mpv first.');
|
|
return client;
|
|
}
|
|
|
|
async function readState(client: SelectionMpvClient): Promise<SubtitleSelectionState> {
|
|
const mediaPath = await client.requestProperty('path');
|
|
if (typeof mediaPath !== 'string' || !mediaPath) throw new Error('Open a video first.');
|
|
const [rawTracks, primary, secondary] = await Promise.all([
|
|
client.requestProperty('track-list'),
|
|
client.requestProperty('sid'),
|
|
client.requestProperty('secondary-sid'),
|
|
]);
|
|
const tracks: SubtitleSelectionState['tracks'] = [];
|
|
const candidates: unknown[] = Array.isArray(rawTracks) ? rawTracks : [];
|
|
for (const track of candidates) {
|
|
if (
|
|
typeof track !== 'object' ||
|
|
track === null ||
|
|
!('type' in track) ||
|
|
track.type !== 'sub' ||
|
|
!('id' in track) ||
|
|
typeof track.id !== 'number' ||
|
|
!Number.isSafeInteger(track.id) ||
|
|
track.id <= 0
|
|
)
|
|
continue;
|
|
const details = [
|
|
'title' in track ? track.title : undefined,
|
|
'lang' in track ? track.lang : undefined,
|
|
'codec' in track ? track.codec : undefined,
|
|
].filter((value): value is string => typeof value === 'string' && value.length > 0);
|
|
if ('external' in track && track.external === true) details.push('external');
|
|
tracks.push({ id: track.id, label: `#${track.id} · ${details.join(' · ') || 'Subtitle'}` });
|
|
}
|
|
if ((await client.requestProperty('path')) !== mediaPath)
|
|
throw new Error('The video changed. Reopen subtitle selection.');
|
|
const selected = (value: unknown): number | null =>
|
|
tracks.find((track) => track.id === value)?.id ?? null;
|
|
return { mediaPath, tracks, primary: selected(primary), secondary: selected(secondary) };
|
|
}
|
|
|
|
async function apply(value: unknown): Promise<void> {
|
|
const selection = parseSubtitleSelectionRequest(value);
|
|
const client = getClient();
|
|
const current = await readState(client);
|
|
if (current.mediaPath !== selection.mediaPath)
|
|
throw new Error('The video changed. Reopen subtitle selection.');
|
|
for (const id of [selection.primary, selection.secondary]) {
|
|
if (id !== null && !current.tracks.some((track) => track.id === id))
|
|
throw new Error('A selected track is no longer available. Reopen subtitle selection.');
|
|
}
|
|
const set = async (property: string, id: number | null): Promise<void> => {
|
|
const response = await client.request(['set_property', property, id ?? 'no']);
|
|
if (response.error && response.error !== 'success') throw new Error(response.error);
|
|
};
|
|
// Clear secondary first so swapping the two tracks works in mpv.
|
|
await set('secondary-sid', null);
|
|
await set('sid', selection.primary);
|
|
await set('secondary-sid', selection.secondary);
|
|
}
|
|
|
|
return { getState: async () => readState(getClient()), apply };
|
|
}
|
|
|
|
export function registerSubtitleSelectionIpc(deps: {
|
|
ipc: Pick<IpcMain, 'handle'>;
|
|
isAllowedSender: (sender: WebContents) => boolean;
|
|
runtime: ReturnType<typeof createSubtitleSelectionRuntime>;
|
|
}): void {
|
|
deps.ipc.handle(IPC_CHANNELS.request.getSubtitleSelection, (event) => {
|
|
if (!deps.isAllowedSender(event.sender))
|
|
throw new Error('Subtitle selection requires the overlay.');
|
|
return deps.runtime.getState();
|
|
});
|
|
deps.ipc.handle(IPC_CHANNELS.request.applySubtitleSelection, (event, value: unknown) => {
|
|
if (!deps.isAllowedSender(event.sender))
|
|
throw new Error('Subtitle selection requires the overlay.');
|
|
return deps.runtime.apply(value);
|
|
});
|
|
}
|