mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
fix(shortcuts): gate feature-dependent bindings
Disable Anki-dependent shortcuts when AnkiConnect is off and require jellyfin.enabled for remote startup warmups to avoid initializing disabled integrations.
This commit is contained in:
76
src/core/utils/shortcut-config.test.ts
Normal file
76
src/core/utils/shortcut-config.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import type { Config } from '../../types';
|
||||
import { resolveConfiguredShortcuts } from './shortcut-config';
|
||||
|
||||
test('forces Anki-dependent shortcuts to null when AnkiConnect is explicitly disabled', () => {
|
||||
const config: Config = {
|
||||
ankiConnect: { enabled: false },
|
||||
shortcuts: {
|
||||
copySubtitle: 'Ctrl+KeyC',
|
||||
updateLastCardFromClipboard: 'Ctrl+KeyU',
|
||||
triggerFieldGrouping: 'Alt+KeyG',
|
||||
mineSentence: 'Ctrl+Digit1',
|
||||
mineSentenceMultiple: 'Ctrl+Digit2',
|
||||
markAudioCard: 'Alt+KeyM',
|
||||
},
|
||||
};
|
||||
const defaults: Config = {
|
||||
shortcuts: {
|
||||
updateLastCardFromClipboard: 'Alt+KeyL',
|
||||
triggerFieldGrouping: 'Alt+KeyF',
|
||||
mineSentence: 'KeyQ',
|
||||
mineSentenceMultiple: 'KeyW',
|
||||
markAudioCard: 'KeyE',
|
||||
},
|
||||
};
|
||||
|
||||
const resolved = resolveConfiguredShortcuts(config, defaults);
|
||||
|
||||
assert.equal(resolved.updateLastCardFromClipboard, null);
|
||||
assert.equal(resolved.triggerFieldGrouping, null);
|
||||
assert.equal(resolved.mineSentence, null);
|
||||
assert.equal(resolved.mineSentenceMultiple, null);
|
||||
assert.equal(resolved.markAudioCard, null);
|
||||
assert.equal(resolved.copySubtitle, 'Ctrl+C');
|
||||
});
|
||||
|
||||
test('keeps Anki-dependent shortcuts enabled and normalized when AnkiConnect is enabled', () => {
|
||||
const config: Config = {
|
||||
ankiConnect: { enabled: true },
|
||||
shortcuts: {
|
||||
updateLastCardFromClipboard: 'Ctrl+KeyU',
|
||||
mineSentence: 'Ctrl+Digit1',
|
||||
mineSentenceMultiple: 'Ctrl+Digit2',
|
||||
},
|
||||
};
|
||||
const defaults: Config = {
|
||||
shortcuts: {
|
||||
triggerFieldGrouping: 'Alt+KeyG',
|
||||
markAudioCard: 'Alt+KeyM',
|
||||
},
|
||||
};
|
||||
|
||||
const resolved = resolveConfiguredShortcuts(config, defaults);
|
||||
|
||||
assert.equal(resolved.updateLastCardFromClipboard, 'Ctrl+U');
|
||||
assert.equal(resolved.triggerFieldGrouping, 'Alt+G');
|
||||
assert.equal(resolved.mineSentence, 'Ctrl+1');
|
||||
assert.equal(resolved.mineSentenceMultiple, 'Ctrl+2');
|
||||
assert.equal(resolved.markAudioCard, 'Alt+M');
|
||||
});
|
||||
|
||||
test('normalizes fallback shortcuts when AnkiConnect flag is unset', () => {
|
||||
const config: Config = {};
|
||||
const defaults: Config = {
|
||||
shortcuts: {
|
||||
mineSentence: 'KeyQ',
|
||||
openRuntimeOptions: 'Digit9',
|
||||
},
|
||||
};
|
||||
|
||||
const resolved = resolveConfiguredShortcuts(config, defaults);
|
||||
|
||||
assert.equal(resolved.mineSentence, 'Q');
|
||||
assert.equal(resolved.openRuntimeOptions, '9');
|
||||
});
|
||||
@@ -21,6 +21,8 @@ export function resolveConfiguredShortcuts(
|
||||
config: Config,
|
||||
defaultConfig: Config,
|
||||
): ConfiguredShortcuts {
|
||||
const isAnkiConnectDisabled = config.ankiConnect?.enabled === false;
|
||||
|
||||
const normalizeShortcut = (value: string | null | undefined): string | null | undefined => {
|
||||
if (typeof value !== 'string') return value;
|
||||
return value.replace(/\bKey([A-Z])\b/g, '$1').replace(/\bDigit([0-9])\b/g, '$1');
|
||||
@@ -42,20 +44,28 @@ export function resolveConfiguredShortcuts(
|
||||
config.shortcuts?.copySubtitleMultiple ?? defaultConfig.shortcuts?.copySubtitleMultiple,
|
||||
),
|
||||
updateLastCardFromClipboard: normalizeShortcut(
|
||||
config.shortcuts?.updateLastCardFromClipboard ??
|
||||
defaultConfig.shortcuts?.updateLastCardFromClipboard,
|
||||
isAnkiConnectDisabled
|
||||
? null
|
||||
: (config.shortcuts?.updateLastCardFromClipboard ??
|
||||
defaultConfig.shortcuts?.updateLastCardFromClipboard),
|
||||
),
|
||||
triggerFieldGrouping: normalizeShortcut(
|
||||
config.shortcuts?.triggerFieldGrouping ?? defaultConfig.shortcuts?.triggerFieldGrouping,
|
||||
isAnkiConnectDisabled
|
||||
? null
|
||||
: (config.shortcuts?.triggerFieldGrouping ?? defaultConfig.shortcuts?.triggerFieldGrouping),
|
||||
),
|
||||
triggerSubsync: normalizeShortcut(
|
||||
config.shortcuts?.triggerSubsync ?? defaultConfig.shortcuts?.triggerSubsync,
|
||||
),
|
||||
mineSentence: normalizeShortcut(
|
||||
config.shortcuts?.mineSentence ?? defaultConfig.shortcuts?.mineSentence,
|
||||
isAnkiConnectDisabled
|
||||
? null
|
||||
: (config.shortcuts?.mineSentence ?? defaultConfig.shortcuts?.mineSentence),
|
||||
),
|
||||
mineSentenceMultiple: normalizeShortcut(
|
||||
config.shortcuts?.mineSentenceMultiple ?? defaultConfig.shortcuts?.mineSentenceMultiple,
|
||||
isAnkiConnectDisabled
|
||||
? null
|
||||
: (config.shortcuts?.mineSentenceMultiple ?? defaultConfig.shortcuts?.mineSentenceMultiple),
|
||||
),
|
||||
multiCopyTimeoutMs:
|
||||
config.shortcuts?.multiCopyTimeoutMs ?? defaultConfig.shortcuts?.multiCopyTimeoutMs ?? 5000,
|
||||
@@ -63,7 +73,9 @@ export function resolveConfiguredShortcuts(
|
||||
config.shortcuts?.toggleSecondarySub ?? defaultConfig.shortcuts?.toggleSecondarySub,
|
||||
),
|
||||
markAudioCard: normalizeShortcut(
|
||||
config.shortcuts?.markAudioCard ?? defaultConfig.shortcuts?.markAudioCard,
|
||||
isAnkiConnectDisabled
|
||||
? null
|
||||
: (config.shortcuts?.markAudioCard ?? defaultConfig.shortcuts?.markAudioCard),
|
||||
),
|
||||
openRuntimeOptions: normalizeShortcut(
|
||||
config.shortcuts?.openRuntimeOptions ?? defaultConfig.shortcuts?.openRuntimeOptions,
|
||||
|
||||
@@ -2233,7 +2233,12 @@ const {
|
||||
},
|
||||
isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
|
||||
ensureYomitanExtensionLoaded: () => ensureYomitanExtensionLoaded().then(() => {}),
|
||||
shouldAutoConnectJellyfinRemote: () => getResolvedConfig().jellyfin.remoteControlAutoConnect,
|
||||
shouldAutoConnectJellyfinRemote: () => {
|
||||
const jellyfin = getResolvedConfig().jellyfin;
|
||||
return (
|
||||
jellyfin.enabled && jellyfin.remoteControlEnabled && jellyfin.remoteControlAutoConnect
|
||||
);
|
||||
},
|
||||
startJellyfinRemoteSession: () => startJellyfinRemoteSession(),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
|
||||
function createConfig(overrides?: Partial<Record<string, unknown>>) {
|
||||
return {
|
||||
enabled: true,
|
||||
remoteControlEnabled: true,
|
||||
remoteControlAutoConnect: true,
|
||||
serverUrl: 'http://localhost',
|
||||
@@ -21,6 +22,34 @@ function createConfig(overrides?: Partial<Record<string, unknown>>) {
|
||||
} as never;
|
||||
}
|
||||
|
||||
test('start handler no-ops when jellyfin integration is disabled', async () => {
|
||||
let created = false;
|
||||
const startRemote = createStartJellyfinRemoteSessionHandler({
|
||||
getJellyfinConfig: () => createConfig({ enabled: false }),
|
||||
getCurrentSession: () => null,
|
||||
setCurrentSession: () => {},
|
||||
createRemoteSessionService: () => {
|
||||
created = true;
|
||||
return {
|
||||
start: () => {},
|
||||
stop: () => {},
|
||||
advertiseNow: async () => true,
|
||||
};
|
||||
},
|
||||
defaultDeviceId: 'default-device',
|
||||
defaultClientName: 'SubMiner',
|
||||
defaultClientVersion: '1.0',
|
||||
handlePlay: async () => {},
|
||||
handlePlaystate: async () => {},
|
||||
handleGeneralCommand: async () => {},
|
||||
logInfo: () => {},
|
||||
logWarn: () => {},
|
||||
});
|
||||
|
||||
await startRemote();
|
||||
assert.equal(created, false);
|
||||
});
|
||||
|
||||
test('start handler no-ops when remote control is disabled', async () => {
|
||||
let created = false;
|
||||
const startRemote = createStartJellyfinRemoteSessionHandler({
|
||||
@@ -50,8 +79,11 @@ test('start handler no-ops when remote control is disabled', async () => {
|
||||
});
|
||||
|
||||
test('start handler creates, starts, and stores session', async () => {
|
||||
let storedSession: { start: () => void; stop: () => void; advertiseNow: () => Promise<boolean> } | null =
|
||||
null;
|
||||
let storedSession: {
|
||||
start: () => void;
|
||||
stop: () => void;
|
||||
advertiseNow: () => Promise<boolean>;
|
||||
} | null = null;
|
||||
let started = false;
|
||||
const infos: string[] = [];
|
||||
const startRemote = createStartJellyfinRemoteSessionHandler({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
type JellyfinRemoteConfig = {
|
||||
enabled: boolean;
|
||||
remoteControlEnabled: boolean;
|
||||
remoteControlAutoConnect: boolean;
|
||||
serverUrl: string;
|
||||
@@ -54,6 +55,7 @@ export function createStartJellyfinRemoteSessionHandler(deps: {
|
||||
}) {
|
||||
return async (): Promise<void> => {
|
||||
const jellyfinConfig = deps.getJellyfinConfig();
|
||||
if (jellyfinConfig.enabled === false) return;
|
||||
if (jellyfinConfig.remoteControlEnabled === false) return;
|
||||
if (jellyfinConfig.remoteControlAutoConnect === false) return;
|
||||
if (!jellyfinConfig.serverUrl || !jellyfinConfig.accessToken || !jellyfinConfig.userId) return;
|
||||
@@ -87,7 +89,9 @@ export function createStartJellyfinRemoteSessionHandler(deps: {
|
||||
if (registered) {
|
||||
deps.logInfo('Jellyfin cast target is visible to server sessions.');
|
||||
} else {
|
||||
deps.logWarn('Jellyfin remote connected but device not visible in server sessions yet.');
|
||||
deps.logWarn(
|
||||
'Jellyfin remote connected but device not visible in server sessions yet.',
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,14 @@ import {
|
||||
createStartBackgroundWarmupsHandler,
|
||||
} from './startup-warmups';
|
||||
|
||||
function shouldAutoConnectJellyfinRemote(config: {
|
||||
enabled: boolean;
|
||||
remoteControlEnabled: boolean;
|
||||
remoteControlAutoConnect: boolean;
|
||||
}): boolean {
|
||||
return config.enabled && config.remoteControlEnabled && config.remoteControlAutoConnect;
|
||||
}
|
||||
|
||||
test('launchBackgroundWarmupTask logs completion timing', async () => {
|
||||
const debugLogs: string[] = [];
|
||||
const launchTask = createLaunchBackgroundWarmupTaskHandler({
|
||||
@@ -41,7 +49,7 @@ test('startBackgroundWarmups no-ops when already started', () => {
|
||||
assert.equal(launches, 0);
|
||||
});
|
||||
|
||||
test('startBackgroundWarmups schedules base warmups and optional jellyfin warmup', () => {
|
||||
test('startBackgroundWarmups does not schedule jellyfin warmup when jellyfin.enabled is false', () => {
|
||||
const labels: string[] = [];
|
||||
let started = false;
|
||||
const startWarmups = createStartBackgroundWarmupsHandler({
|
||||
@@ -56,7 +64,41 @@ test('startBackgroundWarmups schedules base warmups and optional jellyfin warmup
|
||||
createMecabTokenizerAndCheck: async () => {},
|
||||
ensureYomitanExtensionLoaded: async () => {},
|
||||
prewarmSubtitleDictionaries: async () => {},
|
||||
shouldAutoConnectJellyfinRemote: () => true,
|
||||
shouldAutoConnectJellyfinRemote: () =>
|
||||
shouldAutoConnectJellyfinRemote({
|
||||
enabled: false,
|
||||
remoteControlEnabled: true,
|
||||
remoteControlAutoConnect: true,
|
||||
}),
|
||||
startJellyfinRemoteSession: async () => {},
|
||||
});
|
||||
|
||||
startWarmups();
|
||||
assert.equal(started, true);
|
||||
assert.deepEqual(labels, ['mecab', 'yomitan-extension', 'subtitle-dictionaries']);
|
||||
});
|
||||
|
||||
test('startBackgroundWarmups schedules jellyfin warmup when all jellyfin flags are enabled', () => {
|
||||
const labels: string[] = [];
|
||||
let started = false;
|
||||
const startWarmups = createStartBackgroundWarmupsHandler({
|
||||
getStarted: () => started,
|
||||
setStarted: (value) => {
|
||||
started = value;
|
||||
},
|
||||
isTexthookerOnlyMode: () => false,
|
||||
launchTask: (label) => {
|
||||
labels.push(label);
|
||||
},
|
||||
createMecabTokenizerAndCheck: async () => {},
|
||||
ensureYomitanExtensionLoaded: async () => {},
|
||||
prewarmSubtitleDictionaries: async () => {},
|
||||
shouldAutoConnectJellyfinRemote: () =>
|
||||
shouldAutoConnectJellyfinRemote({
|
||||
enabled: true,
|
||||
remoteControlEnabled: true,
|
||||
remoteControlAutoConnect: true,
|
||||
}),
|
||||
startJellyfinRemoteSession: async () => {},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user