fix: address follow-up review feedback

This commit is contained in:
2026-05-17 19:05:28 -07:00
parent 4845944cda
commit 36e767adbc
19 changed files with 191 additions and 38 deletions
-3
View File
@@ -321,7 +321,6 @@ test('launcher forwards --args to mpv as parsed tokens', { timeout: 15000 }, ()
fs.mkdirSync(binDir, { recursive: true }); fs.mkdirSync(binDir, { recursive: true });
fs.mkdirSync(path.join(xdgConfigHome, 'SubMiner'), { recursive: true }); fs.mkdirSync(path.join(xdgConfigHome, 'SubMiner'), { recursive: true });
fs.mkdirSync(path.join(xdgConfigHome, 'SubMiner'), { recursive: true });
fs.writeFileSync(videoPath, 'fake video content'); fs.writeFileSync(videoPath, 'fake video content');
fs.writeFileSync( fs.writeFileSync(
path.join(xdgConfigHome, 'SubMiner', 'setup-state.json'), path.join(xdgConfigHome, 'SubMiner', 'setup-state.json'),
@@ -408,7 +407,6 @@ test('launcher forwards non-info log level into mpv plugin script opts', { timeo
fs.mkdirSync(binDir, { recursive: true }); fs.mkdirSync(binDir, { recursive: true });
fs.mkdirSync(path.join(xdgConfigHome, 'SubMiner'), { recursive: true }); fs.mkdirSync(path.join(xdgConfigHome, 'SubMiner'), { recursive: true });
fs.mkdirSync(path.join(xdgConfigHome, 'SubMiner'), { recursive: true });
fs.writeFileSync(videoPath, 'fake video content'); fs.writeFileSync(videoPath, 'fake video content');
fs.writeFileSync( fs.writeFileSync(
path.join(xdgConfigHome, 'SubMiner', 'setup-state.json'), path.join(xdgConfigHome, 'SubMiner', 'setup-state.json'),
@@ -485,7 +483,6 @@ test('launcher routes youtube urls through regular playback startup', { timeout:
fs.mkdirSync(binDir, { recursive: true }); fs.mkdirSync(binDir, { recursive: true });
fs.mkdirSync(path.join(xdgConfigHome, 'SubMiner'), { recursive: true }); fs.mkdirSync(path.join(xdgConfigHome, 'SubMiner'), { recursive: true });
fs.mkdirSync(path.join(xdgConfigHome, 'SubMiner'), { recursive: true });
fs.writeFileSync( fs.writeFileSync(
path.join(xdgConfigHome, 'SubMiner', 'setup-state.json'), path.join(xdgConfigHome, 'SubMiner', 'setup-state.json'),
JSON.stringify({ JSON.stringify({
+7 -5
View File
@@ -295,10 +295,11 @@ const texthookerService = new services_1.Texthooker(() => {
const config = getResolvedConfig(); const config = getResolvedConfig();
const characterDictionaryEnabled = config.anilist.characterDictionary.enabled && const characterDictionaryEnabled = config.anilist.characterDictionary.enabled &&
yomitanProfilePolicy.isCharacterDictionaryEnabled(); yomitanProfilePolicy.isCharacterDictionaryEnabled();
const knownAndNPlusOneEnabled = getRuntimeBooleanOption('subtitle.annotation.nPlusOne', config.ankiConnect.nPlusOne.enabled); const knownWordColoringEnabled = getRuntimeBooleanOption('subtitle.annotation.knownWords.highlightEnabled', config.ankiConnect.knownWords.highlightEnabled);
const nPlusOneColoringEnabled = getRuntimeBooleanOption('subtitle.annotation.nPlusOne', config.ankiConnect.nPlusOne.enabled);
return { return {
enableKnownWordColoring: knownAndNPlusOneEnabled, enableKnownWordColoring: knownWordColoringEnabled,
enableNPlusOneColoring: knownAndNPlusOneEnabled, enableNPlusOneColoring: nPlusOneColoringEnabled,
enableNameMatchColoring: config.subtitleStyle.nameMatchEnabled && characterDictionaryEnabled, enableNameMatchColoring: config.subtitleStyle.nameMatchEnabled && characterDictionaryEnabled,
enableFrequencyColoring: getRuntimeBooleanOption('subtitle.annotation.frequency', config.subtitleStyle.frequencyDictionary.enabled), enableFrequencyColoring: getRuntimeBooleanOption('subtitle.annotation.frequency', config.subtitleStyle.frequencyDictionary.enabled),
enableJlptColoring: getRuntimeBooleanOption('subtitle.annotation.jlpt', config.subtitleStyle.enableJlpt), enableJlptColoring: getRuntimeBooleanOption('subtitle.annotation.jlpt', config.subtitleStyle.enableJlpt),
@@ -1819,10 +1820,11 @@ function getRuntimeBooleanOption(id, fallback) {
} }
function shouldInitializeMecabForAnnotations() { function shouldInitializeMecabForAnnotations() {
const config = getResolvedConfig(); const config = getResolvedConfig();
const knownWordsEnabled = getRuntimeBooleanOption('subtitle.annotation.knownWords.highlightEnabled', config.ankiConnect.knownWords.highlightEnabled);
const nPlusOneEnabled = getRuntimeBooleanOption('subtitle.annotation.nPlusOne', config.ankiConnect.nPlusOne.enabled); const nPlusOneEnabled = getRuntimeBooleanOption('subtitle.annotation.nPlusOne', config.ankiConnect.nPlusOne.enabled);
const jlptEnabled = getRuntimeBooleanOption('subtitle.annotation.jlpt', config.subtitleStyle.enableJlpt); const jlptEnabled = getRuntimeBooleanOption('subtitle.annotation.jlpt', config.subtitleStyle.enableJlpt);
const frequencyEnabled = getRuntimeBooleanOption('subtitle.annotation.frequency', config.subtitleStyle.frequencyDictionary.enabled); const frequencyEnabled = getRuntimeBooleanOption('subtitle.annotation.frequency', config.subtitleStyle.frequencyDictionary.enabled);
return nPlusOneEnabled || jlptEnabled || frequencyEnabled; return knownWordsEnabled || nPlusOneEnabled || jlptEnabled || frequencyEnabled;
} }
const { getResolvedJellyfinConfig, reportJellyfinRemoteProgress, reportJellyfinRemoteStopped, startJellyfinRemoteSession, stopJellyfinRemoteSession, runJellyfinCommand, openJellyfinSetupWindow, getJellyfinClientInfo, } = (0, composers_1.composeJellyfinRuntimeHandlers)({ const { getResolvedJellyfinConfig, reportJellyfinRemoteProgress, reportJellyfinRemoteStopped, startJellyfinRemoteSession, stopJellyfinRemoteSession, runJellyfinCommand, openJellyfinSetupWindow, getJellyfinClientInfo, } = (0, composers_1.composeJellyfinRuntimeHandlers)({
getResolvedJellyfinConfigMainDeps: { getResolvedJellyfinConfigMainDeps: {
@@ -4706,4 +4708,4 @@ function appendClipboardVideoToQueue() {
return appendClipboardVideoToQueueHandler(); return appendClipboardVideoToQueueHandler();
} }
registerIpcRuntimeHandlers(); registerIpcRuntimeHandlers();
//# sourceMappingURL=main.js.map //# sourceMappingURL=main.js.map
+2 -2
View File
@@ -49,8 +49,8 @@
"test:plugin:src": "lua scripts/test-plugin-lua-compat.lua && lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-session-bindings.lua && lua scripts/test-plugin-binary-windows.lua", "test:plugin:src": "lua scripts/test-plugin-lua-compat.lua && lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-session-bindings.lua && lua scripts/test-plugin-binary-windows.lua",
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts", "test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
"test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/mpv.test.ts launcher/picker.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/smoke.e2e.test.ts && bun run test:plugin:src", "test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/mpv.test.ts launcher/picker.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/smoke.e2e.test.ts && bun run test:plugin:src",
"test:core:src": "bun test src/preload-settings.test.ts src/settings/settings-anki-controls.test.ts src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/core/services/cli-command.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/yomitan-extension-loader.test.ts src/core/services/yomitan-settings.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/stats-window.test.ts src/core/services/__tests__/stats-server.test.ts src/main/runtime/stats-server-routing.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/main/runtime/current-subtitle-snapshot.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/services/anilist/rate-limiter.test.ts src/core/services/jlpt-token-filter.test.ts src/core/services/subtitle-position.test.ts src/core/utils/shortcut-config.test.ts src/main/runtime/startup-mode-flags.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/command-line-launcher.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/main/runtime/update/appimage-updater.test.ts src/main/runtime/update/fetch-adapter.test.ts src/main/runtime/update/release-metadata-policy.test.ts src/main/runtime/update/update-dialogs.test.ts src/main/runtime/update/support-assets.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/hyprland-tracker.test.ts src/window-trackers/x11-tracker.test.ts src/window-trackers/windows-helper.test.ts src/window-trackers/windows-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/setup-gate.test.ts stats/src/lib/api-client.test.ts stats/src/hooks/useExcludedWords.test.ts", "test:core:src": "bun test src/preload-settings.test.ts src/settings/settings-anki-controls.test.ts src/settings/settings-model.test.ts src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/core/services/cli-command.test.ts src/core/services/ipc.test.ts src/core/services/anki-jimaku-ipc.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/yomitan-extension-loader.test.ts src/core/services/yomitan-settings.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/stats-window.test.ts src/core/services/__tests__/stats-server.test.ts src/main/runtime/stats-server-routing.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/main/runtime/current-subtitle-snapshot.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/services/anilist/rate-limiter.test.ts src/core/services/jlpt-token-filter.test.ts src/core/services/subtitle-position.test.ts src/core/utils/shortcut-config.test.ts src/main/runtime/startup-mode-flags.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/command-line-launcher.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/main/runtime/update/appimage-updater.test.ts src/main/runtime/update/fetch-adapter.test.ts src/main/runtime/update/release-metadata-policy.test.ts src/main/runtime/update/update-dialogs.test.ts src/main/runtime/update/support-assets.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/hyprland-tracker.test.ts src/window-trackers/x11-tracker.test.ts src/window-trackers/windows-helper.test.ts src/window-trackers/windows-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/setup-gate.test.ts stats/src/lib/api-client.test.ts stats/src/hooks/useExcludedWords.test.ts",
"test:core:dist": "bun test dist/preload-settings.test.js dist/settings/settings-anki-controls.test.js dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/yomitan-extension-paths.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/main/runtime/current-subtitle-snapshot.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/core/services/anilist/rate-limiter.test.js dist/core/services/jlpt-token-filter.test.js dist/core/services/subtitle-position.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/hyprland-tracker.test.js dist/window-trackers/x11-tracker.test.js dist/window-trackers/windows-helper.test.js dist/window-trackers/windows-tracker.test.js", "test:core:dist": "bun test dist/preload-settings.test.js dist/settings/settings-anki-controls.test.js dist/settings/settings-model.test.js dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/yomitan-extension-paths.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/main/runtime/current-subtitle-snapshot.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/core/services/anilist/rate-limiter.test.js dist/core/services/jlpt-token-filter.test.js dist/core/services/subtitle-position.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/hyprland-tracker.test.js dist/window-trackers/x11-tracker.test.js dist/window-trackers/windows-helper.test.js dist/window-trackers/windows-tracker.test.js",
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js", "test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist", "test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
"test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts", "test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts",
+8 -1
View File
@@ -504,7 +504,14 @@ function M.create(ctx)
subminer_log("info", "process", "Restarting overlay...") subminer_log("info", "process", "Restarting overlay...")
show_osd("Restarting...") show_osd("Restarting...")
run_control_command_async("stop", nil, function() run_control_command_async("stop", nil, function(ok, result)
if not ok then
local reason = result and result.stderr or "unknown error"
subminer_log("warn", "process", "Restart stop command failed: " .. reason)
show_osd("Restart failed")
return
end
state.overlay_running = false state.overlay_running = false
state.texthooker_running = false state.texthooker_running = false
disarm_auto_play_ready_gate() disarm_auto_play_ready_gate()
+29
View File
@@ -108,6 +108,13 @@ local function run_plugin_scenario(config)
return return
end end
end end
for _, value in ipairs(args) do
if value == "--stop" and config.stop_command_fails then
local stderr = config.stop_command_stderr or "stop failed"
callback(false, { status = 1, stdout = "", stderr = stderr }, stderr)
return
end
end
callback(true, { status = 0, stdout = "", stderr = "" }, nil) callback(true, { status = 0, stdout = "", stderr = "" }, nil)
end end
end end
@@ -593,6 +600,28 @@ do
) )
end end
do
local recorded, err = run_plugin_scenario({
process_list = "",
stop_command_fails = true,
stop_command_stderr = "stop refused",
option_overrides = {
binary_path = binary_path,
},
files = {
[binary_path] = true,
},
})
assert_true(recorded ~= nil, "plugin failed to load for failed restart-stop scenario: " .. tostring(err))
recorded.script_messages["subminer-restart"]()
assert_true(find_control_call(recorded.async_calls, "--stop") ~= nil, "restart should attempt stop")
assert_true(count_start_calls(recorded.async_calls) == 0, "restart should not start overlay when stop fails")
assert_true(
has_osd_message(recorded.osd, "SubMiner: Restart failed"),
"restart stop failure should show failure OSD"
)
end
do do
local recorded, err = run_plugin_scenario({ local recorded, err = run_plugin_scenario({
process_list = "", process_list = "",
+1
View File
@@ -1842,6 +1842,7 @@ test('runtime options registry is centralized', () => {
const ids = RUNTIME_OPTION_REGISTRY.map((entry) => entry.id); const ids = RUNTIME_OPTION_REGISTRY.map((entry) => entry.id);
assert.deepEqual(ids, [ assert.deepEqual(ids, [
'anki.autoUpdateNewCards', 'anki.autoUpdateNewCards',
'subtitle.annotation.knownWords.highlightEnabled',
'subtitle.annotation.nPlusOne', 'subtitle.annotation.nPlusOne',
'subtitle.annotation.jlpt', 'subtitle.annotation.jlpt',
'subtitle.annotation.frequency', 'subtitle.annotation.frequency',
+18 -2
View File
@@ -20,9 +20,9 @@ export function buildRuntimeOptionRegistry(
}), }),
}, },
{ {
id: 'subtitle.annotation.nPlusOne', id: 'subtitle.annotation.knownWords.highlightEnabled',
path: 'ankiConnect.knownWords.highlightEnabled', path: 'ankiConnect.knownWords.highlightEnabled',
label: 'N+1 Annotation', label: 'Known Word Annotation',
scope: 'subtitle', scope: 'subtitle',
valueType: 'boolean', valueType: 'boolean',
allowedValues: [true, false], allowedValues: [true, false],
@@ -35,6 +35,22 @@ export function buildRuntimeOptionRegistry(
}, },
}), }),
}, },
{
id: 'subtitle.annotation.nPlusOne',
path: 'ankiConnect.nPlusOne.enabled',
label: 'N+1 Annotation',
scope: 'subtitle',
valueType: 'boolean',
allowedValues: [true, false],
defaultValue: defaultConfig.ankiConnect.nPlusOne.enabled,
requiresRestart: false,
formatValueForOsd: (value) => (value === true ? 'On' : 'Off'),
toAnkiPatch: (value) => ({
nPlusOne: {
enabled: value === true,
},
}),
},
{ {
id: 'subtitle.annotation.jlpt', id: 'subtitle.annotation.jlpt',
path: 'subtitleStyle.enableJlpt', path: 'subtitleStyle.enableJlpt',
+4 -1
View File
@@ -89,7 +89,7 @@ const JSON_OBJECT_FIELDS = new Set([
const SECRET_PATHS = new Set(['ai.apiKey', 'jimaku.apiKey', 'anilist.accessToken']); const SECRET_PATHS = new Set(['ai.apiKey', 'jimaku.apiKey', 'anilist.accessToken']);
const COLOR_SUFFIXES = new Set(['Color', 'color', 'backgroundColor', 'singleColor', 'nPlusOne']); const COLOR_SUFFIXES = new Set(['Color', 'color', 'backgroundColor', 'singleColor']);
const SUBTITLE_CSS_MANAGED_CONFIG_PATHS = new Set([ const SUBTITLE_CSS_MANAGED_CONFIG_PATHS = new Set([
...getSubtitleCssManagedConfigPaths('primary'), ...getSubtitleCssManagedConfigPaths('primary'),
...getSubtitleCssManagedConfigPaths('secondary'), ...getSubtitleCssManagedConfigPaths('secondary'),
@@ -111,6 +111,9 @@ const CATEGORY_ORDER: ConfigSettingsCategory[] = [
const SECTION_ORDER = new Map<string, number>( const SECTION_ORDER = new Map<string, number>(
[ [
'Annotation Display', 'Annotation Display',
'Known Words',
'N+1',
'Frequency Highlighting',
'Primary Subtitle Appearance', 'Primary Subtitle Appearance',
'Secondary Subtitle Appearance', 'Secondary Subtitle Appearance',
'Subtitle Sidebar Appearance', 'Subtitle Sidebar Appearance',
+17 -5
View File
@@ -662,14 +662,18 @@ const texthookerService = new Texthooker(() => {
const characterDictionaryEnabled = const characterDictionaryEnabled =
config.anilist.characterDictionary.enabled && config.anilist.characterDictionary.enabled &&
yomitanProfilePolicy.isCharacterDictionaryEnabled(); yomitanProfilePolicy.isCharacterDictionaryEnabled();
const knownAndNPlusOneEnabled = getRuntimeBooleanOption( const knownWordColoringEnabled = getRuntimeBooleanOption(
'subtitle.annotation.knownWords.highlightEnabled',
config.ankiConnect.knownWords.highlightEnabled,
);
const nPlusOneColoringEnabled = getRuntimeBooleanOption(
'subtitle.annotation.nPlusOne', 'subtitle.annotation.nPlusOne',
config.ankiConnect.nPlusOne.enabled, config.ankiConnect.nPlusOne.enabled,
); );
return { return {
enableKnownWordColoring: knownAndNPlusOneEnabled, enableKnownWordColoring: knownWordColoringEnabled,
enableNPlusOneColoring: knownAndNPlusOneEnabled, enableNPlusOneColoring: nPlusOneColoringEnabled,
enableNameMatchColoring: config.subtitleStyle.nameMatchEnabled && characterDictionaryEnabled, enableNameMatchColoring: config.subtitleStyle.nameMatchEnabled && characterDictionaryEnabled,
enableFrequencyColoring: getRuntimeBooleanOption( enableFrequencyColoring: getRuntimeBooleanOption(
'subtitle.annotation.frequency', 'subtitle.annotation.frequency',
@@ -2571,7 +2575,11 @@ function getResolvedConfig() {
} }
function getRuntimeBooleanOption( function getRuntimeBooleanOption(
id: 'subtitle.annotation.nPlusOne' | 'subtitle.annotation.jlpt' | 'subtitle.annotation.frequency', id:
| 'subtitle.annotation.knownWords.highlightEnabled'
| 'subtitle.annotation.nPlusOne'
| 'subtitle.annotation.jlpt'
| 'subtitle.annotation.frequency',
fallback: boolean, fallback: boolean,
): boolean { ): boolean {
const value = appState.runtimeOptionsManager?.getOptionValue(id); const value = appState.runtimeOptionsManager?.getOptionValue(id);
@@ -2580,6 +2588,10 @@ function getRuntimeBooleanOption(
function shouldInitializeMecabForAnnotations(): boolean { function shouldInitializeMecabForAnnotations(): boolean {
const config = getResolvedConfig(); const config = getResolvedConfig();
const knownWordsEnabled = getRuntimeBooleanOption(
'subtitle.annotation.knownWords.highlightEnabled',
config.ankiConnect.knownWords.highlightEnabled,
);
const nPlusOneEnabled = getRuntimeBooleanOption( const nPlusOneEnabled = getRuntimeBooleanOption(
'subtitle.annotation.nPlusOne', 'subtitle.annotation.nPlusOne',
config.ankiConnect.nPlusOne.enabled, config.ankiConnect.nPlusOne.enabled,
@@ -2592,7 +2604,7 @@ function shouldInitializeMecabForAnnotations(): boolean {
'subtitle.annotation.frequency', 'subtitle.annotation.frequency',
config.subtitleStyle.frequencyDictionary.enabled, config.subtitleStyle.frequencyDictionary.enabled,
); );
return nPlusOneEnabled || jlptEnabled || frequencyEnabled; return knownWordsEnabled || nPlusOneEnabled || jlptEnabled || frequencyEnabled;
} }
const { const {
+30 -1
View File
@@ -521,7 +521,7 @@ test('mpv input forwarding installs local key handling when session binding IPC
testGlobals.setGetSessionBindings(() => new Promise<CompiledSessionBinding[]>(() => {})); testGlobals.setGetSessionBindings(() => new Promise<CompiledSessionBinding[]>(() => {}));
const setupResult = await Promise.race([ const setupResult = await Promise.race([
handlers.setupMpvInputForwarding().then(() => 'resolved'), handlers.setupMpvInputForwarding().then(() => 'resolved'),
wait(25).then(() => 'pending'), wait(75).then(() => 'pending'),
]); ]);
assert.equal(setupResult, 'resolved'); assert.equal(setupResult, 'resolved');
@@ -533,6 +533,35 @@ test('mpv input forwarding installs local key handling when session binding IPC
} }
}); });
test('mpv input forwarding waits for session bindings before resolving setup', async () => {
const { handlers, testGlobals } = createKeyboardHandlerHarness();
try {
testGlobals.setGetSessionBindings(async () => {
await wait(20);
return [
{
sourcePath: 'keybindings[0].key',
originalKey: 'KeyH',
key: { code: 'KeyH', modifiers: [] },
actionType: 'mpv-command',
command: ['cycle', 'pause'],
},
] as CompiledSessionBinding[];
});
await handlers.setupMpvInputForwarding();
assert.deepEqual(handlers.getSessionHelpOpeningInfo(), {
bindingKey: 'KeyK',
fallbackUsed: true,
fallbackUnavailable: false,
});
} finally {
testGlobals.restore();
}
});
test('mpv input forwarding retries a transient keyboard config IPC failure', async () => { test('mpv input forwarding retries a transient keyboard config IPC failure', async () => {
const { handlers, testGlobals } = createKeyboardHandlerHarness(); const { handlers, testGlobals } = createKeyboardHandlerHarness();
let calls = 0; let calls = 0;
+8 -12
View File
@@ -35,6 +35,7 @@ export function createKeyboardHandlers(
) { ) {
// Timeout for the modal chord capture window (e.g. Y followed by H/K). // Timeout for the modal chord capture window (e.g. Y followed by H/K).
const CHORD_TIMEOUT_MS = 1000; const CHORD_TIMEOUT_MS = 1000;
const MPV_INPUT_FORWARDING_CONFIG_LOAD_TIMEOUT_MS = 50;
const KEYBOARD_SELECTED_WORD_CLASS = 'keyboard-selected'; const KEYBOARD_SELECTED_WORD_CLASS = 'keyboard-selected';
let pendingSelectionAnchorAfterSubtitleSeek: 'start' | 'end' | null = null; let pendingSelectionAnchorAfterSubtitleSeek: 'start' | 'end' | null = null;
let pendingLookupRefreshAfterSubtitleSeek = false; let pendingLookupRefreshAfterSubtitleSeek = false;
@@ -975,26 +976,21 @@ export function createKeyboardHandlers(
installMpvInputForwardingListeners(); installMpvInputForwardingListeners();
syncKeyboardTokenSelection(); syncKeyboardTokenSelection();
let configLoadSettled = false;
let configLoadError: unknown = null; let configLoadError: unknown = null;
const configLoad = loadMpvInputForwardingConfigWithRetry().then( const configLoad = loadMpvInputForwardingConfigWithRetry().then(
() => { () => {},
configLoadSettled = true;
},
(error) => { (error) => {
configLoadSettled = true;
configLoadError = error; configLoadError = error;
console.error('Failed to load overlay keyboard configuration.', error); console.error('Failed to load overlay keyboard configuration.', error);
}, },
); );
await new Promise<void>((resolve) => { await Promise.race([
setTimeout(resolve, 0); configLoad,
}); new Promise<void>((resolve) => {
if (!configLoadSettled) { setTimeout(resolve, MPV_INPUT_FORWARDING_CONFIG_LOAD_TIMEOUT_MS);
void configLoad; }),
return; ]);
}
if (configLoadError) { if (configLoadError) {
return; return;
} }
@@ -138,6 +138,14 @@ test('applySidebarCssDeclarations clears declarations removed by config reload',
assert.equal(style.color, '#ffffff'); assert.equal(style.color, '#ffffff');
assert.equal(style.backgroundColor, ''); assert.equal(style.backgroundColor, '');
assert.deepEqual(removed, ['background-color']); assert.deepEqual(removed, ['background-color']);
applySidebarCssDeclarations(target, {
color: '',
'background-color': '',
});
assert.equal(style.color, '');
assert.deepEqual(removed, ['background-color', 'background-color']);
}); });
test('subtitle sidebar modal opens from snapshot and clicking cue seeks playback', async () => { test('subtitle sidebar modal opens from snapshot and clicking cue seeks playback', async () => {
+8 -1
View File
@@ -77,7 +77,14 @@ export function applySidebarCssDeclarations(
for (const [property, rawValue] of Object.entries(declarations)) { for (const [property, rawValue] of Object.entries(declarations)) {
const value = rawValue.trim(); const value = rawValue.trim();
if (value.length === 0) continue; if (value.length === 0) {
if (property.includes('-')) {
targetStyle.removeProperty(property);
} else {
styleTarget[property] = '';
}
continue;
}
if (property.includes('-')) { if (property.includes('-')) {
targetStyle.setProperty(property, value); targetStyle.setProperty(property, value);
} else { } else {
+23
View File
@@ -3,6 +3,7 @@ import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import test from 'node:test'; import test from 'node:test';
import { DEFAULT_CONFIG } from './config';
import { RuntimeOptionsManager } from './runtime-options'; import { RuntimeOptionsManager } from './runtime-options';
test('SM-012 runtime options path does not use JSON serialize-clone helpers', () => { test('SM-012 runtime options path does not use JSON serialize-clone helpers', () => {
@@ -59,3 +60,25 @@ test('RuntimeOptionsManager returns detached effective Anki config copies', () =
assert.deepEqual(baseConfig.tags, ['SubMiner']); assert.deepEqual(baseConfig.tags, ['SubMiner']);
assert.equal(baseConfig.behavior.autoUpdateNewCards, true); assert.equal(baseConfig.behavior.autoUpdateNewCards, true);
}); });
test('RuntimeOptionsManager keeps known-word and n+1 annotation toggles separate', () => {
const baseConfig = structuredClone(DEFAULT_CONFIG.ankiConnect);
const patches: unknown[] = [];
const manager = new RuntimeOptionsManager(() => structuredClone(baseConfig), {
applyAnkiPatch: (patch) => {
patches.push(patch);
},
onOptionsChanged: () => undefined,
});
assert.equal(
manager.setOptionValue('subtitle.annotation.knownWords.highlightEnabled', true).ok,
true,
);
assert.equal(manager.setOptionValue('subtitle.annotation.nPlusOne', true).ok, true);
const effective = manager.getEffectiveAnkiConnectConfig();
assert.equal(effective.knownWords?.highlightEnabled, true);
assert.equal(effective.nPlusOne?.enabled, true);
assert.deepEqual(patches, []);
});
+2 -2
View File
@@ -140,8 +140,8 @@ export function renderMpvKeybindingsInput(
removeButton.type = 'button'; removeButton.type = 'button';
removeButton.textContent = 'Remove'; removeButton.textContent = 'Remove';
removeButton.addEventListener('click', () => { removeButton.addEventListener('click', () => {
rows.splice(i, 1); const nextRows = rows.filter((_, index) => index !== i);
applyMpvRows(context, field, rows); applyMpvRows(context, field, nextRows);
requestRender(); requestRender();
}); });
item.append(keyButton, command, removeButton); item.append(keyButton, command, removeButton);
+21
View File
@@ -87,6 +87,27 @@ test('filterSettingsFields normalizes punctuation in query terms', () => {
); );
}); });
test('filterSettingsFields preserves non-Latin query terms', () => {
const japaneseFields: ConfigSettingsField[] = [
{
id: 'subtitleStyle.japaneseFontFamily',
label: '日本語フォント',
description: '字幕の表示に使う書体。',
configPath: 'subtitleStyle.japaneseFontFamily',
category: 'appearance',
section: 'Primary Subtitle Appearance',
control: 'text',
defaultValue: '',
restartBehavior: 'hot-reload',
},
];
assert.deepEqual(
filterSettingsFields(japaneseFields, { query: '日本語' }).map((field) => field.configPath),
['subtitleStyle.japaneseFontFamily'],
);
});
test('settings draft tracks dirty set and emits save operations', () => { test('settings draft tracks dirty set and emits save operations', () => {
const draft = createSettingsDraft({ const draft = createSettingsDraft({
'subtitleStyle.autoPauseVideoOnHover': true, 'subtitleStyle.autoPauseVideoOnHover': true,
+3 -3
View File
@@ -17,7 +17,7 @@ export interface SettingsDraft {
} }
function normalizeQuery(query: string | undefined): string { function normalizeQuery(query: string | undefined): string {
return (query ?? '').trim().toLowerCase(); return (query ?? '').trim().toLocaleLowerCase();
} }
function searchableText(parts: Array<string | undefined>): string { function searchableText(parts: Array<string | undefined>): string {
@@ -25,8 +25,8 @@ function searchableText(parts: Array<string | undefined>): string {
.filter(Boolean) .filter(Boolean)
.join(' ') .join(' ')
.replace(/([a-z0-9])([A-Z])/g, '$1 $2') .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.replace(/[^a-zA-Z0-9]+/g, ' ') .replace(/[^\p{L}\p{N}]+/gu, ' ')
.toLowerCase(); .toLocaleLowerCase();
} }
function valuesEqual(a: unknown, b: unknown): boolean { function valuesEqual(a: unknown, b: unknown): boolean {
+1
View File
@@ -48,6 +48,7 @@ const SESSION_ACTION_IDS: SessionActionId[] = [
const RUNTIME_OPTION_IDS: RuntimeOptionId[] = [ const RUNTIME_OPTION_IDS: RuntimeOptionId[] = [
'anki.autoUpdateNewCards', 'anki.autoUpdateNewCards',
'subtitle.annotation.knownWords.highlightEnabled',
'subtitle.annotation.nPlusOne', 'subtitle.annotation.nPlusOne',
'subtitle.annotation.jlpt', 'subtitle.annotation.jlpt',
'subtitle.annotation.frequency', 'subtitle.annotation.frequency',
+1
View File
@@ -1,5 +1,6 @@
export type RuntimeOptionId = export type RuntimeOptionId =
| 'anki.autoUpdateNewCards' | 'anki.autoUpdateNewCards'
| 'subtitle.annotation.knownWords.highlightEnabled'
| 'subtitle.annotation.nPlusOne' | 'subtitle.annotation.nPlusOne'
| 'subtitle.annotation.jlpt' | 'subtitle.annotation.jlpt'
| 'subtitle.annotation.frequency' | 'subtitle.annotation.frequency'