mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
55
src/core/utils/notification.ts
Normal file
55
src/core/utils/notification.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { Notification, nativeImage } from "electron";
|
||||||
|
import * as fs from "fs";
|
||||||
|
|
||||||
|
export function showDesktopNotification(
|
||||||
|
title: string,
|
||||||
|
options: { body?: string; icon?: string },
|
||||||
|
): void {
|
||||||
|
const notificationOptions: {
|
||||||
|
title: string;
|
||||||
|
body?: string;
|
||||||
|
icon?: Electron.NativeImage | string;
|
||||||
|
} = { title };
|
||||||
|
|
||||||
|
if (options.body) {
|
||||||
|
notificationOptions.body = options.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.icon) {
|
||||||
|
const isFilePath =
|
||||||
|
typeof options.icon === "string" &&
|
||||||
|
(options.icon.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(options.icon));
|
||||||
|
|
||||||
|
if (isFilePath) {
|
||||||
|
if (fs.existsSync(options.icon)) {
|
||||||
|
notificationOptions.icon = options.icon;
|
||||||
|
} else {
|
||||||
|
console.warn("Notification icon file not found:", options.icon);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
typeof options.icon === "string" &&
|
||||||
|
options.icon.startsWith("data:image/")
|
||||||
|
) {
|
||||||
|
const base64Data = options.icon.replace(/^data:image\/\w+;base64,/, "");
|
||||||
|
try {
|
||||||
|
const image = nativeImage.createFromBuffer(
|
||||||
|
Buffer.from(base64Data, "base64"),
|
||||||
|
);
|
||||||
|
if (image.isEmpty()) {
|
||||||
|
console.warn(
|
||||||
|
"Notification icon created from base64 is empty - image format may not be supported by Electron",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notificationOptions.icon = image;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to create notification icon from base64:", err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notificationOptions.icon = options.icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const notification = new Notification(notificationOptions);
|
||||||
|
notification.show();
|
||||||
|
}
|
||||||
565
src/main.ts
565
src/main.ts
@@ -27,8 +27,6 @@ import {
|
|||||||
screen,
|
screen,
|
||||||
IpcMainEvent,
|
IpcMainEvent,
|
||||||
Extension,
|
Extension,
|
||||||
Notification,
|
|
||||||
nativeImage,
|
|
||||||
} from "electron";
|
} from "electron";
|
||||||
|
|
||||||
protocol.registerSchemesAsPrivileged([
|
protocol.registerSchemesAsPrivileged([
|
||||||
@@ -143,6 +141,18 @@ import {
|
|||||||
registerOverlayShortcutsService,
|
registerOverlayShortcutsService,
|
||||||
unregisterOverlayShortcutsService,
|
unregisterOverlayShortcutsService,
|
||||||
} from "./core/services/overlay-shortcut-service";
|
} from "./core/services/overlay-shortcut-service";
|
||||||
|
import { runOverlayShortcutLocalFallback } from "./core/services/overlay-shortcut-fallback-runner";
|
||||||
|
import { showDesktopNotification } from "./core/utils/notification";
|
||||||
|
import { openYomitanSettingsWindow } from "./core/services/yomitan-settings-service";
|
||||||
|
import {
|
||||||
|
handleMpvCommandFromIpcService,
|
||||||
|
runSubsyncManualFromIpcService,
|
||||||
|
} from "./core/services/ipc-command-service";
|
||||||
|
import { sendToVisibleOverlayService } from "./core/services/overlay-send-service";
|
||||||
|
import {
|
||||||
|
updateInvisibleOverlayVisibilityService,
|
||||||
|
updateVisibleOverlayVisibilityService,
|
||||||
|
} from "./core/services/overlay-visibility-service";
|
||||||
import {
|
import {
|
||||||
ConfigService,
|
ConfigService,
|
||||||
DEFAULT_CONFIG,
|
DEFAULT_CONFIG,
|
||||||
@@ -1976,9 +1986,6 @@ function enforceOverlayLayerOrder(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureExtensionCopy(sourceDir: string): string {
|
function ensureExtensionCopy(sourceDir: string): string {
|
||||||
// Copy extension to writable location on Linux and macOS
|
|
||||||
// MV3 service workers need write access for IndexedDB/storage
|
|
||||||
// App bundles on macOS are read-only, causing service worker failures
|
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
return sourceDir;
|
return sourceDir;
|
||||||
}
|
}
|
||||||
@@ -2038,8 +2045,6 @@ async function loadYomitanExtension(): Promise<Extension | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Yomitan search paths:", searchPaths);
|
|
||||||
console.log("Found Yomitan at:", extPath);
|
|
||||||
|
|
||||||
if (!extPath) {
|
if (!extPath) {
|
||||||
console.error("Yomitan extension not found in any search path");
|
console.error("Yomitan extension not found in any search path");
|
||||||
@@ -2048,7 +2053,6 @@ async function loadYomitanExtension(): Promise<Extension | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extPath = ensureExtensionCopy(extPath);
|
extPath = ensureExtensionCopy(extPath);
|
||||||
console.log("Using extension path:", extPath);
|
|
||||||
|
|
||||||
if (yomitanParserWindow && !yomitanParserWindow.isDestroyed()) {
|
if (yomitanParserWindow && !yomitanParserWindow.isDestroyed()) {
|
||||||
yomitanParserWindow.destroy();
|
yomitanParserWindow.destroy();
|
||||||
@@ -2068,7 +2072,6 @@ async function loadYomitanExtension(): Promise<Extension | null> {
|
|||||||
allowFileAccess: true,
|
allowFileAccess: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log("Yomitan extension loaded successfully:", yomitanExt.id);
|
|
||||||
return yomitanExt;
|
return yomitanExt;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load Yomitan extension:", (err as Error).message);
|
console.error("Failed to load Yomitan extension:", (err as Error).message);
|
||||||
@@ -2103,8 +2106,6 @@ function createOverlayWindow(kind: "visible" | "invisible"): BrowserWindow {
|
|||||||
ensureOverlayWindowLevel(window);
|
ensureOverlayWindowLevel(window);
|
||||||
|
|
||||||
const htmlPath = path.join(__dirname, "renderer", "index.html");
|
const htmlPath = path.join(__dirname, "renderer", "index.html");
|
||||||
console.log(`Loading ${kind} overlay HTML from:`, htmlPath);
|
|
||||||
console.log("HTML file exists:", fs.existsSync(htmlPath));
|
|
||||||
|
|
||||||
window
|
window
|
||||||
.loadFile(htmlPath, {
|
.loadFile(htmlPath, {
|
||||||
@@ -2127,7 +2128,6 @@ function createOverlayWindow(kind: "visible" | "invisible"): BrowserWindow {
|
|||||||
);
|
);
|
||||||
|
|
||||||
window.webContents.on("did-finish-load", () => {
|
window.webContents.on("did-finish-load", () => {
|
||||||
console.log(`${kind} overlay HTML loaded successfully`);
|
|
||||||
broadcastRuntimeOptionsChanged();
|
broadcastRuntimeOptionsChanged();
|
||||||
window.webContents.send(
|
window.webContents.send(
|
||||||
"overlay-debug-visualization:set",
|
"overlay-debug-visualization:set",
|
||||||
@@ -2201,7 +2201,6 @@ function initializeOverlayRuntime(): void {
|
|||||||
updateOverlayBounds(geometry);
|
updateOverlayBounds(geometry);
|
||||||
};
|
};
|
||||||
windowTracker.onWindowFound = (geometry: WindowGeometry) => {
|
windowTracker.onWindowFound = (geometry: WindowGeometry) => {
|
||||||
console.log("MPV window found:", geometry);
|
|
||||||
updateOverlayBounds(geometry);
|
updateOverlayBounds(geometry);
|
||||||
if (visibleOverlayVisible) {
|
if (visibleOverlayVisible) {
|
||||||
updateVisibleOverlayVisibility();
|
updateVisibleOverlayVisibility();
|
||||||
@@ -2211,11 +2210,9 @@ function initializeOverlayRuntime(): void {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
windowTracker.onWindowLost = () => {
|
windowTracker.onWindowLost = () => {
|
||||||
console.log("MPV window lost");
|
|
||||||
for (const window of getOverlayWindows()) {
|
for (const window of getOverlayWindows()) {
|
||||||
window.hide();
|
window.hide();
|
||||||
}
|
}
|
||||||
// Keep overlay shortcuts registered; tracking loss can be transient.
|
|
||||||
syncOverlayShortcuts();
|
syncOverlayShortcuts();
|
||||||
};
|
};
|
||||||
windowTracker.start();
|
windowTracker.start();
|
||||||
@@ -2253,93 +2250,10 @@ function initializeOverlayRuntime(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openYomitanSettings(): void {
|
function openYomitanSettings(): void {
|
||||||
console.log("openYomitanSettings called");
|
openYomitanSettingsWindow({
|
||||||
|
yomitanExt,
|
||||||
if (!yomitanExt) {
|
getExistingWindow: () => yomitanSettingsWindow,
|
||||||
console.error("Yomitan extension not loaded - yomitanExt is:", yomitanExt);
|
setWindow: (window) => (yomitanSettingsWindow = window),
|
||||||
console.error(
|
|
||||||
"This may be due to Manifest V3 service worker issues with Electron",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (yomitanSettingsWindow && !yomitanSettingsWindow.isDestroyed()) {
|
|
||||||
console.log("Settings window already exists, focusing");
|
|
||||||
yomitanSettingsWindow.focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Creating new settings window for extension:", yomitanExt.id);
|
|
||||||
|
|
||||||
yomitanSettingsWindow = new BrowserWindow({
|
|
||||||
width: 1200,
|
|
||||||
height: 800,
|
|
||||||
show: false,
|
|
||||||
webPreferences: {
|
|
||||||
contextIsolation: true,
|
|
||||||
nodeIntegration: false,
|
|
||||||
session: session.defaultSession,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const settingsUrl = `chrome-extension://${yomitanExt.id}/settings.html`;
|
|
||||||
console.log("Loading settings URL:", settingsUrl);
|
|
||||||
|
|
||||||
let loadAttempts = 0;
|
|
||||||
const maxAttempts = 3;
|
|
||||||
|
|
||||||
function attemptLoad(): void {
|
|
||||||
yomitanSettingsWindow!
|
|
||||||
.loadURL(settingsUrl)
|
|
||||||
.then(() => {
|
|
||||||
console.log("Settings URL loaded successfully");
|
|
||||||
})
|
|
||||||
.catch((err: Error) => {
|
|
||||||
console.error("Failed to load settings URL:", err);
|
|
||||||
loadAttempts++;
|
|
||||||
if (
|
|
||||||
loadAttempts < maxAttempts &&
|
|
||||||
yomitanSettingsWindow &&
|
|
||||||
!yomitanSettingsWindow.isDestroyed()
|
|
||||||
) {
|
|
||||||
console.log(
|
|
||||||
`Retrying in 500ms (attempt ${loadAttempts + 1}/${maxAttempts})`,
|
|
||||||
);
|
|
||||||
setTimeout(attemptLoad, 500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
attemptLoad();
|
|
||||||
|
|
||||||
yomitanSettingsWindow.webContents.on(
|
|
||||||
"did-fail-load",
|
|
||||||
(_event, errorCode, errorDescription) => {
|
|
||||||
console.error(
|
|
||||||
"Settings page failed to load:",
|
|
||||||
errorCode,
|
|
||||||
errorDescription,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
yomitanSettingsWindow.webContents.on("did-finish-load", () => {
|
|
||||||
console.log("Settings page loaded successfully");
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (yomitanSettingsWindow && !yomitanSettingsWindow.isDestroyed()) {
|
|
||||||
yomitanSettingsWindow.setSize(
|
|
||||||
yomitanSettingsWindow.getSize()[0],
|
|
||||||
yomitanSettingsWindow.getSize()[1],
|
|
||||||
);
|
|
||||||
yomitanSettingsWindow.webContents.invalidate();
|
|
||||||
yomitanSettingsWindow.show();
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
yomitanSettingsWindow.on("closed", () => {
|
|
||||||
yomitanSettingsWindow = null;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2354,112 +2268,63 @@ function registerGlobalShortcuts(): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConfiguredShortcuts() {
|
function getConfiguredShortcuts() { return resolveConfiguredShortcuts(getResolvedConfig(), DEFAULT_CONFIG); }
|
||||||
return resolveConfiguredShortcuts(getResolvedConfig(), DEFAULT_CONFIG);
|
|
||||||
}
|
|
||||||
|
|
||||||
function tryHandleOverlayShortcutLocalFallback(input: Electron.Input): boolean {
|
function tryHandleOverlayShortcutLocalFallback(input: Electron.Input): boolean {
|
||||||
const shortcuts = getConfiguredShortcuts();
|
const shortcuts = getConfiguredShortcuts();
|
||||||
const handlers: Array<{
|
return runOverlayShortcutLocalFallback(
|
||||||
accelerator: string | null | undefined;
|
input,
|
||||||
run: () => void;
|
shortcuts,
|
||||||
allowWhenRegistered?: boolean;
|
shortcutMatchesInputForLocalFallback,
|
||||||
}> = [
|
|
||||||
{
|
{
|
||||||
accelerator: shortcuts.openRuntimeOptions,
|
openRuntimeOptions: () => {
|
||||||
run: () => {
|
|
||||||
openRuntimeOptionsPalette();
|
openRuntimeOptionsPalette();
|
||||||
},
|
},
|
||||||
},
|
markAudioCard: () => {
|
||||||
{
|
|
||||||
accelerator: shortcuts.markAudioCard,
|
|
||||||
run: () => {
|
|
||||||
markLastCardAsAudioCard().catch((err) => {
|
markLastCardAsAudioCard().catch((err) => {
|
||||||
console.error("markLastCardAsAudioCard failed:", err);
|
console.error("markLastCardAsAudioCard failed:", err);
|
||||||
showMpvOsd(`Audio card failed: ${(err as Error).message}`);
|
showMpvOsd(`Audio card failed: ${(err as Error).message}`);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
copySubtitleMultiple: (timeoutMs) => {
|
||||||
{
|
startPendingMultiCopy(timeoutMs);
|
||||||
accelerator: shortcuts.copySubtitleMultiple,
|
|
||||||
run: () => {
|
|
||||||
startPendingMultiCopy(shortcuts.multiCopyTimeoutMs);
|
|
||||||
},
|
},
|
||||||
},
|
copySubtitle: () => {
|
||||||
{
|
|
||||||
accelerator: shortcuts.copySubtitle,
|
|
||||||
run: () => {
|
|
||||||
copyCurrentSubtitle();
|
copyCurrentSubtitle();
|
||||||
},
|
},
|
||||||
},
|
toggleSecondarySub: () => cycleSecondarySubMode(),
|
||||||
{
|
updateLastCardFromClipboard: () => {
|
||||||
accelerator: shortcuts.toggleSecondarySub,
|
|
||||||
run: () => cycleSecondarySubMode(),
|
|
||||||
allowWhenRegistered: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accelerator: shortcuts.updateLastCardFromClipboard,
|
|
||||||
run: () => {
|
|
||||||
updateLastCardFromClipboard().catch((err) => {
|
updateLastCardFromClipboard().catch((err) => {
|
||||||
console.error("updateLastCardFromClipboard failed:", err);
|
console.error("updateLastCardFromClipboard failed:", err);
|
||||||
showMpvOsd(`Update failed: ${(err as Error).message}`);
|
showMpvOsd(`Update failed: ${(err as Error).message}`);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
triggerFieldGrouping: () => {
|
||||||
{
|
|
||||||
accelerator: shortcuts.triggerFieldGrouping,
|
|
||||||
run: () => {
|
|
||||||
triggerFieldGrouping().catch((err) => {
|
triggerFieldGrouping().catch((err) => {
|
||||||
console.error("triggerFieldGrouping failed:", err);
|
console.error("triggerFieldGrouping failed:", err);
|
||||||
showMpvOsd(`Field grouping failed: ${(err as Error).message}`);
|
showMpvOsd(`Field grouping failed: ${(err as Error).message}`);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
triggerSubsync: () => {
|
||||||
{
|
|
||||||
accelerator: shortcuts.triggerSubsync,
|
|
||||||
run: () => {
|
|
||||||
triggerSubsyncFromConfig().catch((err) => {
|
triggerSubsyncFromConfig().catch((err) => {
|
||||||
console.error("triggerSubsyncFromConfig failed:", err);
|
console.error("triggerSubsyncFromConfig failed:", err);
|
||||||
showMpvOsd(`Subsync failed: ${(err as Error).message}`);
|
showMpvOsd(`Subsync failed: ${(err as Error).message}`);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
mineSentence: () => {
|
||||||
{
|
|
||||||
accelerator: shortcuts.mineSentence,
|
|
||||||
run: () => {
|
|
||||||
mineSentenceCard().catch((err) => {
|
mineSentenceCard().catch((err) => {
|
||||||
console.error("mineSentenceCard failed:", err);
|
console.error("mineSentenceCard failed:", err);
|
||||||
showMpvOsd(`Mine sentence failed: ${(err as Error).message}`);
|
showMpvOsd(`Mine sentence failed: ${(err as Error).message}`);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
mineSentenceMultiple: (timeoutMs) => {
|
||||||
{
|
startPendingMineSentenceMultiple(timeoutMs);
|
||||||
accelerator: shortcuts.mineSentenceMultiple,
|
|
||||||
run: () => {
|
|
||||||
startPendingMineSentenceMultiple(shortcuts.multiCopyTimeoutMs);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
);
|
||||||
|
|
||||||
for (const handler of handlers) {
|
|
||||||
if (!handler.accelerator) continue;
|
|
||||||
if (
|
|
||||||
shortcutMatchesInputForLocalFallback(
|
|
||||||
input,
|
|
||||||
handler.accelerator,
|
|
||||||
handler.allowWhenRegistered === true,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
handler.run();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cycleSecondarySubMode(): void {
|
function cycleSecondarySubMode(): void {
|
||||||
// Some platforms can trigger both global and in-window handlers for one key press.
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastSecondarySubToggleAtMs < 120) {
|
if (now - lastSecondarySubToggleAtMs < 120) {
|
||||||
return;
|
return;
|
||||||
@@ -2610,17 +2475,13 @@ function cleanupTemporaryFile(extraction: FileExtractionResult): void {
|
|||||||
if (fileExists(extraction.path)) {
|
if (fileExists(extraction.path)) {
|
||||||
fs.unlinkSync(extraction.path);
|
fs.unlinkSync(extraction.path);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {}
|
||||||
// Ignore cleanup failures
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const dir = path.dirname(extraction.path);
|
const dir = path.dirname(extraction.path);
|
||||||
if (fs.existsSync(dir)) {
|
if (fs.existsSync(dir)) {
|
||||||
fs.rmdirSync(dir);
|
fs.rmdirSync(dir);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {}
|
||||||
// Ignore cleanup failures
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRetimedPath(subPath: string): string {
|
function buildRetimedPath(subPath: string): string {
|
||||||
@@ -2742,7 +2603,6 @@ async function runSubsyncAuto(): Promise<SubsyncResult> {
|
|||||||
return alassResult;
|
return alassResult;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Fall through to ffsubsync fallback
|
|
||||||
console.warn("Auto alass sync failed, trying ffsubsync fallback:", error);
|
console.warn("Auto alass sync failed, trying ffsubsync fallback:", error);
|
||||||
} finally {
|
} finally {
|
||||||
if (secondaryExtraction) {
|
if (secondaryExtraction) {
|
||||||
@@ -2857,7 +2717,6 @@ function cancelPendingMultiCopy(): void {
|
|||||||
pendingMultiCopyTimeout = null;
|
pendingMultiCopyTimeout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unregister digit and escape shortcuts
|
|
||||||
for (const shortcut of multiCopyDigitShortcuts) {
|
for (const shortcut of multiCopyDigitShortcuts) {
|
||||||
globalShortcut.unregister(shortcut);
|
globalShortcut.unregister(shortcut);
|
||||||
}
|
}
|
||||||
@@ -2873,7 +2732,6 @@ function startPendingMultiCopy(timeoutMs: number): void {
|
|||||||
cancelPendingMultiCopy();
|
cancelPendingMultiCopy();
|
||||||
pendingMultiCopy = true;
|
pendingMultiCopy = true;
|
||||||
|
|
||||||
// Register digit shortcuts 1-9
|
|
||||||
for (let i = 1; i <= 9; i++) {
|
for (let i = 1; i <= 9; i++) {
|
||||||
const shortcut = i.toString();
|
const shortcut = i.toString();
|
||||||
if (
|
if (
|
||||||
@@ -2885,7 +2743,6 @@ function startPendingMultiCopy(timeoutMs: number): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register Escape to cancel
|
|
||||||
if (
|
if (
|
||||||
globalShortcut.register("Escape", () => {
|
globalShortcut.register("Escape", () => {
|
||||||
cancelPendingMultiCopy();
|
cancelPendingMultiCopy();
|
||||||
@@ -2895,7 +2752,6 @@ function startPendingMultiCopy(timeoutMs: number): void {
|
|||||||
multiCopyEscapeShortcut = "Escape";
|
multiCopyEscapeShortcut = "Escape";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set timeout
|
|
||||||
pendingMultiCopyTimeout = setTimeout(() => {
|
pendingMultiCopyTimeout = setTimeout(() => {
|
||||||
cancelPendingMultiCopy();
|
cancelPendingMultiCopy();
|
||||||
showMpvOsd("Copy timeout");
|
showMpvOsd("Copy timeout");
|
||||||
@@ -2909,7 +2765,6 @@ function handleMultiCopyDigit(count: number): void {
|
|||||||
|
|
||||||
cancelPendingMultiCopy();
|
cancelPendingMultiCopy();
|
||||||
|
|
||||||
// Check if we have enough history
|
|
||||||
const availableCount = Math.min(count, 200); // Max history size
|
const availableCount = Math.min(count, 200); // Max history size
|
||||||
const blocks = subtitleTimingTracker.getRecentBlocks(availableCount);
|
const blocks = subtitleTimingTracker.getRecentBlocks(availableCount);
|
||||||
|
|
||||||
@@ -3173,155 +3028,44 @@ function refreshOverlayShortcuts(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateVisibleOverlayVisibility(): void {
|
function updateVisibleOverlayVisibility(): void {
|
||||||
console.log(
|
updateVisibleOverlayVisibilityService({
|
||||||
"updateVisibleOverlayVisibility called, visibleOverlayVisible:",
|
|
||||||
visibleOverlayVisible,
|
visibleOverlayVisible,
|
||||||
);
|
mainWindow,
|
||||||
if (!mainWindow || mainWindow.isDestroyed()) {
|
windowTracker,
|
||||||
console.log("mainWindow not available");
|
trackerNotReadyWarningShown,
|
||||||
return;
|
setTrackerNotReadyWarningShown: (shown) => {
|
||||||
}
|
trackerNotReadyWarningShown = shown;
|
||||||
|
},
|
||||||
if (!visibleOverlayVisible) {
|
shouldBindVisibleOverlayToMpvSubVisibility:
|
||||||
console.log("Hiding visible overlay");
|
shouldBindVisibleOverlayToMpvSubVisibility(),
|
||||||
mainWindow.hide();
|
previousSecondarySubVisibility,
|
||||||
|
setPreviousSecondarySubVisibility: (value) => {
|
||||||
if (
|
previousSecondarySubVisibility = value;
|
||||||
shouldBindVisibleOverlayToMpvSubVisibility() &&
|
},
|
||||||
previousSecondarySubVisibility !== null &&
|
mpvConnected: Boolean(mpvClient && mpvClient.connected),
|
||||||
mpvClient &&
|
mpvSend: (payload) => {
|
||||||
mpvClient.connected
|
if (!mpvClient) return;
|
||||||
) {
|
mpvClient.send(payload);
|
||||||
mpvClient.send({
|
},
|
||||||
command: [
|
secondarySubVisibilityRequestId: MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
||||||
"set_property",
|
updateOverlayBounds: (geometry) => updateOverlayBounds(geometry),
|
||||||
"secondary-sub-visibility",
|
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
||||||
previousSecondarySubVisibility ? "yes" : "no",
|
enforceOverlayLayerOrder: () => enforceOverlayLayerOrder(),
|
||||||
],
|
syncOverlayShortcuts: () => syncOverlayShortcuts(),
|
||||||
});
|
});
|
||||||
previousSecondarySubVisibility = null;
|
|
||||||
} else if (!shouldBindVisibleOverlayToMpvSubVisibility()) {
|
|
||||||
previousSecondarySubVisibility = null;
|
|
||||||
}
|
|
||||||
syncOverlayShortcuts();
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
"Should show visible overlay, isTracking:",
|
|
||||||
windowTracker?.isTracking(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
shouldBindVisibleOverlayToMpvSubVisibility() &&
|
|
||||||
mpvClient &&
|
|
||||||
mpvClient.connected
|
|
||||||
) {
|
|
||||||
mpvClient.send({
|
|
||||||
command: ["get_property", "secondary-sub-visibility"],
|
|
||||||
request_id: MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (windowTracker && windowTracker.isTracking()) {
|
|
||||||
trackerNotReadyWarningShown = false;
|
|
||||||
const geometry = windowTracker.getGeometry();
|
|
||||||
console.log("Geometry:", geometry);
|
|
||||||
if (geometry) {
|
|
||||||
updateOverlayBounds(geometry);
|
|
||||||
}
|
|
||||||
console.log("Showing visible overlay mainWindow");
|
|
||||||
ensureOverlayWindowLevel(mainWindow);
|
|
||||||
mainWindow.show();
|
|
||||||
mainWindow.focus();
|
|
||||||
enforceOverlayLayerOrder();
|
|
||||||
syncOverlayShortcuts();
|
|
||||||
} else if (!windowTracker) {
|
|
||||||
trackerNotReadyWarningShown = false;
|
|
||||||
ensureOverlayWindowLevel(mainWindow);
|
|
||||||
mainWindow.show();
|
|
||||||
mainWindow.focus();
|
|
||||||
enforceOverlayLayerOrder();
|
|
||||||
syncOverlayShortcuts();
|
|
||||||
} else {
|
|
||||||
if (!trackerNotReadyWarningShown) {
|
|
||||||
console.warn(
|
|
||||||
"Window tracker exists but is not tracking yet; using fallback bounds until tracking starts",
|
|
||||||
);
|
|
||||||
trackerNotReadyWarningShown = true;
|
|
||||||
}
|
|
||||||
const cursorPoint = screen.getCursorScreenPoint();
|
|
||||||
const display = screen.getDisplayNearestPoint(cursorPoint);
|
|
||||||
const fallbackBounds = display.workArea;
|
|
||||||
updateOverlayBounds({
|
|
||||||
x: fallbackBounds.x,
|
|
||||||
y: fallbackBounds.y,
|
|
||||||
width: fallbackBounds.width,
|
|
||||||
height: fallbackBounds.height,
|
|
||||||
});
|
|
||||||
ensureOverlayWindowLevel(mainWindow);
|
|
||||||
mainWindow.show();
|
|
||||||
mainWindow.focus();
|
|
||||||
enforceOverlayLayerOrder();
|
|
||||||
syncOverlayShortcuts();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateInvisibleOverlayVisibility(): void {
|
function updateInvisibleOverlayVisibility(): void {
|
||||||
if (!invisibleWindow || invisibleWindow.isDestroyed()) {
|
updateInvisibleOverlayVisibilityService({
|
||||||
return;
|
invisibleWindow,
|
||||||
}
|
visibleOverlayVisible,
|
||||||
|
invisibleOverlayVisible,
|
||||||
// When the visible overlay is shown, keep the invisible layer hidden to
|
windowTracker,
|
||||||
// avoid it intercepting or rerouting pointer interactions.
|
updateOverlayBounds: (geometry) => updateOverlayBounds(geometry),
|
||||||
if (visibleOverlayVisible) {
|
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
||||||
invisibleWindow.hide();
|
enforceOverlayLayerOrder: () => enforceOverlayLayerOrder(),
|
||||||
syncOverlayShortcuts();
|
syncOverlayShortcuts: () => syncOverlayShortcuts(),
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const showInvisibleWithoutFocus = (): void => {
|
|
||||||
ensureOverlayWindowLevel(invisibleWindow!);
|
|
||||||
if (typeof invisibleWindow!.showInactive === "function") {
|
|
||||||
invisibleWindow!.showInactive();
|
|
||||||
} else {
|
|
||||||
invisibleWindow!.show();
|
|
||||||
}
|
|
||||||
enforceOverlayLayerOrder();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!invisibleOverlayVisible) {
|
|
||||||
invisibleWindow.hide();
|
|
||||||
syncOverlayShortcuts();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (windowTracker && windowTracker.isTracking()) {
|
|
||||||
const geometry = windowTracker.getGeometry();
|
|
||||||
if (geometry) {
|
|
||||||
updateOverlayBounds(geometry);
|
|
||||||
}
|
|
||||||
showInvisibleWithoutFocus();
|
|
||||||
syncOverlayShortcuts();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!windowTracker) {
|
|
||||||
showInvisibleWithoutFocus();
|
|
||||||
syncOverlayShortcuts();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cursorPoint = screen.getCursorScreenPoint();
|
|
||||||
const display = screen.getDisplayNearestPoint(cursorPoint);
|
|
||||||
const fallbackBounds = display.workArea;
|
|
||||||
updateOverlayBounds({
|
|
||||||
x: fallbackBounds.x,
|
|
||||||
y: fallbackBounds.y,
|
|
||||||
width: fallbackBounds.width,
|
|
||||||
height: fallbackBounds.height,
|
|
||||||
});
|
});
|
||||||
showInvisibleWithoutFocus();
|
|
||||||
syncOverlayShortcuts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncInvisibleOverlayMousePassthrough(): void {
|
function syncInvisibleOverlayMousePassthrough(): void {
|
||||||
@@ -3378,62 +3122,46 @@ function handleOverlayModalClosed(modal: OverlayHostedModal): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleMpvCommandFromIpc(command: (string | number)[]): void {
|
function handleMpvCommandFromIpc(command: (string | number)[]): void {
|
||||||
const first = typeof command[0] === "string" ? command[0] : "";
|
handleMpvCommandFromIpcService(command, {
|
||||||
if (first === SPECIAL_COMMANDS.SUBSYNC_TRIGGER) {
|
specialCommands: SPECIAL_COMMANDS,
|
||||||
triggerSubsyncFromConfig();
|
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||||
return;
|
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||||
}
|
runtimeOptionsCycle: (id, direction) => {
|
||||||
|
if (!runtimeOptionsManager) {
|
||||||
if (first === SPECIAL_COMMANDS.RUNTIME_OPTIONS_OPEN) {
|
return { ok: false, error: "Runtime options manager unavailable" };
|
||||||
openRuntimeOptionsPalette();
|
}
|
||||||
return;
|
return applyRuntimeOptionResult(
|
||||||
}
|
runtimeOptionsManager.cycleOption(id, direction),
|
||||||
|
);
|
||||||
if (first.startsWith(SPECIAL_COMMANDS.RUNTIME_OPTION_CYCLE_PREFIX)) {
|
},
|
||||||
if (!runtimeOptionsManager) return;
|
showMpvOsd: (text) => showMpvOsd(text),
|
||||||
const [, idToken, directionToken] = first.split(":");
|
mpvReplaySubtitle: () => {
|
||||||
const id = idToken as RuntimeOptionId;
|
if (mpvClient) mpvClient.replayCurrentSubtitle();
|
||||||
const direction: 1 | -1 = directionToken === "prev" ? -1 : 1;
|
},
|
||||||
const result = applyRuntimeOptionResult(
|
mpvPlayNextSubtitle: () => {
|
||||||
runtimeOptionsManager.cycleOption(id, direction),
|
if (mpvClient) mpvClient.playNextSubtitle();
|
||||||
);
|
},
|
||||||
if (!result.ok && result.error) {
|
mpvSendCommand: (rawCommand) => {
|
||||||
showMpvOsd(result.error);
|
if (!mpvClient) return;
|
||||||
}
|
mpvClient.send({ command: rawCommand });
|
||||||
return;
|
},
|
||||||
}
|
isMpvConnected: () => Boolean(mpvClient && mpvClient.connected),
|
||||||
|
hasRuntimeOptionsManager: () => runtimeOptionsManager !== null,
|
||||||
if (mpvClient && mpvClient.connected) {
|
});
|
||||||
if (first === SPECIAL_COMMANDS.REPLAY_SUBTITLE) {
|
|
||||||
mpvClient.replayCurrentSubtitle();
|
|
||||||
} else if (first === SPECIAL_COMMANDS.PLAY_NEXT_SUBTITLE) {
|
|
||||||
mpvClient.playNextSubtitle();
|
|
||||||
} else {
|
|
||||||
mpvClient.send({ command });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runSubsyncManualFromIpc(
|
async function runSubsyncManualFromIpc(
|
||||||
request: SubsyncManualRunRequest,
|
request: SubsyncManualRunRequest,
|
||||||
): Promise<SubsyncResult> {
|
): Promise<SubsyncResult> {
|
||||||
if (subsyncInProgress) {
|
return runSubsyncManualFromIpcService(request, {
|
||||||
const busy = "Subsync already running";
|
isSubsyncInProgress: () => subsyncInProgress,
|
||||||
showMpvOsd(busy);
|
setSubsyncInProgress: (inProgress) => {
|
||||||
return { ok: false, message: busy };
|
subsyncInProgress = inProgress;
|
||||||
}
|
},
|
||||||
try {
|
showMpvOsd: (text) => showMpvOsd(text),
|
||||||
subsyncInProgress = true;
|
runWithSpinner: (task) => runWithSubsyncSpinner(task),
|
||||||
const result = await runWithSubsyncSpinner(() => runSubsyncManual(request));
|
runSubsyncManual: (subsyncRequest) => runSubsyncManual(subsyncRequest),
|
||||||
showMpvOsd(result.message);
|
});
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
const message = `Subsync failed: ${(error as Error).message}`;
|
|
||||||
showMpvOsd(message);
|
|
||||||
return { ok: false, message };
|
|
||||||
} finally {
|
|
||||||
subsyncInProgress = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerIpcHandlersService({
|
registerIpcHandlersService({
|
||||||
@@ -3521,8 +3249,6 @@ function createFieldGroupingCallback() {
|
|||||||
fieldGroupingResolver = null;
|
fieldGroupingResolver = null;
|
||||||
resolve(choice);
|
resolve(choice);
|
||||||
|
|
||||||
// Treat the visible overlay as a temporary modal host and then restore
|
|
||||||
// the user's previous overlay visibility state.
|
|
||||||
if (!previousVisibleOverlay && visibleOverlayVisible) {
|
if (!previousVisibleOverlay && visibleOverlayVisible) {
|
||||||
setVisibleOverlayVisible(false);
|
setVisibleOverlayVisible(false);
|
||||||
}
|
}
|
||||||
@@ -3532,7 +3258,6 @@ function createFieldGroupingCallback() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fieldGroupingResolver = finish;
|
fieldGroupingResolver = finish;
|
||||||
// Manual Kiku flow is rendered only in the visible overlay window.
|
|
||||||
if (!sendToVisibleOverlay("kiku:field-grouping-request", data)) {
|
if (!sendToVisibleOverlay("kiku:field-grouping-request", data)) {
|
||||||
finish({
|
finish({
|
||||||
keepNoteId: 0,
|
keepNoteId: 0,
|
||||||
@@ -3561,78 +3286,16 @@ function sendToVisibleOverlay(
|
|||||||
payload?: unknown,
|
payload?: unknown,
|
||||||
options?: { restoreOnModalClose?: OverlayHostedModal },
|
options?: { restoreOnModalClose?: OverlayHostedModal },
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!mainWindow || mainWindow.isDestroyed()) return false;
|
return sendToVisibleOverlayService({
|
||||||
const wasVisible = visibleOverlayVisible;
|
mainWindow,
|
||||||
if (!visibleOverlayVisible) {
|
visibleOverlayVisible,
|
||||||
setVisibleOverlayVisible(true);
|
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
||||||
}
|
channel,
|
||||||
if (!wasVisible && options?.restoreOnModalClose) {
|
payload,
|
||||||
restoreVisibleOverlayOnModalClose.add(options.restoreOnModalClose);
|
restoreOnModalClose: options?.restoreOnModalClose,
|
||||||
}
|
addRestoreFlag: (modal) =>
|
||||||
if (payload === undefined) {
|
restoreVisibleOverlayOnModalClose.add(modal as OverlayHostedModal),
|
||||||
mainWindow.webContents.send(channel);
|
});
|
||||||
} else {
|
|
||||||
mainWindow.webContents.send(channel, payload);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showDesktopNotification(
|
|
||||||
title: string,
|
|
||||||
options: { body?: string; icon?: string },
|
|
||||||
): void {
|
|
||||||
const notificationOptions: {
|
|
||||||
title: string;
|
|
||||||
body?: string;
|
|
||||||
icon?: Electron.NativeImage | string;
|
|
||||||
} = { title };
|
|
||||||
|
|
||||||
if (options.body) {
|
|
||||||
notificationOptions.body = options.body;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.icon) {
|
|
||||||
// Check if it's a file path (starts with / on Linux/Mac, or drive letter on Windows)
|
|
||||||
const isFilePath =
|
|
||||||
typeof options.icon === "string" &&
|
|
||||||
(options.icon.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(options.icon));
|
|
||||||
|
|
||||||
if (isFilePath) {
|
|
||||||
// File path - preferred for Linux/Wayland compatibility
|
|
||||||
// Verify file exists before using
|
|
||||||
if (fs.existsSync(options.icon)) {
|
|
||||||
notificationOptions.icon = options.icon;
|
|
||||||
} else {
|
|
||||||
console.warn("Notification icon file not found:", options.icon);
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
typeof options.icon === "string" &&
|
|
||||||
options.icon.startsWith("data:image/")
|
|
||||||
) {
|
|
||||||
// Data URL fallback - decode to nativeImage
|
|
||||||
const base64Data = options.icon.replace(/^data:image\/\w+;base64,/, "");
|
|
||||||
try {
|
|
||||||
const image = nativeImage.createFromBuffer(
|
|
||||||
Buffer.from(base64Data, "base64"),
|
|
||||||
);
|
|
||||||
if (image.isEmpty()) {
|
|
||||||
console.warn(
|
|
||||||
"Notification icon created from base64 is empty - image format may not be supported by Electron",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
notificationOptions.icon = image;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to create notification icon from base64:", err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Unknown format, try to use as-is
|
|
||||||
notificationOptions.icon = options.icon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const notification = new Notification(notificationOptions);
|
|
||||||
notification.show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.on(
|
ipcMain.on(
|
||||||
|
|||||||
Reference in New Issue
Block a user