feat(yomitan): add read-only external profile support for shared dictionaries (#18)

This commit is contained in:
2026-03-12 01:17:34 -07:00
committed by GitHub
parent 68833c76c4
commit 1b56360a24
67 changed files with 1230 additions and 135 deletions

View File

@@ -65,7 +65,7 @@ test('ensureDefaultConfigBootstrap creates config dir and default jsonc only whe
});
});
test('ensureDefaultConfigBootstrap does not seed default config into an existing config directory', () => {
test('ensureDefaultConfigBootstrap seeds default config into an existing config directory when missing', () => {
withTempDir((root) => {
const configDir = path.join(root, 'SubMiner');
fs.mkdirSync(configDir, { recursive: true });
@@ -74,10 +74,13 @@ test('ensureDefaultConfigBootstrap does not seed default config into an existing
ensureDefaultConfigBootstrap({
configDir,
configFilePaths: getDefaultConfigFilePaths(configDir),
generateTemplate: () => 'should-not-write',
generateTemplate: () => '{\n "logging": {}\n}\n',
});
assert.equal(fs.existsSync(path.join(configDir, 'config.jsonc')), false);
assert.equal(
fs.readFileSync(path.join(configDir, 'config.jsonc'), 'utf8'),
'{\n "logging": {}\n}\n',
);
assert.equal(fs.readFileSync(path.join(configDir, 'existing-user-file.txt'), 'utf8'), 'keep\n');
});
});
@@ -91,6 +94,7 @@ test('readSetupState ignores invalid files and round-trips valid state', () => {
const state = createDefaultSetupState();
state.status = 'completed';
state.completionSource = 'user';
state.yomitanSetupMode = 'internal';
state.lastSeenYomitanDictionaryCount = 2;
writeSetupState(statePath, state);
@@ -98,7 +102,7 @@ test('readSetupState ignores invalid files and round-trips valid state', () => {
});
});
test('readSetupState migrates v1 state to v2 windows shortcut defaults', () => {
test('readSetupState migrates v1 state to v3 windows shortcut defaults', () => {
withTempDir((root) => {
const statePath = getSetupStatePath(root);
fs.writeFileSync(
@@ -115,10 +119,11 @@ test('readSetupState migrates v1 state to v2 windows shortcut defaults', () => {
);
assert.deepEqual(readSetupState(statePath), {
version: 2,
version: 3,
status: 'incomplete',
completedAt: null,
completionSource: null,
yomitanSetupMode: null,
lastSeenYomitanDictionaryCount: 0,
pluginInstallStatus: 'unknown',
pluginInstallPathSummary: null,
@@ -131,6 +136,45 @@ test('readSetupState migrates v1 state to v2 windows shortcut defaults', () => {
});
});
test('readSetupState migrates completed v2 state to internal yomitan setup mode', () => {
withTempDir((root) => {
const statePath = getSetupStatePath(root);
fs.writeFileSync(
statePath,
JSON.stringify({
version: 2,
status: 'completed',
completedAt: '2026-03-12T00:00:00.000Z',
completionSource: 'user',
lastSeenYomitanDictionaryCount: 1,
pluginInstallStatus: 'unknown',
pluginInstallPathSummary: null,
windowsMpvShortcutPreferences: {
startMenuEnabled: true,
desktopEnabled: true,
},
windowsMpvShortcutLastStatus: 'unknown',
}),
);
assert.deepEqual(readSetupState(statePath), {
version: 3,
status: 'completed',
completedAt: '2026-03-12T00:00:00.000Z',
completionSource: 'user',
yomitanSetupMode: 'internal',
lastSeenYomitanDictionaryCount: 1,
pluginInstallStatus: 'unknown',
pluginInstallPathSummary: null,
windowsMpvShortcutPreferences: {
startMenuEnabled: true,
desktopEnabled: true,
},
windowsMpvShortcutLastStatus: 'unknown',
});
});
});
test('resolveDefaultMpvInstallPaths resolves linux, macOS, and Windows defaults', () => {
const linuxHomeDir = path.join(path.sep, 'tmp', 'home');
const xdgConfigHome = path.join(path.sep, 'tmp', 'xdg');

View File

@@ -5,6 +5,7 @@ import { resolveConfigDir } from '../config/path-resolution';
export type SetupStateStatus = 'incomplete' | 'in_progress' | 'completed' | 'cancelled';
export type SetupCompletionSource = 'user' | 'legacy_auto_detected' | null;
export type SetupYomitanMode = 'internal' | 'external' | null;
export type SetupPluginInstallStatus = 'unknown' | 'installed' | 'skipped' | 'failed';
export type SetupWindowsMpvShortcutInstallStatus = 'unknown' | 'installed' | 'skipped' | 'failed';
@@ -14,10 +15,11 @@ export interface SetupWindowsMpvShortcutPreferences {
}
export interface SetupState {
version: 2;
version: 3;
status: SetupStateStatus;
completedAt: string | null;
completionSource: SetupCompletionSource;
yomitanSetupMode: SetupYomitanMode;
lastSeenYomitanDictionaryCount: number;
pluginInstallStatus: SetupPluginInstallStatus;
pluginInstallPathSummary: string | null;
@@ -52,10 +54,11 @@ function asObject(value: unknown): Record<string, unknown> | null {
export function createDefaultSetupState(): SetupState {
return {
version: 2,
version: 3,
status: 'incomplete',
completedAt: null,
completionSource: null,
yomitanSetupMode: null,
lastSeenYomitanDictionaryCount: 0,
pluginInstallStatus: 'unknown',
pluginInstallPathSummary: null,
@@ -74,11 +77,12 @@ export function normalizeSetupState(value: unknown): SetupState | null {
const status = record.status;
const pluginInstallStatus = record.pluginInstallStatus;
const completionSource = record.completionSource;
const yomitanSetupMode = record.yomitanSetupMode;
const windowsPrefs = asObject(record.windowsMpvShortcutPreferences);
const windowsMpvShortcutLastStatus = record.windowsMpvShortcutLastStatus;
if (
(version !== 1 && version !== 2) ||
(version !== 1 && version !== 2 && version !== 3) ||
(status !== 'incomplete' &&
status !== 'in_progress' &&
status !== 'completed' &&
@@ -94,16 +98,26 @@ export function normalizeSetupState(value: unknown): SetupState | null {
windowsMpvShortcutLastStatus !== 'failed') ||
(completionSource !== null &&
completionSource !== 'user' &&
completionSource !== 'legacy_auto_detected')
completionSource !== 'legacy_auto_detected') ||
(version === 3 &&
yomitanSetupMode !== null &&
yomitanSetupMode !== 'internal' &&
yomitanSetupMode !== 'external')
) {
return null;
}
return {
version: 2,
version: 3,
status,
completedAt: typeof record.completedAt === 'string' ? record.completedAt : null,
completionSource,
yomitanSetupMode:
version === 3 && (yomitanSetupMode === 'internal' || yomitanSetupMode === 'external')
? yomitanSetupMode
: status === 'completed'
? 'internal'
: null,
lastSeenYomitanDictionaryCount:
typeof record.lastSeenYomitanDictionaryCount === 'number' &&
Number.isFinite(record.lastSeenYomitanDictionaryCount) &&
@@ -208,13 +222,8 @@ export function ensureDefaultConfigBootstrap(options: {
const existsSync = options.existsSync ?? fs.existsSync;
const mkdirSync = options.mkdirSync ?? fs.mkdirSync;
const writeFileSync = options.writeFileSync ?? fs.writeFileSync;
const configDirExists = existsSync(options.configDir);
if (
existsSync(options.configFilePaths.jsoncPath) ||
existsSync(options.configFilePaths.jsonPath) ||
configDirExists
) {
if (existsSync(options.configFilePaths.jsoncPath) || existsSync(options.configFilePaths.jsonPath)) {
return;
}