mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -07:00
feat: add auto update support
This commit is contained in:
@@ -70,6 +70,8 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
texthooker: false,
|
||||
texthookerOpenBrowser: false,
|
||||
help: false,
|
||||
update: false,
|
||||
updateLauncherPath: undefined,
|
||||
autoStartOverlay: false,
|
||||
generateConfig: false,
|
||||
backupOverwrite: false,
|
||||
@@ -231,6 +233,9 @@ function createDeps(overrides: Partial<CliCommandServiceDeps> = {}) {
|
||||
runYoutubePlaybackFlow: async (request) => {
|
||||
calls.push(`runYoutubePlaybackFlow:${request.url}:${request.mode}:${request.source}`);
|
||||
},
|
||||
runUpdateCommand: async (args) => {
|
||||
calls.push(`runUpdateCommand:${args.updateLauncherPath ?? ''}`);
|
||||
},
|
||||
printHelp: () => {
|
||||
calls.push('printHelp');
|
||||
},
|
||||
@@ -363,6 +368,34 @@ test('handleCliCommand opens first-run setup window for --setup', () => {
|
||||
assert.equal(calls.includes('openYomitanSettingsDelayed:1000'), false);
|
||||
});
|
||||
|
||||
test('handleCliCommand runs update command without overlay startup', async () => {
|
||||
const { deps, calls } = createDeps();
|
||||
|
||||
handleCliCommand(
|
||||
makeArgs({ update: true, updateLauncherPath: '/home/kyle/.local/bin/subminer' }),
|
||||
'initial',
|
||||
deps,
|
||||
);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.deepEqual(calls, ['runUpdateCommand:/home/kyle/.local/bin/subminer']);
|
||||
});
|
||||
|
||||
test('handleCliCommand stops app after headless initial update completes', async () => {
|
||||
const { deps, calls } = createDeps({
|
||||
hasMainWindow: () => false,
|
||||
});
|
||||
|
||||
handleCliCommand(
|
||||
makeArgs({ update: true, updateLauncherPath: '/home/kyle/.local/bin/subminer' }),
|
||||
'initial',
|
||||
deps,
|
||||
);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.deepEqual(calls, ['runUpdateCommand:/home/kyle/.local/bin/subminer', 'stopApp']);
|
||||
});
|
||||
|
||||
test('handleCliCommand dispatches stats command without overlay startup', async () => {
|
||||
const { deps, calls } = createDeps({
|
||||
runStatsCommand: async () => {
|
||||
|
||||
@@ -95,6 +95,7 @@ export interface CliCommandServiceDeps {
|
||||
}) => Promise<CharacterDictionarySelectionResult>;
|
||||
runStatsCommand: (args: CliArgs, source: CliCommandSource) => Promise<void>;
|
||||
runJellyfinCommand: (args: CliArgs) => Promise<void>;
|
||||
runUpdateCommand: (args: CliArgs, source: CliCommandSource) => Promise<void>;
|
||||
runYoutubePlaybackFlow: (request: {
|
||||
url: string;
|
||||
mode: NonNullable<CliArgs['youtubeMode']>;
|
||||
@@ -174,6 +175,7 @@ interface AnilistCliRuntime {
|
||||
interface AppCliRuntime {
|
||||
stop: () => void;
|
||||
hasMainWindow: () => boolean;
|
||||
runUpdateCommand: CliCommandServiceDeps['runUpdateCommand'];
|
||||
runYoutubePlaybackFlow: CliCommandServiceDeps['runYoutubePlaybackFlow'];
|
||||
}
|
||||
|
||||
@@ -277,6 +279,7 @@ export function createCliCommandDepsRuntime(
|
||||
setCharacterDictionarySelection: options.dictionary.setSelection,
|
||||
runStatsCommand: options.jellyfin.runStatsCommand,
|
||||
runJellyfinCommand: options.jellyfin.runCommand,
|
||||
runUpdateCommand: options.app.runUpdateCommand,
|
||||
runYoutubePlaybackFlow: options.app.runYoutubePlaybackFlow,
|
||||
printHelp: options.ui.printHelp,
|
||||
hasMainWindow: options.app.hasMainWindow,
|
||||
@@ -416,6 +419,19 @@ export function handleCliCommand(
|
||||
deps.stopApp();
|
||||
}
|
||||
});
|
||||
} else if (args.update) {
|
||||
const shouldStopAfterRun = source === 'initial' && !deps.hasMainWindow();
|
||||
deps
|
||||
.runUpdateCommand(args, source)
|
||||
.catch((err) => {
|
||||
deps.error('runUpdateCommand failed:', err);
|
||||
deps.showMpvOsd(`Update failed: ${(err as Error).message}`);
|
||||
})
|
||||
.finally(() => {
|
||||
if (shouldStopAfterRun) {
|
||||
deps.stopApp();
|
||||
}
|
||||
});
|
||||
} else if (args.toggleSecondarySub) {
|
||||
deps.cycleSecondarySubMode();
|
||||
} else if (args.triggerFieldGrouping) {
|
||||
|
||||
@@ -27,7 +27,7 @@ export {
|
||||
isAutoUpdateEnabledRuntime,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig,
|
||||
} from './startup';
|
||||
export { openYomitanSettingsWindow } from './yomitan-settings';
|
||||
export { destroyYomitanSettingsWindow, openYomitanSettingsWindow } from './yomitan-settings';
|
||||
export { createTokenizerDepsRuntime, tokenizeSubtitle } from './tokenizer';
|
||||
export {
|
||||
addYomitanNoteViaSearch,
|
||||
|
||||
@@ -223,3 +223,23 @@ test('runStartupBootstrapRuntime enables quiet background mode by default', () =
|
||||
assert.equal(result.backgroundMode, true);
|
||||
assert.deepEqual(calls, ['setLog:warn:cli', 'forceX11', 'enforceWayland', 'startLifecycle']);
|
||||
});
|
||||
|
||||
test('runStartupBootstrapRuntime enables quiet update mode by default', () => {
|
||||
const calls: string[] = [];
|
||||
const args = makeArgs({ update: true });
|
||||
|
||||
const result = runStartupBootstrapRuntime({
|
||||
argv: ['node', 'main.ts', '--update'],
|
||||
parseArgs: () => args,
|
||||
setLogLevel: (level, source) => calls.push(`setLog:${level}:${source}`),
|
||||
forceX11Backend: () => calls.push('forceX11'),
|
||||
enforceUnsupportedWaylandMode: () => calls.push('enforceWayland'),
|
||||
getDefaultSocketPath: () => '/tmp/default.sock',
|
||||
defaultTexthookerPort: 5174,
|
||||
runGenerateConfigFlow: () => false,
|
||||
startAppLifecycle: () => calls.push('startLifecycle'),
|
||||
});
|
||||
|
||||
assert.equal(result.backgroundMode, false);
|
||||
assert.deepEqual(calls, ['setLog:warn:cli', 'forceX11', 'enforceWayland', 'startLifecycle']);
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ export function runStartupBootstrapRuntime(
|
||||
|
||||
if (initialArgs.logLevel) {
|
||||
deps.setLogLevel(initialArgs.logLevel, 'cli');
|
||||
} else if (initialArgs.background) {
|
||||
} else if (initialArgs.background || initialArgs.update) {
|
||||
deps.setLogLevel('warn', 'cli');
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,13 @@ const YOMITAN_SYNC_SCRIPT_PATHS = [
|
||||
path.join('js', 'display', 'display-audio.js'),
|
||||
];
|
||||
|
||||
type ExtensionCopyResult = {
|
||||
targetDir: string;
|
||||
copied: boolean;
|
||||
};
|
||||
|
||||
const asyncExtensionCopyInFlight = new Map<string, Promise<ExtensionCopyResult>>();
|
||||
|
||||
function readManifestVersion(manifestPath: string): string | null {
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as { version?: unknown };
|
||||
@@ -18,6 +25,15 @@ function readManifestVersion(manifestPath: string): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
async function pathExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.promises.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function hashDirectoryContents(dirPath: string): string | null {
|
||||
try {
|
||||
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
|
||||
@@ -53,6 +69,42 @@ export function hashDirectoryContents(dirPath: string): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
export async function hashDirectoryContentsAsync(dirPath: string): Promise<string | null> {
|
||||
try {
|
||||
const dirStat = await fs.promises.stat(dirPath);
|
||||
if (!dirStat.isDirectory()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hash = createHash('sha256');
|
||||
const queue = [''];
|
||||
while (queue.length > 0) {
|
||||
const relativeDir = queue.shift()!;
|
||||
const absoluteDir = path.join(dirPath, relativeDir);
|
||||
const entries = await fs.promises.readdir(absoluteDir, { withFileTypes: true });
|
||||
entries.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
for (const entry of entries) {
|
||||
const relativePath = path.join(relativeDir, entry.name);
|
||||
const normalizedRelativePath = relativePath.split(path.sep).join('/');
|
||||
hash.update(normalizedRelativePath);
|
||||
if (entry.isDirectory()) {
|
||||
queue.push(relativePath);
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
hash.update(await fs.promises.readFile(path.join(dirPath, relativePath)));
|
||||
}
|
||||
}
|
||||
|
||||
return hash.digest('hex');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function areFilesEqual(sourcePath: string, targetPath: string): boolean {
|
||||
if (!fs.existsSync(sourcePath) || !fs.existsSync(targetPath)) return false;
|
||||
try {
|
||||
@@ -93,10 +145,7 @@ export function shouldCopyYomitanExtension(sourceDir: string, targetDir: string)
|
||||
export function ensureExtensionCopy(
|
||||
sourceDir: string,
|
||||
userDataPath: string,
|
||||
): {
|
||||
targetDir: string;
|
||||
copied: boolean;
|
||||
} {
|
||||
): ExtensionCopyResult {
|
||||
if (process.platform === 'win32') {
|
||||
return { targetDir: sourceDir, copied: false };
|
||||
}
|
||||
@@ -117,3 +166,53 @@ export function ensureExtensionCopy(
|
||||
|
||||
return { targetDir, copied: shouldCopy };
|
||||
}
|
||||
|
||||
export async function ensureExtensionCopyAsync(
|
||||
sourceDir: string,
|
||||
userDataPath: string,
|
||||
): Promise<ExtensionCopyResult> {
|
||||
if (process.platform === 'win32') {
|
||||
return { targetDir: sourceDir, copied: false };
|
||||
}
|
||||
|
||||
const extensionsRoot = path.join(userDataPath, 'extensions');
|
||||
const targetDir = path.join(extensionsRoot, 'yomitan');
|
||||
const inFlightKey = path.resolve(targetDir);
|
||||
const inFlight = asyncExtensionCopyInFlight.get(inFlightKey);
|
||||
if (inFlight) {
|
||||
return await inFlight;
|
||||
}
|
||||
|
||||
const copyPromise = ensureExtensionCopyAsyncInternal(sourceDir, extensionsRoot, targetDir);
|
||||
asyncExtensionCopyInFlight.set(inFlightKey, copyPromise);
|
||||
try {
|
||||
return await copyPromise;
|
||||
} finally {
|
||||
if (asyncExtensionCopyInFlight.get(inFlightKey) === copyPromise) {
|
||||
asyncExtensionCopyInFlight.delete(inFlightKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureExtensionCopyAsyncInternal(
|
||||
sourceDir: string,
|
||||
extensionsRoot: string,
|
||||
targetDir: string,
|
||||
): Promise<ExtensionCopyResult> {
|
||||
let shouldCopy = !(await pathExists(targetDir));
|
||||
if (!shouldCopy) {
|
||||
const [sourceHash, targetHash] = await Promise.all([
|
||||
hashDirectoryContentsAsync(sourceDir),
|
||||
hashDirectoryContentsAsync(targetDir),
|
||||
]);
|
||||
shouldCopy = sourceHash !== targetHash;
|
||||
}
|
||||
|
||||
if (shouldCopy) {
|
||||
await fs.promises.mkdir(extensionsRoot, { recursive: true });
|
||||
await fs.promises.rm(targetDir, { recursive: true, force: true });
|
||||
await fs.promises.cp(sourceDir, targetDir, { recursive: true });
|
||||
}
|
||||
|
||||
return { targetDir, copied: shouldCopy };
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
import { ensureExtensionCopy, shouldCopyYomitanExtension } from './yomitan-extension-copy';
|
||||
import {
|
||||
ensureExtensionCopy,
|
||||
ensureExtensionCopyAsync,
|
||||
shouldCopyYomitanExtension,
|
||||
} from './yomitan-extension-copy';
|
||||
|
||||
function makeTempDir(prefix: string): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
@@ -82,3 +86,115 @@ test('ensureExtensionCopy refreshes copied extension when display files change',
|
||||
'new display code',
|
||||
);
|
||||
});
|
||||
|
||||
test('ensureExtensionCopyAsync refreshes copied extension without completing synchronously', async () => {
|
||||
const sourceRoot = makeTempDir('subminer-yomitan-src-');
|
||||
const userDataRoot = makeTempDir('subminer-yomitan-user-');
|
||||
|
||||
const sourceDir = path.join(sourceRoot, 'yomitan');
|
||||
const targetDir = path.join(userDataRoot, 'extensions', 'yomitan');
|
||||
|
||||
fs.mkdirSync(path.join(sourceDir, 'js', 'display'), { recursive: true });
|
||||
fs.mkdirSync(path.join(targetDir, 'js', 'display'), { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(sourceDir, 'manifest.json'), JSON.stringify({ version: '1.0.0' }));
|
||||
fs.writeFileSync(path.join(targetDir, 'manifest.json'), JSON.stringify({ version: '1.0.0' }));
|
||||
fs.writeFileSync(
|
||||
path.join(sourceDir, 'js', 'display', 'structured-content-generator.js'),
|
||||
'new display code',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(targetDir, 'js', 'display', 'structured-content-generator.js'),
|
||||
'old display code',
|
||||
);
|
||||
|
||||
let completed = false;
|
||||
const resultPromise = ensureExtensionCopyAsync(sourceDir, userDataRoot).then((result) => {
|
||||
completed = true;
|
||||
return result;
|
||||
});
|
||||
|
||||
assert.equal(completed, false);
|
||||
|
||||
const result = await resultPromise;
|
||||
|
||||
assert.equal(result.targetDir, targetDir);
|
||||
assert.equal(result.copied, true);
|
||||
assert.equal(
|
||||
fs.readFileSync(
|
||||
path.join(targetDir, 'js', 'display', 'structured-content-generator.js'),
|
||||
'utf8',
|
||||
),
|
||||
'new display code',
|
||||
);
|
||||
});
|
||||
|
||||
test('ensureExtensionCopyAsync shares an in-flight refresh for the same copied extension', async () => {
|
||||
const sourceRoot = makeTempDir('subminer-yomitan-src-');
|
||||
const userDataRoot = makeTempDir('subminer-yomitan-user-');
|
||||
|
||||
const sourceDir = path.join(sourceRoot, 'yomitan');
|
||||
const targetDir = path.join(userDataRoot, 'extensions', 'yomitan');
|
||||
|
||||
fs.mkdirSync(path.join(sourceDir, 'js', 'pages', 'settings'), { recursive: true });
|
||||
fs.mkdirSync(path.join(targetDir, 'js', 'pages', 'settings'), { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(sourceDir, 'manifest.json'), JSON.stringify({ version: '1.0.0' }));
|
||||
fs.writeFileSync(path.join(targetDir, 'manifest.json'), JSON.stringify({ version: '1.0.0' }));
|
||||
fs.writeFileSync(
|
||||
path.join(sourceDir, 'js', 'pages', 'settings', 'settings-main.js'),
|
||||
'new settings code',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(targetDir, 'js', 'pages', 'settings', 'settings-main.js'),
|
||||
'old settings code',
|
||||
);
|
||||
|
||||
const originalCp = fs.promises.cp;
|
||||
let cpCalls = 0;
|
||||
let firstCopyStarted = false;
|
||||
let releaseFirstCopy: () => void = () => {};
|
||||
const firstCopyStartedPromise = new Promise<void>((resolve) => {
|
||||
const fsPromises = fs.promises as typeof fs.promises & {
|
||||
cp: typeof fs.promises.cp;
|
||||
};
|
||||
fsPromises.cp = async (...args: Parameters<typeof fs.promises.cp>) => {
|
||||
cpCalls++;
|
||||
if (!firstCopyStarted) {
|
||||
firstCopyStarted = true;
|
||||
resolve();
|
||||
await new Promise<void>((release) => {
|
||||
releaseFirstCopy = release;
|
||||
});
|
||||
}
|
||||
return await originalCp(...args);
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
const first = ensureExtensionCopyAsync(sourceDir, userDataRoot);
|
||||
await firstCopyStartedPromise;
|
||||
const second = ensureExtensionCopyAsync(sourceDir, userDataRoot);
|
||||
|
||||
releaseFirstCopy();
|
||||
const results = await Promise.all([first, second]);
|
||||
|
||||
assert.equal(cpCalls, 1);
|
||||
assert.equal(results[0].targetDir, targetDir);
|
||||
assert.equal(results[1].targetDir, targetDir);
|
||||
assert.equal(results[0].copied, true);
|
||||
assert.equal(results[1].copied, true);
|
||||
assert.equal(
|
||||
fs.readFileSync(
|
||||
path.join(targetDir, 'js', 'pages', 'settings', 'settings-main.js'),
|
||||
'utf8',
|
||||
),
|
||||
'new settings code',
|
||||
);
|
||||
} finally {
|
||||
const fsPromises = fs.promises as typeof fs.promises & {
|
||||
cp: typeof fs.promises.cp;
|
||||
};
|
||||
fsPromises.cp = originalCp;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { BrowserWindow, Extension, Session } from 'electron';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { createLogger } from '../../logger';
|
||||
import { ensureExtensionCopy } from './yomitan-extension-copy';
|
||||
import { ensureExtensionCopyAsync } from './yomitan-extension-copy';
|
||||
import {
|
||||
getYomitanExtensionSearchPaths,
|
||||
resolveExternalYomitanExtensionPath,
|
||||
@@ -79,7 +79,7 @@ export async function loadYomitanExtension(
|
||||
return null;
|
||||
}
|
||||
|
||||
const extensionCopy = ensureExtensionCopy(extPath, deps.userDataPath);
|
||||
const extensionCopy = await ensureExtensionCopyAsync(extPath, deps.userDataPath);
|
||||
if (extensionCopy.copied) {
|
||||
logger.info(`Copied yomitan extension to ${extensionCopy.targetDir}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
buildYomitanSettingsUrl,
|
||||
configureYomitanSettingsWindowChrome,
|
||||
destroyYomitanSettingsWindow,
|
||||
showYomitanSettingsWindow,
|
||||
} from './yomitan-settings';
|
||||
|
||||
function assertGuardedBySubminerSettingsSafe(source: string, call: string): void {
|
||||
const callIndex = source.indexOf(call);
|
||||
assert.notEqual(callIndex, -1, `missing call: ${call}`);
|
||||
|
||||
const beforeCall = source.slice(0, callIndex);
|
||||
const guardIndex = beforeCall.lastIndexOf('if (!subminerSettingsSafe) {');
|
||||
const blockCloseIndex = beforeCall.lastIndexOf('\n }');
|
||||
assert.ok(
|
||||
guardIndex > blockCloseIndex,
|
||||
`${call} must be inside its own !subminerSettingsSafe startup guard`,
|
||||
);
|
||||
}
|
||||
|
||||
test('yomitan settings window removes default app menu quit action', () => {
|
||||
const calls: string[] = [];
|
||||
|
||||
configureYomitanSettingsWindowChrome({
|
||||
setAutoHideMenuBar: (hide: boolean) => calls.push(`auto-hide:${hide}`),
|
||||
setMenu: (menu: unknown) => calls.push(`menu:${menu === null ? 'null' : 'custom'}`),
|
||||
} as never);
|
||||
|
||||
assert.deepEqual(calls, ['auto-hide:true', 'menu:null']);
|
||||
});
|
||||
|
||||
test('yomitan settings URL disables the embedded popup preview', () => {
|
||||
assert.equal(
|
||||
buildYomitanSettingsUrl('abc123'),
|
||||
'chrome-extension://abc123/settings.html?popup-preview=false&subminer-settings-safe=true',
|
||||
);
|
||||
});
|
||||
|
||||
test('vendored Yomitan settings safe mode skips heavy startup controllers', () => {
|
||||
const source = readFileSync(
|
||||
'vendor/subminer-yomitan/ext/js/pages/settings/settings-main.js',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
assert.match(source, /subminer-settings-safe/);
|
||||
assertGuardedBySubminerSettingsSafe(source, 'popupPreviewController.prepare()');
|
||||
assertGuardedBySubminerSettingsSafe(source, 'persistentStorageController.prepare()');
|
||||
assertGuardedBySubminerSettingsSafe(source, 'storageController.prepare()');
|
||||
assertGuardedBySubminerSettingsSafe(source, 'dictionaryController.prepare()');
|
||||
assertGuardedBySubminerSettingsSafe(source, 'ankiController.prepare()');
|
||||
assert.match(source, /if \(!subminerSettingsSafe\)[\s\S]*new AnkiDeckGeneratorController/);
|
||||
assert.match(source, /if \(!subminerSettingsSafe\)[\s\S]*new SecondarySearchDictionaryController/);
|
||||
assert.match(source, /if \(!subminerSettingsSafe\)[\s\S]*new SortFrequencyDictionaryController/);
|
||||
});
|
||||
|
||||
test('vendored Yomitan settings caches dictionary metadata requests', () => {
|
||||
const source = readFileSync(
|
||||
'vendor/subminer-yomitan/ext/js/pages/settings/settings-controller.js',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
assert.match(source, /_dictionaryInfoPromise/);
|
||||
assert.match(source, /_dictionaryInfoCache/);
|
||||
assert.match(source, /databaseUpdated/);
|
||||
assert.match(
|
||||
source,
|
||||
/this\._dictionaryInfoPromise = this\._application\.api\.getDictionaryInfo\(\)/,
|
||||
);
|
||||
});
|
||||
|
||||
test('vendored Yomitan Anki settings reuses SettingsController dictionary metadata cache', () => {
|
||||
const source = readFileSync(
|
||||
'vendor/subminer-yomitan/ext/js/pages/settings/anki-controller.js',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
assert.match(source, /this\._settingsController\.getDictionaryInfo\(\)/);
|
||||
assert.doesNotMatch(source, /this\._application\.api\.getDictionaryInfo\(\)/);
|
||||
});
|
||||
|
||||
test('showYomitanSettingsWindow restores, repaints, shows, and focuses an existing window', () => {
|
||||
const calls: string[] = [];
|
||||
|
||||
showYomitanSettingsWindow({
|
||||
isDestroyed: () => false,
|
||||
isMinimized: () => true,
|
||||
restore: () => calls.push('restore'),
|
||||
getSize: () => [1200, 800],
|
||||
setSize: (width: number, height: number) => calls.push(`set-size:${width}x${height}`),
|
||||
webContents: {
|
||||
invalidate: () => calls.push('invalidate'),
|
||||
},
|
||||
show: () => calls.push('show'),
|
||||
focus: () => calls.push('focus'),
|
||||
} as never);
|
||||
|
||||
assert.deepEqual(calls, ['restore', 'set-size:1200x800', 'invalidate', 'show', 'focus']);
|
||||
});
|
||||
|
||||
test('destroyYomitanSettingsWindow destroys a live settings window before app quit', () => {
|
||||
const calls: string[] = [];
|
||||
|
||||
const destroyed = destroyYomitanSettingsWindow({
|
||||
isDestroyed: () => false,
|
||||
destroy: () => calls.push('destroy'),
|
||||
} as never);
|
||||
|
||||
assert.equal(destroyed, true);
|
||||
assert.deepEqual(calls, ['destroy']);
|
||||
});
|
||||
|
||||
test('destroyYomitanSettingsWindow skips missing or already destroyed settings windows', () => {
|
||||
assert.equal(destroyYomitanSettingsWindow(null), false);
|
||||
assert.equal(
|
||||
destroyYomitanSettingsWindow({
|
||||
isDestroyed: () => true,
|
||||
destroy: () => {
|
||||
throw new Error('should not destroy twice');
|
||||
},
|
||||
} as never),
|
||||
false,
|
||||
);
|
||||
});
|
||||
@@ -13,6 +13,39 @@ export interface OpenYomitanSettingsWindowOptions {
|
||||
onWindowClosed?: () => void;
|
||||
}
|
||||
|
||||
export function configureYomitanSettingsWindowChrome(
|
||||
settingsWindow: Pick<BrowserWindow, 'setAutoHideMenuBar' | 'setMenu'>,
|
||||
): void {
|
||||
settingsWindow.setAutoHideMenuBar(true);
|
||||
settingsWindow.setMenu(null);
|
||||
}
|
||||
|
||||
export function buildYomitanSettingsUrl(extensionId: string): string {
|
||||
return `chrome-extension://${extensionId}/settings.html?popup-preview=false&subminer-settings-safe=true`;
|
||||
}
|
||||
|
||||
export function showYomitanSettingsWindow(settingsWindow: BrowserWindow): void {
|
||||
if (settingsWindow.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
if (settingsWindow.isMinimized()) {
|
||||
settingsWindow.restore();
|
||||
}
|
||||
const [width = 0, height = 0] = settingsWindow.getSize();
|
||||
settingsWindow.setSize(width, height);
|
||||
settingsWindow.webContents.invalidate();
|
||||
settingsWindow.show();
|
||||
settingsWindow.focus();
|
||||
}
|
||||
|
||||
export function destroyYomitanSettingsWindow(settingsWindow: BrowserWindow | null): boolean {
|
||||
if (!settingsWindow || settingsWindow.isDestroyed()) {
|
||||
return false;
|
||||
}
|
||||
settingsWindow.destroy();
|
||||
return true;
|
||||
}
|
||||
|
||||
export function openYomitanSettingsWindow(options: OpenYomitanSettingsWindowOptions): void {
|
||||
logger.info('openYomitanSettings called');
|
||||
|
||||
@@ -24,8 +57,8 @@ export function openYomitanSettingsWindow(options: OpenYomitanSettingsWindowOpti
|
||||
|
||||
const existingWindow = options.getExistingWindow();
|
||||
if (existingWindow && !existingWindow.isDestroyed()) {
|
||||
logger.info('Settings window already exists, focusing');
|
||||
existingWindow.focus();
|
||||
logger.info('Settings window already exists, showing and focusing');
|
||||
showYomitanSettingsWindow(existingWindow);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,15 +68,17 @@ export function openYomitanSettingsWindow(options: OpenYomitanSettingsWindowOpti
|
||||
width: 1200,
|
||||
height: 800,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
session: options.yomitanSession ?? session.defaultSession,
|
||||
},
|
||||
});
|
||||
configureYomitanSettingsWindowChrome(settingsWindow);
|
||||
options.setWindow(settingsWindow);
|
||||
|
||||
const settingsUrl = `chrome-extension://${options.yomitanExt.id}/settings.html`;
|
||||
const settingsUrl = buildYomitanSettingsUrl(options.yomitanExt.id);
|
||||
logger.info('Loading settings URL:', settingsUrl);
|
||||
|
||||
let loadAttempts = 0;
|
||||
@@ -76,12 +111,7 @@ export function openYomitanSettingsWindow(options: OpenYomitanSettingsWindowOpti
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (!settingsWindow.isDestroyed()) {
|
||||
const [width = 0, height = 0] = settingsWindow.getSize();
|
||||
settingsWindow.setSize(width, height);
|
||||
settingsWindow.webContents.invalidate();
|
||||
settingsWindow.show();
|
||||
}
|
||||
showYomitanSettingsWindow(settingsWindow);
|
||||
}, 500);
|
||||
|
||||
settingsWindow.on('closed', () => {
|
||||
|
||||
Reference in New Issue
Block a user