mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-11-10 04:46:07 -08:00
* feat(controller): add hold + press combos to main bindings Main Alt+C actions can now be learned as combos such as L1 + D-pad right. A combo fires on each press while its hold button is down, and claims its press input so token move and popup scroll ignore it until it is released. A plain action on a hold button fires on release, unless another bound button was pressed during the hold. The save path needed two changes for combos to persist. The IPC validator rebuilt bindings field by field and dropped `hold`, so Alt+C showed a combo from its local copy until the next reload turned it back into a plain button. Validator and config resolver now share one parser. Saves also replace the controller section, since the deep merge in patchRawConfig kept the old `hold` when a combo was rebound to one button. Also fixes Learn on the (D-pad) rows, which threw after cancel() cleared the capture target. * test(launcher): wait for content in background child test The test polled for the fake app's output files to exist, but the shell creates a redirect target before writing to it. A poll landing in that gap read an empty env file and failed with '' !== '1', as seen once in CI. Wait until both files have content instead. * fix(controller): keep a diagonal push from learning as a combo Learn took the first axis of a diagonal stick push as the hold and the second as the press, saving a combo that one axis could never trigger. This reached main Alt+C rows when they moved to combo capture. Axes crossing the threshold in the same poll as the first input now count as one movement. A second axis moved in a later poll still records a combo. * fix(controller): only treat simultaneous axes as one movement The diagonal push guard also blocked an axis activated in the same poll as a button, so L1 plus a stick direction could not record the stick until it was released and moved again. Apply the guard only when the first input is an axis, so that input pair records as a combo again.
794 lines
27 KiB
TypeScript
794 lines
27 KiB
TypeScript
import type { MpvInputBindingsSnapshot } from './session-bindings';
|
||
import type {
|
||
KikuFieldGroupingChoice,
|
||
KikuFieldGroupingRequestData,
|
||
KikuMergePreviewRequest,
|
||
KikuMergePreviewResponse,
|
||
MediaTimingReviewActionResult,
|
||
MediaTimingReviewOpenPayload,
|
||
MediaTimingReviewPreviewPosition,
|
||
MediaTimingReviewPreviewRequest,
|
||
MediaTimingReviewResolveRequest,
|
||
MediaTimingReviewFrameRequest,
|
||
MediaTimingReviewFrameResult,
|
||
MediaTimingReviewWaveformRequest,
|
||
MediaTimingReviewWaveformResult,
|
||
} from './anki';
|
||
import type { ChangelogSnapshot } from './changelog';
|
||
import type { SubtitleGenerationProgress } from '../shared/subtitle-generation';
|
||
import type { SubtitleGenerationModelId } from '../shared/subtitle-generation-model-catalog';
|
||
import type {
|
||
SubtitleGenerationResult,
|
||
SubtitleGenerationStatus,
|
||
} from '../shared/subtitle-generation-ipc';
|
||
import type { ResolvedConfig, ShortcutsConfig } from './config';
|
||
import type {
|
||
CompiledSessionBinding,
|
||
SessionActionId,
|
||
SessionActionPayload,
|
||
SessionBindingWarning,
|
||
} from './session-bindings';
|
||
import type {
|
||
TsukihimeApiResponse,
|
||
TsukihimeDownloadQuery,
|
||
TsukihimeDownloadResult,
|
||
TsukihimeEntry,
|
||
TsukihimeFilesQuery,
|
||
TsukihimeSearchQuery,
|
||
TsukihimeSubtitleFile,
|
||
JimakuApiResponse,
|
||
JimakuDownloadQuery,
|
||
JimakuDownloadResult,
|
||
JimakuEntry,
|
||
JimakuFileEntry,
|
||
JimakuFilesQuery,
|
||
JimakuMediaInfo,
|
||
JimakuSearchQuery,
|
||
YoutubePickerOpenPayload,
|
||
YoutubePickerResolveRequest,
|
||
YoutubePickerResolveResult,
|
||
} from './integrations';
|
||
import type {
|
||
PrimarySubMode,
|
||
ResolvedSubtitleSidebarConfig,
|
||
SecondarySubMode,
|
||
SubtitleData,
|
||
SubtitleMiningContext,
|
||
SubtitlePosition,
|
||
SubtitleSidebarPlaybackState,
|
||
SubtitleSidebarSnapshot,
|
||
SubtitleRendererStyleConfig,
|
||
} from './subtitle';
|
||
import type {
|
||
RuntimeOptionApplyResult,
|
||
RuntimeOptionId,
|
||
RuntimeOptionState,
|
||
RuntimeOptionValue,
|
||
} from './runtime-options';
|
||
import type {
|
||
OverlayNotificationAction,
|
||
OverlayNotificationEventPayload,
|
||
OverlayNotificationPosition,
|
||
} from './notification';
|
||
|
||
export interface WindowGeometry {
|
||
x: number;
|
||
y: number;
|
||
width: number;
|
||
height: number;
|
||
}
|
||
|
||
export interface Keybinding {
|
||
key: string;
|
||
command: (string | number)[] | null;
|
||
}
|
||
|
||
export interface MpvClient {
|
||
currentSubText: string;
|
||
currentVideoPath: string;
|
||
currentMediaTitle?: string | null;
|
||
currentTimePos: number;
|
||
currentSubStart: number;
|
||
currentSubEnd: number;
|
||
/** mpv `sub-delay`; `sub-start`/`sub-end` do not include it. */
|
||
currentSubDelay: number;
|
||
currentAudioStreamIndex: number | null;
|
||
requestProperty?: (name: string) => Promise<unknown>;
|
||
send(command: { command: unknown[]; request_id?: number }): boolean;
|
||
}
|
||
|
||
export interface SubsyncSubtitleTrack {
|
||
id: number;
|
||
label: string;
|
||
}
|
||
|
||
export interface SubsyncManualPayload {
|
||
subtitleTracks: SubsyncSubtitleTrack[];
|
||
defaultReferenceTrackId: number | null;
|
||
defaultTargetTrackId: number | null;
|
||
videoReferenceAvailable: boolean;
|
||
ffsubsyncAvailable: boolean;
|
||
}
|
||
|
||
export type SubsyncReferenceMode = 'track' | 'video';
|
||
|
||
export interface SubsyncManualRunRequest {
|
||
engine: 'alass' | 'ffsubsync';
|
||
/** alass reference source: another subtitle track, or the loaded media file itself. */
|
||
referenceMode?: SubsyncReferenceMode;
|
||
referenceTrackId?: number | null;
|
||
/** Subtitle track to retime. Defaults to the active primary track. */
|
||
targetTrackId?: number | null;
|
||
}
|
||
|
||
export interface SubsyncResult {
|
||
ok: boolean;
|
||
message: string;
|
||
}
|
||
|
||
export interface PlaylistBrowserDirectoryItem {
|
||
path: string;
|
||
basename: string;
|
||
episodeLabel?: string | null;
|
||
isCurrentFile: boolean;
|
||
}
|
||
|
||
export interface PlaylistBrowserQueueItem {
|
||
index: number;
|
||
id: number | null;
|
||
filename: string;
|
||
title: string | null;
|
||
displayLabel: string;
|
||
current: boolean;
|
||
playing: boolean;
|
||
path: string | null;
|
||
}
|
||
|
||
export interface PlaylistBrowserSnapshot {
|
||
directoryPath: string | null;
|
||
directoryAvailable: boolean;
|
||
directoryStatus: string;
|
||
directoryItems: PlaylistBrowserDirectoryItem[];
|
||
playlistItems: PlaylistBrowserQueueItem[];
|
||
playingIndex: number | null;
|
||
currentFilePath: string | null;
|
||
}
|
||
|
||
export interface PlaylistBrowserMutationResult {
|
||
ok: boolean;
|
||
message: string;
|
||
snapshot?: PlaylistBrowserSnapshot;
|
||
}
|
||
|
||
export type ControllerButtonBinding =
|
||
| 'none'
|
||
| 'select'
|
||
| 'buttonSouth'
|
||
| 'buttonEast'
|
||
| 'buttonNorth'
|
||
| 'buttonWest'
|
||
| 'leftShoulder'
|
||
| 'rightShoulder'
|
||
| 'leftStickPress'
|
||
| 'rightStickPress'
|
||
| 'leftTrigger'
|
||
| 'rightTrigger';
|
||
|
||
export type ControllerAxisBinding = 'leftStickX' | 'leftStickY' | 'rightStickX' | 'rightStickY';
|
||
export type ControllerTriggerInputMode = 'auto' | 'digital' | 'analog';
|
||
/**
|
||
* Face button style: Xbox (A at the bottom), Nintendo (A on the right) or PlayStation
|
||
* (╳ at the bottom, laid out like Xbox). Decides button names and where letters land.
|
||
*/
|
||
export type ControllerFaceButtonLabels = 'xbox' | 'nintendo' | 'playstation';
|
||
export type ControllerAxisDirection = 'negative' | 'positive';
|
||
export type ControllerDpadFallback = 'none' | 'horizontal' | 'vertical';
|
||
|
||
export interface ControllerNoneBinding {
|
||
kind: 'none';
|
||
}
|
||
|
||
export interface ControllerButtonInputBinding {
|
||
kind: 'button';
|
||
buttonIndex: number;
|
||
/** Makes this a combo: it fires when this input is pressed while `hold` is down. */
|
||
hold?: ControllerRawInput;
|
||
}
|
||
|
||
export interface ControllerAxisDirectionInputBinding {
|
||
kind: 'axis';
|
||
axisIndex: number;
|
||
direction: ControllerAxisDirection;
|
||
/** Makes this a combo: it fires when this input is pressed while `hold` is down. */
|
||
hold?: ControllerRawInput;
|
||
}
|
||
|
||
export interface ControllerAxisInputBinding {
|
||
kind: 'axis';
|
||
axisIndex: number;
|
||
dpadFallback?: ControllerDpadFallback;
|
||
}
|
||
|
||
export type ControllerDiscreteBindingConfig =
|
||
| ControllerButtonBinding
|
||
| ControllerNoneBinding
|
||
| ControllerButtonInputBinding
|
||
| ControllerAxisDirectionInputBinding;
|
||
|
||
export type ResolvedControllerDiscreteBinding =
|
||
| ControllerNoneBinding
|
||
| ControllerButtonInputBinding
|
||
| ControllerAxisDirectionInputBinding;
|
||
|
||
export type ControllerAxisBindingConfig =
|
||
| ControllerAxisBinding
|
||
| ControllerNoneBinding
|
||
| ControllerAxisInputBinding;
|
||
|
||
export type ResolvedControllerAxisBinding =
|
||
| ControllerNoneBinding
|
||
| {
|
||
kind: 'axis';
|
||
axisIndex: number;
|
||
dpadFallback: ControllerDpadFallback;
|
||
};
|
||
|
||
export interface ControllerBindingsConfig {
|
||
toggleLookup?: ControllerDiscreteBindingConfig;
|
||
closeLookup?: ControllerDiscreteBindingConfig;
|
||
toggleKeyboardOnlyMode?: ControllerDiscreteBindingConfig;
|
||
mineCard?: ControllerDiscreteBindingConfig;
|
||
quitMpv?: ControllerDiscreteBindingConfig;
|
||
previousAudio?: ControllerDiscreteBindingConfig;
|
||
nextAudio?: ControllerDiscreteBindingConfig;
|
||
playCurrentAudio?: ControllerDiscreteBindingConfig;
|
||
toggleMpvPause?: ControllerDiscreteBindingConfig;
|
||
holdSecondarySub?: ControllerDiscreteBindingConfig;
|
||
previousEntry?: ControllerDiscreteBindingConfig;
|
||
nextEntry?: ControllerDiscreteBindingConfig;
|
||
toggleCharacterNavigation?: ControllerDiscreteBindingConfig;
|
||
previousSubtitle?: ControllerDiscreteBindingConfig;
|
||
nextSubtitle?: ControllerDiscreteBindingConfig;
|
||
replaySubtitle?: ControllerDiscreteBindingConfig;
|
||
playNextSubtitle?: ControllerDiscreteBindingConfig;
|
||
leftStickHorizontal?: ControllerAxisBindingConfig;
|
||
leftStickVertical?: ControllerAxisBindingConfig;
|
||
rightStickHorizontal?: ControllerAxisBindingConfig;
|
||
rightStickVertical?: ControllerAxisBindingConfig;
|
||
}
|
||
|
||
export interface ResolvedControllerBindingsConfig {
|
||
toggleLookup?: ResolvedControllerDiscreteBinding;
|
||
closeLookup?: ResolvedControllerDiscreteBinding;
|
||
toggleKeyboardOnlyMode?: ResolvedControllerDiscreteBinding;
|
||
mineCard?: ResolvedControllerDiscreteBinding;
|
||
quitMpv?: ResolvedControllerDiscreteBinding;
|
||
previousAudio?: ResolvedControllerDiscreteBinding;
|
||
nextAudio?: ResolvedControllerDiscreteBinding;
|
||
playCurrentAudio?: ResolvedControllerDiscreteBinding;
|
||
toggleMpvPause?: ResolvedControllerDiscreteBinding;
|
||
holdSecondarySub?: ResolvedControllerDiscreteBinding;
|
||
previousEntry?: ResolvedControllerDiscreteBinding;
|
||
nextEntry?: ResolvedControllerDiscreteBinding;
|
||
toggleCharacterNavigation?: ResolvedControllerDiscreteBinding;
|
||
previousSubtitle?: ResolvedControllerDiscreteBinding;
|
||
nextSubtitle?: ResolvedControllerDiscreteBinding;
|
||
replaySubtitle?: ResolvedControllerDiscreteBinding;
|
||
playNextSubtitle?: ResolvedControllerDiscreteBinding;
|
||
leftStickHorizontal?: ResolvedControllerAxisBinding;
|
||
leftStickVertical?: ResolvedControllerAxisBinding;
|
||
rightStickHorizontal?: ResolvedControllerAxisBinding;
|
||
rightStickVertical?: ResolvedControllerAxisBinding;
|
||
}
|
||
|
||
export interface ControllerButtonIndicesConfig {
|
||
select?: number;
|
||
buttonSouth?: number;
|
||
buttonEast?: number;
|
||
buttonNorth?: number;
|
||
buttonWest?: number;
|
||
leftShoulder?: number;
|
||
rightShoulder?: number;
|
||
leftStickPress?: number;
|
||
rightStickPress?: number;
|
||
leftTrigger?: number;
|
||
rightTrigger?: number;
|
||
}
|
||
|
||
/** Every button on the media timing review dialog, named by what it does. */
|
||
export type ControllerReviewAction =
|
||
| 'confirm'
|
||
| 'cancel'
|
||
| 'play'
|
||
| 'resetTiming'
|
||
| 'showEarlier'
|
||
| 'showLater'
|
||
| 'framePrevious'
|
||
| 'frameNext'
|
||
| 'frameReset'
|
||
| 'startBack'
|
||
| 'startForward'
|
||
| 'previousLineAdd'
|
||
| 'previousLineRemove'
|
||
| 'endBack'
|
||
| 'endForward'
|
||
| 'nextLineAdd'
|
||
| 'nextLineRemove'
|
||
| 'useOriginal'
|
||
| 'keepEditing'
|
||
| 'skipMedia'
|
||
| 'discard';
|
||
|
||
/** Every button on the duplicate-card field grouping dialog, named by what it does. */
|
||
export type ControllerFieldGroupingAction =
|
||
| 'selectOriginal'
|
||
| 'selectNew'
|
||
| 'toggleDeleteDuplicate'
|
||
| 'continue'
|
||
| 'cancel'
|
||
| 'confirmMerge'
|
||
| 'back'
|
||
| 'cancelMerge'
|
||
| 'toggleView'
|
||
| 'scrollUp'
|
||
| 'scrollDown';
|
||
|
||
export type ControllerFaceLetter = 'a' | 'b' | 'x' | 'y';
|
||
export type ControllerDpadDirection = 'left' | 'right' | 'up' | 'down';
|
||
|
||
/**
|
||
* One input in a review binding. `button` and `axis` are raw indices recorded by Learn;
|
||
* `face` (printed letter, via `faceButtonLabels`) and `dpad` (any D-pad report) keep the
|
||
* defaults working on every controller layout.
|
||
*/
|
||
export type ControllerReviewInput =
|
||
| { kind: 'button'; buttonIndex: number }
|
||
| { kind: 'axis'; axisIndex: number; direction: ControllerAxisDirection }
|
||
| { kind: 'face'; letter: ControllerFaceLetter }
|
||
| { kind: 'dpad'; direction: ControllerDpadDirection };
|
||
|
||
/** An input as Learn records it: a raw button index or a raw axis direction. */
|
||
export type ControllerRawInput = Extract<ControllerReviewInput, { kind: 'button' | 'axis' }>;
|
||
|
||
/** Press `press`, optionally while holding `hold`. */
|
||
export type ControllerReviewBindingConfig =
|
||
| { kind: 'none' }
|
||
| { kind?: 'combo'; press: ControllerReviewInput; hold?: ControllerReviewInput | null };
|
||
|
||
export type ResolvedControllerReviewBinding =
|
||
| { kind: 'none' }
|
||
| { kind: 'combo'; press: ControllerReviewInput; hold: ControllerReviewInput | null };
|
||
|
||
export type ControllerReviewBindingsConfig = Partial<
|
||
Record<ControllerReviewAction, ControllerReviewBindingConfig>
|
||
>;
|
||
export type ResolvedControllerReviewBindings = Record<
|
||
ControllerReviewAction,
|
||
ResolvedControllerReviewBinding
|
||
>;
|
||
|
||
export type ControllerFieldGroupingBindingsConfig = Partial<
|
||
Record<ControllerFieldGroupingAction, ControllerReviewBindingConfig>
|
||
>;
|
||
export type ResolvedControllerFieldGroupingBindings = Record<
|
||
ControllerFieldGroupingAction,
|
||
ResolvedControllerReviewBinding
|
||
>;
|
||
|
||
export interface ControllerProfileConfig {
|
||
label?: string;
|
||
/** Overrides `controller.faceButtonLabels` for this controller; set from Alt+C. */
|
||
faceButtonLabels?: ControllerFaceButtonLabels;
|
||
buttonIndices?: ControllerButtonIndicesConfig;
|
||
bindings?: ControllerBindingsConfig;
|
||
reviewBindings?: ControllerReviewBindingsConfig;
|
||
fieldGroupingBindings?: ControllerFieldGroupingBindingsConfig;
|
||
}
|
||
|
||
export interface ResolvedControllerProfileConfig {
|
||
label: string;
|
||
faceButtonLabels: ControllerFaceButtonLabels;
|
||
buttonIndices: Required<ControllerButtonIndicesConfig>;
|
||
bindings: Required<ResolvedControllerBindingsConfig>;
|
||
reviewBindings: ResolvedControllerReviewBindings;
|
||
fieldGroupingBindings: ResolvedControllerFieldGroupingBindings;
|
||
}
|
||
|
||
export interface ControllerConfig {
|
||
enabled?: boolean;
|
||
preferredGamepadId?: string;
|
||
preferredGamepadLabel?: string;
|
||
smoothScroll?: boolean;
|
||
scrollPixelsPerSecond?: number;
|
||
horizontalJumpPixels?: number;
|
||
stickDeadzone?: number;
|
||
triggerInputMode?: ControllerTriggerInputMode;
|
||
faceButtonLabels?: ControllerFaceButtonLabels;
|
||
triggerDeadzone?: number;
|
||
repeatDelayMs?: number;
|
||
repeatIntervalMs?: number;
|
||
buttonIndices?: ControllerButtonIndicesConfig;
|
||
bindings?: ControllerBindingsConfig;
|
||
reviewBindings?: ControllerReviewBindingsConfig;
|
||
fieldGroupingBindings?: ControllerFieldGroupingBindingsConfig;
|
||
profiles?: Record<string, ControllerProfileConfig>;
|
||
}
|
||
|
||
export type ControllerConfigUpdate = ControllerConfig;
|
||
|
||
export interface ControllerDeviceInfo {
|
||
id: string;
|
||
index: number;
|
||
mapping: string;
|
||
connected: boolean;
|
||
/** Gamepad API timestamp of the pad's last input; the newest is the one being used. */
|
||
timestamp?: number;
|
||
}
|
||
|
||
export interface ControllerButtonSnapshot {
|
||
value: number;
|
||
pressed: boolean;
|
||
touched?: boolean;
|
||
}
|
||
|
||
export interface ControllerRuntimeSnapshot {
|
||
connectedGamepads: ControllerDeviceInfo[];
|
||
activeGamepadId: string | null;
|
||
rawAxes: number[];
|
||
rawButtons: ControllerButtonSnapshot[];
|
||
}
|
||
|
||
export interface MpvSubtitleRenderMetrics {
|
||
subPos: number;
|
||
subFontSize: number;
|
||
subScale: number;
|
||
subMarginY: number;
|
||
subMarginX: number;
|
||
subFont: string;
|
||
subSpacing: number;
|
||
subBold: boolean;
|
||
subItalic: boolean;
|
||
subBorderSize: number;
|
||
subShadowOffset: number;
|
||
subAssOverride: string;
|
||
subScaleByWindow: boolean;
|
||
subUseMargins: boolean;
|
||
osdHeight: number;
|
||
osdDimensions: {
|
||
w: number;
|
||
h: number;
|
||
ml: number;
|
||
mr: number;
|
||
mt: number;
|
||
mb: number;
|
||
} | null;
|
||
}
|
||
|
||
export type OverlayLayer = 'visible';
|
||
|
||
export interface OverlayContentRect {
|
||
x: number;
|
||
y: number;
|
||
width: number;
|
||
height: number;
|
||
}
|
||
|
||
export interface OverlayContentMeasurement {
|
||
layer: OverlayLayer;
|
||
measuredAtMs: number;
|
||
viewport: {
|
||
width: number;
|
||
height: number;
|
||
};
|
||
contentRect: OverlayContentRect | null;
|
||
interactiveRects?: OverlayContentRect[];
|
||
}
|
||
|
||
export interface MecabStatus {
|
||
available: boolean;
|
||
enabled: boolean;
|
||
path: string | null;
|
||
}
|
||
|
||
export interface ClipboardAppendResult {
|
||
ok: boolean;
|
||
message: string;
|
||
}
|
||
|
||
export interface ConfigHotReloadPayload {
|
||
keybindings: Keybinding[];
|
||
sessionBindings: CompiledSessionBinding[];
|
||
sessionBindingWarnings: SessionBindingWarning[];
|
||
subtitleStyle: SubtitleRendererStyleConfig | null;
|
||
subtitleSidebar: ResolvedSubtitleSidebarConfig;
|
||
primarySubMode: PrimarySubMode;
|
||
secondarySubMode: SecondarySubMode;
|
||
}
|
||
|
||
/** Content of the shared plugin panel. `panelId` changes each time a plugin opens a new panel. */
|
||
export interface PluginPanelSnapshot {
|
||
panelId: number;
|
||
title: string;
|
||
markdown: string;
|
||
}
|
||
|
||
export interface SessionActionDispatchRequest {
|
||
actionId: SessionActionId;
|
||
payload?: SessionActionPayload;
|
||
}
|
||
|
||
export type ResolvedControllerConfig = ResolvedConfig['controller'];
|
||
|
||
export interface CharacterDictionaryCandidate {
|
||
id: number;
|
||
title: string;
|
||
episodes: number | null;
|
||
}
|
||
|
||
export interface CharacterDictionarySelectionSnapshot {
|
||
seriesKey: string;
|
||
guessTitle: string | null;
|
||
current: CharacterDictionaryCandidate | null;
|
||
override: CharacterDictionaryCandidate | null;
|
||
candidates: CharacterDictionaryCandidate[];
|
||
}
|
||
|
||
export interface CharacterDictionarySelectionResult {
|
||
ok: boolean;
|
||
seriesKey: string;
|
||
selected: CharacterDictionaryCandidate;
|
||
staleMediaIds: number[];
|
||
}
|
||
|
||
export interface CharacterDictionaryManagerEntry {
|
||
mediaId: number;
|
||
label: string;
|
||
title: string;
|
||
current: boolean;
|
||
}
|
||
|
||
export interface CharacterDictionaryManagerSnapshot {
|
||
entries: CharacterDictionaryManagerEntry[];
|
||
}
|
||
|
||
export type CharacterDictionaryManagerMutationResult =
|
||
| (CharacterDictionaryManagerSnapshot & { ok: true; rebuildRequired?: boolean })
|
||
| { ok: false; message: string; entries: CharacterDictionaryManagerEntry[] };
|
||
|
||
/** Sent with session-help:open; commands only run when a video is loaded in mpv. */
|
||
export interface SessionHelpOpenPayload {
|
||
commandsEnabled: boolean;
|
||
}
|
||
|
||
export interface SessionNumericSelectionStartPayload {
|
||
actionId: Extract<SessionActionId, 'copySubtitleMultiple' | 'mineSentenceMultiple'>;
|
||
timeoutMs: number;
|
||
}
|
||
|
||
export interface ElectronAPI {
|
||
requestSubtitleGenerationOpen: () => Promise<boolean>;
|
||
onSubtitleGenerationOpen: (callback: () => void) => void;
|
||
getSubtitleGenerationStatus: () => Promise<SubtitleGenerationStatus>;
|
||
selectSubtitleGenerationModel: (
|
||
model: SubtitleGenerationModelId,
|
||
) => Promise<SubtitleGenerationStatus>;
|
||
startSubtitleGeneration: () => Promise<SubtitleGenerationResult>;
|
||
downloadSubtitleGenerationModel: () => Promise<SubtitleGenerationResult>;
|
||
downloadSubtitleGenerationVadModel: () => Promise<SubtitleGenerationResult>;
|
||
setSubtitleGenerationVadEnabled: (enabled: boolean) => Promise<SubtitleGenerationStatus>;
|
||
cancelSubtitleGeneration: () => Promise<void>;
|
||
onSubtitleGenerationProgress: (
|
||
callback: (progress: SubtitleGenerationProgress) => void,
|
||
) => () => void;
|
||
getOverlayLayer: () => 'visible' | 'modal' | null;
|
||
getPathForFile: (file: File) => string;
|
||
onSubtitle: (callback: (data: SubtitleData) => void) => void;
|
||
onOverlayPointerRecoveryRequested: (callback: () => void) => void;
|
||
onOverlayNotification: (callback: (payload: OverlayNotificationEventPayload) => void) => void;
|
||
sendOverlayNotificationAction?: (
|
||
notificationId: string,
|
||
actionId: string,
|
||
options?: Pick<OverlayNotificationAction, 'noteId'>,
|
||
) => void;
|
||
onNotificationHistoryToggle: (callback: () => void) => void;
|
||
onSubtitlePosition: (callback: (position: SubtitlePosition | null) => void) => void;
|
||
getCurrentSubtitle: () => Promise<SubtitleData>;
|
||
getCurrentSubtitleRaw: () => Promise<string>;
|
||
getSubtitleSidebarSnapshot: () => Promise<SubtitleSidebarSnapshot>;
|
||
getSubtitleSidebarPlaybackState: () => Promise<SubtitleSidebarPlaybackState>;
|
||
copySubtitleSidebarSelection: (text: string) => Promise<void>;
|
||
getSubtitleSidebarOpen: () => Promise<boolean>;
|
||
getPlaybackPaused: () => Promise<boolean | null>;
|
||
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => void;
|
||
reportOverlayInteractive: (interactive: boolean) => void;
|
||
recordYomitanLookup: (context?: SubtitleMiningContext | null) => void;
|
||
getSubtitlePosition: () => Promise<SubtitlePosition | null>;
|
||
saveSubtitlePosition: (position: SubtitlePosition) => void;
|
||
sendMpvCommand: (command: (string | number)[]) => void;
|
||
getMpvInputBindings: () => Promise<MpvInputBindingsSnapshot>;
|
||
getSessionBindings: () => Promise<CompiledSessionBinding[]>;
|
||
getConfiguredShortcuts: () => Promise<Required<ShortcutsConfig>>;
|
||
dispatchSessionAction: (
|
||
actionId: SessionActionId,
|
||
payload?: SessionActionPayload,
|
||
) => Promise<void>;
|
||
getStatsToggleKey: () => Promise<string>;
|
||
getMarkWatchedKey: () => Promise<string>;
|
||
getOverlayNotificationPosition: () => Promise<OverlayNotificationPosition>;
|
||
markActiveVideoWatched: () => Promise<boolean>;
|
||
getControllerConfig: () => Promise<ResolvedControllerConfig>;
|
||
saveControllerConfig: (update: ControllerConfigUpdate) => Promise<void>;
|
||
/** The overlay reports which controller it is reading (null when none). */
|
||
reportActiveController: (gamepadId: string | null) => void;
|
||
/** Announces a controller status message through the configured notification surfaces. */
|
||
notifyControllerStatus: (message: string) => void;
|
||
/** The controller the overlay last reported, for Alt+C before it sees a pad itself. */
|
||
getActiveControllerId: () => Promise<string | null>;
|
||
getJimakuMediaInfo: () => Promise<JimakuMediaInfo>;
|
||
jimakuSearchEntries: (query: JimakuSearchQuery) => Promise<JimakuApiResponse<JimakuEntry[]>>;
|
||
jimakuListFiles: (query: JimakuFilesQuery) => Promise<JimakuApiResponse<JimakuFileEntry[]>>;
|
||
jimakuDownloadFile: (query: JimakuDownloadQuery) => Promise<JimakuDownloadResult>;
|
||
tsukihimeSearchEntries: (
|
||
query: TsukihimeSearchQuery,
|
||
) => Promise<TsukihimeApiResponse<TsukihimeEntry[]>>;
|
||
tsukihimeListFiles: (
|
||
query: TsukihimeFilesQuery,
|
||
) => Promise<TsukihimeApiResponse<TsukihimeSubtitleFile[]>>;
|
||
tsukihimeDownloadFile: (query: TsukihimeDownloadQuery) => Promise<TsukihimeDownloadResult>;
|
||
tsukihimeGetSecondaryLanguages: () => Promise<string[]>;
|
||
toggleDevTools: () => void;
|
||
toggleStatsOverlay: () => void;
|
||
onSecondarySub: (callback: (text: string) => void) => void;
|
||
onSecondarySubMode: (callback: (mode: SecondarySubMode) => void) => void;
|
||
getSecondarySubMode: () => Promise<SecondarySubMode>;
|
||
getCurrentSecondarySub: () => Promise<string>;
|
||
focusMainWindow: () => Promise<void>;
|
||
activatePlaybackWindowForOverlayInteraction: () => Promise<boolean>;
|
||
getSubtitleStyle: () => Promise<SubtitleRendererStyleConfig | null>;
|
||
onSubtitleSelectionOpen: (callback: () => void) => void;
|
||
getSubtitleSelection: () => Promise<
|
||
import('../shared/subtitle-selection').SubtitleSelectionState
|
||
>;
|
||
applySubtitleSelection: (
|
||
request: import('../shared/subtitle-selection').SubtitleSelectionRequest,
|
||
) => Promise<void>;
|
||
onSubsyncManualOpen: (callback: (payload: SubsyncManualPayload) => void) => void;
|
||
runSubsyncManual: (request: SubsyncManualRunRequest) => Promise<SubsyncResult>;
|
||
onKikuFieldGroupingRequest: (callback: (data: KikuFieldGroupingRequestData) => void) => void;
|
||
onKikuFieldGroupingCancel: (callback: () => void) => void;
|
||
kikuBuildMergePreview: (request: KikuMergePreviewRequest) => Promise<KikuMergePreviewResponse>;
|
||
kikuFieldGroupingRespond: (choice: KikuFieldGroupingChoice) => void;
|
||
getRuntimeOptions: () => Promise<RuntimeOptionState[]>;
|
||
setRuntimeOptionValue: (
|
||
id: RuntimeOptionId,
|
||
value: RuntimeOptionValue,
|
||
) => Promise<RuntimeOptionApplyResult>;
|
||
onRuntimeOptionsChanged: (callback: (options: RuntimeOptionState[]) => void) => void;
|
||
onOpenRuntimeOptions: (callback: () => void) => void;
|
||
onOpenSessionHelp: (callback: (payload: SessionHelpOpenPayload) => void) => void;
|
||
onOpenChangelog: (callback: () => void) => void;
|
||
getChangelogSnapshot: (options?: { refresh?: boolean }) => Promise<ChangelogSnapshot>;
|
||
onOpenPluginPanel: (callback: (snapshot: PluginPanelSnapshot) => void) => void;
|
||
onPluginPanelUpdate: (callback: (snapshot: PluginPanelSnapshot) => void) => void;
|
||
onPluginPanelClose: (callback: (panelId: number) => void) => void;
|
||
onOpenControllerSelect: (callback: () => void) => void;
|
||
onOpenControllerDebug: (callback: () => void) => void;
|
||
onOpenJimaku: (callback: () => void) => void;
|
||
onOpenTsukihime: (callback: () => void) => void;
|
||
onOpenYoutubeTrackPicker: (callback: (payload: YoutubePickerOpenPayload) => void) => void;
|
||
onOpenMediaTimingReview: (callback: (payload: MediaTimingReviewOpenPayload) => void) => void;
|
||
onMediaTimingReviewPreviewEnded: (callback: (reviewId: string) => void) => void;
|
||
onMediaTimingReviewPreviewPosition: (
|
||
callback: (position: MediaTimingReviewPreviewPosition) => void,
|
||
) => void;
|
||
previewMediaTimingReview: (
|
||
request: MediaTimingReviewPreviewRequest,
|
||
) => Promise<MediaTimingReviewActionResult>;
|
||
getMediaTimingReviewFrame: (
|
||
request: MediaTimingReviewFrameRequest,
|
||
) => Promise<MediaTimingReviewFrameResult>;
|
||
getMediaTimingReviewWaveform: (
|
||
request: MediaTimingReviewWaveformRequest,
|
||
) => Promise<MediaTimingReviewWaveformResult>;
|
||
stopMediaTimingReviewPreview: (reviewId: string) => Promise<MediaTimingReviewActionResult>;
|
||
resolveMediaTimingReview: (
|
||
request: MediaTimingReviewResolveRequest,
|
||
) => Promise<MediaTimingReviewActionResult>;
|
||
onOpenPlaylistBrowser: (callback: () => void) => void;
|
||
onOpenCharacterDictionaryManager: (callback: () => void) => void;
|
||
onSubtitleSidebarToggle: (callback: () => void) => void;
|
||
onPrimarySubtitleBarToggle: (callback: () => void) => void;
|
||
onCancelYoutubeTrackPicker: (callback: () => void) => void;
|
||
onSessionNumericSelectionStart: (
|
||
callback: (payload: SessionNumericSelectionStartPayload) => void,
|
||
) => void;
|
||
onKeyboardModeToggleRequested: (callback: () => void) => void;
|
||
onControllerSuspendedChanged: (callback: (suspended: boolean) => void) => void;
|
||
/** Alt+C saved controller settings; reload them with `getControllerConfig`. */
|
||
onControllerConfigChanged: (callback: () => void) => void;
|
||
/** macOS: a button pressed while mpv was in front, which the Gamepad API never saw. */
|
||
onControllerBackgroundPress: (callback: (buttonIndex: number) => void) => void;
|
||
/** mpv's pause property changed, from any source. */
|
||
onPlaybackPauseChanged: (callback: (paused: boolean) => void) => void;
|
||
onLookupWindowToggleRequested: (callback: () => void) => void;
|
||
getPlaylistBrowserSnapshot: () => Promise<PlaylistBrowserSnapshot>;
|
||
appendPlaylistBrowserFile: (path: string) => Promise<PlaylistBrowserMutationResult>;
|
||
playPlaylistBrowserIndex: (index: number) => Promise<PlaylistBrowserMutationResult>;
|
||
removePlaylistBrowserIndex: (index: number) => Promise<PlaylistBrowserMutationResult>;
|
||
movePlaylistBrowserIndex: (
|
||
index: number,
|
||
direction: 1 | -1,
|
||
) => Promise<PlaylistBrowserMutationResult>;
|
||
youtubePickerResolve: (
|
||
request: YoutubePickerResolveRequest,
|
||
) => Promise<YoutubePickerResolveResult>;
|
||
getCharacterDictionarySelection: (
|
||
searchTitle?: string,
|
||
) => Promise<CharacterDictionarySelectionSnapshot>;
|
||
setCharacterDictionarySelection: (
|
||
mediaId: number,
|
||
replaceManagedMediaId?: number,
|
||
mediaTitle?: string,
|
||
) => Promise<CharacterDictionarySelectionResult | CharacterDictionaryManagerMutationResult>;
|
||
getCharacterDictionaryManagerSnapshot: () => Promise<CharacterDictionaryManagerSnapshot>;
|
||
removeCharacterDictionaryManagedEntry: (
|
||
mediaId: number,
|
||
) => Promise<CharacterDictionaryManagerMutationResult>;
|
||
moveCharacterDictionaryManagedEntry: (
|
||
mediaId: number,
|
||
direction: 1 | -1,
|
||
) => Promise<CharacterDictionaryManagerMutationResult>;
|
||
notifyOverlayModalClosed: (
|
||
modal:
|
||
| 'runtime-options'
|
||
| 'subsync'
|
||
| 'subtitle-selection'
|
||
| 'subtitle-generation'
|
||
| 'jimaku'
|
||
| 'tsukihime'
|
||
| 'youtube-track-picker'
|
||
| 'media-timing-review'
|
||
| 'playlist-browser'
|
||
| 'kiku'
|
||
| 'controller-select'
|
||
| 'controller-debug'
|
||
| 'subtitle-sidebar'
|
||
| 'session-help'
|
||
| 'character-dictionary'
|
||
| 'changelog'
|
||
| 'plugin-panel',
|
||
/** Which instance closed, for modals that swap content in place (the plugin panel's id). */
|
||
instanceId?: number,
|
||
) => void;
|
||
notifyOverlayModalOpened: (
|
||
modal:
|
||
| 'runtime-options'
|
||
| 'subsync'
|
||
| 'subtitle-selection'
|
||
| 'subtitle-generation'
|
||
| 'jimaku'
|
||
| 'tsukihime'
|
||
| 'youtube-track-picker'
|
||
| 'media-timing-review'
|
||
| 'playlist-browser'
|
||
| 'kiku'
|
||
| 'controller-select'
|
||
| 'controller-debug'
|
||
| 'subtitle-sidebar'
|
||
| 'session-help'
|
||
| 'character-dictionary'
|
||
| 'changelog'
|
||
| 'plugin-panel',
|
||
) => void;
|
||
reportOverlayContentBounds: (measurement: OverlayContentMeasurement) => void;
|
||
onSessionBindingsChanged: (callback: (bindings: CompiledSessionBinding[]) => void) => void;
|
||
onMpvInputBindingsChanged: (callback: () => void) => void;
|
||
onConfigHotReload: (callback: (payload: ConfigHotReloadPayload) => void) => void;
|
||
}
|
||
|
||
declare global {
|
||
interface Window {
|
||
electronAPI: ElectronAPI;
|
||
}
|
||
}
|