mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 12:11:28 -07:00
Harden Yomitan runtime state and profile policy handling
- Centralize external-profile read-only behavior in a shared Yomitan profile policy - Clear parser/extension runtime state via dedicated helpers on load failures and reloads - Prevent opening Yomitan settings when the Yomitan session is unavailable - Add focused runtime-policy and state-clearing regression tests
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
type: added
|
type: changed
|
||||||
area: config
|
area: config
|
||||||
|
|
||||||
- Added `yomitan.externalProfilePath` to reuse another Electron app's Yomitan profile in read-only mode.
|
- Added `yomitan.externalProfilePath` to reuse another Electron app's Yomitan profile in read-only mode.
|
||||||
- SubMiner now reuses external Yomitan dictionaries/settings without writing back to that profile.
|
- SubMiner now reuses external Yomitan dictionaries/settings without writing back to that profile.
|
||||||
- Fixed default config bootstrap so `config.jsonc` is seeded even when the config directory already exists.
|
- SubMiner now seeds `config.jsonc` even when the default config directory already exists.
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import {
|
|||||||
resolveExternalYomitanExtensionPath,
|
resolveExternalYomitanExtensionPath,
|
||||||
resolveExistingYomitanExtensionPath,
|
resolveExistingYomitanExtensionPath,
|
||||||
} from './yomitan-extension-paths';
|
} from './yomitan-extension-paths';
|
||||||
|
import {
|
||||||
|
clearYomitanExtensionRuntimeState,
|
||||||
|
clearYomitanParserRuntimeState,
|
||||||
|
} from './yomitan-extension-runtime-state';
|
||||||
|
|
||||||
const { session } = electron;
|
const { session } = electron;
|
||||||
const logger = createLogger('main:yomitan-extension-loader');
|
const logger = createLogger('main:yomitan-extension-loader');
|
||||||
@@ -28,6 +32,22 @@ export interface YomitanExtensionLoaderDeps {
|
|||||||
export async function loadYomitanExtension(
|
export async function loadYomitanExtension(
|
||||||
deps: YomitanExtensionLoaderDeps,
|
deps: YomitanExtensionLoaderDeps,
|
||||||
): Promise<Extension | null> {
|
): Promise<Extension | null> {
|
||||||
|
const clearRuntimeState = () =>
|
||||||
|
clearYomitanExtensionRuntimeState({
|
||||||
|
getYomitanParserWindow: deps.getYomitanParserWindow,
|
||||||
|
setYomitanParserWindow: deps.setYomitanParserWindow,
|
||||||
|
setYomitanParserReadyPromise: deps.setYomitanParserReadyPromise,
|
||||||
|
setYomitanParserInitPromise: deps.setYomitanParserInitPromise,
|
||||||
|
setYomitanExtension: () => deps.setYomitanExtension(null),
|
||||||
|
setYomitanSession: () => deps.setYomitanSession(null),
|
||||||
|
});
|
||||||
|
const clearParserState = () =>
|
||||||
|
clearYomitanParserRuntimeState({
|
||||||
|
getYomitanParserWindow: deps.getYomitanParserWindow,
|
||||||
|
setYomitanParserWindow: deps.setYomitanParserWindow,
|
||||||
|
setYomitanParserReadyPromise: deps.setYomitanParserReadyPromise,
|
||||||
|
setYomitanParserInitPromise: deps.setYomitanParserInitPromise,
|
||||||
|
});
|
||||||
const externalProfilePath = deps.externalProfilePath?.trim() ?? '';
|
const externalProfilePath = deps.externalProfilePath?.trim() ?? '';
|
||||||
let extPath: string | null = null;
|
let extPath: string | null = null;
|
||||||
let targetSession: Session = session.defaultSession;
|
let targetSession: Session = session.defaultSession;
|
||||||
@@ -38,8 +58,7 @@ export async function loadYomitanExtension(
|
|||||||
if (!extPath) {
|
if (!extPath) {
|
||||||
logger.error('External Yomitan extension not found in configured profile path');
|
logger.error('External Yomitan extension not found in configured profile path');
|
||||||
logger.error('Expected unpacked extension at:', path.join(resolvedProfilePath, 'extensions'));
|
logger.error('Expected unpacked extension at:', path.join(resolvedProfilePath, 'extensions'));
|
||||||
deps.setYomitanExtension(null);
|
clearRuntimeState();
|
||||||
deps.setYomitanSession(null);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,8 +75,7 @@ export async function loadYomitanExtension(
|
|||||||
if (!extPath) {
|
if (!extPath) {
|
||||||
logger.error('Yomitan extension not found in any search path');
|
logger.error('Yomitan extension not found in any search path');
|
||||||
logger.error('Run `bun run build:yomitan` or install Yomitan to one of:', searchPaths);
|
logger.error('Run `bun run build:yomitan` or install Yomitan to one of:', searchPaths);
|
||||||
deps.setYomitanExtension(null);
|
clearRuntimeState();
|
||||||
deps.setYomitanSession(null);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,13 +86,7 @@ export async function loadYomitanExtension(
|
|||||||
extPath = extensionCopy.targetDir;
|
extPath = extensionCopy.targetDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parserWindow = deps.getYomitanParserWindow();
|
clearParserState();
|
||||||
if (parserWindow && !parserWindow.isDestroyed()) {
|
|
||||||
parserWindow.destroy();
|
|
||||||
}
|
|
||||||
deps.setYomitanParserWindow(null);
|
|
||||||
deps.setYomitanParserReadyPromise(null);
|
|
||||||
deps.setYomitanParserInitPromise(null);
|
|
||||||
deps.setYomitanSession(targetSession);
|
deps.setYomitanSession(targetSession);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -91,8 +103,7 @@ export async function loadYomitanExtension(
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Failed to load Yomitan extension:', (err as Error).message);
|
logger.error('Failed to load Yomitan extension:', (err as Error).message);
|
||||||
logger.error('Full error:', err);
|
logger.error('Full error:', err);
|
||||||
deps.setYomitanExtension(null);
|
clearRuntimeState();
|
||||||
deps.setYomitanSession(null);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
45
src/core/services/yomitan-extension-runtime-state.test.ts
Normal file
45
src/core/services/yomitan-extension-runtime-state.test.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { clearYomitanParserRuntimeState } from './yomitan-extension-runtime-state';
|
||||||
|
|
||||||
|
test('clearYomitanParserRuntimeState destroys parser window and clears parser promises', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const parserWindow = {
|
||||||
|
isDestroyed: () => false,
|
||||||
|
destroy: () => {
|
||||||
|
calls.push('destroy');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
clearYomitanParserRuntimeState({
|
||||||
|
getYomitanParserWindow: () => parserWindow as never,
|
||||||
|
setYomitanParserWindow: (window) => calls.push(`window:${window === null ? 'null' : 'set'}`),
|
||||||
|
setYomitanParserReadyPromise: (promise) =>
|
||||||
|
calls.push(`ready:${promise === null ? 'null' : 'set'}`),
|
||||||
|
setYomitanParserInitPromise: (promise) =>
|
||||||
|
calls.push(`init:${promise === null ? 'null' : 'set'}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(calls, ['destroy', 'window:null', 'ready:null', 'init:null']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clearYomitanParserRuntimeState skips destroy when parser window is already gone', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const parserWindow = {
|
||||||
|
isDestroyed: () => true,
|
||||||
|
destroy: () => {
|
||||||
|
calls.push('destroy');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
clearYomitanParserRuntimeState({
|
||||||
|
getYomitanParserWindow: () => parserWindow as never,
|
||||||
|
setYomitanParserWindow: (window) => calls.push(`window:${window === null ? 'null' : 'set'}`),
|
||||||
|
setYomitanParserReadyPromise: (promise) =>
|
||||||
|
calls.push(`ready:${promise === null ? 'null' : 'set'}`),
|
||||||
|
setYomitanParserInitPromise: (promise) =>
|
||||||
|
calls.push(`init:${promise === null ? 'null' : 'set'}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(calls, ['window:null', 'ready:null', 'init:null']);
|
||||||
|
});
|
||||||
34
src/core/services/yomitan-extension-runtime-state.ts
Normal file
34
src/core/services/yomitan-extension-runtime-state.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
type ParserWindowLike = {
|
||||||
|
isDestroyed?: () => boolean;
|
||||||
|
destroy?: () => void;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
export interface YomitanParserRuntimeStateDeps {
|
||||||
|
getYomitanParserWindow: () => ParserWindowLike;
|
||||||
|
setYomitanParserWindow: (window: null) => void;
|
||||||
|
setYomitanParserReadyPromise: (promise: Promise<void> | null) => void;
|
||||||
|
setYomitanParserInitPromise: (promise: Promise<boolean> | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YomitanExtensionRuntimeStateDeps extends YomitanParserRuntimeStateDeps {
|
||||||
|
setYomitanExtension: (extension: null) => void;
|
||||||
|
setYomitanSession: (session: null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearYomitanParserRuntimeState(deps: YomitanParserRuntimeStateDeps): void {
|
||||||
|
const parserWindow = deps.getYomitanParserWindow();
|
||||||
|
if (parserWindow && !parserWindow.isDestroyed?.()) {
|
||||||
|
parserWindow.destroy?.();
|
||||||
|
}
|
||||||
|
deps.setYomitanParserWindow(null);
|
||||||
|
deps.setYomitanParserReadyPromise(null);
|
||||||
|
deps.setYomitanParserInitPromise(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearYomitanExtensionRuntimeState(
|
||||||
|
deps: YomitanExtensionRuntimeStateDeps,
|
||||||
|
): void {
|
||||||
|
clearYomitanParserRuntimeState(deps);
|
||||||
|
deps.setYomitanExtension(null);
|
||||||
|
deps.setYomitanSession(null);
|
||||||
|
}
|
||||||
67
src/main.ts
67
src/main.ts
@@ -374,12 +374,9 @@ import { createOverlayVisibilityRuntimeService } from './main/overlay-visibility
|
|||||||
import { createCharacterDictionaryRuntimeService } from './main/character-dictionary-runtime';
|
import { createCharacterDictionaryRuntimeService } from './main/character-dictionary-runtime';
|
||||||
import { createCharacterDictionaryAutoSyncRuntimeService } from './main/runtime/character-dictionary-auto-sync';
|
import { createCharacterDictionaryAutoSyncRuntimeService } from './main/runtime/character-dictionary-auto-sync';
|
||||||
import { notifyCharacterDictionaryAutoSyncStatus } from './main/runtime/character-dictionary-auto-sync-notifications';
|
import { notifyCharacterDictionaryAutoSyncStatus } from './main/runtime/character-dictionary-auto-sync-notifications';
|
||||||
import {
|
|
||||||
getCharacterDictionaryDisabledReason,
|
|
||||||
isCharacterDictionaryRuntimeEnabled,
|
|
||||||
} from './main/runtime/character-dictionary-availability';
|
|
||||||
import { createCurrentMediaTokenizationGate } from './main/runtime/current-media-tokenization-gate';
|
import { createCurrentMediaTokenizationGate } from './main/runtime/current-media-tokenization-gate';
|
||||||
import { createStartupOsdSequencer } from './main/runtime/startup-osd-sequencer';
|
import { createStartupOsdSequencer } from './main/runtime/startup-osd-sequencer';
|
||||||
|
import { createYomitanProfilePolicy } from './main/runtime/yomitan-profile-policy';
|
||||||
import { formatSkippedYomitanWriteAction } from './main/runtime/yomitan-read-only-log';
|
import { formatSkippedYomitanWriteAction } from './main/runtime/yomitan-read-only-log';
|
||||||
import {
|
import {
|
||||||
getPreferredYomitanAnkiServerUrl as getPreferredYomitanAnkiServerUrlRuntime,
|
getPreferredYomitanAnkiServerUrl as getPreferredYomitanAnkiServerUrlRuntime,
|
||||||
@@ -1332,9 +1329,7 @@ const characterDictionaryAutoSyncRuntime = createCharacterDictionaryAutoSyncRunt
|
|||||||
getConfig: () => {
|
getConfig: () => {
|
||||||
const config = getResolvedConfig().anilist.characterDictionary;
|
const config = getResolvedConfig().anilist.characterDictionary;
|
||||||
return {
|
return {
|
||||||
enabled:
|
enabled: config.enabled && yomitanProfilePolicy.isCharacterDictionaryEnabled(),
|
||||||
config.enabled &&
|
|
||||||
isCharacterDictionaryRuntimeEnabled(getConfiguredExternalYomitanProfilePath()),
|
|
||||||
maxLoaded: config.maxLoaded,
|
maxLoaded: config.maxLoaded,
|
||||||
profileScope: config.profileScope,
|
profileScope: config.profileScope,
|
||||||
};
|
};
|
||||||
@@ -1354,8 +1349,10 @@ const characterDictionaryAutoSyncRuntime = createCharacterDictionaryAutoSyncRunt
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
importYomitanDictionary: async (zipPath) => {
|
importYomitanDictionary: async (zipPath) => {
|
||||||
if (isYomitanExternalReadOnlyMode()) {
|
if (yomitanProfilePolicy.isExternalReadOnlyMode()) {
|
||||||
logSkippedYomitanWrite(formatSkippedYomitanWriteAction('importYomitanDictionary', zipPath));
|
yomitanProfilePolicy.logSkippedWrite(
|
||||||
|
formatSkippedYomitanWriteAction('importYomitanDictionary', zipPath),
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await ensureYomitanExtensionLoaded();
|
await ensureYomitanExtensionLoaded();
|
||||||
@@ -1365,8 +1362,8 @@ const characterDictionaryAutoSyncRuntime = createCharacterDictionaryAutoSyncRunt
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
deleteYomitanDictionary: async (dictionaryTitle) => {
|
deleteYomitanDictionary: async (dictionaryTitle) => {
|
||||||
if (isYomitanExternalReadOnlyMode()) {
|
if (yomitanProfilePolicy.isExternalReadOnlyMode()) {
|
||||||
logSkippedYomitanWrite(
|
yomitanProfilePolicy.logSkippedWrite(
|
||||||
formatSkippedYomitanWriteAction('deleteYomitanDictionary', dictionaryTitle),
|
formatSkippedYomitanWriteAction('deleteYomitanDictionary', dictionaryTitle),
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
@@ -1378,8 +1375,8 @@ const characterDictionaryAutoSyncRuntime = createCharacterDictionaryAutoSyncRunt
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
upsertYomitanDictionarySettings: async (dictionaryTitle, profileScope) => {
|
upsertYomitanDictionarySettings: async (dictionaryTitle, profileScope) => {
|
||||||
if (isYomitanExternalReadOnlyMode()) {
|
if (yomitanProfilePolicy.isExternalReadOnlyMode()) {
|
||||||
logSkippedYomitanWrite(
|
yomitanProfilePolicy.logSkippedWrite(
|
||||||
formatSkippedYomitanWriteAction('upsertYomitanDictionarySettings', dictionaryTitle),
|
formatSkippedYomitanWriteAction('upsertYomitanDictionarySettings', dictionaryTitle),
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
@@ -2762,7 +2759,7 @@ const {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
scheduleCharacterDictionarySync: () => {
|
scheduleCharacterDictionarySync: () => {
|
||||||
if (!isCharacterDictionaryEnabledForCurrentProcess()) {
|
if (!yomitanProfilePolicy.isCharacterDictionaryEnabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
characterDictionaryAutoSyncRuntime.scheduleSync();
|
characterDictionaryAutoSyncRuntime.scheduleSync();
|
||||||
@@ -2844,7 +2841,7 @@ const {
|
|||||||
),
|
),
|
||||||
getCharacterDictionaryEnabled: () =>
|
getCharacterDictionaryEnabled: () =>
|
||||||
getResolvedConfig().anilist.characterDictionary.enabled &&
|
getResolvedConfig().anilist.characterDictionary.enabled &&
|
||||||
isCharacterDictionaryEnabledForCurrentProcess(),
|
yomitanProfilePolicy.isCharacterDictionaryEnabled(),
|
||||||
getNameMatchEnabled: () => getResolvedConfig().subtitleStyle.nameMatchEnabled,
|
getNameMatchEnabled: () => getResolvedConfig().subtitleStyle.nameMatchEnabled,
|
||||||
getFrequencyDictionaryEnabled: () =>
|
getFrequencyDictionaryEnabled: () =>
|
||||||
getRuntimeBooleanOption(
|
getRuntimeBooleanOption(
|
||||||
@@ -3018,7 +3015,7 @@ const enforceOverlayLayerOrder = createEnforceOverlayLayerOrderHandler(
|
|||||||
|
|
||||||
async function loadYomitanExtension(): Promise<Extension | null> {
|
async function loadYomitanExtension(): Promise<Extension | null> {
|
||||||
const extension = await yomitanExtensionRuntime.loadYomitanExtension();
|
const extension = await yomitanExtensionRuntime.loadYomitanExtension();
|
||||||
if (extension && !isYomitanExternalReadOnlyMode()) {
|
if (extension && !yomitanProfilePolicy.isExternalReadOnlyMode()) {
|
||||||
await syncYomitanDefaultProfileAnkiServer();
|
await syncYomitanDefaultProfileAnkiServer();
|
||||||
}
|
}
|
||||||
return extension;
|
return extension;
|
||||||
@@ -3026,7 +3023,7 @@ async function loadYomitanExtension(): Promise<Extension | null> {
|
|||||||
|
|
||||||
async function ensureYomitanExtensionLoaded(): Promise<Extension | null> {
|
async function ensureYomitanExtensionLoaded(): Promise<Extension | null> {
|
||||||
const extension = await yomitanExtensionRuntime.ensureYomitanExtensionLoaded();
|
const extension = await yomitanExtensionRuntime.ensureYomitanExtensionLoaded();
|
||||||
if (extension && !isYomitanExternalReadOnlyMode()) {
|
if (extension && !yomitanProfilePolicy.isExternalReadOnlyMode()) {
|
||||||
await syncYomitanDefaultProfileAnkiServer();
|
await syncYomitanDefaultProfileAnkiServer();
|
||||||
}
|
}
|
||||||
return extension;
|
return extension;
|
||||||
@@ -3038,28 +3035,6 @@ function getPreferredYomitanAnkiServerUrl(): string {
|
|||||||
return getPreferredYomitanAnkiServerUrlRuntime(getResolvedConfig().ankiConnect);
|
return getPreferredYomitanAnkiServerUrlRuntime(getResolvedConfig().ankiConnect);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConfiguredExternalYomitanProfilePath(): string {
|
|
||||||
return configuredExternalYomitanProfilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isYomitanExternalReadOnlyMode(): boolean {
|
|
||||||
return getConfiguredExternalYomitanProfilePath().length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCharacterDictionaryEnabledForCurrentProcess(): boolean {
|
|
||||||
return isCharacterDictionaryRuntimeEnabled(getConfiguredExternalYomitanProfilePath());
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCharacterDictionaryDisabledReasonForCurrentProcess(): string | null {
|
|
||||||
return getCharacterDictionaryDisabledReason(getConfiguredExternalYomitanProfilePath());
|
|
||||||
}
|
|
||||||
|
|
||||||
function logSkippedYomitanWrite(action: string): void {
|
|
||||||
logger.info(
|
|
||||||
`[yomitan] skipping ${action}: yomitan.externalProfilePath is configured; external profile mode is read-only`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getYomitanParserRuntimeDeps() {
|
function getYomitanParserRuntimeDeps() {
|
||||||
return {
|
return {
|
||||||
getYomitanExt: () => appState.yomitanExt,
|
getYomitanExt: () => appState.yomitanExt,
|
||||||
@@ -3080,7 +3055,7 @@ function getYomitanParserRuntimeDeps() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function syncYomitanDefaultProfileAnkiServer(): Promise<void> {
|
async function syncYomitanDefaultProfileAnkiServer(): Promise<void> {
|
||||||
if (isYomitanExternalReadOnlyMode()) {
|
if (yomitanProfilePolicy.isExternalReadOnlyMode()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3138,7 +3113,7 @@ function initializeOverlayRuntime(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openYomitanSettings(): boolean {
|
function openYomitanSettings(): boolean {
|
||||||
if (isYomitanExternalReadOnlyMode()) {
|
if (yomitanProfilePolicy.isExternalReadOnlyMode()) {
|
||||||
const message =
|
const message =
|
||||||
'Yomitan settings unavailable while using read-only external-profile mode.';
|
'Yomitan settings unavailable while using read-only external-profile mode.';
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -3557,7 +3532,7 @@ const createCliCommandContextHandler = createCliCommandContextFactory({
|
|||||||
getAnilistQueueStatus: () => anilistStateRuntime.getQueueStatusSnapshot(),
|
getAnilistQueueStatus: () => anilistStateRuntime.getQueueStatusSnapshot(),
|
||||||
processNextAnilistRetryUpdate: () => processNextAnilistRetryUpdate(),
|
processNextAnilistRetryUpdate: () => processNextAnilistRetryUpdate(),
|
||||||
generateCharacterDictionary: async (targetPath?: string) => {
|
generateCharacterDictionary: async (targetPath?: string) => {
|
||||||
const disabledReason = getCharacterDictionaryDisabledReasonForCurrentProcess();
|
const disabledReason = yomitanProfilePolicy.getCharacterDictionaryDisabledReason();
|
||||||
if (disabledReason) {
|
if (disabledReason) {
|
||||||
throw new Error(disabledReason);
|
throw new Error(disabledReason);
|
||||||
}
|
}
|
||||||
@@ -3650,11 +3625,15 @@ const { ensureTray: ensureTrayHandler, destroyTray: destroyTrayHandler } =
|
|||||||
},
|
},
|
||||||
buildMenuFromTemplate: (template) => Menu.buildFromTemplate(template),
|
buildMenuFromTemplate: (template) => Menu.buildFromTemplate(template),
|
||||||
});
|
});
|
||||||
const configuredExternalYomitanProfilePath = getResolvedConfig().yomitan.externalProfilePath.trim();
|
const yomitanProfilePolicy = createYomitanProfilePolicy({
|
||||||
|
externalProfilePath: getResolvedConfig().yomitan.externalProfilePath,
|
||||||
|
logInfo: (message) => logger.info(message),
|
||||||
|
});
|
||||||
|
const configuredExternalYomitanProfilePath = yomitanProfilePolicy.externalProfilePath;
|
||||||
const yomitanExtensionRuntime = createYomitanExtensionRuntime({
|
const yomitanExtensionRuntime = createYomitanExtensionRuntime({
|
||||||
loadYomitanExtensionCore,
|
loadYomitanExtensionCore,
|
||||||
userDataPath: USER_DATA_PATH,
|
userDataPath: USER_DATA_PATH,
|
||||||
externalProfilePath: getConfiguredExternalYomitanProfilePath(),
|
externalProfilePath: configuredExternalYomitanProfilePath,
|
||||||
getYomitanParserWindow: () => appState.yomitanParserWindow,
|
getYomitanParserWindow: () => appState.yomitanParserWindow,
|
||||||
setYomitanParserWindow: (window) => {
|
setYomitanParserWindow: (window) => {
|
||||||
appState.yomitanParserWindow = window as BrowserWindow | null;
|
appState.yomitanParserWindow = window as BrowserWindow | null;
|
||||||
|
|||||||
36
src/main/runtime/yomitan-profile-policy.test.ts
Normal file
36
src/main/runtime/yomitan-profile-policy.test.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { createYomitanProfilePolicy } from './yomitan-profile-policy';
|
||||||
|
|
||||||
|
test('yomitan profile policy trims external profile path and marks read-only mode', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const policy = createYomitanProfilePolicy({
|
||||||
|
externalProfilePath: ' /tmp/gsm-profile ',
|
||||||
|
logInfo: (message) => calls.push(message),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(policy.externalProfilePath, '/tmp/gsm-profile');
|
||||||
|
assert.equal(policy.isExternalReadOnlyMode(), true);
|
||||||
|
assert.equal(policy.isCharacterDictionaryEnabled(), false);
|
||||||
|
assert.equal(
|
||||||
|
policy.getCharacterDictionaryDisabledReason(),
|
||||||
|
'Character dictionary is disabled while yomitan.externalProfilePath is configured.',
|
||||||
|
);
|
||||||
|
|
||||||
|
policy.logSkippedWrite('importYomitanDictionary(sample.zip)');
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'[yomitan] skipping importYomitanDictionary(sample.zip): yomitan.externalProfilePath is configured; external profile mode is read-only',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('yomitan profile policy keeps character dictionary enabled without external profile path', () => {
|
||||||
|
const policy = createYomitanProfilePolicy({
|
||||||
|
externalProfilePath: ' ',
|
||||||
|
logInfo: () => undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(policy.externalProfilePath, '');
|
||||||
|
assert.equal(policy.isExternalReadOnlyMode(), false);
|
||||||
|
assert.equal(policy.isCharacterDictionaryEnabled(), true);
|
||||||
|
assert.equal(policy.getCharacterDictionaryDisabledReason(), null);
|
||||||
|
});
|
||||||
25
src/main/runtime/yomitan-profile-policy.ts
Normal file
25
src/main/runtime/yomitan-profile-policy.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import {
|
||||||
|
getCharacterDictionaryDisabledReason,
|
||||||
|
isCharacterDictionaryRuntimeEnabled,
|
||||||
|
} from './character-dictionary-availability';
|
||||||
|
|
||||||
|
export function createYomitanProfilePolicy(options: {
|
||||||
|
externalProfilePath: string;
|
||||||
|
logInfo: (message: string) => void;
|
||||||
|
}) {
|
||||||
|
const externalProfilePath = options.externalProfilePath.trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
externalProfilePath,
|
||||||
|
isExternalReadOnlyMode: (): boolean => externalProfilePath.length > 0,
|
||||||
|
isCharacterDictionaryEnabled: (): boolean =>
|
||||||
|
isCharacterDictionaryRuntimeEnabled(externalProfilePath),
|
||||||
|
getCharacterDictionaryDisabledReason: (): string | null =>
|
||||||
|
getCharacterDictionaryDisabledReason(externalProfilePath),
|
||||||
|
logSkippedWrite: (action: string): void => {
|
||||||
|
options.logInfo(
|
||||||
|
`[yomitan] skipping ${action}: yomitan.externalProfilePath is configured; external profile mode is read-only`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -24,11 +24,16 @@ export function createOpenYomitanSettingsHandler(deps: {
|
|||||||
deps.logWarn('Unable to open Yomitan settings: extension failed to load.');
|
deps.logWarn('Unable to open Yomitan settings: extension failed to load.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const yomitanSession = deps.getYomitanSession?.() ?? null;
|
||||||
|
if (!yomitanSession) {
|
||||||
|
deps.logWarn('Unable to open Yomitan settings: Yomitan session is unavailable.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
deps.openYomitanSettingsWindow({
|
deps.openYomitanSettingsWindow({
|
||||||
yomitanExt: extension,
|
yomitanExt: extension,
|
||||||
getExistingWindow: deps.getExistingWindow,
|
getExistingWindow: deps.getExistingWindow,
|
||||||
setWindow: deps.setWindow,
|
setWindow: deps.setWindow,
|
||||||
yomitanSession: deps.getYomitanSession?.() ?? null,
|
yomitanSession,
|
||||||
});
|
});
|
||||||
})().catch((error) => {
|
})().catch((error) => {
|
||||||
deps.logError('Failed to open Yomitan settings window.', error);
|
deps.logError('Failed to open Yomitan settings window.', error);
|
||||||
|
|||||||
@@ -31,3 +31,28 @@ test('yomitan settings runtime composes opener with built deps', async () => {
|
|||||||
assert.deepEqual(existingWindow, { id: 'settings' });
|
assert.deepEqual(existingWindow, { id: 'settings' });
|
||||||
assert.deepEqual(calls, ['open-window:session']);
|
assert.deepEqual(calls, ['open-window:session']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('yomitan settings runtime warns and does not open when no yomitan session is available', async () => {
|
||||||
|
let existingWindow: { id: string } | null = null;
|
||||||
|
const calls: string[] = [];
|
||||||
|
|
||||||
|
const runtime = createYomitanSettingsRuntime({
|
||||||
|
ensureYomitanExtensionLoaded: async () => ({ id: 'ext' }),
|
||||||
|
openYomitanSettingsWindow: () => {
|
||||||
|
calls.push('open-window');
|
||||||
|
},
|
||||||
|
getExistingWindow: () => existingWindow as never,
|
||||||
|
setWindow: (window) => {
|
||||||
|
existingWindow = window as { id: string } | null;
|
||||||
|
},
|
||||||
|
getYomitanSession: () => null,
|
||||||
|
logWarn: (message) => calls.push(`warn:${message}`),
|
||||||
|
logError: (message) => calls.push(`error:${message}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
runtime.openYomitanSettings();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
assert.equal(existingWindow, null);
|
||||||
|
assert.deepEqual(calls, ['warn:Unable to open Yomitan settings: Yomitan session is unavailable.']);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user