feat(controller): add inline remap modal with descriptor-based bindings (#21)

This commit is contained in:
2026-03-15 15:55:45 -07:00
committed by GitHub
parent 9eed37420e
commit 478869ff28
38 changed files with 3136 additions and 1431 deletions

View File

@@ -19,6 +19,7 @@ export const IPC_CHANNELS = {
toggleDevTools: 'toggle-dev-tools',
toggleOverlay: 'toggle-overlay',
saveSubtitlePosition: 'save-subtitle-position',
saveControllerConfig: 'save-controller-config',
saveControllerPreference: 'save-controller-preference',
setMecabEnabled: 'set-mecab-enabled',
mpvCommand: 'mpv-command',

View File

@@ -1,4 +1,5 @@
import type {
ControllerConfigUpdate,
ControllerPreferenceUpdate,
JimakuDownloadQuery,
JimakuFilesQuery,
@@ -59,6 +60,99 @@ export function parseControllerPreferenceUpdate(value: unknown): ControllerPrefe
};
}
function parseDiscreteBinding(value: unknown) {
if (!isObject(value) || typeof value.kind !== 'string') return null;
if (value.kind === 'none') {
return { kind: 'none' };
}
if (value.kind === 'button') {
if (!isInteger(value.buttonIndex) || value.buttonIndex < 0) return null;
return { kind: 'button', buttonIndex: value.buttonIndex };
}
if (value.kind === 'axis') {
if (!isInteger(value.axisIndex) || value.axisIndex < 0) return null;
if (value.direction !== 'negative' && value.direction !== 'positive') return null;
return { kind: 'axis', axisIndex: value.axisIndex, direction: value.direction };
}
return null;
}
function parseAxisBinding(value: unknown) {
if (isObject(value) && value.kind === 'none') {
return { kind: 'none' };
}
if (!isObject(value) || value.kind !== 'axis') return null;
if (!isInteger(value.axisIndex) || value.axisIndex < 0) return null;
if (
value.dpadFallback !== undefined &&
value.dpadFallback !== 'none' &&
value.dpadFallback !== 'horizontal' &&
value.dpadFallback !== 'vertical'
) {
return null;
}
return {
kind: 'axis',
axisIndex: value.axisIndex,
...(value.dpadFallback === undefined ? {} : { dpadFallback: value.dpadFallback }),
};
}
export function parseControllerConfigUpdate(value: unknown): ControllerConfigUpdate | null {
if (!isObject(value)) return null;
const update: ControllerConfigUpdate = {};
if (value.enabled !== undefined) {
if (typeof value.enabled !== 'boolean') return null;
update.enabled = value.enabled;
}
if (value.preferredGamepadId !== undefined) {
if (typeof value.preferredGamepadId !== 'string') return null;
update.preferredGamepadId = value.preferredGamepadId;
}
if (value.preferredGamepadLabel !== undefined) {
if (typeof value.preferredGamepadLabel !== 'string') return null;
update.preferredGamepadLabel = value.preferredGamepadLabel;
}
if (value.bindings !== undefined) {
if (!isObject(value.bindings)) return null;
const bindings: NonNullable<ControllerConfigUpdate['bindings']> = {};
const discreteKeys = [
'toggleLookup',
'closeLookup',
'toggleKeyboardOnlyMode',
'mineCard',
'quitMpv',
'previousAudio',
'nextAudio',
'playCurrentAudio',
'toggleMpvPause',
] as const;
for (const key of discreteKeys) {
if (value.bindings[key] === undefined) continue;
const parsed = parseDiscreteBinding(value.bindings[key]);
if (!parsed) return null;
bindings[key] = parsed as NonNullable<ControllerConfigUpdate['bindings']>[typeof key];
}
const axisKeys = [
'leftStickHorizontal',
'leftStickVertical',
'rightStickHorizontal',
'rightStickVertical',
] as const;
for (const key of axisKeys) {
if (value.bindings[key] === undefined) continue;
const parsed = parseAxisBinding(value.bindings[key]);
if (!parsed) return null;
bindings[key] = parsed as NonNullable<ControllerConfigUpdate['bindings']>[typeof key];
}
update.bindings = bindings;
}
return update;
}
export function parseSubsyncManualRunRequest(value: unknown): SubsyncManualRunRequest | null {
if (!isObject(value)) return null;
const { engine, sourceTrackId } = value;