mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-11-10 04:46:07 -08:00
refactor(main): move IPC, CLI and app-ready wiring into assemblies (#311)
This commit is contained in:
1 parent
655510cf2e
commit
696462a82e
33 files changed
+3719
-2101
No files matched your search
@@ -0,0 +1,289 @@
|
||||
import type { GlobalShortcut } from 'electron';
|
||||
import { DEFAULT_CONFIG } from '../../config/definitions';
|
||||
import {
|
||||
registerGlobalShortcuts as registerGlobalShortcutsCore,
|
||||
type createOverlayManager,
|
||||
} from '../../core/services';
|
||||
import { resolveConfiguredShortcuts } from '../../core/utils';
|
||||
import type { Logger } from '../../logger';
|
||||
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
|
||||
import type { ResolvedConfig } from '../../types';
|
||||
import { createOverlayShortcutsRuntimeService } from '../overlay-shortcuts-runtime';
|
||||
import { composeShortcutRuntimes } from '../runtime/composers';
|
||||
import { tryBeginVisibleOverlayNumericSelection } from '../runtime/overlay-numeric-selection';
|
||||
import type { createOverlayNotificationsRuntime } from '../runtime/overlay-notifications-runtime';
|
||||
import { readMpvInputBindings } from '../runtime/mpv-input-bindings';
|
||||
import { createSessionBindingsRuntime } from '../runtime/session-bindings-runtime';
|
||||
import type { AppState } from '../state';
|
||||
import type { MiningAssembly } from './mining';
|
||||
import type { OverlayModalsAssembly } from './overlay-modals';
|
||||
import type { CliSlice, IpcSlice } from './slices';
|
||||
|
||||
type OverlayManager = ReturnType<typeof createOverlayManager>;
|
||||
|
||||
/**
|
||||
* What shortcut handling needs from the rest of the app. Siblings built after this assembly
|
||||
* in `main.ts` are reached through callbacks.
|
||||
*/
|
||||
export interface ShortcutsAssemblyDeps {
|
||||
appState: Pick<
|
||||
AppState,
|
||||
| 'shortcutsRegistered'
|
||||
| 'overlayRuntimeInitialized'
|
||||
| 'windowTracker'
|
||||
| 'keybindings'
|
||||
| 'mpvClient'
|
||||
| 'sessionBindings'
|
||||
| 'sessionBindingsInitialized'
|
||||
>;
|
||||
getConfig: () => ResolvedConfig;
|
||||
logger: Pick<Logger, 'warn'>;
|
||||
configDir: string;
|
||||
isDev: boolean;
|
||||
overlayManager: Pick<
|
||||
OverlayManager,
|
||||
'getMainWindow' | 'getVisibleOverlayVisible' | 'broadcastToOverlayWindows'
|
||||
>;
|
||||
/** Electron's globalShortcut, injected so the assembly loads under `bun test`. */
|
||||
globalShortcut: GlobalShortcut;
|
||||
getOverlayNotifications: () => Pick<
|
||||
ReturnType<typeof createOverlayNotificationsRuntime>,
|
||||
'showConfiguredStatusNotification' | 'showOverlayNotification'
|
||||
>;
|
||||
getOverlayModals: () => Pick<
|
||||
OverlayModalsAssembly,
|
||||
| 'openRuntimeOptionsPalette'
|
||||
| 'openCharacterDictionaryManagerOverlay'
|
||||
| 'openJimakuOverlay'
|
||||
| 'openTsukihimeOverlay'
|
||||
>;
|
||||
getMining: () => Pick<
|
||||
MiningAssembly,
|
||||
| 'markLastCardAsAudioCard'
|
||||
| 'copyCurrentSubtitle'
|
||||
| 'copySubtitleCount'
|
||||
| 'updateLastCardFromClipboard'
|
||||
| 'triggerFieldGrouping'
|
||||
| 'mineSentenceCard'
|
||||
| 'mineSentenceCount'
|
||||
>;
|
||||
triggerSubsyncFromConfig: () => Promise<void>;
|
||||
cycleSecondarySubMode: () => void;
|
||||
toggleVisibleOverlay: () => void;
|
||||
openDictionarySettings: () => void;
|
||||
}
|
||||
|
||||
export type ShortcutsAssembly = ReturnType<typeof createShortcutsAssembly>;
|
||||
|
||||
/**
|
||||
* Owns keyboard input outside the renderer: the overlay's local shortcuts, global shortcuts,
|
||||
* the numeric multi-copy/multi-mine sessions, and the session bindings shared with mpv.
|
||||
*/
|
||||
export function createShortcutsAssembly(deps: ShortcutsAssemblyDeps) {
|
||||
const { appState, overlayManager } = deps;
|
||||
const showStatus = (text: string) =>
|
||||
deps.getOverlayNotifications().showConfiguredStatusNotification(text);
|
||||
|
||||
const overlayShortcutsRuntime = createOverlayShortcutsRuntimeService({
|
||||
getConfiguredShortcuts: () => getConfiguredShortcuts(),
|
||||
getShortcutsRegistered: () => appState.shortcutsRegistered,
|
||||
setShortcutsRegistered: (registered: boolean) => {
|
||||
appState.shortcutsRegistered = registered;
|
||||
},
|
||||
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
||||
isOverlayShortcutContextActive: () => {
|
||||
if (process.platform !== 'win32') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!overlayManager.getVisibleOverlayVisible()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const windowTracker = appState.windowTracker;
|
||||
if (!windowTracker || !windowTracker.isTracking()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return windowTracker.isTargetWindowFocused();
|
||||
},
|
||||
showMpvOsd: showStatus,
|
||||
openRuntimeOptionsPalette: () => {
|
||||
deps.getOverlayModals().openRuntimeOptionsPalette();
|
||||
},
|
||||
openCharacterDictionaryManager: () => {
|
||||
deps.getOverlayModals().openCharacterDictionaryManagerOverlay();
|
||||
},
|
||||
openJimaku: () => {
|
||||
deps.getOverlayModals().openJimakuOverlay();
|
||||
},
|
||||
openTsukihime: () => {
|
||||
deps.getOverlayModals().openTsukihimeOverlay();
|
||||
},
|
||||
markAudioCard: () => deps.getMining().markLastCardAsAudioCard(),
|
||||
copySubtitleMultiple: (timeoutMs: number) => {
|
||||
startPendingMultiCopy(timeoutMs);
|
||||
},
|
||||
copySubtitle: () => {
|
||||
deps.getMining().copyCurrentSubtitle();
|
||||
},
|
||||
toggleSecondarySubMode: () => deps.cycleSecondarySubMode(),
|
||||
updateLastCardFromClipboard: () => deps.getMining().updateLastCardFromClipboard(),
|
||||
triggerFieldGrouping: () => deps.getMining().triggerFieldGrouping(),
|
||||
triggerSubsyncFromConfig: () => deps.triggerSubsyncFromConfig(),
|
||||
mineSentenceCard: () => deps.getMining().mineSentenceCard(),
|
||||
mineSentenceMultiple: (timeoutMs: number) => {
|
||||
startPendingMineSentenceMultiple(timeoutMs);
|
||||
},
|
||||
cancelPendingMultiCopy: () => {
|
||||
cancelPendingMultiCopy();
|
||||
},
|
||||
cancelPendingMineSentenceMultiple: () => {
|
||||
cancelPendingMineSentenceMultiple();
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
getConfiguredShortcuts,
|
||||
registerGlobalShortcuts,
|
||||
refreshGlobalAndOverlayShortcuts,
|
||||
cancelPendingMultiCopy,
|
||||
startPendingMultiCopy,
|
||||
cancelPendingMineSentenceMultiple,
|
||||
startPendingMineSentenceMultiple,
|
||||
syncOverlayShortcuts,
|
||||
refreshOverlayShortcuts,
|
||||
} = composeShortcutRuntimes({
|
||||
globalShortcuts: {
|
||||
getConfiguredShortcutsMainDeps: {
|
||||
getResolvedConfig: () => deps.getConfig(),
|
||||
defaultConfig: DEFAULT_CONFIG,
|
||||
resolveConfiguredShortcuts,
|
||||
},
|
||||
buildRegisterGlobalShortcutsMainDeps: (getConfiguredShortcutsHandler) => ({
|
||||
getConfiguredShortcuts: () => getConfiguredShortcutsHandler(),
|
||||
registerGlobalShortcutsCore,
|
||||
onToggleVisibleOverlay: () => deps.toggleVisibleOverlay(),
|
||||
onOpenYomitanSettings: () => deps.openDictionarySettings(),
|
||||
isDev: deps.isDev,
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
}),
|
||||
buildRefreshGlobalAndOverlayShortcutsMainDeps: (registerGlobalShortcutsHandler) => ({
|
||||
unregisterAllGlobalShortcuts: () => deps.globalShortcut.unregisterAll(),
|
||||
registerGlobalShortcuts: () => registerGlobalShortcutsHandler(),
|
||||
syncOverlayShortcuts: () => syncOverlayShortcuts(),
|
||||
}),
|
||||
},
|
||||
numericShortcutRuntimeMainDeps: {
|
||||
globalShortcut: deps.globalShortcut,
|
||||
showMpvOsd: showStatus,
|
||||
setTimer: (handler, timeoutMs) => setTimeout(handler, timeoutMs),
|
||||
clearTimer: (timer) => clearTimeout(timer),
|
||||
},
|
||||
numericSessions: {
|
||||
onMultiCopyDigit: (count) => deps.getMining().copySubtitleCount(count),
|
||||
onMineSentenceDigit: (count) => deps.getMining().mineSentenceCount(count),
|
||||
tryBeginMultiCopyOverlaySelection: (timeoutMs) =>
|
||||
tryBeginVisibleOverlayNumericSelection({
|
||||
actionId: 'copySubtitleMultiple',
|
||||
timeoutMs,
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
}),
|
||||
tryBeginMineSentenceOverlaySelection: (timeoutMs) =>
|
||||
tryBeginVisibleOverlayNumericSelection({
|
||||
actionId: 'mineSentenceMultiple',
|
||||
timeoutMs,
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
}),
|
||||
},
|
||||
overlayShortcutsRuntimeMainDeps: {
|
||||
overlayShortcutsRuntime,
|
||||
},
|
||||
});
|
||||
|
||||
const { persistSessionBindings, refreshCurrentSessionBindings, refreshMpvSessionBindings } =
|
||||
createSessionBindingsRuntime({
|
||||
configDir: deps.configDir,
|
||||
getKeybindings: () => appState.keybindings,
|
||||
getConfiguredShortcuts: () => getConfiguredShortcuts(),
|
||||
getResolvedConfig: () => deps.getConfig(),
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
setSessionBindings: (bindings) => {
|
||||
appState.sessionBindings = bindings;
|
||||
},
|
||||
setSessionBindingsInitialized: (initialized) => {
|
||||
appState.sessionBindingsInitialized = initialized;
|
||||
},
|
||||
logWarn: (message) => deps.logger.warn(message),
|
||||
onBindingsChanged: (bindings) =>
|
||||
overlayManager.broadcastToOverlayWindows(
|
||||
IPC_CHANNELS.event.sessionBindingsChanged,
|
||||
bindings,
|
||||
),
|
||||
onMpvInputBindingsChanged: () =>
|
||||
overlayManager.broadcastToOverlayWindows(IPC_CHANNELS.event.mpvInputBindingsChanged),
|
||||
onWarning: (warning) => {
|
||||
if (warning.kind !== 'conflict') return;
|
||||
deps.getOverlayNotifications().showOverlayNotification({
|
||||
id: `session-binding-conflict:${warning.path}`,
|
||||
title: 'Shortcut conflict',
|
||||
body: warning.message,
|
||||
variant: 'warning',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const ipc: IpcSlice<'getMpvInputBindings' | 'getSessionBindings' | 'getConfiguredShortcuts'> = {
|
||||
getMpvInputBindings: async () => {
|
||||
await refreshMpvSessionBindings();
|
||||
return readMpvInputBindings({
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
getConfiguredKeybindings: () => deps.getConfig().keybindings ?? [],
|
||||
platform:
|
||||
process.platform === 'darwin'
|
||||
? 'darwin'
|
||||
: process.platform === 'win32'
|
||||
? 'win32'
|
||||
: 'linux',
|
||||
});
|
||||
},
|
||||
getSessionBindings: async () => {
|
||||
await refreshMpvSessionBindings();
|
||||
return appState.sessionBindings;
|
||||
},
|
||||
getConfiguredShortcuts: () => getConfiguredShortcuts(),
|
||||
};
|
||||
|
||||
const cli: CliSlice<
|
||||
'startPendingMultiCopy' | 'startPendingMineSentenceMultiple' | 'getMultiCopyTimeoutMs'
|
||||
> = {
|
||||
startPendingMultiCopy: (timeoutMs) => startPendingMultiCopy(timeoutMs),
|
||||
startPendingMineSentenceMultiple: (timeoutMs) => startPendingMineSentenceMultiple(timeoutMs),
|
||||
getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
|
||||
};
|
||||
|
||||
return {
|
||||
ipc,
|
||||
cli,
|
||||
overlayShortcutsRuntime,
|
||||
getConfiguredShortcuts,
|
||||
registerGlobalShortcuts,
|
||||
refreshGlobalAndOverlayShortcuts,
|
||||
startPendingMultiCopy,
|
||||
startPendingMineSentenceMultiple,
|
||||
refreshOverlayShortcuts,
|
||||
persistSessionBindings,
|
||||
refreshCurrentSessionBindings,
|
||||
refreshMpvSessionBindings,
|
||||
/** Overlay shortcuts are released while a modal owns keyboard input. */
|
||||
syncOverlayShortcutsForModal: (isActive: boolean): void => {
|
||||
if (isActive) {
|
||||
overlayShortcutsRuntime.unregisterOverlayShortcuts();
|
||||
} else {
|
||||
overlayShortcutsRuntime.syncOverlayShortcuts();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user