Harden Yomitan settings open flow for external profile mode

- Return status from `openYomitanSettings` and show user-facing warning when external read-only profile mode blocks settings
- Thread `yomitanSession` through settings runtime/opener deps so settings window uses the active session
- Expand tests for session forwarding and external profile path propagation
- Move AniList setup/token/CLI docs into the AniList section in configuration docs
This commit is contained in:
2026-03-11 18:32:16 -07:00
parent 1a2745579e
commit c82d247e2b
8 changed files with 84 additions and 41 deletions

View File

@@ -23,6 +23,7 @@ import {
shell,
protocol,
Extension,
Session,
Menu,
nativeImage,
Tray,
@@ -1849,8 +1850,9 @@ const openFirstRunSetupWindowHandler = createOpenFirstRunSetupWindowHandler({
return;
}
if (submission.action === 'open-yomitan-settings') {
openYomitanSettings();
firstRunSetupMessage = 'Opened Yomitan settings. Install dictionaries, then refresh status.';
firstRunSetupMessage = openYomitanSettings()
? 'Opened Yomitan settings. Install dictionaries, then refresh status.'
: 'Yomitan settings are unavailable while external read-only profile mode is enabled.';
return;
}
if (submission.action === 'refresh') {
@@ -3022,7 +3024,7 @@ function getPreferredYomitanAnkiServerUrl(): string {
}
function getConfiguredExternalYomitanProfilePath(): string {
return getResolvedConfig().yomitan.externalProfilePath.trim();
return configuredExternalYomitanProfilePath;
}
function isYomitanExternalReadOnlyMode(): boolean {
@@ -3112,14 +3114,19 @@ function initializeOverlayRuntime(): void {
syncOverlayMpvSubtitleSuppression();
}
function openYomitanSettings(): void {
function openYomitanSettings(): boolean {
if (isYomitanExternalReadOnlyMode()) {
const message =
'Yomitan settings unavailable while using read-only external-profile mode.';
logger.warn(
'Yomitan settings window disabled while yomitan.externalProfilePath is configured because external profile mode is read-only.',
);
return;
showDesktopNotification('SubMiner', { body: message });
showMpvOsd(message);
return false;
}
openYomitanSettingsHandler();
return true;
}
const {
@@ -3623,6 +3630,7 @@ const { ensureTray: ensureTrayHandler, destroyTray: destroyTrayHandler } =
},
buildMenuFromTemplate: (template) => Menu.buildFromTemplate(template),
});
const configuredExternalYomitanProfilePath = getResolvedConfig().yomitan.externalProfilePath.trim();
const yomitanExtensionRuntime = createYomitanExtensionRuntime({
loadYomitanExtensionCore,
userDataPath: USER_DATA_PATH,
@@ -3684,12 +3692,18 @@ const { initializeOverlayRuntime: initializeOverlayRuntimeHandler } =
});
const { openYomitanSettings: openYomitanSettingsHandler } = createYomitanSettingsRuntime({
ensureYomitanExtensionLoaded: () => ensureYomitanExtensionLoaded(),
openYomitanSettingsWindow: ({ yomitanExt, getExistingWindow, setWindow }) => {
getYomitanSession: () => appState.yomitanSession,
openYomitanSettingsWindow: ({
yomitanExt,
getExistingWindow,
setWindow,
yomitanSession,
}) => {
openYomitanSettingsWindow({
yomitanExt: yomitanExt as Extension,
getExistingWindow: () => getExistingWindow() as BrowserWindow | null,
setWindow: (window) => setWindow(window as BrowserWindow | null),
yomitanSession: appState.yomitanSession,
yomitanSession: (yomitanSession as Session | null | undefined) ?? appState.yomitanSession,
onWindowClosed: () => {
if (appState.yomitanParserWindow) {
clearYomitanParserCachesForWindow(appState.yomitanParserWindow);

View File

@@ -68,15 +68,19 @@ test('open yomitan settings main deps map async open callbacks', async () => {
const calls: string[] = [];
let currentWindow: unknown = null;
const extension = { id: 'ext' };
const yomitanSession = { id: 'session' };
const deps = createBuildOpenYomitanSettingsMainDepsHandler({
ensureYomitanExtensionLoaded: async () => extension,
openYomitanSettingsWindow: ({ yomitanExt }) =>
calls.push(`open:${(yomitanExt as { id: string }).id}`),
openYomitanSettingsWindow: ({ yomitanExt, yomitanSession: forwardedSession }) =>
calls.push(
`open:${(yomitanExt as { id: string }).id}:${(forwardedSession as { id: string } | null)?.id ?? 'null'}`,
),
getExistingWindow: () => currentWindow,
setWindow: (window) => {
currentWindow = window;
calls.push('set-window');
},
getYomitanSession: () => yomitanSession,
logWarn: (message) => calls.push(`warn:${message}`),
logError: (message) => calls.push(`error:${message}`),
})();
@@ -88,9 +92,10 @@ test('open yomitan settings main deps map async open callbacks', async () => {
yomitanExt: extension,
getExistingWindow: () => deps.getExistingWindow(),
setWindow: (window) => deps.setWindow(window),
yomitanSession: deps.getYomitanSession(),
});
deps.logWarn('warn');
deps.logError('error', new Error('boom'));
assert.deepEqual(calls, ['set-window', 'open:ext', 'warn:warn', 'error:error']);
assert.deepEqual(calls, ['set-window', 'open:ext:session', 'warn:warn', 'error:error']);
assert.deepEqual(currentWindow, { id: 'win' });
});

View File

@@ -66,10 +66,12 @@ export function createBuildOpenYomitanSettingsMainDepsHandler<TYomitanExt, TWind
yomitanExt: TYomitanExt;
getExistingWindow: () => TWindow | null;
setWindow: (window: TWindow | null) => void;
yomitanSession?: unknown | null;
onWindowClosed?: () => void;
}) => void;
getExistingWindow: () => TWindow | null;
setWindow: (window: TWindow | null) => void;
getYomitanSession?: () => unknown | null;
logWarn: (message: string) => void;
logError: (message: string, error: unknown) => void;
}) {
@@ -79,10 +81,12 @@ export function createBuildOpenYomitanSettingsMainDepsHandler<TYomitanExt, TWind
yomitanExt: TYomitanExt;
getExistingWindow: () => TWindow | null;
setWindow: (window: TWindow | null) => void;
yomitanSession?: unknown | null;
onWindowClosed?: () => void;
}) => deps.openYomitanSettingsWindow(params),
getExistingWindow: () => deps.getExistingWindow(),
setWindow: (window: TWindow | null) => deps.setWindow(window),
getYomitanSession: () => deps.getYomitanSession?.() ?? null,
logWarn: (message: string) => deps.logWarn(message),
logError: (message: string, error: unknown) => deps.logError(message, error),
});

View File

@@ -10,6 +10,7 @@ test('yomitan extension runtime reuses in-flight ensure load and clears it after
let readyPromise: Promise<void> | null = null;
let initPromise: Promise<boolean> | null = null;
let yomitanSession: unknown = null;
let receivedExternalProfilePath = '';
let loadCalls = 0;
const releaseLoadState: { releaseLoad: ((value: Extension | null) => void) | null } = {
releaseLoad: null,
@@ -18,9 +19,11 @@ test('yomitan extension runtime reuses in-flight ensure load and clears it after
const runtime = createYomitanExtensionRuntime({
loadYomitanExtensionCore: async (options) => {
loadCalls += 1;
receivedExternalProfilePath = options.externalProfilePath ?? '';
options.setYomitanParserWindow(null);
options.setYomitanParserReadyPromise(Promise.resolve());
options.setYomitanParserInitPromise(Promise.resolve(true));
options.setYomitanSession({ id: 'session' } as never);
return await new Promise<Extension | null>((resolve) => {
releaseLoadState.releaseLoad = (value) => {
options.setYomitanExtension(value);
@@ -60,7 +63,8 @@ test('yomitan extension runtime reuses in-flight ensure load and clears it after
assert.equal(parserWindow, null);
assert.ok(readyPromise);
assert.ok(initPromise);
assert.equal(yomitanSession, null);
assert.deepEqual(yomitanSession, { id: 'session' });
assert.equal(receivedExternalProfilePath, '/tmp/gsm-profile');
const fakeExtension = { id: 'yomitan' } as Extension;
const releaseLoad = releaseLoadState.releaseLoad;
@@ -80,20 +84,26 @@ test('yomitan extension runtime reuses in-flight ensure load and clears it after
test('yomitan extension runtime direct load delegates to core', async () => {
let loadCalls = 0;
let receivedExternalProfilePath = '';
let yomitanSession: unknown = null;
const runtime = createYomitanExtensionRuntime({
loadYomitanExtensionCore: async () => {
loadYomitanExtensionCore: async (options) => {
loadCalls += 1;
receivedExternalProfilePath = options.externalProfilePath ?? '';
options.setYomitanSession({ id: 'session' } as never);
return null;
},
userDataPath: '/tmp',
externalProfilePath: '',
externalProfilePath: '/tmp/gsm-profile',
getYomitanParserWindow: () => null,
setYomitanParserWindow: () => {},
setYomitanParserReadyPromise: () => {},
setYomitanParserInitPromise: () => {},
setYomitanExtension: () => {},
setYomitanSession: () => {},
setYomitanSession: (next) => {
yomitanSession = next;
},
getYomitanExtension: () => null,
getLoadInFlight: () => null,
setLoadInFlight: () => {},
@@ -101,4 +111,6 @@ test('yomitan extension runtime direct load delegates to core', async () => {
assert.equal(await runtime.loadYomitanExtension(), null);
assert.equal(loadCalls, 1);
assert.equal(receivedExternalProfilePath, '/tmp/gsm-profile');
assert.deepEqual(yomitanSession, { id: 'session' });
});

View File

@@ -23,13 +23,15 @@ test('yomitan opener warns when extension cannot be loaded', async () => {
test('yomitan opener opens settings window when extension is available', async () => {
let opened = false;
const yomitanSession = { id: 'session' };
const openSettings = createOpenYomitanSettingsHandler({
ensureYomitanExtensionLoaded: async () => ({ id: 'ext' }),
openYomitanSettingsWindow: () => {
opened = true;
openYomitanSettingsWindow: ({ yomitanSession: forwardedSession }) => {
opened = (forwardedSession as { id: string } | null)?.id === 'session';
},
getExistingWindow: () => null,
setWindow: () => {},
getYomitanSession: () => yomitanSession,
logWarn: () => {},
logError: () => {},
});

View File

@@ -1,5 +1,6 @@
type YomitanExtensionLike = unknown;
type BrowserWindowLike = unknown;
type SessionLike = unknown;
export function createOpenYomitanSettingsHandler(deps: {
ensureYomitanExtensionLoaded: () => Promise<YomitanExtensionLike | null>;
@@ -7,10 +8,12 @@ export function createOpenYomitanSettingsHandler(deps: {
yomitanExt: YomitanExtensionLike;
getExistingWindow: () => BrowserWindowLike | null;
setWindow: (window: BrowserWindowLike | null) => void;
yomitanSession?: SessionLike | null;
onWindowClosed?: () => void;
}) => void;
getExistingWindow: () => BrowserWindowLike | null;
setWindow: (window: BrowserWindowLike | null) => void;
getYomitanSession?: () => SessionLike | null;
logWarn: (message: string) => void;
logError: (message: string, error: unknown) => void;
}) {
@@ -25,6 +28,7 @@ export function createOpenYomitanSettingsHandler(deps: {
yomitanExt: extension,
getExistingWindow: deps.getExistingWindow,
setWindow: deps.setWindow,
yomitanSession: deps.getYomitanSession?.() ?? null,
});
})().catch((error) => {
deps.logError('Failed to open Yomitan settings window.', error);

View File

@@ -5,11 +5,12 @@ import { createYomitanSettingsRuntime } from './yomitan-settings-runtime';
test('yomitan settings runtime composes opener with built deps', async () => {
let existingWindow: { id: string } | null = null;
const calls: string[] = [];
const yomitanSession = { id: 'session' };
const runtime = createYomitanSettingsRuntime({
ensureYomitanExtensionLoaded: async () => ({ id: 'ext' }),
openYomitanSettingsWindow: ({ getExistingWindow, setWindow }) => {
calls.push('open-window');
openYomitanSettingsWindow: ({ getExistingWindow, setWindow, yomitanSession: forwardedSession }) => {
calls.push(`open-window:${(forwardedSession as { id: string } | null)?.id ?? 'null'}`);
const current = getExistingWindow();
if (!current) {
setWindow({ id: 'settings' });
@@ -19,6 +20,7 @@ test('yomitan settings runtime composes opener with built deps', async () => {
setWindow: (window) => {
existingWindow = window as { id: string } | null;
},
getYomitanSession: () => yomitanSession,
logWarn: (message) => calls.push(`warn:${message}`),
logError: (message) => calls.push(`error:${message}`),
});
@@ -27,5 +29,5 @@ test('yomitan settings runtime composes opener with built deps', async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
assert.deepEqual(existingWindow, { id: 'settings' });
assert.deepEqual(calls, ['open-window']);
assert.deepEqual(calls, ['open-window:session']);
});