Add read-only external Yomitan profile support

- add `yomitan.externalProfilePath` config and default/template wiring
- load Yomitan from an external Electron profile/session when configured
- disable SubMiner Yomitan writes/settings UI in external-profile mode and update docs/tests
This commit is contained in:
2026-03-11 02:08:02 -07:00
parent 2f17859b7b
commit 3ee71139a6
30 changed files with 316 additions and 32 deletions

View File

@@ -0,0 +1,5 @@
type: added
area: yomitan
- 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.

View File

@@ -336,6 +336,17 @@
} // Character dictionary setting. } // Character dictionary setting.
}, // Anilist API credentials and update behavior. }, // Anilist API credentials and update behavior.
// ==========================================
// Yomitan
// Optional external Yomitan profile integration.
// Setting yomitan.externalProfilePath switches SubMiner to read-only external-profile mode.
// For GameSentenceMiner, the default Linux overlay profile is usually ~/.config/gsm_overlay.
// In external-profile mode SubMiner will not import, delete, or modify Yomitan dictionaries/settings.
// ==========================================
"yomitan": {
"externalProfilePath": "" // Optional external Yomitan Electron profile path to use in read-only mode for shared dictionaries/settings. Example: ~/.config/gsm_overlay
}, // Optional external Yomitan profile integration.
// ========================================== // ==========================================
// Jellyfin // Jellyfin
// Optional Jellyfin integration for auth, browsing, and playback launch. // Optional Jellyfin integration for auth, browsing, and playback launch.

View File

@@ -62,6 +62,10 @@ Character dictionary sync is disabled by default. To turn it on:
The first sync for a media title takes a few seconds while character data and portraits are fetched from AniList. Subsequent launches reuse the cached snapshot. The first sync for a media title takes a few seconds while character data and portraits are fetched from AniList. Subsequent launches reuse the cached snapshot.
::: :::
::: warning
If `yomitan.externalProfilePath` is set, SubMiner switches to read-only external-profile mode. In that mode SubMiner can reuse another app's installed Yomitan dictionaries/settings, but character-dictionary auto-sync does not import or update the merged dictionary.
:::
## Name Generation ## Name Generation
A single character produces many searchable terms so that names are recognized regardless of how they appear in dialogue. SubMiner generates variants for: A single character produces many searchable terms so that names are recognized regardless of how they appear in dialogue. SubMiner generates variants for:

View File

@@ -912,6 +912,32 @@ Current post-watch behavior:
- If embedded AniList auth UI fails to render, SubMiner opens the authorize URL in your default browser and shows fallback instructions in-app. - If embedded AniList auth UI fails to render, SubMiner opens the authorize URL in your default browser and shows fallback instructions in-app.
- Failed updates are retried with a persistent backoff queue in the background. - Failed updates are retried with a persistent backoff queue in the background.
### Yomitan
SubMiner normally uses its bundled Yomitan profile under the app config directory. If you want to reuse dictionaries and profile settings from another Electron app, point SubMiner at that app's Yomitan Electron profile in read-only mode.
For GameSentenceMiner on Linux, the default overlay profile path is typically `~/.config/gsm_overlay`.
```json
{
"yomitan": {
"externalProfilePath": "/home/you/.config/gsm_overlay"
}
}
```
| Option | Values | Description |
| --------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `externalProfilePath` | string path | Optional absolute path to another app's Yomitan Electron profile. SubMiner loads that profile read-only and reuses its dictionaries/settings. |
External-profile mode behavior:
- SubMiner uses the external profile's Yomitan extension/session instead of its local copy.
- SubMiner reads the external profile's currently active Yomitan profile selection and installed dictionaries.
- SubMiner does not open its own Yomitan settings window in this mode.
- SubMiner does not import, delete, or update dictionaries/settings in the external profile.
- SubMiner character-dictionary auto-sync is effectively disabled in this mode because it requires Yomitan writes.
Setup flow details: Setup flow details:
1. Set `anilist.enabled` to `true`. 1. Set `anilist.enabled` to `true`.

View File

@@ -336,6 +336,17 @@
} // Character dictionary setting. } // Character dictionary setting.
}, // Anilist API credentials and update behavior. }, // Anilist API credentials and update behavior.
// ==========================================
// Yomitan
// Optional external Yomitan profile integration.
// Setting yomitan.externalProfilePath switches SubMiner to read-only external-profile mode.
// For GameSentenceMiner, the default Linux overlay profile is usually ~/.config/gsm_overlay.
// In external-profile mode SubMiner will not import, delete, or modify Yomitan dictionaries/settings.
// ==========================================
"yomitan": {
"externalProfilePath": "" // Optional external Yomitan Electron profile path to use in read-only mode for shared dictionaries/settings. Example: ~/.config/gsm_overlay
}, // Optional external Yomitan profile integration.
// ========================================== // ==========================================
// Jellyfin // Jellyfin
// Optional Jellyfin integration for auth, browsing, and playback launch. // Optional Jellyfin integration for auth, browsing, and playback launch.

View File

@@ -182,6 +182,7 @@ If you installed from the AppImage and see this error, the package may be incomp
- Verify Yomitan loaded successfully — check the terminal output for "Loaded Yomitan extension". - Verify Yomitan loaded successfully — check the terminal output for "Loaded Yomitan extension".
- Yomitan requires dictionaries to be installed. Open Yomitan settings (`Alt+Shift+Y` or `SubMiner.AppImage --settings`) and confirm at least one dictionary is imported. - Yomitan requires dictionaries to be installed. Open Yomitan settings (`Alt+Shift+Y` or `SubMiner.AppImage --settings`) and confirm at least one dictionary is imported.
- If `yomitan.externalProfilePath` is set, import/check dictionaries in the external app/profile instead. SubMiner treats that profile as read-only and does not open its own Yomitan settings window.
- If the overlay shows subtitles but words are not clickable, the tokenizer may have failed. See the MeCab section below. - If the overlay shows subtitles but words are not clickable, the tokenizer may have failed. See the MeCab section below.
## MeCab / Tokenization ## MeCab / Tokenization

View File

@@ -30,6 +30,7 @@ test('loads defaults when config is missing', () => {
assert.equal(config.anilist.characterDictionary.collapsibleSections.description, false); assert.equal(config.anilist.characterDictionary.collapsibleSections.description, false);
assert.equal(config.anilist.characterDictionary.collapsibleSections.characterInformation, false); assert.equal(config.anilist.characterDictionary.collapsibleSections.characterInformation, false);
assert.equal(config.anilist.characterDictionary.collapsibleSections.voicedBy, false); assert.equal(config.anilist.characterDictionary.collapsibleSections.voicedBy, false);
assert.equal(config.yomitan.externalProfilePath, '');
assert.equal(config.jellyfin.remoteControlEnabled, true); assert.equal(config.jellyfin.remoteControlEnabled, true);
assert.equal(config.jellyfin.remoteControlAutoConnect, true); assert.equal(config.jellyfin.remoteControlAutoConnect, true);
assert.equal(config.jellyfin.autoAnnounce, false); assert.equal(config.jellyfin.autoAnnounce, false);

View File

@@ -31,7 +31,7 @@ const {
startupWarmups, startupWarmups,
auto_start_overlay, auto_start_overlay,
} = CORE_DEFAULT_CONFIG; } = CORE_DEFAULT_CONFIG;
const { ankiConnect, jimaku, anilist, jellyfin, discordPresence, ai, youtubeSubgen } = const { ankiConnect, jimaku, anilist, yomitan, jellyfin, discordPresence, ai, youtubeSubgen } =
INTEGRATIONS_DEFAULT_CONFIG; INTEGRATIONS_DEFAULT_CONFIG;
const { subtitleStyle } = SUBTITLE_DEFAULT_CONFIG; const { subtitleStyle } = SUBTITLE_DEFAULT_CONFIG;
const { immersionTracking } = IMMERSION_DEFAULT_CONFIG; const { immersionTracking } = IMMERSION_DEFAULT_CONFIG;
@@ -52,6 +52,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
auto_start_overlay, auto_start_overlay,
jimaku, jimaku,
anilist, anilist,
yomitan,
jellyfin, jellyfin,
discordPresence, discordPresence,
ai, ai,

View File

@@ -2,7 +2,14 @@ import { ResolvedConfig } from '../../types';
export const INTEGRATIONS_DEFAULT_CONFIG: Pick< export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
ResolvedConfig, ResolvedConfig,
'ankiConnect' | 'jimaku' | 'anilist' | 'jellyfin' | 'discordPresence' | 'ai' | 'youtubeSubgen' | 'ankiConnect'
| 'jimaku'
| 'anilist'
| 'yomitan'
| 'jellyfin'
| 'discordPresence'
| 'ai'
| 'youtubeSubgen'
> = { > = {
ankiConnect: { ankiConnect: {
enabled: false, enabled: false,
@@ -94,6 +101,9 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
}, },
}, },
}, },
yomitan: {
externalProfilePath: '',
},
jellyfin: { jellyfin: {
enabled: false, enabled: false,
serverUrl: '', serverUrl: '',

View File

@@ -25,6 +25,7 @@ test('config option registry includes critical paths and has unique entries', ()
'ankiConnect.enabled', 'ankiConnect.enabled',
'anilist.characterDictionary.enabled', 'anilist.characterDictionary.enabled',
'anilist.characterDictionary.collapsibleSections.description', 'anilist.characterDictionary.collapsibleSections.description',
'yomitan.externalProfilePath',
'immersionTracking.enabled', 'immersionTracking.enabled',
]) { ]) {
assert.ok(paths.includes(requiredPath), `missing config path: ${requiredPath}`); assert.ok(paths.includes(requiredPath), `missing config path: ${requiredPath}`);
@@ -41,6 +42,7 @@ test('config template sections include expected domains and unique keys', () =>
'startupWarmups', 'startupWarmups',
'subtitleStyle', 'subtitleStyle',
'ankiConnect', 'ankiConnect',
'yomitan',
'immersionTracking', 'immersionTracking',
]; ];

View File

@@ -211,6 +211,13 @@ export function buildIntegrationConfigOptionRegistry(
description: description:
'Open the Voiced by section by default in character dictionary glossary entries.', 'Open the Voiced by section by default in character dictionary glossary entries.',
}, },
{
path: 'yomitan.externalProfilePath',
kind: 'string',
defaultValue: defaultConfig.yomitan.externalProfilePath,
description:
'Optional external Yomitan Electron profile path to use in read-only mode for shared dictionaries/settings.',
},
{ {
path: 'jellyfin.enabled', path: 'jellyfin.enabled',
kind: 'boolean', kind: 'boolean',

View File

@@ -127,6 +127,15 @@ const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
], ],
key: 'anilist', key: 'anilist',
}, },
{
title: 'Yomitan',
description: [
'Optional external Yomitan profile integration.',
'Setting yomitan.externalProfilePath switches SubMiner to read-only external-profile mode.',
'In external-profile mode SubMiner will not import, delete, or modify Yomitan dictionaries/settings.',
],
key: 'yomitan',
},
{ {
title: 'Jellyfin', title: 'Jellyfin',
description: [ description: [

View File

@@ -199,6 +199,22 @@ export function applyIntegrationConfig(context: ResolveContext): void {
} }
} }
if (isObject(src.yomitan)) {
const externalProfilePath = asString(src.yomitan.externalProfilePath);
if (externalProfilePath !== undefined) {
resolved.yomitan.externalProfilePath = externalProfilePath.trim();
} else if (src.yomitan.externalProfilePath !== undefined) {
warn(
'yomitan.externalProfilePath',
src.yomitan.externalProfilePath,
resolved.yomitan.externalProfilePath,
'Expected string.',
);
}
} else if (src.yomitan !== undefined) {
warn('yomitan', src.yomitan, resolved.yomitan, 'Expected object.');
}
if (isObject(src.jellyfin)) { if (isObject(src.jellyfin)) {
const enabled = asBoolean(src.jellyfin.enabled); const enabled = asBoolean(src.jellyfin.enabled);
if (enabled !== undefined) { if (enabled !== undefined) {

View File

@@ -104,3 +104,26 @@ test('anilist character dictionary fields are parsed, clamped, and enum-validate
warnedPaths.includes('anilist.characterDictionary.collapsibleSections.characterInformation'), warnedPaths.includes('anilist.characterDictionary.collapsibleSections.characterInformation'),
); );
}); });
test('yomitan externalProfilePath is trimmed and invalid values warn', () => {
const { context, warnings } = createResolveContext({
yomitan: {
externalProfilePath: ' /tmp/gsm-profile ',
},
});
applyIntegrationConfig(context);
assert.equal(context.resolved.yomitan.externalProfilePath, '/tmp/gsm-profile');
const invalid = createResolveContext({
yomitan: {
externalProfilePath: 42 as never,
},
});
applyIntegrationConfig(invalid.context);
assert.equal(invalid.context.resolved.yomitan.externalProfilePath, '');
assert.ok(invalid.warnings.some((warning) => warning.path === 'yomitan.externalProfilePath'));
});

View File

@@ -1,4 +1,4 @@
import type { BrowserWindow, Extension } from 'electron'; import type { BrowserWindow, Extension, Session } from 'electron';
import { mergeTokens } from '../../token-merger'; import { mergeTokens } from '../../token-merger';
import { createLogger } from '../../logger'; import { createLogger } from '../../logger';
import { import {
@@ -33,6 +33,7 @@ type MecabTokenEnrichmentFn = (
export interface TokenizerServiceDeps { export interface TokenizerServiceDeps {
getYomitanExt: () => Extension | null; getYomitanExt: () => Extension | null;
getYomitanSession?: () => Session | null;
getYomitanParserWindow: () => BrowserWindow | null; getYomitanParserWindow: () => BrowserWindow | null;
setYomitanParserWindow: (window: BrowserWindow | null) => void; setYomitanParserWindow: (window: BrowserWindow | null) => void;
getYomitanParserReadyPromise: () => Promise<void> | null; getYomitanParserReadyPromise: () => Promise<void> | null;
@@ -63,6 +64,7 @@ interface MecabTokenizerLike {
export interface TokenizerDepsRuntimeOptions { export interface TokenizerDepsRuntimeOptions {
getYomitanExt: () => Extension | null; getYomitanExt: () => Extension | null;
getYomitanSession?: () => Session | null;
getYomitanParserWindow: () => BrowserWindow | null; getYomitanParserWindow: () => BrowserWindow | null;
setYomitanParserWindow: (window: BrowserWindow | null) => void; setYomitanParserWindow: (window: BrowserWindow | null) => void;
getYomitanParserReadyPromise: () => Promise<void> | null; getYomitanParserReadyPromise: () => Promise<void> | null;
@@ -182,6 +184,7 @@ export function createTokenizerDepsRuntime(
return { return {
getYomitanExt: options.getYomitanExt, getYomitanExt: options.getYomitanExt,
getYomitanSession: options.getYomitanSession,
getYomitanParserWindow: options.getYomitanParserWindow, getYomitanParserWindow: options.getYomitanParserWindow,
setYomitanParserWindow: options.setYomitanParserWindow, setYomitanParserWindow: options.setYomitanParserWindow,
getYomitanParserReadyPromise: options.getYomitanParserReadyPromise, getYomitanParserReadyPromise: options.getYomitanParserReadyPromise,

View File

@@ -1,4 +1,4 @@
import type { BrowserWindow, Extension } from 'electron'; import type { BrowserWindow, Extension, Session } from 'electron';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { selectYomitanParseTokens } from './parser-selection-stage'; import { selectYomitanParseTokens } from './parser-selection-stage';
@@ -10,6 +10,7 @@ interface LoggerLike {
interface YomitanParserRuntimeDeps { interface YomitanParserRuntimeDeps {
getYomitanExt: () => Extension | null; getYomitanExt: () => Extension | null;
getYomitanSession?: () => Session | null;
getYomitanParserWindow: () => BrowserWindow | null; getYomitanParserWindow: () => BrowserWindow | null;
setYomitanParserWindow: (window: BrowserWindow | null) => void; setYomitanParserWindow: (window: BrowserWindow | null) => void;
getYomitanParserReadyPromise: () => Promise<void> | null; getYomitanParserReadyPromise: () => Promise<void> | null;
@@ -465,6 +466,7 @@ async function ensureYomitanParserWindow(
const initPromise = (async () => { const initPromise = (async () => {
const { BrowserWindow, session } = electron; const { BrowserWindow, session } = electron;
const yomitanSession = deps.getYomitanSession?.() ?? session.defaultSession;
const parserWindow = new BrowserWindow({ const parserWindow = new BrowserWindow({
show: false, show: false,
width: 800, width: 800,
@@ -472,7 +474,7 @@ async function ensureYomitanParserWindow(
webPreferences: { webPreferences: {
contextIsolation: true, contextIsolation: true,
nodeIntegration: false, nodeIntegration: false,
session: session.defaultSession, session: yomitanSession,
}, },
}); });
deps.setYomitanParserWindow(parserWindow); deps.setYomitanParserWindow(parserWindow);
@@ -539,6 +541,7 @@ async function createYomitanExtensionWindow(
} }
const { BrowserWindow, session } = electron; const { BrowserWindow, session } = electron;
const yomitanSession = deps.getYomitanSession?.() ?? session.defaultSession;
const window = new BrowserWindow({ const window = new BrowserWindow({
show: false, show: false,
width: 1200, width: 1200,
@@ -546,7 +549,7 @@ async function createYomitanExtensionWindow(
webPreferences: { webPreferences: {
contextIsolation: true, contextIsolation: true,
nodeIntegration: false, nodeIntegration: false,
session: session.defaultSession, session: yomitanSession,
}, },
}); });

View File

@@ -1,10 +1,12 @@
import electron from 'electron'; import electron from 'electron';
import type { BrowserWindow, Extension } from 'electron'; import type { BrowserWindow, Extension, Session } from 'electron';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path';
import { createLogger } from '../../logger'; import { createLogger } from '../../logger';
import { ensureExtensionCopy } from './yomitan-extension-copy'; import { ensureExtensionCopy } from './yomitan-extension-copy';
import { import {
getYomitanExtensionSearchPaths, getYomitanExtensionSearchPaths,
resolveExternalYomitanExtensionPath,
resolveExistingYomitanExtensionPath, resolveExistingYomitanExtensionPath,
} from './yomitan-extension-paths'; } from './yomitan-extension-paths';
@@ -14,35 +16,57 @@ const logger = createLogger('main:yomitan-extension-loader');
export interface YomitanExtensionLoaderDeps { export interface YomitanExtensionLoaderDeps {
userDataPath: string; userDataPath: string;
extensionPath?: string; extensionPath?: string;
externalProfilePath?: string;
getYomitanParserWindow: () => BrowserWindow | null; getYomitanParserWindow: () => BrowserWindow | null;
setYomitanParserWindow: (window: BrowserWindow | null) => void; setYomitanParserWindow: (window: BrowserWindow | null) => void;
setYomitanParserReadyPromise: (promise: Promise<void> | null) => void; setYomitanParserReadyPromise: (promise: Promise<void> | null) => void;
setYomitanParserInitPromise: (promise: Promise<boolean> | null) => void; setYomitanParserInitPromise: (promise: Promise<boolean> | null) => void;
setYomitanExtension: (extension: Extension | null) => void; setYomitanExtension: (extension: Extension | null) => void;
setYomitanSession: (session: Session | null) => void;
} }
export async function loadYomitanExtension( export async function loadYomitanExtension(
deps: YomitanExtensionLoaderDeps, deps: YomitanExtensionLoaderDeps,
): Promise<Extension | null> { ): Promise<Extension | null> {
const searchPaths = getYomitanExtensionSearchPaths({ const externalProfilePath = deps.externalProfilePath?.trim() ?? '';
explicitPath: deps.extensionPath, let extPath: string | null = null;
moduleDir: __dirname, let targetSession: Session = session.defaultSession;
resourcesPath: process.resourcesPath,
userDataPath: deps.userDataPath,
});
let extPath = resolveExistingYomitanExtensionPath(searchPaths, fs.existsSync);
if (!extPath) { if (externalProfilePath) {
logger.error('Yomitan extension not found in any search path'); const resolvedProfilePath = path.resolve(externalProfilePath);
logger.error('Run `bun run build:yomitan` or install Yomitan to one of:', searchPaths); extPath = resolveExternalYomitanExtensionPath(resolvedProfilePath, fs.existsSync);
return null; if (!extPath) {
} logger.error('External Yomitan extension not found in configured profile path');
logger.error('Expected unpacked extension at:', path.join(resolvedProfilePath, 'extensions'));
deps.setYomitanExtension(null);
deps.setYomitanSession(null);
return null;
}
const extensionCopy = ensureExtensionCopy(extPath, deps.userDataPath); targetSession = session.fromPath(resolvedProfilePath);
if (extensionCopy.copied) { } else {
logger.info(`Copied yomitan extension to ${extensionCopy.targetDir}`); const searchPaths = getYomitanExtensionSearchPaths({
explicitPath: deps.extensionPath,
moduleDir: __dirname,
resourcesPath: process.resourcesPath,
userDataPath: deps.userDataPath,
});
extPath = resolveExistingYomitanExtensionPath(searchPaths, fs.existsSync);
if (!extPath) {
logger.error('Yomitan extension not found in any search path');
logger.error('Run `bun run build:yomitan` or install Yomitan to one of:', searchPaths);
deps.setYomitanExtension(null);
deps.setYomitanSession(null);
return null;
}
const extensionCopy = ensureExtensionCopy(extPath, deps.userDataPath);
if (extensionCopy.copied) {
logger.info(`Copied yomitan extension to ${extensionCopy.targetDir}`);
}
extPath = extensionCopy.targetDir;
} }
extPath = extensionCopy.targetDir;
const parserWindow = deps.getYomitanParserWindow(); const parserWindow = deps.getYomitanParserWindow();
if (parserWindow && !parserWindow.isDestroyed()) { if (parserWindow && !parserWindow.isDestroyed()) {
@@ -51,14 +75,15 @@ export async function loadYomitanExtension(
deps.setYomitanParserWindow(null); deps.setYomitanParserWindow(null);
deps.setYomitanParserReadyPromise(null); deps.setYomitanParserReadyPromise(null);
deps.setYomitanParserInitPromise(null); deps.setYomitanParserInitPromise(null);
deps.setYomitanSession(targetSession);
try { try {
const extensions = session.defaultSession.extensions; const extensions = targetSession.extensions;
const extension = extensions const extension = extensions
? await extensions.loadExtension(extPath, { ? await extensions.loadExtension(extPath, {
allowFileAccess: true, allowFileAccess: true,
}) })
: await session.defaultSession.loadExtension(extPath, { : await targetSession.loadExtension(extPath, {
allowFileAccess: true, allowFileAccess: true,
}); });
deps.setYomitanExtension(extension); deps.setYomitanExtension(extension);
@@ -67,6 +92,7 @@ export async function loadYomitanExtension(
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); deps.setYomitanExtension(null);
deps.setYomitanSession(null);
return null; return null;
} }
} }

View File

@@ -4,6 +4,7 @@ import test from 'node:test';
import { import {
getYomitanExtensionSearchPaths, getYomitanExtensionSearchPaths,
resolveExternalYomitanExtensionPath,
resolveExistingYomitanExtensionPath, resolveExistingYomitanExtensionPath,
} from './yomitan-extension-paths'; } from './yomitan-extension-paths';
@@ -51,3 +52,19 @@ test('resolveExistingYomitanExtensionPath ignores source tree without built mani
assert.equal(resolved, null); assert.equal(resolved, null);
}); });
test('resolveExternalYomitanExtensionPath returns external extension dir when manifest exists', () => {
const profilePath = path.join('/Users', 'kyle', '.local', 'share', 'gsm-profile');
const resolved = resolveExternalYomitanExtensionPath(profilePath, (candidate) =>
candidate === path.join(profilePath, 'extensions', 'yomitan', 'manifest.json'),
);
assert.equal(resolved, path.join(profilePath, 'extensions', 'yomitan'));
});
test('resolveExternalYomitanExtensionPath returns null when external profile has no extension', () => {
const profilePath = path.join('/Users', 'kyle', '.local', 'share', 'gsm-profile');
const resolved = resolveExternalYomitanExtensionPath(profilePath, () => false);
assert.equal(resolved, null);
});

View File

@@ -58,3 +58,16 @@ export function resolveYomitanExtensionPath(
): string | null { ): string | null {
return resolveExistingYomitanExtensionPath(getYomitanExtensionSearchPaths(options), existsSync); return resolveExistingYomitanExtensionPath(getYomitanExtensionSearchPaths(options), existsSync);
} }
export function resolveExternalYomitanExtensionPath(
externalProfilePath: string,
existsSync: (path: string) => boolean = fs.existsSync,
): string | null {
const normalizedProfilePath = externalProfilePath.trim();
if (!normalizedProfilePath) {
return null;
}
const candidate = path.join(path.resolve(normalizedProfilePath), 'extensions', 'yomitan');
return existsSync(path.join(candidate, 'manifest.json')) ? candidate : null;
}

View File

@@ -1,5 +1,5 @@
import electron from 'electron'; import electron from 'electron';
import type { BrowserWindow, Extension } from 'electron'; import type { BrowserWindow, Extension, Session } from 'electron';
import { createLogger } from '../../logger'; import { createLogger } from '../../logger';
const { BrowserWindow: ElectronBrowserWindow, session } = electron; const { BrowserWindow: ElectronBrowserWindow, session } = electron;
@@ -9,6 +9,7 @@ export interface OpenYomitanSettingsWindowOptions {
yomitanExt: Extension | null; yomitanExt: Extension | null;
getExistingWindow: () => BrowserWindow | null; getExistingWindow: () => BrowserWindow | null;
setWindow: (window: BrowserWindow | null) => void; setWindow: (window: BrowserWindow | null) => void;
yomitanSession?: Session | null;
onWindowClosed?: () => void; onWindowClosed?: () => void;
} }
@@ -37,7 +38,7 @@ export function openYomitanSettingsWindow(options: OpenYomitanSettingsWindowOpti
webPreferences: { webPreferences: {
contextIsolation: true, contextIsolation: true,
nodeIntegration: false, nodeIntegration: false,
session: session.defaultSession, session: options.yomitanSession ?? session.defaultSession,
}, },
}); });
options.setWindow(settingsWindow); options.setWindow(settingsWindow);

View File

@@ -1346,6 +1346,10 @@ const characterDictionaryAutoSyncRuntime = createCharacterDictionaryAutoSyncRunt
}); });
}, },
importYomitanDictionary: async (zipPath) => { importYomitanDictionary: async (zipPath) => {
if (isYomitanExternalReadOnlyMode()) {
logSkippedYomitanWrite(`importYomitanDictionary(${zipPath})`);
return false;
}
await ensureYomitanExtensionLoaded(); await ensureYomitanExtensionLoaded();
return await importYomitanDictionaryFromZip(zipPath, getYomitanParserRuntimeDeps(), { return await importYomitanDictionaryFromZip(zipPath, getYomitanParserRuntimeDeps(), {
error: (message, ...args) => logger.error(message, ...args), error: (message, ...args) => logger.error(message, ...args),
@@ -1353,6 +1357,10 @@ const characterDictionaryAutoSyncRuntime = createCharacterDictionaryAutoSyncRunt
}); });
}, },
deleteYomitanDictionary: async (dictionaryTitle) => { deleteYomitanDictionary: async (dictionaryTitle) => {
if (isYomitanExternalReadOnlyMode()) {
logSkippedYomitanWrite(`deleteYomitanDictionary(${dictionaryTitle})`);
return false;
}
await ensureYomitanExtensionLoaded(); await ensureYomitanExtensionLoaded();
return await deleteYomitanDictionaryByTitle(dictionaryTitle, getYomitanParserRuntimeDeps(), { return await deleteYomitanDictionaryByTitle(dictionaryTitle, getYomitanParserRuntimeDeps(), {
error: (message, ...args) => logger.error(message, ...args), error: (message, ...args) => logger.error(message, ...args),
@@ -1360,6 +1368,10 @@ const characterDictionaryAutoSyncRuntime = createCharacterDictionaryAutoSyncRunt
}); });
}, },
upsertYomitanDictionarySettings: async (dictionaryTitle, profileScope) => { upsertYomitanDictionarySettings: async (dictionaryTitle, profileScope) => {
if (isYomitanExternalReadOnlyMode()) {
logSkippedYomitanWrite(`upsertYomitanDictionarySettings(${dictionaryTitle})`);
return false;
}
await ensureYomitanExtensionLoaded(); await ensureYomitanExtensionLoaded();
return await upsertYomitanDictionarySettings( return await upsertYomitanDictionarySettings(
dictionaryTitle, dictionaryTitle,
@@ -2319,6 +2331,7 @@ const {
appState.yomitanParserWindow = null; appState.yomitanParserWindow = null;
appState.yomitanParserReadyPromise = null; appState.yomitanParserReadyPromise = null;
appState.yomitanParserInitPromise = null; appState.yomitanParserInitPromise = null;
appState.yomitanSession = null;
}, },
getWindowTracker: () => appState.windowTracker, getWindowTracker: () => appState.windowTracker,
flushMpvLog: () => flushPendingMpvLogWrites(), flushMpvLog: () => flushPendingMpvLogWrites(),
@@ -2779,6 +2792,7 @@ const {
tokenizer: { tokenizer: {
buildTokenizerDepsMainDeps: { buildTokenizerDepsMainDeps: {
getYomitanExt: () => appState.yomitanExt, getYomitanExt: () => appState.yomitanExt,
getYomitanSession: () => appState.yomitanSession,
getYomitanParserWindow: () => appState.yomitanParserWindow, getYomitanParserWindow: () => appState.yomitanParserWindow,
setYomitanParserWindow: (window) => { setYomitanParserWindow: (window) => {
appState.yomitanParserWindow = window as BrowserWindow | null; appState.yomitanParserWindow = window as BrowserWindow | null;
@@ -2986,7 +3000,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) { if (extension && !isYomitanExternalReadOnlyMode()) {
await syncYomitanDefaultProfileAnkiServer(); await syncYomitanDefaultProfileAnkiServer();
} }
return extension; return extension;
@@ -2994,7 +3008,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) { if (extension && !isYomitanExternalReadOnlyMode()) {
await syncYomitanDefaultProfileAnkiServer(); await syncYomitanDefaultProfileAnkiServer();
} }
return extension; return extension;
@@ -3006,9 +3020,24 @@ function getPreferredYomitanAnkiServerUrl(): string {
return getPreferredYomitanAnkiServerUrlRuntime(getResolvedConfig().ankiConnect); return getPreferredYomitanAnkiServerUrlRuntime(getResolvedConfig().ankiConnect);
} }
function getConfiguredExternalYomitanProfilePath(): string {
return getResolvedConfig().yomitan.externalProfilePath.trim();
}
function isYomitanExternalReadOnlyMode(): boolean {
return getConfiguredExternalYomitanProfilePath().length > 0;
}
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,
getYomitanSession: () => appState.yomitanSession,
getYomitanParserWindow: () => appState.yomitanParserWindow, getYomitanParserWindow: () => appState.yomitanParserWindow,
setYomitanParserWindow: (window: BrowserWindow | null) => { setYomitanParserWindow: (window: BrowserWindow | null) => {
appState.yomitanParserWindow = window; appState.yomitanParserWindow = window;
@@ -3025,6 +3054,10 @@ function getYomitanParserRuntimeDeps() {
} }
async function syncYomitanDefaultProfileAnkiServer(): Promise<void> { async function syncYomitanDefaultProfileAnkiServer(): Promise<void> {
if (isYomitanExternalReadOnlyMode()) {
return;
}
const targetUrl = getPreferredYomitanAnkiServerUrl().trim(); const targetUrl = getPreferredYomitanAnkiServerUrl().trim();
if (!targetUrl || targetUrl === lastSyncedYomitanAnkiServer) { if (!targetUrl || targetUrl === lastSyncedYomitanAnkiServer) {
return; return;
@@ -3079,6 +3112,12 @@ function initializeOverlayRuntime(): void {
} }
function openYomitanSettings(): void { function openYomitanSettings(): void {
if (isYomitanExternalReadOnlyMode()) {
logger.warn(
'Yomitan settings window disabled while yomitan.externalProfilePath is configured because external profile mode is read-only.',
);
return;
}
openYomitanSettingsHandler(); openYomitanSettingsHandler();
} }
@@ -3577,6 +3616,7 @@ const { ensureTray: ensureTrayHandler, destroyTray: destroyTrayHandler } =
const yomitanExtensionRuntime = createYomitanExtensionRuntime({ const yomitanExtensionRuntime = createYomitanExtensionRuntime({
loadYomitanExtensionCore, loadYomitanExtensionCore,
userDataPath: USER_DATA_PATH, userDataPath: USER_DATA_PATH,
externalProfilePath: getConfiguredExternalYomitanProfilePath(),
getYomitanParserWindow: () => appState.yomitanParserWindow, getYomitanParserWindow: () => appState.yomitanParserWindow,
setYomitanParserWindow: (window) => { setYomitanParserWindow: (window) => {
appState.yomitanParserWindow = window as BrowserWindow | null; appState.yomitanParserWindow = window as BrowserWindow | null;
@@ -3590,6 +3630,9 @@ const yomitanExtensionRuntime = createYomitanExtensionRuntime({
setYomitanExtension: (extension) => { setYomitanExtension: (extension) => {
appState.yomitanExt = extension; appState.yomitanExt = extension;
}, },
setYomitanSession: (nextSession) => {
appState.yomitanSession = nextSession;
},
getYomitanExtension: () => appState.yomitanExt, getYomitanExtension: () => appState.yomitanExt,
getLoadInFlight: () => yomitanLoadInFlight, getLoadInFlight: () => yomitanLoadInFlight,
setLoadInFlight: (promise) => { setLoadInFlight: (promise) => {
@@ -3636,6 +3679,7 @@ const { openYomitanSettings: openYomitanSettingsHandler } = createYomitanSetting
yomitanExt: yomitanExt as Extension, yomitanExt: yomitanExt as Extension,
getExistingWindow: () => getExistingWindow() as BrowserWindow | null, getExistingWindow: () => getExistingWindow() as BrowserWindow | null,
setWindow: (window) => setWindow(window as BrowserWindow | null), setWindow: (window) => setWindow(window as BrowserWindow | null),
yomitanSession: appState.yomitanSession,
onWindowClosed: () => { onWindowClosed: () => {
if (appState.yomitanParserWindow) { if (appState.yomitanParserWindow) {
clearYomitanParserCachesForWindow(appState.yomitanParserWindow); clearYomitanParserCachesForWindow(appState.yomitanParserWindow);

View File

@@ -23,6 +23,7 @@ type TokenizerMainDeps = TokenizerDepsRuntimeOptions & {
export function createBuildTokenizerDepsMainHandler(deps: TokenizerMainDeps) { export function createBuildTokenizerDepsMainHandler(deps: TokenizerMainDeps) {
return (): TokenizerDepsRuntimeOptions => ({ return (): TokenizerDepsRuntimeOptions => ({
getYomitanExt: () => deps.getYomitanExt(), getYomitanExt: () => deps.getYomitanExt(),
getYomitanSession: () => deps.getYomitanSession?.() ?? null,
getYomitanParserWindow: () => deps.getYomitanParserWindow(), getYomitanParserWindow: () => deps.getYomitanParserWindow(),
setYomitanParserWindow: (window) => deps.setYomitanParserWindow(window), setYomitanParserWindow: (window) => deps.setYomitanParserWindow(window),
getYomitanParserReadyPromise: () => deps.getYomitanParserReadyPromise(), getYomitanParserReadyPromise: () => deps.getYomitanParserReadyPromise(),

View File

@@ -13,20 +13,31 @@ test('load yomitan extension main deps builder maps callbacks', async () => {
return null; return null;
}, },
userDataPath: '/tmp/subminer', userDataPath: '/tmp/subminer',
externalProfilePath: '/tmp/gsm-profile',
getYomitanParserWindow: () => null, getYomitanParserWindow: () => null,
setYomitanParserWindow: () => calls.push('set-window'), setYomitanParserWindow: () => calls.push('set-window'),
setYomitanParserReadyPromise: () => calls.push('set-ready'), setYomitanParserReadyPromise: () => calls.push('set-ready'),
setYomitanParserInitPromise: () => calls.push('set-init'), setYomitanParserInitPromise: () => calls.push('set-init'),
setYomitanExtension: () => calls.push('set-ext'), setYomitanExtension: () => calls.push('set-ext'),
setYomitanSession: () => calls.push('set-session'),
})(); })();
assert.equal(deps.userDataPath, '/tmp/subminer'); assert.equal(deps.userDataPath, '/tmp/subminer');
assert.equal(deps.externalProfilePath, '/tmp/gsm-profile');
await deps.loadYomitanExtensionCore({} as never); await deps.loadYomitanExtensionCore({} as never);
deps.setYomitanParserWindow(null); deps.setYomitanParserWindow(null);
deps.setYomitanParserReadyPromise(null); deps.setYomitanParserReadyPromise(null);
deps.setYomitanParserInitPromise(null); deps.setYomitanParserInitPromise(null);
deps.setYomitanExtension(null); deps.setYomitanExtension(null);
assert.deepEqual(calls, ['load-core', 'set-window', 'set-ready', 'set-init', 'set-ext']); deps.setYomitanSession(null as never);
assert.deepEqual(calls, [
'load-core',
'set-window',
'set-ready',
'set-init',
'set-ext',
'set-session',
]);
}); });
test('ensure yomitan extension loaded main deps builder maps callbacks', async () => { test('ensure yomitan extension loaded main deps builder maps callbacks', async () => {

View File

@@ -12,11 +12,13 @@ export function createBuildLoadYomitanExtensionMainDepsHandler(deps: LoadYomitan
return (): LoadYomitanExtensionMainDeps => ({ return (): LoadYomitanExtensionMainDeps => ({
loadYomitanExtensionCore: (options) => deps.loadYomitanExtensionCore(options), loadYomitanExtensionCore: (options) => deps.loadYomitanExtensionCore(options),
userDataPath: deps.userDataPath, userDataPath: deps.userDataPath,
externalProfilePath: deps.externalProfilePath,
getYomitanParserWindow: () => deps.getYomitanParserWindow(), getYomitanParserWindow: () => deps.getYomitanParserWindow(),
setYomitanParserWindow: (window) => deps.setYomitanParserWindow(window), setYomitanParserWindow: (window) => deps.setYomitanParserWindow(window),
setYomitanParserReadyPromise: (promise) => deps.setYomitanParserReadyPromise(promise), setYomitanParserReadyPromise: (promise) => deps.setYomitanParserReadyPromise(promise),
setYomitanParserInitPromise: (promise) => deps.setYomitanParserInitPromise(promise), setYomitanParserInitPromise: (promise) => deps.setYomitanParserInitPromise(promise),
setYomitanExtension: (extension) => deps.setYomitanExtension(extension), setYomitanExtension: (extension) => deps.setYomitanExtension(extension),
setYomitanSession: (session) => deps.setYomitanSession(session),
}); });
} }

View File

@@ -12,23 +12,35 @@ test('load yomitan extension handler forwards parser state dependencies', async
const loadYomitanExtension = createLoadYomitanExtensionHandler({ const loadYomitanExtension = createLoadYomitanExtensionHandler({
loadYomitanExtensionCore: async (options) => { loadYomitanExtensionCore: async (options) => {
calls.push(`path:${options.userDataPath}`); calls.push(`path:${options.userDataPath}`);
calls.push(`external:${options.externalProfilePath ?? ''}`);
assert.equal(options.getYomitanParserWindow(), parserWindow); assert.equal(options.getYomitanParserWindow(), parserWindow);
options.setYomitanParserWindow(null); options.setYomitanParserWindow(null);
options.setYomitanParserReadyPromise(null); options.setYomitanParserReadyPromise(null);
options.setYomitanParserInitPromise(null); options.setYomitanParserInitPromise(null);
options.setYomitanExtension(extension); options.setYomitanExtension(extension);
options.setYomitanSession(null);
return extension; return extension;
}, },
userDataPath: '/tmp/subminer', userDataPath: '/tmp/subminer',
externalProfilePath: '/tmp/gsm-profile',
getYomitanParserWindow: () => parserWindow, getYomitanParserWindow: () => parserWindow,
setYomitanParserWindow: () => calls.push('set-window'), setYomitanParserWindow: () => calls.push('set-window'),
setYomitanParserReadyPromise: () => calls.push('set-ready'), setYomitanParserReadyPromise: () => calls.push('set-ready'),
setYomitanParserInitPromise: () => calls.push('set-init'), setYomitanParserInitPromise: () => calls.push('set-init'),
setYomitanExtension: () => calls.push('set-ext'), setYomitanExtension: () => calls.push('set-ext'),
setYomitanSession: () => calls.push('set-session'),
}); });
assert.equal(await loadYomitanExtension(), extension); assert.equal(await loadYomitanExtension(), extension);
assert.deepEqual(calls, ['path:/tmp/subminer', 'set-window', 'set-ready', 'set-init', 'set-ext']); assert.deepEqual(calls, [
'path:/tmp/subminer',
'external:/tmp/gsm-profile',
'set-window',
'set-ready',
'set-init',
'set-ext',
'set-session',
]);
}); });
test('ensure yomitan loader returns existing extension when available', async () => { test('ensure yomitan loader returns existing extension when available', async () => {

View File

@@ -4,20 +4,24 @@ import type { YomitanExtensionLoaderDeps } from '../../core/services/yomitan-ext
export function createLoadYomitanExtensionHandler(deps: { export function createLoadYomitanExtensionHandler(deps: {
loadYomitanExtensionCore: (options: YomitanExtensionLoaderDeps) => Promise<Extension | null>; loadYomitanExtensionCore: (options: YomitanExtensionLoaderDeps) => Promise<Extension | null>;
userDataPath: YomitanExtensionLoaderDeps['userDataPath']; userDataPath: YomitanExtensionLoaderDeps['userDataPath'];
externalProfilePath?: YomitanExtensionLoaderDeps['externalProfilePath'];
getYomitanParserWindow: YomitanExtensionLoaderDeps['getYomitanParserWindow']; getYomitanParserWindow: YomitanExtensionLoaderDeps['getYomitanParserWindow'];
setYomitanParserWindow: YomitanExtensionLoaderDeps['setYomitanParserWindow']; setYomitanParserWindow: YomitanExtensionLoaderDeps['setYomitanParserWindow'];
setYomitanParserReadyPromise: YomitanExtensionLoaderDeps['setYomitanParserReadyPromise']; setYomitanParserReadyPromise: YomitanExtensionLoaderDeps['setYomitanParserReadyPromise'];
setYomitanParserInitPromise: YomitanExtensionLoaderDeps['setYomitanParserInitPromise']; setYomitanParserInitPromise: YomitanExtensionLoaderDeps['setYomitanParserInitPromise'];
setYomitanExtension: YomitanExtensionLoaderDeps['setYomitanExtension']; setYomitanExtension: YomitanExtensionLoaderDeps['setYomitanExtension'];
setYomitanSession: YomitanExtensionLoaderDeps['setYomitanSession'];
}) { }) {
return async (): Promise<Extension | null> => { return async (): Promise<Extension | null> => {
return deps.loadYomitanExtensionCore({ return deps.loadYomitanExtensionCore({
userDataPath: deps.userDataPath, userDataPath: deps.userDataPath,
externalProfilePath: deps.externalProfilePath,
getYomitanParserWindow: deps.getYomitanParserWindow, getYomitanParserWindow: deps.getYomitanParserWindow,
setYomitanParserWindow: deps.setYomitanParserWindow, setYomitanParserWindow: deps.setYomitanParserWindow,
setYomitanParserReadyPromise: deps.setYomitanParserReadyPromise, setYomitanParserReadyPromise: deps.setYomitanParserReadyPromise,
setYomitanParserInitPromise: deps.setYomitanParserInitPromise, setYomitanParserInitPromise: deps.setYomitanParserInitPromise,
setYomitanExtension: deps.setYomitanExtension, setYomitanExtension: deps.setYomitanExtension,
setYomitanSession: deps.setYomitanSession,
}); });
}; };
} }

View File

@@ -9,6 +9,7 @@ test('yomitan extension runtime reuses in-flight ensure load and clears it after
let parserWindow: unknown = null; let parserWindow: unknown = null;
let readyPromise: Promise<void> | null = null; let readyPromise: Promise<void> | null = null;
let initPromise: Promise<boolean> | null = null; let initPromise: Promise<boolean> | null = null;
let yomitanSession: unknown = null;
let loadCalls = 0; let loadCalls = 0;
const releaseLoadState: { releaseLoad: ((value: Extension | null) => void) | null } = { const releaseLoadState: { releaseLoad: ((value: Extension | null) => void) | null } = {
releaseLoad: null, releaseLoad: null,
@@ -28,6 +29,7 @@ test('yomitan extension runtime reuses in-flight ensure load and clears it after
}); });
}, },
userDataPath: '/tmp', userDataPath: '/tmp',
externalProfilePath: '/tmp/gsm-profile',
getYomitanParserWindow: () => parserWindow as never, getYomitanParserWindow: () => parserWindow as never,
setYomitanParserWindow: (window) => { setYomitanParserWindow: (window) => {
parserWindow = window; parserWindow = window;
@@ -41,6 +43,9 @@ test('yomitan extension runtime reuses in-flight ensure load and clears it after
setYomitanExtension: (next) => { setYomitanExtension: (next) => {
extension = next; extension = next;
}, },
setYomitanSession: (next) => {
yomitanSession = next;
},
getYomitanExtension: () => extension, getYomitanExtension: () => extension,
getLoadInFlight: () => inFlight, getLoadInFlight: () => inFlight,
setLoadInFlight: (promise) => { setLoadInFlight: (promise) => {
@@ -55,6 +60,7 @@ test('yomitan extension runtime reuses in-flight ensure load and clears it after
assert.equal(parserWindow, null); assert.equal(parserWindow, null);
assert.ok(readyPromise); assert.ok(readyPromise);
assert.ok(initPromise); assert.ok(initPromise);
assert.equal(yomitanSession, null);
const fakeExtension = { id: 'yomitan' } as Extension; const fakeExtension = { id: 'yomitan' } as Extension;
const releaseLoad = releaseLoadState.releaseLoad; const releaseLoad = releaseLoadState.releaseLoad;
@@ -81,11 +87,13 @@ test('yomitan extension runtime direct load delegates to core', async () => {
return null; return null;
}, },
userDataPath: '/tmp', userDataPath: '/tmp',
externalProfilePath: '',
getYomitanParserWindow: () => null, getYomitanParserWindow: () => null,
setYomitanParserWindow: () => {}, setYomitanParserWindow: () => {},
setYomitanParserReadyPromise: () => {}, setYomitanParserReadyPromise: () => {},
setYomitanParserInitPromise: () => {}, setYomitanParserInitPromise: () => {},
setYomitanExtension: () => {}, setYomitanExtension: () => {},
setYomitanSession: () => {},
getYomitanExtension: () => null, getYomitanExtension: () => null,
getLoadInFlight: () => null, getLoadInFlight: () => null,
setLoadInFlight: () => {}, setLoadInFlight: () => {},

View File

@@ -23,11 +23,13 @@ export function createYomitanExtensionRuntime(deps: YomitanExtensionRuntimeDeps)
const buildLoadYomitanExtensionMainDepsHandler = createBuildLoadYomitanExtensionMainDepsHandler({ const buildLoadYomitanExtensionMainDepsHandler = createBuildLoadYomitanExtensionMainDepsHandler({
loadYomitanExtensionCore: deps.loadYomitanExtensionCore, loadYomitanExtensionCore: deps.loadYomitanExtensionCore,
userDataPath: deps.userDataPath, userDataPath: deps.userDataPath,
externalProfilePath: deps.externalProfilePath,
getYomitanParserWindow: deps.getYomitanParserWindow, getYomitanParserWindow: deps.getYomitanParserWindow,
setYomitanParserWindow: deps.setYomitanParserWindow, setYomitanParserWindow: deps.setYomitanParserWindow,
setYomitanParserReadyPromise: deps.setYomitanParserReadyPromise, setYomitanParserReadyPromise: deps.setYomitanParserReadyPromise,
setYomitanParserInitPromise: deps.setYomitanParserInitPromise, setYomitanParserInitPromise: deps.setYomitanParserInitPromise,
setYomitanExtension: deps.setYomitanExtension, setYomitanExtension: deps.setYomitanExtension,
setYomitanSession: deps.setYomitanSession,
}); });
const loadYomitanExtensionHandler = createLoadYomitanExtensionHandler( const loadYomitanExtensionHandler = createLoadYomitanExtensionHandler(
buildLoadYomitanExtensionMainDepsHandler(), buildLoadYomitanExtensionMainDepsHandler(),

View File

@@ -1,4 +1,4 @@
import type { BrowserWindow, Extension } from 'electron'; import type { BrowserWindow, Extension, Session } from 'electron';
import type { import type {
Keybinding, Keybinding,
@@ -143,6 +143,7 @@ export function transitionAnilistUpdateInFlightState(
export interface AppState { export interface AppState {
yomitanExt: Extension | null; yomitanExt: Extension | null;
yomitanSession: Session | null;
yomitanSettingsWindow: BrowserWindow | null; yomitanSettingsWindow: BrowserWindow | null;
yomitanParserWindow: BrowserWindow | null; yomitanParserWindow: BrowserWindow | null;
anilistSetupWindow: BrowserWindow | null; anilistSetupWindow: BrowserWindow | null;
@@ -219,6 +220,7 @@ export interface StartupState {
export function createAppState(values: AppStateInitialValues): AppState { export function createAppState(values: AppStateInitialValues): AppState {
return { return {
yomitanExt: null, yomitanExt: null,
yomitanSession: null,
yomitanSettingsWindow: null, yomitanSettingsWindow: null,
yomitanParserWindow: null, yomitanParserWindow: null,
anilistSetupWindow: null, anilistSetupWindow: null,

View File

@@ -413,6 +413,10 @@ export interface AnilistConfig {
characterDictionary?: AnilistCharacterDictionaryConfig; characterDictionary?: AnilistCharacterDictionaryConfig;
} }
export interface YomitanConfig {
externalProfilePath?: string;
}
export interface JellyfinConfig { export interface JellyfinConfig {
enabled?: boolean; enabled?: boolean;
serverUrl?: string; serverUrl?: string;
@@ -496,6 +500,7 @@ export interface Config {
auto_start_overlay?: boolean; auto_start_overlay?: boolean;
jimaku?: JimakuConfig; jimaku?: JimakuConfig;
anilist?: AnilistConfig; anilist?: AnilistConfig;
yomitan?: YomitanConfig;
jellyfin?: JellyfinConfig; jellyfin?: JellyfinConfig;
discordPresence?: DiscordPresenceConfig; discordPresence?: DiscordPresenceConfig;
ai?: AiConfig; ai?: AiConfig;
@@ -621,6 +626,9 @@ export interface ResolvedConfig {
collapsibleSections: Required<AnilistCharacterDictionaryCollapsibleSectionsConfig>; collapsibleSections: Required<AnilistCharacterDictionaryCollapsibleSectionsConfig>;
}; };
}; };
yomitan: {
externalProfilePath: string;
};
jellyfin: { jellyfin: {
enabled: boolean; enabled: boolean;
serverUrl: string; serverUrl: string;