mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-15 08:12:53 -07:00
fix: stabilize tray Yomitan settings
This commit is contained in:
@@ -4,6 +4,7 @@ area: tray
|
||||
- Kept the tray app running when closing tray-launched Yomitan settings.
|
||||
- Kept tray-launched Yomitan settings loading from blocking other tray actions.
|
||||
- Replaced the default native Yomitan settings menu with a close-only menu so closing settings does not quit the tray app.
|
||||
- Added an in-page close button for Yomitan settings on Linux window managers that do not show native decorations.
|
||||
- Disabled Yomitan's embedded popup preview in the tray-launched settings window to avoid renderer hangs during normal sidebar navigation.
|
||||
- Serialized copied Yomitan extension refreshes so startup cannot race itself and leave extension loading in an error state.
|
||||
- Fixed tray-launched session help focus handling so the modal can close without mpv running.
|
||||
|
||||
@@ -358,3 +358,89 @@ test('runAppReadyRuntime loads Yomitan before auto-initializing overlay runtime'
|
||||
assert.ok(calls.indexOf('init-overlay') !== -1);
|
||||
assert.ok(calls.indexOf('load-yomitan') < calls.indexOf('init-overlay'));
|
||||
});
|
||||
|
||||
test('runAppReadyRuntime reuses guarded Yomitan loader after scheduling startup warmups', async () => {
|
||||
const calls: string[] = [];
|
||||
|
||||
await runAppReadyRuntime({
|
||||
ensureDefaultConfigBootstrap: () => {
|
||||
calls.push('bootstrap');
|
||||
},
|
||||
loadSubtitlePosition: () => {
|
||||
calls.push('load-subtitle-position');
|
||||
},
|
||||
resolveKeybindings: () => {
|
||||
calls.push('resolve-keybindings');
|
||||
},
|
||||
createMpvClient: () => {
|
||||
calls.push('create-mpv');
|
||||
},
|
||||
reloadConfig: () => {
|
||||
calls.push('reload-config');
|
||||
},
|
||||
getResolvedConfig: () => ({
|
||||
websocket: { enabled: false },
|
||||
annotationWebsocket: { enabled: false },
|
||||
texthooker: { launchAtStartup: false },
|
||||
}),
|
||||
getConfigWarnings: () => [],
|
||||
logConfigWarning: () => {},
|
||||
setLogLevel: () => {
|
||||
calls.push('set-log-level');
|
||||
},
|
||||
initRuntimeOptionsManager: () => {
|
||||
calls.push('init-runtime-options');
|
||||
},
|
||||
setSecondarySubMode: () => {
|
||||
calls.push('set-secondary-sub-mode');
|
||||
},
|
||||
defaultSecondarySubMode: 'hover',
|
||||
defaultWebsocketPort: 0,
|
||||
defaultAnnotationWebsocketPort: 0,
|
||||
defaultTexthookerPort: 0,
|
||||
hasMpvWebsocketPlugin: () => false,
|
||||
startSubtitleWebsocket: () => {},
|
||||
startAnnotationWebsocket: () => {},
|
||||
startTexthooker: () => {},
|
||||
log: () => {
|
||||
calls.push('log');
|
||||
},
|
||||
createMecabTokenizerAndCheck: async () => {},
|
||||
createSubtitleTimingTracker: () => {
|
||||
calls.push('subtitle-timing');
|
||||
},
|
||||
createImmersionTracker: () => {
|
||||
calls.push('immersion');
|
||||
},
|
||||
startJellyfinRemoteSession: async () => {},
|
||||
loadYomitanExtension: async () => {
|
||||
calls.push('load-yomitan-direct');
|
||||
},
|
||||
ensureYomitanExtensionLoaded: async () => {
|
||||
calls.push('load-yomitan-guarded');
|
||||
},
|
||||
handleFirstRunSetup: async () => {
|
||||
calls.push('first-run');
|
||||
},
|
||||
prewarmSubtitleDictionaries: async () => {},
|
||||
startBackgroundWarmups: () => {
|
||||
calls.push('warmups');
|
||||
},
|
||||
texthookerOnlyMode: false,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig: () => false,
|
||||
setVisibleOverlayVisible: () => {
|
||||
calls.push('visible-overlay');
|
||||
},
|
||||
initializeOverlayRuntime: () => {
|
||||
calls.push('init-overlay');
|
||||
},
|
||||
handleInitialArgs: () => {
|
||||
calls.push('handle-initial-args');
|
||||
},
|
||||
shouldUseMinimalStartup: () => false,
|
||||
shouldSkipHeavyStartup: () => false,
|
||||
});
|
||||
|
||||
assert.equal(calls.includes('load-yomitan-direct'), false);
|
||||
assert.equal(calls.includes('load-yomitan-guarded'), true);
|
||||
});
|
||||
|
||||
@@ -131,6 +131,7 @@ export interface AppReadyRuntimeDeps {
|
||||
createImmersionTracker?: () => void;
|
||||
startJellyfinRemoteSession?: () => Promise<void>;
|
||||
loadYomitanExtension: () => Promise<void>;
|
||||
ensureYomitanExtensionLoaded?: () => Promise<void>;
|
||||
handleFirstRunSetup: () => Promise<void>;
|
||||
prewarmSubtitleDictionaries?: () => Promise<void>;
|
||||
startBackgroundWarmups: () => void;
|
||||
@@ -215,6 +216,8 @@ export function isAutoUpdateEnabledRuntime(
|
||||
export async function runAppReadyRuntime(deps: AppReadyRuntimeDeps): Promise<void> {
|
||||
const now = deps.now ?? (() => Date.now());
|
||||
const startupStartedAtMs = now();
|
||||
const ensureYomitanExtensionReady =
|
||||
deps.ensureYomitanExtensionLoaded ?? deps.loadYomitanExtension;
|
||||
deps.ensureDefaultConfigBootstrap();
|
||||
if (deps.shouldRunHeadlessInitialCommand?.()) {
|
||||
deps.reloadConfig();
|
||||
@@ -224,7 +227,7 @@ export async function runAppReadyRuntime(deps: AppReadyRuntimeDeps): Promise<voi
|
||||
} else {
|
||||
deps.createMpvClient();
|
||||
deps.createSubtitleTimingTracker();
|
||||
await deps.loadYomitanExtension();
|
||||
await ensureYomitanExtensionReady();
|
||||
deps.initializeOverlayRuntime();
|
||||
deps.handleInitialArgs();
|
||||
}
|
||||
@@ -238,7 +241,7 @@ export async function runAppReadyRuntime(deps: AppReadyRuntimeDeps): Promise<voi
|
||||
}
|
||||
|
||||
if (deps.shouldSkipHeavyStartup?.()) {
|
||||
await deps.loadYomitanExtension();
|
||||
await ensureYomitanExtensionReady();
|
||||
deps.reloadConfig();
|
||||
await deps.handleFirstRunSetup();
|
||||
deps.handleInitialArgs();
|
||||
@@ -248,7 +251,7 @@ export async function runAppReadyRuntime(deps: AppReadyRuntimeDeps): Promise<voi
|
||||
deps.logDebug?.('App-ready critical path started.');
|
||||
|
||||
if (deps.shouldSkipHeavyStartup?.()) {
|
||||
await deps.loadYomitanExtension();
|
||||
await ensureYomitanExtensionReady();
|
||||
deps.reloadConfig();
|
||||
await deps.handleFirstRunSetup();
|
||||
deps.handleInitialArgs();
|
||||
@@ -319,12 +322,12 @@ export async function runAppReadyRuntime(deps: AppReadyRuntimeDeps): Promise<voi
|
||||
if (deps.texthookerOnlyMode) {
|
||||
deps.log('Texthooker-only mode enabled; skipping overlay window.');
|
||||
} else if (deps.shouldAutoInitializeOverlayRuntimeFromConfig()) {
|
||||
await deps.loadYomitanExtension();
|
||||
await ensureYomitanExtensionReady();
|
||||
deps.setVisibleOverlayVisible(true);
|
||||
deps.initializeOverlayRuntime();
|
||||
} else {
|
||||
deps.log('Overlay runtime deferred: waiting for explicit overlay command.');
|
||||
await deps.loadYomitanExtension();
|
||||
await ensureYomitanExtensionReady();
|
||||
}
|
||||
|
||||
await deps.handleFirstRunSetup();
|
||||
|
||||
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
buildYomitanSettingsCloseButtonScript,
|
||||
buildYomitanSettingsWindowMenuTemplate,
|
||||
buildYomitanSettingsUrl,
|
||||
configureYomitanSettingsWindowChrome,
|
||||
@@ -43,6 +44,15 @@ test('yomitan settings close menu skips destroyed windows', () => {
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
|
||||
test('yomitan settings close button script installs an idempotent in-page close control', () => {
|
||||
const script = buildYomitanSettingsCloseButtonScript();
|
||||
|
||||
assert.match(script, /subminer-yomitan-settings-close/);
|
||||
assert.match(script, /aria-label', 'Close Yomitan settings'/);
|
||||
assert.match(script, /window\.close\(\)/);
|
||||
assert.match(script, /getElementById\(buttonId\)/);
|
||||
});
|
||||
|
||||
test('yomitan settings URL disables the embedded popup preview', () => {
|
||||
assert.equal(
|
||||
buildYomitanSettingsUrl('abc123'),
|
||||
|
||||
@@ -36,6 +36,73 @@ export function buildYomitanSettingsWindowMenuTemplate(
|
||||
];
|
||||
}
|
||||
|
||||
export function buildYomitanSettingsCloseButtonScript(): string {
|
||||
return `
|
||||
(() => {
|
||||
const buttonId = 'subminer-yomitan-settings-close';
|
||||
const styleId = 'subminer-yomitan-settings-close-style';
|
||||
if (document.getElementById(buttonId)) {
|
||||
return;
|
||||
}
|
||||
if (!document.getElementById(styleId)) {
|
||||
const style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
style.textContent = \`
|
||||
#\${buttonId} {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 2147483647;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.28);
|
||||
border-radius: 4px;
|
||||
background: rgba(24, 24, 24, 0.92);
|
||||
color: #f2f2f2;
|
||||
font: 22px/1 system-ui, sans-serif;
|
||||
cursor: pointer;
|
||||
}
|
||||
#\${buttonId}:hover {
|
||||
background: rgba(54, 54, 54, 0.96);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
#\${buttonId}:focus-visible {
|
||||
outline: 2px solid #8ab4f8;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
\`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
const button = document.createElement('button');
|
||||
button.id = buttonId;
|
||||
button.type = 'button';
|
||||
button.title = 'Close';
|
||||
button.setAttribute('aria-label', 'Close Yomitan settings');
|
||||
button.textContent = '\\u00d7';
|
||||
button.addEventListener('click', () => {
|
||||
window.close();
|
||||
});
|
||||
document.body.appendChild(button);
|
||||
})();
|
||||
`;
|
||||
}
|
||||
|
||||
export function installYomitanSettingsCloseButton(
|
||||
settingsWindow: Pick<BrowserWindow, 'isDestroyed' | 'webContents'>,
|
||||
): void {
|
||||
if (settingsWindow.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
settingsWindow.webContents
|
||||
.executeJavaScript(buildYomitanSettingsCloseButtonScript())
|
||||
.catch((error: Error) => {
|
||||
logger.warn('Failed to install Yomitan settings close button:', error.message);
|
||||
});
|
||||
}
|
||||
|
||||
export function configureYomitanSettingsWindowChrome(
|
||||
settingsWindow: Pick<BrowserWindow, 'close' | 'isDestroyed' | 'setAutoHideMenuBar' | 'setMenu'>,
|
||||
buildMenu: (template: MenuItemConstructorOptions[]) => Menu = (template) =>
|
||||
@@ -133,6 +200,7 @@ export function openYomitanSettingsWindow(options: OpenYomitanSettingsWindowOpti
|
||||
|
||||
settingsWindow.webContents.on('did-finish-load', () => {
|
||||
logger.info('Settings page loaded successfully');
|
||||
installYomitanSettingsCloseButton(settingsWindow);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -3854,6 +3854,9 @@ const { appReadyRuntimeRunner } = composeAppReadyRuntime({
|
||||
loadYomitanExtension: async () => {
|
||||
await loadYomitanExtension();
|
||||
},
|
||||
ensureYomitanExtensionLoaded: async () => {
|
||||
await ensureYomitanExtensionLoaded();
|
||||
},
|
||||
handleFirstRunSetup: async () => {
|
||||
const snapshot = await firstRunSetupService.ensureSetupStateInitialized();
|
||||
appState.firstRunSetupCompleted = snapshot.state.status === 'completed';
|
||||
|
||||
@@ -44,6 +44,7 @@ export interface AppReadyRuntimeDepsFactoryInput {
|
||||
createImmersionTracker?: AppReadyRuntimeDeps['createImmersionTracker'];
|
||||
startJellyfinRemoteSession?: AppReadyRuntimeDeps['startJellyfinRemoteSession'];
|
||||
loadYomitanExtension: AppReadyRuntimeDeps['loadYomitanExtension'];
|
||||
ensureYomitanExtensionLoaded?: AppReadyRuntimeDeps['ensureYomitanExtensionLoaded'];
|
||||
handleFirstRunSetup: AppReadyRuntimeDeps['handleFirstRunSetup'];
|
||||
prewarmSubtitleDictionaries?: AppReadyRuntimeDeps['prewarmSubtitleDictionaries'];
|
||||
startBackgroundWarmups: AppReadyRuntimeDeps['startBackgroundWarmups'];
|
||||
@@ -109,6 +110,7 @@ export function createAppReadyRuntimeDeps(
|
||||
createImmersionTracker: params.createImmersionTracker,
|
||||
startJellyfinRemoteSession: params.startJellyfinRemoteSession,
|
||||
loadYomitanExtension: params.loadYomitanExtension,
|
||||
ensureYomitanExtensionLoaded: params.ensureYomitanExtensionLoaded,
|
||||
handleFirstRunSetup: params.handleFirstRunSetup,
|
||||
prewarmSubtitleDictionaries: params.prewarmSubtitleDictionaries,
|
||||
startBackgroundWarmups: params.startBackgroundWarmups,
|
||||
|
||||
@@ -36,6 +36,9 @@ test('app-ready main deps builder returns mapped app-ready runtime deps', async
|
||||
loadYomitanExtension: async () => {
|
||||
calls.push('load-yomitan');
|
||||
},
|
||||
ensureYomitanExtensionLoaded: async () => {
|
||||
calls.push('ensure-yomitan');
|
||||
},
|
||||
handleFirstRunSetup: async () => {
|
||||
calls.push('handle-first-run-setup');
|
||||
},
|
||||
@@ -67,6 +70,7 @@ test('app-ready main deps builder returns mapped app-ready runtime deps', async
|
||||
onReady.createMpvClient();
|
||||
await onReady.createMecabTokenizerAndCheck();
|
||||
await onReady.loadYomitanExtension();
|
||||
await onReady.ensureYomitanExtensionLoaded?.();
|
||||
await onReady.handleFirstRunSetup();
|
||||
await onReady.prewarmSubtitleDictionaries?.();
|
||||
onReady.startBackgroundWarmups();
|
||||
@@ -79,6 +83,7 @@ test('app-ready main deps builder returns mapped app-ready runtime deps', async
|
||||
'create-mpv-client',
|
||||
'create-mecab',
|
||||
'load-yomitan',
|
||||
'ensure-yomitan',
|
||||
'handle-first-run-setup',
|
||||
'prewarm-dicts',
|
||||
'start-warmups',
|
||||
|
||||
@@ -27,6 +27,7 @@ export function createBuildAppReadyRuntimeMainDepsHandler(deps: AppReadyRuntimeD
|
||||
createImmersionTracker: deps.createImmersionTracker,
|
||||
startJellyfinRemoteSession: deps.startJellyfinRemoteSession,
|
||||
loadYomitanExtension: deps.loadYomitanExtension,
|
||||
ensureYomitanExtensionLoaded: deps.ensureYomitanExtensionLoaded,
|
||||
handleFirstRunSetup: deps.handleFirstRunSetup,
|
||||
prewarmSubtitleDictionaries: deps.prewarmSubtitleDictionaries,
|
||||
startBackgroundWarmups: deps.startBackgroundWarmups,
|
||||
|
||||
Reference in New Issue
Block a user