mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
refactor: extract jimaku, subtitle position, and render metric services
This commit is contained in:
81
src/core/services/jimaku-runtime-service.ts
Normal file
81
src/core/services/jimaku-runtime-service.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import {
|
||||||
|
JimakuApiResponse,
|
||||||
|
JimakuConfig,
|
||||||
|
JimakuLanguagePreference,
|
||||||
|
} from "../../types";
|
||||||
|
import {
|
||||||
|
jimakuFetchJson as jimakuFetchJsonRequest,
|
||||||
|
resolveJimakuApiKey as resolveJimakuApiKeyFromConfig,
|
||||||
|
} from "../../jimaku/utils";
|
||||||
|
|
||||||
|
export function getJimakuConfigService(
|
||||||
|
getResolvedConfig: () => { jimaku?: JimakuConfig },
|
||||||
|
): JimakuConfig {
|
||||||
|
const config = getResolvedConfig();
|
||||||
|
return config.jimaku ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getJimakuBaseUrlService(
|
||||||
|
getResolvedConfig: () => { jimaku?: JimakuConfig },
|
||||||
|
defaultBaseUrl: string,
|
||||||
|
): string {
|
||||||
|
const config = getJimakuConfigService(getResolvedConfig);
|
||||||
|
return config.apiBaseUrl || defaultBaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getJimakuLanguagePreferenceService(
|
||||||
|
getResolvedConfig: () => { jimaku?: JimakuConfig },
|
||||||
|
defaultPreference: JimakuLanguagePreference,
|
||||||
|
): JimakuLanguagePreference {
|
||||||
|
const config = getJimakuConfigService(getResolvedConfig);
|
||||||
|
return config.languagePreference || defaultPreference;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getJimakuMaxEntryResultsService(
|
||||||
|
getResolvedConfig: () => { jimaku?: JimakuConfig },
|
||||||
|
defaultValue: number,
|
||||||
|
): number {
|
||||||
|
const config = getJimakuConfigService(getResolvedConfig);
|
||||||
|
const value = config.maxEntryResults;
|
||||||
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
||||||
|
return Math.floor(value);
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveJimakuApiKeyService(
|
||||||
|
getResolvedConfig: () => { jimaku?: JimakuConfig },
|
||||||
|
): Promise<string | null> {
|
||||||
|
return resolveJimakuApiKeyFromConfig(getJimakuConfigService(getResolvedConfig));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function jimakuFetchJsonService<T>(
|
||||||
|
endpoint: string,
|
||||||
|
query: Record<string, string | number | boolean | null | undefined> = {},
|
||||||
|
options: {
|
||||||
|
getResolvedConfig: () => { jimaku?: JimakuConfig };
|
||||||
|
defaultBaseUrl: string;
|
||||||
|
defaultMaxEntryResults: number;
|
||||||
|
defaultLanguagePreference: JimakuLanguagePreference;
|
||||||
|
},
|
||||||
|
): Promise<JimakuApiResponse<T>> {
|
||||||
|
const apiKey = await resolveJimakuApiKeyService(options.getResolvedConfig);
|
||||||
|
if (!apiKey) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
error:
|
||||||
|
"Jimaku API key not set. Configure jimaku.apiKey or jimaku.apiKeyCommand.",
|
||||||
|
code: 401,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return jimakuFetchJsonRequest<T>(endpoint, query, {
|
||||||
|
baseUrl: getJimakuBaseUrlService(
|
||||||
|
options.getResolvedConfig,
|
||||||
|
options.defaultBaseUrl,
|
||||||
|
),
|
||||||
|
apiKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
57
src/core/services/mpv-render-metrics-service.ts
Normal file
57
src/core/services/mpv-render-metrics-service.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { MpvSubtitleRenderMetrics } from "../../types";
|
||||||
|
import { asBoolean, asFiniteNumber, asString } from "../utils/coerce";
|
||||||
|
|
||||||
|
export function updateMpvSubtitleRenderMetricsService(
|
||||||
|
current: MpvSubtitleRenderMetrics,
|
||||||
|
patch: Partial<MpvSubtitleRenderMetrics>,
|
||||||
|
): MpvSubtitleRenderMetrics {
|
||||||
|
const patchOsd = patch.osdDimensions;
|
||||||
|
const nextOsdDimensions =
|
||||||
|
patchOsd &&
|
||||||
|
typeof patchOsd.w === "number" &&
|
||||||
|
typeof patchOsd.h === "number" &&
|
||||||
|
typeof patchOsd.ml === "number" &&
|
||||||
|
typeof patchOsd.mr === "number" &&
|
||||||
|
typeof patchOsd.mt === "number" &&
|
||||||
|
typeof patchOsd.mb === "number"
|
||||||
|
? {
|
||||||
|
w: asFiniteNumber(patchOsd.w, 0, 1, 100000),
|
||||||
|
h: asFiniteNumber(patchOsd.h, 0, 1, 100000),
|
||||||
|
ml: asFiniteNumber(patchOsd.ml, 0, 0, 100000),
|
||||||
|
mr: asFiniteNumber(patchOsd.mr, 0, 0, 100000),
|
||||||
|
mt: asFiniteNumber(patchOsd.mt, 0, 0, 100000),
|
||||||
|
mb: asFiniteNumber(patchOsd.mb, 0, 0, 100000),
|
||||||
|
}
|
||||||
|
: patchOsd === null
|
||||||
|
? null
|
||||||
|
: current.osdDimensions;
|
||||||
|
|
||||||
|
return {
|
||||||
|
subPos: asFiniteNumber(patch.subPos, current.subPos, 0, 150),
|
||||||
|
subFontSize: asFiniteNumber(patch.subFontSize, current.subFontSize, 1, 200),
|
||||||
|
subScale: asFiniteNumber(patch.subScale, current.subScale, 0.1, 10),
|
||||||
|
subMarginY: asFiniteNumber(patch.subMarginY, current.subMarginY, 0, 200),
|
||||||
|
subMarginX: asFiniteNumber(patch.subMarginX, current.subMarginX, 0, 200),
|
||||||
|
subFont: asString(patch.subFont, current.subFont),
|
||||||
|
subSpacing: asFiniteNumber(patch.subSpacing, current.subSpacing, -100, 100),
|
||||||
|
subBold: asBoolean(patch.subBold, current.subBold),
|
||||||
|
subItalic: asBoolean(patch.subItalic, current.subItalic),
|
||||||
|
subBorderSize: asFiniteNumber(
|
||||||
|
patch.subBorderSize,
|
||||||
|
current.subBorderSize,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
),
|
||||||
|
subShadowOffset: asFiniteNumber(
|
||||||
|
patch.subShadowOffset,
|
||||||
|
current.subShadowOffset,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
),
|
||||||
|
subAssOverride: asString(patch.subAssOverride, current.subAssOverride),
|
||||||
|
subScaleByWindow: asBoolean(patch.subScaleByWindow, current.subScaleByWindow),
|
||||||
|
subUseMargins: asBoolean(patch.subUseMargins, current.subUseMargins),
|
||||||
|
osdHeight: asFiniteNumber(patch.osdHeight, current.osdHeight, 1, 10000),
|
||||||
|
osdDimensions: nextOsdDimensions,
|
||||||
|
};
|
||||||
|
}
|
||||||
154
src/core/services/subtitle-position-service.ts
Normal file
154
src/core/services/subtitle-position-service.ts
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import * as crypto from "crypto";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import { SubtitlePosition } from "../../types";
|
||||||
|
|
||||||
|
function getSubtitlePositionFilePath(
|
||||||
|
mediaPath: string,
|
||||||
|
subtitlePositionsDir: string,
|
||||||
|
): string {
|
||||||
|
const key = normalizeMediaPathForSubtitlePosition(mediaPath);
|
||||||
|
const hash = crypto.createHash("sha256").update(key).digest("hex");
|
||||||
|
return path.join(subtitlePositionsDir, `${hash}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMediaPathForSubtitlePosition(mediaPath: string): string {
|
||||||
|
const trimmed = mediaPath.trim();
|
||||||
|
if (!trimmed) return trimmed;
|
||||||
|
|
||||||
|
if (
|
||||||
|
/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(trimmed) ||
|
||||||
|
/^ytsearch:/.test(trimmed)
|
||||||
|
) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = path.resolve(trimmed);
|
||||||
|
let normalized = resolved;
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(resolved)) {
|
||||||
|
normalized = fs.realpathSync(resolved);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
normalized = resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
normalized = normalized.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistSubtitlePosition(
|
||||||
|
position: SubtitlePosition,
|
||||||
|
currentMediaPath: string | null,
|
||||||
|
subtitlePositionsDir: string,
|
||||||
|
): void {
|
||||||
|
if (!currentMediaPath) return;
|
||||||
|
if (!fs.existsSync(subtitlePositionsDir)) {
|
||||||
|
fs.mkdirSync(subtitlePositionsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
const positionPath = getSubtitlePositionFilePath(
|
||||||
|
currentMediaPath,
|
||||||
|
subtitlePositionsDir,
|
||||||
|
);
|
||||||
|
fs.writeFileSync(positionPath, JSON.stringify(position, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadSubtitlePositionService(options: {
|
||||||
|
currentMediaPath: string | null;
|
||||||
|
fallbackPosition: SubtitlePosition;
|
||||||
|
} & { subtitlePositionsDir: string }): SubtitlePosition | null {
|
||||||
|
if (!options.currentMediaPath) {
|
||||||
|
return options.fallbackPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const positionPath = getSubtitlePositionFilePath(
|
||||||
|
options.currentMediaPath,
|
||||||
|
options.subtitlePositionsDir,
|
||||||
|
);
|
||||||
|
if (!fs.existsSync(positionPath)) {
|
||||||
|
return options.fallbackPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = fs.readFileSync(positionPath, "utf-8");
|
||||||
|
const parsed = JSON.parse(data) as Partial<SubtitlePosition>;
|
||||||
|
if (
|
||||||
|
parsed &&
|
||||||
|
typeof parsed.yPercent === "number" &&
|
||||||
|
Number.isFinite(parsed.yPercent)
|
||||||
|
) {
|
||||||
|
return { yPercent: parsed.yPercent };
|
||||||
|
}
|
||||||
|
return options.fallbackPosition;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load subtitle position:", (err as Error).message);
|
||||||
|
return options.fallbackPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveSubtitlePositionService(options: {
|
||||||
|
position: SubtitlePosition;
|
||||||
|
currentMediaPath: string | null;
|
||||||
|
subtitlePositionsDir: string;
|
||||||
|
onQueuePending: (position: SubtitlePosition) => void;
|
||||||
|
onPersisted: () => void;
|
||||||
|
}): void {
|
||||||
|
if (!options.currentMediaPath) {
|
||||||
|
options.onQueuePending(options.position);
|
||||||
|
console.warn("Queued subtitle position save - no media path yet");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
persistSubtitlePosition(
|
||||||
|
options.position,
|
||||||
|
options.currentMediaPath,
|
||||||
|
options.subtitlePositionsDir,
|
||||||
|
);
|
||||||
|
options.onPersisted();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to save subtitle position:", (err as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateCurrentMediaPathService(options: {
|
||||||
|
mediaPath: unknown;
|
||||||
|
currentMediaPath: string | null;
|
||||||
|
pendingSubtitlePosition: SubtitlePosition | null;
|
||||||
|
subtitlePositionsDir: string;
|
||||||
|
loadSubtitlePosition: () => SubtitlePosition | null;
|
||||||
|
setCurrentMediaPath: (mediaPath: string | null) => void;
|
||||||
|
clearPendingSubtitlePosition: () => void;
|
||||||
|
setSubtitlePosition: (position: SubtitlePosition | null) => void;
|
||||||
|
broadcastSubtitlePosition: (position: SubtitlePosition | null) => void;
|
||||||
|
}): void {
|
||||||
|
const nextPath =
|
||||||
|
typeof options.mediaPath === "string" && options.mediaPath.trim().length > 0
|
||||||
|
? options.mediaPath
|
||||||
|
: null;
|
||||||
|
if (nextPath === options.currentMediaPath) return;
|
||||||
|
options.setCurrentMediaPath(nextPath);
|
||||||
|
|
||||||
|
if (nextPath && options.pendingSubtitlePosition) {
|
||||||
|
try {
|
||||||
|
persistSubtitlePosition(
|
||||||
|
options.pendingSubtitlePosition,
|
||||||
|
nextPath,
|
||||||
|
options.subtitlePositionsDir,
|
||||||
|
);
|
||||||
|
options.setSubtitlePosition(options.pendingSubtitlePosition);
|
||||||
|
options.clearPendingSubtitlePosition();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
"Failed to persist queued subtitle position:",
|
||||||
|
(err as Error).message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = options.loadSubtitlePosition();
|
||||||
|
options.broadcastSubtitlePosition(position);
|
||||||
|
}
|
||||||
460
src/main.ts
460
src/main.ts
@@ -49,7 +49,6 @@ import { MecabTokenizer } from "./mecab-tokenizer";
|
|||||||
import { mergeTokens } from "./token-merger";
|
import { mergeTokens } from "./token-merger";
|
||||||
import { BaseWindowTracker } from "./window-trackers";
|
import { BaseWindowTracker } from "./window-trackers";
|
||||||
import {
|
import {
|
||||||
Config,
|
|
||||||
JimakuApiResponse,
|
JimakuApiResponse,
|
||||||
JimakuDownloadResult,
|
JimakuDownloadResult,
|
||||||
JimakuMediaInfo,
|
JimakuMediaInfo,
|
||||||
@@ -78,9 +77,7 @@ import { RuntimeOptionsManager } from "./runtime-options";
|
|||||||
import {
|
import {
|
||||||
downloadToFile,
|
downloadToFile,
|
||||||
isRemoteMediaPath,
|
isRemoteMediaPath,
|
||||||
jimakuFetchJson as jimakuFetchJsonRequest,
|
|
||||||
parseMediaInfo,
|
parseMediaInfo,
|
||||||
resolveJimakuApiKey as resolveJimakuApiKeyFromConfig,
|
|
||||||
} from "./jimaku/utils";
|
} from "./jimaku/utils";
|
||||||
import {
|
import {
|
||||||
getSubsyncConfig,
|
getSubsyncConfig,
|
||||||
@@ -123,6 +120,17 @@ 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 {
|
||||||
|
getJimakuLanguagePreferenceService,
|
||||||
|
getJimakuMaxEntryResultsService,
|
||||||
|
jimakuFetchJsonService,
|
||||||
|
resolveJimakuApiKeyService,
|
||||||
|
} from "./core/services/jimaku-runtime-service";
|
||||||
|
import {
|
||||||
|
loadSubtitlePositionService,
|
||||||
|
saveSubtitlePositionService,
|
||||||
|
updateCurrentMediaPathService,
|
||||||
|
} from "./core/services/subtitle-position-service";
|
||||||
import {
|
import {
|
||||||
createOverlayWindowService,
|
createOverlayWindowService,
|
||||||
enforceOverlayLayerOrderService,
|
enforceOverlayLayerOrderService,
|
||||||
@@ -140,6 +148,7 @@ import {
|
|||||||
MpvIpcClient,
|
MpvIpcClient,
|
||||||
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
||||||
} from "./core/services/mpv-service";
|
} from "./core/services/mpv-service";
|
||||||
|
import { updateMpvSubtitleRenderMetricsService } from "./core/services/mpv-render-metrics-service";
|
||||||
import {
|
import {
|
||||||
handleMpvCommandFromIpcService,
|
handleMpvCommandFromIpcService,
|
||||||
runSubsyncManualFromIpcService,
|
runSubsyncManualFromIpcService,
|
||||||
@@ -261,29 +270,7 @@ const restoreVisibleOverlayOnModalClose = new Set<OverlayHostedModal>();
|
|||||||
|
|
||||||
const SUBTITLE_POSITIONS_DIR = path.join(CONFIG_DIR, "subtitle-positions");
|
const SUBTITLE_POSITIONS_DIR = path.join(CONFIG_DIR, "subtitle-positions");
|
||||||
|
|
||||||
interface LoadConfigResult {
|
function getRuntimeOptionsState(): RuntimeOptionState[] { if (!runtimeOptionsManager) return []; return runtimeOptionsManager.listOptions(); }
|
||||||
success: boolean;
|
|
||||||
config: Config;
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadConfig(): LoadConfigResult {
|
|
||||||
const config = configService.getRawConfig();
|
|
||||||
return { success: true, config };
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveConfig(config: Config): void {
|
|
||||||
try {
|
|
||||||
configService.saveRawConfig(config);
|
|
||||||
configService.reloadConfig();
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to save config:", (err as Error).message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRuntimeOptionsState(): RuntimeOptionState[] {
|
|
||||||
if (!runtimeOptionsManager) return [];
|
|
||||||
return runtimeOptionsManager.listOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOverlayWindows(): BrowserWindow[] {
|
function getOverlayWindows(): BrowserWindow[] {
|
||||||
const windows: BrowserWindow[] = [];
|
const windows: BrowserWindow[] = [];
|
||||||
@@ -302,21 +289,9 @@ function broadcastToOverlayWindows(channel: string, ...args: unknown[]): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function broadcastRuntimeOptionsChanged(): void {
|
function broadcastRuntimeOptionsChanged(): void { broadcastToOverlayWindows("runtime-options:changed", getRuntimeOptionsState()); }
|
||||||
broadcastToOverlayWindows(
|
|
||||||
"runtime-options:changed",
|
|
||||||
getRuntimeOptionsState(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setOverlayDebugVisualizationEnabled(enabled: boolean): void {
|
function setOverlayDebugVisualizationEnabled(enabled: boolean): void { if (overlayDebugVisualizationEnabled === enabled) return; overlayDebugVisualizationEnabled = enabled; broadcastToOverlayWindows("overlay-debug-visualization:set", overlayDebugVisualizationEnabled); }
|
||||||
if (overlayDebugVisualizationEnabled === enabled) return;
|
|
||||||
overlayDebugVisualizationEnabled = enabled;
|
|
||||||
broadcastToOverlayWindows(
|
|
||||||
"overlay-debug-visualization:set",
|
|
||||||
overlayDebugVisualizationEnabled,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyRuntimeOptionResult(
|
function applyRuntimeOptionResult(
|
||||||
result: RuntimeOptionApplyResult,
|
result: RuntimeOptionApplyResult,
|
||||||
@@ -327,34 +302,15 @@ function applyRuntimeOptionResult(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openRuntimeOptionsPalette(): void {
|
function openRuntimeOptionsPalette(): void { sendToVisibleOverlay("runtime-options:open", undefined, { restoreOnModalClose: "runtime-options" }); }
|
||||||
sendToVisibleOverlay("runtime-options:open", undefined, {
|
|
||||||
restoreOnModalClose: "runtime-options",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getResolvedConfig() {
|
function getResolvedConfig() { return configService.getConfig(); }
|
||||||
return configService.getConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInitialInvisibleOverlayVisibility(): boolean {
|
function getInitialInvisibleOverlayVisibility(): boolean { const visibility = getResolvedConfig().invisibleOverlay.startupVisibility; if (visibility === "visible") return true; if (visibility === "hidden") return false; if (process.platform === "linux") return false; return true; }
|
||||||
const visibility = getResolvedConfig().invisibleOverlay.startupVisibility;
|
|
||||||
if (visibility === "visible") return true;
|
|
||||||
if (visibility === "hidden") return false;
|
|
||||||
if (process.platform === "linux") return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldAutoInitializeOverlayRuntimeFromConfig(): boolean {
|
function shouldAutoInitializeOverlayRuntimeFromConfig(): boolean { const config = getResolvedConfig(); if (config.auto_start_overlay === true) return true; if (config.invisibleOverlay.startupVisibility === "visible") return true; return false; }
|
||||||
const config = getResolvedConfig();
|
|
||||||
if (config.auto_start_overlay === true) return true;
|
|
||||||
if (config.invisibleOverlay.startupVisibility === "visible") return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldBindVisibleOverlayToMpvSubVisibility(): boolean {
|
function shouldBindVisibleOverlayToMpvSubVisibility(): boolean { return getResolvedConfig().bind_visible_overlay_to_mpv_sub_visibility; }
|
||||||
return getResolvedConfig().bind_visible_overlay_to_mpv_sub_visibility;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAutoUpdateEnabledRuntime(): boolean {
|
function isAutoUpdateEnabledRuntime(): boolean {
|
||||||
const value = runtimeOptionsManager?.getOptionValue(
|
const value = runtimeOptionsManager?.getOptionValue(
|
||||||
@@ -365,171 +321,68 @@ function isAutoUpdateEnabledRuntime(): boolean {
|
|||||||
return config.ankiConnect?.behavior?.autoUpdateNewCards !== false;
|
return config.ankiConnect?.behavior?.autoUpdateNewCards !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getJimakuConfig(): JimakuConfig {
|
function getJimakuLanguagePreference(): JimakuLanguagePreference { return getJimakuLanguagePreferenceService(() => getResolvedConfig(), DEFAULT_CONFIG.jimaku.languagePreference); }
|
||||||
const config = getResolvedConfig();
|
|
||||||
return config.jimaku ?? {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getJimakuBaseUrl(): string {
|
function getJimakuMaxEntryResults(): number { return getJimakuMaxEntryResultsService(() => getResolvedConfig(), DEFAULT_CONFIG.jimaku.maxEntryResults); }
|
||||||
const config = getJimakuConfig();
|
|
||||||
return config.apiBaseUrl || DEFAULT_CONFIG.jimaku.apiBaseUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getJimakuLanguagePreference(): JimakuLanguagePreference {
|
async function resolveJimakuApiKey(): Promise<string | null> { return resolveJimakuApiKeyService(() => getResolvedConfig()); }
|
||||||
const config = getJimakuConfig();
|
|
||||||
return config.languagePreference || DEFAULT_CONFIG.jimaku.languagePreference;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getJimakuMaxEntryResults(): number {
|
|
||||||
const config = getJimakuConfig();
|
|
||||||
const value = config.maxEntryResults;
|
|
||||||
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
||||||
return Math.floor(value);
|
|
||||||
}
|
|
||||||
return DEFAULT_CONFIG.jimaku.maxEntryResults;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveJimakuApiKey(): Promise<string | null> {
|
|
||||||
return resolveJimakuApiKeyFromConfig(getJimakuConfig());
|
|
||||||
}
|
|
||||||
|
|
||||||
async function jimakuFetchJson<T>(
|
async function jimakuFetchJson<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
query: Record<string, string | number | boolean | null | undefined> = {},
|
query: Record<string, string | number | boolean | null | undefined> = {},
|
||||||
): Promise<JimakuApiResponse<T>> {
|
): Promise<JimakuApiResponse<T>> {
|
||||||
const apiKey = await resolveJimakuApiKey();
|
return jimakuFetchJsonService<T>(endpoint, query, {
|
||||||
if (!apiKey) {
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
return {
|
defaultBaseUrl: DEFAULT_CONFIG.jimaku.apiBaseUrl,
|
||||||
ok: false,
|
defaultMaxEntryResults: DEFAULT_CONFIG.jimaku.maxEntryResults,
|
||||||
error: {
|
defaultLanguagePreference: DEFAULT_CONFIG.jimaku.languagePreference,
|
||||||
error:
|
|
||||||
"Jimaku API key not set. Configure jimaku.apiKey or jimaku.apiKeyCommand.",
|
|
||||||
code: 401,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return jimakuFetchJsonRequest<T>(endpoint, query, {
|
|
||||||
baseUrl: getJimakuBaseUrl(),
|
|
||||||
apiKey,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSubtitlePositionFilePath(mediaPath: string): string {
|
|
||||||
const key = normalizeMediaPathForSubtitlePosition(mediaPath);
|
|
||||||
const hash = crypto.createHash("sha256").update(key).digest("hex");
|
|
||||||
return path.join(SUBTITLE_POSITIONS_DIR, `${hash}.json`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeMediaPathForSubtitlePosition(mediaPath: string): string {
|
|
||||||
const trimmed = mediaPath.trim();
|
|
||||||
if (!trimmed) return trimmed;
|
|
||||||
|
|
||||||
if (
|
|
||||||
/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(trimmed) ||
|
|
||||||
/^ytsearch:/.test(trimmed)
|
|
||||||
) {
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolved = path.resolve(trimmed);
|
|
||||||
let normalized = resolved;
|
|
||||||
try {
|
|
||||||
if (fs.existsSync(resolved)) {
|
|
||||||
normalized = fs.realpathSync(resolved);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
normalized = resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.platform === "win32") {
|
|
||||||
normalized = normalized.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
function persistSubtitlePosition(position: SubtitlePosition): void {
|
|
||||||
if (!currentMediaPath) return;
|
|
||||||
if (!fs.existsSync(SUBTITLE_POSITIONS_DIR)) {
|
|
||||||
fs.mkdirSync(SUBTITLE_POSITIONS_DIR, { recursive: true });
|
|
||||||
}
|
|
||||||
const positionPath = getSubtitlePositionFilePath(currentMediaPath);
|
|
||||||
fs.writeFileSync(positionPath, JSON.stringify(position, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadSubtitlePosition(): SubtitlePosition | null {
|
function loadSubtitlePosition(): SubtitlePosition | null {
|
||||||
const fallbackPosition = getResolvedConfig().subtitlePosition;
|
subtitlePosition = loadSubtitlePositionService({
|
||||||
if (!currentMediaPath) {
|
currentMediaPath,
|
||||||
subtitlePosition = fallbackPosition;
|
fallbackPosition: getResolvedConfig().subtitlePosition,
|
||||||
return subtitlePosition;
|
subtitlePositionsDir: SUBTITLE_POSITIONS_DIR,
|
||||||
}
|
});
|
||||||
|
|
||||||
try {
|
|
||||||
const positionPath = getSubtitlePositionFilePath(currentMediaPath);
|
|
||||||
if (!fs.existsSync(positionPath)) {
|
|
||||||
subtitlePosition = fallbackPosition;
|
|
||||||
return subtitlePosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = fs.readFileSync(positionPath, "utf-8");
|
|
||||||
const parsed = JSON.parse(data) as Partial<SubtitlePosition>;
|
|
||||||
if (
|
|
||||||
parsed &&
|
|
||||||
typeof parsed.yPercent === "number" &&
|
|
||||||
Number.isFinite(parsed.yPercent)
|
|
||||||
) {
|
|
||||||
subtitlePosition = { yPercent: parsed.yPercent };
|
|
||||||
} else {
|
|
||||||
subtitlePosition = fallbackPosition;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to load subtitle position:", (err as Error).message);
|
|
||||||
subtitlePosition = fallbackPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
return subtitlePosition;
|
return subtitlePosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSubtitlePosition(position: SubtitlePosition): void {
|
function saveSubtitlePosition(position: SubtitlePosition): void {
|
||||||
subtitlePosition = position;
|
subtitlePosition = position;
|
||||||
if (!currentMediaPath) {
|
saveSubtitlePositionService({
|
||||||
pendingSubtitlePosition = position;
|
position,
|
||||||
console.warn("Queued subtitle position save - no media path yet");
|
currentMediaPath,
|
||||||
return;
|
subtitlePositionsDir: SUBTITLE_POSITIONS_DIR,
|
||||||
}
|
onQueuePending: (queued) => {
|
||||||
|
pendingSubtitlePosition = queued;
|
||||||
try {
|
},
|
||||||
persistSubtitlePosition(position);
|
onPersisted: () => {
|
||||||
pendingSubtitlePosition = null;
|
pendingSubtitlePosition = null;
|
||||||
} catch (err) {
|
},
|
||||||
console.error("Failed to save subtitle position:", (err as Error).message);
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCurrentMediaPath(mediaPath: unknown): void {
|
function updateCurrentMediaPath(mediaPath: unknown): void {
|
||||||
const nextPath =
|
updateCurrentMediaPathService({
|
||||||
typeof mediaPath === "string" && mediaPath.trim().length > 0
|
mediaPath,
|
||||||
? mediaPath
|
currentMediaPath,
|
||||||
: null;
|
pendingSubtitlePosition,
|
||||||
if (nextPath === currentMediaPath) return;
|
subtitlePositionsDir: SUBTITLE_POSITIONS_DIR,
|
||||||
|
loadSubtitlePosition: () => loadSubtitlePosition(),
|
||||||
|
setCurrentMediaPath: (nextPath) => {
|
||||||
currentMediaPath = nextPath;
|
currentMediaPath = nextPath;
|
||||||
|
},
|
||||||
if (currentMediaPath && pendingSubtitlePosition) {
|
clearPendingSubtitlePosition: () => {
|
||||||
try {
|
|
||||||
persistSubtitlePosition(pendingSubtitlePosition);
|
|
||||||
subtitlePosition = pendingSubtitlePosition;
|
|
||||||
pendingSubtitlePosition = null;
|
pendingSubtitlePosition = null;
|
||||||
} catch (err) {
|
},
|
||||||
console.error(
|
setSubtitlePosition: (position) => {
|
||||||
"Failed to persist queued subtitle position:",
|
subtitlePosition = position;
|
||||||
(err as Error).message,
|
},
|
||||||
);
|
broadcastSubtitlePosition: (position) => {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const position = loadSubtitlePosition();
|
|
||||||
broadcastToOverlayWindows("subtitle-position:set", position);
|
broadcastToOverlayWindows("subtitle-position:set", position);
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const AUTOSUBSYNC_SPINNER_FRAMES = ["|", "/", "-", "\\"];
|
const AUTOSUBSYNC_SPINNER_FRAMES = ["|", "/", "-", "\\"];
|
||||||
@@ -609,45 +462,17 @@ if (initialArgs.generateConfig && !shouldStartApp(initialArgs)) {
|
|||||||
keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS);
|
keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS);
|
||||||
|
|
||||||
mpvClient = new MpvIpcClient(mpvSocketPath, {
|
mpvClient = new MpvIpcClient(mpvSocketPath, {
|
||||||
getResolvedConfig: () => getResolvedConfig(),
|
getResolvedConfig: () => getResolvedConfig(), autoStartOverlay,
|
||||||
autoStartOverlay,
|
|
||||||
setOverlayVisible: (visible) => setOverlayVisible(visible),
|
setOverlayVisible: (visible) => setOverlayVisible(visible),
|
||||||
shouldBindVisibleOverlayToMpvSubVisibility: () =>
|
shouldBindVisibleOverlayToMpvSubVisibility: () => shouldBindVisibleOverlayToMpvSubVisibility(),
|
||||||
shouldBindVisibleOverlayToMpvSubVisibility(),
|
|
||||||
isVisibleOverlayVisible: () => visibleOverlayVisible,
|
isVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||||
getReconnectTimer: () => reconnectTimer,
|
getReconnectTimer: () => reconnectTimer, setReconnectTimer: (timer) => { reconnectTimer = timer; },
|
||||||
setReconnectTimer: (timer) => {
|
getCurrentSubText: () => currentSubText, setCurrentSubText: (text) => { currentSubText = text; }, setCurrentSubAssText: (text) => { currentSubAssText = text; },
|
||||||
reconnectTimer = timer;
|
getSubtitleTimingTracker: () => subtitleTimingTracker, subtitleWsBroadcast: (text) => { subtitleWsService.broadcast(text); },
|
||||||
},
|
getOverlayWindowsCount: () => getOverlayWindows().length, tokenizeSubtitle: (text) => tokenizeSubtitle(text),
|
||||||
getCurrentSubText: () => currentSubText,
|
broadcastToOverlayWindows: (channel, ...args) => { broadcastToOverlayWindows(channel, ...args); },
|
||||||
setCurrentSubText: (text) => {
|
updateCurrentMediaPath: (mediaPath) => { updateCurrentMediaPath(mediaPath); }, updateMpvSubtitleRenderMetrics: (patch) => { updateMpvSubtitleRenderMetrics(patch); },
|
||||||
currentSubText = text;
|
getMpvSubtitleRenderMetrics: () => mpvSubtitleRenderMetrics, setPreviousSecondarySubVisibility: (value) => { previousSecondarySubVisibility = value; }, showMpvOsd: (text) => { showMpvOsd(text); },
|
||||||
},
|
|
||||||
setCurrentSubAssText: (text) => {
|
|
||||||
currentSubAssText = text;
|
|
||||||
},
|
|
||||||
getSubtitleTimingTracker: () => subtitleTimingTracker,
|
|
||||||
subtitleWsBroadcast: (text) => {
|
|
||||||
subtitleWsService.broadcast(text);
|
|
||||||
},
|
|
||||||
getOverlayWindowsCount: () => getOverlayWindows().length,
|
|
||||||
tokenizeSubtitle: (text) => tokenizeSubtitle(text),
|
|
||||||
broadcastToOverlayWindows: (channel, ...args) => {
|
|
||||||
broadcastToOverlayWindows(channel, ...args);
|
|
||||||
},
|
|
||||||
updateCurrentMediaPath: (mediaPath) => {
|
|
||||||
updateCurrentMediaPath(mediaPath);
|
|
||||||
},
|
|
||||||
updateMpvSubtitleRenderMetrics: (patch) => {
|
|
||||||
updateMpvSubtitleRenderMetrics(patch);
|
|
||||||
},
|
|
||||||
getMpvSubtitleRenderMetrics: () => mpvSubtitleRenderMetrics,
|
|
||||||
setPreviousSecondarySubVisibility: (value) => {
|
|
||||||
previousSecondarySubVisibility = value;
|
|
||||||
},
|
|
||||||
showMpvOsd: (text) => {
|
|
||||||
showMpvOsd(text);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
configService.reloadConfig();
|
configService.reloadConfig();
|
||||||
@@ -902,99 +727,10 @@ function handleInitialArgs(): void {
|
|||||||
function updateMpvSubtitleRenderMetrics(
|
function updateMpvSubtitleRenderMetrics(
|
||||||
patch: Partial<MpvSubtitleRenderMetrics>,
|
patch: Partial<MpvSubtitleRenderMetrics>,
|
||||||
): void {
|
): void {
|
||||||
const patchOsd = patch.osdDimensions;
|
const next = updateMpvSubtitleRenderMetricsService(
|
||||||
const nextOsdDimensions =
|
mpvSubtitleRenderMetrics,
|
||||||
patchOsd &&
|
patch,
|
||||||
typeof patchOsd.w === "number" &&
|
);
|
||||||
typeof patchOsd.h === "number" &&
|
|
||||||
typeof patchOsd.ml === "number" &&
|
|
||||||
typeof patchOsd.mr === "number" &&
|
|
||||||
typeof patchOsd.mt === "number" &&
|
|
||||||
typeof patchOsd.mb === "number"
|
|
||||||
? {
|
|
||||||
w: asFiniteNumber(patchOsd.w, 0, 1, 100000),
|
|
||||||
h: asFiniteNumber(patchOsd.h, 0, 1, 100000),
|
|
||||||
ml: asFiniteNumber(patchOsd.ml, 0, 0, 100000),
|
|
||||||
mr: asFiniteNumber(patchOsd.mr, 0, 0, 100000),
|
|
||||||
mt: asFiniteNumber(patchOsd.mt, 0, 0, 100000),
|
|
||||||
mb: asFiniteNumber(patchOsd.mb, 0, 0, 100000),
|
|
||||||
}
|
|
||||||
: patchOsd === null
|
|
||||||
? null
|
|
||||||
: mpvSubtitleRenderMetrics.osdDimensions;
|
|
||||||
|
|
||||||
const next: MpvSubtitleRenderMetrics = {
|
|
||||||
subPos: asFiniteNumber(
|
|
||||||
patch.subPos,
|
|
||||||
mpvSubtitleRenderMetrics.subPos,
|
|
||||||
0,
|
|
||||||
150,
|
|
||||||
),
|
|
||||||
subFontSize: asFiniteNumber(
|
|
||||||
patch.subFontSize,
|
|
||||||
mpvSubtitleRenderMetrics.subFontSize,
|
|
||||||
1,
|
|
||||||
200,
|
|
||||||
),
|
|
||||||
subScale: asFiniteNumber(
|
|
||||||
patch.subScale,
|
|
||||||
mpvSubtitleRenderMetrics.subScale,
|
|
||||||
0.1,
|
|
||||||
10,
|
|
||||||
),
|
|
||||||
subMarginY: asFiniteNumber(
|
|
||||||
patch.subMarginY,
|
|
||||||
mpvSubtitleRenderMetrics.subMarginY,
|
|
||||||
0,
|
|
||||||
200,
|
|
||||||
),
|
|
||||||
subMarginX: asFiniteNumber(
|
|
||||||
patch.subMarginX,
|
|
||||||
mpvSubtitleRenderMetrics.subMarginX,
|
|
||||||
0,
|
|
||||||
200,
|
|
||||||
),
|
|
||||||
subFont: asString(patch.subFont, mpvSubtitleRenderMetrics.subFont),
|
|
||||||
subSpacing: asFiniteNumber(
|
|
||||||
patch.subSpacing,
|
|
||||||
mpvSubtitleRenderMetrics.subSpacing,
|
|
||||||
-100,
|
|
||||||
100,
|
|
||||||
),
|
|
||||||
subBold: asBoolean(patch.subBold, mpvSubtitleRenderMetrics.subBold),
|
|
||||||
subItalic: asBoolean(patch.subItalic, mpvSubtitleRenderMetrics.subItalic),
|
|
||||||
subBorderSize: asFiniteNumber(
|
|
||||||
patch.subBorderSize,
|
|
||||||
mpvSubtitleRenderMetrics.subBorderSize,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
),
|
|
||||||
subShadowOffset: asFiniteNumber(
|
|
||||||
patch.subShadowOffset,
|
|
||||||
mpvSubtitleRenderMetrics.subShadowOffset,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
),
|
|
||||||
subAssOverride: asString(
|
|
||||||
patch.subAssOverride,
|
|
||||||
mpvSubtitleRenderMetrics.subAssOverride,
|
|
||||||
),
|
|
||||||
subScaleByWindow: asBoolean(
|
|
||||||
patch.subScaleByWindow,
|
|
||||||
mpvSubtitleRenderMetrics.subScaleByWindow,
|
|
||||||
),
|
|
||||||
subUseMargins: asBoolean(
|
|
||||||
patch.subUseMargins,
|
|
||||||
mpvSubtitleRenderMetrics.subUseMargins,
|
|
||||||
),
|
|
||||||
osdHeight: asFiniteNumber(
|
|
||||||
patch.osdHeight,
|
|
||||||
mpvSubtitleRenderMetrics.osdHeight,
|
|
||||||
1,
|
|
||||||
10000,
|
|
||||||
),
|
|
||||||
osdDimensions: nextOsdDimensions,
|
|
||||||
};
|
|
||||||
|
|
||||||
const changed =
|
const changed =
|
||||||
next.subPos !== mpvSubtitleRenderMetrics.subPos ||
|
next.subPos !== mpvSubtitleRenderMetrics.subPos ||
|
||||||
@@ -1722,52 +1458,16 @@ registerIpcHandlersService({
|
|||||||
* Create and show a desktop notification with robust icon handling.
|
* Create and show a desktop notification with robust icon handling.
|
||||||
* 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 createFieldGroupingCallbackService({ getVisibleOverlayVisible: () => visibleOverlayVisible, getInvisibleOverlayVisible: () => invisibleOverlayVisible, setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible), setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible), getResolver: () => fieldGroupingResolver, setResolver: (resolver) => { fieldGroupingResolver = resolver; }, sendRequestToVisibleOverlay: (data) => sendToVisibleOverlay("kiku:field-grouping-request", data) }); }
|
||||||
return createFieldGroupingCallbackService({
|
|
||||||
getVisibleOverlayVisible: () => visibleOverlayVisible,
|
|
||||||
getInvisibleOverlayVisible: () => invisibleOverlayVisible,
|
|
||||||
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
|
||||||
setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible),
|
|
||||||
getResolver: () => fieldGroupingResolver,
|
|
||||||
setResolver: (resolver) => {
|
|
||||||
fieldGroupingResolver = resolver;
|
|
||||||
},
|
|
||||||
sendRequestToVisibleOverlay: (data) =>
|
|
||||||
sendToVisibleOverlay("kiku:field-grouping-request", data),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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) }); }
|
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) }); }
|
||||||
|
|
||||||
registerAnkiJimakuIpcRuntimeService({
|
registerAnkiJimakuIpcRuntimeService({
|
||||||
patchAnkiConnectEnabled: (enabled) => {
|
patchAnkiConnectEnabled: (enabled) => { configService.patchRawConfig({ ankiConnect: { enabled } }); },
|
||||||
configService.patchRawConfig({
|
getResolvedConfig: () => getResolvedConfig(), getRuntimeOptionsManager: () => runtimeOptionsManager, getSubtitleTimingTracker: () => subtitleTimingTracker, getMpvClient: () => mpvClient, getAnkiIntegration: () => ankiIntegration, setAnkiIntegration: (integration) => { ankiIntegration = integration; },
|
||||||
ankiConnect: {
|
showDesktopNotification, createFieldGroupingCallback: () => createFieldGroupingCallback(), broadcastRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
||||||
enabled,
|
getFieldGroupingResolver: () => fieldGroupingResolver, setFieldGroupingResolver: (resolver) => { fieldGroupingResolver = resolver; },
|
||||||
},
|
parseMediaInfo: (mediaPath) => parseMediaInfo(mediaPath), getCurrentMediaPath: () => currentMediaPath, jimakuFetchJson: (endpoint, query) => jimakuFetchJson(endpoint, query),
|
||||||
});
|
getJimakuMaxEntryResults: () => getJimakuMaxEntryResults(), getJimakuLanguagePreference: () => getJimakuLanguagePreference(), resolveJimakuApiKey: () => resolveJimakuApiKey(),
|
||||||
},
|
isRemoteMediaPath: (mediaPath) => isRemoteMediaPath(mediaPath), downloadToFile: (url, destPath, headers) => downloadToFile(url, destPath, headers),
|
||||||
getResolvedConfig: () => getResolvedConfig(),
|
|
||||||
getRuntimeOptionsManager: () => runtimeOptionsManager,
|
|
||||||
getSubtitleTimingTracker: () => subtitleTimingTracker,
|
|
||||||
getMpvClient: () => mpvClient,
|
|
||||||
getAnkiIntegration: () => ankiIntegration,
|
|
||||||
setAnkiIntegration: (integration) => {
|
|
||||||
ankiIntegration = integration;
|
|
||||||
},
|
|
||||||
showDesktopNotification,
|
|
||||||
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
|
||||||
broadcastRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
|
||||||
getFieldGroupingResolver: () => fieldGroupingResolver,
|
|
||||||
setFieldGroupingResolver: (resolver) => {
|
|
||||||
fieldGroupingResolver = resolver;
|
|
||||||
},
|
|
||||||
parseMediaInfo: (mediaPath) => parseMediaInfo(mediaPath),
|
|
||||||
getCurrentMediaPath: () => currentMediaPath,
|
|
||||||
jimakuFetchJson: (endpoint, query) => jimakuFetchJson(endpoint, query),
|
|
||||||
getJimakuMaxEntryResults: () => getJimakuMaxEntryResults(),
|
|
||||||
getJimakuLanguagePreference: () => getJimakuLanguagePreference(),
|
|
||||||
resolveJimakuApiKey: () => resolveJimakuApiKey(),
|
|
||||||
isRemoteMediaPath: (mediaPath) => isRemoteMediaPath(mediaPath),
|
|
||||||
downloadToFile: (url, destPath, headers) => downloadToFile(url, destPath, headers),
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user