fix: always hide mpv primary subtitles for visible overlay

This commit is contained in:
2026-02-27 18:32:29 -08:00
parent dde51f8634
commit 9e4e588f33
22 changed files with 153 additions and 108 deletions

View File

@@ -100,6 +100,12 @@ Enable automatic Anki card creation and updates with media generation:
"enabled": true, "enabled": true,
"url": "http://127.0.0.1:8765", "url": "http://127.0.0.1:8765",
"pollingRate": 3000, "pollingRate": 3000,
"proxy": {
"enabled": false,
"host": "127.0.0.1",
"port": 8766,
"upstreamUrl": "http://127.0.0.1:8765"
},
"tags": ["SubMiner"], "tags": ["SubMiner"],
"deck": "Learning::Japanese", "deck": "Learning::Japanese",
"fields": { "fields": {
@@ -163,7 +169,11 @@ This example is intentionally compact. The option table below documents availabl
| --------------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | --------------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled` | `true`, `false` | Enable AnkiConnect integration (default: `false`) | | `enabled` | `true`, `false` | Enable AnkiConnect integration (default: `false`) |
| `url` | string (URL) | AnkiConnect API URL (default: `http://127.0.0.1:8765`) | | `url` | string (URL) | AnkiConnect API URL (default: `http://127.0.0.1:8765`) |
| `pollingRate` | number (ms) | How often to check for new cards (default: `3000`) | | `pollingRate` | number (ms) | How often to check for new cards in polling mode (default: `3000`; ignored for direct proxy `addNote`/`addNotes` updates) |
| `proxy.enabled` | `true`, `false` | Enable local AnkiConnect-compatible proxy for push-based auto-enrichment (default: `false`) |
| `proxy.host` | string | Bind host for local AnkiConnect proxy (default: `127.0.0.1`) |
| `proxy.port` | number | Bind port for local AnkiConnect proxy (default: `8766`) |
| `proxy.upstreamUrl` | string (URL) | Upstream AnkiConnect URL that proxy forwards to (default: `http://127.0.0.1:8765`) |
| `tags` | array of strings | Tags automatically added to cards mined/updated by SubMiner (default: `['SubMiner']`; set `[]` to disable automatic tagging). | | `tags` | array of strings | Tags automatically added to cards mined/updated by SubMiner (default: `['SubMiner']`; set `[]` to disable automatic tagging). |
| `deck` | string | Anki deck to monitor for new cards | | `deck` | string | Anki deck to monitor for new cards |
| `ankiConnect.nPlusOne.decks` | array of strings | Decks used for N+1 known-word cache lookups. When omitted/empty, falls back to `ankiConnect.deck`. | | `ankiConnect.nPlusOne.decks` | array of strings | Decks used for N+1 known-word cache lookups. When omitted/empty, falls back to `ankiConnect.deck`. |
@@ -340,20 +350,6 @@ Control whether the overlay automatically becomes visible when it connects to mp
The mpv plugin controls startup overlay visibility via `auto_start_visible_overlay` in `subminer.conf`. The mpv plugin controls startup overlay visibility via `auto_start_visible_overlay` in `subminer.conf`.
### Visible Overlay Subtitle Binding
Control whether toggling the visible overlay also toggles MPV subtitle visibility:
```json
{
"bind_visible_overlay_to_mpv_sub_visibility": true
}
```
| Option | Values | Description |
| -------------------------------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bind_visible_overlay_to_mpv_sub_visibility` | `true`, `false` | When `true` (default), visible overlay hides MPV primary/secondary subtitles and restores them when hidden. When `false`, visible overlay toggles do not change MPV subtitle visibility. |
### Auto Subtitle Sync ### Auto Subtitle Sync
Sync the active subtitle track using `alass` (preferred) or `ffsubsync`: Sync the active subtitle track using `alass` (preferred) or `ffsubsync`:

View File

@@ -650,7 +650,7 @@ test('warns and ignores unknown top-level config keys', () => {
assert.ok(warnings.some((warning) => warning.path === 'unknownFeatureFlag')); assert.ok(warnings.some((warning) => warning.path === 'unknownFeatureFlag'));
}); });
test('parses global shortcuts and startup visibility flags', () => { test('parses global shortcuts and startup settings', () => {
const dir = makeTempDir(); const dir = makeTempDir();
fs.writeFileSync( fs.writeFileSync(
path.join(dir, 'config.jsonc'), path.join(dir, 'config.jsonc'),
@@ -659,7 +659,6 @@ test('parses global shortcuts and startup visibility flags', () => {
"toggleVisibleOverlayGlobal": "Alt+Shift+U", "toggleVisibleOverlayGlobal": "Alt+Shift+U",
"openJimaku": "Ctrl+Alt+J" "openJimaku": "Ctrl+Alt+J"
}, },
"bind_visible_overlay_to_mpv_sub_visibility": false,
"youtubeSubgen": { "youtubeSubgen": {
"primarySubLanguages": ["ja", "jpn", "jp"] "primarySubLanguages": ["ja", "jpn", "jp"]
} }
@@ -671,7 +670,6 @@ test('parses global shortcuts and startup visibility flags', () => {
const config = service.getConfig(); const config = service.getConfig();
assert.equal(config.shortcuts.toggleVisibleOverlayGlobal, 'Alt+Shift+U'); assert.equal(config.shortcuts.toggleVisibleOverlayGlobal, 'Alt+Shift+U');
assert.equal(config.shortcuts.openJimaku, 'Ctrl+Alt+J'); assert.equal(config.shortcuts.openJimaku, 'Ctrl+Alt+J');
assert.equal(config.bind_visible_overlay_to_mpv_sub_visibility, false);
assert.deepEqual(config.youtubeSubgen.primarySubLanguages, ['ja', 'jpn', 'jp']); assert.deepEqual(config.youtubeSubgen.primarySubLanguages, ['ja', 'jpn', 'jp']);
}); });

View File

@@ -28,7 +28,6 @@ const {
secondarySub, secondarySub,
subsync, subsync,
auto_start_overlay, auto_start_overlay,
bind_visible_overlay_to_mpv_sub_visibility,
} = CORE_DEFAULT_CONFIG; } = CORE_DEFAULT_CONFIG;
const { ankiConnect, jimaku, anilist, jellyfin, discordPresence, youtubeSubgen } = const { ankiConnect, jimaku, anilist, jellyfin, discordPresence, youtubeSubgen } =
INTEGRATIONS_DEFAULT_CONFIG; INTEGRATIONS_DEFAULT_CONFIG;
@@ -47,7 +46,6 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
subsync, subsync,
subtitleStyle, subtitleStyle,
auto_start_overlay, auto_start_overlay,
bind_visible_overlay_to_mpv_sub_visibility,
jimaku, jimaku,
anilist, anilist,
jellyfin, jellyfin,

View File

@@ -11,7 +11,6 @@ export const CORE_DEFAULT_CONFIG: Pick<
| 'secondarySub' | 'secondarySub'
| 'subsync' | 'subsync'
| 'auto_start_overlay' | 'auto_start_overlay'
| 'bind_visible_overlay_to_mpv_sub_visibility'
> = { > = {
subtitlePosition: { yPercent: 10 }, subtitlePosition: { yPercent: 10 },
keybindings: [], keybindings: [],
@@ -52,5 +51,4 @@ export const CORE_DEFAULT_CONFIG: Pick<
ffmpeg_path: '', ffmpeg_path: '',
}, },
auto_start_overlay: false, auto_start_overlay: false,
bind_visible_overlay_to_mpv_sub_visibility: true,
}; };

View File

@@ -38,12 +38,5 @@ export function buildCoreConfigOptionRegistry(
defaultValue: defaultConfig.shortcuts.multiCopyTimeoutMs, defaultValue: defaultConfig.shortcuts.multiCopyTimeoutMs,
description: 'Timeout for multi-copy/mine modes.', description: 'Timeout for multi-copy/mine modes.',
}, },
{
path: 'bind_visible_overlay_to_mpv_sub_visibility',
kind: 'boolean',
defaultValue: defaultConfig.bind_visible_overlay_to_mpv_sub_visibility,
description:
'Link visible overlay toggles to MPV primary subtitle visibility.',
},
]; ];
} }

View File

@@ -8,14 +8,6 @@ const CORE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
], ],
key: 'auto_start_overlay', key: 'auto_start_overlay',
}, },
{
title: 'Visible Overlay Subtitle Binding',
description: [
'Control whether visible overlay toggles also toggle MPV subtitle visibility.',
'When enabled, visible overlay hides MPV subtitles; when disabled, MPV subtitles are left unchanged.',
],
key: 'bind_visible_overlay_to_mpv_sub_visibility',
},
{ {
title: 'Texthooker Server', title: 'Texthooker Server',
description: ['Control whether browser opens automatically for texthooker.'], description: ['Control whether browser opens automatically for texthooker.'],

View File

@@ -13,16 +13,4 @@ export function applyTopLevelConfig(context: ResolveContext): void {
if (asBoolean(src.auto_start_overlay) !== undefined) { if (asBoolean(src.auto_start_overlay) !== undefined) {
resolved.auto_start_overlay = src.auto_start_overlay as boolean; resolved.auto_start_overlay = src.auto_start_overlay as boolean;
} }
if (asBoolean(src.bind_visible_overlay_to_mpv_sub_visibility) !== undefined) {
resolved.bind_visible_overlay_to_mpv_sub_visibility =
src.bind_visible_overlay_to_mpv_sub_visibility as boolean;
} else if (src.bind_visible_overlay_to_mpv_sub_visibility !== undefined) {
warn(
'bind_visible_overlay_to_mpv_sub_visibility',
src.bind_visible_overlay_to_mpv_sub_visibility,
resolved.bind_visible_overlay_to_mpv_sub_visibility,
'Expected boolean.',
);
}
} }

View File

@@ -25,10 +25,10 @@ export { cycleSecondarySubMode } from './subtitle-position';
export { export {
isAutoUpdateEnabledRuntime, isAutoUpdateEnabledRuntime,
shouldAutoInitializeOverlayRuntimeFromConfig, shouldAutoInitializeOverlayRuntimeFromConfig,
shouldBindVisibleOverlayToMpvSubVisibility,
} from './startup'; } from './startup';
export { openYomitanSettingsWindow } from './yomitan-settings'; export { openYomitanSettingsWindow } from './yomitan-settings';
export { createTokenizerDepsRuntime, tokenizeSubtitle } from './tokenizer'; export { createTokenizerDepsRuntime, tokenizeSubtitle } from './tokenizer';
export { syncYomitanDefaultAnkiServer } from './tokenizer/yomitan-parser-runtime';
export { createSubtitleProcessingController } from './subtitle-processing-controller'; export { createSubtitleProcessingController } from './subtitle-processing-controller';
export { createFrequencyDictionaryLookup } from './frequency-dictionary'; export { createFrequencyDictionaryLookup } from './frequency-dictionary';
export { createJlptVocabularyLookup } from './jlpt-vocab'; export { createJlptVocabularyLookup } from './jlpt-vocab';

View File

@@ -121,7 +121,6 @@ test('dispatchMpvProtocolMessage emits subtitle text on property change', async
test('dispatchMpvProtocolMessage enforces sub-visibility hidden when overlay suppression is enabled', async () => { test('dispatchMpvProtocolMessage enforces sub-visibility hidden when overlay suppression is enabled', async () => {
const { deps, state } = createDeps({ const { deps, state } = createDeps({
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
isVisibleOverlayVisible: () => true, isVisibleOverlayVisible: () => true,
}); });
@@ -130,15 +129,22 @@ test('dispatchMpvProtocolMessage enforces sub-visibility hidden when overlay sup
deps, deps,
); );
assert.deepEqual(state.commands.pop(), { assert.deepEqual(state.commands, [
command: ['set_property', 'sub-visibility', 'no'], {
}); command: ['set_property', 'sub-visibility', false],
},
{
command: ['set_property', 'sub-visibility', 'no'],
},
{
command: ['set', 'sub-visibility', 'no'],
},
]);
}); });
test('dispatchMpvProtocolMessage skips sub-visibility suppression when overlay binding is disabled', async () => { test('dispatchMpvProtocolMessage skips sub-visibility suppression when overlay is hidden', async () => {
const { deps, state } = createDeps({ const { deps, state } = createDeps({
shouldBindVisibleOverlayToMpvSubVisibility: () => false, isVisibleOverlayVisible: () => false,
isVisibleOverlayVisible: () => true,
}); });
await dispatchMpvProtocolMessage( await dispatchMpvProtocolMessage(

View File

@@ -48,7 +48,6 @@ export interface MpvProtocolHandleMessageDeps {
}; };
getSubtitleMetrics: () => MpvSubtitleRenderMetrics; getSubtitleMetrics: () => MpvSubtitleRenderMetrics;
isVisibleOverlayVisible: () => boolean; isVisibleOverlayVisible: () => boolean;
shouldBindVisibleOverlayToMpvSubVisibility?: () => boolean;
emitSubtitleChange: (payload: { text: string; isOverlayVisible: boolean }) => void; emitSubtitleChange: (payload: { text: string; isOverlayVisible: boolean }) => void;
emitSubtitleAssChange: (payload: { text: string }) => void; emitSubtitleAssChange: (payload: { text: string }) => void;
emitSubtitleTiming: (payload: { text: string; start: number; end: number }) => void; emitSubtitleTiming: (payload: { text: string; start: number; end: number }) => void;
@@ -218,12 +217,10 @@ export async function dispatchMpvProtocolMessage(
subScaleByWindow: asBoolean(msg.data, deps.getSubtitleMetrics().subScaleByWindow), subScaleByWindow: asBoolean(msg.data, deps.getSubtitleMetrics().subScaleByWindow),
}); });
} else if (msg.name === 'sub-visibility') { } else if (msg.name === 'sub-visibility') {
if ( if (deps.isVisibleOverlayVisible() && asBoolean(msg.data, false)) {
deps.isVisibleOverlayVisible() && deps.sendCommand({ command: ['set_property', 'sub-visibility', false] });
asBoolean(msg.data, false) &&
(deps.shouldBindVisibleOverlayToMpvSubVisibility?.() ?? true)
) {
deps.sendCommand({ command: ['set_property', 'sub-visibility', 'no'] }); deps.sendCommand({ command: ['set_property', 'sub-visibility', 'no'] });
deps.sendCommand({ command: ['set', 'sub-visibility', 'no'] });
} }
} else if (msg.name === 'sub-use-margins') { } else if (msg.name === 'sub-use-margins') {
deps.emitSubtitleMetricsChange({ deps.emitSubtitleMetricsChange({

View File

@@ -13,7 +13,6 @@ function makeDeps(overrides: Partial<MpvIpcClientProtocolDeps> = {}): MpvIpcClie
getResolvedConfig: () => ({}) as any, getResolvedConfig: () => ({}) as any,
autoStartOverlay: false, autoStartOverlay: false,
setOverlayVisible: () => {}, setOverlayVisible: () => {},
shouldBindVisibleOverlayToMpvSubVisibility: () => false,
isVisibleOverlayVisible: () => false, isVisibleOverlayVisible: () => false,
getReconnectTimer: () => null, getReconnectTimer: () => null,
setReconnectTimer: () => {}, setReconnectTimer: () => {},
@@ -311,7 +310,6 @@ test('MpvIpcClient connect does not force primary subtitle visibility from bindi
const client = new MpvIpcClient( const client = new MpvIpcClient(
'/tmp/mpv.sock', '/tmp/mpv.sock',
makeDeps({ makeDeps({
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
isVisibleOverlayVisible: () => true, isVisibleOverlayVisible: () => true,
}), }),
); );
@@ -332,6 +330,29 @@ test('MpvIpcClient connect does not force primary subtitle visibility from bindi
assert.equal(hasPrimaryVisibilityMutation, false); assert.equal(hasPrimaryVisibilityMutation, false);
}); });
test('MpvIpcClient setSubVisibility writes compatibility commands for visibility toggle', () => {
const commands: unknown[] = [];
const client = new MpvIpcClient('/tmp/mpv.sock', makeDeps());
(client as any).send = (payload: unknown) => {
commands.push(payload);
return true;
};
client.setSubVisibility(false);
assert.deepEqual(commands, [
{
command: ['set_property', 'sub-visibility', false],
},
{
command: ['set_property', 'sub-visibility', 'no'],
},
{
command: ['set', 'sub-visibility', 'no'],
},
]);
});
test('MpvIpcClient captures and disables secondary subtitle visibility on request', async () => { test('MpvIpcClient captures and disables secondary subtitle visibility on request', async () => {
const commands: unknown[] = []; const commands: unknown[] = [];
const client = new MpvIpcClient('/tmp/mpv.sock', makeDeps()); const client = new MpvIpcClient('/tmp/mpv.sock', makeDeps());

View File

@@ -99,7 +99,6 @@ export interface MpvIpcClientProtocolDeps {
getResolvedConfig: () => Config; getResolvedConfig: () => Config;
autoStartOverlay: boolean; autoStartOverlay: boolean;
setOverlayVisible: (visible: boolean) => void; setOverlayVisible: (visible: boolean) => void;
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
isVisibleOverlayVisible: () => boolean; isVisibleOverlayVisible: () => boolean;
getReconnectTimer: () => ReturnType<typeof setTimeout> | null; getReconnectTimer: () => ReturnType<typeof setTimeout> | null;
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => void; setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => void;
@@ -297,8 +296,6 @@ export class MpvIpcClient implements MpvClient {
getResolvedConfig: () => this.deps.getResolvedConfig(), getResolvedConfig: () => this.deps.getResolvedConfig(),
getSubtitleMetrics: () => this.mpvSubtitleRenderMetrics, getSubtitleMetrics: () => this.mpvSubtitleRenderMetrics,
isVisibleOverlayVisible: () => this.deps.isVisibleOverlayVisible(), isVisibleOverlayVisible: () => this.deps.isVisibleOverlayVisible(),
shouldBindVisibleOverlayToMpvSubVisibility: () =>
this.deps.shouldBindVisibleOverlayToMpvSubVisibility(),
emitSubtitleChange: (payload) => { emitSubtitleChange: (payload) => {
this.emit('subtitle-change', payload); this.emit('subtitle-change', payload);
}, },
@@ -474,6 +471,9 @@ export class MpvIpcClient implements MpvClient {
setSubVisibility(visible: boolean): void { setSubVisibility(visible: boolean): void {
const value = visible ? 'yes' : 'no'; const value = visible ? 'yes' : 'no';
this.send({
command: ['set_property', 'sub-visibility', visible],
});
this.send({ this.send({
command: ['set_property', 'sub-visibility', value], command: ['set_property', 'sub-visibility', value],
}); });

View File

@@ -3,12 +3,10 @@ import assert from 'node:assert/strict';
import { import {
isAutoUpdateEnabledRuntime, isAutoUpdateEnabledRuntime,
shouldAutoInitializeOverlayRuntimeFromConfig, shouldAutoInitializeOverlayRuntimeFromConfig,
shouldBindVisibleOverlayToMpvSubVisibility,
} from './startup'; } from './startup';
const BASE_CONFIG = { const BASE_CONFIG = {
auto_start_overlay: false, auto_start_overlay: false,
bind_visible_overlay_to_mpv_sub_visibility: true,
ankiConnect: { ankiConnect: {
behavior: { behavior: {
autoUpdateNewCards: true, autoUpdateNewCards: true,
@@ -27,17 +25,6 @@ test('shouldAutoInitializeOverlayRuntimeFromConfig respects auto start', () => {
); );
}); });
test('shouldBindVisibleOverlayToMpvSubVisibility returns config value', () => {
assert.equal(shouldBindVisibleOverlayToMpvSubVisibility(BASE_CONFIG), true);
assert.equal(
shouldBindVisibleOverlayToMpvSubVisibility({
...BASE_CONFIG,
bind_visible_overlay_to_mpv_sub_visibility: false,
}),
false,
);
});
test('isAutoUpdateEnabledRuntime prefers runtime option and falls back to config', () => { test('isAutoUpdateEnabledRuntime prefers runtime option and falls back to config', () => {
assert.equal( assert.equal(
isAutoUpdateEnabledRuntime(BASE_CONFIG, { isAutoUpdateEnabledRuntime(BASE_CONFIG, {

View File

@@ -18,7 +18,6 @@ interface RuntimeAutoUpdateOptionManagerLike {
export interface RuntimeConfigLike { export interface RuntimeConfigLike {
auto_start_overlay?: boolean; auto_start_overlay?: boolean;
bind_visible_overlay_to_mpv_sub_visibility: boolean;
ankiConnect?: { ankiConnect?: {
behavior?: { behavior?: {
autoUpdateNewCards?: boolean; autoUpdateNewCards?: boolean;
@@ -156,10 +155,6 @@ export function shouldAutoInitializeOverlayRuntimeFromConfig(config: RuntimeConf
return config.auto_start_overlay === true; return config.auto_start_overlay === true;
} }
export function shouldBindVisibleOverlayToMpvSubVisibility(config: RuntimeConfigLike): boolean {
return config.bind_visible_overlay_to_mpv_sub_visibility;
}
export function isAutoUpdateEnabledRuntime( export function isAutoUpdateEnabledRuntime(
config: ResolvedConfig | RuntimeConfigLike, config: ResolvedConfig | RuntimeConfigLike,
runtimeOptionsManager: RuntimeAutoUpdateOptionManagerLike | null, runtimeOptionsManager: RuntimeAutoUpdateOptionManagerLike | null,

View File

@@ -106,6 +106,7 @@ import type { CliArgs, CliCommandSource } from './cli/args';
import { printHelp } from './cli/help'; import { printHelp } from './cli/help';
import { import {
buildConfigParseErrorDetails, buildConfigParseErrorDetails,
buildConfigWarningDialogDetails,
buildConfigWarningNotificationBody, buildConfigWarningNotificationBody,
failStartupFromConfig, failStartupFromConfig,
} from './main/config-validation'; } from './main/config-validation';
@@ -353,6 +354,7 @@ import {
resolveJellyfinPlaybackPlanRuntime, resolveJellyfinPlaybackPlanRuntime,
runStartupBootstrapRuntime, runStartupBootstrapRuntime,
saveSubtitlePosition as saveSubtitlePositionCore, saveSubtitlePosition as saveSubtitlePositionCore,
syncYomitanDefaultAnkiServer as syncYomitanDefaultAnkiServerCore,
sendMpvCommandRuntime, sendMpvCommandRuntime,
setMpvSubVisibilityRuntime, setMpvSubVisibilityRuntime,
setOverlayDebugVisualizationEnabledRuntime, setOverlayDebugVisualizationEnabledRuntime,
@@ -758,10 +760,7 @@ const restoreOverlayMpvSubtitles = createRestoreOverlayMpvSubtitlesHandler({
}); });
function shouldSuppressMpvSubtitlesForOverlay(): boolean { function shouldSuppressMpvSubtitlesForOverlay(): boolean {
return ( return overlayManager.getVisibleOverlayVisible();
overlayManager.getVisibleOverlayVisible() &&
configDerivedRuntime.shouldBindVisibleOverlayToMpvSubVisibility()
);
} }
function syncOverlayMpvSubtitleSuppression(): void { function syncOverlayMpvSubtitleSuppression(): void {
@@ -961,6 +960,12 @@ const buildConfigHotReloadRuntimeMainDepsHandler = createBuildConfigHotReloadRun
showDesktopNotification('SubMiner', { showDesktopNotification('SubMiner', {
body: buildConfigWarningNotificationBody(configPath, warnings), body: buildConfigWarningNotificationBody(configPath, warnings),
}); });
if (process.platform === 'darwin') {
dialog.showErrorBox(
'SubMiner config validation warning',
buildConfigWarningDialogDetails(configPath, warnings),
);
}
}, },
}, },
); );
@@ -1931,6 +1936,10 @@ const { reloadConfig: reloadConfigHandler, appReadyRuntimeRunner } = composeAppR
logInfo: (message) => appLogger.logInfo(message), logInfo: (message) => appLogger.logInfo(message),
logWarning: (message) => appLogger.logWarning(message), logWarning: (message) => appLogger.logWarning(message),
showDesktopNotification: (title, options) => showDesktopNotification(title, options), showDesktopNotification: (title, options) => showDesktopNotification(title, options),
showConfigWarningsDialog:
process.platform === 'darwin'
? (title, details) => dialog.showErrorBox(title, details)
: undefined,
startConfigHotReload: () => configHotReloadRuntime.start(), startConfigHotReload: () => configHotReloadRuntime.start(),
refreshAnilistClientSecretState: (options) => refreshAnilistClientSecretState(options), refreshAnilistClientSecretState: (options) => refreshAnilistClientSecretState(options),
failHandlers: { failHandlers: {
@@ -2203,8 +2212,6 @@ const {
getResolvedConfig: () => getResolvedConfig(), getResolvedConfig: () => getResolvedConfig(),
isAutoStartOverlayEnabled: () => appState.autoStartOverlay, isAutoStartOverlayEnabled: () => appState.autoStartOverlay,
setOverlayVisible: (visible: boolean) => setOverlayVisible(visible), setOverlayVisible: (visible: boolean) => setOverlayVisible(visible),
shouldBindVisibleOverlayToMpvSubVisibility: () =>
configDerivedRuntime.shouldBindVisibleOverlayToMpvSubVisibility(),
isVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), isVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
getReconnectTimer: () => appState.reconnectTimer, getReconnectTimer: () => appState.reconnectTimer,
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => { setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => {
@@ -2372,11 +2379,70 @@ const enforceOverlayLayerOrder = createEnforceOverlayLayerOrderHandler(
); );
async function loadYomitanExtension(): Promise<Extension | null> { async function loadYomitanExtension(): Promise<Extension | null> {
return yomitanExtensionRuntime.loadYomitanExtension(); const extension = await yomitanExtensionRuntime.loadYomitanExtension();
if (extension) {
await syncYomitanDefaultProfileAnkiServer();
}
return extension;
} }
async function ensureYomitanExtensionLoaded(): Promise<Extension | null> { async function ensureYomitanExtensionLoaded(): Promise<Extension | null> {
return yomitanExtensionRuntime.ensureYomitanExtensionLoaded(); const extension = await yomitanExtensionRuntime.ensureYomitanExtensionLoaded();
if (extension) {
await syncYomitanDefaultProfileAnkiServer();
}
return extension;
}
let lastSyncedYomitanAnkiServer: string | null = null;
function getPreferredYomitanAnkiServerUrl(): string {
const config = getResolvedConfig().ankiConnect;
if (config.proxy?.enabled) {
const host = config.proxy.host || '127.0.0.1';
const port = config.proxy.port || 8766;
return `http://${host}:${port}`;
}
return config.url;
}
async function syncYomitanDefaultProfileAnkiServer(): Promise<void> {
const targetUrl = getPreferredYomitanAnkiServerUrl().trim();
if (!targetUrl || targetUrl === lastSyncedYomitanAnkiServer) {
return;
}
const updated = await syncYomitanDefaultAnkiServerCore(
targetUrl,
{
getYomitanExt: () => appState.yomitanExt,
getYomitanParserWindow: () => appState.yomitanParserWindow,
setYomitanParserWindow: (window) => {
appState.yomitanParserWindow = window;
},
getYomitanParserReadyPromise: () => appState.yomitanParserReadyPromise,
setYomitanParserReadyPromise: (promise) => {
appState.yomitanParserReadyPromise = promise;
},
getYomitanParserInitPromise: () => appState.yomitanParserInitPromise,
setYomitanParserInitPromise: (promise) => {
appState.yomitanParserInitPromise = promise;
},
},
{
error: (message, ...args) => {
logger.error(message, ...args);
},
info: (message, ...args) => {
logger.info(message, ...args);
},
},
);
if (updated) {
logger.info(`Yomitan default profile Anki server set to ${targetUrl}`);
}
lastSyncedYomitanAnkiServer = targetUrl;
} }
function createOverlayWindow(kind: 'visible' | 'modal'): BrowserWindow { function createOverlayWindow(kind: 'visible' | 'modal'): BrowserWindow {
@@ -2999,20 +3065,32 @@ function ensureOverlayWindowsReadyForVisibilityActions(): void {
function setVisibleOverlayVisible(visible: boolean): void { function setVisibleOverlayVisible(visible: boolean): void {
ensureOverlayWindowsReadyForVisibilityActions(); ensureOverlayWindowsReadyForVisibilityActions();
if (visible) {
void ensureOverlayMpvSubtitlesHidden();
}
setVisibleOverlayVisibleHandler(visible); setVisibleOverlayVisibleHandler(visible);
syncOverlayMpvSubtitleSuppression(); syncOverlayMpvSubtitleSuppression();
} }
function toggleVisibleOverlay(): void { function toggleVisibleOverlay(): void {
ensureOverlayWindowsReadyForVisibilityActions(); ensureOverlayWindowsReadyForVisibilityActions();
if (!overlayManager.getVisibleOverlayVisible()) {
void ensureOverlayMpvSubtitlesHidden();
}
toggleVisibleOverlayHandler(); toggleVisibleOverlayHandler();
syncOverlayMpvSubtitleSuppression(); syncOverlayMpvSubtitleSuppression();
} }
function setOverlayVisible(visible: boolean): void { function setOverlayVisible(visible: boolean): void {
if (visible) {
void ensureOverlayMpvSubtitlesHidden();
}
setOverlayVisibleHandler(visible); setOverlayVisibleHandler(visible);
syncOverlayMpvSubtitleSuppression(); syncOverlayMpvSubtitleSuppression();
} }
function toggleOverlay(): void { function toggleOverlay(): void {
if (!overlayManager.getVisibleOverlayVisible()) {
void ensureOverlayMpvSubtitlesHidden();
}
toggleOverlayHandler(); toggleOverlayHandler();
syncOverlayMpvSubtitleSuppression(); syncOverlayMpvSubtitleSuppression();
} }

View File

@@ -92,7 +92,6 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
getResolvedConfig: () => ({ auto_start_overlay: false }), getResolvedConfig: () => ({ auto_start_overlay: false }),
isAutoStartOverlayEnabled: () => true, isAutoStartOverlayEnabled: () => true,
setOverlayVisible: () => {}, setOverlayVisible: () => {},
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
isVisibleOverlayVisible: () => false, isVisibleOverlayVisible: () => false,
getReconnectTimer: () => null, getReconnectTimer: () => null,
setReconnectTimer: () => {}, setReconnectTimer: () => {},

View File

@@ -7,7 +7,6 @@ import {
jimakuFetchJson as jimakuFetchJsonCore, jimakuFetchJson as jimakuFetchJsonCore,
resolveJimakuApiKey as resolveJimakuApiKeyCore, resolveJimakuApiKey as resolveJimakuApiKeyCore,
shouldAutoInitializeOverlayRuntimeFromConfig as shouldAutoInitializeOverlayRuntimeFromConfigCore, shouldAutoInitializeOverlayRuntimeFromConfig as shouldAutoInitializeOverlayRuntimeFromConfigCore,
shouldBindVisibleOverlayToMpvSubVisibility as shouldBindVisibleOverlayToMpvSubVisibilityCore,
} from '../../core/services'; } from '../../core/services';
export type ConfigDerivedRuntimeDeps = { export type ConfigDerivedRuntimeDeps = {
@@ -20,7 +19,6 @@ export type ConfigDerivedRuntimeDeps = {
export function createConfigDerivedRuntime(deps: ConfigDerivedRuntimeDeps): { export function createConfigDerivedRuntime(deps: ConfigDerivedRuntimeDeps): {
shouldAutoInitializeOverlayRuntimeFromConfig: () => boolean; shouldAutoInitializeOverlayRuntimeFromConfig: () => boolean;
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
isAutoUpdateEnabledRuntime: () => boolean; isAutoUpdateEnabledRuntime: () => boolean;
getJimakuLanguagePreference: () => JimakuLanguagePreference; getJimakuLanguagePreference: () => JimakuLanguagePreference;
getJimakuMaxEntryResults: () => number; getJimakuMaxEntryResults: () => number;
@@ -33,8 +31,6 @@ export function createConfigDerivedRuntime(deps: ConfigDerivedRuntimeDeps): {
return { return {
shouldAutoInitializeOverlayRuntimeFromConfig: () => shouldAutoInitializeOverlayRuntimeFromConfig: () =>
shouldAutoInitializeOverlayRuntimeFromConfigCore(deps.getResolvedConfig()), shouldAutoInitializeOverlayRuntimeFromConfigCore(deps.getResolvedConfig()),
shouldBindVisibleOverlayToMpvSubVisibility: () =>
shouldBindVisibleOverlayToMpvSubVisibilityCore(deps.getResolvedConfig()),
isAutoUpdateEnabledRuntime: () => isAutoUpdateEnabledRuntime: () =>
isAutoUpdateEnabledRuntimeCore(deps.getResolvedConfig(), deps.getRuntimeOptionsManager()), isAutoUpdateEnabledRuntimeCore(deps.getResolvedConfig(), deps.getRuntimeOptionsManager()),
getJimakuLanguagePreference: () => getJimakuLanguagePreference: () =>

View File

@@ -16,7 +16,6 @@ test('mpv runtime service main deps builder maps state and callbacks', () => {
getResolvedConfig: () => ({ mode: 'test' }), getResolvedConfig: () => ({ mode: 'test' }),
isAutoStartOverlayEnabled: () => true, isAutoStartOverlayEnabled: () => true,
setOverlayVisible: (visible) => calls.push(`overlay:${visible}`), setOverlayVisible: (visible) => calls.push(`overlay:${visible}`),
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
isVisibleOverlayVisible: () => false, isVisibleOverlayVisible: () => false,
getReconnectTimer: () => reconnectTimer, getReconnectTimer: () => reconnectTimer,
setReconnectTimer: (timer) => { setReconnectTimer: (timer) => {
@@ -29,7 +28,6 @@ test('mpv runtime service main deps builder maps state and callbacks', () => {
const deps = build(); const deps = build();
assert.equal(deps.socketPath, '/tmp/mpv.sock'); assert.equal(deps.socketPath, '/tmp/mpv.sock');
assert.equal(deps.options.autoStartOverlay, true); assert.equal(deps.options.autoStartOverlay, true);
assert.equal(deps.options.shouldBindVisibleOverlayToMpvSubVisibility(), true);
assert.equal(deps.options.isVisibleOverlayVisible(), false); assert.equal(deps.options.isVisibleOverlayVisible(), false);
assert.deepEqual(deps.options.getResolvedConfig(), { mode: 'test' }); assert.deepEqual(deps.options.getResolvedConfig(), { mode: 'test' });

View File

@@ -8,7 +8,6 @@ export function createBuildMpvClientRuntimeServiceFactoryDepsHandler<
getResolvedConfig: () => TResolvedConfig; getResolvedConfig: () => TResolvedConfig;
isAutoStartOverlayEnabled: () => boolean; isAutoStartOverlayEnabled: () => boolean;
setOverlayVisible: (visible: boolean) => void; setOverlayVisible: (visible: boolean) => void;
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
isVisibleOverlayVisible: () => boolean; isVisibleOverlayVisible: () => boolean;
getReconnectTimer: () => ReturnType<typeof setTimeout> | null; getReconnectTimer: () => ReturnType<typeof setTimeout> | null;
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => void; setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => void;
@@ -21,8 +20,6 @@ export function createBuildMpvClientRuntimeServiceFactoryDepsHandler<
getResolvedConfig: () => deps.getResolvedConfig(), getResolvedConfig: () => deps.getResolvedConfig(),
autoStartOverlay: deps.isAutoStartOverlayEnabled(), autoStartOverlay: deps.isAutoStartOverlayEnabled(),
setOverlayVisible: (visible: boolean) => deps.setOverlayVisible(visible), setOverlayVisible: (visible: boolean) => deps.setOverlayVisible(visible),
shouldBindVisibleOverlayToMpvSubVisibility: () =>
deps.shouldBindVisibleOverlayToMpvSubVisibility(),
isVisibleOverlayVisible: () => deps.isVisibleOverlayVisible(), isVisibleOverlayVisible: () => deps.isVisibleOverlayVisible(),
getReconnectTimer: () => deps.getReconnectTimer(), getReconnectTimer: () => deps.getReconnectTimer(),
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => deps.setReconnectTimer(timer), setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => deps.setReconnectTimer(timer),

View File

@@ -23,7 +23,6 @@ test('mpv runtime service factory constructs client, binds handlers, and connect
getResolvedConfig: () => ({}), getResolvedConfig: () => ({}),
autoStartOverlay: true, autoStartOverlay: true,
setOverlayVisible: () => {}, setOverlayVisible: () => {},
shouldBindVisibleOverlayToMpvSubVisibility: () => false,
isVisibleOverlayVisible: () => false, isVisibleOverlayVisible: () => false,
getReconnectTimer: () => null, getReconnectTimer: () => null,
setReconnectTimer: () => {}, setReconnectTimer: () => {},

View File

@@ -4,7 +4,6 @@ export type MpvClientRuntimeServiceOptions = {
getResolvedConfig: () => Config; getResolvedConfig: () => Config;
autoStartOverlay: boolean; autoStartOverlay: boolean;
setOverlayVisible: (visible: boolean) => void; setOverlayVisible: (visible: boolean) => void;
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
isVisibleOverlayVisible: () => boolean; isVisibleOverlayVisible: () => boolean;
getReconnectTimer: () => ReturnType<typeof setTimeout> | null; getReconnectTimer: () => ReturnType<typeof setTimeout> | null;
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => void; setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => void;

View File

@@ -192,6 +192,12 @@ export interface AnkiConnectConfig {
enabled?: boolean; enabled?: boolean;
url?: string; url?: string;
pollingRate?: number; pollingRate?: number;
proxy?: {
enabled?: boolean;
host?: string;
port?: number;
upstreamUrl?: string;
};
tags?: string[]; tags?: string[];
fields?: { fields?: {
audio?: string; audio?: string;
@@ -413,7 +419,6 @@ export interface Config {
subsync?: SubsyncConfig; subsync?: SubsyncConfig;
subtitleStyle?: SubtitleStyleConfig; subtitleStyle?: SubtitleStyleConfig;
auto_start_overlay?: boolean; auto_start_overlay?: boolean;
bind_visible_overlay_to_mpv_sub_visibility?: boolean;
jimaku?: JimakuConfig; jimaku?: JimakuConfig;
anilist?: AnilistConfig; anilist?: AnilistConfig;
jellyfin?: JellyfinConfig; jellyfin?: JellyfinConfig;
@@ -436,6 +441,12 @@ export interface ResolvedConfig {
enabled: boolean; enabled: boolean;
url: string; url: string;
pollingRate: number; pollingRate: number;
proxy: {
enabled: boolean;
host: string;
port: number;
upstreamUrl: string;
};
tags: string[]; tags: string[];
fields: { fields: {
audio: string; audio: string;
@@ -514,7 +525,6 @@ export interface ResolvedConfig {
}; };
}; };
auto_start_overlay: boolean; auto_start_overlay: boolean;
bind_visible_overlay_to_mpv_sub_visibility: boolean;
jimaku: JimakuConfig & { jimaku: JimakuConfig & {
apiBaseUrl: string; apiBaseUrl: string;
languagePreference: JimakuLanguagePreference; languagePreference: JimakuLanguagePreference;