mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
refactor: extract overlay runtime and anki/jimaku orchestration
This commit is contained in:
169
src/core/services/anki-jimaku-runtime-service.ts
Normal file
169
src/core/services/anki-jimaku-runtime-service.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { AnkiIntegration } from "../../anki-integration";
|
||||||
|
import {
|
||||||
|
AnkiConnectConfig,
|
||||||
|
JimakuApiResponse,
|
||||||
|
JimakuEntry,
|
||||||
|
JimakuFileEntry,
|
||||||
|
JimakuLanguagePreference,
|
||||||
|
JimakuMediaInfo,
|
||||||
|
KikuFieldGroupingChoice,
|
||||||
|
KikuFieldGroupingRequestData,
|
||||||
|
} from "../../types";
|
||||||
|
import { sortJimakuFiles } from "../../jimaku/utils";
|
||||||
|
import { registerAnkiJimakuIpcHandlers } from "./anki-jimaku-ipc-service";
|
||||||
|
|
||||||
|
interface MpvClientLike {
|
||||||
|
connected: boolean;
|
||||||
|
send: (payload: { command: string[] }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RuntimeOptionsManagerLike {
|
||||||
|
getEffectiveAnkiConnectConfig: (config?: AnkiConnectConfig) => AnkiConnectConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SubtitleTimingTrackerLike {
|
||||||
|
cleanup: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerAnkiJimakuIpcRuntimeService(options: {
|
||||||
|
patchAnkiConnectEnabled: (enabled: boolean) => void;
|
||||||
|
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
|
||||||
|
getRuntimeOptionsManager: () => RuntimeOptionsManagerLike | null;
|
||||||
|
getSubtitleTimingTracker: () => SubtitleTimingTrackerLike | null;
|
||||||
|
getMpvClient: () => MpvClientLike | null;
|
||||||
|
getAnkiIntegration: () => AnkiIntegration | null;
|
||||||
|
setAnkiIntegration: (integration: AnkiIntegration | null) => void;
|
||||||
|
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||||
|
createFieldGroupingCallback: () => (
|
||||||
|
data: KikuFieldGroupingRequestData,
|
||||||
|
) => Promise<KikuFieldGroupingChoice>;
|
||||||
|
broadcastRuntimeOptionsChanged: () => void;
|
||||||
|
getFieldGroupingResolver: () => ((choice: KikuFieldGroupingChoice) => void) | null;
|
||||||
|
setFieldGroupingResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) => void;
|
||||||
|
parseMediaInfo: (mediaPath: string | null) => JimakuMediaInfo;
|
||||||
|
getCurrentMediaPath: () => string | null;
|
||||||
|
jimakuFetchJson: <T>(
|
||||||
|
endpoint: string,
|
||||||
|
query?: Record<string, string | number | boolean | null | undefined>,
|
||||||
|
) => Promise<JimakuApiResponse<T>>;
|
||||||
|
getJimakuMaxEntryResults: () => number;
|
||||||
|
getJimakuLanguagePreference: () => JimakuLanguagePreference;
|
||||||
|
resolveJimakuApiKey: () => Promise<string | null>;
|
||||||
|
isRemoteMediaPath: (mediaPath: string) => boolean;
|
||||||
|
downloadToFile: (
|
||||||
|
url: string,
|
||||||
|
destPath: string,
|
||||||
|
headers: Record<string, string>,
|
||||||
|
) => Promise<{ ok: true; path: string } | { ok: false; error: { error: string; code?: number; retryAfter?: number } }>;
|
||||||
|
}): void {
|
||||||
|
registerAnkiJimakuIpcHandlers({
|
||||||
|
setAnkiConnectEnabled: (enabled) => {
|
||||||
|
options.patchAnkiConnectEnabled(enabled);
|
||||||
|
const config = options.getResolvedConfig();
|
||||||
|
const subtitleTimingTracker = options.getSubtitleTimingTracker();
|
||||||
|
const mpvClient = options.getMpvClient();
|
||||||
|
const ankiIntegration = options.getAnkiIntegration();
|
||||||
|
|
||||||
|
if (enabled && !ankiIntegration && subtitleTimingTracker && mpvClient) {
|
||||||
|
const runtimeOptionsManager = options.getRuntimeOptionsManager();
|
||||||
|
const effectiveAnkiConfig = runtimeOptionsManager
|
||||||
|
? runtimeOptionsManager.getEffectiveAnkiConnectConfig(config.ankiConnect)
|
||||||
|
: config.ankiConnect;
|
||||||
|
const integration = new AnkiIntegration(
|
||||||
|
effectiveAnkiConfig as never,
|
||||||
|
subtitleTimingTracker as never,
|
||||||
|
mpvClient as never,
|
||||||
|
(text: string) => {
|
||||||
|
if (mpvClient) {
|
||||||
|
mpvClient.send({
|
||||||
|
command: ["show-text", text, "3000"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options.showDesktopNotification,
|
||||||
|
options.createFieldGroupingCallback(),
|
||||||
|
);
|
||||||
|
integration.start();
|
||||||
|
options.setAnkiIntegration(integration);
|
||||||
|
console.log("AnkiConnect integration enabled");
|
||||||
|
} else if (!enabled && ankiIntegration) {
|
||||||
|
ankiIntegration.destroy();
|
||||||
|
options.setAnkiIntegration(null);
|
||||||
|
console.log("AnkiConnect integration disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
options.broadcastRuntimeOptionsChanged();
|
||||||
|
},
|
||||||
|
clearAnkiHistory: () => {
|
||||||
|
const subtitleTimingTracker = options.getSubtitleTimingTracker();
|
||||||
|
if (subtitleTimingTracker) {
|
||||||
|
subtitleTimingTracker.cleanup();
|
||||||
|
console.log("AnkiConnect subtitle timing history cleared");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
respondFieldGrouping: (choice) => {
|
||||||
|
const resolver = options.getFieldGroupingResolver();
|
||||||
|
if (resolver) {
|
||||||
|
resolver(choice);
|
||||||
|
options.setFieldGroupingResolver(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildKikuMergePreview: async (request) => {
|
||||||
|
const integration = options.getAnkiIntegration();
|
||||||
|
if (!integration) {
|
||||||
|
return { ok: false, error: "AnkiConnect integration not enabled" };
|
||||||
|
}
|
||||||
|
return integration.buildFieldGroupingPreview(
|
||||||
|
request.keepNoteId,
|
||||||
|
request.deleteNoteId,
|
||||||
|
request.deleteDuplicate,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getJimakuMediaInfo: () => options.parseMediaInfo(options.getCurrentMediaPath()),
|
||||||
|
searchJimakuEntries: async (query) => {
|
||||||
|
console.log(`[jimaku] search-entries query: "${query.query}"`);
|
||||||
|
const response = await options.jimakuFetchJson<JimakuEntry[]>(
|
||||||
|
"/api/entries/search",
|
||||||
|
{
|
||||||
|
anime: true,
|
||||||
|
query: query.query,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!response.ok) return response;
|
||||||
|
const maxResults = options.getJimakuMaxEntryResults();
|
||||||
|
console.log(
|
||||||
|
`[jimaku] search-entries returned ${response.data.length} results (capped to ${maxResults})`,
|
||||||
|
);
|
||||||
|
return { ok: true, data: response.data.slice(0, maxResults) };
|
||||||
|
},
|
||||||
|
listJimakuFiles: async (query) => {
|
||||||
|
console.log(
|
||||||
|
`[jimaku] list-files entryId=${query.entryId} episode=${query.episode ?? "all"}`,
|
||||||
|
);
|
||||||
|
const response = await options.jimakuFetchJson<JimakuFileEntry[]>(
|
||||||
|
`/api/entries/${query.entryId}/files`,
|
||||||
|
{
|
||||||
|
episode: query.episode ?? undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!response.ok) return response;
|
||||||
|
const sorted = sortJimakuFiles(
|
||||||
|
response.data,
|
||||||
|
options.getJimakuLanguagePreference(),
|
||||||
|
);
|
||||||
|
console.log(`[jimaku] list-files returned ${sorted.length} files`);
|
||||||
|
return { ok: true, data: sorted };
|
||||||
|
},
|
||||||
|
resolveJimakuApiKey: () => options.resolveJimakuApiKey(),
|
||||||
|
getCurrentMediaPath: () => options.getCurrentMediaPath(),
|
||||||
|
isRemoteMediaPath: (mediaPath) => options.isRemoteMediaPath(mediaPath),
|
||||||
|
downloadToFile: (url, destPath, headers) =>
|
||||||
|
options.downloadToFile(url, destPath, headers),
|
||||||
|
onDownloadedSubtitle: (pathToSubtitle) => {
|
||||||
|
const mpvClient = options.getMpvClient();
|
||||||
|
if (mpvClient && mpvClient.connected) {
|
||||||
|
mpvClient.send({ command: ["sub-add", pathToSubtitle, "select"] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
59
src/core/services/field-grouping-service.ts
Normal file
59
src/core/services/field-grouping-service.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import {
|
||||||
|
KikuFieldGroupingChoice,
|
||||||
|
KikuFieldGroupingRequestData,
|
||||||
|
} from "../../types";
|
||||||
|
|
||||||
|
export function createFieldGroupingCallbackService(options: {
|
||||||
|
getVisibleOverlayVisible: () => boolean;
|
||||||
|
getInvisibleOverlayVisible: () => boolean;
|
||||||
|
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||||
|
setInvisibleOverlayVisible: (visible: boolean) => void;
|
||||||
|
getResolver: () => ((choice: KikuFieldGroupingChoice) => void) | null;
|
||||||
|
setResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) => void;
|
||||||
|
sendRequestToVisibleOverlay: (data: KikuFieldGroupingRequestData) => boolean;
|
||||||
|
}): (data: KikuFieldGroupingRequestData) => Promise<KikuFieldGroupingChoice> {
|
||||||
|
return async (
|
||||||
|
data: KikuFieldGroupingRequestData,
|
||||||
|
): Promise<KikuFieldGroupingChoice> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const previousVisibleOverlay = options.getVisibleOverlayVisible();
|
||||||
|
const previousInvisibleOverlay = options.getInvisibleOverlayVisible();
|
||||||
|
let settled = false;
|
||||||
|
|
||||||
|
const finish = (choice: KikuFieldGroupingChoice): void => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
options.setResolver(null);
|
||||||
|
resolve(choice);
|
||||||
|
|
||||||
|
if (!previousVisibleOverlay && options.getVisibleOverlayVisible()) {
|
||||||
|
options.setVisibleOverlayVisible(false);
|
||||||
|
}
|
||||||
|
if (options.getInvisibleOverlayVisible() !== previousInvisibleOverlay) {
|
||||||
|
options.setInvisibleOverlayVisible(previousInvisibleOverlay);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
options.setResolver(finish);
|
||||||
|
if (!options.sendRequestToVisibleOverlay(data)) {
|
||||||
|
finish({
|
||||||
|
keepNoteId: 0,
|
||||||
|
deleteNoteId: 0,
|
||||||
|
deleteDuplicate: true,
|
||||||
|
cancelled: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!settled) {
|
||||||
|
finish({
|
||||||
|
keepNoteId: 0,
|
||||||
|
deleteNoteId: 0,
|
||||||
|
deleteDuplicate: true,
|
||||||
|
cancelled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 90000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
103
src/core/services/overlay-runtime-init-service.ts
Normal file
103
src/core/services/overlay-runtime-init-service.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { BrowserWindow } from "electron";
|
||||||
|
import { AnkiIntegration } from "../../anki-integration";
|
||||||
|
import { BaseWindowTracker, createWindowTracker } from "../../window-trackers";
|
||||||
|
import {
|
||||||
|
AnkiConnectConfig,
|
||||||
|
KikuFieldGroupingChoice,
|
||||||
|
KikuFieldGroupingRequestData,
|
||||||
|
WindowGeometry,
|
||||||
|
} from "../../types";
|
||||||
|
|
||||||
|
export function initializeOverlayRuntimeService(options: {
|
||||||
|
backendOverride: string | null;
|
||||||
|
getInitialInvisibleOverlayVisibility: () => boolean;
|
||||||
|
createMainWindow: () => void;
|
||||||
|
createInvisibleWindow: () => void;
|
||||||
|
registerGlobalShortcuts: () => void;
|
||||||
|
updateOverlayBounds: (geometry: WindowGeometry) => void;
|
||||||
|
isVisibleOverlayVisible: () => boolean;
|
||||||
|
isInvisibleOverlayVisible: () => boolean;
|
||||||
|
updateVisibleOverlayVisibility: () => void;
|
||||||
|
updateInvisibleOverlayVisibility: () => void;
|
||||||
|
getOverlayWindows: () => BrowserWindow[];
|
||||||
|
syncOverlayShortcuts: () => void;
|
||||||
|
setWindowTracker: (tracker: BaseWindowTracker | null) => void;
|
||||||
|
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
|
||||||
|
getSubtitleTimingTracker: () => unknown | null;
|
||||||
|
getMpvClient: () => { send?: (payload: { command: string[] }) => void } | null;
|
||||||
|
getRuntimeOptionsManager: () => {
|
||||||
|
getEffectiveAnkiConnectConfig: (config?: AnkiConnectConfig) => AnkiConnectConfig;
|
||||||
|
} | null;
|
||||||
|
setAnkiIntegration: (integration: unknown | null) => void;
|
||||||
|
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||||
|
createFieldGroupingCallback: () => (
|
||||||
|
data: KikuFieldGroupingRequestData,
|
||||||
|
) => Promise<KikuFieldGroupingChoice>;
|
||||||
|
}): {
|
||||||
|
invisibleOverlayVisible: boolean;
|
||||||
|
} {
|
||||||
|
options.createMainWindow();
|
||||||
|
options.createInvisibleWindow();
|
||||||
|
const invisibleOverlayVisible = options.getInitialInvisibleOverlayVisibility();
|
||||||
|
options.registerGlobalShortcuts();
|
||||||
|
|
||||||
|
const windowTracker = createWindowTracker(options.backendOverride);
|
||||||
|
options.setWindowTracker(windowTracker);
|
||||||
|
if (windowTracker) {
|
||||||
|
windowTracker.onGeometryChange = (geometry: WindowGeometry) => {
|
||||||
|
options.updateOverlayBounds(geometry);
|
||||||
|
};
|
||||||
|
windowTracker.onWindowFound = (geometry: WindowGeometry) => {
|
||||||
|
options.updateOverlayBounds(geometry);
|
||||||
|
if (options.isVisibleOverlayVisible()) {
|
||||||
|
options.updateVisibleOverlayVisibility();
|
||||||
|
}
|
||||||
|
if (options.isInvisibleOverlayVisible()) {
|
||||||
|
options.updateInvisibleOverlayVisibility();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
windowTracker.onWindowLost = () => {
|
||||||
|
for (const window of options.getOverlayWindows()) {
|
||||||
|
window.hide();
|
||||||
|
}
|
||||||
|
options.syncOverlayShortcuts();
|
||||||
|
};
|
||||||
|
windowTracker.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = options.getResolvedConfig();
|
||||||
|
const subtitleTimingTracker = options.getSubtitleTimingTracker();
|
||||||
|
const mpvClient = options.getMpvClient();
|
||||||
|
const runtimeOptionsManager = options.getRuntimeOptionsManager();
|
||||||
|
|
||||||
|
if (
|
||||||
|
config.ankiConnect &&
|
||||||
|
subtitleTimingTracker &&
|
||||||
|
mpvClient &&
|
||||||
|
runtimeOptionsManager
|
||||||
|
) {
|
||||||
|
const effectiveAnkiConfig =
|
||||||
|
runtimeOptionsManager.getEffectiveAnkiConnectConfig(config.ankiConnect);
|
||||||
|
const integration = new AnkiIntegration(
|
||||||
|
effectiveAnkiConfig,
|
||||||
|
subtitleTimingTracker as never,
|
||||||
|
mpvClient as never,
|
||||||
|
(text: string) => {
|
||||||
|
if (mpvClient && typeof mpvClient.send === "function") {
|
||||||
|
mpvClient.send({
|
||||||
|
command: ["show-text", text, "3000"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options.showDesktopNotification,
|
||||||
|
options.createFieldGroupingCallback(),
|
||||||
|
);
|
||||||
|
integration.start();
|
||||||
|
options.setAnkiIntegration(integration);
|
||||||
|
}
|
||||||
|
|
||||||
|
options.updateVisibleOverlayVisibility();
|
||||||
|
options.updateInvisibleOverlayVisibility();
|
||||||
|
|
||||||
|
return { invisibleOverlayVisible };
|
||||||
|
}
|
||||||
105
src/core/services/overlay-shortcut-runtime-service.ts
Normal file
105
src/core/services/overlay-shortcut-runtime-service.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import {
|
||||||
|
OverlayShortcutFallbackHandlers,
|
||||||
|
} from "./overlay-shortcut-fallback-runner";
|
||||||
|
import { OverlayShortcutHandlers } from "./overlay-shortcut-service";
|
||||||
|
|
||||||
|
export interface OverlayShortcutRuntimeDeps {
|
||||||
|
showMpvOsd: (text: string) => void;
|
||||||
|
openRuntimeOptions: () => void;
|
||||||
|
openJimaku: () => void;
|
||||||
|
markAudioCard: () => Promise<void>;
|
||||||
|
copySubtitleMultiple: (timeoutMs: number) => void;
|
||||||
|
copySubtitle: () => void;
|
||||||
|
toggleSecondarySub: () => void;
|
||||||
|
updateLastCardFromClipboard: () => Promise<void>;
|
||||||
|
triggerFieldGrouping: () => Promise<void>;
|
||||||
|
triggerSubsync: () => Promise<void>;
|
||||||
|
mineSentence: () => Promise<void>;
|
||||||
|
mineSentenceMultiple: (timeoutMs: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapAsync(
|
||||||
|
task: () => Promise<void>,
|
||||||
|
deps: OverlayShortcutRuntimeDeps,
|
||||||
|
logLabel: string,
|
||||||
|
osdLabel: string,
|
||||||
|
): () => void {
|
||||||
|
return () => {
|
||||||
|
task().catch((err) => {
|
||||||
|
console.error(`${logLabel} failed:`, err);
|
||||||
|
deps.showMpvOsd(`${osdLabel}: ${(err as Error).message}`);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createOverlayShortcutRuntimeHandlers(
|
||||||
|
deps: OverlayShortcutRuntimeDeps,
|
||||||
|
): {
|
||||||
|
overlayHandlers: OverlayShortcutHandlers;
|
||||||
|
fallbackHandlers: OverlayShortcutFallbackHandlers;
|
||||||
|
} {
|
||||||
|
const overlayHandlers: OverlayShortcutHandlers = {
|
||||||
|
copySubtitle: () => {
|
||||||
|
deps.copySubtitle();
|
||||||
|
},
|
||||||
|
copySubtitleMultiple: (timeoutMs) => {
|
||||||
|
deps.copySubtitleMultiple(timeoutMs);
|
||||||
|
},
|
||||||
|
updateLastCardFromClipboard: wrapAsync(
|
||||||
|
() => deps.updateLastCardFromClipboard(),
|
||||||
|
deps,
|
||||||
|
"updateLastCardFromClipboard",
|
||||||
|
"Update failed",
|
||||||
|
),
|
||||||
|
triggerFieldGrouping: wrapAsync(
|
||||||
|
() => deps.triggerFieldGrouping(),
|
||||||
|
deps,
|
||||||
|
"triggerFieldGrouping",
|
||||||
|
"Field grouping failed",
|
||||||
|
),
|
||||||
|
triggerSubsync: wrapAsync(
|
||||||
|
() => deps.triggerSubsync(),
|
||||||
|
deps,
|
||||||
|
"triggerSubsyncFromConfig",
|
||||||
|
"Subsync failed",
|
||||||
|
),
|
||||||
|
mineSentence: wrapAsync(
|
||||||
|
() => deps.mineSentence(),
|
||||||
|
deps,
|
||||||
|
"mineSentenceCard",
|
||||||
|
"Mine sentence failed",
|
||||||
|
),
|
||||||
|
mineSentenceMultiple: (timeoutMs) => {
|
||||||
|
deps.mineSentenceMultiple(timeoutMs);
|
||||||
|
},
|
||||||
|
toggleSecondarySub: () => deps.toggleSecondarySub(),
|
||||||
|
markAudioCard: wrapAsync(
|
||||||
|
() => deps.markAudioCard(),
|
||||||
|
deps,
|
||||||
|
"markLastCardAsAudioCard",
|
||||||
|
"Audio card failed",
|
||||||
|
),
|
||||||
|
openRuntimeOptions: () => {
|
||||||
|
deps.openRuntimeOptions();
|
||||||
|
},
|
||||||
|
openJimaku: () => {
|
||||||
|
deps.openJimaku();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const fallbackHandlers: OverlayShortcutFallbackHandlers = {
|
||||||
|
openRuntimeOptions: overlayHandlers.openRuntimeOptions,
|
||||||
|
openJimaku: overlayHandlers.openJimaku,
|
||||||
|
markAudioCard: overlayHandlers.markAudioCard,
|
||||||
|
copySubtitleMultiple: overlayHandlers.copySubtitleMultiple,
|
||||||
|
copySubtitle: overlayHandlers.copySubtitle,
|
||||||
|
toggleSecondarySub: overlayHandlers.toggleSecondarySub,
|
||||||
|
updateLastCardFromClipboard: overlayHandlers.updateLastCardFromClipboard,
|
||||||
|
triggerFieldGrouping: overlayHandlers.triggerFieldGrouping,
|
||||||
|
triggerSubsync: overlayHandlers.triggerSubsync,
|
||||||
|
mineSentence: overlayHandlers.mineSentence,
|
||||||
|
mineSentenceMultiple: overlayHandlers.mineSentenceMultiple,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { overlayHandlers, fallbackHandlers };
|
||||||
|
}
|
||||||
46
src/core/services/overlay-visibility-runtime-service.ts
Normal file
46
src/core/services/overlay-visibility-runtime-service.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
export function syncInvisibleOverlayMousePassthroughService(options: {
|
||||||
|
hasInvisibleWindow: () => boolean;
|
||||||
|
setIgnoreMouseEvents: (ignore: boolean, extra?: { forward: boolean }) => void;
|
||||||
|
visibleOverlayVisible: boolean;
|
||||||
|
invisibleOverlayVisible: boolean;
|
||||||
|
}): void {
|
||||||
|
if (!options.hasInvisibleWindow()) return;
|
||||||
|
if (options.visibleOverlayVisible) {
|
||||||
|
options.setIgnoreMouseEvents(true, { forward: true });
|
||||||
|
} else if (options.invisibleOverlayVisible) {
|
||||||
|
options.setIgnoreMouseEvents(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setVisibleOverlayVisibleService(options: {
|
||||||
|
visible: boolean;
|
||||||
|
setVisibleOverlayVisibleState: (visible: boolean) => void;
|
||||||
|
updateVisibleOverlayVisibility: () => void;
|
||||||
|
updateInvisibleOverlayVisibility: () => void;
|
||||||
|
syncInvisibleOverlayMousePassthrough: () => void;
|
||||||
|
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
|
||||||
|
isMpvConnected: () => boolean;
|
||||||
|
setMpvSubVisibility: (visible: boolean) => void;
|
||||||
|
}): void {
|
||||||
|
options.setVisibleOverlayVisibleState(options.visible);
|
||||||
|
options.updateVisibleOverlayVisibility();
|
||||||
|
options.updateInvisibleOverlayVisibility();
|
||||||
|
options.syncInvisibleOverlayMousePassthrough();
|
||||||
|
if (
|
||||||
|
options.shouldBindVisibleOverlayToMpvSubVisibility() &&
|
||||||
|
options.isMpvConnected()
|
||||||
|
) {
|
||||||
|
options.setMpvSubVisibility(!options.visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setInvisibleOverlayVisibleService(options: {
|
||||||
|
visible: boolean;
|
||||||
|
setInvisibleOverlayVisibleState: (visible: boolean) => void;
|
||||||
|
updateInvisibleOverlayVisibility: () => void;
|
||||||
|
syncInvisibleOverlayMousePassthrough: () => void;
|
||||||
|
}): void {
|
||||||
|
options.setInvisibleOverlayVisibleState(options.visible);
|
||||||
|
options.updateInvisibleOverlayVisibility();
|
||||||
|
options.syncInvisibleOverlayMousePassthrough();
|
||||||
|
}
|
||||||
146
src/core/services/overlay-window-service.ts
Normal file
146
src/core/services/overlay-window-service.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { BrowserWindow } from "electron";
|
||||||
|
import * as path from "path";
|
||||||
|
import { WindowGeometry } from "../../types";
|
||||||
|
|
||||||
|
export type OverlayWindowKind = "visible" | "invisible";
|
||||||
|
|
||||||
|
export function updateOverlayBoundsService(
|
||||||
|
geometry: WindowGeometry,
|
||||||
|
getOverlayWindows: () => BrowserWindow[],
|
||||||
|
): void {
|
||||||
|
if (!geometry) return;
|
||||||
|
for (const window of getOverlayWindows()) {
|
||||||
|
window.setBounds({
|
||||||
|
x: geometry.x,
|
||||||
|
y: geometry.y,
|
||||||
|
width: geometry.width,
|
||||||
|
height: geometry.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureOverlayWindowLevelService(window: BrowserWindow): void {
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
window.setAlwaysOnTop(true, "screen-saver", 1);
|
||||||
|
window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
||||||
|
window.setFullScreenable(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.setAlwaysOnTop(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enforceOverlayLayerOrderService(options: {
|
||||||
|
visibleOverlayVisible: boolean;
|
||||||
|
invisibleOverlayVisible: boolean;
|
||||||
|
mainWindow: BrowserWindow | null;
|
||||||
|
invisibleWindow: BrowserWindow | null;
|
||||||
|
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
|
||||||
|
}): void {
|
||||||
|
if (!options.visibleOverlayVisible || !options.invisibleOverlayVisible) return;
|
||||||
|
if (!options.mainWindow || options.mainWindow.isDestroyed()) return;
|
||||||
|
if (!options.invisibleWindow || options.invisibleWindow.isDestroyed()) return;
|
||||||
|
|
||||||
|
options.ensureOverlayWindowLevel(options.mainWindow);
|
||||||
|
options.mainWindow.moveTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createOverlayWindowService(
|
||||||
|
kind: OverlayWindowKind,
|
||||||
|
options: {
|
||||||
|
isDev: boolean;
|
||||||
|
overlayDebugVisualizationEnabled: boolean;
|
||||||
|
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
|
||||||
|
onRuntimeOptionsChanged: () => void;
|
||||||
|
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
||||||
|
isOverlayVisible: (kind: OverlayWindowKind) => boolean;
|
||||||
|
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
||||||
|
onWindowClosed: (kind: OverlayWindowKind) => void;
|
||||||
|
},
|
||||||
|
): BrowserWindow {
|
||||||
|
const window = new BrowserWindow({
|
||||||
|
show: false,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
transparent: true,
|
||||||
|
frame: false,
|
||||||
|
alwaysOnTop: true,
|
||||||
|
skipTaskbar: true,
|
||||||
|
resizable: false,
|
||||||
|
hasShadow: false,
|
||||||
|
focusable: true,
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, "..", "..", "preload.js"),
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
webSecurity: true,
|
||||||
|
additionalArguments: [`--overlay-layer=${kind}`],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
options.ensureOverlayWindowLevel(window);
|
||||||
|
|
||||||
|
const htmlPath = path.join(__dirname, "..", "..", "renderer", "index.html");
|
||||||
|
|
||||||
|
window
|
||||||
|
.loadFile(htmlPath, {
|
||||||
|
query: { layer: kind === "visible" ? "visible" : "invisible" },
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Failed to load HTML file:", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.webContents.on(
|
||||||
|
"did-fail-load",
|
||||||
|
(_event, errorCode, errorDescription, validatedURL) => {
|
||||||
|
console.error(
|
||||||
|
"Page failed to load:",
|
||||||
|
errorCode,
|
||||||
|
errorDescription,
|
||||||
|
validatedURL,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
window.webContents.on("did-finish-load", () => {
|
||||||
|
options.onRuntimeOptionsChanged();
|
||||||
|
window.webContents.send(
|
||||||
|
"overlay-debug-visualization:set",
|
||||||
|
options.overlayDebugVisualizationEnabled,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (kind === "visible") {
|
||||||
|
window.webContents.on("devtools-opened", () => {
|
||||||
|
options.setOverlayDebugVisualizationEnabled(true);
|
||||||
|
});
|
||||||
|
window.webContents.on("devtools-closed", () => {
|
||||||
|
options.setOverlayDebugVisualizationEnabled(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.webContents.on("before-input-event", (event, input) => {
|
||||||
|
if (!options.isOverlayVisible(kind)) return;
|
||||||
|
if (!options.tryHandleOverlayShortcutLocalFallback(input)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.hide();
|
||||||
|
|
||||||
|
window.on("closed", () => {
|
||||||
|
options.onWindowClosed(kind);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.on("blur", () => {
|
||||||
|
if (!window.isDestroyed()) {
|
||||||
|
options.ensureOverlayWindowLevel(window);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.isDev && kind === "visible") {
|
||||||
|
window.webContents.openDevTools({ mode: "detach" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return window;
|
||||||
|
}
|
||||||
703
src/main.ts
703
src/main.ts
@@ -47,13 +47,11 @@ import * as fs from "fs";
|
|||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
import { MecabTokenizer } from "./mecab-tokenizer";
|
import { MecabTokenizer } from "./mecab-tokenizer";
|
||||||
import { mergeTokens } from "./token-merger";
|
import { mergeTokens } from "./token-merger";
|
||||||
import { createWindowTracker, BaseWindowTracker } from "./window-trackers";
|
import { BaseWindowTracker } from "./window-trackers";
|
||||||
import {
|
import {
|
||||||
Config,
|
Config,
|
||||||
JimakuApiResponse,
|
JimakuApiResponse,
|
||||||
JimakuDownloadResult,
|
JimakuDownloadResult,
|
||||||
JimakuEntry,
|
|
||||||
JimakuFileEntry,
|
|
||||||
JimakuMediaInfo,
|
JimakuMediaInfo,
|
||||||
JimakuConfig,
|
JimakuConfig,
|
||||||
JimakuLanguagePreference,
|
JimakuLanguagePreference,
|
||||||
@@ -65,7 +63,6 @@ import {
|
|||||||
SubsyncManualPayload,
|
SubsyncManualPayload,
|
||||||
SubsyncManualRunRequest,
|
SubsyncManualRunRequest,
|
||||||
SubsyncResult,
|
SubsyncResult,
|
||||||
KikuFieldGroupingRequestData,
|
|
||||||
KikuFieldGroupingChoice,
|
KikuFieldGroupingChoice,
|
||||||
KikuMergePreviewRequest,
|
KikuMergePreviewRequest,
|
||||||
KikuMergePreviewResponse,
|
KikuMergePreviewResponse,
|
||||||
@@ -84,7 +81,6 @@ import {
|
|||||||
jimakuFetchJson as jimakuFetchJsonRequest,
|
jimakuFetchJson as jimakuFetchJsonRequest,
|
||||||
parseMediaInfo,
|
parseMediaInfo,
|
||||||
resolveJimakuApiKey as resolveJimakuApiKeyFromConfig,
|
resolveJimakuApiKey as resolveJimakuApiKeyFromConfig,
|
||||||
sortJimakuFiles,
|
|
||||||
} from "./jimaku/utils";
|
} from "./jimaku/utils";
|
||||||
import {
|
import {
|
||||||
getSubsyncConfig,
|
getSubsyncConfig,
|
||||||
@@ -122,10 +118,24 @@ import {
|
|||||||
unregisterOverlayShortcutsService,
|
unregisterOverlayShortcutsService,
|
||||||
} from "./core/services/overlay-shortcut-service";
|
} from "./core/services/overlay-shortcut-service";
|
||||||
import { runOverlayShortcutLocalFallback } from "./core/services/overlay-shortcut-fallback-runner";
|
import { runOverlayShortcutLocalFallback } from "./core/services/overlay-shortcut-fallback-runner";
|
||||||
|
import { createOverlayShortcutRuntimeHandlers } from "./core/services/overlay-shortcut-runtime-service";
|
||||||
import { showDesktopNotification } from "./core/utils/notification";
|
import { showDesktopNotification } from "./core/utils/notification";
|
||||||
import { openYomitanSettingsWindow } from "./core/services/yomitan-settings-service";
|
import { openYomitanSettingsWindow } from "./core/services/yomitan-settings-service";
|
||||||
import { tokenizeSubtitleService } from "./core/services/tokenizer-service";
|
import { tokenizeSubtitleService } from "./core/services/tokenizer-service";
|
||||||
import { loadYomitanExtensionService } from "./core/services/yomitan-extension-loader-service";
|
import { loadYomitanExtensionService } from "./core/services/yomitan-extension-loader-service";
|
||||||
|
import {
|
||||||
|
createOverlayWindowService,
|
||||||
|
enforceOverlayLayerOrderService,
|
||||||
|
ensureOverlayWindowLevelService,
|
||||||
|
updateOverlayBoundsService,
|
||||||
|
} from "./core/services/overlay-window-service";
|
||||||
|
import { createFieldGroupingCallbackService } from "./core/services/field-grouping-service";
|
||||||
|
import { initializeOverlayRuntimeService } from "./core/services/overlay-runtime-init-service";
|
||||||
|
import {
|
||||||
|
setInvisibleOverlayVisibleService,
|
||||||
|
setVisibleOverlayVisibleService,
|
||||||
|
syncInvisibleOverlayMousePassthroughService,
|
||||||
|
} from "./core/services/overlay-visibility-runtime-service";
|
||||||
import {
|
import {
|
||||||
MpvIpcClient,
|
MpvIpcClient,
|
||||||
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
||||||
@@ -143,7 +153,7 @@ import {
|
|||||||
updateInvisibleOverlayVisibilityService,
|
updateInvisibleOverlayVisibilityService,
|
||||||
updateVisibleOverlayVisibilityService,
|
updateVisibleOverlayVisibilityService,
|
||||||
} from "./core/services/overlay-visibility-service";
|
} from "./core/services/overlay-visibility-service";
|
||||||
import { registerAnkiJimakuIpcHandlers } from "./core/services/anki-jimaku-ipc-service";
|
import { registerAnkiJimakuIpcRuntimeService } from "./core/services/anki-jimaku-runtime-service";
|
||||||
import {
|
import {
|
||||||
ConfigService,
|
ConfigService,
|
||||||
DEFAULT_CONFIG,
|
DEFAULT_CONFIG,
|
||||||
@@ -1042,34 +1052,21 @@ async function tokenizeSubtitle(text: string): Promise<SubtitleData> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateOverlayBounds(geometry: WindowGeometry): void {
|
function updateOverlayBounds(geometry: WindowGeometry): void {
|
||||||
if (!geometry) return;
|
updateOverlayBoundsService(geometry, () => getOverlayWindows());
|
||||||
for (const window of getOverlayWindows()) {
|
|
||||||
window.setBounds({
|
|
||||||
x: geometry.x,
|
|
||||||
y: geometry.y,
|
|
||||||
width: geometry.width,
|
|
||||||
height: geometry.height,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureOverlayWindowLevel(window: BrowserWindow): void {
|
function ensureOverlayWindowLevel(window: BrowserWindow): void {
|
||||||
if (process.platform === "darwin") {
|
ensureOverlayWindowLevelService(window);
|
||||||
window.setAlwaysOnTop(true, "screen-saver", 1);
|
|
||||||
window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
||||||
window.setFullScreenable(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.setAlwaysOnTop(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function enforceOverlayLayerOrder(): void {
|
function enforceOverlayLayerOrder(): void {
|
||||||
if (!visibleOverlayVisible || !invisibleOverlayVisible) return;
|
enforceOverlayLayerOrderService({
|
||||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
visibleOverlayVisible,
|
||||||
if (!invisibleWindow || invisibleWindow.isDestroyed()) return;
|
invisibleOverlayVisible,
|
||||||
|
mainWindow,
|
||||||
ensureOverlayWindowLevel(mainWindow);
|
invisibleWindow,
|
||||||
mainWindow.moveTop();
|
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadYomitanExtension(): Promise<Extension | null> {
|
async function loadYomitanExtension(): Promise<Extension | null> {
|
||||||
@@ -1092,249 +1089,118 @@ async function loadYomitanExtension(): Promise<Extension | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createOverlayWindow(kind: "visible" | "invisible"): BrowserWindow {
|
function createOverlayWindow(kind: "visible" | "invisible"): BrowserWindow {
|
||||||
const window = new BrowserWindow({
|
return createOverlayWindowService(kind, {
|
||||||
show: false,
|
isDev,
|
||||||
width: 800,
|
overlayDebugVisualizationEnabled,
|
||||||
height: 600,
|
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
||||||
x: 0,
|
onRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
||||||
y: 0,
|
setOverlayDebugVisualizationEnabled: (enabled) =>
|
||||||
transparent: true,
|
setOverlayDebugVisualizationEnabled(enabled),
|
||||||
frame: false,
|
isOverlayVisible: (windowKind) =>
|
||||||
alwaysOnTop: true,
|
windowKind === "visible" ? visibleOverlayVisible : invisibleOverlayVisible,
|
||||||
skipTaskbar: true,
|
tryHandleOverlayShortcutLocalFallback: (input) =>
|
||||||
resizable: false,
|
tryHandleOverlayShortcutLocalFallback(input),
|
||||||
hasShadow: false,
|
onWindowClosed: (windowKind) => {
|
||||||
focusable: true,
|
if (windowKind === "visible") {
|
||||||
webPreferences: {
|
mainWindow = null;
|
||||||
preload: path.join(__dirname, "preload.js"),
|
} else {
|
||||||
contextIsolation: true,
|
invisibleWindow = null;
|
||||||
nodeIntegration: false,
|
}
|
||||||
webSecurity: true,
|
|
||||||
additionalArguments: [`--overlay-layer=${kind}`],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
ensureOverlayWindowLevel(window);
|
|
||||||
|
|
||||||
const htmlPath = path.join(__dirname, "renderer", "index.html");
|
|
||||||
|
|
||||||
window
|
|
||||||
.loadFile(htmlPath, {
|
|
||||||
query: { layer: kind === "visible" ? "visible" : "invisible" },
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error("Failed to load HTML file:", err);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.webContents.on(
|
|
||||||
"did-fail-load",
|
|
||||||
(_event, errorCode, errorDescription, validatedURL) => {
|
|
||||||
console.error(
|
|
||||||
"Page failed to load:",
|
|
||||||
errorCode,
|
|
||||||
errorDescription,
|
|
||||||
validatedURL,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
window.webContents.on("did-finish-load", () => {
|
|
||||||
broadcastRuntimeOptionsChanged();
|
|
||||||
window.webContents.send(
|
|
||||||
"overlay-debug-visualization:set",
|
|
||||||
overlayDebugVisualizationEnabled,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (kind === "visible") {
|
|
||||||
window.webContents.on("devtools-opened", () => {
|
|
||||||
setOverlayDebugVisualizationEnabled(true);
|
|
||||||
});
|
|
||||||
window.webContents.on("devtools-closed", () => {
|
|
||||||
setOverlayDebugVisualizationEnabled(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.webContents.on("before-input-event", (event, input) => {
|
|
||||||
const isOverlayVisible =
|
|
||||||
kind === "visible" ? visibleOverlayVisible : invisibleOverlayVisible;
|
|
||||||
if (!isOverlayVisible) return;
|
|
||||||
if (!tryHandleOverlayShortcutLocalFallback(input)) return;
|
|
||||||
event.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
window.hide();
|
|
||||||
|
|
||||||
window.on("closed", () => {
|
|
||||||
if (kind === "visible") {
|
|
||||||
mainWindow = null;
|
|
||||||
} else {
|
|
||||||
invisibleWindow = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.on("blur", () => {
|
|
||||||
if (!window.isDestroyed()) {
|
|
||||||
ensureOverlayWindowLevel(window);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isDev && kind === "visible") {
|
|
||||||
window.webContents.openDevTools({ mode: "detach" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return window;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMainWindow(): BrowserWindow {
|
function createMainWindow(): BrowserWindow { mainWindow = createOverlayWindow("visible"); return mainWindow; }
|
||||||
mainWindow = createOverlayWindow("visible");
|
function createInvisibleWindow(): BrowserWindow { invisibleWindow = createOverlayWindow("invisible"); return invisibleWindow; }
|
||||||
return mainWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createInvisibleWindow(): BrowserWindow {
|
|
||||||
invisibleWindow = createOverlayWindow("invisible");
|
|
||||||
return invisibleWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeOverlayRuntime(): void {
|
function initializeOverlayRuntime(): void {
|
||||||
if (overlayRuntimeInitialized) {
|
if (overlayRuntimeInitialized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const result = initializeOverlayRuntimeService({
|
||||||
createMainWindow();
|
backendOverride,
|
||||||
createInvisibleWindow();
|
getInitialInvisibleOverlayVisibility: () => getInitialInvisibleOverlayVisibility(),
|
||||||
invisibleOverlayVisible = getInitialInvisibleOverlayVisibility();
|
createMainWindow: () => {
|
||||||
registerGlobalShortcuts();
|
createMainWindow();
|
||||||
|
},
|
||||||
windowTracker = createWindowTracker(backendOverride);
|
createInvisibleWindow: () => {
|
||||||
if (windowTracker) {
|
createInvisibleWindow();
|
||||||
windowTracker.onGeometryChange = (geometry: WindowGeometry) => {
|
},
|
||||||
|
registerGlobalShortcuts: () => {
|
||||||
|
registerGlobalShortcuts();
|
||||||
|
},
|
||||||
|
updateOverlayBounds: (geometry) => {
|
||||||
updateOverlayBounds(geometry);
|
updateOverlayBounds(geometry);
|
||||||
};
|
},
|
||||||
windowTracker.onWindowFound = (geometry: WindowGeometry) => {
|
isVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||||
updateOverlayBounds(geometry);
|
isInvisibleOverlayVisible: () => invisibleOverlayVisible,
|
||||||
if (visibleOverlayVisible) {
|
updateVisibleOverlayVisibility: () => {
|
||||||
updateVisibleOverlayVisibility();
|
updateVisibleOverlayVisibility();
|
||||||
}
|
},
|
||||||
if (invisibleOverlayVisible) {
|
updateInvisibleOverlayVisibility: () => {
|
||||||
updateInvisibleOverlayVisibility();
|
updateInvisibleOverlayVisibility();
|
||||||
}
|
},
|
||||||
};
|
getOverlayWindows: () => getOverlayWindows(),
|
||||||
windowTracker.onWindowLost = () => {
|
syncOverlayShortcuts: () => {
|
||||||
for (const window of getOverlayWindows()) {
|
|
||||||
window.hide();
|
|
||||||
}
|
|
||||||
syncOverlayShortcuts();
|
syncOverlayShortcuts();
|
||||||
};
|
},
|
||||||
windowTracker.start();
|
setWindowTracker: (tracker) => {
|
||||||
}
|
windowTracker = tracker;
|
||||||
|
},
|
||||||
const config = getResolvedConfig();
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
if (
|
getSubtitleTimingTracker: () => subtitleTimingTracker,
|
||||||
config.ankiConnect?.enabled &&
|
getMpvClient: () => mpvClient,
|
||||||
subtitleTimingTracker &&
|
getRuntimeOptionsManager: () => runtimeOptionsManager,
|
||||||
mpvClient &&
|
setAnkiIntegration: (integration) => {
|
||||||
runtimeOptionsManager
|
ankiIntegration = integration as AnkiIntegration | null;
|
||||||
) {
|
},
|
||||||
const effectiveAnkiConfig =
|
showDesktopNotification,
|
||||||
runtimeOptionsManager.getEffectiveAnkiConnectConfig(config.ankiConnect);
|
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
||||||
ankiIntegration = new AnkiIntegration(
|
});
|
||||||
effectiveAnkiConfig,
|
invisibleOverlayVisible = result.invisibleOverlayVisible;
|
||||||
subtitleTimingTracker,
|
|
||||||
mpvClient,
|
|
||||||
(text: string) => {
|
|
||||||
if (mpvClient) {
|
|
||||||
mpvClient.send({
|
|
||||||
command: ["show-text", text, "3000"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
showDesktopNotification,
|
|
||||||
createFieldGroupingCallback(),
|
|
||||||
);
|
|
||||||
ankiIntegration.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
overlayRuntimeInitialized = true;
|
overlayRuntimeInitialized = true;
|
||||||
updateVisibleOverlayVisibility();
|
|
||||||
updateInvisibleOverlayVisibility();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openYomitanSettings(): void {
|
function openYomitanSettings(): void { openYomitanSettingsWindow({ yomitanExt, getExistingWindow: () => yomitanSettingsWindow, setWindow: (window) => (yomitanSettingsWindow = window) }); }
|
||||||
openYomitanSettingsWindow({
|
function registerGlobalShortcuts(): void { registerGlobalShortcutsService({ shortcuts: getConfiguredShortcuts(), onToggleVisibleOverlay: () => toggleVisibleOverlay(), onToggleInvisibleOverlay: () => toggleInvisibleOverlay(), onOpenYomitanSettings: () => openYomitanSettings(), isDev, getMainWindow: () => mainWindow }); }
|
||||||
yomitanExt,
|
|
||||||
getExistingWindow: () => yomitanSettingsWindow,
|
|
||||||
setWindow: (window) => (yomitanSettingsWindow = window),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerGlobalShortcuts(): void {
|
|
||||||
registerGlobalShortcutsService({
|
|
||||||
shortcuts: getConfiguredShortcuts(),
|
|
||||||
onToggleVisibleOverlay: () => toggleVisibleOverlay(),
|
|
||||||
onToggleInvisibleOverlay: () => toggleInvisibleOverlay(),
|
|
||||||
onOpenYomitanSettings: () => openYomitanSettings(),
|
|
||||||
isDev,
|
|
||||||
getMainWindow: () => mainWindow,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getConfiguredShortcuts() { return resolveConfiguredShortcuts(getResolvedConfig(), DEFAULT_CONFIG); }
|
function getConfiguredShortcuts() { return resolveConfiguredShortcuts(getResolvedConfig(), DEFAULT_CONFIG); }
|
||||||
|
|
||||||
|
function getOverlayShortcutRuntimeHandlers() {
|
||||||
|
return createOverlayShortcutRuntimeHandlers({
|
||||||
|
showMpvOsd: (text) => showMpvOsd(text),
|
||||||
|
openRuntimeOptions: () => {
|
||||||
|
openRuntimeOptionsPalette();
|
||||||
|
},
|
||||||
|
openJimaku: () => {
|
||||||
|
sendToVisibleOverlay("jimaku:open");
|
||||||
|
},
|
||||||
|
markAudioCard: () => markLastCardAsAudioCard(),
|
||||||
|
copySubtitleMultiple: (timeoutMs) => {
|
||||||
|
startPendingMultiCopy(timeoutMs);
|
||||||
|
},
|
||||||
|
copySubtitle: () => {
|
||||||
|
copyCurrentSubtitle();
|
||||||
|
},
|
||||||
|
toggleSecondarySub: () => cycleSecondarySubMode(),
|
||||||
|
updateLastCardFromClipboard: () => updateLastCardFromClipboard(),
|
||||||
|
triggerFieldGrouping: () => triggerFieldGrouping(),
|
||||||
|
triggerSubsync: () => triggerSubsyncFromConfig(),
|
||||||
|
mineSentence: () => mineSentenceCard(),
|
||||||
|
mineSentenceMultiple: (timeoutMs) => {
|
||||||
|
startPendingMineSentenceMultiple(timeoutMs);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function tryHandleOverlayShortcutLocalFallback(input: Electron.Input): boolean {
|
function tryHandleOverlayShortcutLocalFallback(input: Electron.Input): boolean {
|
||||||
const shortcuts = getConfiguredShortcuts();
|
const shortcuts = getConfiguredShortcuts();
|
||||||
|
const handlers = getOverlayShortcutRuntimeHandlers();
|
||||||
return runOverlayShortcutLocalFallback(
|
return runOverlayShortcutLocalFallback(
|
||||||
input,
|
input,
|
||||||
shortcuts,
|
shortcuts,
|
||||||
shortcutMatchesInputForLocalFallback,
|
shortcutMatchesInputForLocalFallback,
|
||||||
{
|
handlers.fallbackHandlers,
|
||||||
openRuntimeOptions: () => {
|
|
||||||
openRuntimeOptionsPalette();
|
|
||||||
},
|
|
||||||
openJimaku: () => {
|
|
||||||
sendToVisibleOverlay("jimaku:open");
|
|
||||||
},
|
|
||||||
markAudioCard: () => {
|
|
||||||
markLastCardAsAudioCard().catch((err) => {
|
|
||||||
console.error("markLastCardAsAudioCard failed:", err);
|
|
||||||
showMpvOsd(`Audio card failed: ${(err as Error).message}`);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
copySubtitleMultiple: (timeoutMs) => {
|
|
||||||
startPendingMultiCopy(timeoutMs);
|
|
||||||
},
|
|
||||||
copySubtitle: () => {
|
|
||||||
copyCurrentSubtitle();
|
|
||||||
},
|
|
||||||
toggleSecondarySub: () => cycleSecondarySubMode(),
|
|
||||||
updateLastCardFromClipboard: () => {
|
|
||||||
updateLastCardFromClipboard().catch((err) => {
|
|
||||||
console.error("updateLastCardFromClipboard failed:", err);
|
|
||||||
showMpvOsd(`Update failed: ${(err as Error).message}`);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
triggerFieldGrouping: () => {
|
|
||||||
triggerFieldGrouping().catch((err) => {
|
|
||||||
console.error("triggerFieldGrouping failed:", err);
|
|
||||||
showMpvOsd(`Field grouping failed: ${(err as Error).message}`);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
triggerSubsync: () => {
|
|
||||||
triggerSubsyncFromConfig().catch((err) => {
|
|
||||||
console.error("triggerSubsyncFromConfig failed:", err);
|
|
||||||
showMpvOsd(`Subsync failed: ${(err as Error).message}`);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
mineSentence: () => {
|
|
||||||
mineSentenceCard().catch((err) => {
|
|
||||||
console.error("mineSentenceCard failed:", err);
|
|
||||||
showMpvOsd(`Mine sentence failed: ${(err as Error).message}`);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
mineSentenceMultiple: (timeoutMs) => {
|
|
||||||
startPendingMineSentenceMultiple(timeoutMs);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1629,54 +1495,11 @@ function handleMineSentenceDigit(count: number): void {
|
|||||||
|
|
||||||
function registerOverlayShortcuts(): void {
|
function registerOverlayShortcuts(): void {
|
||||||
const shortcuts = getConfiguredShortcuts();
|
const shortcuts = getConfiguredShortcuts();
|
||||||
shortcutsRegistered = registerOverlayShortcutsService(shortcuts, {
|
const handlers = getOverlayShortcutRuntimeHandlers();
|
||||||
copySubtitle: () => {
|
shortcutsRegistered = registerOverlayShortcutsService(
|
||||||
copyCurrentSubtitle();
|
shortcuts,
|
||||||
},
|
handlers.overlayHandlers,
|
||||||
copySubtitleMultiple: (timeoutMs) => {
|
);
|
||||||
startPendingMultiCopy(timeoutMs);
|
|
||||||
},
|
|
||||||
updateLastCardFromClipboard: () => {
|
|
||||||
updateLastCardFromClipboard().catch((err) => {
|
|
||||||
console.error("updateLastCardFromClipboard failed:", err);
|
|
||||||
showMpvOsd(`Update failed: ${(err as Error).message}`);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
triggerFieldGrouping: () => {
|
|
||||||
triggerFieldGrouping().catch((err) => {
|
|
||||||
console.error("triggerFieldGrouping failed:", err);
|
|
||||||
showMpvOsd(`Field grouping failed: ${(err as Error).message}`);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
triggerSubsync: () => {
|
|
||||||
triggerSubsyncFromConfig().catch((err) => {
|
|
||||||
console.error("triggerSubsyncFromConfig failed:", err);
|
|
||||||
showMpvOsd(`Subsync failed: ${(err as Error).message}`);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
mineSentence: () => {
|
|
||||||
mineSentenceCard().catch((err) => {
|
|
||||||
console.error("mineSentenceCard failed:", err);
|
|
||||||
showMpvOsd(`Mine sentence failed: ${(err as Error).message}`);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
mineSentenceMultiple: (timeoutMs) => {
|
|
||||||
startPendingMineSentenceMultiple(timeoutMs);
|
|
||||||
},
|
|
||||||
toggleSecondarySub: () => cycleSecondarySubMode(),
|
|
||||||
markAudioCard: () => {
|
|
||||||
markLastCardAsAudioCard().catch((err) => {
|
|
||||||
console.error("markLastCardAsAudioCard failed:", err);
|
|
||||||
showMpvOsd(`Audio card failed: ${(err as Error).message}`);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
openRuntimeOptions: () => {
|
|
||||||
openRuntimeOptionsPalette();
|
|
||||||
},
|
|
||||||
openJimaku: () => {
|
|
||||||
sendToVisibleOverlay("jimaku:open");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function unregisterOverlayShortcuts(): void {
|
function unregisterOverlayShortcuts(): void {
|
||||||
@@ -1690,22 +1513,9 @@ function unregisterOverlayShortcuts(): void {
|
|||||||
shortcutsRegistered = false;
|
shortcutsRegistered = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldOverlayShortcutsBeActive(): boolean {
|
function shouldOverlayShortcutsBeActive(): boolean { return overlayRuntimeInitialized; }
|
||||||
return overlayRuntimeInitialized;
|
function syncOverlayShortcuts(): void { if (shouldOverlayShortcutsBeActive()) { registerOverlayShortcuts(); } else { unregisterOverlayShortcuts(); } }
|
||||||
}
|
function refreshOverlayShortcuts(): void { unregisterOverlayShortcuts(); syncOverlayShortcuts(); }
|
||||||
|
|
||||||
function syncOverlayShortcuts(): void {
|
|
||||||
if (shouldOverlayShortcutsBeActive()) {
|
|
||||||
registerOverlayShortcuts();
|
|
||||||
} else {
|
|
||||||
unregisterOverlayShortcuts();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshOverlayShortcuts(): void {
|
|
||||||
unregisterOverlayShortcuts();
|
|
||||||
syncOverlayShortcuts();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateVisibleOverlayVisibility(): void {
|
function updateVisibleOverlayVisibility(): void {
|
||||||
updateVisibleOverlayVisibilityService({
|
updateVisibleOverlayVisibilityService({
|
||||||
@@ -1749,57 +1559,55 @@ function updateInvisibleOverlayVisibility(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function syncInvisibleOverlayMousePassthrough(): void {
|
function syncInvisibleOverlayMousePassthrough(): void {
|
||||||
if (!invisibleWindow || invisibleWindow.isDestroyed()) return;
|
syncInvisibleOverlayMousePassthroughService({
|
||||||
if (visibleOverlayVisible) {
|
hasInvisibleWindow: () => Boolean(invisibleWindow && !invisibleWindow.isDestroyed()),
|
||||||
invisibleWindow.setIgnoreMouseEvents(true, { forward: true });
|
setIgnoreMouseEvents: (ignore, extra) => {
|
||||||
} else if (invisibleOverlayVisible) {
|
if (!invisibleWindow || invisibleWindow.isDestroyed()) return;
|
||||||
invisibleWindow.setIgnoreMouseEvents(false);
|
invisibleWindow.setIgnoreMouseEvents(ignore, extra);
|
||||||
}
|
},
|
||||||
|
visibleOverlayVisible,
|
||||||
|
invisibleOverlayVisible,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setVisibleOverlayVisible(visible: boolean): void {
|
function setVisibleOverlayVisible(visible: boolean): void {
|
||||||
visibleOverlayVisible = visible;
|
setVisibleOverlayVisibleService({
|
||||||
updateVisibleOverlayVisibility();
|
visible,
|
||||||
updateInvisibleOverlayVisibility();
|
setVisibleOverlayVisibleState: (nextVisible) => {
|
||||||
syncInvisibleOverlayMousePassthrough();
|
visibleOverlayVisible = nextVisible;
|
||||||
if (
|
},
|
||||||
shouldBindVisibleOverlayToMpvSubVisibility() &&
|
updateVisibleOverlayVisibility: () => updateVisibleOverlayVisibility(),
|
||||||
mpvClient &&
|
updateInvisibleOverlayVisibility: () => updateInvisibleOverlayVisibility(),
|
||||||
mpvClient.connected
|
syncInvisibleOverlayMousePassthrough: () =>
|
||||||
) {
|
syncInvisibleOverlayMousePassthrough(),
|
||||||
mpvClient.setSubVisibility(!visible);
|
shouldBindVisibleOverlayToMpvSubVisibility: () =>
|
||||||
}
|
shouldBindVisibleOverlayToMpvSubVisibility(),
|
||||||
|
isMpvConnected: () => Boolean(mpvClient && mpvClient.connected),
|
||||||
|
setMpvSubVisibility: (mpvSubVisible) => {
|
||||||
|
if (mpvClient) {
|
||||||
|
mpvClient.setSubVisibility(mpvSubVisible);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setInvisibleOverlayVisible(visible: boolean): void {
|
function setInvisibleOverlayVisible(visible: boolean): void {
|
||||||
invisibleOverlayVisible = visible;
|
setInvisibleOverlayVisibleService({
|
||||||
updateInvisibleOverlayVisibility();
|
visible,
|
||||||
syncInvisibleOverlayMousePassthrough();
|
setInvisibleOverlayVisibleState: (nextVisible) => {
|
||||||
|
invisibleOverlayVisible = nextVisible;
|
||||||
|
},
|
||||||
|
updateInvisibleOverlayVisibility: () => updateInvisibleOverlayVisibility(),
|
||||||
|
syncInvisibleOverlayMousePassthrough: () =>
|
||||||
|
syncInvisibleOverlayMousePassthrough(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleVisibleOverlay(): void {
|
function toggleVisibleOverlay(): void { setVisibleOverlayVisible(!visibleOverlayVisible); }
|
||||||
setVisibleOverlayVisible(!visibleOverlayVisible);
|
function toggleInvisibleOverlay(): void { setInvisibleOverlayVisible(!invisibleOverlayVisible); }
|
||||||
}
|
function setOverlayVisible(visible: boolean): void { setVisibleOverlayVisible(visible); }
|
||||||
|
function toggleOverlay(): void { toggleVisibleOverlay(); }
|
||||||
function toggleInvisibleOverlay(): void {
|
function handleOverlayModalClosed(modal: OverlayHostedModal): void { if (!restoreVisibleOverlayOnModalClose.has(modal)) return; restoreVisibleOverlayOnModalClose.delete(modal); if (restoreVisibleOverlayOnModalClose.size === 0) { setVisibleOverlayVisible(false); } }
|
||||||
setInvisibleOverlayVisible(!invisibleOverlayVisible);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setOverlayVisible(visible: boolean): void {
|
|
||||||
setVisibleOverlayVisible(visible);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleOverlay(): void {
|
|
||||||
toggleVisibleOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOverlayModalClosed(modal: OverlayHostedModal): void {
|
|
||||||
if (!restoreVisibleOverlayOnModalClose.has(modal)) return;
|
|
||||||
restoreVisibleOverlayOnModalClose.delete(modal);
|
|
||||||
if (restoreVisibleOverlayOnModalClose.size === 0) {
|
|
||||||
setVisibleOverlayVisible(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMpvCommandFromIpc(command: (string | number)[]): void {
|
function handleMpvCommandFromIpc(command: (string | number)[]): void {
|
||||||
handleMpvCommandFromIpcService(command, {
|
handleMpvCommandFromIpcService(command, {
|
||||||
@@ -1915,172 +1723,51 @@ registerIpcHandlersService({
|
|||||||
* Supports both file paths (preferred on Linux/Wayland) and data URLs (fallback).
|
* Supports both file paths (preferred on Linux/Wayland) and data URLs (fallback).
|
||||||
*/
|
*/
|
||||||
function createFieldGroupingCallback() {
|
function createFieldGroupingCallback() {
|
||||||
return async (
|
return createFieldGroupingCallbackService({
|
||||||
data: KikuFieldGroupingRequestData,
|
getVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||||
): Promise<KikuFieldGroupingChoice> => {
|
getInvisibleOverlayVisible: () => invisibleOverlayVisible,
|
||||||
return new Promise((resolve) => {
|
|
||||||
const previousVisibleOverlay = visibleOverlayVisible;
|
|
||||||
const previousInvisibleOverlay = invisibleOverlayVisible;
|
|
||||||
let settled = false;
|
|
||||||
|
|
||||||
const finish = (choice: KikuFieldGroupingChoice): void => {
|
|
||||||
if (settled) return;
|
|
||||||
settled = true;
|
|
||||||
fieldGroupingResolver = null;
|
|
||||||
resolve(choice);
|
|
||||||
|
|
||||||
if (!previousVisibleOverlay && visibleOverlayVisible) {
|
|
||||||
setVisibleOverlayVisible(false);
|
|
||||||
}
|
|
||||||
if (invisibleOverlayVisible !== previousInvisibleOverlay) {
|
|
||||||
setInvisibleOverlayVisible(previousInvisibleOverlay);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fieldGroupingResolver = finish;
|
|
||||||
if (!sendToVisibleOverlay("kiku:field-grouping-request", data)) {
|
|
||||||
finish({
|
|
||||||
keepNoteId: 0,
|
|
||||||
deleteNoteId: 0,
|
|
||||||
deleteDuplicate: true,
|
|
||||||
cancelled: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!settled) {
|
|
||||||
finish({
|
|
||||||
keepNoteId: 0,
|
|
||||||
deleteNoteId: 0,
|
|
||||||
deleteDuplicate: true,
|
|
||||||
cancelled: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 90000);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendToVisibleOverlay(
|
|
||||||
channel: string,
|
|
||||||
payload?: unknown,
|
|
||||||
options?: { restoreOnModalClose?: OverlayHostedModal },
|
|
||||||
): boolean {
|
|
||||||
return sendToVisibleOverlayService({
|
|
||||||
mainWindow,
|
|
||||||
visibleOverlayVisible,
|
|
||||||
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
||||||
channel,
|
setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible),
|
||||||
payload,
|
getResolver: () => fieldGroupingResolver,
|
||||||
restoreOnModalClose: options?.restoreOnModalClose,
|
setResolver: (resolver) => {
|
||||||
addRestoreFlag: (modal) =>
|
fieldGroupingResolver = resolver;
|
||||||
restoreVisibleOverlayOnModalClose.add(modal as OverlayHostedModal),
|
},
|
||||||
|
sendRequestToVisibleOverlay: (data) =>
|
||||||
|
sendToVisibleOverlay("kiku:field-grouping-request", data),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerAnkiJimakuIpcHandlers({
|
function sendToVisibleOverlay(channel: string, payload?: unknown, options?: { restoreOnModalClose?: OverlayHostedModal }): boolean { return sendToVisibleOverlayService({ mainWindow, visibleOverlayVisible, setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible), channel, payload, restoreOnModalClose: options?.restoreOnModalClose, addRestoreFlag: (modal) => restoreVisibleOverlayOnModalClose.add(modal as OverlayHostedModal) }); }
|
||||||
setAnkiConnectEnabled: (enabled) => {
|
|
||||||
|
registerAnkiJimakuIpcRuntimeService({
|
||||||
|
patchAnkiConnectEnabled: (enabled) => {
|
||||||
configService.patchRawConfig({
|
configService.patchRawConfig({
|
||||||
ankiConnect: {
|
ankiConnect: {
|
||||||
enabled,
|
enabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const config = getResolvedConfig();
|
|
||||||
|
|
||||||
if (enabled && !ankiIntegration && subtitleTimingTracker && mpvClient) {
|
|
||||||
const effectiveAnkiConfig = runtimeOptionsManager
|
|
||||||
? runtimeOptionsManager.getEffectiveAnkiConnectConfig(
|
|
||||||
config.ankiConnect,
|
|
||||||
)
|
|
||||||
: config.ankiConnect;
|
|
||||||
ankiIntegration = new AnkiIntegration(
|
|
||||||
effectiveAnkiConfig,
|
|
||||||
subtitleTimingTracker,
|
|
||||||
mpvClient,
|
|
||||||
(text: string) => {
|
|
||||||
if (mpvClient) {
|
|
||||||
mpvClient.send({
|
|
||||||
command: ["show-text", text, "3000"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
showDesktopNotification,
|
|
||||||
createFieldGroupingCallback(),
|
|
||||||
);
|
|
||||||
ankiIntegration.start();
|
|
||||||
console.log("AnkiConnect integration enabled");
|
|
||||||
} else if (!enabled && ankiIntegration) {
|
|
||||||
ankiIntegration.destroy();
|
|
||||||
ankiIntegration = null;
|
|
||||||
console.log("AnkiConnect integration disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcastRuntimeOptionsChanged();
|
|
||||||
},
|
},
|
||||||
clearAnkiHistory: () => {
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
if (subtitleTimingTracker) {
|
getRuntimeOptionsManager: () => runtimeOptionsManager,
|
||||||
subtitleTimingTracker.cleanup();
|
getSubtitleTimingTracker: () => subtitleTimingTracker,
|
||||||
console.log("AnkiConnect subtitle timing history cleared");
|
getMpvClient: () => mpvClient,
|
||||||
}
|
getAnkiIntegration: () => ankiIntegration,
|
||||||
|
setAnkiIntegration: (integration) => {
|
||||||
|
ankiIntegration = integration;
|
||||||
},
|
},
|
||||||
respondFieldGrouping: (choice) => {
|
showDesktopNotification,
|
||||||
if (fieldGroupingResolver) {
|
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
||||||
fieldGroupingResolver(choice);
|
broadcastRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
||||||
fieldGroupingResolver = null;
|
getFieldGroupingResolver: () => fieldGroupingResolver,
|
||||||
}
|
setFieldGroupingResolver: (resolver) => {
|
||||||
|
fieldGroupingResolver = resolver;
|
||||||
},
|
},
|
||||||
buildKikuMergePreview: async (request) => {
|
parseMediaInfo: (mediaPath) => parseMediaInfo(mediaPath),
|
||||||
if (!ankiIntegration) {
|
|
||||||
return { ok: false, error: "AnkiConnect integration not enabled" };
|
|
||||||
}
|
|
||||||
return ankiIntegration.buildFieldGroupingPreview(
|
|
||||||
request.keepNoteId,
|
|
||||||
request.deleteNoteId,
|
|
||||||
request.deleteDuplicate,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
getJimakuMediaInfo: () => parseMediaInfo(currentMediaPath),
|
|
||||||
searchJimakuEntries: async (query) => {
|
|
||||||
console.log(`[jimaku] search-entries query: "${query.query}"`);
|
|
||||||
const response = await jimakuFetchJson<JimakuEntry[]>(
|
|
||||||
"/api/entries/search",
|
|
||||||
{
|
|
||||||
anime: true,
|
|
||||||
query: query.query,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (!response.ok) return response;
|
|
||||||
const maxResults = getJimakuMaxEntryResults();
|
|
||||||
console.log(
|
|
||||||
`[jimaku] search-entries returned ${response.data.length} results (capped to ${maxResults})`,
|
|
||||||
);
|
|
||||||
return { ok: true, data: response.data.slice(0, maxResults) };
|
|
||||||
},
|
|
||||||
listJimakuFiles: async (query) => {
|
|
||||||
console.log(
|
|
||||||
`[jimaku] list-files entryId=${query.entryId} episode=${query.episode ?? "all"}`,
|
|
||||||
);
|
|
||||||
const response = await jimakuFetchJson<JimakuFileEntry[]>(
|
|
||||||
`/api/entries/${query.entryId}/files`,
|
|
||||||
{
|
|
||||||
episode: query.episode ?? undefined,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (!response.ok) return response;
|
|
||||||
const sorted = sortJimakuFiles(
|
|
||||||
response.data,
|
|
||||||
getJimakuLanguagePreference(),
|
|
||||||
);
|
|
||||||
console.log(`[jimaku] list-files returned ${sorted.length} files`);
|
|
||||||
return { ok: true, data: sorted };
|
|
||||||
},
|
|
||||||
resolveJimakuApiKey: () => resolveJimakuApiKey(),
|
|
||||||
getCurrentMediaPath: () => currentMediaPath,
|
getCurrentMediaPath: () => currentMediaPath,
|
||||||
|
jimakuFetchJson: (endpoint, query) => jimakuFetchJson(endpoint, query),
|
||||||
|
getJimakuMaxEntryResults: () => getJimakuMaxEntryResults(),
|
||||||
|
getJimakuLanguagePreference: () => getJimakuLanguagePreference(),
|
||||||
|
resolveJimakuApiKey: () => resolveJimakuApiKey(),
|
||||||
isRemoteMediaPath: (mediaPath) => isRemoteMediaPath(mediaPath),
|
isRemoteMediaPath: (mediaPath) => isRemoteMediaPath(mediaPath),
|
||||||
downloadToFile: (url, destPath, headers) => downloadToFile(url, destPath, headers),
|
downloadToFile: (url, destPath, headers) => downloadToFile(url, destPath, headers),
|
||||||
onDownloadedSubtitle: (pathToSubtitle) => {
|
|
||||||
if (mpvClient && mpvClient.connected) {
|
|
||||||
mpvClient.send({ command: ["sub-add", pathToSubtitle, "select"] });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user