mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-27 00:55:16 -07:00
feat(config): unify mpv plugin options under main config and add CSS/Ani
- Replace subminer.conf plugin config with mpv.* fields in config.jsonc - Add socketPath, backend, autoStartSubMiner, pauseUntilOverlayReady, aniskipEnabled/buttonKey, subminerBinaryPath to mpv config - Add subtitleSidebar.css field; migrate legacy sidebar appearance fields - Add paintOrder and WebkitTextStroke to subtitle style options - Update default subtitle/sidebar fontFamily to CJK-first stack - Fix overlay visible state surviving mpv y-r restart - Fix live config saves applying subtitle CSS immediately to open overlays - Migrate legacy primary/secondary subtitle appearance into subtitleStyle.css on load - Switch AniSkip button key setting to click-to-learn key capture
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
import { createKeyboardHandlers } from './keyboard.js';
|
||||
@@ -108,6 +110,7 @@ function installKeyboardTestGlobals() {
|
||||
const mpvCommands: Array<Array<string | number>> = [];
|
||||
const sessionActions: Array<{ actionId: string; payload?: unknown }> = [];
|
||||
let sessionBindings: CompiledSessionBinding[] = [];
|
||||
let getSessionBindingsImpl: () => Promise<CompiledSessionBinding[]> = async () => sessionBindings;
|
||||
let playbackPausedResponse: boolean | null = false;
|
||||
let statsToggleKey = 'Backquote';
|
||||
let markWatchedKey = 'KeyW';
|
||||
@@ -216,7 +219,7 @@ function installKeyboardTestGlobals() {
|
||||
},
|
||||
electronAPI: {
|
||||
getKeybindings: async () => [],
|
||||
getSessionBindings: async () => sessionBindings,
|
||||
getSessionBindings: () => getSessionBindingsImpl(),
|
||||
getConfiguredShortcuts: async () => configuredShortcuts,
|
||||
sendMpvCommand: (command: Array<string | number>) => {
|
||||
mpvCommands.push(command);
|
||||
@@ -366,6 +369,9 @@ function installKeyboardTestGlobals() {
|
||||
setSessionBindings: (value: CompiledSessionBinding[]) => {
|
||||
sessionBindings = value;
|
||||
},
|
||||
setGetSessionBindings: (value: () => Promise<CompiledSessionBinding[]>) => {
|
||||
getSessionBindingsImpl = value;
|
||||
},
|
||||
setMarkActiveVideoWatchedResult: (value: boolean) => {
|
||||
markActiveVideoWatchedResult = value;
|
||||
},
|
||||
@@ -462,6 +468,16 @@ function createKeyboardHandlerHarness() {
|
||||
};
|
||||
}
|
||||
|
||||
test('renderer installs keyboard forwarding before startup subtitle IPC awaits', () => {
|
||||
const source = fs.readFileSync(path.join(process.cwd(), 'src', 'renderer', 'renderer.ts'), 'utf8');
|
||||
const keyboardSetupIndex = source.indexOf('await keyboardHandlers.setupMpvInputForwarding();');
|
||||
const subtitleRequestIndex = source.indexOf('await window.electronAPI.getCurrentSubtitle();');
|
||||
|
||||
assert.notEqual(keyboardSetupIndex, -1);
|
||||
assert.notEqual(subtitleRequestIndex, -1);
|
||||
assert.equal(keyboardSetupIndex < subtitleRequestIndex, true);
|
||||
});
|
||||
|
||||
test('primary subtitle visibility key cycles modes with primary OSD without mpv sub-visibility', async () => {
|
||||
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||
|
||||
@@ -498,6 +514,25 @@ test('primary subtitle visibility key cycles modes with primary OSD without mpv
|
||||
}
|
||||
});
|
||||
|
||||
test('mpv input forwarding installs local key handling when session binding IPC stalls', async () => {
|
||||
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||
|
||||
try {
|
||||
testGlobals.setGetSessionBindings(() => new Promise<CompiledSessionBinding[]>(() => {}));
|
||||
const setupResult = await Promise.race([
|
||||
handlers.setupMpvInputForwarding().then(() => 'resolved'),
|
||||
wait(25).then(() => 'pending'),
|
||||
]);
|
||||
|
||||
assert.equal(setupResult, 'resolved');
|
||||
testGlobals.dispatchKeydown({ key: '`', code: 'Backquote' });
|
||||
|
||||
assert.equal(testGlobals.statsToggleOverlayCalls(), 1);
|
||||
} finally {
|
||||
testGlobals.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('session help chord resolver follows remapped session bindings', async () => {
|
||||
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ export function createKeyboardHandlers(
|
||||
actionId: 'copySubtitleMultiple' | 'mineSentenceMultiple';
|
||||
timeout: ReturnType<typeof setTimeout> | null;
|
||||
} | null = null;
|
||||
let mpvInputForwardingListenersInstalled = false;
|
||||
|
||||
const CHORD_MAP = new Map<
|
||||
string,
|
||||
@@ -940,7 +941,7 @@ export function createKeyboardHandlers(
|
||||
}
|
||||
}
|
||||
|
||||
async function setupMpvInputForwarding(): Promise<void> {
|
||||
async function loadMpvInputForwardingConfig(): Promise<void> {
|
||||
const [sessionBindings, shortcuts, statsToggleKey, markWatchedKey] = await Promise.all([
|
||||
window.electronAPI.getSessionBindings(),
|
||||
window.electronAPI.getConfiguredShortcuts(),
|
||||
@@ -950,6 +951,42 @@ export function createKeyboardHandlers(
|
||||
updateSessionBindings(sessionBindings);
|
||||
updateConfiguredShortcuts(shortcuts, statsToggleKey, markWatchedKey);
|
||||
syncKeyboardTokenSelection();
|
||||
}
|
||||
|
||||
async function setupMpvInputForwarding(): Promise<void> {
|
||||
installMpvInputForwardingListeners();
|
||||
syncKeyboardTokenSelection();
|
||||
|
||||
let configLoadSettled = false;
|
||||
let configLoadError: unknown = null;
|
||||
const configLoad = loadMpvInputForwardingConfig().then(
|
||||
() => {
|
||||
configLoadSettled = true;
|
||||
},
|
||||
(error) => {
|
||||
configLoadSettled = true;
|
||||
configLoadError = error;
|
||||
console.error('Failed to load overlay keyboard configuration.', error);
|
||||
},
|
||||
);
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
if (!configLoadSettled) {
|
||||
void configLoad;
|
||||
return;
|
||||
}
|
||||
if (configLoadError) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function installMpvInputForwardingListeners(): void {
|
||||
if (mpvInputForwardingListenersInstalled) {
|
||||
return;
|
||||
}
|
||||
mpvInputForwardingListenersInstalled = true;
|
||||
|
||||
const subtitleMutationObserver = new MutationObserver(() => {
|
||||
syncKeyboardTokenSelection();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
import { SPECIAL_COMMANDS } from '../../config/definitions';
|
||||
import { SPECIAL_COMMANDS } from '../../config/definitions/shared';
|
||||
import { createRendererState } from '../state.js';
|
||||
import {
|
||||
createSessionHelpModal,
|
||||
@@ -30,6 +32,16 @@ test('session help formats bracket keybindings as physical keys', () => {
|
||||
assert.equal(formatSessionHelpKeybinding('Shift+BracketLeft'), 'Shift + [');
|
||||
});
|
||||
|
||||
test('session help imports browser-safe special command constants', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(process.cwd(), 'src', 'renderer', 'modals', 'session-help.ts'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
assert.match(source, /from ['"]\.\.\/\.\.\/config\/definitions\/shared['"]/);
|
||||
assert.doesNotMatch(source, /from ['"]\.\.\/\.\.\/config\/definitions['"]/);
|
||||
});
|
||||
|
||||
function createClassList(initialTokens: string[] = []) {
|
||||
const tokens = new Set(initialTokens);
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Keybinding } from '../../types';
|
||||
import type { ShortcutsConfig } from '../../types';
|
||||
import { SPECIAL_COMMANDS } from '../../config/definitions';
|
||||
import { SPECIAL_COMMANDS } from '../../config/definitions/shared';
|
||||
import type { ModalStateReader, RendererContext } from '../context';
|
||||
|
||||
type SessionHelpBindingInfo = {
|
||||
|
||||
@@ -141,6 +141,11 @@ test('subtitle sidebar modal opens from snapshot and clicking cue seeks playback
|
||||
activeLineColor: '#f5bde6',
|
||||
activeLineBackgroundColor: 'rgba(138, 173, 244, 0.22)',
|
||||
hoverLineBackgroundColor: 'rgba(54, 58, 79, 0.84)',
|
||||
css: {
|
||||
'font-size': '22px',
|
||||
color: '#ffffff',
|
||||
'--subtitle-sidebar-timestamp-color': '#aaaaaa',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -175,6 +180,12 @@ test('subtitle sidebar modal opens from snapshot and clicking cue seeks playback
|
||||
const overlayClassList = createClassList();
|
||||
const modalClassList = createClassList(['hidden']);
|
||||
const cueList = createListStub();
|
||||
const contentStyleValues = new Map<string, string>();
|
||||
const contentStyle = {
|
||||
setProperty: (name: string, value: string) => {
|
||||
contentStyleValues.set(name, value);
|
||||
},
|
||||
} as CSSStyleDeclaration & { color?: string };
|
||||
const ctx = {
|
||||
dom: {
|
||||
overlay: { classList: overlayClassList },
|
||||
@@ -187,6 +198,7 @@ test('subtitle sidebar modal opens from snapshot and clicking cue seeks playback
|
||||
subtitleSidebarContent: {
|
||||
classList: createClassList(),
|
||||
getBoundingClientRect: () => ({ width: 420 }),
|
||||
style: contentStyle,
|
||||
},
|
||||
subtitleSidebarClose: { addEventListener: () => {} },
|
||||
subtitleSidebarStatus: { textContent: '' },
|
||||
@@ -207,6 +219,9 @@ test('subtitle sidebar modal opens from snapshot and clicking cue seeks playback
|
||||
assert.equal(cueList.children.length, 2);
|
||||
assert.equal(cueList.scrollTop, 0);
|
||||
assert.deepEqual(cueList.scrollToCalls, []);
|
||||
assert.equal(contentStyleValues.get('font-size'), '22px');
|
||||
assert.equal(contentStyle.color, '#ffffff');
|
||||
assert.equal(contentStyleValues.get('--subtitle-sidebar-timestamp-color'), '#aaaaaa');
|
||||
|
||||
modal.seekToCue(snapshot.cues[0]!);
|
||||
assert.deepEqual(mpvCommands.at(-1), ['seek', 1.08, 'absolute+exact']);
|
||||
|
||||
@@ -55,6 +55,24 @@ function formatCueTimestamp(seconds: number): string {
|
||||
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function applySidebarCssDeclarations(
|
||||
target: HTMLElement,
|
||||
declarations: Record<string, string>,
|
||||
): void {
|
||||
const targetStyle = (target as HTMLElement & { style?: CSSStyleDeclaration }).style;
|
||||
if (!targetStyle) return;
|
||||
for (const [property, rawValue] of Object.entries(declarations)) {
|
||||
const value = rawValue.trim();
|
||||
if (value.length === 0) continue;
|
||||
if (property.includes('-')) {
|
||||
targetStyle.setProperty(property, value);
|
||||
continue;
|
||||
}
|
||||
const styleTarget = targetStyle as unknown as Record<string, string>;
|
||||
styleTarget[property] = value;
|
||||
}
|
||||
}
|
||||
|
||||
export function findActiveSubtitleCueIndex(
|
||||
cues: SubtitleCue[],
|
||||
current: { text: string; startTime?: number | null } | null,
|
||||
@@ -266,6 +284,7 @@ export function createSubtitleSidebarModal(
|
||||
'--subtitle-sidebar-hover-background-color',
|
||||
snapshot.config.hoverLineBackgroundColor,
|
||||
);
|
||||
applySidebarCssDeclarations(ctx.dom.subtitleSidebarContent, snapshot.config.css ?? {});
|
||||
}
|
||||
|
||||
function seekToCue(cue: SubtitleCue): void {
|
||||
|
||||
@@ -613,6 +613,8 @@ async function init(): Promise<void> {
|
||||
});
|
||||
});
|
||||
|
||||
await keyboardHandlers.setupMpvInputForwarding();
|
||||
|
||||
let initialSubtitle: SubtitleData | string = '';
|
||||
try {
|
||||
initialSubtitle = await window.electronAPI.getCurrentSubtitle();
|
||||
@@ -698,8 +700,6 @@ async function init(): Promise<void> {
|
||||
});
|
||||
});
|
||||
mouseHandlers.setupDragging();
|
||||
|
||||
await keyboardHandlers.setupMpvInputForwarding();
|
||||
try {
|
||||
ctx.state.controllerConfig = await window.electronAPI.getControllerConfig();
|
||||
} catch (error) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
CharacterDictionarySelectionSnapshot,
|
||||
PrimarySubMode,
|
||||
SubtitlePosition,
|
||||
SubtitleSidebarConfig,
|
||||
SubtitleSidebarSnapshotConfig,
|
||||
SubtitleCue,
|
||||
SubsyncSourceTrack,
|
||||
YoutubePickerOpenPayload,
|
||||
@@ -98,7 +98,7 @@ export type RendererState = {
|
||||
subtitleSidebarToggleKey: string;
|
||||
subtitleSidebarPauseVideoOnHover: boolean;
|
||||
subtitleSidebarAutoScroll: boolean;
|
||||
subtitleSidebarConfig: Required<SubtitleSidebarConfig> | null;
|
||||
subtitleSidebarConfig: SubtitleSidebarSnapshotConfig | null;
|
||||
subtitleSidebarManualScrollUntilMs: number;
|
||||
subtitleSidebarPausedByHover: boolean;
|
||||
|
||||
|
||||
@@ -1912,10 +1912,10 @@ body.subtitle-sidebar-embedded-open .subtitle-sidebar-modal {
|
||||
margin-left: auto;
|
||||
font-family: var(
|
||||
--subtitle-sidebar-font-family,
|
||||
'M PLUS 1',
|
||||
'Noto Sans CJK JP',
|
||||
'Hiragino Sans',
|
||||
sans-serif
|
||||
Hiragino Sans,
|
||||
M PLUS 1,
|
||||
Source Han Sans JP,
|
||||
Noto Sans CJK JP
|
||||
);
|
||||
font-size: var(--subtitle-sidebar-font-size, 16px);
|
||||
background: var(--subtitle-sidebar-background-color, rgba(73, 77, 100, 0.9));
|
||||
@@ -2062,7 +2062,7 @@ body.subtitle-sidebar-embedded-open #subtitleSidebarContent {
|
||||
}
|
||||
|
||||
.subtitle-sidebar-timestamp {
|
||||
font-size: calc(var(--subtitle-sidebar-font-size, 16px) * 0.72);
|
||||
font-size: 0.72em;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: 0.03em;
|
||||
|
||||
Reference in New Issue
Block a user