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
@@ -2216,6 +2216,34 @@ test('parses controller face button labels and warns on unknown styles', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('controller profiles inherit the button style unless they set their own', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'config.jsonc'),
|
||||
JSON.stringify({
|
||||
controller: {
|
||||
faceButtonLabels: 'nintendo',
|
||||
profiles: {
|
||||
'pad-ps': { faceButtonLabels: 'playstation' },
|
||||
'pad-plain': { label: 'Plain' },
|
||||
'pad-bad': { faceButtonLabels: 'sega' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
'utf-8',
|
||||
);
|
||||
const service = new ConfigService(dir);
|
||||
const { profiles } = service.getConfig().controller;
|
||||
|
||||
assert.equal(profiles['pad-ps']!.faceButtonLabels, 'playstation');
|
||||
assert.equal(profiles['pad-plain']!.faceButtonLabels, 'nintendo');
|
||||
assert.equal(profiles['pad-bad']!.faceButtonLabels, 'nintendo');
|
||||
assert.deepEqual(
|
||||
service.getWarnings().map((warning) => warning.path),
|
||||
['controller.profiles.pad-bad.faceButtonLabels'],
|
||||
);
|
||||
});
|
||||
|
||||
test('parses controller review bindings globally and per profile, and warns on bad entries', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
|
||||
@@ -85,6 +85,10 @@ export const CORE_DEFAULT_CONFIG: Pick<
|
||||
holdSecondarySub: { kind: 'none' },
|
||||
previousEntry: { kind: 'none' },
|
||||
nextEntry: { kind: 'none' },
|
||||
previousSubtitle: { kind: 'none' },
|
||||
nextSubtitle: { kind: 'none' },
|
||||
replaySubtitle: { kind: 'none' },
|
||||
playNextSubtitle: { kind: 'none' },
|
||||
leftStickHorizontal: { kind: 'axis', axisIndex: 0, dpadFallback: 'horizontal' },
|
||||
leftStickVertical: { kind: 'axis', axisIndex: 1, dpadFallback: 'vertical' },
|
||||
rightStickHorizontal: { kind: 'axis', axisIndex: 3, dpadFallback: 'none' },
|
||||
|
||||
@@ -8,6 +8,7 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle' | 'su
|
||||
preserveLineBreaks: false,
|
||||
autoPauseVideoOnHover: true,
|
||||
autoPauseVideoOnYomitanPopup: true,
|
||||
autoPauseVideoOnKeyboardMode: false,
|
||||
primaryVisibleOnYomitanPopup: true,
|
||||
hoverTokenColor: '#f4dbd6',
|
||||
hoverTokenBackgroundColor: 'transparent',
|
||||
|
||||
@@ -61,7 +61,8 @@ export function buildCoreConfigOptionRegistry(
|
||||
{
|
||||
id: 'toggleMpvPause',
|
||||
defaultValue: defaultConfig.controller.bindings.toggleMpvPause,
|
||||
description: 'Controller binding descriptor for toggling mpv play/pause.',
|
||||
description:
|
||||
'Controller binding descriptor for toggling mpv play/pause. Works with or without keyboard-only mode.',
|
||||
},
|
||||
{
|
||||
id: 'holdSecondarySub',
|
||||
@@ -81,6 +82,30 @@ export function buildCoreConfigOptionRegistry(
|
||||
description:
|
||||
'Controller binding descriptor that selects the next word entry in the open Yomitan popup. Mine Card mines the selected entry.',
|
||||
},
|
||||
{
|
||||
id: 'previousSubtitle',
|
||||
defaultValue: defaultConfig.controller.bindings.previousSubtitle,
|
||||
description:
|
||||
'Controller binding descriptor that jumps to the previous subtitle line, like Shift+H. Works with or without keyboard-only mode.',
|
||||
},
|
||||
{
|
||||
id: 'nextSubtitle',
|
||||
defaultValue: defaultConfig.controller.bindings.nextSubtitle,
|
||||
description:
|
||||
'Controller binding descriptor that jumps to the next subtitle line, like Shift+L. Works with or without keyboard-only mode.',
|
||||
},
|
||||
{
|
||||
id: 'replaySubtitle',
|
||||
defaultValue: defaultConfig.controller.bindings.replaySubtitle,
|
||||
description:
|
||||
'Controller binding descriptor that replays the current subtitle line, then pauses, like Ctrl+Shift+H. Works with or without keyboard-only mode.',
|
||||
},
|
||||
{
|
||||
id: 'playNextSubtitle',
|
||||
defaultValue: defaultConfig.controller.bindings.playNextSubtitle,
|
||||
description:
|
||||
'Controller binding descriptor that plays the next subtitle line, then pauses, like Ctrl+Shift+L. Works with or without keyboard-only mode.',
|
||||
},
|
||||
] as const;
|
||||
|
||||
const axisBindings = [
|
||||
@@ -235,10 +260,10 @@ export function buildCoreConfigOptionRegistry(
|
||||
{
|
||||
path: 'controller.faceButtonLabels',
|
||||
kind: 'enum',
|
||||
enumValues: ['xbox', 'nintendo'],
|
||||
enumValues: ['xbox', 'nintendo', 'playstation'],
|
||||
defaultValue: defaultConfig.controller.faceButtonLabels,
|
||||
description:
|
||||
"Letters printed on your controller's face buttons: xbox (A at the bottom; also PlayStation) or nintendo (A on the right, B at the bottom; Switch-style and many 8BitDo pads). Used by fixed controller layouts and button names in Alt+C.",
|
||||
'Face button style of your controller: xbox (A at the bottom), nintendo (A on the right, B at the bottom; Switch-style and many 8BitDo pads) or playstation (╳ ○ □ △, laid out like xbox). Places the A/B/X/Y of the dialog layouts and names buttons in Alt+C and the dialog hints. Alt+C sets it per controller (controller.profiles.<id>.faceButtonLabels); this is the default for the others.',
|
||||
},
|
||||
{
|
||||
path: 'controller.triggerDeadzone',
|
||||
|
||||
@@ -100,6 +100,13 @@ export function buildSubtitleConfigOptionRegistry(
|
||||
description:
|
||||
'Automatically pause mpv playback while Yomitan popup is open, then resume when popup closes.',
|
||||
},
|
||||
{
|
||||
path: 'subtitleStyle.autoPauseVideoOnKeyboardMode',
|
||||
kind: 'boolean',
|
||||
defaultValue: defaultConfig.subtitleStyle.autoPauseVideoOnKeyboardMode,
|
||||
description:
|
||||
'Pause mpv when keyboard-only mode turns on (from the keyboard or a controller) and resume when it turns off. Leaving the mode resumes only a pause it made: a video that was already paused, or that you played or paused yourself meanwhile, is left as it is.',
|
||||
},
|
||||
{
|
||||
path: 'subtitleStyle.primaryVisibleOnYomitanPopup',
|
||||
kind: 'boolean',
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
ResolvedControllerDiscreteBinding,
|
||||
} from '../../types/runtime';
|
||||
import {
|
||||
isControllerFaceButtonLabels,
|
||||
isControllerScreenAction,
|
||||
parseControllerReviewBinding,
|
||||
type ControllerScreenBindings,
|
||||
@@ -18,6 +19,8 @@ import { CONTROLLER_SCREENS } from '../../shared/controller-screens';
|
||||
import { ResolveContext } from './context';
|
||||
import { asBoolean, asNumber, asString, isObject } from './shared';
|
||||
|
||||
const FACE_BUTTON_LABELS_EXPECTED = "Expected 'xbox', 'nintendo', or 'playstation'.";
|
||||
|
||||
const CONTROLLER_BUTTON_BINDINGS = [
|
||||
'none',
|
||||
'select',
|
||||
@@ -98,6 +101,10 @@ const CONTROLLER_DISCRETE_BINDING_KEYS = [
|
||||
'holdSecondarySub',
|
||||
'previousEntry',
|
||||
'nextEntry',
|
||||
'previousSubtitle',
|
||||
'nextSubtitle',
|
||||
'replaySubtitle',
|
||||
'playNextSubtitle',
|
||||
] as const;
|
||||
|
||||
const CONTROLLER_AXIS_BINDING_KEYS = [
|
||||
@@ -360,15 +367,15 @@ export function applyControllerConfig(context: ResolveContext): void {
|
||||
);
|
||||
}
|
||||
|
||||
const faceButtonLabels = asString(src.controller.faceButtonLabels);
|
||||
if (faceButtonLabels === 'xbox' || faceButtonLabels === 'nintendo') {
|
||||
const faceButtonLabels = src.controller.faceButtonLabels;
|
||||
if (isControllerFaceButtonLabels(faceButtonLabels)) {
|
||||
resolved.controller.faceButtonLabels = faceButtonLabels;
|
||||
} else if (src.controller.faceButtonLabels !== undefined) {
|
||||
} else if (faceButtonLabels !== undefined) {
|
||||
warn(
|
||||
'controller.faceButtonLabels',
|
||||
src.controller.faceButtonLabels,
|
||||
faceButtonLabels,
|
||||
resolved.controller.faceButtonLabels,
|
||||
"Expected 'xbox' or 'nintendo'.",
|
||||
FACE_BUTTON_LABELS_EXPECTED,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -465,9 +472,21 @@ export function applyControllerConfig(context: ResolveContext): void {
|
||||
label: label ?? profileId,
|
||||
buttonIndices: structuredClone(resolved.controller.buttonIndices),
|
||||
bindings: structuredClone(resolved.controller.bindings),
|
||||
faceButtonLabels: resolved.controller.faceButtonLabels,
|
||||
reviewBindings: structuredClone(resolved.controller.reviewBindings),
|
||||
fieldGroupingBindings: structuredClone(resolved.controller.fieldGroupingBindings),
|
||||
};
|
||||
// Set per controller from Alt+C; falls back to controller.faceButtonLabels.
|
||||
if (isControllerFaceButtonLabels(rawProfile.faceButtonLabels)) {
|
||||
profile.faceButtonLabels = rawProfile.faceButtonLabels;
|
||||
} else if (rawProfile.faceButtonLabels !== undefined) {
|
||||
warn(
|
||||
`controller.profiles.${profileId}.faceButtonLabels`,
|
||||
rawProfile.faceButtonLabels,
|
||||
profile.faceButtonLabels,
|
||||
FACE_BUTTON_LABELS_EXPECTED,
|
||||
);
|
||||
}
|
||||
applyControllerButtonIndices(
|
||||
rawProfile.buttonIndices,
|
||||
profile.buttonIndices,
|
||||
|
||||
@@ -125,6 +125,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
||||
const fallbackSubtitleStyleAutoPauseVideoOnHover = resolved.subtitleStyle.autoPauseVideoOnHover;
|
||||
const fallbackSubtitleStyleAutoPauseVideoOnYomitanPopup =
|
||||
resolved.subtitleStyle.autoPauseVideoOnYomitanPopup;
|
||||
const fallbackSubtitleStyleAutoPauseVideoOnKeyboardMode =
|
||||
resolved.subtitleStyle.autoPauseVideoOnKeyboardMode;
|
||||
const fallbackSubtitleStylePrimaryVisibleOnYomitanPopup =
|
||||
resolved.subtitleStyle.primaryVisibleOnYomitanPopup;
|
||||
const fallbackSubtitleStyleHoverTokenColor = resolved.subtitleStyle.hoverTokenColor;
|
||||
@@ -274,6 +276,23 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
||||
);
|
||||
}
|
||||
|
||||
const rawAutoPauseVideoOnKeyboardMode = (
|
||||
src.subtitleStyle as { autoPauseVideoOnKeyboardMode?: unknown }
|
||||
).autoPauseVideoOnKeyboardMode;
|
||||
const autoPauseVideoOnKeyboardMode = asBoolean(rawAutoPauseVideoOnKeyboardMode);
|
||||
if (autoPauseVideoOnKeyboardMode !== undefined) {
|
||||
resolved.subtitleStyle.autoPauseVideoOnKeyboardMode = autoPauseVideoOnKeyboardMode;
|
||||
} else if (rawAutoPauseVideoOnKeyboardMode !== undefined) {
|
||||
resolved.subtitleStyle.autoPauseVideoOnKeyboardMode =
|
||||
fallbackSubtitleStyleAutoPauseVideoOnKeyboardMode;
|
||||
warn(
|
||||
'subtitleStyle.autoPauseVideoOnKeyboardMode',
|
||||
rawAutoPauseVideoOnKeyboardMode,
|
||||
resolved.subtitleStyle.autoPauseVideoOnKeyboardMode,
|
||||
'Expected boolean.',
|
||||
);
|
||||
}
|
||||
|
||||
const primaryVisibleOnYomitanPopup = asBoolean(
|
||||
(src.subtitleStyle as { primaryVisibleOnYomitanPopup?: unknown })
|
||||
.primaryVisibleOnYomitanPopup,
|
||||
|
||||
@@ -346,3 +346,28 @@ test('subtitleStyle frequencyDictionary.matchMode accepts valid values and warns
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('subtitleStyle autoPauseVideoOnKeyboardMode is off by default and falls back on invalid value', () => {
|
||||
const defaults = createResolveContext({});
|
||||
applySubtitleDomainConfig(defaults.context);
|
||||
assert.equal(defaults.context.resolved.subtitleStyle.autoPauseVideoOnKeyboardMode, false);
|
||||
|
||||
const enabled = createResolveContext({ subtitleStyle: { autoPauseVideoOnKeyboardMode: true } });
|
||||
applySubtitleDomainConfig(enabled.context);
|
||||
assert.equal(enabled.context.resolved.subtitleStyle.autoPauseVideoOnKeyboardMode, true);
|
||||
|
||||
const { context, warnings } = createResolveContext({
|
||||
subtitleStyle: {
|
||||
autoPauseVideoOnKeyboardMode: 'yes' as unknown as boolean,
|
||||
},
|
||||
});
|
||||
applySubtitleDomainConfig(context);
|
||||
assert.equal(context.resolved.subtitleStyle.autoPauseVideoOnKeyboardMode, false);
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(warning) =>
|
||||
warning.path === 'subtitleStyle.autoPauseVideoOnKeyboardMode' &&
|
||||
warning.message === 'Expected boolean.',
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -233,6 +233,7 @@ const LABEL_OVERRIDES: Record<string, string> = {
|
||||
'subtitleSidebar.pauseVideoOnHover': 'Pause Video On Hover - Sidebar',
|
||||
'subtitleStyle.autoPauseVideoOnHover': 'Pause Video On Hover - Subtitles',
|
||||
'subtitleStyle.autoPauseVideoOnYomitanPopup': 'Pause Video On Dictionary Popup',
|
||||
'subtitleStyle.autoPauseVideoOnKeyboardMode': 'Pause Video In Keyboard-Only Mode',
|
||||
'subtitleStyle.primaryVisibleOnYomitanPopup': 'Keep Primary Visible On Dictionary Popup',
|
||||
'subtitleStyle.primaryDefaultMode': 'Primary Subtitle Visibility Mode',
|
||||
'subtitleStyle.frequencyDictionary.mode': 'Frequency Mode',
|
||||
@@ -344,6 +345,7 @@ function categoryAndSection(path: string): { category: ConfigSettingsCategory; s
|
||||
if (
|
||||
path === 'subtitleStyle.autoPauseVideoOnHover' ||
|
||||
path === 'subtitleStyle.autoPauseVideoOnYomitanPopup' ||
|
||||
path === 'subtitleStyle.autoPauseVideoOnKeyboardMode' ||
|
||||
path === 'subtitleSidebar.pauseVideoOnHover'
|
||||
) {
|
||||
return { category: 'behavior', section: 'Playback Behavior' };
|
||||
|
||||
Reference in new issue
Block a user