mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
refactor: extract overlay visibility and ipc helper services
This commit is contained in:
93
src/core/services/ipc-command-service.ts
Normal file
93
src/core/services/ipc-command-service.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
RuntimeOptionApplyResult,
|
||||
RuntimeOptionId,
|
||||
RuntimeOptionValue,
|
||||
SubsyncManualRunRequest,
|
||||
SubsyncResult,
|
||||
} from "../../types";
|
||||
|
||||
export function handleMpvCommandFromIpcService(
|
||||
command: (string | number)[],
|
||||
options: {
|
||||
specialCommands: {
|
||||
SUBSYNC_TRIGGER: string;
|
||||
RUNTIME_OPTIONS_OPEN: string;
|
||||
RUNTIME_OPTION_CYCLE_PREFIX: string;
|
||||
REPLAY_SUBTITLE: string;
|
||||
PLAY_NEXT_SUBTITLE: string;
|
||||
};
|
||||
triggerSubsyncFromConfig: () => void;
|
||||
openRuntimeOptionsPalette: () => void;
|
||||
runtimeOptionsCycle: (id: RuntimeOptionId, direction: 1 | -1) => RuntimeOptionApplyResult;
|
||||
showMpvOsd: (text: string) => void;
|
||||
mpvReplaySubtitle: () => void;
|
||||
mpvPlayNextSubtitle: () => void;
|
||||
mpvSendCommand: (command: (string | number)[]) => void;
|
||||
isMpvConnected: () => boolean;
|
||||
hasRuntimeOptionsManager: () => boolean;
|
||||
},
|
||||
): void {
|
||||
const first = typeof command[0] === "string" ? command[0] : "";
|
||||
if (first === options.specialCommands.SUBSYNC_TRIGGER) {
|
||||
options.triggerSubsyncFromConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
if (first === options.specialCommands.RUNTIME_OPTIONS_OPEN) {
|
||||
options.openRuntimeOptionsPalette();
|
||||
return;
|
||||
}
|
||||
|
||||
if (first.startsWith(options.specialCommands.RUNTIME_OPTION_CYCLE_PREFIX)) {
|
||||
if (!options.hasRuntimeOptionsManager()) return;
|
||||
const [, idToken, directionToken] = first.split(":");
|
||||
const id = idToken as RuntimeOptionId;
|
||||
const direction: 1 | -1 = directionToken === "prev" ? -1 : 1;
|
||||
const result = options.runtimeOptionsCycle(id, direction);
|
||||
if (!result.ok && result.error) {
|
||||
options.showMpvOsd(result.error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.isMpvConnected()) {
|
||||
if (first === options.specialCommands.REPLAY_SUBTITLE) {
|
||||
options.mpvReplaySubtitle();
|
||||
} else if (first === options.specialCommands.PLAY_NEXT_SUBTITLE) {
|
||||
options.mpvPlayNextSubtitle();
|
||||
} else {
|
||||
options.mpvSendCommand(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function runSubsyncManualFromIpcService(
|
||||
request: SubsyncManualRunRequest,
|
||||
options: {
|
||||
isSubsyncInProgress: () => boolean;
|
||||
setSubsyncInProgress: (inProgress: boolean) => void;
|
||||
showMpvOsd: (text: string) => void;
|
||||
runWithSpinner: (task: () => Promise<SubsyncResult>) => Promise<SubsyncResult>;
|
||||
runSubsyncManual: (request: SubsyncManualRunRequest) => Promise<SubsyncResult>;
|
||||
},
|
||||
): Promise<SubsyncResult> {
|
||||
if (options.isSubsyncInProgress()) {
|
||||
const busy = "Subsync already running";
|
||||
options.showMpvOsd(busy);
|
||||
return { ok: false, message: busy };
|
||||
}
|
||||
try {
|
||||
options.setSubsyncInProgress(true);
|
||||
const result = await options.runWithSpinner(() =>
|
||||
options.runSubsyncManual(request),
|
||||
);
|
||||
options.showMpvOsd(result.message);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const message = `Subsync failed: ${(error as Error).message}`;
|
||||
options.showMpvOsd(message);
|
||||
return { ok: false, message };
|
||||
} finally {
|
||||
options.setSubsyncInProgress(false);
|
||||
}
|
||||
}
|
||||
26
src/core/services/overlay-send-service.ts
Normal file
26
src/core/services/overlay-send-service.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
|
||||
export function sendToVisibleOverlayService(options: {
|
||||
mainWindow: BrowserWindow | null;
|
||||
visibleOverlayVisible: boolean;
|
||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||
channel: string;
|
||||
payload?: unknown;
|
||||
restoreOnModalClose?: string;
|
||||
addRestoreFlag: (modal: string) => void;
|
||||
}): boolean {
|
||||
if (!options.mainWindow || options.mainWindow.isDestroyed()) return false;
|
||||
const wasVisible = options.visibleOverlayVisible;
|
||||
if (!options.visibleOverlayVisible) {
|
||||
options.setVisibleOverlayVisible(true);
|
||||
}
|
||||
if (!wasVisible && options.restoreOnModalClose) {
|
||||
options.addRestoreFlag(options.restoreOnModalClose);
|
||||
}
|
||||
if (options.payload === undefined) {
|
||||
options.mainWindow.webContents.send(options.channel);
|
||||
} else {
|
||||
options.mainWindow.webContents.send(options.channel, options.payload);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
107
src/core/services/overlay-shortcut-fallback-runner.ts
Normal file
107
src/core/services/overlay-shortcut-fallback-runner.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { ConfiguredShortcuts } from "../utils/shortcut-config";
|
||||
|
||||
export interface OverlayShortcutFallbackHandlers {
|
||||
openRuntimeOptions: () => void;
|
||||
markAudioCard: () => void;
|
||||
copySubtitleMultiple: (timeoutMs: number) => void;
|
||||
copySubtitle: () => void;
|
||||
toggleSecondarySub: () => void;
|
||||
updateLastCardFromClipboard: () => void;
|
||||
triggerFieldGrouping: () => void;
|
||||
triggerSubsync: () => void;
|
||||
mineSentence: () => void;
|
||||
mineSentenceMultiple: (timeoutMs: number) => void;
|
||||
}
|
||||
|
||||
export function runOverlayShortcutLocalFallback(
|
||||
input: Electron.Input,
|
||||
shortcuts: ConfiguredShortcuts,
|
||||
matcher: (
|
||||
input: Electron.Input,
|
||||
accelerator: string,
|
||||
allowWhenRegistered?: boolean,
|
||||
) => boolean,
|
||||
handlers: OverlayShortcutFallbackHandlers,
|
||||
): boolean {
|
||||
const actions: Array<{
|
||||
accelerator: string | null | undefined;
|
||||
run: () => void;
|
||||
allowWhenRegistered?: boolean;
|
||||
}> = [
|
||||
{
|
||||
accelerator: shortcuts.openRuntimeOptions,
|
||||
run: () => {
|
||||
handlers.openRuntimeOptions();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.markAudioCard,
|
||||
run: () => {
|
||||
handlers.markAudioCard();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.copySubtitleMultiple,
|
||||
run: () => {
|
||||
handlers.copySubtitleMultiple(shortcuts.multiCopyTimeoutMs);
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.copySubtitle,
|
||||
run: () => {
|
||||
handlers.copySubtitle();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.toggleSecondarySub,
|
||||
run: () => handlers.toggleSecondarySub(),
|
||||
allowWhenRegistered: true,
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.updateLastCardFromClipboard,
|
||||
run: () => {
|
||||
handlers.updateLastCardFromClipboard();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.triggerFieldGrouping,
|
||||
run: () => {
|
||||
handlers.triggerFieldGrouping();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.triggerSubsync,
|
||||
run: () => {
|
||||
handlers.triggerSubsync();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.mineSentence,
|
||||
run: () => {
|
||||
handlers.mineSentence();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.mineSentenceMultiple,
|
||||
run: () => {
|
||||
handlers.mineSentenceMultiple(shortcuts.multiCopyTimeoutMs);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const action of actions) {
|
||||
if (!action.accelerator) continue;
|
||||
if (
|
||||
matcher(
|
||||
input,
|
||||
action.accelerator,
|
||||
action.allowWhenRegistered === true,
|
||||
)
|
||||
) {
|
||||
action.run();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
183
src/core/services/overlay-visibility-service.ts
Normal file
183
src/core/services/overlay-visibility-service.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { BrowserWindow, screen } from "electron";
|
||||
import { BaseWindowTracker } from "../../window-trackers";
|
||||
import { WindowGeometry } from "../../types";
|
||||
|
||||
interface MpvCommandSender {
|
||||
command: Array<string | number>;
|
||||
request_id?: number;
|
||||
}
|
||||
|
||||
export function updateVisibleOverlayVisibilityService(args: {
|
||||
visibleOverlayVisible: boolean;
|
||||
mainWindow: BrowserWindow | null;
|
||||
windowTracker: BaseWindowTracker | null;
|
||||
trackerNotReadyWarningShown: boolean;
|
||||
setTrackerNotReadyWarningShown: (shown: boolean) => void;
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: boolean;
|
||||
previousSecondarySubVisibility: boolean | null;
|
||||
setPreviousSecondarySubVisibility: (value: boolean | null) => void;
|
||||
mpvConnected: boolean;
|
||||
mpvSend: (payload: MpvCommandSender) => void;
|
||||
secondarySubVisibilityRequestId: number;
|
||||
updateOverlayBounds: (geometry: WindowGeometry) => void;
|
||||
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
|
||||
enforceOverlayLayerOrder: () => void;
|
||||
syncOverlayShortcuts: () => void;
|
||||
}): void {
|
||||
console.log(
|
||||
"updateVisibleOverlayVisibility called, visibleOverlayVisible:",
|
||||
args.visibleOverlayVisible,
|
||||
);
|
||||
if (!args.mainWindow || args.mainWindow.isDestroyed()) {
|
||||
console.log("mainWindow not available");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.visibleOverlayVisible) {
|
||||
console.log("Hiding visible overlay");
|
||||
args.mainWindow.hide();
|
||||
|
||||
if (
|
||||
args.shouldBindVisibleOverlayToMpvSubVisibility &&
|
||||
args.previousSecondarySubVisibility !== null &&
|
||||
args.mpvConnected
|
||||
) {
|
||||
args.mpvSend({
|
||||
command: [
|
||||
"set_property",
|
||||
"secondary-sub-visibility",
|
||||
args.previousSecondarySubVisibility ? "yes" : "no",
|
||||
],
|
||||
});
|
||||
args.setPreviousSecondarySubVisibility(null);
|
||||
} else if (!args.shouldBindVisibleOverlayToMpvSubVisibility) {
|
||||
args.setPreviousSecondarySubVisibility(null);
|
||||
}
|
||||
args.syncOverlayShortcuts();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
"Should show visible overlay, isTracking:",
|
||||
args.windowTracker?.isTracking(),
|
||||
);
|
||||
|
||||
if (args.shouldBindVisibleOverlayToMpvSubVisibility && args.mpvConnected) {
|
||||
args.mpvSend({
|
||||
command: ["get_property", "secondary-sub-visibility"],
|
||||
request_id: args.secondarySubVisibilityRequestId,
|
||||
});
|
||||
}
|
||||
|
||||
if (args.windowTracker && args.windowTracker.isTracking()) {
|
||||
args.setTrackerNotReadyWarningShown(false);
|
||||
const geometry = args.windowTracker.getGeometry();
|
||||
console.log("Geometry:", geometry);
|
||||
if (geometry) {
|
||||
args.updateOverlayBounds(geometry);
|
||||
}
|
||||
console.log("Showing visible overlay mainWindow");
|
||||
args.ensureOverlayWindowLevel(args.mainWindow);
|
||||
args.mainWindow.show();
|
||||
args.mainWindow.focus();
|
||||
args.enforceOverlayLayerOrder();
|
||||
args.syncOverlayShortcuts();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.windowTracker) {
|
||||
args.setTrackerNotReadyWarningShown(false);
|
||||
args.ensureOverlayWindowLevel(args.mainWindow);
|
||||
args.mainWindow.show();
|
||||
args.mainWindow.focus();
|
||||
args.enforceOverlayLayerOrder();
|
||||
args.syncOverlayShortcuts();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.trackerNotReadyWarningShown) {
|
||||
console.warn(
|
||||
"Window tracker exists but is not tracking yet; using fallback bounds until tracking starts",
|
||||
);
|
||||
args.setTrackerNotReadyWarningShown(true);
|
||||
}
|
||||
const cursorPoint = screen.getCursorScreenPoint();
|
||||
const display = screen.getDisplayNearestPoint(cursorPoint);
|
||||
const fallbackBounds = display.workArea;
|
||||
args.updateOverlayBounds({
|
||||
x: fallbackBounds.x,
|
||||
y: fallbackBounds.y,
|
||||
width: fallbackBounds.width,
|
||||
height: fallbackBounds.height,
|
||||
});
|
||||
args.ensureOverlayWindowLevel(args.mainWindow);
|
||||
args.mainWindow.show();
|
||||
args.mainWindow.focus();
|
||||
args.enforceOverlayLayerOrder();
|
||||
args.syncOverlayShortcuts();
|
||||
}
|
||||
|
||||
export function updateInvisibleOverlayVisibilityService(args: {
|
||||
invisibleWindow: BrowserWindow | null;
|
||||
visibleOverlayVisible: boolean;
|
||||
invisibleOverlayVisible: boolean;
|
||||
windowTracker: BaseWindowTracker | null;
|
||||
updateOverlayBounds: (geometry: WindowGeometry) => void;
|
||||
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
|
||||
enforceOverlayLayerOrder: () => void;
|
||||
syncOverlayShortcuts: () => void;
|
||||
}): void {
|
||||
if (!args.invisibleWindow || args.invisibleWindow.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.visibleOverlayVisible) {
|
||||
args.invisibleWindow.hide();
|
||||
args.syncOverlayShortcuts();
|
||||
return;
|
||||
}
|
||||
|
||||
const showInvisibleWithoutFocus = (): void => {
|
||||
args.ensureOverlayWindowLevel(args.invisibleWindow!);
|
||||
if (typeof args.invisibleWindow!.showInactive === "function") {
|
||||
args.invisibleWindow!.showInactive();
|
||||
} else {
|
||||
args.invisibleWindow!.show();
|
||||
}
|
||||
args.enforceOverlayLayerOrder();
|
||||
};
|
||||
|
||||
if (!args.invisibleOverlayVisible) {
|
||||
args.invisibleWindow.hide();
|
||||
args.syncOverlayShortcuts();
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.windowTracker && args.windowTracker.isTracking()) {
|
||||
const geometry = args.windowTracker.getGeometry();
|
||||
if (geometry) {
|
||||
args.updateOverlayBounds(geometry);
|
||||
}
|
||||
showInvisibleWithoutFocus();
|
||||
args.syncOverlayShortcuts();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.windowTracker) {
|
||||
showInvisibleWithoutFocus();
|
||||
args.syncOverlayShortcuts();
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorPoint = screen.getCursorScreenPoint();
|
||||
const display = screen.getDisplayNearestPoint(cursorPoint);
|
||||
const fallbackBounds = display.workArea;
|
||||
args.updateOverlayBounds({
|
||||
x: fallbackBounds.x,
|
||||
y: fallbackBounds.y,
|
||||
width: fallbackBounds.width,
|
||||
height: fallbackBounds.height,
|
||||
});
|
||||
showInvisibleWithoutFocus();
|
||||
args.syncOverlayShortcuts();
|
||||
}
|
||||
97
src/core/services/yomitan-settings-service.ts
Normal file
97
src/core/services/yomitan-settings-service.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { BrowserWindow, Extension, session } from "electron";
|
||||
|
||||
export function openYomitanSettingsWindow(options: {
|
||||
yomitanExt: Extension | null;
|
||||
getExistingWindow: () => BrowserWindow | null;
|
||||
setWindow: (window: BrowserWindow | null) => void;
|
||||
}): void {
|
||||
console.log("openYomitanSettings called");
|
||||
|
||||
if (!options.yomitanExt) {
|
||||
console.error(
|
||||
"Yomitan extension not loaded - yomitanExt is:",
|
||||
options.yomitanExt,
|
||||
);
|
||||
console.error(
|
||||
"This may be due to Manifest V3 service worker issues with Electron",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const existingWindow = options.getExistingWindow();
|
||||
if (existingWindow && !existingWindow.isDestroyed()) {
|
||||
console.log("Settings window already exists, focusing");
|
||||
existingWindow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Creating new settings window for extension:", options.yomitanExt.id);
|
||||
|
||||
const settingsWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
session: session.defaultSession,
|
||||
},
|
||||
});
|
||||
options.setWindow(settingsWindow);
|
||||
|
||||
const settingsUrl = `chrome-extension://${options.yomitanExt.id}/settings.html`;
|
||||
console.log("Loading settings URL:", settingsUrl);
|
||||
|
||||
let loadAttempts = 0;
|
||||
const maxAttempts = 3;
|
||||
|
||||
const attemptLoad = (): void => {
|
||||
settingsWindow
|
||||
.loadURL(settingsUrl)
|
||||
.then(() => {
|
||||
console.log("Settings URL loaded successfully");
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
console.error("Failed to load settings URL:", err);
|
||||
loadAttempts++;
|
||||
if (loadAttempts < maxAttempts && !settingsWindow.isDestroyed()) {
|
||||
console.log(
|
||||
`Retrying in 500ms (attempt ${loadAttempts + 1}/${maxAttempts})`,
|
||||
);
|
||||
setTimeout(attemptLoad, 500);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
attemptLoad();
|
||||
|
||||
settingsWindow.webContents.on(
|
||||
"did-fail-load",
|
||||
(_event, errorCode, errorDescription) => {
|
||||
console.error(
|
||||
"Settings page failed to load:",
|
||||
errorCode,
|
||||
errorDescription,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
settingsWindow.webContents.on("did-finish-load", () => {
|
||||
console.log("Settings page loaded successfully");
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (!settingsWindow.isDestroyed()) {
|
||||
settingsWindow.setSize(
|
||||
settingsWindow.getSize()[0],
|
||||
settingsWindow.getSize()[1],
|
||||
);
|
||||
settingsWindow.webContents.invalidate();
|
||||
settingsWindow.show();
|
||||
}
|
||||
}, 500);
|
||||
|
||||
settingsWindow.on("closed", () => {
|
||||
options.setWindow(null);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user