mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -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
|
||||
THEME_FILE := subminer.rasi
|
||||
@@ -48,6 +48,7 @@ help:
|
||||
" build-linux Build Linux AppImage" \
|
||||
" build-macos Build macOS DMG/ZIP (signed if configured)" \
|
||||
" 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-macos Build and launch local Electron app with macOS tracker backend" \
|
||||
" 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 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
|
||||
@pnpm run build
|
||||
@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
|
||||
- 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)
|
||||
- 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
|
||||
|
||||
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";
|
||||
import {
|
||||
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_SECONDARY,
|
||||
MpvMessage,
|
||||
MpvProtocolHandleMessageDeps,
|
||||
splitMpvMessagesFromBuffer,
|
||||
} from "./mpv-protocol";
|
||||
import { requestMpvInitialState, subscribeToMpvProperties } from "./mpv-properties";
|
||||
import { getMpvReconnectDelay } from "./mpv-transport";
|
||||
|
||||
export {
|
||||
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
||||
@@ -170,8 +150,8 @@ export class MpvIpcClient implements MpvClient {
|
||||
this.reconnectAttempt = 0;
|
||||
this.hasConnectedOnce = true;
|
||||
this.setSecondarySubVisibility(false);
|
||||
this.subscribeToProperties();
|
||||
this.getInitialState();
|
||||
subscribeToMpvProperties(this.send.bind(this));
|
||||
requestMpvInitialState(this.send.bind(this));
|
||||
|
||||
const shouldAutoStart =
|
||||
this.deps.autoStartOverlay ||
|
||||
@@ -217,28 +197,7 @@ export class MpvIpcClient implements MpvClient {
|
||||
clearTimeout(reconnectTimer);
|
||||
}
|
||||
const attempt = this.reconnectAttempt++;
|
||||
let delay: number;
|
||||
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;
|
||||
}
|
||||
}
|
||||
const delay = getMpvReconnectDelay(attempt, this.hasConnectedOnce);
|
||||
this.deps.setReconnectTimer(
|
||||
setTimeout(() => {
|
||||
console.log(
|
||||
@@ -457,128 +416,6 @@ export class MpvIpcClient implements MpvClient {
|
||||
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 {
|
||||
this.send({
|
||||
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 fs from "fs";
|
||||
import { MecabTokenizer } from "./mecab-tokenizer";
|
||||
import { BaseWindowTracker } from "./window-trackers";
|
||||
import type {
|
||||
JimakuApiResponse,
|
||||
JimakuLanguagePreference,
|
||||
SubtitleData,
|
||||
SubtitlePosition,
|
||||
Keybinding,
|
||||
WindowGeometry,
|
||||
SecondarySubMode,
|
||||
SubsyncManualPayload,
|
||||
SubsyncManualRunRequest,
|
||||
SubsyncResult,
|
||||
KikuFieldGroupingChoice,
|
||||
KikuMergePreviewRequest,
|
||||
KikuMergePreviewResponse,
|
||||
RuntimeOptionState,
|
||||
MpvSubtitleRenderMetrics,
|
||||
ResolvedConfig,
|
||||
@@ -94,7 +90,6 @@ import {
|
||||
broadcastRuntimeOptionsChangedRuntimeService,
|
||||
copyCurrentSubtitleService,
|
||||
createAppLifecycleDepsRuntimeService,
|
||||
createCliCommandDepsRuntimeService,
|
||||
createOverlayManagerService,
|
||||
createFieldGroupingOverlayRuntimeService,
|
||||
createNumericShortcutRuntimeService,
|
||||
@@ -108,7 +103,6 @@ import {
|
||||
getInitialInvisibleOverlayVisibilityService,
|
||||
getJimakuLanguagePreferenceService,
|
||||
getJimakuMaxEntryResultsService,
|
||||
handleCliCommandService,
|
||||
handleMineSentenceDigitService,
|
||||
handleMultiCopyDigitService,
|
||||
hasMpvWebsocketPlugin,
|
||||
@@ -128,7 +122,6 @@ import {
|
||||
replayCurrentSubtitleRuntimeService,
|
||||
resolveJimakuApiKeyService,
|
||||
runStartupBootstrapRuntimeService,
|
||||
runSubsyncManualFromIpcRuntimeService,
|
||||
saveSubtitlePositionService,
|
||||
sendMpvCommandRuntimeService,
|
||||
setInvisibleOverlayVisibleService,
|
||||
@@ -144,7 +137,6 @@ import {
|
||||
syncOverlayShortcutsRuntimeService,
|
||||
tokenizeSubtitleService,
|
||||
triggerFieldGroupingService,
|
||||
triggerSubsyncFromConfigRuntimeService,
|
||||
unregisterOverlayShortcutsRuntimeService,
|
||||
updateCurrentMediaPathService,
|
||||
updateInvisibleOverlayVisibilityService,
|
||||
@@ -156,22 +148,28 @@ import {
|
||||
runAppReadyRuntimeService,
|
||||
} 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 {
|
||||
createRuntimeOptionsIpcDeps,
|
||||
createCliCommandRuntimeServiceDeps,
|
||||
createSubsyncRuntimeDeps,
|
||||
} from "./main/dependencies";
|
||||
import {
|
||||
createAppLifecycleRuntimeDeps as createAppLifecycleRuntimeDepsBuilder,
|
||||
createAppReadyRuntimeDeps as createAppReadyRuntimeDepsBuilder,
|
||||
} from "./main/app-lifecycle";
|
||||
import { handleMpvCommandFromIpcRuntime } from "./main/ipc-mpv-command";
|
||||
import {
|
||||
registerAnkiJimakuIpcRuntimeServices,
|
||||
registerMainIpcRuntimeServices,
|
||||
registerIpcRuntimeServices,
|
||||
} 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 {
|
||||
ConfigService,
|
||||
@@ -284,90 +282,14 @@ const overlayContentMeasurementStore = createOverlayContentMeasurementStoreServi
|
||||
console.warn(message);
|
||||
},
|
||||
});
|
||||
type OverlayHostedModal = "runtime-options" | "subsync" | "jimaku";
|
||||
type OverlayHostLayer = "visible" | "invisible";
|
||||
const restoreVisibleOverlayOnModalClose = new Set<OverlayHostedModal>();
|
||||
const overlayModalAutoShownLayer = new Map<OverlayHostedModal, OverlayHostLayer>();
|
||||
|
||||
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,
|
||||
const overlayModalRuntime = createOverlayModalRuntimeService({
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
getInvisibleWindow: () => overlayManager.getInvisibleWindow(),
|
||||
});
|
||||
const appState = createAppState({
|
||||
mpvSocketPath: getDefaultSocketPath(),
|
||||
texthookerPort: DEFAULT_TEXTHOOKER_PORT,
|
||||
backendOverride: null,
|
||||
autoStartOverlay: false,
|
||||
texthookerOnlyMode: false,
|
||||
};
|
||||
});
|
||||
|
||||
function getFieldGroupingResolver(): ((choice: KikuFieldGroupingChoice) => void) | null {
|
||||
return appState.fieldGroupingResolver;
|
||||
@@ -396,10 +318,14 @@ const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntimeService<Ove
|
||||
setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible),
|
||||
getResolver: () => getFieldGroupingResolver(),
|
||||
setResolver: (resolver) => setFieldGroupingResolver(resolver),
|
||||
getRestoreVisibleOverlayOnModalClose: () => restoreVisibleOverlayOnModalClose,
|
||||
sendToVisibleOverlay: (channel, payload) => {
|
||||
sendToActiveOverlayWindow(channel, payload);
|
||||
return true;
|
||||
getRestoreVisibleOverlayOnModalClose: () =>
|
||||
overlayModalRuntime.getRestoreVisibleOverlayOnModalClose(),
|
||||
sendToVisibleOverlay: (channel, payload, runtimeOptions) => {
|
||||
return overlayModalRuntime.sendToActiveOverlayWindow(
|
||||
channel,
|
||||
payload,
|
||||
runtimeOptions,
|
||||
);
|
||||
},
|
||||
});
|
||||
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(
|
||||
channel: string,
|
||||
payload?: unknown,
|
||||
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
|
||||
): void {
|
||||
const target = getTargetOverlayWindow();
|
||||
if (!target) return;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
sendNow();
|
||||
): boolean {
|
||||
return overlayModalRuntime.sendToActiveOverlayWindow(
|
||||
channel,
|
||||
payload,
|
||||
runtimeOptions,
|
||||
);
|
||||
}
|
||||
|
||||
function setOverlayDebugVisualizationEnabled(enabled: boolean): void {
|
||||
@@ -514,9 +379,7 @@ function setOverlayDebugVisualizationEnabled(enabled: boolean): void {
|
||||
}
|
||||
|
||||
function openRuntimeOptionsPalette(): void {
|
||||
sendToActiveOverlayWindow("runtime-options:open", undefined, {
|
||||
restoreOnModalClose: "runtime-options",
|
||||
});
|
||||
overlayModalRuntime.openRuntimeOptionsPalette();
|
||||
}
|
||||
|
||||
function getResolvedConfig() { return configService.getConfig(); }
|
||||
@@ -673,12 +536,7 @@ const startupState = runStartupBootstrapRuntimeService(
|
||||
}),
|
||||
);
|
||||
|
||||
appState.initialArgs = startupState.initialArgs;
|
||||
appState.mpvSocketPath = startupState.mpvSocketPath;
|
||||
appState.texthookerPort = startupState.texthookerPort;
|
||||
appState.backendOverride = startupState.backendOverride;
|
||||
appState.autoStartOverlay = startupState.autoStartOverlay;
|
||||
appState.texthookerOnlyMode = startupState.texthookerOnlyMode;
|
||||
applyStartupState(appState, startupState);
|
||||
|
||||
function createAppLifecycleRuntimeDeps(): AppLifecycleDepsRuntimeOptions {
|
||||
return createAppLifecycleRuntimeDepsBuilder({
|
||||
@@ -799,73 +657,69 @@ function handleCliCommand(
|
||||
args: CliArgs,
|
||||
source: CliCommandSource = "initial",
|
||||
): void {
|
||||
const deps = createCliCommandDepsRuntimeService(
|
||||
createCliCommandRuntimeServiceDeps({
|
||||
mpv: {
|
||||
getSocketPath: () => appState.mpvSocketPath,
|
||||
setSocketPath: (socketPath: string) => {
|
||||
appState.mpvSocketPath = socketPath;
|
||||
},
|
||||
getClient: () => appState.mpvClient,
|
||||
showOsd: (text: string) => showMpvOsd(text),
|
||||
handleCliCommandRuntimeService(args, source, {
|
||||
mpv: {
|
||||
getSocketPath: () => appState.mpvSocketPath,
|
||||
setSocketPath: (socketPath: string) => {
|
||||
appState.mpvSocketPath = socketPath;
|
||||
},
|
||||
texthooker: {
|
||||
service: texthookerService,
|
||||
getPort: () => appState.texthookerPort,
|
||||
setPort: (port: number) => {
|
||||
appState.texthookerPort = port;
|
||||
},
|
||||
shouldOpenBrowser: () =>
|
||||
getResolvedConfig().texthooker?.openBrowser !== false,
|
||||
openInBrowser: (url: string) => {
|
||||
void shell.openExternal(url).catch((error) => {
|
||||
console.error(`Failed to open browser for texthooker URL: ${url}`, error);
|
||||
});
|
||||
},
|
||||
getClient: () => appState.mpvClient,
|
||||
showOsd: (text: string) => showMpvOsd(text),
|
||||
},
|
||||
texthooker: {
|
||||
service: texthookerService,
|
||||
getPort: () => appState.texthookerPort,
|
||||
setPort: (port: number) => {
|
||||
appState.texthookerPort = port;
|
||||
},
|
||||
overlay: {
|
||||
isInitialized: () => appState.overlayRuntimeInitialized,
|
||||
initialize: () => initializeOverlayRuntime(),
|
||||
toggleVisible: () => toggleVisibleOverlay(),
|
||||
toggleInvisible: () => toggleInvisibleOverlay(),
|
||||
setVisible: (visible: boolean) => setVisibleOverlayVisible(visible),
|
||||
setInvisible: (visible: boolean) => setInvisibleOverlayVisible(visible),
|
||||
shouldOpenBrowser: () => getResolvedConfig().texthooker?.openBrowser !== false,
|
||||
openInBrowser: (url: string) => {
|
||||
void shell.openExternal(url).catch((error) => {
|
||||
console.error(`Failed to open browser for texthooker URL: ${url}`, error);
|
||||
});
|
||||
},
|
||||
mining: {
|
||||
copyCurrentSubtitle: () => copyCurrentSubtitle(),
|
||||
startPendingMultiCopy: (timeoutMs: number) => startPendingMultiCopy(timeoutMs),
|
||||
mineSentenceCard: () => mineSentenceCard(),
|
||||
startPendingMineSentenceMultiple: (timeoutMs: number) =>
|
||||
startPendingMineSentenceMultiple(timeoutMs),
|
||||
updateLastCardFromClipboard: () => updateLastCardFromClipboard(),
|
||||
triggerFieldGrouping: () => triggerFieldGrouping(),
|
||||
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||
markLastCardAsAudioCard: () => markLastCardAsAudioCard(),
|
||||
},
|
||||
ui: {
|
||||
openYomitanSettings: () => openYomitanSettings(),
|
||||
cycleSecondarySubMode: () => cycleSecondarySubMode(),
|
||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
||||
},
|
||||
app: {
|
||||
stop: () => app.quit(),
|
||||
hasMainWindow: () => Boolean(overlayManager.getMainWindow()),
|
||||
},
|
||||
getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
|
||||
schedule: (fn: () => void, delayMs: number) => setTimeout(fn, delayMs),
|
||||
log: (message: string) => {
|
||||
console.log(message);
|
||||
},
|
||||
warn: (message: string) => {
|
||||
console.warn(message);
|
||||
},
|
||||
error: (message: string, err: unknown) => {
|
||||
console.error(message, err);
|
||||
},
|
||||
}),
|
||||
);
|
||||
handleCliCommandService(args, source, deps);
|
||||
},
|
||||
overlay: {
|
||||
isInitialized: () => appState.overlayRuntimeInitialized,
|
||||
initialize: () => initializeOverlayRuntime(),
|
||||
toggleVisible: () => toggleVisibleOverlay(),
|
||||
toggleInvisible: () => toggleInvisibleOverlay(),
|
||||
setVisible: (visible: boolean) => setVisibleOverlayVisible(visible),
|
||||
setInvisible: (visible: boolean) => setInvisibleOverlayVisible(visible),
|
||||
},
|
||||
mining: {
|
||||
copyCurrentSubtitle: () => copyCurrentSubtitle(),
|
||||
startPendingMultiCopy: (timeoutMs: number) => startPendingMultiCopy(timeoutMs),
|
||||
mineSentenceCard: () => mineSentenceCard(),
|
||||
startPendingMineSentenceMultiple: (timeoutMs: number) =>
|
||||
startPendingMineSentenceMultiple(timeoutMs),
|
||||
updateLastCardFromClipboard: () => updateLastCardFromClipboard(),
|
||||
triggerFieldGrouping: () => triggerFieldGrouping(),
|
||||
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||
markLastCardAsAudioCard: () => markLastCardAsAudioCard(),
|
||||
},
|
||||
ui: {
|
||||
openYomitanSettings: () => openYomitanSettings(),
|
||||
cycleSecondarySubMode: () => cycleSecondarySubMode(),
|
||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
||||
},
|
||||
app: {
|
||||
stop: () => app.quit(),
|
||||
hasMainWindow: () => Boolean(overlayManager.getMainWindow()),
|
||||
},
|
||||
getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
|
||||
schedule: (fn: () => void, delayMs: number) => setTimeout(fn, delayMs),
|
||||
log: (message: string) => {
|
||||
console.log(message);
|
||||
},
|
||||
warn: (message: string) => {
|
||||
console.warn(message);
|
||||
},
|
||||
error: (message: string, err: unknown) => {
|
||||
console.error(message, err);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleInitialArgs(): void {
|
||||
@@ -1218,8 +1072,8 @@ const numericShortcutRuntime = createNumericShortcutRuntimeService({
|
||||
const multiCopySession = numericShortcutRuntime.createSession();
|
||||
const mineSentenceSession = numericShortcutRuntime.createSession();
|
||||
|
||||
function getSubsyncRuntimeDeps(): SubsyncRuntimeDeps {
|
||||
return createSubsyncRuntimeDeps({
|
||||
function getSubsyncRuntimeServiceParams() {
|
||||
return {
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
getResolvedSubsyncConfig: () => getSubsyncConfig(getResolvedConfig().subsync),
|
||||
isSubsyncInProgress: () => appState.subsyncInProgress,
|
||||
@@ -1232,11 +1086,11 @@ function getSubsyncRuntimeDeps(): SubsyncRuntimeDeps {
|
||||
restoreOnModalClose: "subsync",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
async function triggerSubsyncFromConfig(): Promise<void> {
|
||||
await triggerSubsyncFromConfigRuntimeService(getSubsyncRuntimeDeps());
|
||||
await triggerSubsyncFromConfigRuntime(getSubsyncRuntimeServiceParams());
|
||||
}
|
||||
|
||||
function cancelPendingMultiCopy(): void {
|
||||
@@ -1474,27 +1328,7 @@ function toggleInvisibleOverlay(): void {
|
||||
function setOverlayVisible(visible: boolean): void { setVisibleOverlayVisible(visible); }
|
||||
function toggleOverlay(): void { toggleVisibleOverlay(); }
|
||||
function 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 = overlayManager.getMainWindow();
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.hide();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const invisibleWindow = overlayManager.getInvisibleWindow();
|
||||
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
|
||||
invisibleWindow.hide();
|
||||
}
|
||||
overlayModalRuntime.handleOverlayModalClosed(modal);
|
||||
}
|
||||
|
||||
function handleMpvCommandFromIpc(command: (string | number)[]): void {
|
||||
@@ -1523,83 +1357,85 @@ function handleMpvCommandFromIpc(command: (string | number)[]): void {
|
||||
async function runSubsyncManualFromIpc(
|
||||
request: SubsyncManualRunRequest,
|
||||
): Promise<SubsyncResult> {
|
||||
return runSubsyncManualFromIpcRuntimeService(request, getSubsyncRuntimeDeps());
|
||||
return runSubsyncManualFromIpcRuntime(request, getSubsyncRuntimeServiceParams());
|
||||
}
|
||||
|
||||
const runtimeOptionsIpcDeps = createRuntimeOptionsIpcDeps({
|
||||
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
||||
showMpvOsd,
|
||||
});
|
||||
function buildIpcRuntimeServicesParams() {
|
||||
return {
|
||||
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({
|
||||
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),
|
||||
});
|
||||
registerIpcRuntimeServices(buildIpcRuntimeServicesParams());
|
||||
|
||||
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,
|
||||
createMainIpcRuntimeServiceDeps,
|
||||
MainIpcRuntimeServiceDepsParams,
|
||||
createRuntimeOptionsIpcDeps,
|
||||
RuntimeOptionsIpcDepsParams,
|
||||
} from "./dependencies";
|
||||
|
||||
export interface RegisterIpcRuntimeServicesParams {
|
||||
runtimeOptions: RuntimeOptionsIpcDepsParams;
|
||||
mainDeps: Omit<
|
||||
MainIpcRuntimeServiceDepsParams,
|
||||
"setRuntimeOption" | "cycleRuntimeOption"
|
||||
>;
|
||||
ankiJimakuDeps: AnkiJimakuIpcRuntimeServiceDepsParams;
|
||||
}
|
||||
|
||||
export function registerMainIpcRuntimeServices(
|
||||
params: MainIpcRuntimeServiceDepsParams,
|
||||
): 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