feat(dictionary): add Hachidori backend support (#266)

This commit is contained in:
sudacode authored and GitHub committed 2026-10-24 21:08:18 -07:00
1 parent e690ffba07
commit cbaa4626e1
143 files changed
+5564 -305

No files matched your search

+2
View File
@@ -22,6 +22,7 @@ export type {
} from './definitions/shared';
const {
dictionaryBackend,
subtitlePosition,
keybindings,
websocket,
@@ -57,6 +58,7 @@ const { stats } = STATS_DEFAULT_CONFIG;
export const DEFAULT_CONFIG: ResolvedConfig = {
subtitleSelection: { enabled: false },
subtitleGeneration: { ...DEFAULT_SUBTITLE_GENERATION_CONFIG },
dictionaryBackend,
subtitlePosition,
keybindings,
websocket,
+2
View File
@@ -2,6 +2,7 @@ import { ResolvedConfig } from '../../types/config';
export const CORE_DEFAULT_CONFIG: Pick<
ResolvedConfig,
| 'dictionaryBackend'
| 'subtitlePosition'
| 'keybindings'
| 'websocket'
@@ -18,6 +19,7 @@ export const CORE_DEFAULT_CONFIG: Pick<
| 'notifications'
| 'auto_start_overlay'
> = {
dictionaryBackend: 'yomitan',
subtitlePosition: { yPercent: 10 },
keybindings: [],
websocket: {
+7
View File
@@ -81,6 +81,13 @@ export function buildCoreConfigOptionRegistry(
] as const;
return [
{
path: 'dictionaryBackend',
kind: 'enum',
enumValues: ['yomitan', 'hachidori'],
defaultValue: defaultConfig.dictionaryBackend,
description: 'Dictionary lookup backend. Restart SubMiner after changing this setting.',
},
{
path: 'logging.level',
kind: 'enum',
@@ -1,6 +1,14 @@
import { ConfigTemplateSection } from './shared';
const CORE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
{
title: 'Dictionary Backend',
description: ['Select the dictionary lookup backend: yomitan or hachidori.'],
notes: [
'Restart SubMiner after changing the backend. Each backend keeps separate settings and dictionaries.',
],
key: 'dictionaryBackend',
},
{
title: 'Subtitle Selection',
description: ['Select primary and secondary mpv subtitle tracks from the overlay.'],
+11
View File
@@ -6,6 +6,17 @@ import { asBoolean, asNumber, asString, isObject } from './shared';
export function applyCoreDomainConfig(context: ResolveContext): void {
const { src, resolved, warn } = context;
if (src.dictionaryBackend === 'yomitan' || src.dictionaryBackend === 'hachidori') {
resolved.dictionaryBackend = src.dictionaryBackend;
} else if (src.dictionaryBackend !== undefined) {
warn(
'dictionaryBackend',
src.dictionaryBackend,
resolved.dictionaryBackend,
"Expected 'yomitan' or 'hachidori'.",
);
}
if (isObject(src.subtitleSelection)) {
const enabled = asBoolean(src.subtitleSelection.enabled);
if (enabled !== undefined) resolved.subtitleSelection.enabled = enabled;
@@ -0,0 +1,30 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { resolveConfig } from '../resolve';
import { buildConfigSettingsRegistry } from '../settings/registry';
import { createResolveContext } from './context';
import { applyCoreDomainConfig } from './core-domains';
test('dictionary backend defaults to Yomitan and accepts Hachidori', () => {
assert.equal(resolveConfig({}).resolved.dictionaryBackend, 'yomitan');
const { resolved, warnings } = resolveConfig({ dictionaryBackend: 'hachidori' });
assert.equal(resolved.dictionaryBackend, 'hachidori');
assert.deepEqual(warnings, []);
const field = buildConfigSettingsRegistry(resolved).find(
(entry) => entry.configPath === 'dictionaryBackend',
);
assert.equal(field?.restartBehavior, 'restart');
assert.deepEqual(field?.enumValues, ['yomitan', 'hachidori']);
assert.equal(field?.category, 'integrations');
});
test('unknown dictionary backend values warn and preserve the default', () => {
for (const dictionaryBackend of ['unknown', '', null, true, {}]) {
const { context, warnings } = createResolveContext({});
context.src.dictionaryBackend = dictionaryBackend;
applyCoreDomainConfig(context);
assert.equal(context.resolved.dictionaryBackend, 'yomitan');
assert.equal(warnings.length, 1);
assert.equal(warnings[0]?.path, 'dictionaryBackend');
}
});
+7 -3
View File
@@ -146,6 +146,7 @@ const SECTION_ORDER = new Map<string, number>(
'Discord Rich Presence',
'Jellyfin',
'Texthooker',
'Dictionary Lookup',
'Yomitan',
'Stats dashboard',
'Startup warmups',
@@ -228,8 +229,8 @@ const LABEL_OVERRIDES: Record<string, string> = {
'shortcuts.openCharacterDictionaryManager': 'Open Character Dictionary Manager',
'subtitleSidebar.pauseVideoOnHover': 'Pause Video On Hover - Sidebar',
'subtitleStyle.autoPauseVideoOnHover': 'Pause Video On Hover - Subtitles',
'subtitleStyle.autoPauseVideoOnYomitanPopup': 'Pause Video On Yomitan Popup',
'subtitleStyle.primaryVisibleOnYomitanPopup': 'Keep Primary Visible On Yomitan Popup',
'subtitleStyle.autoPauseVideoOnYomitanPopup': 'Pause Video On Dictionary Popup',
'subtitleStyle.primaryVisibleOnYomitanPopup': 'Keep Primary Visible On Dictionary Popup',
'subtitleStyle.primaryDefaultMode': 'Primary Subtitle Visibility Mode',
'subtitleStyle.frequencyDictionary.mode': 'Frequency Mode',
'subtitleStyle.css': 'CSS Declarations',
@@ -270,7 +271,7 @@ const DESCRIPTION_OVERRIDES: Record<string, string> = {
'subtitleSidebar.css':
'CSS declarations applied to the subtitle sidebar. Includes color, background-color, all font properties, and sidebar CSS variables.',
'subtitleStyle.primaryVisibleOnYomitanPopup':
'When primary subtitles are in hover mode, keep the primary subtitle bar visible while a Yomitan popup is open.',
'When primary subtitles are in hover mode, keep the primary subtitle bar visible while a dictionary popup is open.',
'websocket.enabled':
'Built-in subtitle WebSocket server mode. Auto starts the built-in server only when mpv_websocket is not detected; otherwise it defers to the plugin.',
'discordPresence.updateIntervalMs':
@@ -334,6 +335,9 @@ function humanizePath(path: string): string {
}
function categoryAndSection(path: string): { category: ConfigSettingsCategory; section: string } {
if (path === 'dictionaryBackend') {
return { category: 'integrations', section: 'Dictionary Lookup' };
}
if (
path === 'subtitleStyle.autoPauseVideoOnHover' ||
path === 'subtitleStyle.autoPauseVideoOnYomitanPopup' ||