mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
refactor state and overlay runtime helpers
This commit is contained in:
10
Makefile
10
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: help deps build install build-linux build-macos build-macos-unsigned install-linux install-macos install-plugin uninstall uninstall-linux uninstall-macos print-dirs pretty ensure-pnpm generate-config generate-example-config docs-dev docs docs-preview dev-start dev-start-macos dev-toggle dev-stop
|
.PHONY: help deps build install build-linux build-macos build-macos-unsigned clean install-linux install-macos install-plugin uninstall uninstall-linux uninstall-macos print-dirs pretty ensure-pnpm generate-config generate-example-config docs-dev docs docs-preview dev-start dev-start-macos dev-toggle dev-stop
|
||||||
|
|
||||||
APP_NAME := subminer
|
APP_NAME := subminer
|
||||||
THEME_FILE := subminer.rasi
|
THEME_FILE := subminer.rasi
|
||||||
@@ -48,6 +48,7 @@ help:
|
|||||||
" build-linux Build Linux AppImage" \
|
" build-linux Build Linux AppImage" \
|
||||||
" build-macos Build macOS DMG/ZIP (signed if configured)" \
|
" build-macos Build macOS DMG/ZIP (signed if configured)" \
|
||||||
" build-macos-unsigned Build macOS DMG/ZIP without signing/notarization" \
|
" build-macos-unsigned Build macOS DMG/ZIP without signing/notarization" \
|
||||||
|
" clean Remove build artifacts (dist/, release/, AppImage, binary)" \
|
||||||
" dev-start Build and launch local Electron app" \
|
" dev-start Build and launch local Electron app" \
|
||||||
" dev-start-macos Build and launch local Electron app with macOS tracker backend" \
|
" dev-start-macos Build and launch local Electron app with macOS tracker backend" \
|
||||||
" dev-toggle Toggle overlay in a running local Electron app" \
|
" dev-toggle Toggle overlay in a running local Electron app" \
|
||||||
@@ -130,6 +131,13 @@ build-macos-unsigned: deps
|
|||||||
@pnpm -C vendor/texthooker-ui build
|
@pnpm -C vendor/texthooker-ui build
|
||||||
@pnpm run build:mac:unsigned
|
@pnpm run build:mac:unsigned
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@printf '%s\n' "[INFO] Removing build artifacts"
|
||||||
|
@rm -f release/SubMiner-*.AppImage
|
||||||
|
@rm -f release/linux-unpacked/SubMiner
|
||||||
|
@rm -f "$(BINDIR)/subminer" "$(BINDIR)/SubMiner.AppImage"
|
||||||
|
@rm -rf dist release
|
||||||
|
|
||||||
generate-config: ensure-pnpm
|
generate-config: ensure-pnpm
|
||||||
@pnpm run build
|
@pnpm run build
|
||||||
@pnpm exec electron . --generate-config
|
@pnpm exec electron . --generate-config
|
||||||
|
|||||||
@@ -109,6 +109,12 @@ Adopted sequence (from TASK-27 parent):
|
|||||||
- Risk: lifecycle/cleanup regressions from moving startup hooks and shutdown behavior
|
- Risk: lifecycle/cleanup regressions from moving startup hooks and shutdown behavior
|
||||||
- Mitigation: preserve service construction order and keep existing event registration boundaries
|
- Mitigation: preserve service construction order and keep existing event registration boundaries
|
||||||
|
|
||||||
|
Migration note:
|
||||||
|
- `src/main.ts` now delegates composition edges to `src/main/startup.ts`, `src/main/app-lifecycle.ts`, `src/main/ipc-runtime.ts`, `src/main/cli-runtime.ts`, and `src/main/subsync-runtime.ts`.
|
||||||
|
- Overlay/modal interaction has been moved into `src/main/overlay-runtime.ts` (window selection, modal restore set tracking, runtime-options palette/modal close handling) so `main.ts` now uses a dedicated runtime service for modal routing instead of inline window bookkeeping.
|
||||||
|
- Stateful runtime session data has moved to `src/main/state.ts` via `createAppState()` so `main.ts` no longer owns the `AppState` shape inline, only importing and mutating the shared state instance.
|
||||||
|
- Behavioral contract remains stable: startup flow, CLI dispatch, IPC handlers, and subsync orchestration keep existing external behavior; only dependency wiring moved out of `main.ts`.
|
||||||
|
|
||||||
### TASK-27.4 (mpv-service)
|
### TASK-27.4 (mpv-service)
|
||||||
- Risk: request/deps interface changes could break control and subsync interactions
|
- Risk: request/deps interface changes could break control and subsync interactions
|
||||||
- Mitigation: preserve public `MpvClient` methods, request semantics, and reconnect events while splitting internal modules
|
- Mitigation: preserve public `MpvClient` methods, request semantics, and reconnect events while splitting internal modules
|
||||||
|
|||||||
167
src/core/services/mpv-properties.ts
Normal file
167
src/core/services/mpv-properties.ts
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import {
|
||||||
|
MPV_REQUEST_ID_AID,
|
||||||
|
MPV_REQUEST_ID_OSD_DIMENSIONS,
|
||||||
|
MPV_REQUEST_ID_OSD_HEIGHT,
|
||||||
|
MPV_REQUEST_ID_PATH,
|
||||||
|
MPV_REQUEST_ID_SECONDARY_SUBTEXT,
|
||||||
|
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
||||||
|
MPV_REQUEST_ID_SUB_ASS_OVERRIDE,
|
||||||
|
MPV_REQUEST_ID_SUB_BOLD,
|
||||||
|
MPV_REQUEST_ID_SUB_BORDER_SIZE,
|
||||||
|
MPV_REQUEST_ID_SUB_FONT,
|
||||||
|
MPV_REQUEST_ID_SUB_FONT_SIZE,
|
||||||
|
MPV_REQUEST_ID_SUB_ITALIC,
|
||||||
|
MPV_REQUEST_ID_SUB_MARGIN_X,
|
||||||
|
MPV_REQUEST_ID_SUB_MARGIN_Y,
|
||||||
|
MPV_REQUEST_ID_SUB_POS,
|
||||||
|
MPV_REQUEST_ID_SUB_SCALE,
|
||||||
|
MPV_REQUEST_ID_SUB_SCALE_BY_WINDOW,
|
||||||
|
MPV_REQUEST_ID_SUB_SHADOW_OFFSET,
|
||||||
|
MPV_REQUEST_ID_SUB_SPACING,
|
||||||
|
MPV_REQUEST_ID_SUBTEXT,
|
||||||
|
MPV_REQUEST_ID_SUBTEXT_ASS,
|
||||||
|
MPV_REQUEST_ID_SUB_USE_MARGINS,
|
||||||
|
} from "./mpv-protocol";
|
||||||
|
|
||||||
|
type MpvProtocolCommand = {
|
||||||
|
command: unknown[];
|
||||||
|
request_id?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface MpvSendCommand {
|
||||||
|
(command: MpvProtocolCommand): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MPV_SUBTITLE_PROPERTY_OBSERVATIONS: string[] = [
|
||||||
|
"sub-text",
|
||||||
|
"path",
|
||||||
|
"sub-start",
|
||||||
|
"sub-end",
|
||||||
|
"time-pos",
|
||||||
|
"secondary-sub-text",
|
||||||
|
"aid",
|
||||||
|
"sub-pos",
|
||||||
|
"sub-font-size",
|
||||||
|
"sub-scale",
|
||||||
|
"sub-margin-y",
|
||||||
|
"sub-margin-x",
|
||||||
|
"sub-font",
|
||||||
|
"sub-spacing",
|
||||||
|
"sub-bold",
|
||||||
|
"sub-italic",
|
||||||
|
"sub-scale-by-window",
|
||||||
|
"osd-height",
|
||||||
|
"osd-dimensions",
|
||||||
|
"sub-text-ass",
|
||||||
|
"sub-border-size",
|
||||||
|
"sub-shadow-offset",
|
||||||
|
"sub-ass-override",
|
||||||
|
"sub-use-margins",
|
||||||
|
"media-title",
|
||||||
|
];
|
||||||
|
|
||||||
|
const MPV_INITIAL_PROPERTY_REQUESTS: Array<MpvProtocolCommand> = [
|
||||||
|
{
|
||||||
|
command: ["get_property", "sub-text"],
|
||||||
|
request_id: MPV_REQUEST_ID_SUBTEXT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "sub-text-ass"],
|
||||||
|
request_id: MPV_REQUEST_ID_SUBTEXT_ASS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "path"],
|
||||||
|
request_id: MPV_REQUEST_ID_PATH,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "media-title"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "secondary-sub-text"],
|
||||||
|
request_id: MPV_REQUEST_ID_SECONDARY_SUBTEXT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "secondary-sub-visibility"],
|
||||||
|
request_id: MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "aid"],
|
||||||
|
request_id: MPV_REQUEST_ID_AID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "sub-pos"],
|
||||||
|
request_id: MPV_REQUEST_ID_SUB_POS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "sub-font-size"],
|
||||||
|
request_id: MPV_REQUEST_ID_SUB_FONT_SIZE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "sub-scale"],
|
||||||
|
request_id: MPV_REQUEST_ID_SUB_SCALE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "sub-margin-y"],
|
||||||
|
request_id: MPV_REQUEST_ID_SUB_MARGIN_Y,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "sub-margin-x"],
|
||||||
|
request_id: MPV_REQUEST_ID_SUB_MARGIN_X,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "sub-font"],
|
||||||
|
request_id: MPV_REQUEST_ID_SUB_FONT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "sub-spacing"],
|
||||||
|
request_id: MPV_REQUEST_ID_SUB_SPACING,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "sub-bold"],
|
||||||
|
request_id: MPV_REQUEST_ID_SUB_BOLD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "sub-italic"],
|
||||||
|
request_id: MPV_REQUEST_ID_SUB_ITALIC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "sub-scale-by-window"],
|
||||||
|
request_id: MPV_REQUEST_ID_SUB_SCALE_BY_WINDOW,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "osd-height"],
|
||||||
|
request_id: MPV_REQUEST_ID_OSD_HEIGHT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "osd-dimensions"],
|
||||||
|
request_id: MPV_REQUEST_ID_OSD_DIMENSIONS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "sub-border-size"],
|
||||||
|
request_id: MPV_REQUEST_ID_SUB_BORDER_SIZE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "sub-shadow-offset"],
|
||||||
|
request_id: MPV_REQUEST_ID_SUB_SHADOW_OFFSET,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "sub-ass-override"],
|
||||||
|
request_id: MPV_REQUEST_ID_SUB_ASS_OVERRIDE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: ["get_property", "sub-use-margins"],
|
||||||
|
request_id: MPV_REQUEST_ID_SUB_USE_MARGINS,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function subscribeToMpvProperties(send: MpvSendCommand): void {
|
||||||
|
MPV_SUBTITLE_PROPERTY_OBSERVATIONS.forEach((property, index) => {
|
||||||
|
send({ command: ["observe_property", index + 1, property] });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requestMpvInitialState(send: MpvSendCommand): void {
|
||||||
|
MPV_INITIAL_PROPERTY_REQUESTS.forEach((payload) => {
|
||||||
|
send(payload);
|
||||||
|
});
|
||||||
|
}
|
||||||
168
src/core/services/mpv-protocol.test.ts
Normal file
168
src/core/services/mpv-protocol.test.ts
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import type { MpvSubtitleRenderMetrics } from "../../types";
|
||||||
|
import {
|
||||||
|
dispatchMpvProtocolMessage,
|
||||||
|
MPV_REQUEST_ID_TRACK_LIST_SECONDARY,
|
||||||
|
MpvProtocolHandleMessageDeps,
|
||||||
|
splitMpvMessagesFromBuffer,
|
||||||
|
parseVisibilityProperty,
|
||||||
|
asBoolean,
|
||||||
|
} from "./mpv-protocol";
|
||||||
|
|
||||||
|
function createDeps(overrides: Partial<MpvProtocolHandleMessageDeps> = {}): {
|
||||||
|
deps: MpvProtocolHandleMessageDeps;
|
||||||
|
state: {
|
||||||
|
subText: string;
|
||||||
|
secondarySubText: string;
|
||||||
|
events: Array<unknown>;
|
||||||
|
commands: unknown[];
|
||||||
|
mediaPath: string;
|
||||||
|
restored: number;
|
||||||
|
};
|
||||||
|
} {
|
||||||
|
const state = {
|
||||||
|
subText: "",
|
||||||
|
secondarySubText: "",
|
||||||
|
events: [] as Array<unknown>,
|
||||||
|
commands: [] as unknown[],
|
||||||
|
mediaPath: "",
|
||||||
|
restored: 0,
|
||||||
|
};
|
||||||
|
const metrics: MpvSubtitleRenderMetrics = {
|
||||||
|
subPos: 100,
|
||||||
|
subFontSize: 36,
|
||||||
|
subScale: 1,
|
||||||
|
subMarginY: 0,
|
||||||
|
subMarginX: 0,
|
||||||
|
subFont: "",
|
||||||
|
subSpacing: 0,
|
||||||
|
subBold: false,
|
||||||
|
subItalic: false,
|
||||||
|
subBorderSize: 0,
|
||||||
|
subShadowOffset: 0,
|
||||||
|
subAssOverride: "yes",
|
||||||
|
subScaleByWindow: true,
|
||||||
|
subUseMargins: true,
|
||||||
|
osdHeight: 0,
|
||||||
|
osdDimensions: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
deps: {
|
||||||
|
getResolvedConfig: () => ({ secondarySub: { secondarySubLanguages: ["ja"] } }),
|
||||||
|
getSubtitleMetrics: () => metrics,
|
||||||
|
isVisibleOverlayVisible: () => false,
|
||||||
|
emitSubtitleChange: (payload) => state.events.push(payload),
|
||||||
|
emitSubtitleAssChange: (payload) => state.events.push(payload),
|
||||||
|
emitSubtitleTiming: (payload) => state.events.push(payload),
|
||||||
|
emitSecondarySubtitleChange: (payload) => state.events.push(payload),
|
||||||
|
getCurrentSubText: () => state.subText,
|
||||||
|
setCurrentSubText: (text) => {
|
||||||
|
state.subText = text;
|
||||||
|
},
|
||||||
|
setCurrentSubStart: () => {},
|
||||||
|
getCurrentSubStart: () => 0,
|
||||||
|
setCurrentSubEnd: () => {},
|
||||||
|
getCurrentSubEnd: () => 0,
|
||||||
|
emitMediaPathChange: (payload) => {
|
||||||
|
state.mediaPath = payload.path;
|
||||||
|
},
|
||||||
|
emitMediaTitleChange: (payload) => state.events.push(payload),
|
||||||
|
emitSubtitleMetricsChange: (payload) => state.events.push(payload),
|
||||||
|
setCurrentSecondarySubText: (text) => {
|
||||||
|
state.secondarySubText = text;
|
||||||
|
},
|
||||||
|
resolvePendingRequest: () => false,
|
||||||
|
setSecondarySubVisibility: () => {},
|
||||||
|
syncCurrentAudioStreamIndex: () => {},
|
||||||
|
setCurrentAudioTrackId: () => {},
|
||||||
|
setCurrentTimePos: () => {},
|
||||||
|
getCurrentTimePos: () => 0,
|
||||||
|
getPendingPauseAtSubEnd: () => false,
|
||||||
|
setPendingPauseAtSubEnd: () => {},
|
||||||
|
getPauseAtTime: () => null,
|
||||||
|
setPauseAtTime: () => {},
|
||||||
|
autoLoadSecondarySubTrack: () => {},
|
||||||
|
setCurrentVideoPath: () => {},
|
||||||
|
emitSecondarySubtitleVisibility: (payload) => state.events.push(payload),
|
||||||
|
setCurrentAudioStreamIndex: () => {},
|
||||||
|
sendCommand: (payload) => {
|
||||||
|
state.commands.push(payload);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
restorePreviousSecondarySubVisibility: () => {
|
||||||
|
state.restored += 1;
|
||||||
|
},
|
||||||
|
...overrides,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test("dispatchMpvProtocolMessage emits subtitle text on property change", async () => {
|
||||||
|
const { deps, state } = createDeps();
|
||||||
|
|
||||||
|
await dispatchMpvProtocolMessage(
|
||||||
|
{ event: "property-change", name: "sub-text", data: "字幕" },
|
||||||
|
deps,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(state.subText, "字幕");
|
||||||
|
assert.deepEqual(state.events, [{ text: "字幕", isOverlayVisible: false }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dispatchMpvProtocolMessage sets secondary subtitle track based on track list response", async () => {
|
||||||
|
const { deps, state } = createDeps();
|
||||||
|
|
||||||
|
await dispatchMpvProtocolMessage(
|
||||||
|
{
|
||||||
|
request_id: MPV_REQUEST_ID_TRACK_LIST_SECONDARY,
|
||||||
|
data: [
|
||||||
|
{ type: "audio", id: 1, lang: "eng" },
|
||||||
|
{ type: "sub", id: 2, lang: "ja" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
deps,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(state.commands, [{ command: ["set_property", "secondary-sid", 2] }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dispatchMpvProtocolMessage restores secondary visibility on shutdown", async () => {
|
||||||
|
const { deps, state } = createDeps();
|
||||||
|
|
||||||
|
await dispatchMpvProtocolMessage({ event: "shutdown" }, deps);
|
||||||
|
|
||||||
|
assert.equal(state.restored, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("splitMpvMessagesFromBuffer parses complete lines and preserves partial buffer", () => {
|
||||||
|
const parsed = splitMpvMessagesFromBuffer(
|
||||||
|
"{\"event\":\"shutdown\"}\\n{\"event\":\"property-change\",\"name\":\"media-title\",\"data\":\"x\"}\\n{\"partial\"",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(parsed.messages.length, 2);
|
||||||
|
assert.equal(parsed.nextBuffer, "{\"partial\"");
|
||||||
|
assert.equal(parsed.messages[0].event, "shutdown");
|
||||||
|
assert.equal(parsed.messages[1].name, "property-change");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("splitMpvMessagesFromBuffer reports invalid JSON lines", () => {
|
||||||
|
const errors: Array<{ line: string; error?: string }> = [];
|
||||||
|
|
||||||
|
splitMpvMessagesFromBuffer("{\"event\":\"x\"}\\n{invalid}\\n", undefined, (line, error) => {
|
||||||
|
errors.push({ line, error: String(error) });
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(errors.length, 1);
|
||||||
|
assert.equal(errors[0].line, "{invalid}");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("visibility and boolean parsers handle text values", () => {
|
||||||
|
assert.equal(parseVisibilityProperty("true"), true);
|
||||||
|
assert.equal(parseVisibilityProperty("0"), false);
|
||||||
|
assert.equal(parseVisibilityProperty("unknown"), null);
|
||||||
|
assert.equal(asBoolean("yes", false), true);
|
||||||
|
assert.equal(asBoolean("0", true), false);
|
||||||
|
});
|
||||||
@@ -7,34 +7,14 @@ import {
|
|||||||
} from "../../types";
|
} from "../../types";
|
||||||
import {
|
import {
|
||||||
dispatchMpvProtocolMessage,
|
dispatchMpvProtocolMessage,
|
||||||
MPV_REQUEST_ID_AID,
|
|
||||||
MPV_REQUEST_ID_OSD_DIMENSIONS,
|
|
||||||
MPV_REQUEST_ID_OSD_HEIGHT,
|
|
||||||
MPV_REQUEST_ID_PATH,
|
|
||||||
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
|
||||||
MPV_REQUEST_ID_SECONDARY_SUBTEXT,
|
|
||||||
MPV_REQUEST_ID_SUB_ASS_OVERRIDE,
|
|
||||||
MPV_REQUEST_ID_SUB_BOLD,
|
|
||||||
MPV_REQUEST_ID_SUB_BORDER_SIZE,
|
|
||||||
MPV_REQUEST_ID_SUB_FONT,
|
|
||||||
MPV_REQUEST_ID_SUB_FONT_SIZE,
|
|
||||||
MPV_REQUEST_ID_SUB_ITALIC,
|
|
||||||
MPV_REQUEST_ID_SUB_MARGIN_X,
|
|
||||||
MPV_REQUEST_ID_SUB_MARGIN_Y,
|
|
||||||
MPV_REQUEST_ID_SUB_POS,
|
|
||||||
MPV_REQUEST_ID_SUB_SCALE,
|
|
||||||
MPV_REQUEST_ID_SUB_SCALE_BY_WINDOW,
|
|
||||||
MPV_REQUEST_ID_SUB_SHADOW_OFFSET,
|
|
||||||
MPV_REQUEST_ID_SUB_SPACING,
|
|
||||||
MPV_REQUEST_ID_SUBTEXT,
|
|
||||||
MPV_REQUEST_ID_SUBTEXT_ASS,
|
|
||||||
MPV_REQUEST_ID_SUB_USE_MARGINS,
|
|
||||||
MPV_REQUEST_ID_TRACK_LIST_AUDIO,
|
MPV_REQUEST_ID_TRACK_LIST_AUDIO,
|
||||||
MPV_REQUEST_ID_TRACK_LIST_SECONDARY,
|
MPV_REQUEST_ID_TRACK_LIST_SECONDARY,
|
||||||
MpvMessage,
|
MpvMessage,
|
||||||
MpvProtocolHandleMessageDeps,
|
MpvProtocolHandleMessageDeps,
|
||||||
splitMpvMessagesFromBuffer,
|
splitMpvMessagesFromBuffer,
|
||||||
} from "./mpv-protocol";
|
} from "./mpv-protocol";
|
||||||
|
import { requestMpvInitialState, subscribeToMpvProperties } from "./mpv-properties";
|
||||||
|
import { getMpvReconnectDelay } from "./mpv-transport";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
||||||
@@ -170,8 +150,8 @@ export class MpvIpcClient implements MpvClient {
|
|||||||
this.reconnectAttempt = 0;
|
this.reconnectAttempt = 0;
|
||||||
this.hasConnectedOnce = true;
|
this.hasConnectedOnce = true;
|
||||||
this.setSecondarySubVisibility(false);
|
this.setSecondarySubVisibility(false);
|
||||||
this.subscribeToProperties();
|
subscribeToMpvProperties(this.send.bind(this));
|
||||||
this.getInitialState();
|
requestMpvInitialState(this.send.bind(this));
|
||||||
|
|
||||||
const shouldAutoStart =
|
const shouldAutoStart =
|
||||||
this.deps.autoStartOverlay ||
|
this.deps.autoStartOverlay ||
|
||||||
@@ -217,28 +197,7 @@ export class MpvIpcClient implements MpvClient {
|
|||||||
clearTimeout(reconnectTimer);
|
clearTimeout(reconnectTimer);
|
||||||
}
|
}
|
||||||
const attempt = this.reconnectAttempt++;
|
const attempt = this.reconnectAttempt++;
|
||||||
let delay: number;
|
const delay = getMpvReconnectDelay(attempt, this.hasConnectedOnce);
|
||||||
if (this.hasConnectedOnce) {
|
|
||||||
if (attempt < 2) {
|
|
||||||
delay = 1000;
|
|
||||||
} else if (attempt < 4) {
|
|
||||||
delay = 2000;
|
|
||||||
} else if (attempt < 7) {
|
|
||||||
delay = 5000;
|
|
||||||
} else {
|
|
||||||
delay = 10000;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (attempt < 2) {
|
|
||||||
delay = 200;
|
|
||||||
} else if (attempt < 4) {
|
|
||||||
delay = 500;
|
|
||||||
} else if (attempt < 6) {
|
|
||||||
delay = 1000;
|
|
||||||
} else {
|
|
||||||
delay = 2000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.deps.setReconnectTimer(
|
this.deps.setReconnectTimer(
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log(
|
console.log(
|
||||||
@@ -457,128 +416,6 @@ export class MpvIpcClient implements MpvClient {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private subscribeToProperties(): void {
|
|
||||||
this.send({ command: ["observe_property", 1, "sub-text"] });
|
|
||||||
this.send({ command: ["observe_property", 2, "path"] });
|
|
||||||
this.send({ command: ["observe_property", 3, "sub-start"] });
|
|
||||||
this.send({ command: ["observe_property", 4, "sub-end"] });
|
|
||||||
this.send({ command: ["observe_property", 5, "time-pos"] });
|
|
||||||
this.send({ command: ["observe_property", 6, "secondary-sub-text"] });
|
|
||||||
this.send({ command: ["observe_property", 7, "aid"] });
|
|
||||||
this.send({ command: ["observe_property", 8, "sub-pos"] });
|
|
||||||
this.send({ command: ["observe_property", 9, "sub-font-size"] });
|
|
||||||
this.send({ command: ["observe_property", 10, "sub-scale"] });
|
|
||||||
this.send({ command: ["observe_property", 11, "sub-margin-y"] });
|
|
||||||
this.send({ command: ["observe_property", 12, "sub-margin-x"] });
|
|
||||||
this.send({ command: ["observe_property", 13, "sub-font"] });
|
|
||||||
this.send({ command: ["observe_property", 14, "sub-spacing"] });
|
|
||||||
this.send({ command: ["observe_property", 15, "sub-bold"] });
|
|
||||||
this.send({ command: ["observe_property", 16, "sub-italic"] });
|
|
||||||
this.send({ command: ["observe_property", 17, "sub-scale-by-window"] });
|
|
||||||
this.send({ command: ["observe_property", 18, "osd-height"] });
|
|
||||||
this.send({ command: ["observe_property", 19, "osd-dimensions"] });
|
|
||||||
this.send({ command: ["observe_property", 20, "sub-text-ass"] });
|
|
||||||
this.send({ command: ["observe_property", 21, "sub-border-size"] });
|
|
||||||
this.send({ command: ["observe_property", 22, "sub-shadow-offset"] });
|
|
||||||
this.send({ command: ["observe_property", 23, "sub-ass-override"] });
|
|
||||||
this.send({ command: ["observe_property", 24, "sub-use-margins"] });
|
|
||||||
this.send({ command: ["observe_property", 25, "media-title"] });
|
|
||||||
}
|
|
||||||
|
|
||||||
private getInitialState(): void {
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "sub-text"],
|
|
||||||
request_id: MPV_REQUEST_ID_SUBTEXT,
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "sub-text-ass"],
|
|
||||||
request_id: MPV_REQUEST_ID_SUBTEXT_ASS,
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "path"],
|
|
||||||
request_id: MPV_REQUEST_ID_PATH,
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "media-title"],
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "secondary-sub-text"],
|
|
||||||
request_id: MPV_REQUEST_ID_SECONDARY_SUBTEXT,
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "secondary-sub-visibility"],
|
|
||||||
request_id: MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "aid"],
|
|
||||||
request_id: MPV_REQUEST_ID_AID,
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "sub-pos"],
|
|
||||||
request_id: MPV_REQUEST_ID_SUB_POS,
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "sub-font-size"],
|
|
||||||
request_id: MPV_REQUEST_ID_SUB_FONT_SIZE,
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "sub-scale"],
|
|
||||||
request_id: MPV_REQUEST_ID_SUB_SCALE,
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "sub-margin-y"],
|
|
||||||
request_id: MPV_REQUEST_ID_SUB_MARGIN_Y,
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "sub-margin-x"],
|
|
||||||
request_id: MPV_REQUEST_ID_SUB_MARGIN_X,
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "sub-font"],
|
|
||||||
request_id: MPV_REQUEST_ID_SUB_FONT,
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "sub-spacing"],
|
|
||||||
request_id: MPV_REQUEST_ID_SUB_SPACING,
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "sub-bold"],
|
|
||||||
request_id: MPV_REQUEST_ID_SUB_BOLD,
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "sub-italic"],
|
|
||||||
request_id: MPV_REQUEST_ID_SUB_ITALIC,
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "sub-scale-by-window"],
|
|
||||||
request_id: MPV_REQUEST_ID_SUB_SCALE_BY_WINDOW,
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "osd-height"],
|
|
||||||
request_id: MPV_REQUEST_ID_OSD_HEIGHT,
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "osd-dimensions"],
|
|
||||||
request_id: MPV_REQUEST_ID_OSD_DIMENSIONS,
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "sub-border-size"],
|
|
||||||
request_id: MPV_REQUEST_ID_SUB_BORDER_SIZE,
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "sub-shadow-offset"],
|
|
||||||
request_id: MPV_REQUEST_ID_SUB_SHADOW_OFFSET,
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "sub-ass-override"],
|
|
||||||
request_id: MPV_REQUEST_ID_SUB_ASS_OVERRIDE,
|
|
||||||
});
|
|
||||||
this.send({
|
|
||||||
command: ["get_property", "sub-use-margins"],
|
|
||||||
request_id: MPV_REQUEST_ID_SUB_USE_MARGINS,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setSubVisibility(visible: boolean): void {
|
setSubVisibility(visible: boolean): void {
|
||||||
this.send({
|
this.send({
|
||||||
command: ["set_property", "sub-visibility", visible ? "yes" : "no"],
|
command: ["set_property", "sub-visibility", visible ? "yes" : "no"],
|
||||||
|
|||||||
16
src/core/services/mpv-transport.test.ts
Normal file
16
src/core/services/mpv-transport.test.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { getMpvReconnectDelay } from "./mpv-transport";
|
||||||
|
|
||||||
|
test("getMpvReconnectDelay follows existing reconnect ramp", () => {
|
||||||
|
assert.equal(getMpvReconnectDelay(0, true), 1000);
|
||||||
|
assert.equal(getMpvReconnectDelay(1, true), 1000);
|
||||||
|
assert.equal(getMpvReconnectDelay(2, true), 2000);
|
||||||
|
assert.equal(getMpvReconnectDelay(4, true), 5000);
|
||||||
|
assert.equal(getMpvReconnectDelay(7, true), 10000);
|
||||||
|
|
||||||
|
assert.equal(getMpvReconnectDelay(0, false), 200);
|
||||||
|
assert.equal(getMpvReconnectDelay(2, false), 500);
|
||||||
|
assert.equal(getMpvReconnectDelay(4, false), 1000);
|
||||||
|
assert.equal(getMpvReconnectDelay(6, false), 2000);
|
||||||
|
});
|
||||||
28
src/core/services/mpv-transport.ts
Normal file
28
src/core/services/mpv-transport.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
export function getMpvReconnectDelay(
|
||||||
|
attempt: number,
|
||||||
|
hasConnectedOnce: boolean,
|
||||||
|
): number {
|
||||||
|
if (hasConnectedOnce) {
|
||||||
|
if (attempt < 2) {
|
||||||
|
return 1000;
|
||||||
|
}
|
||||||
|
if (attempt < 4) {
|
||||||
|
return 2000;
|
||||||
|
}
|
||||||
|
if (attempt < 7) {
|
||||||
|
return 5000;
|
||||||
|
}
|
||||||
|
return 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt < 2) {
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
if (attempt < 4) {
|
||||||
|
return 500;
|
||||||
|
}
|
||||||
|
if (attempt < 6) {
|
||||||
|
return 1000;
|
||||||
|
}
|
||||||
|
return 2000;
|
||||||
|
}
|
||||||
524
src/main.ts
524
src/main.ts
@@ -42,21 +42,17 @@ import * as path from "path";
|
|||||||
import * as os from "os";
|
import * as os from "os";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { MecabTokenizer } from "./mecab-tokenizer";
|
import { MecabTokenizer } from "./mecab-tokenizer";
|
||||||
import { BaseWindowTracker } from "./window-trackers";
|
|
||||||
import type {
|
import type {
|
||||||
JimakuApiResponse,
|
JimakuApiResponse,
|
||||||
JimakuLanguagePreference,
|
JimakuLanguagePreference,
|
||||||
SubtitleData,
|
SubtitleData,
|
||||||
SubtitlePosition,
|
SubtitlePosition,
|
||||||
Keybinding,
|
|
||||||
WindowGeometry,
|
WindowGeometry,
|
||||||
SecondarySubMode,
|
SecondarySubMode,
|
||||||
SubsyncManualPayload,
|
SubsyncManualPayload,
|
||||||
SubsyncManualRunRequest,
|
SubsyncManualRunRequest,
|
||||||
SubsyncResult,
|
SubsyncResult,
|
||||||
KikuFieldGroupingChoice,
|
KikuFieldGroupingChoice,
|
||||||
KikuMergePreviewRequest,
|
|
||||||
KikuMergePreviewResponse,
|
|
||||||
RuntimeOptionState,
|
RuntimeOptionState,
|
||||||
MpvSubtitleRenderMetrics,
|
MpvSubtitleRenderMetrics,
|
||||||
ResolvedConfig,
|
ResolvedConfig,
|
||||||
@@ -94,7 +90,6 @@ import {
|
|||||||
broadcastRuntimeOptionsChangedRuntimeService,
|
broadcastRuntimeOptionsChangedRuntimeService,
|
||||||
copyCurrentSubtitleService,
|
copyCurrentSubtitleService,
|
||||||
createAppLifecycleDepsRuntimeService,
|
createAppLifecycleDepsRuntimeService,
|
||||||
createCliCommandDepsRuntimeService,
|
|
||||||
createOverlayManagerService,
|
createOverlayManagerService,
|
||||||
createFieldGroupingOverlayRuntimeService,
|
createFieldGroupingOverlayRuntimeService,
|
||||||
createNumericShortcutRuntimeService,
|
createNumericShortcutRuntimeService,
|
||||||
@@ -108,7 +103,6 @@ import {
|
|||||||
getInitialInvisibleOverlayVisibilityService,
|
getInitialInvisibleOverlayVisibilityService,
|
||||||
getJimakuLanguagePreferenceService,
|
getJimakuLanguagePreferenceService,
|
||||||
getJimakuMaxEntryResultsService,
|
getJimakuMaxEntryResultsService,
|
||||||
handleCliCommandService,
|
|
||||||
handleMineSentenceDigitService,
|
handleMineSentenceDigitService,
|
||||||
handleMultiCopyDigitService,
|
handleMultiCopyDigitService,
|
||||||
hasMpvWebsocketPlugin,
|
hasMpvWebsocketPlugin,
|
||||||
@@ -128,7 +122,6 @@ import {
|
|||||||
replayCurrentSubtitleRuntimeService,
|
replayCurrentSubtitleRuntimeService,
|
||||||
resolveJimakuApiKeyService,
|
resolveJimakuApiKeyService,
|
||||||
runStartupBootstrapRuntimeService,
|
runStartupBootstrapRuntimeService,
|
||||||
runSubsyncManualFromIpcRuntimeService,
|
|
||||||
saveSubtitlePositionService,
|
saveSubtitlePositionService,
|
||||||
sendMpvCommandRuntimeService,
|
sendMpvCommandRuntimeService,
|
||||||
setInvisibleOverlayVisibleService,
|
setInvisibleOverlayVisibleService,
|
||||||
@@ -144,7 +137,6 @@ import {
|
|||||||
syncOverlayShortcutsRuntimeService,
|
syncOverlayShortcutsRuntimeService,
|
||||||
tokenizeSubtitleService,
|
tokenizeSubtitleService,
|
||||||
triggerFieldGroupingService,
|
triggerFieldGroupingService,
|
||||||
triggerSubsyncFromConfigRuntimeService,
|
|
||||||
unregisterOverlayShortcutsRuntimeService,
|
unregisterOverlayShortcutsRuntimeService,
|
||||||
updateCurrentMediaPathService,
|
updateCurrentMediaPathService,
|
||||||
updateInvisibleOverlayVisibilityService,
|
updateInvisibleOverlayVisibilityService,
|
||||||
@@ -156,22 +148,28 @@ import {
|
|||||||
runAppReadyRuntimeService,
|
runAppReadyRuntimeService,
|
||||||
} from "./core/services/startup-service";
|
} from "./core/services/startup-service";
|
||||||
import type { AppReadyRuntimeDeps } from "./core/services/startup-service";
|
import type { AppReadyRuntimeDeps } from "./core/services/startup-service";
|
||||||
import type { SubsyncRuntimeDeps } from "./core/services/subsync-runner-service";
|
|
||||||
import { applyRuntimeOptionResultRuntimeService } from "./core/services/runtime-options-ipc-service";
|
import { applyRuntimeOptionResultRuntimeService } from "./core/services/runtime-options-ipc-service";
|
||||||
import {
|
|
||||||
createRuntimeOptionsIpcDeps,
|
|
||||||
createCliCommandRuntimeServiceDeps,
|
|
||||||
createSubsyncRuntimeDeps,
|
|
||||||
} from "./main/dependencies";
|
|
||||||
import {
|
import {
|
||||||
createAppLifecycleRuntimeDeps as createAppLifecycleRuntimeDepsBuilder,
|
createAppLifecycleRuntimeDeps as createAppLifecycleRuntimeDepsBuilder,
|
||||||
createAppReadyRuntimeDeps as createAppReadyRuntimeDepsBuilder,
|
createAppReadyRuntimeDeps as createAppReadyRuntimeDepsBuilder,
|
||||||
} from "./main/app-lifecycle";
|
} from "./main/app-lifecycle";
|
||||||
import { handleMpvCommandFromIpcRuntime } from "./main/ipc-mpv-command";
|
import { handleMpvCommandFromIpcRuntime } from "./main/ipc-mpv-command";
|
||||||
import {
|
import {
|
||||||
registerAnkiJimakuIpcRuntimeServices,
|
registerIpcRuntimeServices,
|
||||||
registerMainIpcRuntimeServices,
|
|
||||||
} from "./main/ipc-runtime";
|
} from "./main/ipc-runtime";
|
||||||
|
import { handleCliCommandRuntimeService } from "./main/cli-runtime";
|
||||||
|
import {
|
||||||
|
runSubsyncManualFromIpcRuntime,
|
||||||
|
triggerSubsyncFromConfigRuntime,
|
||||||
|
} from "./main/subsync-runtime";
|
||||||
|
import {
|
||||||
|
createOverlayModalRuntimeService,
|
||||||
|
type OverlayHostedModal,
|
||||||
|
} from "./main/overlay-runtime";
|
||||||
|
import {
|
||||||
|
applyStartupState,
|
||||||
|
createAppState,
|
||||||
|
} from "./main/state";
|
||||||
import { createStartupBootstrapRuntimeDeps } from "./main/startup";
|
import { createStartupBootstrapRuntimeDeps } from "./main/startup";
|
||||||
import {
|
import {
|
||||||
ConfigService,
|
ConfigService,
|
||||||
@@ -284,90 +282,14 @@ const overlayContentMeasurementStore = createOverlayContentMeasurementStoreServi
|
|||||||
console.warn(message);
|
console.warn(message);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
type OverlayHostedModal = "runtime-options" | "subsync" | "jimaku";
|
const overlayModalRuntime = createOverlayModalRuntimeService({
|
||||||
type OverlayHostLayer = "visible" | "invisible";
|
getMainWindow: () => overlayManager.getMainWindow(),
|
||||||
const restoreVisibleOverlayOnModalClose = new Set<OverlayHostedModal>();
|
getInvisibleWindow: () => overlayManager.getInvisibleWindow(),
|
||||||
const overlayModalAutoShownLayer = new Map<OverlayHostedModal, OverlayHostLayer>();
|
});
|
||||||
|
const appState = createAppState({
|
||||||
interface AppState {
|
|
||||||
yomitanExt: Extension | null;
|
|
||||||
yomitanSettingsWindow: BrowserWindow | null;
|
|
||||||
yomitanParserWindow: BrowserWindow | null;
|
|
||||||
yomitanParserReadyPromise: Promise<void> | null;
|
|
||||||
yomitanParserInitPromise: Promise<boolean> | null;
|
|
||||||
mpvClient: MpvIpcClient | null;
|
|
||||||
reconnectTimer: ReturnType<typeof setTimeout> | null;
|
|
||||||
currentSubText: string;
|
|
||||||
currentSubAssText: string;
|
|
||||||
windowTracker: BaseWindowTracker | null;
|
|
||||||
subtitlePosition: SubtitlePosition | null;
|
|
||||||
currentMediaPath: string | null;
|
|
||||||
currentMediaTitle: string | null;
|
|
||||||
pendingSubtitlePosition: SubtitlePosition | null;
|
|
||||||
mecabTokenizer: MecabTokenizer | null;
|
|
||||||
keybindings: Keybinding[];
|
|
||||||
subtitleTimingTracker: SubtitleTimingTracker | null;
|
|
||||||
ankiIntegration: AnkiIntegration | null;
|
|
||||||
secondarySubMode: SecondarySubMode;
|
|
||||||
lastSecondarySubToggleAtMs: number;
|
|
||||||
previousSecondarySubVisibility: boolean | null;
|
|
||||||
mpvSubtitleRenderMetrics: MpvSubtitleRenderMetrics;
|
|
||||||
shortcutsRegistered: boolean;
|
|
||||||
overlayRuntimeInitialized: boolean;
|
|
||||||
fieldGroupingResolver: ((choice: KikuFieldGroupingChoice) => void) | null;
|
|
||||||
fieldGroupingResolverSequence: number;
|
|
||||||
runtimeOptionsManager: RuntimeOptionsManager | null;
|
|
||||||
trackerNotReadyWarningShown: boolean;
|
|
||||||
overlayDebugVisualizationEnabled: boolean;
|
|
||||||
subsyncInProgress: boolean;
|
|
||||||
initialArgs: CliArgs | null;
|
|
||||||
mpvSocketPath: string;
|
|
||||||
texthookerPort: number;
|
|
||||||
backendOverride: string | null;
|
|
||||||
autoStartOverlay: boolean;
|
|
||||||
texthookerOnlyMode: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const appState: AppState = {
|
|
||||||
yomitanExt: null,
|
|
||||||
yomitanSettingsWindow: null,
|
|
||||||
yomitanParserWindow: null,
|
|
||||||
yomitanParserReadyPromise: null,
|
|
||||||
yomitanParserInitPromise: null,
|
|
||||||
mpvClient: null,
|
|
||||||
reconnectTimer: null,
|
|
||||||
currentSubText: "",
|
|
||||||
currentSubAssText: "",
|
|
||||||
windowTracker: null,
|
|
||||||
subtitlePosition: null,
|
|
||||||
currentMediaPath: null,
|
|
||||||
currentMediaTitle: null,
|
|
||||||
pendingSubtitlePosition: null,
|
|
||||||
mecabTokenizer: null,
|
|
||||||
keybindings: [],
|
|
||||||
subtitleTimingTracker: null,
|
|
||||||
ankiIntegration: null,
|
|
||||||
secondarySubMode: "hover",
|
|
||||||
lastSecondarySubToggleAtMs: 0,
|
|
||||||
previousSecondarySubVisibility: null,
|
|
||||||
mpvSubtitleRenderMetrics: {
|
|
||||||
...DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
|
|
||||||
},
|
|
||||||
shortcutsRegistered: false,
|
|
||||||
overlayRuntimeInitialized: false,
|
|
||||||
fieldGroupingResolver: null,
|
|
||||||
fieldGroupingResolverSequence: 0,
|
|
||||||
runtimeOptionsManager: null,
|
|
||||||
trackerNotReadyWarningShown: false,
|
|
||||||
overlayDebugVisualizationEnabled: false,
|
|
||||||
subsyncInProgress: false,
|
|
||||||
initialArgs: null,
|
|
||||||
mpvSocketPath: getDefaultSocketPath(),
|
mpvSocketPath: getDefaultSocketPath(),
|
||||||
texthookerPort: DEFAULT_TEXTHOOKER_PORT,
|
texthookerPort: DEFAULT_TEXTHOOKER_PORT,
|
||||||
backendOverride: null,
|
});
|
||||||
autoStartOverlay: false,
|
|
||||||
texthookerOnlyMode: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
function getFieldGroupingResolver(): ((choice: KikuFieldGroupingChoice) => void) | null {
|
function getFieldGroupingResolver(): ((choice: KikuFieldGroupingChoice) => void) | null {
|
||||||
return appState.fieldGroupingResolver;
|
return appState.fieldGroupingResolver;
|
||||||
@@ -396,10 +318,14 @@ const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntimeService<Ove
|
|||||||
setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible),
|
setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible),
|
||||||
getResolver: () => getFieldGroupingResolver(),
|
getResolver: () => getFieldGroupingResolver(),
|
||||||
setResolver: (resolver) => setFieldGroupingResolver(resolver),
|
setResolver: (resolver) => setFieldGroupingResolver(resolver),
|
||||||
getRestoreVisibleOverlayOnModalClose: () => restoreVisibleOverlayOnModalClose,
|
getRestoreVisibleOverlayOnModalClose: () =>
|
||||||
sendToVisibleOverlay: (channel, payload) => {
|
overlayModalRuntime.getRestoreVisibleOverlayOnModalClose(),
|
||||||
sendToActiveOverlayWindow(channel, payload);
|
sendToVisibleOverlay: (channel, payload, runtimeOptions) => {
|
||||||
return true;
|
return overlayModalRuntime.sendToActiveOverlayWindow(
|
||||||
|
channel,
|
||||||
|
payload,
|
||||||
|
runtimeOptions,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const createFieldGroupingCallback =
|
const createFieldGroupingCallback =
|
||||||
@@ -429,77 +355,16 @@ function broadcastRuntimeOptionsChanged(): void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTargetOverlayWindow(): {
|
|
||||||
window: BrowserWindow;
|
|
||||||
layer: OverlayHostLayer;
|
|
||||||
} | null {
|
|
||||||
const visibleMainWindow = overlayManager.getMainWindow();
|
|
||||||
const invisibleWindow = overlayManager.getInvisibleWindow();
|
|
||||||
|
|
||||||
if (visibleMainWindow && !visibleMainWindow.isDestroyed()) {
|
|
||||||
return { window: visibleMainWindow, layer: "visible" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
|
|
||||||
return { window: invisibleWindow, layer: "invisible" };
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showOverlayWindowForModal(window: BrowserWindow, layer: OverlayHostLayer): void {
|
|
||||||
if (layer === "invisible" && typeof window.showInactive === "function") {
|
|
||||||
window.showInactive();
|
|
||||||
} else {
|
|
||||||
window.show();
|
|
||||||
}
|
|
||||||
if (!window.isFocused()) {
|
|
||||||
window.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendToActiveOverlayWindow(
|
function sendToActiveOverlayWindow(
|
||||||
channel: string,
|
channel: string,
|
||||||
payload?: unknown,
|
payload?: unknown,
|
||||||
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
|
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
|
||||||
): void {
|
): boolean {
|
||||||
const target = getTargetOverlayWindow();
|
return overlayModalRuntime.sendToActiveOverlayWindow(
|
||||||
if (!target) return;
|
channel,
|
||||||
|
payload,
|
||||||
const { window: targetWindow, layer } = target;
|
runtimeOptions,
|
||||||
const wasVisible = targetWindow.isVisible();
|
);
|
||||||
const restoreOnModalClose = runtimeOptions?.restoreOnModalClose;
|
|
||||||
|
|
||||||
const sendNow = (): void => {
|
|
||||||
if (payload === undefined) {
|
|
||||||
targetWindow.webContents.send(channel);
|
|
||||||
} else {
|
|
||||||
targetWindow.webContents.send(channel, payload);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!wasVisible) {
|
|
||||||
showOverlayWindowForModal(targetWindow, layer);
|
|
||||||
}
|
|
||||||
if (!wasVisible && restoreOnModalClose) {
|
|
||||||
restoreVisibleOverlayOnModalClose.add(restoreOnModalClose);
|
|
||||||
overlayModalAutoShownLayer.set(restoreOnModalClose, layer);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetWindow.webContents.isLoading()) {
|
|
||||||
targetWindow.webContents.once("did-finish-load", () => {
|
|
||||||
if (
|
|
||||||
targetWindow &&
|
|
||||||
!targetWindow.isDestroyed() &&
|
|
||||||
!targetWindow.webContents.isLoading()
|
|
||||||
) {
|
|
||||||
sendNow();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sendNow();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOverlayDebugVisualizationEnabled(enabled: boolean): void {
|
function setOverlayDebugVisualizationEnabled(enabled: boolean): void {
|
||||||
@@ -514,9 +379,7 @@ function setOverlayDebugVisualizationEnabled(enabled: boolean): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openRuntimeOptionsPalette(): void {
|
function openRuntimeOptionsPalette(): void {
|
||||||
sendToActiveOverlayWindow("runtime-options:open", undefined, {
|
overlayModalRuntime.openRuntimeOptionsPalette();
|
||||||
restoreOnModalClose: "runtime-options",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getResolvedConfig() { return configService.getConfig(); }
|
function getResolvedConfig() { return configService.getConfig(); }
|
||||||
@@ -673,12 +536,7 @@ const startupState = runStartupBootstrapRuntimeService(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
appState.initialArgs = startupState.initialArgs;
|
applyStartupState(appState, startupState);
|
||||||
appState.mpvSocketPath = startupState.mpvSocketPath;
|
|
||||||
appState.texthookerPort = startupState.texthookerPort;
|
|
||||||
appState.backendOverride = startupState.backendOverride;
|
|
||||||
appState.autoStartOverlay = startupState.autoStartOverlay;
|
|
||||||
appState.texthookerOnlyMode = startupState.texthookerOnlyMode;
|
|
||||||
|
|
||||||
function createAppLifecycleRuntimeDeps(): AppLifecycleDepsRuntimeOptions {
|
function createAppLifecycleRuntimeDeps(): AppLifecycleDepsRuntimeOptions {
|
||||||
return createAppLifecycleRuntimeDepsBuilder({
|
return createAppLifecycleRuntimeDepsBuilder({
|
||||||
@@ -799,73 +657,69 @@ function handleCliCommand(
|
|||||||
args: CliArgs,
|
args: CliArgs,
|
||||||
source: CliCommandSource = "initial",
|
source: CliCommandSource = "initial",
|
||||||
): void {
|
): void {
|
||||||
const deps = createCliCommandDepsRuntimeService(
|
handleCliCommandRuntimeService(args, source, {
|
||||||
createCliCommandRuntimeServiceDeps({
|
mpv: {
|
||||||
mpv: {
|
getSocketPath: () => appState.mpvSocketPath,
|
||||||
getSocketPath: () => appState.mpvSocketPath,
|
setSocketPath: (socketPath: string) => {
|
||||||
setSocketPath: (socketPath: string) => {
|
appState.mpvSocketPath = socketPath;
|
||||||
appState.mpvSocketPath = socketPath;
|
|
||||||
},
|
|
||||||
getClient: () => appState.mpvClient,
|
|
||||||
showOsd: (text: string) => showMpvOsd(text),
|
|
||||||
},
|
},
|
||||||
texthooker: {
|
getClient: () => appState.mpvClient,
|
||||||
service: texthookerService,
|
showOsd: (text: string) => showMpvOsd(text),
|
||||||
getPort: () => appState.texthookerPort,
|
},
|
||||||
setPort: (port: number) => {
|
texthooker: {
|
||||||
appState.texthookerPort = port;
|
service: texthookerService,
|
||||||
},
|
getPort: () => appState.texthookerPort,
|
||||||
shouldOpenBrowser: () =>
|
setPort: (port: number) => {
|
||||||
getResolvedConfig().texthooker?.openBrowser !== false,
|
appState.texthookerPort = port;
|
||||||
openInBrowser: (url: string) => {
|
|
||||||
void shell.openExternal(url).catch((error) => {
|
|
||||||
console.error(`Failed to open browser for texthooker URL: ${url}`, error);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
overlay: {
|
shouldOpenBrowser: () => getResolvedConfig().texthooker?.openBrowser !== false,
|
||||||
isInitialized: () => appState.overlayRuntimeInitialized,
|
openInBrowser: (url: string) => {
|
||||||
initialize: () => initializeOverlayRuntime(),
|
void shell.openExternal(url).catch((error) => {
|
||||||
toggleVisible: () => toggleVisibleOverlay(),
|
console.error(`Failed to open browser for texthooker URL: ${url}`, error);
|
||||||
toggleInvisible: () => toggleInvisibleOverlay(),
|
});
|
||||||
setVisible: (visible: boolean) => setVisibleOverlayVisible(visible),
|
|
||||||
setInvisible: (visible: boolean) => setInvisibleOverlayVisible(visible),
|
|
||||||
},
|
},
|
||||||
mining: {
|
},
|
||||||
copyCurrentSubtitle: () => copyCurrentSubtitle(),
|
overlay: {
|
||||||
startPendingMultiCopy: (timeoutMs: number) => startPendingMultiCopy(timeoutMs),
|
isInitialized: () => appState.overlayRuntimeInitialized,
|
||||||
mineSentenceCard: () => mineSentenceCard(),
|
initialize: () => initializeOverlayRuntime(),
|
||||||
startPendingMineSentenceMultiple: (timeoutMs: number) =>
|
toggleVisible: () => toggleVisibleOverlay(),
|
||||||
startPendingMineSentenceMultiple(timeoutMs),
|
toggleInvisible: () => toggleInvisibleOverlay(),
|
||||||
updateLastCardFromClipboard: () => updateLastCardFromClipboard(),
|
setVisible: (visible: boolean) => setVisibleOverlayVisible(visible),
|
||||||
triggerFieldGrouping: () => triggerFieldGrouping(),
|
setInvisible: (visible: boolean) => setInvisibleOverlayVisible(visible),
|
||||||
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
},
|
||||||
markLastCardAsAudioCard: () => markLastCardAsAudioCard(),
|
mining: {
|
||||||
},
|
copyCurrentSubtitle: () => copyCurrentSubtitle(),
|
||||||
ui: {
|
startPendingMultiCopy: (timeoutMs: number) => startPendingMultiCopy(timeoutMs),
|
||||||
openYomitanSettings: () => openYomitanSettings(),
|
mineSentenceCard: () => mineSentenceCard(),
|
||||||
cycleSecondarySubMode: () => cycleSecondarySubMode(),
|
startPendingMineSentenceMultiple: (timeoutMs: number) =>
|
||||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
startPendingMineSentenceMultiple(timeoutMs),
|
||||||
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
updateLastCardFromClipboard: () => updateLastCardFromClipboard(),
|
||||||
},
|
triggerFieldGrouping: () => triggerFieldGrouping(),
|
||||||
app: {
|
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||||
stop: () => app.quit(),
|
markLastCardAsAudioCard: () => markLastCardAsAudioCard(),
|
||||||
hasMainWindow: () => Boolean(overlayManager.getMainWindow()),
|
},
|
||||||
},
|
ui: {
|
||||||
getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
|
openYomitanSettings: () => openYomitanSettings(),
|
||||||
schedule: (fn: () => void, delayMs: number) => setTimeout(fn, delayMs),
|
cycleSecondarySubMode: () => cycleSecondarySubMode(),
|
||||||
log: (message: string) => {
|
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||||
console.log(message);
|
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
||||||
},
|
},
|
||||||
warn: (message: string) => {
|
app: {
|
||||||
console.warn(message);
|
stop: () => app.quit(),
|
||||||
},
|
hasMainWindow: () => Boolean(overlayManager.getMainWindow()),
|
||||||
error: (message: string, err: unknown) => {
|
},
|
||||||
console.error(message, err);
|
getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
|
||||||
},
|
schedule: (fn: () => void, delayMs: number) => setTimeout(fn, delayMs),
|
||||||
}),
|
log: (message: string) => {
|
||||||
);
|
console.log(message);
|
||||||
handleCliCommandService(args, source, deps);
|
},
|
||||||
|
warn: (message: string) => {
|
||||||
|
console.warn(message);
|
||||||
|
},
|
||||||
|
error: (message: string, err: unknown) => {
|
||||||
|
console.error(message, err);
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInitialArgs(): void {
|
function handleInitialArgs(): void {
|
||||||
@@ -1218,8 +1072,8 @@ const numericShortcutRuntime = createNumericShortcutRuntimeService({
|
|||||||
const multiCopySession = numericShortcutRuntime.createSession();
|
const multiCopySession = numericShortcutRuntime.createSession();
|
||||||
const mineSentenceSession = numericShortcutRuntime.createSession();
|
const mineSentenceSession = numericShortcutRuntime.createSession();
|
||||||
|
|
||||||
function getSubsyncRuntimeDeps(): SubsyncRuntimeDeps {
|
function getSubsyncRuntimeServiceParams() {
|
||||||
return createSubsyncRuntimeDeps({
|
return {
|
||||||
getMpvClient: () => appState.mpvClient,
|
getMpvClient: () => appState.mpvClient,
|
||||||
getResolvedSubsyncConfig: () => getSubsyncConfig(getResolvedConfig().subsync),
|
getResolvedSubsyncConfig: () => getSubsyncConfig(getResolvedConfig().subsync),
|
||||||
isSubsyncInProgress: () => appState.subsyncInProgress,
|
isSubsyncInProgress: () => appState.subsyncInProgress,
|
||||||
@@ -1232,11 +1086,11 @@ function getSubsyncRuntimeDeps(): SubsyncRuntimeDeps {
|
|||||||
restoreOnModalClose: "subsync",
|
restoreOnModalClose: "subsync",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function triggerSubsyncFromConfig(): Promise<void> {
|
async function triggerSubsyncFromConfig(): Promise<void> {
|
||||||
await triggerSubsyncFromConfigRuntimeService(getSubsyncRuntimeDeps());
|
await triggerSubsyncFromConfigRuntime(getSubsyncRuntimeServiceParams());
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelPendingMultiCopy(): void {
|
function cancelPendingMultiCopy(): void {
|
||||||
@@ -1474,27 +1328,7 @@ function toggleInvisibleOverlay(): void {
|
|||||||
function setOverlayVisible(visible: boolean): void { setVisibleOverlayVisible(visible); }
|
function setOverlayVisible(visible: boolean): void { setVisibleOverlayVisible(visible); }
|
||||||
function toggleOverlay(): void { toggleVisibleOverlay(); }
|
function toggleOverlay(): void { toggleVisibleOverlay(); }
|
||||||
function handleOverlayModalClosed(modal: OverlayHostedModal): void {
|
function handleOverlayModalClosed(modal: OverlayHostedModal): void {
|
||||||
if (!restoreVisibleOverlayOnModalClose.has(modal)) return;
|
overlayModalRuntime.handleOverlayModalClosed(modal);
|
||||||
restoreVisibleOverlayOnModalClose.delete(modal);
|
|
||||||
const layer = overlayModalAutoShownLayer.get(modal);
|
|
||||||
overlayModalAutoShownLayer.delete(modal);
|
|
||||||
if (!layer) return;
|
|
||||||
const shouldKeepLayerVisible = [...restoreVisibleOverlayOnModalClose].some(
|
|
||||||
(pendingModal) => overlayModalAutoShownLayer.get(pendingModal) === layer,
|
|
||||||
);
|
|
||||||
if (shouldKeepLayerVisible) return;
|
|
||||||
|
|
||||||
if (layer === "visible") {
|
|
||||||
const mainWindow = overlayManager.getMainWindow();
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
mainWindow.hide();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const invisibleWindow = overlayManager.getInvisibleWindow();
|
|
||||||
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
|
|
||||||
invisibleWindow.hide();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMpvCommandFromIpc(command: (string | number)[]): void {
|
function handleMpvCommandFromIpc(command: (string | number)[]): void {
|
||||||
@@ -1523,83 +1357,85 @@ function handleMpvCommandFromIpc(command: (string | number)[]): void {
|
|||||||
async function runSubsyncManualFromIpc(
|
async function runSubsyncManualFromIpc(
|
||||||
request: SubsyncManualRunRequest,
|
request: SubsyncManualRunRequest,
|
||||||
): Promise<SubsyncResult> {
|
): Promise<SubsyncResult> {
|
||||||
return runSubsyncManualFromIpcRuntimeService(request, getSubsyncRuntimeDeps());
|
return runSubsyncManualFromIpcRuntime(request, getSubsyncRuntimeServiceParams());
|
||||||
}
|
}
|
||||||
|
|
||||||
const runtimeOptionsIpcDeps = createRuntimeOptionsIpcDeps({
|
function buildIpcRuntimeServicesParams() {
|
||||||
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
return {
|
||||||
showMpvOsd,
|
runtimeOptions: {
|
||||||
});
|
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
||||||
|
showMpvOsd: (text: string) => showMpvOsd(text),
|
||||||
|
},
|
||||||
|
mainDeps: {
|
||||||
|
getInvisibleWindow: () => overlayManager.getInvisibleWindow(),
|
||||||
|
getMainWindow: () => overlayManager.getMainWindow(),
|
||||||
|
getVisibleOverlayVisibility: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
|
getInvisibleOverlayVisibility: () => overlayManager.getInvisibleOverlayVisible(),
|
||||||
|
onOverlayModalClosed: (modal: string) => {
|
||||||
|
handleOverlayModalClosed(modal as OverlayHostedModal);
|
||||||
|
},
|
||||||
|
openYomitanSettings: () => openYomitanSettings(),
|
||||||
|
quitApp: () => app.quit(),
|
||||||
|
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
||||||
|
tokenizeCurrentSubtitle: () => tokenizeSubtitle(appState.currentSubText),
|
||||||
|
getCurrentSubtitleAss: () => appState.currentSubAssText,
|
||||||
|
getMpvSubtitleRenderMetrics: () => appState.mpvSubtitleRenderMetrics,
|
||||||
|
getSubtitlePosition: () => loadSubtitlePosition(),
|
||||||
|
getSubtitleStyle: () => getResolvedConfig().subtitleStyle ?? null,
|
||||||
|
saveSubtitlePosition: (position: unknown) =>
|
||||||
|
saveSubtitlePosition(position as SubtitlePosition),
|
||||||
|
getMecabTokenizer: () => appState.mecabTokenizer,
|
||||||
|
handleMpvCommand: (command: (string | number)[]) =>
|
||||||
|
handleMpvCommandFromIpc(command),
|
||||||
|
getKeybindings: () => appState.keybindings,
|
||||||
|
getSecondarySubMode: () => appState.secondarySubMode,
|
||||||
|
getMpvClient: () => appState.mpvClient,
|
||||||
|
runSubsyncManual: (request: unknown) =>
|
||||||
|
runSubsyncManualFromIpc(request as SubsyncManualRunRequest),
|
||||||
|
getAnkiConnectStatus: () => appState.ankiIntegration !== null,
|
||||||
|
getRuntimeOptions: () => getRuntimeOptionsState(),
|
||||||
|
reportOverlayContentBounds: (payload: unknown) => {
|
||||||
|
overlayContentMeasurementStore.report(payload);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ankiJimakuDeps: {
|
||||||
|
patchAnkiConnectEnabled: (enabled: boolean) => {
|
||||||
|
configService.patchRawConfig({ ankiConnect: { enabled } });
|
||||||
|
},
|
||||||
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
|
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
||||||
|
getSubtitleTimingTracker: () => appState.subtitleTimingTracker,
|
||||||
|
getMpvClient: () => appState.mpvClient,
|
||||||
|
getAnkiIntegration: () => appState.ankiIntegration,
|
||||||
|
setAnkiIntegration: (integration: AnkiIntegration | null) => {
|
||||||
|
appState.ankiIntegration = integration;
|
||||||
|
},
|
||||||
|
showDesktopNotification,
|
||||||
|
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
||||||
|
broadcastRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
||||||
|
getFieldGroupingResolver: () => getFieldGroupingResolver(),
|
||||||
|
setFieldGroupingResolver: (
|
||||||
|
resolver: ((choice: KikuFieldGroupingChoice) => void) | null,
|
||||||
|
) => setFieldGroupingResolver(resolver),
|
||||||
|
parseMediaInfo: (mediaPath: string | null) =>
|
||||||
|
parseMediaInfo(resolveMediaPathForJimaku(mediaPath)),
|
||||||
|
getCurrentMediaPath: () => appState.currentMediaPath,
|
||||||
|
jimakuFetchJson: <T>(
|
||||||
|
endpoint: string,
|
||||||
|
query?: Record<string, string | number | boolean | null | undefined>,
|
||||||
|
): Promise<JimakuApiResponse<T>> =>
|
||||||
|
jimakuFetchJson<T>(endpoint, query),
|
||||||
|
getJimakuMaxEntryResults: () => getJimakuMaxEntryResults(),
|
||||||
|
getJimakuLanguagePreference: () => getJimakuLanguagePreference(),
|
||||||
|
resolveJimakuApiKey: () => resolveJimakuApiKey(),
|
||||||
|
isRemoteMediaPath: (mediaPath: string) => isRemoteMediaPath(mediaPath),
|
||||||
|
downloadToFile: (
|
||||||
|
url: string,
|
||||||
|
destPath: string,
|
||||||
|
headers: Record<string, string>,
|
||||||
|
) => downloadToFile(url, destPath, headers),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
registerMainIpcRuntimeServices({
|
registerIpcRuntimeServices(buildIpcRuntimeServicesParams());
|
||||||
getInvisibleWindow: () => overlayManager.getInvisibleWindow(),
|
|
||||||
getMainWindow: () => overlayManager.getMainWindow(),
|
|
||||||
getVisibleOverlayVisibility: () => overlayManager.getVisibleOverlayVisible(),
|
|
||||||
getInvisibleOverlayVisibility: () => overlayManager.getInvisibleOverlayVisible(),
|
|
||||||
onOverlayModalClosed: (modal) => {
|
|
||||||
handleOverlayModalClosed(modal as OverlayHostedModal);
|
|
||||||
},
|
|
||||||
openYomitanSettings: () => openYomitanSettings(),
|
|
||||||
quitApp: () => app.quit(),
|
|
||||||
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
|
||||||
tokenizeCurrentSubtitle: () => tokenizeSubtitle(appState.currentSubText),
|
|
||||||
getCurrentSubtitleAss: () => appState.currentSubAssText,
|
|
||||||
getMpvSubtitleRenderMetrics: () => appState.mpvSubtitleRenderMetrics,
|
|
||||||
getSubtitlePosition: () => loadSubtitlePosition(),
|
|
||||||
getSubtitleStyle: () => getResolvedConfig().subtitleStyle ?? null,
|
|
||||||
saveSubtitlePosition: (position: unknown) =>
|
|
||||||
saveSubtitlePosition(position as SubtitlePosition),
|
|
||||||
getMecabTokenizer: () => appState.mecabTokenizer,
|
|
||||||
handleMpvCommand: (command: (string | number)[]) =>
|
|
||||||
handleMpvCommandFromIpc(command),
|
|
||||||
getKeybindings: () => appState.keybindings,
|
|
||||||
getSecondarySubMode: () => appState.secondarySubMode,
|
|
||||||
getMpvClient: () => appState.mpvClient,
|
|
||||||
runSubsyncManual: (request: unknown) =>
|
|
||||||
runSubsyncManualFromIpc(request as SubsyncManualRunRequest),
|
|
||||||
getAnkiConnectStatus: () => appState.ankiIntegration !== null,
|
|
||||||
getRuntimeOptions: () => getRuntimeOptionsState(),
|
|
||||||
setRuntimeOption: runtimeOptionsIpcDeps.setRuntimeOption,
|
|
||||||
cycleRuntimeOption: runtimeOptionsIpcDeps.cycleRuntimeOption,
|
|
||||||
reportOverlayContentBounds: (payload: unknown) => {
|
|
||||||
overlayContentMeasurementStore.report(payload);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
registerAnkiJimakuIpcRuntimeServices({
|
|
||||||
patchAnkiConnectEnabled: (enabled: boolean) => {
|
|
||||||
configService.patchRawConfig({ ankiConnect: { enabled } });
|
|
||||||
},
|
|
||||||
getResolvedConfig: () => getResolvedConfig(),
|
|
||||||
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
|
||||||
getSubtitleTimingTracker: () => appState.subtitleTimingTracker,
|
|
||||||
getMpvClient: () => appState.mpvClient,
|
|
||||||
getAnkiIntegration: () => appState.ankiIntegration,
|
|
||||||
setAnkiIntegration: (integration: AnkiIntegration | null) => {
|
|
||||||
appState.ankiIntegration = integration;
|
|
||||||
},
|
|
||||||
showDesktopNotification,
|
|
||||||
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
|
||||||
broadcastRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
|
||||||
getFieldGroupingResolver: () => getFieldGroupingResolver(),
|
|
||||||
setFieldGroupingResolver: (
|
|
||||||
resolver: ((choice: KikuFieldGroupingChoice) => void) | null,
|
|
||||||
) => setFieldGroupingResolver(resolver),
|
|
||||||
parseMediaInfo: (mediaPath: string | null) =>
|
|
||||||
parseMediaInfo(resolveMediaPathForJimaku(mediaPath)),
|
|
||||||
getCurrentMediaPath: () => appState.currentMediaPath,
|
|
||||||
jimakuFetchJson: <T>(
|
|
||||||
endpoint: string,
|
|
||||||
query?: Record<string, string | number | boolean | null | undefined>,
|
|
||||||
): Promise<JimakuApiResponse<T>> =>
|
|
||||||
jimakuFetchJson<T>(endpoint, query),
|
|
||||||
getJimakuMaxEntryResults: () => getJimakuMaxEntryResults(),
|
|
||||||
getJimakuLanguagePreference: () => getJimakuLanguagePreference(),
|
|
||||||
resolveJimakuApiKey: () => resolveJimakuApiKey(),
|
|
||||||
isRemoteMediaPath: (mediaPath: string) => isRemoteMediaPath(mediaPath),
|
|
||||||
downloadToFile: (
|
|
||||||
url: string,
|
|
||||||
destPath: string,
|
|
||||||
headers: Record<string, string>,
|
|
||||||
) => downloadToFile(url, destPath, headers),
|
|
||||||
});
|
|
||||||
|
|||||||
15
src/main/cli-runtime.ts
Normal file
15
src/main/cli-runtime.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { handleCliCommandService, createCliCommandDepsRuntimeService } from "../core/services";
|
||||||
|
import type { CliArgs, CliCommandSource } from "../cli/args";
|
||||||
|
import { createCliCommandRuntimeServiceDeps, CliCommandRuntimeServiceDepsParams } from "./dependencies";
|
||||||
|
|
||||||
|
export function handleCliCommandRuntimeService(
|
||||||
|
args: CliArgs,
|
||||||
|
source: CliCommandSource,
|
||||||
|
params: CliCommandRuntimeServiceDepsParams,
|
||||||
|
): void {
|
||||||
|
const deps = createCliCommandDepsRuntimeService(
|
||||||
|
createCliCommandRuntimeServiceDeps(params),
|
||||||
|
);
|
||||||
|
handleCliCommandService(args, source, deps);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -8,8 +8,19 @@ import {
|
|||||||
AnkiJimakuIpcRuntimeServiceDepsParams,
|
AnkiJimakuIpcRuntimeServiceDepsParams,
|
||||||
createMainIpcRuntimeServiceDeps,
|
createMainIpcRuntimeServiceDeps,
|
||||||
MainIpcRuntimeServiceDepsParams,
|
MainIpcRuntimeServiceDepsParams,
|
||||||
|
createRuntimeOptionsIpcDeps,
|
||||||
|
RuntimeOptionsIpcDepsParams,
|
||||||
} from "./dependencies";
|
} from "./dependencies";
|
||||||
|
|
||||||
|
export interface RegisterIpcRuntimeServicesParams {
|
||||||
|
runtimeOptions: RuntimeOptionsIpcDepsParams;
|
||||||
|
mainDeps: Omit<
|
||||||
|
MainIpcRuntimeServiceDepsParams,
|
||||||
|
"setRuntimeOption" | "cycleRuntimeOption"
|
||||||
|
>;
|
||||||
|
ankiJimakuDeps: AnkiJimakuIpcRuntimeServiceDepsParams;
|
||||||
|
}
|
||||||
|
|
||||||
export function registerMainIpcRuntimeServices(
|
export function registerMainIpcRuntimeServices(
|
||||||
params: MainIpcRuntimeServiceDepsParams,
|
params: MainIpcRuntimeServiceDepsParams,
|
||||||
): void {
|
): void {
|
||||||
@@ -26,3 +37,17 @@ export function registerAnkiJimakuIpcRuntimeServices(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function registerIpcRuntimeServices(
|
||||||
|
params: RegisterIpcRuntimeServicesParams,
|
||||||
|
): void {
|
||||||
|
const runtimeOptionsIpcDeps = createRuntimeOptionsIpcDeps({
|
||||||
|
getRuntimeOptionsManager: params.runtimeOptions.getRuntimeOptionsManager,
|
||||||
|
showMpvOsd: params.runtimeOptions.showMpvOsd,
|
||||||
|
});
|
||||||
|
registerMainIpcRuntimeServices({
|
||||||
|
...params.mainDeps,
|
||||||
|
setRuntimeOption: runtimeOptionsIpcDeps.setRuntimeOption,
|
||||||
|
cycleRuntimeOption: runtimeOptionsIpcDeps.cycleRuntimeOption,
|
||||||
|
});
|
||||||
|
registerAnkiJimakuIpcRuntimeServices(params.ankiJimakuDeps);
|
||||||
|
}
|
||||||
|
|||||||
140
src/main/overlay-runtime.ts
Normal file
140
src/main/overlay-runtime.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import type { BrowserWindow } from "electron";
|
||||||
|
|
||||||
|
type OverlayHostedModal = "runtime-options" | "subsync" | "jimaku";
|
||||||
|
type OverlayHostLayer = "visible" | "invisible";
|
||||||
|
|
||||||
|
export interface OverlayWindowResolver {
|
||||||
|
getMainWindow: () => BrowserWindow | null;
|
||||||
|
getInvisibleWindow: () => BrowserWindow | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OverlayModalRuntime {
|
||||||
|
sendToActiveOverlayWindow: (
|
||||||
|
channel: string,
|
||||||
|
payload?: unknown,
|
||||||
|
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
|
||||||
|
) => boolean;
|
||||||
|
openRuntimeOptionsPalette: () => void;
|
||||||
|
handleOverlayModalClosed: (modal: OverlayHostedModal) => void;
|
||||||
|
getRestoreVisibleOverlayOnModalClose: () => Set<OverlayHostedModal>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createOverlayModalRuntimeService(
|
||||||
|
deps: OverlayWindowResolver,
|
||||||
|
): OverlayModalRuntime {
|
||||||
|
const restoreVisibleOverlayOnModalClose = new Set<OverlayHostedModal>();
|
||||||
|
const overlayModalAutoShownLayer = new Map<OverlayHostedModal, OverlayHostLayer>();
|
||||||
|
|
||||||
|
const getTargetOverlayWindow = (): {
|
||||||
|
window: BrowserWindow;
|
||||||
|
layer: OverlayHostLayer;
|
||||||
|
} | null => {
|
||||||
|
const visibleMainWindow = deps.getMainWindow();
|
||||||
|
const invisibleWindow = deps.getInvisibleWindow();
|
||||||
|
|
||||||
|
if (visibleMainWindow && !visibleMainWindow.isDestroyed()) {
|
||||||
|
return { window: visibleMainWindow, layer: "visible" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
|
||||||
|
return { window: invisibleWindow, layer: "invisible" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showOverlayWindowForModal = (window: BrowserWindow, layer: OverlayHostLayer): void => {
|
||||||
|
if (layer === "invisible" && typeof window.showInactive === "function") {
|
||||||
|
window.showInactive();
|
||||||
|
} else {
|
||||||
|
window.show();
|
||||||
|
}
|
||||||
|
if (!window.isFocused()) {
|
||||||
|
window.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendToActiveOverlayWindow = (
|
||||||
|
channel: string,
|
||||||
|
payload?: unknown,
|
||||||
|
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
|
||||||
|
): boolean => {
|
||||||
|
const target = getTargetOverlayWindow();
|
||||||
|
if (!target) return false;
|
||||||
|
|
||||||
|
const { window: targetWindow, layer } = target;
|
||||||
|
const wasVisible = targetWindow.isVisible();
|
||||||
|
const restoreOnModalClose = runtimeOptions?.restoreOnModalClose;
|
||||||
|
|
||||||
|
const sendNow = (): void => {
|
||||||
|
if (payload === undefined) {
|
||||||
|
targetWindow.webContents.send(channel);
|
||||||
|
} else {
|
||||||
|
targetWindow.webContents.send(channel, payload);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!wasVisible) {
|
||||||
|
showOverlayWindowForModal(targetWindow, layer);
|
||||||
|
}
|
||||||
|
if (!wasVisible && restoreOnModalClose) {
|
||||||
|
restoreVisibleOverlayOnModalClose.add(restoreOnModalClose);
|
||||||
|
overlayModalAutoShownLayer.set(restoreOnModalClose, layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetWindow.webContents.isLoading()) {
|
||||||
|
targetWindow.webContents.once("did-finish-load", () => {
|
||||||
|
if (
|
||||||
|
targetWindow &&
|
||||||
|
!targetWindow.isDestroyed() &&
|
||||||
|
!targetWindow.webContents.isLoading()
|
||||||
|
) {
|
||||||
|
sendNow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendNow();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openRuntimeOptionsPalette = (): void => {
|
||||||
|
sendToActiveOverlayWindow("runtime-options:open", undefined, {
|
||||||
|
restoreOnModalClose: "runtime-options",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOverlayModalClosed = (modal: OverlayHostedModal): void => {
|
||||||
|
if (!restoreVisibleOverlayOnModalClose.has(modal)) return;
|
||||||
|
restoreVisibleOverlayOnModalClose.delete(modal);
|
||||||
|
const layer = overlayModalAutoShownLayer.get(modal);
|
||||||
|
overlayModalAutoShownLayer.delete(modal);
|
||||||
|
if (!layer) return;
|
||||||
|
const shouldKeepLayerVisible = [...restoreVisibleOverlayOnModalClose].some(
|
||||||
|
(pendingModal) => overlayModalAutoShownLayer.get(pendingModal) === layer,
|
||||||
|
);
|
||||||
|
if (shouldKeepLayerVisible) return;
|
||||||
|
|
||||||
|
if (layer === "visible") {
|
||||||
|
const mainWindow = deps.getMainWindow();
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.hide();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const invisibleWindow = deps.getInvisibleWindow();
|
||||||
|
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
|
||||||
|
invisibleWindow.hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
sendToActiveOverlayWindow,
|
||||||
|
openRuntimeOptionsPalette,
|
||||||
|
handleOverlayModalClosed,
|
||||||
|
getRestoreVisibleOverlayOnModalClose: () => restoreVisibleOverlayOnModalClose,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { OverlayHostedModal };
|
||||||
128
src/main/state.ts
Normal file
128
src/main/state.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import type { BrowserWindow, Extension } from "electron";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Keybinding,
|
||||||
|
MpvSubtitleRenderMetrics,
|
||||||
|
SecondarySubMode,
|
||||||
|
SubtitlePosition,
|
||||||
|
KikuFieldGroupingChoice,
|
||||||
|
} from "../types";
|
||||||
|
import type { CliArgs } from "../cli/args";
|
||||||
|
import type { SubtitleTimingTracker } from "../subtitle-timing-tracker";
|
||||||
|
import type { AnkiIntegration } from "../anki-integration";
|
||||||
|
import type { MpvIpcClient } from "../core/services";
|
||||||
|
import { DEFAULT_MPV_SUBTITLE_RENDER_METRICS } from "../core/services";
|
||||||
|
import type { RuntimeOptionsManager } from "../runtime-options";
|
||||||
|
import type { MecabTokenizer } from "../mecab-tokenizer";
|
||||||
|
import type { BaseWindowTracker } from "../window-trackers";
|
||||||
|
|
||||||
|
export interface AppState {
|
||||||
|
yomitanExt: Extension | null;
|
||||||
|
yomitanSettingsWindow: BrowserWindow | null;
|
||||||
|
yomitanParserWindow: BrowserWindow | null;
|
||||||
|
yomitanParserReadyPromise: Promise<void> | null;
|
||||||
|
yomitanParserInitPromise: Promise<boolean> | null;
|
||||||
|
mpvClient: MpvIpcClient | null;
|
||||||
|
reconnectTimer: ReturnType<typeof setTimeout> | null;
|
||||||
|
currentSubText: string;
|
||||||
|
currentSubAssText: string;
|
||||||
|
windowTracker: BaseWindowTracker | null;
|
||||||
|
subtitlePosition: SubtitlePosition | null;
|
||||||
|
currentMediaPath: string | null;
|
||||||
|
currentMediaTitle: string | null;
|
||||||
|
pendingSubtitlePosition: SubtitlePosition | null;
|
||||||
|
mecabTokenizer: MecabTokenizer | null;
|
||||||
|
keybindings: Keybinding[];
|
||||||
|
subtitleTimingTracker: SubtitleTimingTracker | null;
|
||||||
|
ankiIntegration: AnkiIntegration | null;
|
||||||
|
secondarySubMode: SecondarySubMode;
|
||||||
|
lastSecondarySubToggleAtMs: number;
|
||||||
|
previousSecondarySubVisibility: boolean | null;
|
||||||
|
mpvSubtitleRenderMetrics: MpvSubtitleRenderMetrics;
|
||||||
|
shortcutsRegistered: boolean;
|
||||||
|
overlayRuntimeInitialized: boolean;
|
||||||
|
fieldGroupingResolver: ((choice: KikuFieldGroupingChoice) => void) | null;
|
||||||
|
fieldGroupingResolverSequence: number;
|
||||||
|
runtimeOptionsManager: RuntimeOptionsManager | null;
|
||||||
|
trackerNotReadyWarningShown: boolean;
|
||||||
|
overlayDebugVisualizationEnabled: boolean;
|
||||||
|
subsyncInProgress: boolean;
|
||||||
|
initialArgs: CliArgs | null;
|
||||||
|
mpvSocketPath: string;
|
||||||
|
texthookerPort: number;
|
||||||
|
backendOverride: string | null;
|
||||||
|
autoStartOverlay: boolean;
|
||||||
|
texthookerOnlyMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppStateInitialValues {
|
||||||
|
mpvSocketPath: string;
|
||||||
|
texthookerPort: number;
|
||||||
|
backendOverride?: string | null;
|
||||||
|
autoStartOverlay?: boolean;
|
||||||
|
texthookerOnlyMode?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StartupState {
|
||||||
|
initialArgs: Exclude<AppState["initialArgs"], null>;
|
||||||
|
mpvSocketPath: AppState["mpvSocketPath"];
|
||||||
|
texthookerPort: AppState["texthookerPort"];
|
||||||
|
backendOverride: AppState["backendOverride"];
|
||||||
|
autoStartOverlay: AppState["autoStartOverlay"];
|
||||||
|
texthookerOnlyMode: AppState["texthookerOnlyMode"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAppState(values: AppStateInitialValues): AppState {
|
||||||
|
return {
|
||||||
|
yomitanExt: null,
|
||||||
|
yomitanSettingsWindow: null,
|
||||||
|
yomitanParserWindow: null,
|
||||||
|
yomitanParserReadyPromise: null,
|
||||||
|
yomitanParserInitPromise: null,
|
||||||
|
mpvClient: null,
|
||||||
|
reconnectTimer: null,
|
||||||
|
currentSubText: "",
|
||||||
|
currentSubAssText: "",
|
||||||
|
windowTracker: null,
|
||||||
|
subtitlePosition: null,
|
||||||
|
currentMediaPath: null,
|
||||||
|
currentMediaTitle: null,
|
||||||
|
pendingSubtitlePosition: null,
|
||||||
|
mecabTokenizer: null,
|
||||||
|
keybindings: [],
|
||||||
|
subtitleTimingTracker: null,
|
||||||
|
ankiIntegration: null,
|
||||||
|
secondarySubMode: "hover",
|
||||||
|
lastSecondarySubToggleAtMs: 0,
|
||||||
|
previousSecondarySubVisibility: null,
|
||||||
|
mpvSubtitleRenderMetrics: {
|
||||||
|
...DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
|
||||||
|
},
|
||||||
|
runtimeOptionsManager: null,
|
||||||
|
trackerNotReadyWarningShown: false,
|
||||||
|
overlayDebugVisualizationEnabled: false,
|
||||||
|
shortcutsRegistered: false,
|
||||||
|
overlayRuntimeInitialized: false,
|
||||||
|
fieldGroupingResolver: null,
|
||||||
|
fieldGroupingResolverSequence: 0,
|
||||||
|
subsyncInProgress: false,
|
||||||
|
initialArgs: null,
|
||||||
|
mpvSocketPath: values.mpvSocketPath,
|
||||||
|
texthookerPort: values.texthookerPort,
|
||||||
|
backendOverride: values.backendOverride ?? null,
|
||||||
|
autoStartOverlay: values.autoStartOverlay ?? false,
|
||||||
|
texthookerOnlyMode: values.texthookerOnlyMode ?? false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyStartupState(
|
||||||
|
appState: AppState,
|
||||||
|
startupState: StartupState,
|
||||||
|
): void {
|
||||||
|
appState.initialArgs = startupState.initialArgs;
|
||||||
|
appState.mpvSocketPath = startupState.mpvSocketPath;
|
||||||
|
appState.texthookerPort = startupState.texthookerPort;
|
||||||
|
appState.backendOverride = startupState.backendOverride;
|
||||||
|
appState.autoStartOverlay = startupState.autoStartOverlay;
|
||||||
|
appState.texthookerOnlyMode = startupState.texthookerOnlyMode;
|
||||||
|
}
|
||||||
43
src/main/subsync-runtime.ts
Normal file
43
src/main/subsync-runtime.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { SubsyncResolvedConfig } from "../subsync/utils";
|
||||||
|
import type { SubsyncManualPayload, SubsyncManualRunRequest, SubsyncResult } from "../types";
|
||||||
|
import type { SubsyncRuntimeDeps } from "../core/services/subsync-runner-service";
|
||||||
|
import { createSubsyncRuntimeDeps } from "./dependencies";
|
||||||
|
import { runSubsyncManualFromIpcRuntimeService, triggerSubsyncFromConfigRuntimeService } from "../core/services";
|
||||||
|
|
||||||
|
export interface SubsyncRuntimeServiceInput {
|
||||||
|
getMpvClient: SubsyncRuntimeDeps["getMpvClient"];
|
||||||
|
getResolvedSubsyncConfig: () => SubsyncResolvedConfig;
|
||||||
|
isSubsyncInProgress: SubsyncRuntimeDeps["isSubsyncInProgress"];
|
||||||
|
setSubsyncInProgress: SubsyncRuntimeDeps["setSubsyncInProgress"];
|
||||||
|
showMpvOsd: SubsyncRuntimeDeps["showMpvOsd"];
|
||||||
|
openManualPicker: (payload: SubsyncManualPayload) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSubsyncRuntimeServiceDeps(
|
||||||
|
params: SubsyncRuntimeServiceInput,
|
||||||
|
): SubsyncRuntimeDeps {
|
||||||
|
return createSubsyncRuntimeDeps({
|
||||||
|
getMpvClient: params.getMpvClient,
|
||||||
|
getResolvedSubsyncConfig: params.getResolvedSubsyncConfig,
|
||||||
|
isSubsyncInProgress: params.isSubsyncInProgress,
|
||||||
|
setSubsyncInProgress: params.setSubsyncInProgress,
|
||||||
|
showMpvOsd: params.showMpvOsd,
|
||||||
|
openManualPicker: params.openManualPicker,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function triggerSubsyncFromConfigRuntime(
|
||||||
|
params: SubsyncRuntimeServiceInput,
|
||||||
|
): Promise<void> {
|
||||||
|
return triggerSubsyncFromConfigRuntimeService(createSubsyncRuntimeServiceDeps(params));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runSubsyncManualFromIpcRuntime(
|
||||||
|
request: SubsyncManualRunRequest,
|
||||||
|
params: SubsyncRuntimeServiceInput,
|
||||||
|
): Promise<SubsyncResult> {
|
||||||
|
return runSubsyncManualFromIpcRuntimeService(
|
||||||
|
request,
|
||||||
|
createSubsyncRuntimeServiceDeps(params),
|
||||||
|
);
|
||||||
|
}
|
||||||
2
vendor/texthooker-ui
vendored
2
vendor/texthooker-ui
vendored
Submodule vendor/texthooker-ui updated: 3c1150740a...c969bf5a81
Reference in New Issue
Block a user