mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-11-10 04:46:07 -08:00
Controller: subtitle line actions, Button Style, keyboard-only mode pause, Alt+C fixes (#316)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
6d0f37ac21
commit
b99409bbcd
60 files changed
+1900
-219
No files matched your search
@@ -6,6 +6,8 @@ import {
|
||||
controllerReviewInputSourceKey,
|
||||
formatControllerReviewBinding,
|
||||
formatControllerReviewInput,
|
||||
getControllerFaceButtonName,
|
||||
isControllerFaceButtonLabels,
|
||||
parseControllerReviewBinding,
|
||||
resolveControllerReviewInput,
|
||||
type ControllerReviewInputContext,
|
||||
@@ -148,3 +150,55 @@ test('Alt+C updates accept field grouping bindings, checked against that dialog'
|
||||
);
|
||||
assert.equal(parseControllerConfigUpdate({ reviewBindings: { back: { kind: 'none' } } }), null);
|
||||
});
|
||||
|
||||
test('PlayStation pads print symbols at the Xbox positions', () => {
|
||||
const playstation = { ...standardXbox, faceButtonLabels: 'playstation' as const };
|
||||
const name = (input: Parameters<typeof formatControllerReviewInput>[0]) =>
|
||||
formatControllerReviewInput(input, playstation);
|
||||
|
||||
// Same physical buttons as Xbox: A is the bottom button, shown as ╳.
|
||||
assert.equal(
|
||||
controllerReviewInputSourceKey(
|
||||
resolveControllerReviewInput({ kind: 'face', letter: 'a' }, playstation),
|
||||
),
|
||||
'button:0',
|
||||
);
|
||||
assert.deepEqual(
|
||||
(['a', 'b', 'x', 'y'] as const).map((letter) => name({ kind: 'face', letter })),
|
||||
['╳', '○', '□', '△'],
|
||||
);
|
||||
assert.equal(name({ kind: 'button', buttonIndex: 3 }), '△');
|
||||
assert.equal(
|
||||
formatControllerReviewBinding(
|
||||
{
|
||||
kind: 'combo',
|
||||
press: { kind: 'face', letter: 'a' },
|
||||
hold: { kind: 'button', buttonIndex: 4 },
|
||||
},
|
||||
playstation,
|
||||
),
|
||||
'L1 + ╳',
|
||||
);
|
||||
assert.equal(getControllerFaceButtonName(1, 'nintendo'), 'A');
|
||||
assert.equal(getControllerFaceButtonName(4, 'playstation'), null);
|
||||
assert.equal(isControllerFaceButtonLabels('playstation'), true);
|
||||
assert.equal(isControllerFaceButtonLabels('sega'), false);
|
||||
});
|
||||
|
||||
test('Alt+C updates accept a button style globally and per controller', () => {
|
||||
const plain = (value: unknown) => JSON.parse(JSON.stringify(value));
|
||||
assert.deepEqual(
|
||||
plain(
|
||||
parseControllerConfigUpdate({
|
||||
faceButtonLabels: 'xbox',
|
||||
profiles: { 'pad-1': { faceButtonLabels: 'playstation' } },
|
||||
}),
|
||||
),
|
||||
{ faceButtonLabels: 'xbox', profiles: { 'pad-1': { faceButtonLabels: 'playstation' } } },
|
||||
);
|
||||
assert.equal(parseControllerConfigUpdate({ faceButtonLabels: 'sega' }), null);
|
||||
assert.equal(
|
||||
parseControllerConfigUpdate({ profiles: { 'pad-1': { faceButtonLabels: 1 } } }),
|
||||
null,
|
||||
);
|
||||
});
|
||||
@@ -246,15 +246,35 @@ export type ControllerReviewInputSource =
|
||||
|
||||
type FacePosition = 'south' | 'east' | 'west' | 'north';
|
||||
|
||||
/** Xbox (and PlayStation) pads put A at the bottom; Nintendo-style pads put it on the right. */
|
||||
/**
|
||||
* Where each layout letter sits. Xbox pads put A at the bottom, Nintendo-style pads on the
|
||||
* right. PlayStation pads follow the Xbox layout, so A is ╳, B ○, X □ and Y △.
|
||||
*/
|
||||
const FACE_POSITIONS: Record<
|
||||
ControllerFaceButtonLabels,
|
||||
Record<ControllerFaceLetter, FacePosition>
|
||||
> = {
|
||||
xbox: { a: 'south', b: 'east', x: 'west', y: 'north' },
|
||||
nintendo: { a: 'east', b: 'south', x: 'north', y: 'west' },
|
||||
playstation: { a: 'south', b: 'east', x: 'west', y: 'north' },
|
||||
};
|
||||
|
||||
/** Every face button style, in the order Alt+C offers them. */
|
||||
export const CONTROLLER_FACE_BUTTON_STYLES: readonly {
|
||||
id: ControllerFaceButtonLabels;
|
||||
name: string;
|
||||
/** Tells the layouts apart, since 8BitDo and other pads come in more than one. */
|
||||
hint: string;
|
||||
}[] = [
|
||||
{ id: 'xbox', name: 'Xbox', hint: 'A at the bottom, B on the right' },
|
||||
{ id: 'nintendo', name: 'Nintendo', hint: 'B at the bottom, A on the right' },
|
||||
{ id: 'playstation', name: 'PlayStation', hint: '\u2573 at the bottom, \u25cb on the right' },
|
||||
];
|
||||
|
||||
export function isControllerFaceButtonLabels(value: unknown): value is ControllerFaceButtonLabels {
|
||||
return CONTROLLER_FACE_BUTTON_STYLES.some((style) => style.id === value);
|
||||
}
|
||||
|
||||
const POSITION_INDEX_KEYS = {
|
||||
south: 'buttonSouth',
|
||||
east: 'buttonEast',
|
||||
@@ -359,11 +379,6 @@ const DPAD_GLYPHS: Record<ControllerDpadDirection, string> = {
|
||||
down: '▼',
|
||||
};
|
||||
|
||||
const LETTER_AT_POSITION: Record<ControllerFaceButtonLabels, Record<FacePosition, string>> = {
|
||||
xbox: { south: 'A', east: 'B', west: 'X', north: 'Y' },
|
||||
nintendo: { south: 'B', east: 'A', west: 'Y', north: 'X' },
|
||||
};
|
||||
|
||||
const FACE_POSITION_BY_INDEX: Record<number, FacePosition> = {
|
||||
0: 'south',
|
||||
1: 'east',
|
||||
@@ -371,6 +386,32 @@ const FACE_POSITION_BY_INDEX: Record<number, FacePosition> = {
|
||||
3: 'north',
|
||||
};
|
||||
|
||||
/** What is printed at each position, per style. */
|
||||
const LETTER_AT_POSITION: Record<ControllerFaceButtonLabels, Record<FacePosition, string>> = {
|
||||
xbox: { south: 'A', east: 'B', west: 'X', north: 'Y' },
|
||||
nintendo: { south: 'B', east: 'A', west: 'Y', north: 'X' },
|
||||
playstation: { south: '\u2573', east: '\u25cb', west: '\u25a1', north: '\u25b3' },
|
||||
};
|
||||
|
||||
/** The printed name of the face button at index 0-3 (south, east, west, north), if any. */
|
||||
export function getControllerFaceButtonName(
|
||||
buttonIndex: number,
|
||||
faceButtonLabels: ControllerFaceButtonLabels,
|
||||
): string | null {
|
||||
const position = FACE_POSITION_BY_INDEX[buttonIndex];
|
||||
if (!position) return null;
|
||||
return (LETTER_AT_POSITION[faceButtonLabels] ?? LETTER_AT_POSITION.xbox)[position];
|
||||
}
|
||||
|
||||
/** How a layout letter is printed on this style, e.g. `a` is `A` on Xbox and `╳` on PlayStation. */
|
||||
function formatFaceLetter(
|
||||
letter: ControllerFaceLetter,
|
||||
faceButtonLabels: ControllerFaceButtonLabels,
|
||||
): string {
|
||||
const positions = FACE_POSITIONS[faceButtonLabels] ?? FACE_POSITIONS.xbox;
|
||||
return (LETTER_AT_POSITION[faceButtonLabels] ?? LETTER_AT_POSITION.xbox)[positions[letter]];
|
||||
}
|
||||
|
||||
const STANDARD_BUTTON_NAMES: Record<number, string> = {
|
||||
4: 'L1',
|
||||
5: 'R1',
|
||||
@@ -420,10 +461,8 @@ function formatSource(
|
||||
}
|
||||
const standard = isStandardMapping(context.mapping);
|
||||
if (source.kind === 'button') {
|
||||
const position = FACE_POSITION_BY_INDEX[source.buttonIndex];
|
||||
if (position) {
|
||||
return (LETTER_AT_POSITION[context.faceButtonLabels] ?? LETTER_AT_POSITION.xbox)[position];
|
||||
}
|
||||
const faceName = getControllerFaceButtonName(source.buttonIndex, context.faceButtonLabels);
|
||||
if (faceName) return faceName;
|
||||
const names = standard ? STANDARD_BUTTON_NAMES : RAW_BUTTON_NAMES;
|
||||
return names[source.buttonIndex] ?? `Button ${source.buttonIndex}`;
|
||||
}
|
||||
@@ -448,7 +487,7 @@ export function formatControllerReviewInput(
|
||||
options: { dpadPrefix?: boolean } = {},
|
||||
): string {
|
||||
// Letters are shown as printed, whatever position they resolve to.
|
||||
if (input.kind === 'face') return input.letter.toUpperCase();
|
||||
if (input.kind === 'face') return formatFaceLetter(input.letter, context.faceButtonLabels);
|
||||
return formatSource(
|
||||
resolveControllerReviewInput(input, context),
|
||||
context,
|
||||
|
||||
@@ -34,6 +34,7 @@ export const IPC_CHANNELS = {
|
||||
statsNativeDialogOpened: 'stats:native-dialog-opened',
|
||||
statsNativeDialogClosed: 'stats:native-dialog-closed',
|
||||
saveControllerConfig: 'save-controller-config',
|
||||
reportActiveController: 'controller:report-active',
|
||||
mpvCommand: 'mpv-command',
|
||||
refreshKnownWords: 'anki:refresh-known-words',
|
||||
kikuFieldGroupingRespond: 'kiku:field-grouping-respond',
|
||||
@@ -72,6 +73,7 @@ export const IPC_CHANNELS = {
|
||||
getMarkWatchedKey: 'get-mark-watched-key',
|
||||
getOverlayNotificationPosition: 'get-overlay-notification-position',
|
||||
getControllerConfig: 'get-controller-config',
|
||||
getActiveControllerId: 'controller:get-active',
|
||||
getSecondarySubMode: 'get-secondary-sub-mode',
|
||||
getCurrentSecondarySub: 'get-current-secondary-sub',
|
||||
youtubePickerResolve: 'youtube:picker-resolve',
|
||||
@@ -159,6 +161,8 @@ export const IPC_CHANNELS = {
|
||||
sessionNumericSelectionStart: 'session:numeric-selection-start',
|
||||
keyboardModeToggleRequested: 'keyboard-mode-toggle:requested',
|
||||
controllerSuspendedChanged: 'controller:suspended-changed',
|
||||
controllerConfigChanged: 'controller:config-changed',
|
||||
playbackPauseChanged: 'mpv:pause-changed',
|
||||
lookupWindowToggleRequested: 'lookup-window-toggle:requested',
|
||||
sessionHelpOpen: 'session-help:open',
|
||||
changelogOpen: 'changelog:open',
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { RuntimeOptionId, RuntimeOptionValue } from '../../types/runtime-op
|
||||
import type { SessionActionId, SessionActionPayload } from '../../types/session-bindings';
|
||||
import type { SubtitlePosition } from '../../types/subtitle';
|
||||
import { OVERLAY_HOSTED_MODALS, type OverlayHostedModal } from './contracts';
|
||||
import { isControllerFaceButtonLabels } from '../controller-review';
|
||||
import { CONTROLLER_SCREENS, parseControllerScreenBindingsPatch } from '../controller-screens';
|
||||
|
||||
const RESERVED_CONTROLLER_PROFILE_IDS = new Set(['__proto__', 'constructor', 'prototype']);
|
||||
@@ -221,6 +222,10 @@ function parseControllerBindings(value: unknown): ControllerConfigUpdate['bindin
|
||||
'holdSecondarySub',
|
||||
'previousEntry',
|
||||
'nextEntry',
|
||||
'previousSubtitle',
|
||||
'nextSubtitle',
|
||||
'replaySubtitle',
|
||||
'playNextSubtitle',
|
||||
] as const;
|
||||
for (const key of discreteKeys) {
|
||||
if (value[key] === undefined) continue;
|
||||
@@ -273,6 +278,10 @@ export function parseControllerConfigUpdate(value: unknown): ControllerConfigUpd
|
||||
if (typeof value.preferredGamepadLabel !== 'string') return null;
|
||||
update.preferredGamepadLabel = value.preferredGamepadLabel;
|
||||
}
|
||||
if (value.faceButtonLabels !== undefined) {
|
||||
if (!isControllerFaceButtonLabels(value.faceButtonLabels)) return null;
|
||||
update.faceButtonLabels = value.faceButtonLabels;
|
||||
}
|
||||
if (value.buttonIndices !== undefined) {
|
||||
const parsed = parseControllerButtonIndices(value.buttonIndices);
|
||||
if (!parsed) return null;
|
||||
@@ -297,6 +306,10 @@ export function parseControllerConfigUpdate(value: unknown): ControllerConfigUpd
|
||||
if (typeof rawProfile.label !== 'string') return null;
|
||||
profile.label = rawProfile.label;
|
||||
}
|
||||
if (rawProfile.faceButtonLabels !== undefined) {
|
||||
if (!isControllerFaceButtonLabels(rawProfile.faceButtonLabels)) return null;
|
||||
profile.faceButtonLabels = rawProfile.faceButtonLabels;
|
||||
}
|
||||
if (rawProfile.buttonIndices !== undefined) {
|
||||
const parsed = parseControllerButtonIndices(rawProfile.buttonIndices);
|
||||
if (!parsed) return null;
|
||||
|
||||
Reference in new issue
Block a user