mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 12:11:28 -07:00
feat(core): add Electron runtime, services, and app composition
This commit is contained in:
86
src/shared/ipc/contracts.ts
Normal file
86
src/shared/ipc/contracts.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { OverlayContentMeasurement, RuntimeOptionId, RuntimeOptionValue } from '../../types';
|
||||
|
||||
export const OVERLAY_HOSTED_MODALS = ['runtime-options', 'subsync', 'jimaku'] as const;
|
||||
export type OverlayHostedModal = (typeof OVERLAY_HOSTED_MODALS)[number];
|
||||
|
||||
export const IPC_CHANNELS = {
|
||||
command: {
|
||||
setIgnoreMouseEvents: 'set-ignore-mouse-events',
|
||||
overlayModalClosed: 'overlay:modal-closed',
|
||||
openYomitanSettings: 'open-yomitan-settings',
|
||||
quitApp: 'quit-app',
|
||||
toggleDevTools: 'toggle-dev-tools',
|
||||
toggleOverlay: 'toggle-overlay',
|
||||
saveSubtitlePosition: 'save-subtitle-position',
|
||||
setMecabEnabled: 'set-mecab-enabled',
|
||||
mpvCommand: 'mpv-command',
|
||||
setAnkiConnectEnabled: 'set-anki-connect-enabled',
|
||||
clearAnkiConnectHistory: 'clear-anki-connect-history',
|
||||
refreshKnownWords: 'anki:refresh-known-words',
|
||||
kikuFieldGroupingRespond: 'kiku:field-grouping-respond',
|
||||
reportOverlayContentBounds: 'overlay-content-bounds:report',
|
||||
},
|
||||
request: {
|
||||
getOverlayVisibility: 'get-overlay-visibility',
|
||||
getVisibleOverlayVisibility: 'get-visible-overlay-visibility',
|
||||
getInvisibleOverlayVisibility: 'get-invisible-overlay-visibility',
|
||||
getCurrentSubtitle: 'get-current-subtitle',
|
||||
getCurrentSubtitleRaw: 'get-current-subtitle-raw',
|
||||
getCurrentSubtitleAss: 'get-current-subtitle-ass',
|
||||
getMpvSubtitleRenderMetrics: 'get-mpv-subtitle-render-metrics',
|
||||
getSubtitlePosition: 'get-subtitle-position',
|
||||
getSubtitleStyle: 'get-subtitle-style',
|
||||
getMecabStatus: 'get-mecab-status',
|
||||
getKeybindings: 'get-keybindings',
|
||||
getConfigShortcuts: 'get-config-shortcuts',
|
||||
getSecondarySubMode: 'get-secondary-sub-mode',
|
||||
getCurrentSecondarySub: 'get-current-secondary-sub',
|
||||
focusMainWindow: 'focus-main-window',
|
||||
runSubsyncManual: 'subsync:run-manual',
|
||||
getAnkiConnectStatus: 'get-anki-connect-status',
|
||||
getRuntimeOptions: 'runtime-options:get',
|
||||
setRuntimeOption: 'runtime-options:set',
|
||||
cycleRuntimeOption: 'runtime-options:cycle',
|
||||
getAnilistStatus: 'anilist:get-status',
|
||||
clearAnilistToken: 'anilist:clear-token',
|
||||
openAnilistSetup: 'anilist:open-setup',
|
||||
getAnilistQueueStatus: 'anilist:get-queue-status',
|
||||
retryAnilistNow: 'anilist:retry-now',
|
||||
appendClipboardVideoToQueue: 'clipboard:append-video-to-queue',
|
||||
jimakuGetMediaInfo: 'jimaku:get-media-info',
|
||||
jimakuSearchEntries: 'jimaku:search-entries',
|
||||
jimakuListFiles: 'jimaku:list-files',
|
||||
jimakuDownloadFile: 'jimaku:download-file',
|
||||
kikuBuildMergePreview: 'kiku:build-merge-preview',
|
||||
},
|
||||
event: {
|
||||
subtitleSet: 'subtitle:set',
|
||||
subtitleVisibility: 'mpv:subVisibility',
|
||||
subtitlePositionSet: 'subtitle-position:set',
|
||||
mpvSubtitleRenderMetricsSet: 'mpv-subtitle-render-metrics:set',
|
||||
subtitleAssSet: 'subtitle-ass:set',
|
||||
overlayDebugVisualizationSet: 'overlay-debug-visualization:set',
|
||||
secondarySubtitleSet: 'secondary-subtitle:set',
|
||||
secondarySubtitleMode: 'secondary-subtitle:mode',
|
||||
subsyncOpenManual: 'subsync:open-manual',
|
||||
kikuFieldGroupingRequest: 'kiku:field-grouping-request',
|
||||
runtimeOptionsChanged: 'runtime-options:changed',
|
||||
runtimeOptionsOpen: 'runtime-options:open',
|
||||
jimakuOpen: 'jimaku:open',
|
||||
configHotReload: 'config:hot-reload',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type RuntimeOptionsSetRequest = {
|
||||
id: RuntimeOptionId;
|
||||
value: RuntimeOptionValue;
|
||||
};
|
||||
|
||||
export type RuntimeOptionsCycleRequest = {
|
||||
id: RuntimeOptionId;
|
||||
direction: 1 | -1;
|
||||
};
|
||||
|
||||
export type OverlayContentBoundsReportRequest = {
|
||||
measurement: OverlayContentMeasurement;
|
||||
};
|
||||
157
src/shared/ipc/validators.ts
Normal file
157
src/shared/ipc/validators.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import type {
|
||||
JimakuDownloadQuery,
|
||||
JimakuFilesQuery,
|
||||
JimakuSearchQuery,
|
||||
KikuFieldGroupingChoice,
|
||||
KikuMergePreviewRequest,
|
||||
RuntimeOptionId,
|
||||
RuntimeOptionValue,
|
||||
SubtitlePosition,
|
||||
SubsyncManualRunRequest,
|
||||
} from '../../types';
|
||||
import { OVERLAY_HOSTED_MODALS, type OverlayHostedModal } from './contracts';
|
||||
|
||||
const RUNTIME_OPTION_IDS: RuntimeOptionId[] = [
|
||||
'anki.autoUpdateNewCards',
|
||||
'anki.kikuFieldGrouping',
|
||||
'anki.nPlusOneMatchMode',
|
||||
];
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isFiniteNumber(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isFinite(value);
|
||||
}
|
||||
|
||||
function isInteger(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isInteger(value);
|
||||
}
|
||||
|
||||
export function parseOverlayHostedModal(value: unknown): OverlayHostedModal | null {
|
||||
if (typeof value !== 'string') return null;
|
||||
return OVERLAY_HOSTED_MODALS.includes(value as OverlayHostedModal)
|
||||
? (value as OverlayHostedModal)
|
||||
: null;
|
||||
}
|
||||
|
||||
export function parseSubtitlePosition(value: unknown): SubtitlePosition | null {
|
||||
if (!isObject(value) || !isFiniteNumber(value.yPercent)) {
|
||||
return null;
|
||||
}
|
||||
const hasX = value.invisibleOffsetXPx !== undefined;
|
||||
if (hasX && !isFiniteNumber(value.invisibleOffsetXPx)) {
|
||||
return null;
|
||||
}
|
||||
const hasY = value.invisibleOffsetYPx !== undefined;
|
||||
if (hasY && !isFiniteNumber(value.invisibleOffsetYPx)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
yPercent: value.yPercent,
|
||||
invisibleOffsetXPx: hasX ? (value.invisibleOffsetXPx as number) : undefined,
|
||||
invisibleOffsetYPx: hasY ? (value.invisibleOffsetYPx as number) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseSubsyncManualRunRequest(value: unknown): SubsyncManualRunRequest | null {
|
||||
if (!isObject(value)) return null;
|
||||
const { engine, sourceTrackId } = value;
|
||||
if (engine !== 'alass' && engine !== 'ffsubsync') return null;
|
||||
if (sourceTrackId !== undefined && sourceTrackId !== null && !isInteger(sourceTrackId)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
engine,
|
||||
sourceTrackId: sourceTrackId === undefined ? undefined : (sourceTrackId as number | null),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseRuntimeOptionId(value: unknown): RuntimeOptionId | null {
|
||||
if (typeof value !== 'string') return null;
|
||||
return RUNTIME_OPTION_IDS.includes(value as RuntimeOptionId) ? (value as RuntimeOptionId) : null;
|
||||
}
|
||||
|
||||
export function parseRuntimeOptionDirection(value: unknown): 1 | -1 | null {
|
||||
return value === 1 || value === -1 ? value : null;
|
||||
}
|
||||
|
||||
export function parseRuntimeOptionValue(value: unknown): RuntimeOptionValue | null {
|
||||
return typeof value === 'boolean' || typeof value === 'string'
|
||||
? (value as RuntimeOptionValue)
|
||||
: null;
|
||||
}
|
||||
|
||||
export function parseMpvCommand(value: unknown): Array<string | number> | null {
|
||||
if (!Array.isArray(value)) return null;
|
||||
return value.every((entry) => typeof entry === 'string' || typeof entry === 'number')
|
||||
? (value as Array<string | number>)
|
||||
: null;
|
||||
}
|
||||
|
||||
export function parseOptionalForwardingOptions(value: unknown): {
|
||||
forward?: boolean;
|
||||
} {
|
||||
if (!isObject(value)) return {};
|
||||
const { forward } = value;
|
||||
if (forward === undefined) return {};
|
||||
return typeof forward === 'boolean' ? { forward } : {};
|
||||
}
|
||||
|
||||
export function parseKikuFieldGroupingChoice(value: unknown): KikuFieldGroupingChoice | null {
|
||||
if (!isObject(value)) return null;
|
||||
const { keepNoteId, deleteNoteId, deleteDuplicate, cancelled } = value;
|
||||
if (!isInteger(keepNoteId) || !isInteger(deleteNoteId)) return null;
|
||||
if (typeof deleteDuplicate !== 'boolean' || typeof cancelled !== 'boolean') return null;
|
||||
return {
|
||||
keepNoteId,
|
||||
deleteNoteId,
|
||||
deleteDuplicate,
|
||||
cancelled,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseKikuMergePreviewRequest(value: unknown): KikuMergePreviewRequest | null {
|
||||
if (!isObject(value)) return null;
|
||||
const { keepNoteId, deleteNoteId, deleteDuplicate } = value;
|
||||
if (!isInteger(keepNoteId) || !isInteger(deleteNoteId)) return null;
|
||||
if (typeof deleteDuplicate !== 'boolean') return null;
|
||||
return {
|
||||
keepNoteId,
|
||||
deleteNoteId,
|
||||
deleteDuplicate,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseJimakuSearchQuery(value: unknown): JimakuSearchQuery | null {
|
||||
if (!isObject(value) || typeof value.query !== 'string') return null;
|
||||
return { query: value.query };
|
||||
}
|
||||
|
||||
export function parseJimakuFilesQuery(value: unknown): JimakuFilesQuery | null {
|
||||
if (!isObject(value) || !isInteger(value.entryId)) return null;
|
||||
if (value.episode !== undefined && value.episode !== null && !isInteger(value.episode)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
entryId: value.entryId,
|
||||
episode: (value.episode as number | null | undefined) ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseJimakuDownloadQuery(value: unknown): JimakuDownloadQuery | null {
|
||||
if (!isObject(value)) return null;
|
||||
if (
|
||||
!isInteger(value.entryId) ||
|
||||
typeof value.url !== 'string' ||
|
||||
typeof value.name !== 'string'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
entryId: value.entryId,
|
||||
url: value.url,
|
||||
name: value.name,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user