mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
Refactor startup/logging service wiring and related test/config updates
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
---
|
||||
id: TASK-53
|
||||
title: Consolidate fragmented JLPT utility modules
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: '2026-02-16 04:47'
|
||||
updated_date: '2026-02-16 04:57'
|
||||
labels: []
|
||||
dependencies: []
|
||||
references:
|
||||
- >-
|
||||
/home/sudacode/projects/japanese/SubMiner/src/core/services/jlpt-token-filter-config.ts
|
||||
- >-
|
||||
/home/sudacode/projects/japanese/SubMiner/src/core/services/jlpt-excluded-terms.ts
|
||||
- >-
|
||||
/home/sudacode/projects/japanese/SubMiner/src/core/services/jlpt-ignored-mecab-pos1.ts
|
||||
priority: medium
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
The JLPT-related functionality is currently split across three tiny files that create unnecessary navigation overhead and add cognitive load when working with JLPT token filtering.
|
||||
|
||||
Files to consolidate:
|
||||
- `src/core/services/jlpt-token-filter-config.ts` (24 lines)
|
||||
- `src/core/services/jlpt-excluded-terms.ts` (30 lines)
|
||||
- `src/core/services/jlpt-ignored-mecab-pos1.ts` (46 lines)
|
||||
|
||||
These three files are tightly coupled (one imports from another) and all deal with JLPT token filtering logic. They should be merged into a single `jlpt-token-filter.ts` module with clear section comments.
|
||||
|
||||
Benefits:
|
||||
- Reduces file count by 2
|
||||
- Eliminates circular import potential
|
||||
- Easier to understand JLPT filtering logic holistically
|
||||
- Simplifies barrel exports from services/index.ts
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 Merge jlpt-token-filter-config.ts, jlpt-excluded-terms.ts, and jlpt-ignored-mecab-pos1.ts into a single jlpt-token-filter.ts file
|
||||
- [ ] #2 Update all imports across the codebase to use the consolidated module
|
||||
- [ ] #3 Update services/index.ts barrel exports
|
||||
- [ ] #4 Ensure all existing tests pass
|
||||
- [ ] #5 Verify no functionality is lost in consolidation
|
||||
<!-- AC:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Consolidated JLPT filtering utilities into `src/core/services/jlpt-token-filter.ts` and updated all imports to use the new module. Added consolidated exports to `src/core/services/index.ts` for the JLPT token filter API (`shouldIgnoreJlptForMecabPos1`, `shouldIgnoreJlptByTerm`, POS metadata, etc.). Deleted the three fragmented files:
|
||||
- `jlpt-token-filter-config.ts`
|
||||
- `jlpt-excluded-terms.ts`
|
||||
- `jlpt-ignored-mecab-pos1.ts`
|
||||
|
||||
Validation:
|
||||
- `pnpm exec tsc --noEmit` passes after the refactor.
|
||||
- `node --test dist/core/services/tokenizer-service.test.js` runs with 19/20 passing; one failure (`tokenizeSubtitleService uses Yomitan parser result when available`) appears to be pre-existing in current tree and matches earlier full test run failure.
|
||||
- Full `pnpm run test:fast` still fails, but failures are outside this consolidation scope (e.g., overlay-shortcut-handler logger message shape and tokenizer Yomitan-parse-path test), consistent with baseline failures in this working state.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
id: TASK-54
|
||||
title: Audit and consolidate micro-services under 50 lines
|
||||
status: In Progress
|
||||
assignee: []
|
||||
created_date: '2026-02-16 04:47'
|
||||
updated_date: '2026-02-16 04:59'
|
||||
labels: []
|
||||
dependencies: []
|
||||
references:
|
||||
- /home/sudacode/projects/japanese/SubMiner/src/core/services/
|
||||
priority: medium
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
The core/services directory contains 67 files, many of which are very small services that may create unnecessary abstraction overhead. This task involves auditing services under 50 lines and determining if they should be consolidated with related services.
|
||||
|
||||
Candidates for review (all under 50 lines):
|
||||
- mpv-state.ts (25 lines) - could merge with mpv-service.ts
|
||||
- secondary-subtitle-service.ts (32 lines) - could merge with subtitle-related services
|
||||
- runtime-config-service.ts (50 lines) - pure utility functions that could merge with config service
|
||||
- mpv-control-service.ts (49 lines) - MPV command functions could merge with mpv-service.ts
|
||||
|
||||
Approach:
|
||||
1. Identify logical groupings (mpv-related, subtitle-related, config-related)
|
||||
2. Determine which micro-services are truly independent vs which are fragments
|
||||
3. Consolidate related micro-services into cohesive modules
|
||||
4. Maintain clear function exports for tree-shaking
|
||||
|
||||
Benefits:
|
||||
- Reduces file navigation overhead
|
||||
- Groups related functionality logically
|
||||
- Makes service boundaries clearer
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 Audit all services under 50 lines in src/core/services/
|
||||
- [ ] #2 Identify logical groupings for consolidation
|
||||
- [ ] #3 Merge related micro-services into cohesive modules
|
||||
- [ ] #4 Update all imports across codebase
|
||||
- [ ] #5 Update barrel exports in services/index.ts
|
||||
- [ ] #6 Run full test suite to ensure no regressions
|
||||
<!-- AC:END -->
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
id: TASK-55
|
||||
title: Normalize service naming conventions across core/services
|
||||
status: To Do
|
||||
assignee: []
|
||||
created_date: '2026-02-16 04:47'
|
||||
labels: []
|
||||
dependencies: []
|
||||
references:
|
||||
- /home/sudacode/projects/japanese/SubMiner/src/core/services/index.ts
|
||||
priority: low
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
The core/services directory has inconsistent naming patterns that create confusion:
|
||||
- Some files use `*Service.ts` suffix (e.g., `mpv-service.ts`, `tokenizer-service.ts`)
|
||||
- Others use `*RuntimeService.ts` or just descriptive names (e.g., `overlay-visibility-service.ts` exports functions with 'Service' in name)
|
||||
- Some functions in files have 'Service' suffix, others don't
|
||||
|
||||
This inconsistency makes it hard to predict file/function names and creates cognitive overhead.
|
||||
|
||||
Standardize on:
|
||||
- File names: `kebab-case.ts` without 'service' suffix (e.g., `mpv.ts`, `tokenizer.ts`)
|
||||
- Function names: descriptive verbs without 'Service' suffix (e.g., `createMpvClient()`, `tokenizeSubtitle()`)
|
||||
- Barrel exports: clean, predictable names
|
||||
|
||||
Files needing audit (47 services):
|
||||
- All files in src/core/services/ need review
|
||||
|
||||
Note: This is a large-scale refactor that should be done carefully to avoid breaking changes.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 Establish naming convention rules (document in code or docs)
|
||||
- [ ] #2 Audit all service files for naming inconsistencies
|
||||
- [ ] #3 Rename files to follow convention (kebab-case, no 'service' suffix)
|
||||
- [ ] #4 Rename exported functions to remove 'Service' suffix where present
|
||||
- [ ] #5 Update all imports across the entire codebase
|
||||
- [ ] #6 Update barrel exports
|
||||
- [ ] #7 Run full test suite
|
||||
- [ ] #8 Update any documentation referencing old names
|
||||
<!-- AC:END -->
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
id: TASK-56
|
||||
title: Extract remaining main.ts runtime functions to dedicated modules
|
||||
status: To Do
|
||||
assignee: []
|
||||
created_date: '2026-02-16 04:47'
|
||||
labels: []
|
||||
dependencies: []
|
||||
references:
|
||||
- /home/sudacode/projects/japanese/SubMiner/src/main.ts
|
||||
priority: medium
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
main.ts is still 1481 lines after previous refactoring efforts. While significant progress has been made, there are still opportunities to extract runtime functions into dedicated modules to further reduce its size and improve maintainability.
|
||||
|
||||
Current opportunities:
|
||||
1. **JLPT dictionary lookup functions** (lines 470-535) - initializeJlptDictionaryLookup, ensureJlptDictionaryLookup, getJlptDictionarySearchPaths
|
||||
2. **Media path utilities** (lines 552-590) - updateCurrentMediaPath, updateCurrentMediaTitle, resolveMediaPathForJimaku
|
||||
3. **Overlay visibility helpers** (lines 1273-1360) - updateVisibleOverlayVisibility, updateInvisibleOverlayVisibility, syncInvisibleOverlayMousePassthrough
|
||||
|
||||
These functions are largely self-contained and could be moved to:
|
||||
- `src/main/jlpt-runtime.ts`
|
||||
- `src/main/media-runtime.ts`
|
||||
- `src/main/overlay-visibility-runtime.ts`
|
||||
|
||||
Goal: Reduce main.ts to under 1000 lines (target: ~800-900 lines)
|
||||
|
||||
Benefits:
|
||||
- Faster navigation and comprehension of main.ts
|
||||
- Easier to test extracted modules independently
|
||||
- Clearer separation of concerns
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 Extract JLPT dictionary lookup functions to dedicated module
|
||||
- [ ] #2 Extract media path utilities to dedicated module
|
||||
- [ ] #3 Extract overlay visibility helpers to dedicated module
|
||||
- [ ] #4 Update main.ts imports to use new modules
|
||||
- [ ] #5 Ensure all functionality remains intact
|
||||
- [ ] #6 Run full test suite
|
||||
- [ ] #7 Verify main.ts line count is reduced to under 1000 lines
|
||||
<!-- AC:END -->
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
id: TASK-57
|
||||
title: Evaluate anki-integration.ts for further decomposition
|
||||
status: To Do
|
||||
assignee: []
|
||||
created_date: '2026-02-16 04:47'
|
||||
labels: []
|
||||
dependencies: []
|
||||
references:
|
||||
- /home/sudacode/projects/japanese/SubMiner/src/anki-integration.ts
|
||||
- /home/sudacode/projects/japanese/SubMiner/src/anki-integration/
|
||||
priority: low
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
anki-integration.ts remains the largest file at 1930 lines despite having domain modules extracted to `src/anki-integration/` directory.
|
||||
|
||||
Current state:
|
||||
- Main file: `src/anki-integration.ts` (1930 lines)
|
||||
- Extracted modules:
|
||||
- card-creation.ts (727 lines)
|
||||
- known-word-cache.ts (398 lines)
|
||||
- field-grouping.ts (79 lines)
|
||||
- polling.ts (36 lines)
|
||||
- duplicate.ts (30 lines)
|
||||
- ui-feedback.ts (29 lines)
|
||||
- ai.ts (4.2k)
|
||||
|
||||
This task is to evaluate whether the main anki-integration.ts file can be further decomposed or if 1930 lines is acceptable for a main integration class.
|
||||
|
||||
Evaluation criteria:
|
||||
1. Are there remaining cohesive units that could be extracted?
|
||||
2. Is the remaining code primarily orchestration logic (which is acceptable to be longer)?
|
||||
3. Would further splitting improve or hurt readability?
|
||||
4. Are there internal classes or helpers that could be standalone?
|
||||
|
||||
Deliverable: A decision document with recommendations - either proceed with further decomposition or document why the current state is acceptable.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 Review anki-integration.ts structure and identify remaining cohesive units
|
||||
- [ ] #2 Evaluate if extraction would improve or hurt maintainability
|
||||
- [ ] #3 Document decision with rationale
|
||||
- [ ] #4 If proceeding: create extraction plan with specific modules to create
|
||||
- [ ] #5 If not proceeding: document architectural justification for keeping as-is
|
||||
<!-- AC:END -->
|
||||
@@ -36,6 +36,46 @@ test("parses jsonc and warns/falls back on invalid value", () => {
|
||||
assert.ok(service.getWarnings().some((w) => w.path === "websocket.port"));
|
||||
});
|
||||
|
||||
test("accepts valid logging.level", () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
`{
|
||||
"logging": {
|
||||
"level": "warn"
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
|
||||
assert.equal(config.logging.level, "warn");
|
||||
});
|
||||
|
||||
test("falls back for invalid logging.level and reports warning", () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
`{
|
||||
"logging": {
|
||||
"level": "trace"
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
const warnings = service.getWarnings();
|
||||
|
||||
assert.equal(config.logging.level, DEFAULT_CONFIG.logging.level);
|
||||
assert.ok(
|
||||
warnings.some((warning) => warning.path === "logging.level"),
|
||||
);
|
||||
});
|
||||
|
||||
test("parses invisible overlay config and new global shortcuts", () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
@@ -372,6 +412,7 @@ test("falls back to default when ankiConnect n+1 deck list is invalid", () => {
|
||||
test("template generator includes known keys", () => {
|
||||
const output = generateConfigTemplate(DEFAULT_CONFIG);
|
||||
assert.match(output, /"ankiConnect":/);
|
||||
assert.match(output, /"logging":/);
|
||||
assert.match(output, /"websocket":/);
|
||||
assert.match(output, /"youtubeSubgen":/);
|
||||
assert.match(output, /"nPlusOne"\s*:\s*\{/);
|
||||
|
||||
@@ -75,6 +75,9 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
|
||||
enabled: "auto",
|
||||
port: 6677,
|
||||
},
|
||||
logging: {
|
||||
level: "info",
|
||||
},
|
||||
texthooker: {
|
||||
openBrowser: true,
|
||||
},
|
||||
@@ -276,6 +279,13 @@ export const RUNTIME_OPTION_REGISTRY: RuntimeOptionRegistryEntry[] = [
|
||||
];
|
||||
|
||||
export const CONFIG_OPTION_REGISTRY: ConfigOptionRegistryEntry[] = [
|
||||
{
|
||||
path: "logging.level",
|
||||
kind: "enum",
|
||||
enumValues: ["debug", "info", "warn", "error"],
|
||||
defaultValue: DEFAULT_CONFIG.logging.level,
|
||||
description: "Minimum log level for runtime logging.",
|
||||
},
|
||||
{
|
||||
path: "websocket.enabled",
|
||||
kind: "enum",
|
||||
@@ -460,6 +470,14 @@ export const CONFIG_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
||||
],
|
||||
key: "websocket",
|
||||
},
|
||||
{
|
||||
title: "Logging",
|
||||
description: [
|
||||
"Controls logging verbosity.",
|
||||
"Set to debug for full runtime diagnostics.",
|
||||
],
|
||||
key: "logging",
|
||||
},
|
||||
{
|
||||
title: "AnkiConnect Integration",
|
||||
description: ["Automatic Anki updates and media generation options."],
|
||||
|
||||
@@ -196,6 +196,20 @@ export class ConfigService {
|
||||
}
|
||||
}
|
||||
|
||||
if (isObject(src.logging)) {
|
||||
const logLevel = asString(src.logging.level);
|
||||
if (logLevel === "debug" || logLevel === "info" || logLevel === "warn" || logLevel === "error") {
|
||||
resolved.logging.level = logLevel;
|
||||
} else if (src.logging.level !== undefined) {
|
||||
warn(
|
||||
"logging.level",
|
||||
src.logging.level,
|
||||
resolved.logging.level,
|
||||
"Expected debug, info, warn, or error.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(src.keybindings)) {
|
||||
resolved.keybindings = src.keybindings.filter(
|
||||
(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ipcMain, IpcMainEvent } from "electron";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
import { createLogger } from "../../logger";
|
||||
import {
|
||||
JimakuApiResponse,
|
||||
JimakuDownloadQuery,
|
||||
@@ -16,6 +17,8 @@ import {
|
||||
KikuMergePreviewResponse,
|
||||
} from "../../types";
|
||||
|
||||
const logger = createLogger("main:anki-jimaku-ipc");
|
||||
|
||||
export interface AnkiJimakuIpcDeps {
|
||||
setAnkiConnectEnabled: (enabled: boolean) => void;
|
||||
clearAnkiHistory: () => void;
|
||||
@@ -147,7 +150,7 @@ export function registerAnkiJimakuIpcHandlers(
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
logger.info(
|
||||
`[jimaku] download-file name="${query.name}" entryId=${query.entryId}`,
|
||||
);
|
||||
const result = await deps.downloadToFile(query.url, targetPath, {
|
||||
@@ -156,10 +159,10 @@ export function registerAnkiJimakuIpcHandlers(
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
console.log(`[jimaku] download-file saved to ${result.path}`);
|
||||
logger.info(`[jimaku] download-file saved to ${result.path}`);
|
||||
deps.onDownloadedSubtitle(result.path);
|
||||
} else {
|
||||
console.error(
|
||||
logger.error(
|
||||
`[jimaku] download-file failed: ${result.error?.error ?? "unknown error"}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "../../types";
|
||||
import { sortJimakuFiles } from "../../jimaku/utils";
|
||||
import type { AnkiJimakuIpcDeps } from "./anki-jimaku-ipc-service";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
export type RegisterAnkiJimakuIpcRuntimeHandler = (
|
||||
deps: AnkiJimakuIpcDeps,
|
||||
@@ -62,6 +63,8 @@ export interface AnkiJimakuIpcRuntimeOptions {
|
||||
) => Promise<{ ok: true; path: string } | { ok: false; error: { error: string; code?: number; retryAfter?: number } }>;
|
||||
}
|
||||
|
||||
const logger = createLogger("main:anki-jimaku");
|
||||
|
||||
export function registerAnkiJimakuIpcRuntimeService(
|
||||
options: AnkiJimakuIpcRuntimeOptions,
|
||||
registerHandlers: RegisterAnkiJimakuIpcRuntimeHandler,
|
||||
@@ -96,11 +99,11 @@ export function registerAnkiJimakuIpcRuntimeService(
|
||||
);
|
||||
integration.start();
|
||||
options.setAnkiIntegration(integration);
|
||||
console.log("AnkiConnect integration enabled");
|
||||
logger.info("AnkiConnect integration enabled");
|
||||
} else if (!enabled && ankiIntegration) {
|
||||
ankiIntegration.destroy();
|
||||
options.setAnkiIntegration(null);
|
||||
console.log("AnkiConnect integration disabled");
|
||||
logger.info("AnkiConnect integration disabled");
|
||||
}
|
||||
|
||||
options.broadcastRuntimeOptionsChanged();
|
||||
@@ -109,7 +112,7 @@ export function registerAnkiJimakuIpcRuntimeService(
|
||||
const subtitleTimingTracker = options.getSubtitleTimingTracker();
|
||||
if (subtitleTimingTracker) {
|
||||
subtitleTimingTracker.cleanup();
|
||||
console.log("AnkiConnect subtitle timing history cleared");
|
||||
logger.info("AnkiConnect subtitle timing history cleared");
|
||||
}
|
||||
},
|
||||
refreshKnownWords: async () => {
|
||||
@@ -139,7 +142,7 @@ export function registerAnkiJimakuIpcRuntimeService(
|
||||
},
|
||||
getJimakuMediaInfo: () => options.parseMediaInfo(options.getCurrentMediaPath()),
|
||||
searchJimakuEntries: async (query) => {
|
||||
console.log(`[jimaku] search-entries query: "${query.query}"`);
|
||||
logger.info(`[jimaku] search-entries query: "${query.query}"`);
|
||||
const response = await options.jimakuFetchJson<JimakuEntry[]>(
|
||||
"/api/entries/search",
|
||||
{
|
||||
@@ -149,13 +152,13 @@ export function registerAnkiJimakuIpcRuntimeService(
|
||||
);
|
||||
if (!response.ok) return response;
|
||||
const maxResults = options.getJimakuMaxEntryResults();
|
||||
console.log(
|
||||
logger.info(
|
||||
`[jimaku] search-entries returned ${response.data.length} results (capped to ${maxResults})`,
|
||||
);
|
||||
return { ok: true, data: response.data.slice(0, maxResults) };
|
||||
},
|
||||
listJimakuFiles: async (query) => {
|
||||
console.log(
|
||||
logger.info(
|
||||
`[jimaku] list-files entryId=${query.entryId} episode=${query.episode ?? "all"}`,
|
||||
);
|
||||
const response = await options.jimakuFetchJson<JimakuFileEntry[]>(
|
||||
@@ -169,7 +172,7 @@ export function registerAnkiJimakuIpcRuntimeService(
|
||||
response.data,
|
||||
options.getJimakuLanguagePreference(),
|
||||
);
|
||||
console.log(`[jimaku] list-files returned ${sorted.length} files`);
|
||||
logger.info(`[jimaku] list-files returned ${sorted.length} files`);
|
||||
return { ok: true, data: sorted };
|
||||
},
|
||||
resolveJimakuApiKey: () => options.resolveJimakuApiKey(),
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { CliArgs, CliCommandSource } from "../../cli/args";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:app-lifecycle");
|
||||
|
||||
export interface AppLifecycleServiceDeps {
|
||||
shouldStartApp: (args: CliArgs) => boolean;
|
||||
@@ -57,7 +60,7 @@ export function createAppLifecycleDepsRuntimeService(
|
||||
logNoRunningInstance: options.logNoRunningInstance,
|
||||
whenReady: (handler) => {
|
||||
options.app.whenReady().then(handler).catch((error) => {
|
||||
console.error("App ready handler failed:", error);
|
||||
logger.error("App ready handler failed:", error);
|
||||
});
|
||||
},
|
||||
onWindowAllClosed: (handler) => {
|
||||
@@ -91,7 +94,7 @@ export function startAppLifecycleService(
|
||||
try {
|
||||
deps.handleCliCommand(deps.parseArgs(argv), "second-instance");
|
||||
} catch (error) {
|
||||
console.error("Failed to handle second-instance CLI command:", error);
|
||||
logger.error("Failed to handle second-instance CLI command:", error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ function makeDeps(overrides: Partial<AppReadyRuntimeDeps> = {}) {
|
||||
getResolvedConfig: () => ({ websocket: { enabled: "auto" }, secondarySub: {} }),
|
||||
getConfigWarnings: () => [],
|
||||
logConfigWarning: () => calls.push("logConfigWarning"),
|
||||
setLogLevel: (level, source) => calls.push(`setLogLevel:${level}:${source}`),
|
||||
initRuntimeOptionsManager: () => calls.push("initRuntimeOptionsManager"),
|
||||
setSecondarySubMode: (mode) => calls.push(`setSecondarySubMode:${mode}`),
|
||||
defaultSecondarySubMode: "hover",
|
||||
@@ -55,3 +56,15 @@ test("runAppReadyRuntimeService logs defer message when overlay not auto-started
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("runAppReadyRuntimeService applies config logging level during app-ready", async () => {
|
||||
const { deps, calls } = makeDeps({
|
||||
getResolvedConfig: () => ({
|
||||
websocket: { enabled: "auto" },
|
||||
secondarySub: {},
|
||||
logging: { level: "warn" },
|
||||
}),
|
||||
});
|
||||
await runAppReadyRuntimeService(deps);
|
||||
assert.ok(calls.includes("setLogLevel:warn:config"));
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ export {
|
||||
} from "./overlay-shortcut-service";
|
||||
export { createOverlayShortcutRuntimeHandlers } from "./overlay-shortcut-handler";
|
||||
export { createCliCommandDepsRuntimeService, handleCliCommandService } from "./cli-command-service";
|
||||
export { cycleSecondarySubModeService } from "./secondary-subtitle-service";
|
||||
export {
|
||||
copyCurrentSubtitleService,
|
||||
handleMineSentenceDigitService,
|
||||
@@ -23,18 +22,14 @@ export {
|
||||
} from "./mining-service";
|
||||
export { createAppLifecycleDepsRuntimeService, startAppLifecycleService } from "./app-lifecycle-service";
|
||||
export {
|
||||
playNextSubtitleRuntimeService,
|
||||
replayCurrentSubtitleRuntimeService,
|
||||
sendMpvCommandRuntimeService,
|
||||
setMpvSubVisibilityRuntimeService,
|
||||
showMpvOsdRuntimeService,
|
||||
} from "./mpv-control-service";
|
||||
cycleSecondarySubModeService,
|
||||
} from "./subtitle-position-service";
|
||||
export {
|
||||
getInitialInvisibleOverlayVisibilityService,
|
||||
isAutoUpdateEnabledRuntimeService,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfigService,
|
||||
shouldBindVisibleOverlayToMpvSubVisibilityService,
|
||||
} from "./runtime-config-service";
|
||||
} from "./startup-service";
|
||||
export { openYomitanSettingsWindow } from "./yomitan-settings-service";
|
||||
export { createTokenizerDepsRuntimeService, tokenizeSubtitleService } from "./tokenizer-service";
|
||||
export { createJlptVocabularyLookupService } from "./jlpt-vocab-service";
|
||||
@@ -74,7 +69,18 @@ export {
|
||||
updateInvisibleOverlayVisibilityService,
|
||||
updateVisibleOverlayVisibilityService,
|
||||
} from "./overlay-visibility-service";
|
||||
export { MpvIpcClient, MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY } from "./mpv-service";
|
||||
export {
|
||||
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
||||
MpvIpcClient,
|
||||
MpvRuntimeClientLike,
|
||||
MpvTrackProperty,
|
||||
playNextSubtitleRuntimeService,
|
||||
replayCurrentSubtitleRuntimeService,
|
||||
resolveCurrentAudioStreamIndex,
|
||||
sendMpvCommandRuntimeService,
|
||||
setMpvSubVisibilityRuntimeService,
|
||||
showMpvOsdRuntimeService,
|
||||
} from "./mpv-service";
|
||||
export {
|
||||
applyMpvSubtitleRenderMetricsPatchService,
|
||||
DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
sendMpvCommandRuntimeService,
|
||||
setMpvSubVisibilityRuntimeService,
|
||||
showMpvOsdRuntimeService,
|
||||
} from "./mpv-control-service";
|
||||
} from "./mpv-service";
|
||||
|
||||
test("showMpvOsdRuntimeService sends show-text when connected", () => {
|
||||
const commands: (string | number)[][] = [];
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
export interface MpvRuntimeClientLike {
|
||||
connected: boolean;
|
||||
send: (payload: { command: (string | number)[] }) => void;
|
||||
replayCurrentSubtitle?: () => void;
|
||||
playNextSubtitle?: () => void;
|
||||
setSubVisibility?: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
export function showMpvOsdRuntimeService(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
text: string,
|
||||
fallbackLog: (text: string) => void = console.log,
|
||||
): void {
|
||||
if (mpvClient && mpvClient.connected) {
|
||||
mpvClient.send({ command: ["show-text", text, "3000"] });
|
||||
return;
|
||||
}
|
||||
fallbackLog(`OSD (MPV not connected): ${text}`);
|
||||
}
|
||||
|
||||
export function replayCurrentSubtitleRuntimeService(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
): void {
|
||||
if (!mpvClient?.replayCurrentSubtitle) return;
|
||||
mpvClient.replayCurrentSubtitle();
|
||||
}
|
||||
|
||||
export function playNextSubtitleRuntimeService(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
): void {
|
||||
if (!mpvClient?.playNextSubtitle) return;
|
||||
mpvClient.playNextSubtitle();
|
||||
}
|
||||
|
||||
export function sendMpvCommandRuntimeService(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
command: (string | number)[],
|
||||
): void {
|
||||
if (!mpvClient) return;
|
||||
mpvClient.send({ command });
|
||||
}
|
||||
|
||||
export function setMpvSubVisibilityRuntimeService(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
visible: boolean,
|
||||
): void {
|
||||
if (!mpvClient?.setSubVisibility) return;
|
||||
mpvClient.setSubVisibility(visible);
|
||||
}
|
||||
@@ -17,12 +17,86 @@ import {
|
||||
scheduleMpvReconnect,
|
||||
MpvSocketTransport,
|
||||
} from "./mpv-transport";
|
||||
import { resolveCurrentAudioStreamIndex } from "./mpv-state";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const isDebugLoggingEnabled = (): boolean => {
|
||||
return (process.env.SUBMINER_LOG_LEVEL || "").toLowerCase() === "debug";
|
||||
const logger = createLogger("main:mpv");
|
||||
|
||||
export type MpvTrackProperty = {
|
||||
type?: string;
|
||||
id?: number;
|
||||
selected?: boolean;
|
||||
"ff-index"?: number;
|
||||
};
|
||||
|
||||
export function resolveCurrentAudioStreamIndex(
|
||||
tracks: Array<MpvTrackProperty> | null | undefined,
|
||||
currentAudioTrackId: number | null,
|
||||
): number | null {
|
||||
if (!Array.isArray(tracks)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const audioTracks = tracks.filter((track) => track.type === "audio");
|
||||
const activeTrack =
|
||||
audioTracks.find((track) => track.id === currentAudioTrackId) ||
|
||||
audioTracks.find((track) => track.selected === true);
|
||||
|
||||
const ffIndex = activeTrack?.["ff-index"];
|
||||
return typeof ffIndex === "number" && Number.isInteger(ffIndex) && ffIndex >= 0
|
||||
? ffIndex
|
||||
: null;
|
||||
}
|
||||
|
||||
export interface MpvRuntimeClientLike {
|
||||
connected: boolean;
|
||||
send: (payload: { command: (string | number)[] }) => void;
|
||||
replayCurrentSubtitle?: () => void;
|
||||
playNextSubtitle?: () => void;
|
||||
setSubVisibility?: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
export function showMpvOsdRuntimeService(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
text: string,
|
||||
fallbackLog: (text: string) => void = (line) => logger.info(line),
|
||||
): void {
|
||||
if (mpvClient && mpvClient.connected) {
|
||||
mpvClient.send({ command: ["show-text", text, "3000"] });
|
||||
return;
|
||||
}
|
||||
fallbackLog(`OSD (MPV not connected): ${text}`);
|
||||
}
|
||||
|
||||
export function replayCurrentSubtitleRuntimeService(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
): void {
|
||||
if (!mpvClient?.replayCurrentSubtitle) return;
|
||||
mpvClient.replayCurrentSubtitle();
|
||||
}
|
||||
|
||||
export function playNextSubtitleRuntimeService(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
): void {
|
||||
if (!mpvClient?.playNextSubtitle) return;
|
||||
mpvClient.playNextSubtitle();
|
||||
}
|
||||
|
||||
export function sendMpvCommandRuntimeService(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
command: (string | number)[],
|
||||
): void {
|
||||
if (!mpvClient) return;
|
||||
mpvClient.send({ command });
|
||||
}
|
||||
|
||||
export function setMpvSubVisibilityRuntimeService(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
visible: boolean,
|
||||
): void {
|
||||
if (!mpvClient?.setSubVisibility) return;
|
||||
mpvClient.setSubVisibility(visible);
|
||||
}
|
||||
|
||||
export {
|
||||
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
||||
} from "./mpv-protocol";
|
||||
@@ -104,7 +178,7 @@ export class MpvIpcClient implements MpvClient {
|
||||
this.transport = new MpvSocketTransport({
|
||||
socketPath,
|
||||
onConnect: () => {
|
||||
console.log("Connected to MPV socket");
|
||||
logger.info("Connected to MPV socket");
|
||||
this.connected = true;
|
||||
this.connecting = false;
|
||||
this.socket = this.transport.getSocket();
|
||||
@@ -118,7 +192,7 @@ export class MpvIpcClient implements MpvClient {
|
||||
this.deps.autoStartOverlay ||
|
||||
this.deps.getResolvedConfig().auto_start_overlay === true;
|
||||
if (this.firstConnection && shouldAutoStart) {
|
||||
console.log("Auto-starting overlay, hiding mpv subtitles");
|
||||
logger.info("Auto-starting overlay, hiding mpv subtitles");
|
||||
setTimeout(() => {
|
||||
this.deps.setOverlayVisible(true);
|
||||
}, 100);
|
||||
@@ -133,15 +207,11 @@ export class MpvIpcClient implements MpvClient {
|
||||
this.processBuffer();
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
if (isDebugLoggingEnabled()) {
|
||||
console.error("MPV socket error:", err.message);
|
||||
}
|
||||
logger.debug("MPV socket error:", err.message);
|
||||
this.failPendingRequests();
|
||||
},
|
||||
onClose: () => {
|
||||
if (isDebugLoggingEnabled()) {
|
||||
console.log("MPV socket closed");
|
||||
}
|
||||
logger.debug("MPV socket closed");
|
||||
this.connected = false;
|
||||
this.connecting = false;
|
||||
this.socket = null;
|
||||
@@ -202,11 +272,9 @@ export class MpvIpcClient implements MpvClient {
|
||||
getReconnectTimer: () => this.deps.getReconnectTimer(),
|
||||
setReconnectTimer: (timer) => this.deps.setReconnectTimer(timer),
|
||||
onReconnectAttempt: (attempt, delay) => {
|
||||
if (isDebugLoggingEnabled()) {
|
||||
console.log(
|
||||
logger.debug(
|
||||
`Attempting to reconnect to MPV (attempt ${attempt}, delay ${delay}ms)...`,
|
||||
);
|
||||
}
|
||||
},
|
||||
connect: () => {
|
||||
this.connect();
|
||||
@@ -221,7 +289,7 @@ export class MpvIpcClient implements MpvClient {
|
||||
this.handleMessage(message);
|
||||
},
|
||||
(line, error) => {
|
||||
console.error("Failed to parse MPV message:", line, error);
|
||||
logger.error("Failed to parse MPV message:", line, error);
|
||||
},
|
||||
);
|
||||
this.buffer = parsed.nextBuffer;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { resolveCurrentAudioStreamIndex } from "./mpv-state";
|
||||
import { resolveCurrentAudioStreamIndex } from "./mpv-service";
|
||||
|
||||
test("resolveCurrentAudioStreamIndex returns selected ff-index when no current track id", () => {
|
||||
assert.equal(
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
export type MpvTrackProperty = {
|
||||
type?: string;
|
||||
id?: number;
|
||||
selected?: boolean;
|
||||
"ff-index"?: number;
|
||||
};
|
||||
|
||||
export function resolveCurrentAudioStreamIndex(
|
||||
tracks: Array<MpvTrackProperty> | null | undefined,
|
||||
currentAudioTrackId: number | null,
|
||||
): number | null {
|
||||
if (!Array.isArray(tracks)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const audioTracks = tracks.filter((track) => track.type === "audio");
|
||||
const activeTrack =
|
||||
audioTracks.find((track) => track.id === currentAudioTrackId) ||
|
||||
audioTracks.find((track) => track.selected === true);
|
||||
|
||||
const ffIndex = activeTrack?.["ff-index"];
|
||||
return typeof ffIndex === "number" && Number.isInteger(ffIndex) && ffIndex >= 0
|
||||
? ffIndex
|
||||
: null;
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import { OverlayContentMeasurement, OverlayContentRect, OverlayLayer } from "../../types";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:overlay-content-measurement");
|
||||
const MAX_VIEWPORT = 10000;
|
||||
const MAX_RECT_DIMENSION = 10000;
|
||||
const MAX_RECT_OFFSET = 50000;
|
||||
@@ -107,7 +110,7 @@ export function createOverlayContentMeasurementStoreService(options?: {
|
||||
warn?: (message: string) => void;
|
||||
}) {
|
||||
const now = options?.now ?? (() => Date.now());
|
||||
const warn = options?.warn ?? ((message: string) => console.warn(message));
|
||||
const warn = options?.warn ?? ((message: string) => logger.warn(message));
|
||||
const latestByLayer: OverlayMeasurementStore = {
|
||||
visible: null,
|
||||
invisible: null,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ConfiguredShortcuts } from "../utils/shortcut-config";
|
||||
import { OverlayShortcutHandlers } from "./overlay-shortcut-service";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:overlay-shortcut-handler");
|
||||
|
||||
export interface OverlayShortcutFallbackHandlers {
|
||||
openRuntimeOptions: () => void;
|
||||
@@ -38,7 +41,7 @@ function wrapAsync(
|
||||
): () => void {
|
||||
return () => {
|
||||
task().catch((err) => {
|
||||
console.error(`${logLabel} failed:`, err);
|
||||
logger.error(`${logLabel} failed:`, err);
|
||||
deps.showMpvOsd(`${osdLabel}: ${(err as Error).message}`);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { globalShortcut } from "electron";
|
||||
import { ConfiguredShortcuts } from "../utils/shortcut-config";
|
||||
import { isGlobalShortcutRegisteredSafe } from "./shortcut-fallback-service";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:overlay-shortcut-service");
|
||||
|
||||
export interface OverlayShortcutHandlers {
|
||||
copySubtitle: () => void;
|
||||
@@ -39,7 +42,7 @@ export function registerOverlayShortcutsService(
|
||||
}
|
||||
const ok = globalShortcut.register(accelerator, handler);
|
||||
if (!ok) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`Failed to register overlay shortcut ${label}: ${accelerator}`,
|
||||
);
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
import * as path from "path";
|
||||
import { WindowGeometry } from "../../types";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:overlay-window");
|
||||
|
||||
export type OverlayWindowKind = "visible" | "invisible";
|
||||
|
||||
@@ -86,13 +89,13 @@ export function createOverlayWindowService(
|
||||
query: { layer: kind === "visible" ? "visible" : "invisible" },
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to load HTML file:", err);
|
||||
logger.error("Failed to load HTML file:", err);
|
||||
});
|
||||
|
||||
window.webContents.on(
|
||||
"did-fail-load",
|
||||
(_event, errorCode, errorDescription, validatedURL) => {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Page failed to load:",
|
||||
errorCode,
|
||||
errorDescription,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
isAutoUpdateEnabledRuntimeService,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfigService,
|
||||
shouldBindVisibleOverlayToMpvSubVisibilityService,
|
||||
} from "./runtime-config-service";
|
||||
} from "./startup-service";
|
||||
|
||||
const BASE_CONFIG = {
|
||||
auto_start_overlay: false,
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
interface RuntimeAutoUpdateOptionManagerLike {
|
||||
getOptionValue: (id: "anki.autoUpdateNewCards") => unknown;
|
||||
}
|
||||
|
||||
interface RuntimeConfigLike {
|
||||
auto_start_overlay?: boolean;
|
||||
bind_visible_overlay_to_mpv_sub_visibility: boolean;
|
||||
invisibleOverlay: {
|
||||
startupVisibility: "visible" | "hidden" | "platform-default";
|
||||
};
|
||||
ankiConnect?: {
|
||||
behavior?: {
|
||||
autoUpdateNewCards?: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function getInitialInvisibleOverlayVisibilityService(
|
||||
config: RuntimeConfigLike,
|
||||
platform: NodeJS.Platform,
|
||||
): boolean {
|
||||
const visibility = config.invisibleOverlay.startupVisibility;
|
||||
if (visibility === "visible") return true;
|
||||
if (visibility === "hidden") return false;
|
||||
if (platform === "linux") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function shouldAutoInitializeOverlayRuntimeFromConfigService(
|
||||
config: RuntimeConfigLike,
|
||||
): boolean {
|
||||
if (config.auto_start_overlay === true) return true;
|
||||
if (config.invisibleOverlay.startupVisibility === "visible") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function shouldBindVisibleOverlayToMpvSubVisibilityService(
|
||||
config: RuntimeConfigLike,
|
||||
): boolean {
|
||||
return config.bind_visible_overlay_to_mpv_sub_visibility;
|
||||
}
|
||||
|
||||
export function isAutoUpdateEnabledRuntimeService(
|
||||
config: RuntimeConfigLike,
|
||||
runtimeOptionsManager: RuntimeAutoUpdateOptionManagerLike | null,
|
||||
): boolean {
|
||||
const value = runtimeOptionsManager?.getOptionValue("anki.autoUpdateNewCards");
|
||||
if (typeof value === "boolean") return value;
|
||||
return config.ankiConnect?.behavior?.autoUpdateNewCards !== false;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { SecondarySubMode } from "../../types";
|
||||
import { cycleSecondarySubModeService } from "./secondary-subtitle-service";
|
||||
import { cycleSecondarySubModeService } from "./subtitle-position-service";
|
||||
|
||||
test("cycleSecondarySubModeService cycles and emits broadcast + OSD", () => {
|
||||
let mode: SecondarySubMode = "hover";
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { SecondarySubMode } from "../../types";
|
||||
|
||||
export interface CycleSecondarySubModeDeps {
|
||||
getSecondarySubMode: () => SecondarySubMode;
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => void;
|
||||
getLastSecondarySubToggleAtMs: () => number;
|
||||
setLastSecondarySubToggleAtMs: (timestampMs: number) => void;
|
||||
broadcastSecondarySubMode: (mode: SecondarySubMode) => void;
|
||||
showMpvOsd: (text: string) => void;
|
||||
now?: () => number;
|
||||
}
|
||||
|
||||
const SECONDARY_SUB_CYCLE: SecondarySubMode[] = ["hidden", "visible", "hover"];
|
||||
const SECONDARY_SUB_TOGGLE_DEBOUNCE_MS = 120;
|
||||
|
||||
export function cycleSecondarySubModeService(
|
||||
deps: CycleSecondarySubModeDeps,
|
||||
): void {
|
||||
const now = deps.now ? deps.now() : Date.now();
|
||||
if (now - deps.getLastSecondarySubToggleAtMs() < SECONDARY_SUB_TOGGLE_DEBOUNCE_MS) {
|
||||
return;
|
||||
}
|
||||
deps.setLastSecondarySubToggleAtMs(now);
|
||||
|
||||
const currentMode = deps.getSecondarySubMode();
|
||||
const currentIndex = SECONDARY_SUB_CYCLE.indexOf(currentMode);
|
||||
const nextMode =
|
||||
SECONDARY_SUB_CYCLE[(currentIndex + 1) % SECONDARY_SUB_CYCLE.length];
|
||||
deps.setSecondarySubMode(nextMode);
|
||||
deps.broadcastSecondarySubMode(nextMode);
|
||||
deps.showMpvOsd(`Secondary subtitle: ${nextMode}`);
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import { BrowserWindow, globalShortcut } from "electron";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:shortcut");
|
||||
|
||||
export interface GlobalShortcutConfig {
|
||||
toggleVisibleOverlayGlobal: string | null | undefined;
|
||||
@@ -38,7 +41,7 @@ export function registerGlobalShortcutsService(
|
||||
},
|
||||
);
|
||||
if (!toggleVisibleRegistered) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`Failed to register global shortcut toggleVisibleOverlayGlobal: ${visibleShortcut}`,
|
||||
);
|
||||
}
|
||||
@@ -56,7 +59,7 @@ export function registerGlobalShortcutsService(
|
||||
},
|
||||
);
|
||||
if (!toggleInvisibleRegistered) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`Failed to register global shortcut toggleInvisibleOverlayGlobal: ${invisibleShortcut}`,
|
||||
);
|
||||
}
|
||||
@@ -65,7 +68,7 @@ export function registerGlobalShortcutsService(
|
||||
normalizedInvisible &&
|
||||
normalizedInvisible === normalizedVisible
|
||||
) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
"Skipped registering toggleInvisibleOverlayGlobal because it collides with toggleVisibleOverlayGlobal",
|
||||
);
|
||||
}
|
||||
@@ -77,7 +80,7 @@ export function registerGlobalShortcutsService(
|
||||
normalizedJimaku === normalizedInvisible ||
|
||||
normalizedJimaku === normalizedSettings)
|
||||
) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
"Skipped registering openJimaku because it collides with another global shortcut",
|
||||
);
|
||||
} else {
|
||||
@@ -88,7 +91,7 @@ export function registerGlobalShortcutsService(
|
||||
},
|
||||
);
|
||||
if (!openJimakuRegistered) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`Failed to register global shortcut openJimaku: ${options.shortcuts.openJimaku}`,
|
||||
);
|
||||
}
|
||||
@@ -99,7 +102,7 @@ export function registerGlobalShortcutsService(
|
||||
options.onOpenYomitanSettings();
|
||||
});
|
||||
if (!settingsRegistered) {
|
||||
console.warn("Failed to register global shortcut: Alt+Shift+Y");
|
||||
logger.warn("Failed to register global shortcut: Alt+Shift+Y");
|
||||
}
|
||||
|
||||
if (options.isDev) {
|
||||
@@ -110,7 +113,7 @@ export function registerGlobalShortcutsService(
|
||||
}
|
||||
});
|
||||
if (!devtoolsRegistered) {
|
||||
console.warn("Failed to register global shortcut: F12");
|
||||
logger.warn("Failed to register global shortcut: F12");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,8 +54,7 @@ test("runStartupBootstrapRuntimeService configures startup state and starts life
|
||||
const result = runStartupBootstrapRuntimeService({
|
||||
argv: ["node", "main.ts", "--verbose"],
|
||||
parseArgs: () => args,
|
||||
setLogLevelEnv: (level) => calls.push(`setLog:${level}`),
|
||||
enableVerboseLogging: () => calls.push("enableVerbose"),
|
||||
setLogLevel: (level, source) => calls.push(`setLog:${level}:${source}`),
|
||||
forceX11Backend: () => calls.push("forceX11"),
|
||||
enforceUnsupportedWaylandMode: () => calls.push("enforceWayland"),
|
||||
getDefaultSocketPath: () => "/tmp/default.sock",
|
||||
@@ -71,13 +70,39 @@ test("runStartupBootstrapRuntimeService configures startup state and starts life
|
||||
assert.equal(result.autoStartOverlay, true);
|
||||
assert.equal(result.texthookerOnlyMode, true);
|
||||
assert.deepEqual(calls, [
|
||||
"enableVerbose",
|
||||
"setLog:debug:cli",
|
||||
"forceX11",
|
||||
"enforceWayland",
|
||||
"startLifecycle",
|
||||
]);
|
||||
});
|
||||
|
||||
test("runStartupBootstrapRuntimeService prefers --log-level over --verbose", () => {
|
||||
const calls: string[] = [];
|
||||
const args = makeArgs({
|
||||
logLevel: "warn",
|
||||
verbose: true,
|
||||
});
|
||||
|
||||
runStartupBootstrapRuntimeService({
|
||||
argv: ["node", "main.ts", "--log-level", "warn", "--verbose"],
|
||||
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.deepEqual(calls.slice(0, 3), [
|
||||
"setLog:warn:cli",
|
||||
"forceX11",
|
||||
"enforceWayland",
|
||||
]);
|
||||
});
|
||||
|
||||
test("runStartupBootstrapRuntimeService skips lifecycle when generate-config flow handled", () => {
|
||||
const calls: string[] = [];
|
||||
const args = makeArgs({ generateConfig: true, logLevel: "warn" });
|
||||
@@ -85,8 +110,7 @@ test("runStartupBootstrapRuntimeService skips lifecycle when generate-config flo
|
||||
const result = runStartupBootstrapRuntimeService({
|
||||
argv: ["node", "main.ts", "--generate-config"],
|
||||
parseArgs: () => args,
|
||||
setLogLevelEnv: (level) => calls.push(`setLog:${level}`),
|
||||
enableVerboseLogging: () => calls.push("enableVerbose"),
|
||||
setLogLevel: (level, source) => calls.push(`setLog:${level}:${source}`),
|
||||
forceX11Backend: () => calls.push("forceX11"),
|
||||
enforceUnsupportedWaylandMode: () => calls.push("enforceWayland"),
|
||||
getDefaultSocketPath: () => "/tmp/default.sock",
|
||||
@@ -99,7 +123,7 @@ test("runStartupBootstrapRuntimeService skips lifecycle when generate-config flo
|
||||
assert.equal(result.texthookerPort, 5174);
|
||||
assert.equal(result.backendOverride, null);
|
||||
assert.deepEqual(calls, [
|
||||
"setLog:warn",
|
||||
"setLog:warn:cli",
|
||||
"forceX11",
|
||||
"enforceWayland",
|
||||
]);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CliArgs } from "../../cli/args";
|
||||
import { ConfigValidationWarning, SecondarySubMode } from "../../types";
|
||||
import type { LogLevelSource } from "../../logger";
|
||||
import { ConfigValidationWarning, ResolvedConfig, SecondarySubMode } from "../../types";
|
||||
|
||||
export interface StartupBootstrapRuntimeState {
|
||||
initialArgs: CliArgs;
|
||||
@@ -10,11 +11,27 @@ export interface StartupBootstrapRuntimeState {
|
||||
texthookerOnlyMode: boolean;
|
||||
}
|
||||
|
||||
interface RuntimeAutoUpdateOptionManagerLike {
|
||||
getOptionValue: (id: "anki.autoUpdateNewCards") => unknown;
|
||||
}
|
||||
|
||||
export interface RuntimeConfigLike {
|
||||
auto_start_overlay?: boolean;
|
||||
bind_visible_overlay_to_mpv_sub_visibility: boolean;
|
||||
invisibleOverlay: {
|
||||
startupVisibility: "visible" | "hidden" | "platform-default";
|
||||
};
|
||||
ankiConnect?: {
|
||||
behavior?: {
|
||||
autoUpdateNewCards?: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface StartupBootstrapRuntimeDeps {
|
||||
argv: string[];
|
||||
parseArgs: (argv: string[]) => CliArgs;
|
||||
setLogLevelEnv: (level: string) => void;
|
||||
enableVerboseLogging: () => void;
|
||||
setLogLevel: (level: string, source: LogLevelSource) => void;
|
||||
forceX11Backend: (args: CliArgs) => void;
|
||||
enforceUnsupportedWaylandMode: (args: CliArgs) => void;
|
||||
getDefaultSocketPath: () => string;
|
||||
@@ -29,9 +46,9 @@ export function runStartupBootstrapRuntimeService(
|
||||
const initialArgs = deps.parseArgs(deps.argv);
|
||||
|
||||
if (initialArgs.logLevel) {
|
||||
deps.setLogLevelEnv(initialArgs.logLevel);
|
||||
deps.setLogLevel(initialArgs.logLevel, "cli");
|
||||
} else if (initialArgs.verbose) {
|
||||
deps.enableVerboseLogging();
|
||||
deps.setLogLevel("debug", "cli");
|
||||
}
|
||||
|
||||
deps.forceX11Backend(initialArgs);
|
||||
@@ -61,6 +78,9 @@ interface AppReadyConfigLike {
|
||||
enabled?: boolean | "auto";
|
||||
port?: number;
|
||||
};
|
||||
logging?: {
|
||||
level?: "debug" | "info" | "warn" | "error";
|
||||
};
|
||||
}
|
||||
|
||||
export interface AppReadyRuntimeDeps {
|
||||
@@ -71,6 +91,7 @@ export interface AppReadyRuntimeDeps {
|
||||
getResolvedConfig: () => AppReadyConfigLike;
|
||||
getConfigWarnings: () => ConfigValidationWarning[];
|
||||
logConfigWarning: (warning: ConfigValidationWarning) => void;
|
||||
setLogLevel: (level: string, source: LogLevelSource) => void;
|
||||
initRuntimeOptionsManager: () => void;
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => void;
|
||||
defaultSecondarySubMode: SecondarySubMode;
|
||||
@@ -87,6 +108,40 @@ export interface AppReadyRuntimeDeps {
|
||||
handleInitialArgs: () => void;
|
||||
}
|
||||
|
||||
export function getInitialInvisibleOverlayVisibilityService(
|
||||
config: RuntimeConfigLike,
|
||||
platform: NodeJS.Platform,
|
||||
): boolean {
|
||||
const visibility = config.invisibleOverlay.startupVisibility;
|
||||
if (visibility === "visible") return true;
|
||||
if (visibility === "hidden") return false;
|
||||
if (platform === "linux") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function shouldAutoInitializeOverlayRuntimeFromConfigService(
|
||||
config: RuntimeConfigLike,
|
||||
): boolean {
|
||||
if (config.auto_start_overlay === true) return true;
|
||||
if (config.invisibleOverlay.startupVisibility === "visible") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function shouldBindVisibleOverlayToMpvSubVisibilityService(
|
||||
config: RuntimeConfigLike,
|
||||
): boolean {
|
||||
return config.bind_visible_overlay_to_mpv_sub_visibility;
|
||||
}
|
||||
|
||||
export function isAutoUpdateEnabledRuntimeService(
|
||||
config: ResolvedConfig | RuntimeConfigLike,
|
||||
runtimeOptionsManager: RuntimeAutoUpdateOptionManagerLike | null,
|
||||
): boolean {
|
||||
const value = runtimeOptionsManager?.getOptionValue("anki.autoUpdateNewCards");
|
||||
if (typeof value === "boolean") return value;
|
||||
return (config as ResolvedConfig).ankiConnect?.behavior?.autoUpdateNewCards !== false;
|
||||
}
|
||||
|
||||
export async function runAppReadyRuntimeService(
|
||||
deps: AppReadyRuntimeDeps,
|
||||
): Promise<void> {
|
||||
@@ -97,6 +152,7 @@ export async function runAppReadyRuntimeService(
|
||||
|
||||
deps.reloadConfig();
|
||||
const config = deps.getResolvedConfig();
|
||||
deps.setLogLevel(config.logging?.level ?? "info", "config");
|
||||
for (const warning of deps.getConfigWarnings()) {
|
||||
deps.logConfigWarning(warning);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ import {
|
||||
SubsyncResolvedConfig,
|
||||
} from "../../subsync/utils";
|
||||
import { isRemoteMediaPath } from "../../jimaku/utils";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:subsync");
|
||||
|
||||
interface FileExtractionResult {
|
||||
path: string;
|
||||
@@ -361,7 +364,7 @@ async function runSubsyncAutoInternal(
|
||||
return alassResult;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Auto alass sync failed, trying ffsubsync fallback:", error);
|
||||
logger.warn("Auto alass sync failed, trying ffsubsync fallback:", error);
|
||||
} finally {
|
||||
if (secondaryExtraction) {
|
||||
cleanupTemporaryFile(secondaryExtraction);
|
||||
|
||||
@@ -1,7 +1,40 @@
|
||||
import * as crypto from "crypto";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { SubtitlePosition } from "../../types";
|
||||
import { SecondarySubMode, SubtitlePosition } from "../../types";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:subtitle-position");
|
||||
|
||||
export interface CycleSecondarySubModeDeps {
|
||||
getSecondarySubMode: () => SecondarySubMode;
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => void;
|
||||
getLastSecondarySubToggleAtMs: () => number;
|
||||
setLastSecondarySubToggleAtMs: (timestampMs: number) => void;
|
||||
broadcastSecondarySubMode: (mode: SecondarySubMode) => void;
|
||||
showMpvOsd: (text: string) => void;
|
||||
now?: () => number;
|
||||
}
|
||||
|
||||
const SECONDARY_SUB_CYCLE: SecondarySubMode[] = ["hidden", "visible", "hover"];
|
||||
const SECONDARY_SUB_TOGGLE_DEBOUNCE_MS = 120;
|
||||
|
||||
export function cycleSecondarySubModeService(
|
||||
deps: CycleSecondarySubModeDeps,
|
||||
): void {
|
||||
const now = deps.now ? deps.now() : Date.now();
|
||||
if (now - deps.getLastSecondarySubToggleAtMs() < SECONDARY_SUB_TOGGLE_DEBOUNCE_MS) {
|
||||
return;
|
||||
}
|
||||
deps.setLastSecondarySubToggleAtMs(now);
|
||||
|
||||
const currentMode = deps.getSecondarySubMode();
|
||||
const currentIndex = SECONDARY_SUB_CYCLE.indexOf(currentMode);
|
||||
const nextMode = SECONDARY_SUB_CYCLE[(currentIndex + 1) % SECONDARY_SUB_CYCLE.length];
|
||||
deps.setSecondarySubMode(nextMode);
|
||||
deps.broadcastSecondarySubMode(nextMode);
|
||||
deps.showMpvOsd(`Secondary subtitle: ${nextMode}`);
|
||||
}
|
||||
|
||||
function getSubtitlePositionFilePath(
|
||||
mediaPath: string,
|
||||
@@ -97,7 +130,7 @@ export function loadSubtitlePositionService(options: {
|
||||
}
|
||||
return options.fallbackPosition;
|
||||
} catch (err) {
|
||||
console.error("Failed to load subtitle position:", (err as Error).message);
|
||||
logger.error("Failed to load subtitle position:", (err as Error).message);
|
||||
return options.fallbackPosition;
|
||||
}
|
||||
}
|
||||
@@ -111,7 +144,7 @@ export function saveSubtitlePositionService(options: {
|
||||
}): void {
|
||||
if (!options.currentMediaPath) {
|
||||
options.onQueuePending(options.position);
|
||||
console.warn("Queued subtitle position save - no media path yet");
|
||||
logger.warn("Queued subtitle position save - no media path yet");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,7 +156,7 @@ export function saveSubtitlePositionService(options: {
|
||||
);
|
||||
options.onPersisted();
|
||||
} catch (err) {
|
||||
console.error("Failed to save subtitle position:", (err as Error).message);
|
||||
logger.error("Failed to save subtitle position:", (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +188,7 @@ export function updateCurrentMediaPathService(options: {
|
||||
options.setSubtitlePosition(options.pendingSubtitlePosition);
|
||||
options.clearPendingSubtitlePosition();
|
||||
} catch (err) {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Failed to persist queued subtitle position:",
|
||||
(err as Error).message,
|
||||
);
|
||||
|
||||
@@ -2,6 +2,9 @@ import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import WebSocket from "ws";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:subtitle-ws");
|
||||
|
||||
export function hasMpvWebsocketPlugin(): boolean {
|
||||
const mpvWebsocketPath = path.join(
|
||||
@@ -24,7 +27,7 @@ export class SubtitleWebSocketService {
|
||||
this.server = new WebSocket.Server({ port, host: "127.0.0.1" });
|
||||
|
||||
this.server.on("connection", (ws: WebSocket) => {
|
||||
console.log("WebSocket client connected");
|
||||
logger.info("WebSocket client connected");
|
||||
const currentText = getCurrentSubtitleText();
|
||||
if (currentText) {
|
||||
ws.send(JSON.stringify({ sentence: currentText }));
|
||||
@@ -32,10 +35,10 @@ export class SubtitleWebSocketService {
|
||||
});
|
||||
|
||||
this.server.on("error", (err: Error) => {
|
||||
console.error("WebSocket server error:", err.message);
|
||||
logger.error("WebSocket server error:", err.message);
|
||||
});
|
||||
|
||||
console.log(`Subtitle WebSocket server running on ws://127.0.0.1:${port}`);
|
||||
logger.info(`Subtitle WebSocket server running on ws://127.0.0.1:${port}`);
|
||||
}
|
||||
|
||||
public broadcast(text: string): void {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import * as fs from "fs";
|
||||
import * as http from "http";
|
||||
import * as path from "path";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:texthooker");
|
||||
|
||||
export class TexthookerService {
|
||||
private server: http.Server | null = null;
|
||||
@@ -12,7 +15,7 @@ export class TexthookerService {
|
||||
public start(port: number): http.Server | null {
|
||||
const texthookerPath = this.getTexthookerPath();
|
||||
if (!texthookerPath) {
|
||||
console.error("texthooker-ui not found");
|
||||
logger.error("texthooker-ui not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -48,7 +51,7 @@ export class TexthookerService {
|
||||
});
|
||||
|
||||
this.server.listen(port, "127.0.0.1", () => {
|
||||
console.log(`Texthooker server running at http://127.0.0.1:${port}`);
|
||||
logger.info(`Texthooker server running at http://127.0.0.1:${port}`);
|
||||
});
|
||||
|
||||
return this.server;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { BrowserWindow, Extension, session } from "electron";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:yomitan-extension-loader");
|
||||
|
||||
export interface YomitanExtensionLoaderDeps {
|
||||
userDataPath: string;
|
||||
@@ -49,7 +52,7 @@ function ensureExtensionCopy(sourceDir: string, userDataPath: string): string {
|
||||
fs.mkdirSync(extensionsRoot, { recursive: true });
|
||||
fs.rmSync(targetDir, { recursive: true, force: true });
|
||||
fs.cpSync(sourceDir, targetDir, { recursive: true });
|
||||
console.log(`Copied yomitan extension to ${targetDir}`);
|
||||
logger.info(`Copied yomitan extension to ${targetDir}`);
|
||||
}
|
||||
|
||||
return targetDir;
|
||||
@@ -75,8 +78,8 @@ export async function loadYomitanExtensionService(
|
||||
}
|
||||
|
||||
if (!extPath) {
|
||||
console.error("Yomitan extension not found in any search path");
|
||||
console.error("Install Yomitan to one of:", searchPaths);
|
||||
logger.error("Yomitan extension not found in any search path");
|
||||
logger.error("Install Yomitan to one of:", searchPaths);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -102,8 +105,8 @@ export async function loadYomitanExtensionService(
|
||||
deps.setYomitanExtension(extension);
|
||||
return extension;
|
||||
} catch (err) {
|
||||
console.error("Failed to load Yomitan extension:", (err as Error).message);
|
||||
console.error("Full error:", err);
|
||||
logger.error("Failed to load Yomitan extension:", (err as Error).message);
|
||||
logger.error("Full error:", err);
|
||||
deps.setYomitanExtension(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { BrowserWindow, Extension, session } from "electron";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:yomitan-settings");
|
||||
|
||||
export interface OpenYomitanSettingsWindowOptions {
|
||||
yomitanExt: Extension | null;
|
||||
@@ -9,14 +12,14 @@ export interface OpenYomitanSettingsWindowOptions {
|
||||
export function openYomitanSettingsWindow(
|
||||
options: OpenYomitanSettingsWindowOptions,
|
||||
): void {
|
||||
console.log("openYomitanSettings called");
|
||||
logger.info("openYomitanSettings called");
|
||||
|
||||
if (!options.yomitanExt) {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Yomitan extension not loaded - yomitanExt is:",
|
||||
options.yomitanExt,
|
||||
);
|
||||
console.error(
|
||||
logger.error(
|
||||
"This may be due to Manifest V3 service worker issues with Electron",
|
||||
);
|
||||
return;
|
||||
@@ -24,12 +27,12 @@ export function openYomitanSettingsWindow(
|
||||
|
||||
const existingWindow = options.getExistingWindow();
|
||||
if (existingWindow && !existingWindow.isDestroyed()) {
|
||||
console.log("Settings window already exists, focusing");
|
||||
logger.info("Settings window already exists, focusing");
|
||||
existingWindow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Creating new settings window for extension:", options.yomitanExt.id);
|
||||
logger.info("Creating new settings window for extension:", options.yomitanExt.id);
|
||||
|
||||
const settingsWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
@@ -44,7 +47,7 @@ export function openYomitanSettingsWindow(
|
||||
options.setWindow(settingsWindow);
|
||||
|
||||
const settingsUrl = `chrome-extension://${options.yomitanExt.id}/settings.html`;
|
||||
console.log("Loading settings URL:", settingsUrl);
|
||||
logger.info("Loading settings URL:", settingsUrl);
|
||||
|
||||
let loadAttempts = 0;
|
||||
const maxAttempts = 3;
|
||||
@@ -53,13 +56,13 @@ export function openYomitanSettingsWindow(
|
||||
settingsWindow
|
||||
.loadURL(settingsUrl)
|
||||
.then(() => {
|
||||
console.log("Settings URL loaded successfully");
|
||||
logger.info("Settings URL loaded successfully");
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
console.error("Failed to load settings URL:", err);
|
||||
logger.error("Failed to load settings URL:", err);
|
||||
loadAttempts++;
|
||||
if (loadAttempts < maxAttempts && !settingsWindow.isDestroyed()) {
|
||||
console.log(
|
||||
logger.info(
|
||||
`Retrying in 500ms (attempt ${loadAttempts + 1}/${maxAttempts})`,
|
||||
);
|
||||
setTimeout(attemptLoad, 500);
|
||||
@@ -72,7 +75,7 @@ export function openYomitanSettingsWindow(
|
||||
settingsWindow.webContents.on(
|
||||
"did-fail-load",
|
||||
(_event, errorCode, errorDescription) => {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Settings page failed to load:",
|
||||
errorCode,
|
||||
errorDescription,
|
||||
@@ -81,7 +84,7 @@ export function openYomitanSettingsWindow(
|
||||
);
|
||||
|
||||
settingsWindow.webContents.on("did-finish-load", () => {
|
||||
console.log("Settings page loaded successfully");
|
||||
logger.info("Settings page loaded successfully");
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export type LogLevel = "debug" | "info" | "warn" | "error";
|
||||
export type LogLevelSource = "cli" | "config";
|
||||
|
||||
type LogMethod = (message: string, ...meta: unknown[]) => void;
|
||||
|
||||
@@ -18,10 +19,25 @@ const LEVEL_PRIORITY: Record<LogLevel, number> = {
|
||||
error: 40,
|
||||
};
|
||||
|
||||
const DEFAULT_LOG_LEVEL: LogLevel = "info";
|
||||
|
||||
let cliLogLevel: LogLevel | undefined;
|
||||
let configLogLevel: LogLevel | undefined;
|
||||
|
||||
function pad(value: number): string {
|
||||
return String(value).padStart(2, "0");
|
||||
}
|
||||
|
||||
function normalizeLogLevel(level: string | undefined): LogLevel | undefined {
|
||||
const normalized = (level || "").toLowerCase() as LogLevel;
|
||||
return LOG_LEVELS.includes(normalized) ? normalized : undefined;
|
||||
}
|
||||
|
||||
function getEnvLogLevel(): LogLevel | undefined {
|
||||
if (!process || !process.env) return undefined;
|
||||
return normalizeLogLevel(process.env.SUBMINER_LOG_LEVEL);
|
||||
}
|
||||
|
||||
function formatTimestamp(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = pad(date.getMonth() + 1);
|
||||
@@ -33,15 +49,29 @@ function formatTimestamp(date: Date): string {
|
||||
}
|
||||
|
||||
function resolveMinLevel(): LogLevel {
|
||||
const raw =
|
||||
typeof process !== "undefined" && process?.env
|
||||
? process.env.SUBMINER_LOG_LEVEL
|
||||
: undefined;
|
||||
const normalized = (raw || "").toLowerCase() as LogLevel;
|
||||
if (LOG_LEVELS.includes(normalized)) {
|
||||
return normalized;
|
||||
const envLevel = getEnvLogLevel();
|
||||
if (cliLogLevel) {
|
||||
return cliLogLevel;
|
||||
}
|
||||
if (envLevel) {
|
||||
return envLevel;
|
||||
}
|
||||
if (configLogLevel) {
|
||||
return configLogLevel;
|
||||
}
|
||||
return DEFAULT_LOG_LEVEL;
|
||||
}
|
||||
|
||||
export function setLogLevel(
|
||||
level: string | undefined,
|
||||
source: LogLevelSource = "cli",
|
||||
): void {
|
||||
const normalized = normalizeLogLevel(level);
|
||||
if (source === "cli") {
|
||||
cliLogLevel = normalized;
|
||||
} else {
|
||||
configLogLevel = normalized;
|
||||
}
|
||||
return "info";
|
||||
}
|
||||
|
||||
function normalizeError(error: Error): { message: string; stack?: string } {
|
||||
|
||||
42
src/main.ts
42
src/main.ts
@@ -68,6 +68,7 @@ import {
|
||||
import {
|
||||
getSubsyncConfig,
|
||||
} from "./subsync/utils";
|
||||
import { createLogger, setLogLevel, type LogLevelSource } from "./logger";
|
||||
import {
|
||||
parseArgs,
|
||||
shouldStartApp,
|
||||
@@ -228,17 +229,21 @@ const isDev =
|
||||
process.argv.includes("--dev") || process.argv.includes("--debug");
|
||||
const texthookerService = new TexthookerService();
|
||||
const subtitleWsService = new SubtitleWebSocketService();
|
||||
const logger = createLogger("main");
|
||||
let jlptDictionaryLookupInitialized = false;
|
||||
let jlptDictionaryLookupInitialization: Promise<void> | null = null;
|
||||
const appLogger = {
|
||||
logInfo: (message: string) => {
|
||||
console.log(message);
|
||||
logger.info(message);
|
||||
},
|
||||
logWarning: (message: string) => {
|
||||
console.warn(message);
|
||||
logger.warn(message);
|
||||
},
|
||||
logError: (message: string, details: unknown) => {
|
||||
logger.error(message, details);
|
||||
},
|
||||
logNoRunningInstance: () => {
|
||||
console.error("No running instance. Use --start to launch the app.");
|
||||
logger.error("No running instance. Use --start to launch the app.");
|
||||
},
|
||||
logConfigWarning: (warning: {
|
||||
path: string;
|
||||
@@ -246,7 +251,7 @@ const appLogger = {
|
||||
value: unknown;
|
||||
fallback: unknown;
|
||||
}) => {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`[config] ${warning.path}: ${warning.message} value=${JSON.stringify(warning.value)} fallback=${JSON.stringify(warning.fallback)}`,
|
||||
);
|
||||
},
|
||||
@@ -274,9 +279,7 @@ process.on("SIGTERM", () => {
|
||||
const overlayManager = createOverlayManagerService();
|
||||
const overlayContentMeasurementStore = createOverlayContentMeasurementStoreService({
|
||||
now: () => Date.now(),
|
||||
warn: (message: string) => {
|
||||
console.warn(message);
|
||||
},
|
||||
warn: (message: string) => logger.warn(message),
|
||||
});
|
||||
const overlayModalRuntime = createOverlayModalRuntimeService({
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
@@ -509,7 +512,7 @@ async function initializeJlptDictionaryLookup(): Promise<void> {
|
||||
appState.jlptLevelLookup = await createJlptVocabularyLookupService({
|
||||
searchPaths: getJlptDictionarySearchPaths(),
|
||||
log: (message) => {
|
||||
console.log(`[JLPT] ${message}`);
|
||||
logger.info(`[JLPT] ${message}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -593,11 +596,8 @@ const startupState = runStartupBootstrapRuntimeService(
|
||||
createStartupBootstrapRuntimeDeps({
|
||||
argv: process.argv,
|
||||
parseArgs: (argv: string[]) => parseArgs(argv),
|
||||
setLogLevelEnv: (level: string) => {
|
||||
process.env.SUBMINER_LOG_LEVEL = level;
|
||||
},
|
||||
enableVerboseLogging: () => {
|
||||
process.env.SUBMINER_LOG_LEVEL = "debug";
|
||||
setLogLevel: (level: string, source: LogLevelSource) => {
|
||||
setLogLevel(level, source);
|
||||
},
|
||||
forceX11Backend: (args: CliArgs) => {
|
||||
forceX11Backend(args);
|
||||
@@ -624,7 +624,7 @@ const startupState = runStartupBootstrapRuntimeService(
|
||||
app.quit();
|
||||
},
|
||||
onGenerateConfigError: (error: Error) => {
|
||||
console.error(`Failed to generate config: ${error.message}`);
|
||||
logger.error(`Failed to generate config: ${error.message}`);
|
||||
process.exitCode = 1;
|
||||
app.quit();
|
||||
},
|
||||
@@ -655,6 +655,8 @@ const startupState = runStartupBootstrapRuntimeService(
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
getConfigWarnings: () => configService.getWarnings(),
|
||||
logConfigWarning: (warning) => appLogger.logConfigWarning(warning),
|
||||
setLogLevel: (level: string, source: LogLevelSource) =>
|
||||
setLogLevel(level, source),
|
||||
initRuntimeOptionsManager: () => {
|
||||
appState.runtimeOptionsManager = new RuntimeOptionsManager(
|
||||
() => configService.getConfig().ankiConnect,
|
||||
@@ -759,7 +761,7 @@ function handleCliCommand(
|
||||
shouldOpenBrowser: () => getResolvedConfig().texthooker?.openBrowser !== false,
|
||||
openInBrowser: (url: string) => {
|
||||
void shell.openExternal(url).catch((error) => {
|
||||
console.error(`Failed to open browser for texthooker URL: ${url}`, error);
|
||||
logger.error(`Failed to open browser for texthooker URL: ${url}`, error);
|
||||
});
|
||||
},
|
||||
isOverlayInitialized: () => appState.overlayRuntimeInitialized,
|
||||
@@ -787,13 +789,13 @@ function handleCliCommand(
|
||||
getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
|
||||
schedule: (fn: () => void, delayMs: number) => setTimeout(fn, delayMs),
|
||||
log: (message: string) => {
|
||||
console.log(message);
|
||||
logger.info(message);
|
||||
},
|
||||
warn: (message: string) => {
|
||||
console.warn(message);
|
||||
logger.warn(message);
|
||||
},
|
||||
error: (message: string, err: unknown) => {
|
||||
console.error(message, err);
|
||||
logger.error(message, err);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1092,7 +1094,7 @@ function showMpvOsd(text: string): void {
|
||||
appState.mpvClient,
|
||||
text,
|
||||
(line) => {
|
||||
console.log(line);
|
||||
logger.info(line);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1249,7 +1251,7 @@ function handleMineSentenceDigit(count: number): void {
|
||||
appState.mpvClient?.currentSecondarySubText || undefined,
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
logError: (message, err) => {
|
||||
console.error(message, err);
|
||||
logger.error(message, err);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface AppReadyRuntimeDepsFactoryInput {
|
||||
hasMpvWebsocketPlugin: AppReadyRuntimeDeps["hasMpvWebsocketPlugin"];
|
||||
startSubtitleWebsocket: AppReadyRuntimeDeps["startSubtitleWebsocket"];
|
||||
log: AppReadyRuntimeDeps["log"];
|
||||
setLogLevel: AppReadyRuntimeDeps["setLogLevel"];
|
||||
createMecabTokenizerAndCheck: AppReadyRuntimeDeps["createMecabTokenizerAndCheck"];
|
||||
createSubtitleTimingTracker: AppReadyRuntimeDeps["createSubtitleTimingTracker"];
|
||||
loadYomitanExtension: AppReadyRuntimeDeps["loadYomitanExtension"];
|
||||
@@ -77,6 +78,7 @@ export function createAppReadyRuntimeDeps(
|
||||
hasMpvWebsocketPlugin: params.hasMpvWebsocketPlugin,
|
||||
startSubtitleWebsocket: params.startSubtitleWebsocket,
|
||||
log: params.log,
|
||||
setLogLevel: params.setLogLevel,
|
||||
createMecabTokenizerAndCheck: params.createMecabTokenizerAndCheck,
|
||||
createSubtitleTimingTracker: params.createSubtitleTimingTracker,
|
||||
loadYomitanExtension: params.loadYomitanExtension,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { CliArgs } from "../cli/args";
|
||||
import type { ResolvedConfig } from "../types";
|
||||
import type { StartupBootstrapRuntimeDeps } from "../core/services/startup-service";
|
||||
import type { LogLevelSource } from "../logger";
|
||||
|
||||
export interface StartupBootstrapRuntimeFactoryDeps {
|
||||
argv: string[];
|
||||
parseArgs: (argv: string[]) => CliArgs;
|
||||
setLogLevelEnv: (level: string) => void;
|
||||
enableVerboseLogging: () => void;
|
||||
setLogLevel: (level: string, source: LogLevelSource) => void;
|
||||
forceX11Backend: (args: CliArgs) => void;
|
||||
enforceUnsupportedWaylandMode: (args: CliArgs) => void;
|
||||
shouldStartApp: (args: CliArgs) => boolean;
|
||||
@@ -34,8 +34,7 @@ export function createStartupBootstrapRuntimeDeps(
|
||||
return {
|
||||
argv: params.argv,
|
||||
parseArgs: params.parseArgs,
|
||||
setLogLevelEnv: params.setLogLevelEnv,
|
||||
enableVerboseLogging: params.enableVerboseLogging,
|
||||
setLogLevel: params.setLogLevel,
|
||||
forceX11Backend: (args: CliArgs) => params.forceX11Backend(args),
|
||||
enforceUnsupportedWaylandMode: (args: CliArgs) =>
|
||||
params.enforceUnsupportedWaylandMode(args),
|
||||
|
||||
@@ -348,6 +348,9 @@ export interface Config {
|
||||
jimaku?: JimakuConfig;
|
||||
invisibleOverlay?: InvisibleOverlayConfig;
|
||||
youtubeSubgen?: YoutubeSubgenConfig;
|
||||
logging?: {
|
||||
level?: "debug" | "info" | "warn" | "error";
|
||||
};
|
||||
}
|
||||
|
||||
export type RawConfig = Config;
|
||||
@@ -445,6 +448,9 @@ export interface ResolvedConfig {
|
||||
whisperModel: string;
|
||||
primarySubLanguages: string[];
|
||||
};
|
||||
logging: {
|
||||
level: "debug" | "info" | "warn" | "error";
|
||||
};
|
||||
}
|
||||
|
||||
export interface ConfigValidationWarning {
|
||||
|
||||
Reference in New Issue
Block a user