mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-26 12:11:26 -07:00
* fix: harden preload argv parsing for popup windows * fix: align youtube playback with shared overlay startup * fix: unwrap mpv youtube streams for anki media mining * docs: update docs for youtube subtitle and mining flow * refactor: unify cli and runtime wiring for startup and youtube flow * feat: update subtitle sidebar overlay behavior * chore: add shared log-file source for diagnostics * fix(ci): add changelog fragment for immersion changes * fix: address CodeRabbit review feedback * fix: persist canonical title from youtube metadata * style: format stats library tab * fix: address latest review feedback * style: format stats library files * test: stub launcher youtube deps in CI * test: isolate launcher youtube flow deps * test: stub launcher youtube deps in failing case * test: force x11 backend in launcher ci harness * test: address latest review feedback * fix(launcher): preserve user YouTube ytdl raw options * docs(backlog): update task tracking notes * fix(immersion): special-case youtube media paths in runtime and tracking * feat(stats): improve YouTube media metadata and picker key handling * fix(ci): format stats media library hook * fix: address latest CodeRabbit review items * docs: update youtube release notes and docs * feat: auto-load youtube subtitles before manual picker * fix: restore app-owned youtube subtitle flow * docs: update youtube playback docs and config copy * refactor: remove legacy youtube launcher mode plumbing * fix: refine youtube subtitle startup binding * docs: clarify youtube subtitle startup behavior * fix: address PR #31 latest review follow-ups * fix: address PR #31 follow-up review comments * test: harden youtube picker test harness * udpate backlog * fix: add timeout to youtube metadata probe * docs: refresh youtube and stats docs * update backlog * update backlog * chore: release v0.9.0
177 lines
6.1 KiB
TypeScript
177 lines
6.1 KiB
TypeScript
import type { BrowserWindow } from 'electron';
|
|
import { BaseWindowTracker, createWindowTracker } from '../../window-trackers';
|
|
import { mergeAiConfig } from '../../ai/config';
|
|
import {
|
|
AiConfig,
|
|
AnkiConnectConfig,
|
|
KikuFieldGroupingChoice,
|
|
KikuFieldGroupingRequestData,
|
|
WindowGeometry,
|
|
} from '../../types';
|
|
|
|
type AnkiIntegrationLike = {
|
|
start: () => void;
|
|
};
|
|
|
|
type CreateAnkiIntegrationArgs = {
|
|
config: AnkiConnectConfig;
|
|
aiConfig: AiConfig;
|
|
subtitleTimingTracker: unknown;
|
|
mpvClient: { send?: (payload: { command: string[] }) => void };
|
|
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
|
createFieldGroupingCallback: () => (
|
|
data: KikuFieldGroupingRequestData,
|
|
) => Promise<KikuFieldGroupingChoice>;
|
|
knownWordCacheStatePath: string;
|
|
};
|
|
|
|
function createDefaultAnkiIntegration(args: CreateAnkiIntegrationArgs): AnkiIntegrationLike {
|
|
const { AnkiIntegration } =
|
|
require('../../anki-integration') as typeof import('../../anki-integration');
|
|
return new AnkiIntegration(
|
|
args.config,
|
|
args.subtitleTimingTracker as never,
|
|
args.mpvClient as never,
|
|
(text: string) => {
|
|
if (args.mpvClient && typeof args.mpvClient.send === 'function') {
|
|
args.mpvClient.send({
|
|
command: ['show-text', text, '3000'],
|
|
});
|
|
}
|
|
},
|
|
args.showDesktopNotification,
|
|
args.createFieldGroupingCallback(),
|
|
args.knownWordCacheStatePath,
|
|
args.aiConfig,
|
|
);
|
|
}
|
|
|
|
export function initializeOverlayRuntime(options: {
|
|
getMpvSocketPath: () => string;
|
|
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig; ai?: AiConfig };
|
|
getSubtitleTimingTracker: () => unknown | null;
|
|
getMpvClient: () => {
|
|
send?: (payload: { command: string[] }) => void;
|
|
} | null;
|
|
getRuntimeOptionsManager: () => {
|
|
getEffectiveAnkiConnectConfig: (config?: AnkiConnectConfig) => AnkiConnectConfig;
|
|
} | null;
|
|
getAnkiIntegration?: () => unknown | null;
|
|
setAnkiIntegration: (integration: unknown | null) => void;
|
|
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
|
createFieldGroupingCallback: () => (
|
|
data: KikuFieldGroupingRequestData,
|
|
) => Promise<KikuFieldGroupingChoice>;
|
|
getKnownWordCacheStatePath: () => string;
|
|
shouldStartAnkiIntegration?: () => boolean;
|
|
createAnkiIntegration?: (args: CreateAnkiIntegrationArgs) => AnkiIntegrationLike;
|
|
backendOverride: string | null;
|
|
createMainWindow: () => void;
|
|
registerGlobalShortcuts: () => void;
|
|
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
|
isVisibleOverlayVisible: () => boolean;
|
|
updateVisibleOverlayVisibility: () => void;
|
|
getOverlayWindows: () => BrowserWindow[];
|
|
syncOverlayShortcuts: () => void;
|
|
setWindowTracker: (tracker: BaseWindowTracker | null) => void;
|
|
createWindowTracker?: (
|
|
override?: string | null,
|
|
targetMpvSocketPath?: string | null,
|
|
) => BaseWindowTracker | null;
|
|
}): void {
|
|
options.createMainWindow();
|
|
options.registerGlobalShortcuts();
|
|
|
|
const createWindowTrackerHandler = options.createWindowTracker ?? createWindowTracker;
|
|
const windowTracker = createWindowTrackerHandler(
|
|
options.backendOverride,
|
|
options.getMpvSocketPath(),
|
|
);
|
|
options.setWindowTracker(windowTracker);
|
|
if (windowTracker) {
|
|
windowTracker.onGeometryChange = (geometry: WindowGeometry) => {
|
|
options.updateVisibleOverlayBounds(geometry);
|
|
};
|
|
windowTracker.onWindowFound = (geometry: WindowGeometry) => {
|
|
options.updateVisibleOverlayBounds(geometry);
|
|
if (options.isVisibleOverlayVisible()) {
|
|
options.updateVisibleOverlayVisibility();
|
|
}
|
|
};
|
|
windowTracker.onWindowLost = () => {
|
|
for (const window of options.getOverlayWindows()) {
|
|
window.hide();
|
|
}
|
|
options.syncOverlayShortcuts();
|
|
};
|
|
windowTracker.onWindowFocusChange = () => {
|
|
if (options.isVisibleOverlayVisible()) {
|
|
options.updateVisibleOverlayVisibility();
|
|
}
|
|
options.syncOverlayShortcuts();
|
|
};
|
|
windowTracker.start();
|
|
}
|
|
|
|
initializeOverlayAnkiIntegration(options);
|
|
|
|
options.updateVisibleOverlayVisibility();
|
|
}
|
|
|
|
export function initializeOverlayAnkiIntegration(options: {
|
|
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig; ai?: AiConfig };
|
|
getSubtitleTimingTracker: () => unknown | null;
|
|
getMpvClient: () => {
|
|
send?: (payload: { command: string[] }) => void;
|
|
} | null;
|
|
getRuntimeOptionsManager: () => {
|
|
getEffectiveAnkiConnectConfig: (config?: AnkiConnectConfig) => AnkiConnectConfig;
|
|
} | null;
|
|
getAnkiIntegration?: () => unknown | null;
|
|
setAnkiIntegration: (integration: unknown | null) => void;
|
|
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
|
createFieldGroupingCallback: () => (
|
|
data: KikuFieldGroupingRequestData,
|
|
) => Promise<KikuFieldGroupingChoice>;
|
|
getKnownWordCacheStatePath: () => string;
|
|
shouldStartAnkiIntegration?: () => boolean;
|
|
createAnkiIntegration?: (args: CreateAnkiIntegrationArgs) => AnkiIntegrationLike;
|
|
}): boolean {
|
|
if (options.getAnkiIntegration?.()) {
|
|
return false;
|
|
}
|
|
|
|
const config = options.getResolvedConfig();
|
|
const subtitleTimingTracker = options.getSubtitleTimingTracker();
|
|
const mpvClient = options.getMpvClient();
|
|
const runtimeOptionsManager = options.getRuntimeOptionsManager();
|
|
|
|
if (
|
|
config.ankiConnect?.enabled !== true ||
|
|
!subtitleTimingTracker ||
|
|
!mpvClient ||
|
|
!runtimeOptionsManager
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
const effectiveAnkiConfig = runtimeOptionsManager.getEffectiveAnkiConnectConfig(
|
|
config.ankiConnect,
|
|
);
|
|
const createAnkiIntegration = options.createAnkiIntegration ?? createDefaultAnkiIntegration;
|
|
const integration = createAnkiIntegration({
|
|
config: effectiveAnkiConfig,
|
|
aiConfig: mergeAiConfig(config.ai, config.ankiConnect?.ai),
|
|
subtitleTimingTracker,
|
|
mpvClient,
|
|
showDesktopNotification: options.showDesktopNotification,
|
|
createFieldGroupingCallback: options.createFieldGroupingCallback,
|
|
knownWordCacheStatePath: options.getKnownWordCacheStatePath(),
|
|
});
|
|
if (options.shouldStartAnkiIntegration?.() !== false) {
|
|
integration.start();
|
|
}
|
|
options.setAnkiIntegration(integration);
|
|
return true;
|
|
}
|