mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
feat: improve background startup and launcher control
Detach --background launches from terminals with quieter runtime output, make wrapper/plugin overlay start explicit, and allow trailing commas in JSONC configs for safer hot-reload edits. Includes pending Anki/docs/backlog updates in this unreleased batch.
This commit is contained in:
@@ -192,13 +192,35 @@ export class AnkiConnectClient {
|
||||
deckName: string,
|
||||
modelName: string,
|
||||
fields: Record<string, string>,
|
||||
tags: string[] = [],
|
||||
): Promise<number> {
|
||||
const note: {
|
||||
deckName: string;
|
||||
modelName: string;
|
||||
fields: Record<string, string>;
|
||||
tags?: string[];
|
||||
} = { deckName, modelName, fields };
|
||||
if (tags.length > 0) {
|
||||
note.tags = tags;
|
||||
}
|
||||
|
||||
const result = await this.invoke('addNote', {
|
||||
note: { deckName, modelName, fields },
|
||||
note,
|
||||
});
|
||||
return result as number;
|
||||
}
|
||||
|
||||
async addTags(noteIds: number[], tags: string[]): Promise<void> {
|
||||
if (noteIds.length === 0 || tags.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.invoke('addTags', {
|
||||
notes: noteIds,
|
||||
tags: tags.join(' '),
|
||||
});
|
||||
}
|
||||
|
||||
async deleteNotes(noteIds: number[]): Promise<void> {
|
||||
await this.invoke('deleteNotes', { notes: noteIds });
|
||||
}
|
||||
|
||||
@@ -175,7 +175,9 @@ export class AnkiIntegration {
|
||||
getMpvClient: () => this.mpvClient,
|
||||
getDeck: () => this.config.deck,
|
||||
client: {
|
||||
addNote: (deck, modelName, fields) => this.client.addNote(deck, modelName, fields),
|
||||
addNote: (deck, modelName, fields, tags) =>
|
||||
this.client.addNote(deck, modelName, fields, tags),
|
||||
addTags: (noteIds, tags) => this.client.addTags(noteIds, tags),
|
||||
notesInfo: async (noteIds) => (await this.client.notesInfo(noteIds)) as unknown,
|
||||
updateNoteFields: (noteId, fields) =>
|
||||
this.client.updateNoteFields(noteId, fields) as Promise<void>,
|
||||
@@ -295,6 +297,25 @@ export class AnkiIntegration {
|
||||
this.knownWordCache.stopLifecycle();
|
||||
}
|
||||
|
||||
private getConfiguredAnkiTags(): string[] {
|
||||
if (!Array.isArray(this.config.tags)) {
|
||||
return [];
|
||||
}
|
||||
return [...new Set(this.config.tags.map((tag) => tag.trim()).filter((tag) => tag.length > 0))];
|
||||
}
|
||||
|
||||
private async addConfiguredTagsToNote(noteId: number): Promise<void> {
|
||||
const tags = this.getConfiguredAnkiTags();
|
||||
if (tags.length === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.client.addTags([noteId], tags);
|
||||
} catch (error) {
|
||||
log.warn('Failed to add tags to card:', (error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshKnownWordCache(): Promise<void> {
|
||||
return this.knownWordCache.refresh(true);
|
||||
}
|
||||
@@ -512,6 +533,7 @@ export class AnkiIntegration {
|
||||
|
||||
if (updatePerformed) {
|
||||
await this.client.updateNoteFields(noteId, updatedFields);
|
||||
await this.addConfiguredTagsToNote(noteId);
|
||||
log.info('Updated card fields for:', expressionText);
|
||||
await this.showNotification(noteId, expressionText);
|
||||
}
|
||||
@@ -1465,6 +1487,7 @@ export class AnkiIntegration {
|
||||
|
||||
if (Object.keys(mergedFields).length > 0) {
|
||||
await this.client.updateNoteFields(keepNoteId, mergedFields);
|
||||
await this.addConfiguredTagsToNote(keepNoteId);
|
||||
}
|
||||
|
||||
if (deleteDuplicate) {
|
||||
@@ -1621,6 +1644,7 @@ export class AnkiIntegration {
|
||||
await this.client.updateNoteFields(noteId, {
|
||||
[resolvedMiscField]: nextValue,
|
||||
});
|
||||
await this.addConfiguredTagsToNote(noteId);
|
||||
}
|
||||
|
||||
applyRuntimeConfigPatch(patch: Partial<AnkiConnectConfig>): void {
|
||||
|
||||
@@ -16,7 +16,13 @@ export interface CardCreationNoteInfo {
|
||||
type CardKind = 'sentence' | 'audio';
|
||||
|
||||
interface CardCreationClient {
|
||||
addNote(deck: string, modelName: string, fields: Record<string, string>): Promise<number>;
|
||||
addNote(
|
||||
deck: string,
|
||||
modelName: string,
|
||||
fields: Record<string, string>,
|
||||
tags?: string[],
|
||||
): Promise<number>;
|
||||
addTags(noteIds: number[], tags: string[]): Promise<void>;
|
||||
notesInfo(noteIds: number[]): Promise<unknown>;
|
||||
updateNoteFields(noteId: number, fields: Record<string, string>): Promise<void>;
|
||||
storeMediaFile(filename: string, data: Buffer): Promise<void>;
|
||||
@@ -101,6 +107,26 @@ interface CardCreationDeps {
|
||||
export class CardCreationService {
|
||||
constructor(private readonly deps: CardCreationDeps) {}
|
||||
|
||||
private getConfiguredAnkiTags(): string[] {
|
||||
const tags = this.deps.getConfig().tags;
|
||||
if (!Array.isArray(tags)) {
|
||||
return [];
|
||||
}
|
||||
return [...new Set(tags.map((tag) => tag.trim()).filter((tag) => tag.length > 0))];
|
||||
}
|
||||
|
||||
private async addConfiguredTagsToNote(noteId: number): Promise<void> {
|
||||
const tags = this.getConfiguredAnkiTags();
|
||||
if (tags.length === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.deps.client.addTags([noteId], tags);
|
||||
} catch (error) {
|
||||
log.warn('Failed to add tags to card:', (error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
async updateLastAddedFromClipboard(clipboardText: string): Promise<void> {
|
||||
try {
|
||||
if (!clipboardText || !clipboardText.trim()) {
|
||||
@@ -272,6 +298,7 @@ export class CardCreationService {
|
||||
|
||||
if (updatePerformed) {
|
||||
await this.deps.client.updateNoteFields(noteId, updatedFields);
|
||||
await this.addConfiguredTagsToNote(noteId);
|
||||
const label = expressionText || noteId;
|
||||
log.info('Updated card from clipboard:', label);
|
||||
const errorSuffix = errors.length > 0 ? `${errors.join(', ')} failed` : undefined;
|
||||
@@ -408,6 +435,7 @@ export class CardCreationService {
|
||||
}
|
||||
|
||||
await this.deps.client.updateNoteFields(noteId, updatedFields);
|
||||
await this.addConfiguredTagsToNote(noteId);
|
||||
const label = expressionText || noteId;
|
||||
log.info('Marked card as audio card:', label);
|
||||
const errorSuffix = errors.length > 0 ? `${errors.join(', ')} failed` : undefined;
|
||||
@@ -490,7 +518,12 @@ export class CardCreationService {
|
||||
const deck = this.deps.getConfig().deck || 'Default';
|
||||
let noteId: number;
|
||||
try {
|
||||
noteId = await this.deps.client.addNote(deck, sentenceCardModel, fields);
|
||||
noteId = await this.deps.client.addNote(
|
||||
deck,
|
||||
sentenceCardModel,
|
||||
fields,
|
||||
this.getConfiguredAnkiTags(),
|
||||
);
|
||||
log.info('Created sentence card:', noteId);
|
||||
this.deps.trackLastAddedNoteId?.(noteId);
|
||||
} catch (error) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { hasExplicitCommand, parseArgs, shouldStartApp } from './args';
|
||||
|
||||
test('parseArgs parses booleans and value flags', () => {
|
||||
const args = parseArgs([
|
||||
'--background',
|
||||
'--start',
|
||||
'--socket',
|
||||
'/tmp/mpv.sock',
|
||||
@@ -22,6 +23,7 @@ test('parseArgs parses booleans and value flags', () => {
|
||||
'2',
|
||||
]);
|
||||
|
||||
assert.equal(args.background, true);
|
||||
assert.equal(args.start, true);
|
||||
assert.equal(args.socketPath, '/tmp/mpv.sock');
|
||||
assert.equal(args.backend, 'hyprland');
|
||||
@@ -93,4 +95,9 @@ test('hasExplicitCommand and shouldStartApp preserve command intent', () => {
|
||||
assert.equal(jellyfinRemoteAnnounce.jellyfinRemoteAnnounce, true);
|
||||
assert.equal(hasExplicitCommand(jellyfinRemoteAnnounce), true);
|
||||
assert.equal(shouldStartApp(jellyfinRemoteAnnounce), false);
|
||||
|
||||
const background = parseArgs(['--background']);
|
||||
assert.equal(background.background, true);
|
||||
assert.equal(hasExplicitCommand(background), true);
|
||||
assert.equal(shouldStartApp(background), true);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface CliArgs {
|
||||
background: boolean;
|
||||
start: boolean;
|
||||
stop: boolean;
|
||||
toggle: boolean;
|
||||
@@ -61,6 +62,7 @@ export type CliCommandSource = 'initial' | 'second-instance';
|
||||
|
||||
export function parseArgs(argv: string[]): CliArgs {
|
||||
const args: CliArgs = {
|
||||
background: false,
|
||||
start: false,
|
||||
stop: false,
|
||||
toggle: false,
|
||||
@@ -115,7 +117,8 @@ export function parseArgs(argv: string[]): CliArgs {
|
||||
const arg = argv[i];
|
||||
if (!arg.startsWith('--')) continue;
|
||||
|
||||
if (arg === '--start') args.start = true;
|
||||
if (arg === '--background') args.background = true;
|
||||
else if (arg === '--start') args.start = true;
|
||||
else if (arg === '--stop') args.stop = true;
|
||||
else if (arg === '--toggle') args.toggle = true;
|
||||
else if (arg === '--toggle-visible-overlay') args.toggleVisibleOverlay = true;
|
||||
@@ -255,6 +258,7 @@ export function parseArgs(argv: string[]): CliArgs {
|
||||
|
||||
export function hasExplicitCommand(args: CliArgs): boolean {
|
||||
return (
|
||||
args.background ||
|
||||
args.start ||
|
||||
args.stop ||
|
||||
args.toggle ||
|
||||
@@ -299,6 +303,7 @@ export function hasExplicitCommand(args: CliArgs): boolean {
|
||||
export function shouldStartApp(args: CliArgs): boolean {
|
||||
if (args.stop && !args.start) return false;
|
||||
if (
|
||||
args.background ||
|
||||
args.start ||
|
||||
args.toggle ||
|
||||
args.toggleVisibleOverlay ||
|
||||
|
||||
@@ -10,6 +10,7 @@ ${B}SubMiner${R} — Japanese sentence mining with mpv + Yomitan
|
||||
${B}Usage:${R} subminer ${D}[command] [options]${R}
|
||||
|
||||
${B}Session${R}
|
||||
--background Start in tray/background mode
|
||||
--start Connect to mpv and launch overlay
|
||||
--stop Stop the running instance
|
||||
--texthooker Start texthooker server only ${D}(no overlay)${R}
|
||||
|
||||
@@ -17,6 +17,7 @@ test('loads defaults when config is missing', () => {
|
||||
const config = service.getConfig();
|
||||
assert.equal(config.websocket.port, DEFAULT_CONFIG.websocket.port);
|
||||
assert.equal(config.ankiConnect.behavior.autoUpdateNewCards, true);
|
||||
assert.deepEqual(config.ankiConnect.tags, ['SubMiner']);
|
||||
assert.equal(config.anilist.enabled, false);
|
||||
assert.equal(config.jellyfin.remoteControlEnabled, true);
|
||||
assert.equal(config.jellyfin.remoteControlAutoConnect, true);
|
||||
@@ -258,6 +259,28 @@ test('parses jsonc and warns/falls back on invalid value', () => {
|
||||
assert.ok(service.getWarnings().some((w) => w.path === 'websocket.port'));
|
||||
});
|
||||
|
||||
test('accepts trailing commas in jsonc', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"websocket": {
|
||||
"enabled": "auto",
|
||||
"port": 7788,
|
||||
},
|
||||
"youtubeSubgen": {
|
||||
"primarySubLanguages": ["ja", "en",],
|
||||
},
|
||||
}`,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
assert.equal(config.websocket.port, 7788);
|
||||
assert.deepEqual(config.youtubeSubgen.primarySubLanguages, ['ja', 'en']);
|
||||
});
|
||||
|
||||
test('reloadConfigStrict rejects invalid jsonc and preserves previous config', () => {
|
||||
const dir = makeTempDir();
|
||||
const configPath = path.join(dir, 'config.jsonc');
|
||||
@@ -631,6 +654,44 @@ test('accepts valid ankiConnect n+1 deck list', () => {
|
||||
assert.deepEqual(config.ankiConnect.nPlusOne.decks, ['Deck One', 'Deck Two']);
|
||||
});
|
||||
|
||||
test('accepts valid ankiConnect tags list', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"ankiConnect": {
|
||||
"tags": ["SubMiner", "Mining"]
|
||||
}
|
||||
}`,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
|
||||
assert.deepEqual(config.ankiConnect.tags, ['SubMiner', 'Mining']);
|
||||
});
|
||||
|
||||
test('falls back to default when ankiConnect tags list is invalid', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"ankiConnect": {
|
||||
"tags": ["SubMiner", 123]
|
||||
}
|
||||
}`,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
const warnings = service.getWarnings();
|
||||
|
||||
assert.deepEqual(config.ankiConnect.tags, ['SubMiner']);
|
||||
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.tags'));
|
||||
});
|
||||
|
||||
test('falls back to default when ankiConnect n+1 deck list is invalid', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
|
||||
@@ -79,6 +79,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
|
||||
enabled: false,
|
||||
url: 'http://127.0.0.1:8765',
|
||||
pollingRate: 3000,
|
||||
tags: ['SubMiner'],
|
||||
fields: {
|
||||
audio: 'ExpressionAudio',
|
||||
image: 'Picture',
|
||||
@@ -397,6 +398,13 @@ export const CONFIG_OPTION_REGISTRY: ConfigOptionRegistryEntry[] = [
|
||||
defaultValue: DEFAULT_CONFIG.ankiConnect.pollingRate,
|
||||
description: 'Polling interval in milliseconds.',
|
||||
},
|
||||
{
|
||||
path: 'ankiConnect.tags',
|
||||
kind: 'array',
|
||||
defaultValue: DEFAULT_CONFIG.ankiConnect.tags,
|
||||
description:
|
||||
'Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.',
|
||||
},
|
||||
{
|
||||
path: 'ankiConnect.behavior.autoUpdateNewCards',
|
||||
kind: 'boolean',
|
||||
|
||||
@@ -174,7 +174,10 @@ export class ConfigService {
|
||||
const parsed = configPath.endsWith('.jsonc')
|
||||
? (() => {
|
||||
const errors: ParseError[] = [];
|
||||
const result = parseJsonc(data, errors);
|
||||
const result = parseJsonc(data, errors, {
|
||||
allowTrailingComma: true,
|
||||
disallowComments: false,
|
||||
});
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`Invalid JSONC (${errors[0]?.error ?? 'unknown'})`);
|
||||
}
|
||||
@@ -889,6 +892,32 @@ export class ConfigService {
|
||||
},
|
||||
};
|
||||
|
||||
if (Array.isArray(ac.tags)) {
|
||||
const normalizedTags = ac.tags
|
||||
.filter((entry): entry is string => typeof entry === 'string')
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => entry.length > 0);
|
||||
if (normalizedTags.length === ac.tags.length) {
|
||||
resolved.ankiConnect.tags = [...new Set(normalizedTags)];
|
||||
} else {
|
||||
resolved.ankiConnect.tags = DEFAULT_CONFIG.ankiConnect.tags;
|
||||
warn(
|
||||
'ankiConnect.tags',
|
||||
ac.tags,
|
||||
resolved.ankiConnect.tags,
|
||||
'Expected an array of non-empty strings.',
|
||||
);
|
||||
}
|
||||
} else if (ac.tags !== undefined) {
|
||||
resolved.ankiConnect.tags = DEFAULT_CONFIG.ankiConnect.tags;
|
||||
warn(
|
||||
'ankiConnect.tags',
|
||||
ac.tags,
|
||||
resolved.ankiConnect.tags,
|
||||
'Expected an array of strings.',
|
||||
);
|
||||
}
|
||||
|
||||
const legacy = ac as Record<string, unknown>;
|
||||
const mapLegacy = (key: string, apply: (value: unknown) => void): void => {
|
||||
if (legacy[key] !== undefined) apply(legacy[key]);
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface AppLifecycleServiceDeps {
|
||||
onWillQuitCleanup: () => void;
|
||||
shouldRestoreWindowsOnActivate: () => boolean;
|
||||
restoreWindowsOnActivate: () => void;
|
||||
shouldQuitOnWindowAllClosed: () => boolean;
|
||||
}
|
||||
|
||||
interface AppLike {
|
||||
@@ -42,6 +43,7 @@ export interface AppLifecycleDepsRuntimeOptions {
|
||||
onWillQuitCleanup: () => void;
|
||||
shouldRestoreWindowsOnActivate: () => boolean;
|
||||
restoreWindowsOnActivate: () => void;
|
||||
shouldQuitOnWindowAllClosed: () => boolean;
|
||||
}
|
||||
|
||||
export function createAppLifecycleDepsRuntime(
|
||||
@@ -80,6 +82,7 @@ export function createAppLifecycleDepsRuntime(
|
||||
onWillQuitCleanup: options.onWillQuitCleanup,
|
||||
shouldRestoreWindowsOnActivate: options.shouldRestoreWindowsOnActivate,
|
||||
restoreWindowsOnActivate: options.restoreWindowsOnActivate,
|
||||
shouldQuitOnWindowAllClosed: options.shouldQuitOnWindowAllClosed,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -119,7 +122,7 @@ export function startAppLifecycle(initialArgs: CliArgs, deps: AppLifecycleServic
|
||||
});
|
||||
|
||||
deps.onWindowAllClosed(() => {
|
||||
if (!deps.isDarwinPlatform()) {
|
||||
if (!deps.isDarwinPlatform() && deps.shouldQuitOnWindowAllClosed()) {
|
||||
deps.quitApp();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CliCommandServiceDeps, handleCliCommand } from './cli-command';
|
||||
|
||||
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
return {
|
||||
background: false,
|
||||
start: false,
|
||||
stop: false,
|
||||
toggle: false,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CliArgs } from '../../cli/args';
|
||||
|
||||
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
return {
|
||||
background: false,
|
||||
start: false,
|
||||
stop: false,
|
||||
toggle: false,
|
||||
@@ -80,6 +81,7 @@ test('runStartupBootstrapRuntime configures startup state and starts lifecycle',
|
||||
assert.equal(result.backendOverride, 'x11');
|
||||
assert.equal(result.autoStartOverlay, true);
|
||||
assert.equal(result.texthookerOnlyMode, true);
|
||||
assert.equal(result.backgroundMode, false);
|
||||
assert.deepEqual(calls, ['setLog:debug:cli', 'forceX11', 'enforceWayland', 'startLifecycle']);
|
||||
});
|
||||
|
||||
@@ -130,6 +132,7 @@ test('runStartupBootstrapRuntime remains lifecycle-stable with Jellyfin CLI flag
|
||||
assert.equal(result.backendOverride, null);
|
||||
assert.equal(result.autoStartOverlay, false);
|
||||
assert.equal(result.texthookerOnlyMode, false);
|
||||
assert.equal(result.backgroundMode, false);
|
||||
assert.deepEqual(calls, ['forceX11', 'enforceWayland', 'startLifecycle']);
|
||||
});
|
||||
|
||||
@@ -173,5 +176,26 @@ test('runStartupBootstrapRuntime skips lifecycle when generate-config flow handl
|
||||
assert.equal(result.mpvSocketPath, '/tmp/default.sock');
|
||||
assert.equal(result.texthookerPort, 5174);
|
||||
assert.equal(result.backendOverride, null);
|
||||
assert.equal(result.backgroundMode, false);
|
||||
assert.deepEqual(calls, ['setLog:warn:cli', 'forceX11', 'enforceWayland']);
|
||||
});
|
||||
|
||||
test('runStartupBootstrapRuntime enables quiet background mode by default', () => {
|
||||
const calls: string[] = [];
|
||||
const args = makeArgs({ background: true });
|
||||
|
||||
const result = runStartupBootstrapRuntime({
|
||||
argv: ['node', 'main.ts', '--background'],
|
||||
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, true);
|
||||
assert.deepEqual(calls, ['setLog:warn:cli', 'forceX11', 'enforceWayland', 'startLifecycle']);
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface StartupBootstrapRuntimeState {
|
||||
backendOverride: string | null;
|
||||
autoStartOverlay: boolean;
|
||||
texthookerOnlyMode: boolean;
|
||||
backgroundMode: boolean;
|
||||
}
|
||||
|
||||
interface RuntimeAutoUpdateOptionManagerLike {
|
||||
@@ -47,6 +48,8 @@ export function runStartupBootstrapRuntime(
|
||||
|
||||
if (initialArgs.logLevel) {
|
||||
deps.setLogLevel(initialArgs.logLevel, 'cli');
|
||||
} else if (initialArgs.background) {
|
||||
deps.setLogLevel('warn', 'cli');
|
||||
}
|
||||
|
||||
deps.forceX11Backend(initialArgs);
|
||||
@@ -59,6 +62,7 @@ export function runStartupBootstrapRuntime(
|
||||
backendOverride: initialArgs.backend ?? null,
|
||||
autoStartOverlay: initialArgs.autoStartOverlay,
|
||||
texthookerOnlyMode: initialArgs.texthooker,
|
||||
backgroundMode: initialArgs.background,
|
||||
};
|
||||
|
||||
if (!deps.runGenerateConfigFlow(initialArgs)) {
|
||||
|
||||
35
src/main-entry.ts
Normal file
35
src/main-entry.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
const BACKGROUND_ARG = '--background';
|
||||
const BACKGROUND_CHILD_ENV = 'SUBMINER_BACKGROUND_CHILD';
|
||||
|
||||
function shouldDetachBackgroundLaunch(argv: string[], env: NodeJS.ProcessEnv): boolean {
|
||||
if (env.ELECTRON_RUN_AS_NODE === '1') return false;
|
||||
if (!argv.includes(BACKGROUND_ARG)) return false;
|
||||
if (env[BACKGROUND_CHILD_ENV] === '1') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function sanitizeBackgroundEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||
const env = { ...baseEnv };
|
||||
env[BACKGROUND_CHILD_ENV] = '1';
|
||||
if (!env.NODE_NO_WARNINGS) {
|
||||
env.NODE_NO_WARNINGS = '1';
|
||||
}
|
||||
if (typeof env.VK_INSTANCE_LAYERS === 'string' && /lsfg/i.test(env.VK_INSTANCE_LAYERS)) {
|
||||
delete env.VK_INSTANCE_LAYERS;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
if (shouldDetachBackgroundLaunch(process.argv, process.env)) {
|
||||
const child = spawn(process.execPath, process.argv.slice(1), {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
env: sanitizeBackgroundEnv(process.env),
|
||||
});
|
||||
child.unref();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
require('./main.js');
|
||||
@@ -15,6 +15,7 @@ export interface AppLifecycleRuntimeDepsFactoryInput {
|
||||
onWillQuitCleanup: () => void;
|
||||
shouldRestoreWindowsOnActivate: () => boolean;
|
||||
restoreWindowsOnActivate: () => void;
|
||||
shouldQuitOnWindowAllClosed: () => boolean;
|
||||
}
|
||||
|
||||
export interface AppReadyRuntimeDepsFactoryInput {
|
||||
@@ -59,6 +60,7 @@ export function createAppLifecycleRuntimeDeps(
|
||||
onWillQuitCleanup: params.onWillQuitCleanup,
|
||||
shouldRestoreWindowsOnActivate: params.shouldRestoreWindowsOnActivate,
|
||||
restoreWindowsOnActivate: params.restoreWindowsOnActivate,
|
||||
shouldQuitOnWindowAllClosed: params.shouldQuitOnWindowAllClosed,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface AppLifecycleRuntimeRunnerParams {
|
||||
onWillQuitCleanup: () => void;
|
||||
shouldRestoreWindowsOnActivate: () => boolean;
|
||||
restoreWindowsOnActivate: () => void;
|
||||
shouldQuitOnWindowAllClosed: () => boolean;
|
||||
}
|
||||
|
||||
export function createAppLifecycleRuntimeRunner(
|
||||
@@ -37,6 +38,7 @@ export function createAppLifecycleRuntimeRunner(
|
||||
onWillQuitCleanup: params.onWillQuitCleanup,
|
||||
shouldRestoreWindowsOnActivate: params.shouldRestoreWindowsOnActivate,
|
||||
restoreWindowsOnActivate: params.restoreWindowsOnActivate,
|
||||
shouldQuitOnWindowAllClosed: params.shouldQuitOnWindowAllClosed,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -78,6 +78,7 @@ export interface AppState {
|
||||
backendOverride: string | null;
|
||||
autoStartOverlay: boolean;
|
||||
texthookerOnlyMode: boolean;
|
||||
backgroundMode: boolean;
|
||||
jlptLevelLookup: (term: string) => JlptLevel | null;
|
||||
frequencyRankLookup: FrequencyDictionaryLookup;
|
||||
anilistSetupPageOpened: boolean;
|
||||
@@ -90,6 +91,7 @@ export interface AppStateInitialValues {
|
||||
backendOverride?: string | null;
|
||||
autoStartOverlay?: boolean;
|
||||
texthookerOnlyMode?: boolean;
|
||||
backgroundMode?: boolean;
|
||||
}
|
||||
|
||||
export interface StartupState {
|
||||
@@ -99,6 +101,7 @@ export interface StartupState {
|
||||
backendOverride: AppState['backendOverride'];
|
||||
autoStartOverlay: AppState['autoStartOverlay'];
|
||||
texthookerOnlyMode: AppState['texthookerOnlyMode'];
|
||||
backgroundMode: AppState['backgroundMode'];
|
||||
}
|
||||
|
||||
export function createAppState(values: AppStateInitialValues): AppState {
|
||||
@@ -152,6 +155,7 @@ export function createAppState(values: AppStateInitialValues): AppState {
|
||||
backendOverride: values.backendOverride ?? null,
|
||||
autoStartOverlay: values.autoStartOverlay ?? false,
|
||||
texthookerOnlyMode: values.texthookerOnlyMode ?? false,
|
||||
backgroundMode: values.backgroundMode ?? false,
|
||||
jlptLevelLookup: () => null,
|
||||
frequencyRankLookup: () => null,
|
||||
anilistSetupPageOpened: false,
|
||||
@@ -172,4 +176,5 @@ export function applyStartupState(appState: AppState, startupState: StartupState
|
||||
appState.backendOverride = startupState.backendOverride;
|
||||
appState.autoStartOverlay = startupState.autoStartOverlay;
|
||||
appState.texthookerOnlyMode = startupState.texthookerOnlyMode;
|
||||
appState.backgroundMode = startupState.backgroundMode;
|
||||
}
|
||||
|
||||
@@ -194,6 +194,7 @@ export interface AnkiConnectConfig {
|
||||
enabled?: boolean;
|
||||
url?: string;
|
||||
pollingRate?: number;
|
||||
tags?: string[];
|
||||
fields?: {
|
||||
audio?: string;
|
||||
image?: string;
|
||||
@@ -423,6 +424,7 @@ export interface ResolvedConfig {
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
pollingRate: number;
|
||||
tags: string[];
|
||||
fields: {
|
||||
audio: string;
|
||||
image: string;
|
||||
|
||||
Reference in New Issue
Block a user