mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-09 16:19:25 -07:00
Add controller support and harden overlay input handling
- Ship v0.6.0 with keyboard-overlay gamepad support, controller modals, and detection indicator - Tighten controller config/IPC validation (integer button indices, reject malformed preference payloads) - Fix blocked-input edge handling and lookup/modal error paths to avoid stale UI state
This commit is contained in:
11
CHANGELOG.md
11
CHANGELOG.md
@@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## v0.6.0 (2026-03-12)
|
||||
|
||||
### Added
|
||||
- Overlay: Added Chrome Gamepad API controller support for keyboard-only overlay mode, including configurable logical bindings for lookup, mining, popup navigation, Yomitan audio, mpv pause, d-pad fallback navigation, and slower smooth popup scrolling.
|
||||
- Overlay: Added `Alt+C` controller selection and `Alt+Shift+C` controller debug modals, with preferred controller persistence and live raw input inspection.
|
||||
- Overlay: Added a transient in-overlay controller-detected indicator when a controller is first found.
|
||||
- Overlay: Fixed stale keyboard-only token highlight cleanup when keyboard-only mode turns off or the Yomitan popup closes.
|
||||
|
||||
### Internal
|
||||
- Config: add an enforced `verify:config-example` gate so checked-in example config artifacts cannot drift silently
|
||||
|
||||
## v0.5.6 (2026-03-10)
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
type: internal
|
||||
area: config
|
||||
|
||||
- add an enforced `verify:config-example` gate so checked-in example config artifacts cannot drift silently
|
||||
@@ -1,7 +0,0 @@
|
||||
type: added
|
||||
area: overlay
|
||||
|
||||
- Added Chrome Gamepad API controller support for keyboard-only overlay mode, including configurable logical bindings for lookup, mining, popup navigation, Yomitan audio, mpv pause, d-pad fallback navigation, and slower smooth popup scrolling.
|
||||
- Added `Alt+C` controller selection and `Alt+Shift+C` controller debug modals, with preferred controller persistence and live raw input inspection.
|
||||
- Added a transient in-overlay controller-detected indicator when a controller is first found.
|
||||
- Fixed stale keyboard-only token highlight cleanup when keyboard-only mode turns off or the Yomitan popup closes.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "subminer",
|
||||
"version": "0.5.6",
|
||||
"version": "0.6.0",
|
||||
"description": "All-in-one sentence mining overlay with AnkiConnect and dictionary integration",
|
||||
"packageManager": "bun@1.3.5",
|
||||
"main": "dist/main-entry.js",
|
||||
|
||||
@@ -1204,6 +1204,37 @@ test('controller positive-number tuning rejects sub-unit values that floor to ze
|
||||
assert.equal(warnings.some((warning) => warning.path === 'controller.repeatIntervalMs'), true);
|
||||
});
|
||||
|
||||
test('controller button index config rejects fractional values', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"controller": {
|
||||
"buttonIndices": {
|
||||
"select": 6.5,
|
||||
"leftStickPress": 9.1
|
||||
}
|
||||
}
|
||||
}`,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
const warnings = service.getWarnings();
|
||||
|
||||
assert.equal(config.controller.buttonIndices.select, DEFAULT_CONFIG.controller.buttonIndices.select);
|
||||
assert.equal(
|
||||
config.controller.buttonIndices.leftStickPress,
|
||||
DEFAULT_CONFIG.controller.buttonIndices.leftStickPress,
|
||||
);
|
||||
assert.equal(warnings.some((warning) => warning.path === 'controller.buttonIndices.select'), true);
|
||||
assert.equal(
|
||||
warnings.some((warning) => warning.path === 'controller.buttonIndices.leftStickPress'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('runtime options registry is centralized', () => {
|
||||
const ids = RUNTIME_OPTION_REGISTRY.map((entry) => entry.id);
|
||||
assert.deepEqual(ids, [
|
||||
|
||||
@@ -209,8 +209,8 @@ export function applyCoreDomainConfig(context: ResolveContext): void {
|
||||
|
||||
for (const key of buttonIndexKeys) {
|
||||
const value = asNumber(src.controller.buttonIndices[key]);
|
||||
if (value !== undefined && value >= 0) {
|
||||
resolved.controller.buttonIndices[key] = Math.floor(value);
|
||||
if (value !== undefined && value >= 0 && Number.isInteger(value)) {
|
||||
resolved.controller.buttonIndices[key] = value;
|
||||
} else if (src.controller.buttonIndices[key] !== undefined) {
|
||||
warn(
|
||||
`controller.buttonIndices.${key}`,
|
||||
|
||||
@@ -467,7 +467,12 @@ test('registerIpcHandlers awaits saveControllerPreference through request-respon
|
||||
const saveHandler = handlers.handle.get(IPC_CHANNELS.command.saveControllerPreference);
|
||||
assert.ok(saveHandler);
|
||||
|
||||
await saveHandler!({}, { preferredGamepadId: 12 });
|
||||
await assert.rejects(
|
||||
async () => {
|
||||
await saveHandler!({}, { preferredGamepadId: 12 });
|
||||
},
|
||||
/Invalid controller preference payload/,
|
||||
);
|
||||
await saveHandler!({}, {
|
||||
preferredGamepadId: 'pad-1',
|
||||
preferredGamepadLabel: 'Pad 1',
|
||||
@@ -480,3 +485,95 @@ test('registerIpcHandlers awaits saveControllerPreference through request-respon
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('registerIpcHandlers rejects malformed controller preference payloads', async () => {
|
||||
const { registrar, handlers } = createFakeIpcRegistrar();
|
||||
registerIpcHandlers(
|
||||
{
|
||||
onOverlayModalClosed: () => {},
|
||||
openYomitanSettings: () => {},
|
||||
quitApp: () => {},
|
||||
toggleDevTools: () => {},
|
||||
getVisibleOverlayVisibility: () => false,
|
||||
toggleVisibleOverlay: () => {},
|
||||
tokenizeCurrentSubtitle: async () => null,
|
||||
getCurrentSubtitleRaw: () => '',
|
||||
getCurrentSubtitleAss: () => '',
|
||||
getPlaybackPaused: () => false,
|
||||
getSubtitlePosition: () => null,
|
||||
getSubtitleStyle: () => null,
|
||||
saveSubtitlePosition: () => {},
|
||||
getMecabStatus: () => ({ available: false, enabled: false, path: null }),
|
||||
setMecabEnabled: () => {},
|
||||
handleMpvCommand: () => {},
|
||||
getKeybindings: () => [],
|
||||
getConfiguredShortcuts: () => ({}),
|
||||
getControllerConfig: () => ({
|
||||
enabled: true,
|
||||
preferredGamepadId: '',
|
||||
preferredGamepadLabel: '',
|
||||
smoothScroll: true,
|
||||
scrollPixelsPerSecond: 960,
|
||||
horizontalJumpPixels: 160,
|
||||
stickDeadzone: 0.2,
|
||||
triggerInputMode: 'auto',
|
||||
triggerDeadzone: 0.5,
|
||||
repeatDelayMs: 220,
|
||||
repeatIntervalMs: 80,
|
||||
buttonIndices: {
|
||||
select: 6,
|
||||
buttonSouth: 0,
|
||||
buttonEast: 1,
|
||||
buttonWest: 2,
|
||||
buttonNorth: 3,
|
||||
leftShoulder: 4,
|
||||
rightShoulder: 5,
|
||||
leftStickPress: 9,
|
||||
rightStickPress: 10,
|
||||
leftTrigger: 6,
|
||||
rightTrigger: 7,
|
||||
},
|
||||
bindings: {
|
||||
toggleLookup: 'buttonSouth',
|
||||
closeLookup: 'buttonEast',
|
||||
toggleKeyboardOnlyMode: 'buttonNorth',
|
||||
mineCard: 'buttonWest',
|
||||
quitMpv: 'select',
|
||||
previousAudio: 'leftShoulder',
|
||||
nextAudio: 'rightShoulder',
|
||||
playCurrentAudio: 'rightTrigger',
|
||||
toggleMpvPause: 'leftTrigger',
|
||||
leftStickHorizontal: 'leftStickX',
|
||||
leftStickVertical: 'leftStickY',
|
||||
rightStickHorizontal: 'rightStickX',
|
||||
rightStickVertical: 'rightStickY',
|
||||
},
|
||||
}),
|
||||
saveControllerPreference: async () => {},
|
||||
getSecondarySubMode: () => 'hover',
|
||||
getCurrentSecondarySub: () => '',
|
||||
focusMainWindow: () => {},
|
||||
runSubsyncManual: async () => ({ ok: true, message: 'ok' }),
|
||||
getAnkiConnectStatus: () => false,
|
||||
getRuntimeOptions: () => [],
|
||||
setRuntimeOption: () => ({ ok: true }),
|
||||
cycleRuntimeOption: () => ({ ok: true }),
|
||||
reportOverlayContentBounds: () => {},
|
||||
getAnilistStatus: () => ({}),
|
||||
clearAnilistToken: () => {},
|
||||
openAnilistSetup: () => {},
|
||||
getAnilistQueueStatus: () => ({}),
|
||||
retryAnilistQueueNow: async () => ({ ok: true, message: 'ok' }),
|
||||
appendClipboardVideoToQueue: () => ({ ok: true, message: 'ok' }),
|
||||
},
|
||||
registrar,
|
||||
);
|
||||
|
||||
const saveHandler = handlers.handle.get(IPC_CHANNELS.command.saveControllerPreference);
|
||||
await assert.rejects(
|
||||
async () => {
|
||||
await saveHandler!({}, { preferredGamepadId: 12 });
|
||||
},
|
||||
/Invalid controller preference payload/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -267,7 +267,9 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
|
||||
|
||||
ipc.handle(IPC_CHANNELS.command.saveControllerPreference, async (_event: unknown, update: unknown) => {
|
||||
const parsedUpdate = parseControllerPreferenceUpdate(update);
|
||||
if (!parsedUpdate) return;
|
||||
if (!parsedUpdate) {
|
||||
throw new Error('Invalid controller preference payload');
|
||||
}
|
||||
await deps.saveControllerPreference(parsedUpdate);
|
||||
});
|
||||
|
||||
|
||||
@@ -208,6 +208,55 @@ test('gamepad controller does not toggle keyboard mode when controller support i
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
|
||||
test('gamepad controller does not treat blocked held inputs as fresh edges when interaction resumes', () => {
|
||||
const calls: string[] = [];
|
||||
const selectionCalls: number[] = [];
|
||||
const buttons = Array.from({ length: 16 }, () => ({ value: 0, pressed: false, touched: false }));
|
||||
buttons[0] = { value: 1, pressed: true, touched: true };
|
||||
let axes = [0.9, 0, 0, 0];
|
||||
let keyboardModeEnabled = true;
|
||||
let interactionBlocked = true;
|
||||
|
||||
const controller = createGamepadController({
|
||||
getGamepads: () => [createGamepad('pad-1', { buttons, axes })],
|
||||
getConfig: () => createControllerConfig(),
|
||||
getKeyboardModeEnabled: () => keyboardModeEnabled,
|
||||
getLookupWindowOpen: () => false,
|
||||
getInteractionBlocked: () => interactionBlocked,
|
||||
toggleKeyboardMode: () => {},
|
||||
toggleLookup: () => calls.push('toggle-lookup'),
|
||||
closeLookup: () => {},
|
||||
moveSelection: (delta) => selectionCalls.push(delta),
|
||||
mineCard: () => {},
|
||||
quitMpv: () => {},
|
||||
previousAudio: () => {},
|
||||
nextAudio: () => {},
|
||||
playCurrentAudio: () => {},
|
||||
toggleMpvPause: () => {},
|
||||
scrollPopup: () => {},
|
||||
jumpPopup: () => {},
|
||||
onState: () => {},
|
||||
});
|
||||
|
||||
controller.poll(0);
|
||||
interactionBlocked = false;
|
||||
controller.poll(100);
|
||||
|
||||
assert.deepEqual(calls, []);
|
||||
assert.deepEqual(selectionCalls, []);
|
||||
|
||||
buttons[0] = { value: 0, pressed: false, touched: false };
|
||||
axes = [0, 0, 0, 0];
|
||||
controller.poll(200);
|
||||
|
||||
buttons[0] = { value: 1, pressed: true, touched: true };
|
||||
axes = [0.9, 0, 0, 0];
|
||||
controller.poll(300);
|
||||
|
||||
assert.deepEqual(calls, ['toggle-lookup']);
|
||||
assert.deepEqual(selectionCalls, [1]);
|
||||
});
|
||||
|
||||
test('gamepad controller maps left stick horizontal movement to token selection repeats', () => {
|
||||
const calls: number[] = [];
|
||||
let axes = [0.9, 0, 0, 0];
|
||||
|
||||
@@ -226,6 +226,24 @@ function resetHeldAction(state: HoldState): void {
|
||||
state.initialFired = false;
|
||||
}
|
||||
|
||||
function syncHeldActionBlocked(
|
||||
state: HoldState,
|
||||
value: number,
|
||||
now: number,
|
||||
activationThreshold: number,
|
||||
): void {
|
||||
if (Math.abs(value) < activationThreshold) {
|
||||
resetHeldAction(state);
|
||||
return;
|
||||
}
|
||||
|
||||
const direction = value > 0 ? 1 : -1;
|
||||
state.repeatStarted = false;
|
||||
state.direction = direction;
|
||||
state.lastFireAt = now;
|
||||
state.initialFired = true;
|
||||
}
|
||||
|
||||
export function createGamepadController(options: GamepadControllerOptions) {
|
||||
let previousButtons = new Map<ControllerButtonBinding, boolean>();
|
||||
let selectionHold = createHoldState();
|
||||
@@ -331,6 +349,58 @@ export function createGamepadController(options: GamepadControllerOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
function syncBlockedInteractionState(
|
||||
activeGamepad: GamepadLike,
|
||||
config: ResolvedControllerConfig,
|
||||
now: number,
|
||||
): void {
|
||||
const buttonBindings = new Set<ControllerButtonBinding>([
|
||||
config.bindings.toggleKeyboardOnlyMode,
|
||||
config.bindings.toggleLookup,
|
||||
config.bindings.closeLookup,
|
||||
config.bindings.mineCard,
|
||||
config.bindings.quitMpv,
|
||||
config.bindings.previousAudio,
|
||||
config.bindings.nextAudio,
|
||||
config.bindings.playCurrentAudio,
|
||||
config.bindings.toggleMpvPause,
|
||||
]);
|
||||
|
||||
for (const binding of buttonBindings) {
|
||||
if (binding === 'none') continue;
|
||||
previousButtons.set(
|
||||
binding,
|
||||
normalizeButtonState(
|
||||
activeGamepad,
|
||||
config,
|
||||
binding,
|
||||
config.triggerInputMode,
|
||||
config.triggerDeadzone,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const selectionValue = (() => {
|
||||
const axisValue = resolveAxisValue(activeGamepad, config.bindings.leftStickHorizontal);
|
||||
if (Math.abs(axisValue) >= Math.max(config.stickDeadzone, 0.55)) {
|
||||
return axisValue;
|
||||
}
|
||||
return resolveDpadHorizontalValue(activeGamepad, config.triggerDeadzone);
|
||||
})();
|
||||
syncHeldActionBlocked(selectionHold, selectionValue, now, Math.max(config.stickDeadzone, 0.55));
|
||||
|
||||
if (options.getLookupWindowOpen()) {
|
||||
syncHeldActionBlocked(
|
||||
jumpHold,
|
||||
resolveAxisValue(activeGamepad, config.bindings.rightStickVertical),
|
||||
now,
|
||||
Math.max(config.stickDeadzone, 0.55),
|
||||
);
|
||||
} else {
|
||||
resetHeldAction(jumpHold);
|
||||
}
|
||||
}
|
||||
|
||||
function poll(now: number): void {
|
||||
const elapsedMs = lastPollAt === null ? 0 : Math.max(now - lastPollAt, 0);
|
||||
lastPollAt = now;
|
||||
@@ -365,6 +435,7 @@ export function createGamepadController(options: GamepadControllerOptions) {
|
||||
);
|
||||
}
|
||||
if (!interactionAllowed) {
|
||||
syncBlockedInteractionState(activeGamepad, config, now);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -744,6 +744,34 @@ test('keyboard mode: closing lookup clears yomitan active text source so same to
|
||||
}
|
||||
});
|
||||
|
||||
test('keyboard mode: lookup toggle closes popup when DOM visibility is the source of truth', async () => {
|
||||
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||
|
||||
try {
|
||||
await handlers.setupMpvInputForwarding();
|
||||
handlers.handleKeyboardModeToggleRequested();
|
||||
|
||||
ctx.state.keyboardSelectedWordIndex = 1;
|
||||
handlers.syncKeyboardTokenSelection();
|
||||
ctx.state.yomitanPopupVisible = false;
|
||||
testGlobals.setPopupVisible(true);
|
||||
|
||||
handlers.handleLookupWindowToggleRequested();
|
||||
await wait(0);
|
||||
|
||||
const closeCommands = testGlobals.commandEvents.filter(
|
||||
(event) => event.type === 'setVisible' || event.type === 'clearActiveTextSource',
|
||||
);
|
||||
assert.deepEqual(closeCommands.slice(-2), [
|
||||
{ type: 'setVisible', visible: false },
|
||||
{ type: 'clearActiveTextSource' },
|
||||
]);
|
||||
} finally {
|
||||
ctx.state.keyboardDrivenModeEnabled = false;
|
||||
testGlobals.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('keyboard mode: moving right beyond end jumps next subtitle and resets selector to start', async () => {
|
||||
const { ctx, handlers, testGlobals, setWordCount } = createKeyboardHandlerHarness();
|
||||
|
||||
|
||||
@@ -435,15 +435,8 @@ export function createKeyboardHandlers(
|
||||
}
|
||||
|
||||
function handleLookupWindowToggleRequested(): void {
|
||||
if (ctx.state.yomitanPopupVisible) {
|
||||
dispatchYomitanPopupVisibility(false);
|
||||
dispatchYomitanFrontendClearActiveTextSource();
|
||||
clearNativeSubtitleSelection();
|
||||
if (ctx.state.keyboardDrivenModeEnabled) {
|
||||
queueMicrotask(() => {
|
||||
restoreOverlayKeyboardFocus();
|
||||
});
|
||||
}
|
||||
if (ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document)) {
|
||||
closeLookupWindow();
|
||||
return;
|
||||
}
|
||||
triggerLookupForSelectedWord();
|
||||
|
||||
@@ -496,6 +496,120 @@ test('controller select modal preserves saved status across polling updates', as
|
||||
}
|
||||
});
|
||||
|
||||
test('controller select modal surfaces save errors without mutating saved preference', async () => {
|
||||
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
|
||||
const previousWindow = globals.window;
|
||||
const previousDocument = globals.document;
|
||||
|
||||
Object.defineProperty(globalThis, 'window', {
|
||||
configurable: true,
|
||||
value: {
|
||||
focus: () => {},
|
||||
electronAPI: {
|
||||
saveControllerPreference: async () => {
|
||||
throw new Error('disk write failed');
|
||||
},
|
||||
notifyOverlayModalClosed: () => {},
|
||||
},
|
||||
},
|
||||
});
|
||||
Object.defineProperty(globalThis, 'document', {
|
||||
configurable: true,
|
||||
value: {
|
||||
createElement: () => ({
|
||||
className: '',
|
||||
textContent: '',
|
||||
classList: createClassList(),
|
||||
appendChild: () => {},
|
||||
addEventListener: () => {},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const state = createRendererState();
|
||||
state.controllerConfig = {
|
||||
enabled: true,
|
||||
preferredGamepadId: 'pad-1',
|
||||
preferredGamepadLabel: 'pad-1',
|
||||
smoothScroll: true,
|
||||
scrollPixelsPerSecond: 960,
|
||||
horizontalJumpPixels: 160,
|
||||
stickDeadzone: 0.2,
|
||||
triggerInputMode: 'auto',
|
||||
triggerDeadzone: 0.5,
|
||||
repeatDelayMs: 220,
|
||||
repeatIntervalMs: 80,
|
||||
buttonIndices: {
|
||||
select: 6,
|
||||
buttonSouth: 0,
|
||||
buttonEast: 1,
|
||||
buttonWest: 2,
|
||||
buttonNorth: 3,
|
||||
leftShoulder: 4,
|
||||
rightShoulder: 5,
|
||||
leftStickPress: 9,
|
||||
rightStickPress: 10,
|
||||
leftTrigger: 6,
|
||||
rightTrigger: 7,
|
||||
},
|
||||
bindings: {
|
||||
toggleLookup: 'buttonSouth',
|
||||
closeLookup: 'buttonEast',
|
||||
toggleKeyboardOnlyMode: 'buttonNorth',
|
||||
mineCard: 'buttonWest',
|
||||
quitMpv: 'select',
|
||||
previousAudio: 'none',
|
||||
nextAudio: 'rightShoulder',
|
||||
playCurrentAudio: 'leftShoulder',
|
||||
toggleMpvPause: 'leftStickPress',
|
||||
leftStickHorizontal: 'leftStickX',
|
||||
leftStickVertical: 'leftStickY',
|
||||
rightStickHorizontal: 'rightStickX',
|
||||
rightStickVertical: 'rightStickY',
|
||||
},
|
||||
};
|
||||
state.connectedGamepads = [{ id: 'pad-2', index: 1, mapping: 'standard', connected: true }];
|
||||
state.activeGamepadId = 'pad-2';
|
||||
|
||||
const ctx = {
|
||||
dom: {
|
||||
overlay: { classList: createClassList(), focus: () => {} },
|
||||
controllerSelectModal: {
|
||||
classList: createClassList(['hidden']),
|
||||
setAttribute: () => {},
|
||||
},
|
||||
controllerSelectClose: { addEventListener: () => {} },
|
||||
controllerSelectHint: { textContent: '' },
|
||||
controllerSelectStatus: { textContent: '', classList: createClassList() },
|
||||
controllerSelectList: {
|
||||
innerHTML: '',
|
||||
appendChild: () => {},
|
||||
},
|
||||
controllerSelectSave: { addEventListener: () => {} },
|
||||
},
|
||||
state,
|
||||
};
|
||||
|
||||
const modal = createControllerSelectModal(ctx as never, {
|
||||
modalStateReader: { isAnyModalOpen: () => false },
|
||||
syncSettingsModalSubtitleSuppression: () => {},
|
||||
});
|
||||
|
||||
modal.openControllerSelectModal();
|
||||
await modal.handleControllerSelectKeydown({
|
||||
key: 'Enter',
|
||||
preventDefault: () => {},
|
||||
} as KeyboardEvent);
|
||||
|
||||
assert.match(ctx.dom.controllerSelectStatus.textContent, /Failed to save preferred controller/);
|
||||
assert.equal(state.controllerConfig.preferredGamepadId, 'pad-1');
|
||||
} finally {
|
||||
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
||||
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
|
||||
}
|
||||
});
|
||||
|
||||
test('controller select modal does not rerender unchanged device snapshots every poll', () => {
|
||||
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
|
||||
const previousWindow = globals.window;
|
||||
|
||||
@@ -154,10 +154,16 @@ export function createControllerSelectModal(
|
||||
return;
|
||||
}
|
||||
|
||||
await window.electronAPI.saveControllerPreference({
|
||||
preferredGamepadId: selected.id,
|
||||
preferredGamepadLabel: selected.id,
|
||||
});
|
||||
try {
|
||||
await window.electronAPI.saveControllerPreference({
|
||||
preferredGamepadId: selected.id,
|
||||
preferredGamepadLabel: selected.id,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setStatus(`Failed to save preferred controller: ${message}`, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.state.controllerConfig) {
|
||||
ctx.state.controllerConfig.preferredGamepadId = selected.id;
|
||||
|
||||
2
vendor/subminer-yomitan
vendored
2
vendor/subminer-yomitan
vendored
Submodule vendor/subminer-yomitan updated: da2ba5ceb1...994ab578ac
Reference in New Issue
Block a user