mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-27 06:12:05 -07:00
refactor: split shared type entrypoints
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v0.9.3 (2026-03-25)
|
||||||
|
- Moved YouTube primary subtitle language defaults to `youtube.primarySubLanguages`.
|
||||||
|
- Removed the placeholder YouTube subtitle retime step; downloaded primary subtitle tracks are now used directly.
|
||||||
|
- Removed the old internal YouTube retime helper and its tests.
|
||||||
|
- Clarified optional `alass` / `ffsubsync` subtitle-sync setup and fallback behavior in the docs.
|
||||||
|
- Removed the legacy `youtubeSubgen.primarySubLanguages` config path from generated config and docs.
|
||||||
|
|
||||||
## v0.9.2 (2026-03-25)
|
## v0.9.2 (2026-03-25)
|
||||||
- Fixed overlay pointer tracking so Windows click-through toggles immediately when the cursor enters or leaves subtitle regions.
|
- Fixed overlay pointer tracking so Windows click-through toggles immediately when the cursor enters or leaves subtitle regions.
|
||||||
- Fixed Windows overlay window tracking on scaled displays by converting native tracked window bounds to Electron DIP coordinates.
|
- Fixed Windows overlay window tracking on scaled displays by converting native tracked window bounds to Electron DIP coordinates.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# Architecture Map
|
# Architecture Map
|
||||||
|
|
||||||
Status: active
|
Status: active
|
||||||
Last verified: 2026-03-13
|
Last verified: 2026-03-26
|
||||||
Owner: Kyle Yasuda
|
Owner: Kyle Yasuda
|
||||||
Read when: runtime ownership, composition boundaries, or layering questions
|
Read when: runtime ownership, composition boundaries, or layering questions
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@ The desktop app keeps `src/main.ts` as composition root and pushes behavior into
|
|||||||
- `src/core/services/` owns focused runtime services plus pure or side-effect-bounded logic.
|
- `src/core/services/` owns focused runtime services plus pure or side-effect-bounded logic.
|
||||||
- `src/renderer/` owns overlay rendering and input behavior.
|
- `src/renderer/` owns overlay rendering and input behavior.
|
||||||
- `src/config/` owns config definitions, defaults, loading, and resolution.
|
- `src/config/` owns config definitions, defaults, loading, and resolution.
|
||||||
|
- `src/types/` owns shared cross-runtime contracts via domain entrypoints; `src/types.ts` stays a compatibility barrel.
|
||||||
- `src/main/runtime/composers/` owns larger domain compositions.
|
- `src/main/runtime/composers/` owns larger domain compositions.
|
||||||
|
|
||||||
## Architecture Intent
|
## Architecture Intent
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AnkiConnectConfig } from './types';
|
import type { AnkiConnectConfig } from './types/anki';
|
||||||
|
|
||||||
type NoteFieldValue = { value?: string } | string | null | undefined;
|
type NoteFieldValue = { value?: string } | string | null | undefined;
|
||||||
|
|
||||||
@@ -8,7 +8,9 @@ function normalizeFieldName(value: string | null | undefined): string | null {
|
|||||||
return trimmed.length > 0 ? trimmed : null;
|
return trimmed.length > 0 ? trimmed : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getConfiguredWordFieldName(config?: Pick<AnkiConnectConfig, 'fields'> | null): string {
|
export function getConfiguredWordFieldName(
|
||||||
|
config?: Pick<AnkiConnectConfig, 'fields'> | null,
|
||||||
|
): string {
|
||||||
return normalizeFieldName(config?.fields?.word) ?? 'Expression';
|
return normalizeFieldName(config?.fields?.word) ?? 'Expression';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AiConfig } from '../types';
|
import type { AiConfig } from '../types/integrations';
|
||||||
import { requestAiChatCompletion } from '../ai/client';
|
import { requestAiChatCompletion } from '../ai/client';
|
||||||
|
|
||||||
const DEFAULT_AI_SYSTEM_PROMPT =
|
const DEFAULT_AI_SYSTEM_PROMPT =
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as os from 'node:os';
|
|||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
|
|
||||||
import { DEFAULT_ANKI_CONNECT_CONFIG } from '../config';
|
import { DEFAULT_ANKI_CONNECT_CONFIG } from '../config';
|
||||||
import type { AnkiConnectConfig } from '../types';
|
import type { AnkiConnectConfig } from '../types/anki';
|
||||||
|
|
||||||
type NoteInfoLike = {
|
type NoteInfoLike = {
|
||||||
noteId: number;
|
noteId: number;
|
||||||
@@ -36,9 +36,7 @@ export function extractSoundFilenames(value: string): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function shouldSyncAnimatedImageToWordAudio(config: Pick<AnkiConnectConfig, 'media'>): boolean {
|
function shouldSyncAnimatedImageToWordAudio(config: Pick<AnkiConnectConfig, 'media'>): boolean {
|
||||||
return (
|
return config.media?.imageType === 'avif' && config.media?.syncAnimatedImageToWordAudio !== false;
|
||||||
config.media?.imageType === 'avif' && config.media?.syncAnimatedImageToWordAudio !== false
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function probeAudioDurationSeconds(
|
export async function probeAudioDurationSeconds(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
|
|
||||||
import { CardCreationService } from './card-creation';
|
import { CardCreationService } from './card-creation';
|
||||||
import type { AnkiConnectConfig } from '../types';
|
import type { AnkiConnectConfig } from '../types/anki';
|
||||||
|
|
||||||
test('CardCreationService counts locally created sentence cards', async () => {
|
test('CardCreationService counts locally created sentence cards', async () => {
|
||||||
const minedCards: Array<{ count: number; noteIds?: number[] }> = [];
|
const minedCards: Array<{ count: number; noteIds?: number[] }> = [];
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import {
|
|||||||
getConfiguredWordFieldName,
|
getConfiguredWordFieldName,
|
||||||
getPreferredWordValueFromExtractedFields,
|
getPreferredWordValueFromExtractedFields,
|
||||||
} from '../anki-field-config';
|
} from '../anki-field-config';
|
||||||
import { AiConfig, AnkiConnectConfig } from '../types';
|
import { AnkiConnectConfig } from '../types/anki';
|
||||||
import { createLogger } from '../logger';
|
import { createLogger } from '../logger';
|
||||||
import { SubtitleTimingTracker } from '../subtitle-timing-tracker';
|
import { SubtitleTimingTracker } from '../subtitle-timing-tracker';
|
||||||
import { MpvClient } from '../types';
|
import { AiConfig } from '../types/integrations';
|
||||||
|
import { MpvClient } from '../types/runtime';
|
||||||
import { resolveSentenceBackText } from './ai';
|
import { resolveSentenceBackText } from './ai';
|
||||||
import { resolveMediaGenerationInputPath } from './media-source';
|
import { resolveMediaGenerationInputPath } from './media-source';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AnkiConnectConfig } from '../types';
|
import { AnkiConnectConfig } from '../types/anki';
|
||||||
import { getConfiguredWordFieldName } from '../anki-field-config';
|
import { getConfiguredWordFieldName } from '../anki-field-config';
|
||||||
|
|
||||||
interface FieldGroupingMergeMedia {
|
interface FieldGroupingMergeMedia {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { FieldGroupingWorkflow } from './field-grouping-workflow';
|
import { FieldGroupingWorkflow } from './field-grouping-workflow';
|
||||||
import type { KikuDuplicateCardInfo, KikuFieldGroupingChoice } from '../types';
|
import type { KikuDuplicateCardInfo, KikuFieldGroupingChoice } from '../types/anki';
|
||||||
|
|
||||||
type NoteInfo = {
|
type NoteInfo = {
|
||||||
noteId: number;
|
noteId: number;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { KikuDuplicateCardInfo, KikuFieldGroupingChoice } from '../types';
|
import { KikuDuplicateCardInfo, KikuFieldGroupingChoice } from '../types/anki';
|
||||||
import { getPreferredWordValueFromExtractedFields } from '../anki-field-config';
|
import { getPreferredWordValueFromExtractedFields } from '../anki-field-config';
|
||||||
|
|
||||||
export interface FieldGroupingWorkflowNoteInfo {
|
export interface FieldGroupingWorkflowNoteInfo {
|
||||||
@@ -181,7 +181,8 @@ export class FieldGroupingWorkflow {
|
|||||||
return {
|
return {
|
||||||
noteId: noteInfo.noteId,
|
noteId: noteInfo.noteId,
|
||||||
expression:
|
expression:
|
||||||
getPreferredWordValueFromExtractedFields(fields, this.deps.getConfig()) || fallbackExpression,
|
getPreferredWordValueFromExtractedFields(fields, this.deps.getConfig()) ||
|
||||||
|
fallbackExpression,
|
||||||
sentencePreview: this.deps.truncateSentence(
|
sentencePreview: this.deps.truncateSentence(
|
||||||
fields[(sentenceCardConfig.sentenceField || 'sentence').toLowerCase()] ||
|
fields[(sentenceCardConfig.sentenceField || 'sentence').toLowerCase()] ||
|
||||||
(isOriginal ? '' : this.deps.getCurrentSubtitleText() || ''),
|
(isOriginal ? '' : this.deps.getCurrentSubtitleText() || ''),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { KikuMergePreviewResponse } from '../types';
|
import { KikuMergePreviewResponse } from '../types/anki';
|
||||||
import { createLogger } from '../logger';
|
import { createLogger } from '../logger';
|
||||||
import { getPreferredWordValueFromExtractedFields } from '../anki-field-config';
|
import { getPreferredWordValueFromExtractedFields } from '../anki-field-config';
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import fs from 'node:fs';
|
|||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
import type { AnkiConnectConfig } from '../types';
|
import type { AnkiConnectConfig } from '../types/anki';
|
||||||
import { KnownWordCacheManager } from './known-word-cache';
|
import { KnownWordCacheManager } from './known-word-cache';
|
||||||
|
|
||||||
async function waitForCondition(
|
async function waitForCondition(
|
||||||
@@ -351,10 +351,7 @@ test('KnownWordCacheManager preserves cache state key captured before refresh wo
|
|||||||
scope: string;
|
scope: string;
|
||||||
words: string[];
|
words: string[];
|
||||||
};
|
};
|
||||||
assert.equal(
|
assert.equal(persisted.scope, '{"refreshMinutes":1,"scope":"is:note","fieldsWord":"Word"}');
|
||||||
persisted.scope,
|
|
||||||
'{"refreshMinutes":1,"scope":"is:note","fieldsWord":"Word"}',
|
|
||||||
);
|
|
||||||
assert.deepEqual(persisted.words, ['猫']);
|
assert.deepEqual(persisted.words, ['猫']);
|
||||||
} finally {
|
} finally {
|
||||||
fs.rmSync(stateDir, { recursive: true, force: true });
|
fs.rmSync(stateDir, { recursive: true, force: true });
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import path from 'path';
|
|||||||
|
|
||||||
import { DEFAULT_ANKI_CONNECT_CONFIG } from '../config';
|
import { DEFAULT_ANKI_CONNECT_CONFIG } from '../config';
|
||||||
import { getConfiguredWordFieldName } from '../anki-field-config';
|
import { getConfiguredWordFieldName } from '../anki-field-config';
|
||||||
import { AnkiConnectConfig } from '../types';
|
import { AnkiConnectConfig } from '../types/anki';
|
||||||
import { createLogger } from '../logger';
|
import { createLogger } from '../logger';
|
||||||
|
|
||||||
const log = createLogger('anki').child('integration.known-word-cache');
|
const log = createLogger('anki').child('integration.known-word-cache');
|
||||||
@@ -316,9 +316,9 @@ export class KnownWordCacheManager {
|
|||||||
const currentDeck = this.deps.getConfig().deck?.trim();
|
const currentDeck = this.deps.getConfig().deck?.trim();
|
||||||
const selectedDeckEntry =
|
const selectedDeckEntry =
|
||||||
currentDeck !== undefined && currentDeck.length > 0
|
currentDeck !== undefined && currentDeck.length > 0
|
||||||
? trimmedDeckEntries.find(([deckName]) => deckName === currentDeck) ?? null
|
? (trimmedDeckEntries.find(([deckName]) => deckName === currentDeck) ?? null)
|
||||||
: trimmedDeckEntries.length === 1
|
: trimmedDeckEntries.length === 1
|
||||||
? trimmedDeckEntries[0] ?? null
|
? (trimmedDeckEntries[0] ?? null)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!selectedDeckEntry) {
|
if (!selectedDeckEntry) {
|
||||||
@@ -329,7 +329,10 @@ export class KnownWordCacheManager {
|
|||||||
if (Array.isArray(deckFields)) {
|
if (Array.isArray(deckFields)) {
|
||||||
const normalizedFields = [
|
const normalizedFields = [
|
||||||
...new Set(
|
...new Set(
|
||||||
deckFields.map(String).map((field) => field.trim()).filter((field) => field.length > 0),
|
deckFields
|
||||||
|
.map(String)
|
||||||
|
.map((field) => field.trim())
|
||||||
|
.filter((field) => field.length > 0),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
if (normalizedFields.length > 0) {
|
if (normalizedFields.length > 0) {
|
||||||
@@ -353,7 +356,14 @@ export class KnownWordCacheManager {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const normalizedFields = Array.isArray(fields)
|
const normalizedFields = Array.isArray(fields)
|
||||||
? [...new Set(fields.map(String).map((field) => field.trim()).filter(Boolean))]
|
? [
|
||||||
|
...new Set(
|
||||||
|
fields
|
||||||
|
.map(String)
|
||||||
|
.map((field) => field.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
),
|
||||||
|
]
|
||||||
: [];
|
: [];
|
||||||
scopes.push({
|
scopes.push({
|
||||||
query: `deck:"${escapeAnkiSearchValue(trimmedDeckName)}"`,
|
query: `deck:"${escapeAnkiSearchValue(trimmedDeckName)}"`,
|
||||||
@@ -402,7 +412,10 @@ export class KnownWordCacheManager {
|
|||||||
private async fetchKnownWordNoteFieldsById(): Promise<Map<number, string[]>> {
|
private async fetchKnownWordNoteFieldsById(): Promise<Map<number, string[]>> {
|
||||||
const scopes = this.getKnownWordQueryScopes();
|
const scopes = this.getKnownWordQueryScopes();
|
||||||
const noteFieldsById = new Map<number, string[]>();
|
const noteFieldsById = new Map<number, string[]>();
|
||||||
log.debug('Refreshing known-word cache', `queries=${scopes.map((scope) => scope.query).join(' | ')}`);
|
log.debug(
|
||||||
|
'Refreshing known-word cache',
|
||||||
|
`queries=${scopes.map((scope) => scope.query).join(' | ')}`,
|
||||||
|
);
|
||||||
|
|
||||||
for (const scope of scopes) {
|
for (const scope of scopes) {
|
||||||
const noteIds = (await this.deps.client.findNotes(scope.query, {
|
const noteIds = (await this.deps.client.findNotes(scope.query, {
|
||||||
@@ -414,10 +427,7 @@ export class KnownWordCacheManager {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const existingFields = noteFieldsById.get(noteId) ?? [];
|
const existingFields = noteFieldsById.get(noteId) ?? [];
|
||||||
noteFieldsById.set(
|
noteFieldsById.set(noteId, [...new Set([...existingFields, ...scope.fields])]);
|
||||||
noteId,
|
|
||||||
[...new Set([...existingFields, ...scope.fields])],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { isRemoteMediaPath } from '../jimaku/utils';
|
import { isRemoteMediaPath } from '../jimaku/utils';
|
||||||
import type { MpvClient } from '../types';
|
import type { MpvClient } from '../types/runtime';
|
||||||
|
|
||||||
export type MediaGenerationKind = 'audio' | 'video';
|
export type MediaGenerationKind = 'audio' | 'video';
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ function resolvePreferredUrlFromMpvEdlSource(
|
|||||||
|
|
||||||
// mpv EDL sources usually list audio streams first and video streams last, so
|
// mpv EDL sources usually list audio streams first and video streams last, so
|
||||||
// when classifyMediaUrl cannot identify a typed URL we fall back to stream order.
|
// when classifyMediaUrl cannot identify a typed URL we fall back to stream order.
|
||||||
return kind === 'audio' ? urls[0] ?? null : urls[urls.length - 1] ?? null;
|
return kind === 'audio' ? (urls[0] ?? null) : (urls[urls.length - 1] ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveMediaGenerationInputPath(
|
export async function resolveMediaGenerationInputPath(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import test from 'node:test';
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
import { DEFAULT_ANKI_CONNECT_CONFIG } from '../config';
|
import { DEFAULT_ANKI_CONNECT_CONFIG } from '../config';
|
||||||
import type { AnkiConnectConfig } from '../types';
|
import type { AnkiConnectConfig } from '../types/anki';
|
||||||
import { AnkiIntegrationRuntime } from './runtime';
|
import { AnkiIntegrationRuntime } from './runtime';
|
||||||
|
|
||||||
function createRuntime(
|
function createRuntime(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DEFAULT_ANKI_CONNECT_CONFIG } from '../config';
|
import { DEFAULT_ANKI_CONNECT_CONFIG } from '../config';
|
||||||
import type { AnkiConnectConfig } from '../types';
|
import type { AnkiConnectConfig } from '../types/anki';
|
||||||
import {
|
import {
|
||||||
getKnownWordCacheLifecycleConfig,
|
getKnownWordCacheLifecycleConfig,
|
||||||
getKnownWordCacheRefreshIntervalMinutes,
|
getKnownWordCacheRefreshIntervalMinutes,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NotificationOptions } from '../types';
|
import { NotificationOptions } from '../types/anki';
|
||||||
|
|
||||||
export interface UiFeedbackState {
|
export interface UiFeedbackState {
|
||||||
progressDepth: number;
|
progressDepth: number;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { RawConfig, ResolvedConfig } from '../types';
|
import { RawConfig, ResolvedConfig } from '../types/config';
|
||||||
import { CORE_DEFAULT_CONFIG } from './definitions/defaults-core';
|
import { CORE_DEFAULT_CONFIG } from './definitions/defaults-core';
|
||||||
import { IMMERSION_DEFAULT_CONFIG } from './definitions/defaults-immersion';
|
import { IMMERSION_DEFAULT_CONFIG } from './definitions/defaults-immersion';
|
||||||
import { INTEGRATIONS_DEFAULT_CONFIG } from './definitions/defaults-integrations';
|
import { INTEGRATIONS_DEFAULT_CONFIG } from './definitions/defaults-integrations';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ResolvedConfig } from '../../types';
|
import { ResolvedConfig } from '../../types/config';
|
||||||
|
|
||||||
export const CORE_DEFAULT_CONFIG: Pick<
|
export const CORE_DEFAULT_CONFIG: Pick<
|
||||||
ResolvedConfig,
|
ResolvedConfig,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ResolvedConfig } from '../../types';
|
import { ResolvedConfig } from '../../types/config';
|
||||||
|
|
||||||
export const IMMERSION_DEFAULT_CONFIG: Pick<ResolvedConfig, 'immersionTracking'> = {
|
export const IMMERSION_DEFAULT_CONFIG: Pick<ResolvedConfig, 'immersionTracking'> = {
|
||||||
immersionTracking: {
|
immersionTracking: {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ResolvedConfig } from '../../types';
|
import { ResolvedConfig } from '../../types/config';
|
||||||
|
|
||||||
export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
||||||
ResolvedConfig,
|
ResolvedConfig,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ResolvedConfig } from '../../types.js';
|
import { ResolvedConfig } from '../../types/config.js';
|
||||||
|
|
||||||
export const STATS_DEFAULT_CONFIG: Pick<ResolvedConfig, 'stats'> = {
|
export const STATS_DEFAULT_CONFIG: Pick<ResolvedConfig, 'stats'> = {
|
||||||
stats: {
|
stats: {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ResolvedConfig } from '../../types';
|
import { ResolvedConfig } from '../../types/config';
|
||||||
|
|
||||||
export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle' | 'subtitleSidebar'> = {
|
export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle' | 'subtitleSidebar'> = {
|
||||||
subtitleStyle: {
|
subtitleStyle: {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ResolvedConfig } from '../../types';
|
import { ResolvedConfig } from '../../types/config';
|
||||||
import { ConfigOptionRegistryEntry } from './shared';
|
import { ConfigOptionRegistryEntry } from './shared';
|
||||||
|
|
||||||
export function buildCoreConfigOptionRegistry(
|
export function buildCoreConfigOptionRegistry(
|
||||||
@@ -263,7 +263,8 @@ export function buildCoreConfigOptionRegistry(
|
|||||||
{
|
{
|
||||||
path: `controller.bindings.${binding.id}.axisIndex`,
|
path: `controller.bindings.${binding.id}.axisIndex`,
|
||||||
kind: 'number' as const,
|
kind: 'number' as const,
|
||||||
defaultValue: binding.defaultValue.kind === 'axis' ? binding.defaultValue.axisIndex : undefined,
|
defaultValue:
|
||||||
|
binding.defaultValue.kind === 'axis' ? binding.defaultValue.axisIndex : undefined,
|
||||||
description: 'Raw axis index captured for this discrete controller action.',
|
description: 'Raw axis index captured for this discrete controller action.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -293,7 +294,8 @@ export function buildCoreConfigOptionRegistry(
|
|||||||
{
|
{
|
||||||
path: `controller.bindings.${binding.id}.axisIndex`,
|
path: `controller.bindings.${binding.id}.axisIndex`,
|
||||||
kind: 'number' as const,
|
kind: 'number' as const,
|
||||||
defaultValue: binding.defaultValue.kind === 'axis' ? binding.defaultValue.axisIndex : undefined,
|
defaultValue:
|
||||||
|
binding.defaultValue.kind === 'axis' ? binding.defaultValue.axisIndex : undefined,
|
||||||
description: 'Raw axis index captured for this analog controller action.',
|
description: 'Raw axis index captured for this analog controller action.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -302,7 +304,8 @@ export function buildCoreConfigOptionRegistry(
|
|||||||
enumValues: ['none', 'horizontal', 'vertical'],
|
enumValues: ['none', 'horizontal', 'vertical'],
|
||||||
defaultValue:
|
defaultValue:
|
||||||
binding.defaultValue.kind === 'axis' ? binding.defaultValue.dpadFallback : undefined,
|
binding.defaultValue.kind === 'axis' ? binding.defaultValue.dpadFallback : undefined,
|
||||||
description: 'Optional D-pad fallback used when this analog controller action should also read D-pad input.',
|
description:
|
||||||
|
'Optional D-pad fallback used when this analog controller action should also read D-pad input.',
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ResolvedConfig } from '../../types';
|
import { ResolvedConfig } from '../../types/config';
|
||||||
import { ConfigOptionRegistryEntry } from './shared';
|
import { ConfigOptionRegistryEntry } from './shared';
|
||||||
|
|
||||||
export function buildImmersionConfigOptionRegistry(
|
export function buildImmersionConfigOptionRegistry(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ResolvedConfig } from '../../types';
|
import { ResolvedConfig } from '../../types/config';
|
||||||
import { ConfigOptionRegistryEntry, RuntimeOptionRegistryEntry } from './shared';
|
import { ConfigOptionRegistryEntry, RuntimeOptionRegistryEntry } from './shared';
|
||||||
|
|
||||||
export function buildIntegrationConfigOptionRegistry(
|
export function buildIntegrationConfigOptionRegistry(
|
||||||
@@ -369,13 +369,15 @@ export function buildIntegrationConfigOptionRegistry(
|
|||||||
path: 'youtubeSubgen.whisperBin',
|
path: 'youtubeSubgen.whisperBin',
|
||||||
kind: 'string',
|
kind: 'string',
|
||||||
defaultValue: defaultConfig.youtubeSubgen.whisperBin,
|
defaultValue: defaultConfig.youtubeSubgen.whisperBin,
|
||||||
description: 'Legacy compatibility path kept for external subtitle fallback tools; not used by default.',
|
description:
|
||||||
|
'Legacy compatibility path kept for external subtitle fallback tools; not used by default.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'youtubeSubgen.whisperModel',
|
path: 'youtubeSubgen.whisperModel',
|
||||||
kind: 'string',
|
kind: 'string',
|
||||||
defaultValue: defaultConfig.youtubeSubgen.whisperModel,
|
defaultValue: defaultConfig.youtubeSubgen.whisperModel,
|
||||||
description: 'Legacy compatibility model path kept for external subtitle fallback tooling; not used by default.',
|
description:
|
||||||
|
'Legacy compatibility model path kept for external subtitle fallback tooling; not used by default.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'youtubeSubgen.whisperVadModel',
|
path: 'youtubeSubgen.whisperVadModel',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ResolvedConfig } from '../../types.js';
|
import { ResolvedConfig } from '../../types/config.js';
|
||||||
import { ConfigOptionRegistryEntry } from './shared.js';
|
import { ConfigOptionRegistryEntry } from './shared.js';
|
||||||
|
|
||||||
export function buildStatsConfigOptionRegistry(
|
export function buildStatsConfigOptionRegistry(
|
||||||
@@ -15,7 +15,8 @@ export function buildStatsConfigOptionRegistry(
|
|||||||
path: 'stats.markWatchedKey',
|
path: 'stats.markWatchedKey',
|
||||||
kind: 'string',
|
kind: 'string',
|
||||||
defaultValue: defaultConfig.stats.markWatchedKey,
|
defaultValue: defaultConfig.stats.markWatchedKey,
|
||||||
description: 'Key code to mark the current video as watched and advance to the next playlist entry.',
|
description:
|
||||||
|
'Key code to mark the current video as watched and advance to the next playlist entry.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'stats.serverPort',
|
path: 'stats.serverPort',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ResolvedConfig } from '../../types';
|
import { ResolvedConfig } from '../../types/config';
|
||||||
import { ConfigOptionRegistryEntry } from './shared';
|
import { ConfigOptionRegistryEntry } from './shared';
|
||||||
|
|
||||||
export function buildSubtitleConfigOptionRegistry(
|
export function buildSubtitleConfigOptionRegistry(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ResolvedConfig } from '../../types';
|
import { ResolvedConfig } from '../../types/config';
|
||||||
import { RuntimeOptionRegistryEntry } from './shared';
|
import { RuntimeOptionRegistryEntry } from './shared';
|
||||||
|
|
||||||
export function buildRuntimeOptionRegistry(
|
export function buildRuntimeOptionRegistry(
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {
|
import type { AnkiConnectConfig } from '../../types/anki';
|
||||||
AnkiConnectConfig,
|
import type { ResolvedConfig } from '../../types/config';
|
||||||
ResolvedConfig,
|
import type {
|
||||||
RuntimeOptionId,
|
RuntimeOptionId,
|
||||||
RuntimeOptionScope,
|
RuntimeOptionScope,
|
||||||
RuntimeOptionValue,
|
RuntimeOptionValue,
|
||||||
RuntimeOptionValueType,
|
RuntimeOptionValueType,
|
||||||
} from '../../types';
|
} from '../../types/runtime-options';
|
||||||
|
|
||||||
export type ConfigValueKind = 'boolean' | 'number' | 'string' | 'enum' | 'array' | 'object';
|
export type ConfigValueKind = 'boolean' | 'number' | 'string' | 'enum' | 'array' | 'object';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { RawConfig } from '../types';
|
import { RawConfig } from '../types/config';
|
||||||
import { parseConfigContent } from './parse';
|
import { parseConfigContent } from './parse';
|
||||||
|
|
||||||
export interface ConfigPaths {
|
export interface ConfigPaths {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ConfigValidationWarning, RawConfig, ResolvedConfig } from '../types';
|
import { ConfigValidationWarning, RawConfig, ResolvedConfig } from '../types/config';
|
||||||
import { applyAnkiConnectResolution } from './resolve/anki-connect';
|
import { applyAnkiConnectResolution } from './resolve/anki-connect';
|
||||||
import { createResolveContext } from './resolve/context';
|
import { createResolveContext } from './resolve/context';
|
||||||
import { applyCoreDomainConfig } from './resolve/core-domains';
|
import { applyCoreDomainConfig } from './resolve/core-domains';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ConfigValidationWarning, RawConfig, ResolvedConfig } from '../../types';
|
import { ConfigValidationWarning, RawConfig, ResolvedConfig } from '../../types/config';
|
||||||
import { DEFAULT_CONFIG, deepCloneConfig } from '../definitions';
|
import { DEFAULT_CONFIG, deepCloneConfig } from '../definitions';
|
||||||
import { createWarningCollector } from '../warnings';
|
import { createWarningCollector } from '../warnings';
|
||||||
import { isObject } from './shared';
|
import { isObject } from './shared';
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type {
|
|||||||
ControllerDiscreteBindingConfig,
|
ControllerDiscreteBindingConfig,
|
||||||
ResolvedControllerAxisBinding,
|
ResolvedControllerAxisBinding,
|
||||||
ResolvedControllerDiscreteBinding,
|
ResolvedControllerDiscreteBinding,
|
||||||
} from '../../types';
|
} from '../../types/runtime';
|
||||||
import { ResolveContext } from './context';
|
import { ResolveContext } from './context';
|
||||||
import { asBoolean, asNumber, asString, isObject } from './shared';
|
import { asBoolean, asNumber, asString, isObject } from './shared';
|
||||||
|
|
||||||
@@ -27,7 +27,12 @@ const CONTROLLER_BUTTON_BINDINGS = [
|
|||||||
'rightTrigger',
|
'rightTrigger',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const CONTROLLER_AXIS_BINDINGS = ['leftStickX', 'leftStickY', 'rightStickX', 'rightStickY'] as const;
|
const CONTROLLER_AXIS_BINDINGS = [
|
||||||
|
'leftStickX',
|
||||||
|
'leftStickY',
|
||||||
|
'rightStickX',
|
||||||
|
'rightStickY',
|
||||||
|
] as const;
|
||||||
|
|
||||||
const CONTROLLER_AXIS_INDEX_BY_BINDING: Record<ControllerAxisBinding, number> = {
|
const CONTROLLER_AXIS_INDEX_BY_BINDING: Record<ControllerAxisBinding, number> = {
|
||||||
leftStickX: 0,
|
leftStickX: 0,
|
||||||
@@ -98,7 +103,9 @@ function parseDiscreteBindingObject(value: unknown): ResolvedControllerDiscreteB
|
|||||||
return { kind: 'none' };
|
return { kind: 'none' };
|
||||||
}
|
}
|
||||||
if (value.kind === 'button') {
|
if (value.kind === 'button') {
|
||||||
return typeof value.buttonIndex === 'number' && Number.isInteger(value.buttonIndex) && value.buttonIndex >= 0
|
return typeof value.buttonIndex === 'number' &&
|
||||||
|
Number.isInteger(value.buttonIndex) &&
|
||||||
|
value.buttonIndex >= 0
|
||||||
? { kind: 'button', buttonIndex: value.buttonIndex }
|
? { kind: 'button', buttonIndex: value.buttonIndex }
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
@@ -121,7 +128,11 @@ function parseAxisBindingObject(
|
|||||||
return { kind: 'none' };
|
return { kind: 'none' };
|
||||||
}
|
}
|
||||||
if (!isObject(value) || value.kind !== 'axis') return null;
|
if (!isObject(value) || value.kind !== 'axis') return null;
|
||||||
if (typeof value.axisIndex !== 'number' || !Number.isInteger(value.axisIndex) || value.axisIndex < 0) {
|
if (
|
||||||
|
typeof value.axisIndex !== 'number' ||
|
||||||
|
!Number.isInteger(value.axisIndex) ||
|
||||||
|
value.axisIndex < 0
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (value.dpadFallback !== undefined && !isControllerDpadFallback(value.dpadFallback)) {
|
if (value.dpadFallback !== undefined && !isControllerDpadFallback(value.dpadFallback)) {
|
||||||
@@ -368,7 +379,9 @@ export function applyCoreDomainConfig(context: ResolveContext): void {
|
|||||||
const legacyValue = asString(bindingValue);
|
const legacyValue = asString(bindingValue);
|
||||||
if (
|
if (
|
||||||
legacyValue !== undefined &&
|
legacyValue !== undefined &&
|
||||||
CONTROLLER_BUTTON_BINDINGS.includes(legacyValue as (typeof CONTROLLER_BUTTON_BINDINGS)[number])
|
CONTROLLER_BUTTON_BINDINGS.includes(
|
||||||
|
legacyValue as (typeof CONTROLLER_BUTTON_BINDINGS)[number],
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
resolved.controller.bindings[key] = resolveLegacyDiscreteBinding(
|
resolved.controller.bindings[key] = resolveLegacyDiscreteBinding(
|
||||||
legacyValue as ControllerButtonBinding,
|
legacyValue as ControllerButtonBinding,
|
||||||
@@ -401,7 +414,9 @@ export function applyCoreDomainConfig(context: ResolveContext): void {
|
|||||||
const legacyValue = asString(bindingValue);
|
const legacyValue = asString(bindingValue);
|
||||||
if (
|
if (
|
||||||
legacyValue !== undefined &&
|
legacyValue !== undefined &&
|
||||||
CONTROLLER_AXIS_BINDINGS.includes(legacyValue as (typeof CONTROLLER_AXIS_BINDINGS)[number])
|
CONTROLLER_AXIS_BINDINGS.includes(
|
||||||
|
legacyValue as (typeof CONTROLLER_AXIS_BINDINGS)[number],
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
resolved.controller.bindings[key] = resolveLegacyAxisBinding(
|
resolved.controller.bindings[key] = resolveLegacyAxisBinding(
|
||||||
legacyValue as ControllerAxisBinding,
|
legacyValue as ControllerAxisBinding,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { ResolveContext } from './context';
|
import { ResolveContext } from './context';
|
||||||
import { ImmersionTrackingRetentionMode, ImmersionTrackingRetentionPreset } from '../../types';
|
import {
|
||||||
|
ImmersionTrackingRetentionMode,
|
||||||
|
ImmersionTrackingRetentionPreset,
|
||||||
|
} from '../../types/integrations';
|
||||||
import { asBoolean, asNumber, asString, isObject } from './shared';
|
import { asBoolean, asNumber, asString, isObject } from './shared';
|
||||||
|
|
||||||
const DEFAULT_RETENTION_MODE: ImmersionTrackingRetentionMode = 'preset';
|
const DEFAULT_RETENTION_MODE: ImmersionTrackingRetentionMode = 'preset';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ResolvedConfig } from '../../types';
|
import { ResolvedConfig } from '../../types/config';
|
||||||
import { ResolveContext } from './context';
|
import { ResolveContext } from './context';
|
||||||
import {
|
import {
|
||||||
asBoolean,
|
asBoolean,
|
||||||
@@ -467,7 +467,9 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
|||||||
);
|
);
|
||||||
if (pauseVideoOnHover !== undefined) {
|
if (pauseVideoOnHover !== undefined) {
|
||||||
resolved.subtitleSidebar.pauseVideoOnHover = pauseVideoOnHover;
|
resolved.subtitleSidebar.pauseVideoOnHover = pauseVideoOnHover;
|
||||||
} else if ((src.subtitleSidebar as { pauseVideoOnHover?: unknown }).pauseVideoOnHover !== undefined) {
|
} else if (
|
||||||
|
(src.subtitleSidebar as { pauseVideoOnHover?: unknown }).pauseVideoOnHover !== undefined
|
||||||
|
) {
|
||||||
resolved.subtitleSidebar.pauseVideoOnHover = fallback.pauseVideoOnHover;
|
resolved.subtitleSidebar.pauseVideoOnHover = fallback.pauseVideoOnHover;
|
||||||
warn(
|
warn(
|
||||||
'subtitleSidebar.pauseVideoOnHover',
|
'subtitleSidebar.pauseVideoOnHover',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { ConfigValidationWarning, RawConfig, ResolvedConfig } from '../types';
|
import { ConfigValidationWarning, RawConfig, ResolvedConfig } from '../types/config';
|
||||||
import { DEFAULT_CONFIG, deepCloneConfig, deepMergeRawConfig } from './definitions';
|
import { DEFAULT_CONFIG, deepCloneConfig, deepMergeRawConfig } from './definitions';
|
||||||
import { ConfigPaths, loadRawConfig, loadRawConfigStrict } from './load';
|
import { ConfigPaths, loadRawConfig, loadRawConfigStrict } from './load';
|
||||||
import { resolveConfig } from './resolve';
|
import { resolveConfig } from './resolve';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ResolvedConfig } from '../types';
|
import { ResolvedConfig } from '../types/config';
|
||||||
import {
|
import {
|
||||||
CONFIG_OPTION_REGISTRY,
|
CONFIG_OPTION_REGISTRY,
|
||||||
CONFIG_TEMPLATE_SECTIONS,
|
CONFIG_TEMPLATE_SECTIONS,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ConfigValidationWarning } from '../types';
|
import { ConfigValidationWarning } from '../types/config';
|
||||||
|
|
||||||
export interface WarningCollector {
|
export interface WarningCollector {
|
||||||
warnings: ConfigValidationWarning[];
|
warnings: ConfigValidationWarning[];
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { RuntimeOptionApplyResult, RuntimeOptionId, RuntimeOptionValue } from '../../types';
|
import {
|
||||||
|
RuntimeOptionApplyResult,
|
||||||
|
RuntimeOptionId,
|
||||||
|
RuntimeOptionValue,
|
||||||
|
} from '../../types/runtime-options';
|
||||||
|
|
||||||
export interface RuntimeOptionsManagerLike {
|
export interface RuntimeOptionsManagerLike {
|
||||||
setOptionValue: (id: RuntimeOptionId, value: RuntimeOptionValue) => RuntimeOptionApplyResult;
|
setOptionValue: (id: RuntimeOptionId, value: RuntimeOptionValue) => RuntimeOptionApplyResult;
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import type {
|
|||||||
AnkiConnectConfig,
|
AnkiConnectConfig,
|
||||||
KikuFieldGroupingChoice,
|
KikuFieldGroupingChoice,
|
||||||
KikuFieldGroupingRequestData,
|
KikuFieldGroupingRequestData,
|
||||||
WindowGeometry,
|
} from '../../types/anki';
|
||||||
} from '../../types';
|
|
||||||
import type { BrowserWindow } from 'electron';
|
import type { BrowserWindow } from 'electron';
|
||||||
|
import type { WindowGeometry } from '../../types/runtime';
|
||||||
import type { BaseWindowTracker } from '../../window-trackers';
|
import type { BaseWindowTracker } from '../../window-trackers';
|
||||||
|
|
||||||
type OverlayRuntimeOptions = {
|
type OverlayRuntimeOptions = {
|
||||||
|
|||||||
@@ -16,14 +16,14 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { AnkiConnectConfig } from './types/anki';
|
||||||
import {
|
import {
|
||||||
AnkiConnectConfig,
|
|
||||||
RuntimeOptionApplyResult,
|
RuntimeOptionApplyResult,
|
||||||
RuntimeOptionId,
|
RuntimeOptionId,
|
||||||
RuntimeOptionState,
|
RuntimeOptionState,
|
||||||
RuntimeOptionValue,
|
RuntimeOptionValue,
|
||||||
SubtitleStyleConfig,
|
} from './types/runtime-options';
|
||||||
} from './types';
|
import { SubtitleStyleConfig } from './types/subtitle';
|
||||||
import { RUNTIME_OPTION_REGISTRY, RuntimeOptionRegistryEntry } from './config';
|
import { RUNTIME_OPTION_REGISTRY, RuntimeOptionRegistryEntry } from './config';
|
||||||
|
|
||||||
type RuntimeOverrides = Record<string, unknown>;
|
type RuntimeOverrides = Record<string, unknown>;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { OverlayContentMeasurement, RuntimeOptionId, RuntimeOptionValue } from '../../types';
|
import type { OverlayContentMeasurement } from '../../types/runtime';
|
||||||
|
import type { RuntimeOptionId, RuntimeOptionValue } from '../../types/runtime-options';
|
||||||
|
|
||||||
export const OVERLAY_HOSTED_MODALS = [
|
export const OVERLAY_HOSTED_MODALS = [
|
||||||
'runtime-options',
|
'runtime-options',
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
|
import type { KikuFieldGroupingChoice, KikuMergePreviewRequest } from '../../types/anki';
|
||||||
import type {
|
import type {
|
||||||
ControllerConfigUpdate,
|
|
||||||
ControllerPreferenceUpdate,
|
|
||||||
JimakuDownloadQuery,
|
JimakuDownloadQuery,
|
||||||
JimakuFilesQuery,
|
JimakuFilesQuery,
|
||||||
JimakuSearchQuery,
|
JimakuSearchQuery,
|
||||||
KikuFieldGroupingChoice,
|
|
||||||
KikuMergePreviewRequest,
|
|
||||||
RuntimeOptionId,
|
|
||||||
RuntimeOptionValue,
|
|
||||||
SubtitlePosition,
|
|
||||||
SubsyncManualRunRequest,
|
|
||||||
YoutubePickerResolveRequest,
|
YoutubePickerResolveRequest,
|
||||||
} from '../../types';
|
} from '../../types/integrations';
|
||||||
|
import type {
|
||||||
|
ControllerConfigUpdate,
|
||||||
|
ControllerPreferenceUpdate,
|
||||||
|
SubsyncManualRunRequest,
|
||||||
|
} from '../../types/runtime';
|
||||||
|
import type { RuntimeOptionId, RuntimeOptionValue } from '../../types/runtime-options';
|
||||||
|
import type { SubtitlePosition } from '../../types/subtitle';
|
||||||
import { OVERLAY_HOSTED_MODALS, type OverlayHostedModal } from './contracts';
|
import { OVERLAY_HOSTED_MODALS, type OverlayHostedModal } from './contracts';
|
||||||
|
|
||||||
const RUNTIME_OPTION_IDS: RuntimeOptionId[] = [
|
const RUNTIME_OPTION_IDS: RuntimeOptionId[] = [
|
||||||
@@ -255,7 +255,9 @@ export function parseJimakuDownloadQuery(value: unknown): JimakuDownloadQuery |
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseYoutubePickerResolveRequest(value: unknown): YoutubePickerResolveRequest | null {
|
export function parseYoutubePickerResolveRequest(
|
||||||
|
value: unknown,
|
||||||
|
): YoutubePickerResolveRequest | null {
|
||||||
if (!isObject(value)) return null;
|
if (!isObject(value)) return null;
|
||||||
if (typeof value.sessionId !== 'string' || !value.sessionId.trim()) return null;
|
if (typeof value.sessionId !== 'string' || !value.sessionId.trim()) return null;
|
||||||
if (value.action !== 'use-selected' && value.action !== 'continue-without-subtitles') return null;
|
if (value.action !== 'use-selected' && value.action !== 'continue-without-subtitles') return null;
|
||||||
@@ -270,7 +272,11 @@ export function parseYoutubePickerResolveRequest(value: unknown): YoutubePickerR
|
|||||||
secondaryTrackId: null,
|
secondaryTrackId: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (value.primaryTrackId !== null && value.primaryTrackId !== undefined && typeof value.primaryTrackId !== 'string') {
|
if (
|
||||||
|
value.primaryTrackId !== null &&
|
||||||
|
value.primaryTrackId !== undefined &&
|
||||||
|
typeof value.primaryTrackId !== 'string'
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
|||||||
39
src/types-domain-entrypoints.type-test.ts
Normal file
39
src/types-domain-entrypoints.type-test.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { PartOfSpeech as LegacyPartOfSpeech } from './types';
|
||||||
|
import type { AnkiConnectConfig } from './types/anki';
|
||||||
|
import type { ConfigValidationWarning, RawConfig, ResolvedConfig } from './types/config';
|
||||||
|
import type { JimakuConfig, YoutubePickerOpenPayload } from './types/integrations';
|
||||||
|
import type { ElectronAPI, MpvClient, OverlayContentMeasurement } from './types/runtime';
|
||||||
|
import type { RuntimeOptionId, RuntimeOptionValue } from './types/runtime-options';
|
||||||
|
import { PartOfSpeech, type SubtitleSidebarSnapshot } from './types/subtitle';
|
||||||
|
|
||||||
|
type Assert<T extends true> = T;
|
||||||
|
type IsAssignable<From, To> = [From] extends [To] ? true : false;
|
||||||
|
|
||||||
|
const runtimeEntryPointMatchesLegacyBarrel = LegacyPartOfSpeech === PartOfSpeech;
|
||||||
|
void runtimeEntryPointMatchesLegacyBarrel;
|
||||||
|
|
||||||
|
type SubtitleEnumStillCompatible = Assert<
|
||||||
|
IsAssignable<typeof PartOfSpeech, typeof LegacyPartOfSpeech>
|
||||||
|
>;
|
||||||
|
|
||||||
|
type ConfigEntryPointContracts = [
|
||||||
|
RawConfig,
|
||||||
|
ResolvedConfig,
|
||||||
|
ConfigValidationWarning,
|
||||||
|
RuntimeOptionId,
|
||||||
|
RuntimeOptionValue,
|
||||||
|
];
|
||||||
|
|
||||||
|
type IntegrationEntryPointContracts = [AnkiConnectConfig, JimakuConfig, YoutubePickerOpenPayload];
|
||||||
|
|
||||||
|
type RuntimeEntryPointContracts = [
|
||||||
|
OverlayContentMeasurement,
|
||||||
|
ElectronAPI,
|
||||||
|
MpvClient,
|
||||||
|
SubtitleSidebarSnapshot,
|
||||||
|
];
|
||||||
|
|
||||||
|
void (null as unknown as SubtitleEnumStillCompatible);
|
||||||
|
void (null as unknown as ConfigEntryPointContracts);
|
||||||
|
void (null as unknown as IntegrationEntryPointContracts);
|
||||||
|
void (null as unknown as RuntimeEntryPointContracts);
|
||||||
1261
src/types.ts
1261
src/types.ts
File diff suppressed because it is too large
Load Diff
113
src/types/anki.ts
Normal file
113
src/types/anki.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import type { AiFeatureConfig } from './integrations';
|
||||||
|
import type { NPlusOneMatchMode } from './subtitle';
|
||||||
|
|
||||||
|
export interface NotificationOptions {
|
||||||
|
body?: string;
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KikuDuplicateCardInfo {
|
||||||
|
noteId: number;
|
||||||
|
expression: string;
|
||||||
|
sentencePreview: string;
|
||||||
|
hasAudio: boolean;
|
||||||
|
hasImage: boolean;
|
||||||
|
isOriginal: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KikuFieldGroupingRequestData {
|
||||||
|
original: KikuDuplicateCardInfo;
|
||||||
|
duplicate: KikuDuplicateCardInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KikuFieldGroupingChoice {
|
||||||
|
keepNoteId: number;
|
||||||
|
deleteNoteId: number;
|
||||||
|
deleteDuplicate: boolean;
|
||||||
|
cancelled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KikuMergePreviewRequest {
|
||||||
|
keepNoteId: number;
|
||||||
|
deleteNoteId: number;
|
||||||
|
deleteDuplicate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KikuMergePreviewResponse {
|
||||||
|
ok: boolean;
|
||||||
|
compact?: Record<string, unknown>;
|
||||||
|
full?: Record<string, unknown>;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnkiConnectConfig {
|
||||||
|
enabled?: boolean;
|
||||||
|
url?: string;
|
||||||
|
pollingRate?: number;
|
||||||
|
proxy?: {
|
||||||
|
enabled?: boolean;
|
||||||
|
host?: string;
|
||||||
|
port?: number;
|
||||||
|
upstreamUrl?: string;
|
||||||
|
};
|
||||||
|
tags?: string[];
|
||||||
|
fields?: {
|
||||||
|
word?: string;
|
||||||
|
audio?: string;
|
||||||
|
image?: string;
|
||||||
|
sentence?: string;
|
||||||
|
miscInfo?: string;
|
||||||
|
translation?: string;
|
||||||
|
};
|
||||||
|
ai?: boolean | AiFeatureConfig;
|
||||||
|
media?: {
|
||||||
|
generateAudio?: boolean;
|
||||||
|
generateImage?: boolean;
|
||||||
|
imageType?: 'static' | 'avif';
|
||||||
|
imageFormat?: 'jpg' | 'png' | 'webp';
|
||||||
|
imageQuality?: number;
|
||||||
|
imageMaxWidth?: number;
|
||||||
|
imageMaxHeight?: number;
|
||||||
|
animatedFps?: number;
|
||||||
|
animatedMaxWidth?: number;
|
||||||
|
animatedMaxHeight?: number;
|
||||||
|
animatedCrf?: number;
|
||||||
|
syncAnimatedImageToWordAudio?: boolean;
|
||||||
|
audioPadding?: number;
|
||||||
|
fallbackDuration?: number;
|
||||||
|
maxMediaDuration?: number;
|
||||||
|
};
|
||||||
|
knownWords?: {
|
||||||
|
highlightEnabled?: boolean;
|
||||||
|
refreshMinutes?: number;
|
||||||
|
addMinedWordsImmediately?: boolean;
|
||||||
|
matchMode?: NPlusOneMatchMode;
|
||||||
|
decks?: Record<string, string[]>;
|
||||||
|
color?: string;
|
||||||
|
};
|
||||||
|
nPlusOne?: {
|
||||||
|
nPlusOne?: string;
|
||||||
|
minSentenceWords?: number;
|
||||||
|
};
|
||||||
|
behavior?: {
|
||||||
|
overwriteAudio?: boolean;
|
||||||
|
overwriteImage?: boolean;
|
||||||
|
mediaInsertMode?: 'append' | 'prepend';
|
||||||
|
highlightWord?: boolean;
|
||||||
|
notificationType?: 'osd' | 'system' | 'both' | 'none';
|
||||||
|
autoUpdateNewCards?: boolean;
|
||||||
|
};
|
||||||
|
metadata?: {
|
||||||
|
pattern?: string;
|
||||||
|
};
|
||||||
|
deck?: string;
|
||||||
|
isLapis?: {
|
||||||
|
enabled?: boolean;
|
||||||
|
sentenceCardModel?: string;
|
||||||
|
};
|
||||||
|
isKiku?: {
|
||||||
|
enabled?: boolean;
|
||||||
|
fieldGrouping?: 'auto' | 'manual' | 'disabled';
|
||||||
|
deleteDuplicateInAuto?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
340
src/types/config.ts
Normal file
340
src/types/config.ts
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
import type { AnkiConnectConfig } from './anki';
|
||||||
|
import type {
|
||||||
|
AiConfig,
|
||||||
|
AiFeatureConfig,
|
||||||
|
AnilistCharacterDictionaryCollapsibleSectionsConfig,
|
||||||
|
AnilistCharacterDictionaryEvictionPolicy,
|
||||||
|
AnilistCharacterDictionaryProfileScope,
|
||||||
|
AnilistConfig,
|
||||||
|
DiscordPresenceConfig,
|
||||||
|
ImmersionTrackingConfig,
|
||||||
|
ImmersionTrackingRetentionMode,
|
||||||
|
ImmersionTrackingRetentionPreset,
|
||||||
|
JellyfinConfig,
|
||||||
|
JimakuConfig,
|
||||||
|
JimakuLanguagePreference,
|
||||||
|
StatsConfig,
|
||||||
|
YomitanConfig,
|
||||||
|
YoutubeConfig,
|
||||||
|
YoutubeSubgenConfig,
|
||||||
|
} from './integrations';
|
||||||
|
import type {
|
||||||
|
ControllerButtonIndicesConfig,
|
||||||
|
ControllerConfig,
|
||||||
|
ControllerTriggerInputMode,
|
||||||
|
Keybinding,
|
||||||
|
ResolvedControllerBindingsConfig,
|
||||||
|
} from './runtime';
|
||||||
|
import type {
|
||||||
|
FrequencyDictionaryMatchMode,
|
||||||
|
FrequencyDictionaryMode,
|
||||||
|
NPlusOneMatchMode,
|
||||||
|
SecondarySubConfig,
|
||||||
|
SubtitlePosition,
|
||||||
|
SubtitleSidebarConfig,
|
||||||
|
SubtitleStyleConfig,
|
||||||
|
} from './subtitle';
|
||||||
|
|
||||||
|
export interface WebSocketConfig {
|
||||||
|
enabled?: boolean | 'auto';
|
||||||
|
port?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnnotationWebSocketConfig {
|
||||||
|
enabled?: boolean;
|
||||||
|
port?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TexthookerConfig {
|
||||||
|
launchAtStartup?: boolean;
|
||||||
|
openBrowser?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SubsyncMode = 'auto' | 'manual';
|
||||||
|
|
||||||
|
export interface SubsyncConfig {
|
||||||
|
defaultMode?: SubsyncMode;
|
||||||
|
alass_path?: string;
|
||||||
|
ffsubsync_path?: string;
|
||||||
|
ffmpeg_path?: string;
|
||||||
|
replace?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StartupWarmupsConfig {
|
||||||
|
lowPowerMode?: boolean;
|
||||||
|
mecab?: boolean;
|
||||||
|
yomitanExtension?: boolean;
|
||||||
|
subtitleDictionaries?: boolean;
|
||||||
|
jellyfinRemoteSession?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShortcutsConfig {
|
||||||
|
toggleVisibleOverlayGlobal?: string | null;
|
||||||
|
copySubtitle?: string | null;
|
||||||
|
copySubtitleMultiple?: string | null;
|
||||||
|
updateLastCardFromClipboard?: string | null;
|
||||||
|
triggerFieldGrouping?: string | null;
|
||||||
|
triggerSubsync?: string | null;
|
||||||
|
mineSentence?: string | null;
|
||||||
|
mineSentenceMultiple?: string | null;
|
||||||
|
multiCopyTimeoutMs?: number;
|
||||||
|
toggleSecondarySub?: string | null;
|
||||||
|
markAudioCard?: string | null;
|
||||||
|
openRuntimeOptions?: string | null;
|
||||||
|
openJimaku?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
subtitlePosition?: SubtitlePosition;
|
||||||
|
keybindings?: Keybinding[];
|
||||||
|
websocket?: WebSocketConfig;
|
||||||
|
annotationWebsocket?: AnnotationWebSocketConfig;
|
||||||
|
texthooker?: TexthookerConfig;
|
||||||
|
controller?: ControllerConfig;
|
||||||
|
ankiConnect?: AnkiConnectConfig;
|
||||||
|
shortcuts?: ShortcutsConfig;
|
||||||
|
secondarySub?: SecondarySubConfig;
|
||||||
|
subsync?: SubsyncConfig;
|
||||||
|
startupWarmups?: StartupWarmupsConfig;
|
||||||
|
subtitleStyle?: SubtitleStyleConfig;
|
||||||
|
subtitleSidebar?: SubtitleSidebarConfig;
|
||||||
|
auto_start_overlay?: boolean;
|
||||||
|
jimaku?: JimakuConfig;
|
||||||
|
anilist?: AnilistConfig;
|
||||||
|
yomitan?: YomitanConfig;
|
||||||
|
jellyfin?: JellyfinConfig;
|
||||||
|
discordPresence?: DiscordPresenceConfig;
|
||||||
|
ai?: AiConfig;
|
||||||
|
youtube?: YoutubeConfig;
|
||||||
|
youtubeSubgen?: YoutubeSubgenConfig;
|
||||||
|
immersionTracking?: ImmersionTrackingConfig;
|
||||||
|
stats?: StatsConfig;
|
||||||
|
logging?: {
|
||||||
|
level?: 'debug' | 'info' | 'warn' | 'error';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RawConfig = Config;
|
||||||
|
|
||||||
|
export interface ResolvedConfig {
|
||||||
|
subtitlePosition: SubtitlePosition;
|
||||||
|
keybindings: Keybinding[];
|
||||||
|
websocket: Required<WebSocketConfig>;
|
||||||
|
annotationWebsocket: Required<AnnotationWebSocketConfig>;
|
||||||
|
texthooker: Required<TexthookerConfig>;
|
||||||
|
controller: {
|
||||||
|
enabled: boolean;
|
||||||
|
preferredGamepadId: string;
|
||||||
|
preferredGamepadLabel: string;
|
||||||
|
smoothScroll: boolean;
|
||||||
|
scrollPixelsPerSecond: number;
|
||||||
|
horizontalJumpPixels: number;
|
||||||
|
stickDeadzone: number;
|
||||||
|
triggerInputMode: ControllerTriggerInputMode;
|
||||||
|
triggerDeadzone: number;
|
||||||
|
repeatDelayMs: number;
|
||||||
|
repeatIntervalMs: number;
|
||||||
|
buttonIndices: Required<ControllerButtonIndicesConfig>;
|
||||||
|
bindings: Required<ResolvedControllerBindingsConfig>;
|
||||||
|
};
|
||||||
|
ankiConnect: AnkiConnectConfig & {
|
||||||
|
enabled: boolean;
|
||||||
|
url: string;
|
||||||
|
pollingRate: number;
|
||||||
|
proxy: {
|
||||||
|
enabled: boolean;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
upstreamUrl: string;
|
||||||
|
};
|
||||||
|
tags: string[];
|
||||||
|
fields: {
|
||||||
|
word: string;
|
||||||
|
audio: string;
|
||||||
|
image: string;
|
||||||
|
sentence: string;
|
||||||
|
miscInfo: string;
|
||||||
|
translation: string;
|
||||||
|
};
|
||||||
|
ai: AiFeatureConfig & {
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
media: {
|
||||||
|
generateAudio: boolean;
|
||||||
|
generateImage: boolean;
|
||||||
|
imageType: 'static' | 'avif';
|
||||||
|
imageFormat: 'jpg' | 'png' | 'webp';
|
||||||
|
imageQuality: number;
|
||||||
|
imageMaxWidth?: number;
|
||||||
|
imageMaxHeight?: number;
|
||||||
|
animatedFps: number;
|
||||||
|
animatedMaxWidth: number;
|
||||||
|
animatedMaxHeight?: number;
|
||||||
|
animatedCrf: number;
|
||||||
|
syncAnimatedImageToWordAudio: boolean;
|
||||||
|
audioPadding: number;
|
||||||
|
fallbackDuration: number;
|
||||||
|
maxMediaDuration: number;
|
||||||
|
};
|
||||||
|
knownWords: {
|
||||||
|
highlightEnabled: boolean;
|
||||||
|
refreshMinutes: number;
|
||||||
|
addMinedWordsImmediately: boolean;
|
||||||
|
matchMode: NPlusOneMatchMode;
|
||||||
|
decks: Record<string, string[]>;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
nPlusOne: {
|
||||||
|
nPlusOne: string;
|
||||||
|
minSentenceWords: number;
|
||||||
|
};
|
||||||
|
behavior: {
|
||||||
|
overwriteAudio: boolean;
|
||||||
|
overwriteImage: boolean;
|
||||||
|
mediaInsertMode: 'append' | 'prepend';
|
||||||
|
highlightWord: boolean;
|
||||||
|
notificationType: 'osd' | 'system' | 'both' | 'none';
|
||||||
|
autoUpdateNewCards: boolean;
|
||||||
|
};
|
||||||
|
metadata: {
|
||||||
|
pattern: string;
|
||||||
|
};
|
||||||
|
isLapis: {
|
||||||
|
enabled: boolean;
|
||||||
|
sentenceCardModel: string;
|
||||||
|
};
|
||||||
|
isKiku: {
|
||||||
|
enabled: boolean;
|
||||||
|
fieldGrouping: 'auto' | 'manual' | 'disabled';
|
||||||
|
deleteDuplicateInAuto: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
shortcuts: Required<ShortcutsConfig>;
|
||||||
|
secondarySub: Required<SecondarySubConfig>;
|
||||||
|
subsync: Required<SubsyncConfig>;
|
||||||
|
startupWarmups: {
|
||||||
|
lowPowerMode: boolean;
|
||||||
|
mecab: boolean;
|
||||||
|
yomitanExtension: boolean;
|
||||||
|
subtitleDictionaries: boolean;
|
||||||
|
jellyfinRemoteSession: boolean;
|
||||||
|
};
|
||||||
|
subtitleStyle: Required<Omit<SubtitleStyleConfig, 'secondary' | 'frequencyDictionary'>> & {
|
||||||
|
secondary: Required<NonNullable<SubtitleStyleConfig['secondary']>>;
|
||||||
|
frequencyDictionary: {
|
||||||
|
enabled: boolean;
|
||||||
|
sourcePath: string;
|
||||||
|
topX: number;
|
||||||
|
mode: FrequencyDictionaryMode;
|
||||||
|
matchMode: FrequencyDictionaryMatchMode;
|
||||||
|
singleColor: string;
|
||||||
|
bandedColors: [string, string, string, string, string];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
subtitleSidebar: Required<SubtitleSidebarConfig>;
|
||||||
|
auto_start_overlay: boolean;
|
||||||
|
jimaku: JimakuConfig & {
|
||||||
|
apiBaseUrl: string;
|
||||||
|
languagePreference: JimakuLanguagePreference;
|
||||||
|
maxEntryResults: number;
|
||||||
|
};
|
||||||
|
anilist: {
|
||||||
|
enabled: boolean;
|
||||||
|
accessToken: string;
|
||||||
|
characterDictionary: {
|
||||||
|
enabled: boolean;
|
||||||
|
refreshTtlHours: number;
|
||||||
|
maxLoaded: number;
|
||||||
|
evictionPolicy: AnilistCharacterDictionaryEvictionPolicy;
|
||||||
|
profileScope: AnilistCharacterDictionaryProfileScope;
|
||||||
|
collapsibleSections: Required<AnilistCharacterDictionaryCollapsibleSectionsConfig>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
yomitan: {
|
||||||
|
externalProfilePath: string;
|
||||||
|
};
|
||||||
|
jellyfin: {
|
||||||
|
enabled: boolean;
|
||||||
|
serverUrl: string;
|
||||||
|
username: string;
|
||||||
|
deviceId: string;
|
||||||
|
clientName: string;
|
||||||
|
clientVersion: string;
|
||||||
|
defaultLibraryId: string;
|
||||||
|
remoteControlEnabled: boolean;
|
||||||
|
remoteControlAutoConnect: boolean;
|
||||||
|
autoAnnounce: boolean;
|
||||||
|
remoteControlDeviceName: string;
|
||||||
|
pullPictures: boolean;
|
||||||
|
iconCacheDir: string;
|
||||||
|
directPlayPreferred: boolean;
|
||||||
|
directPlayContainers: string[];
|
||||||
|
transcodeVideoCodec: string;
|
||||||
|
};
|
||||||
|
discordPresence: {
|
||||||
|
enabled: boolean;
|
||||||
|
updateIntervalMs: number;
|
||||||
|
debounceMs: number;
|
||||||
|
};
|
||||||
|
ai: AiConfig & {
|
||||||
|
enabled: boolean;
|
||||||
|
apiKey: string;
|
||||||
|
apiKeyCommand: string;
|
||||||
|
baseUrl: string;
|
||||||
|
model: string;
|
||||||
|
systemPrompt: string;
|
||||||
|
requestTimeoutMs: number;
|
||||||
|
};
|
||||||
|
youtube: YoutubeConfig & {
|
||||||
|
primarySubLanguages: string[];
|
||||||
|
};
|
||||||
|
youtubeSubgen: YoutubeSubgenConfig & {
|
||||||
|
whisperBin: string;
|
||||||
|
whisperModel: string;
|
||||||
|
whisperVadModel: string;
|
||||||
|
whisperThreads: number;
|
||||||
|
fixWithAi: boolean;
|
||||||
|
ai: AiFeatureConfig;
|
||||||
|
};
|
||||||
|
immersionTracking: {
|
||||||
|
enabled: boolean;
|
||||||
|
dbPath?: string;
|
||||||
|
batchSize: number;
|
||||||
|
flushIntervalMs: number;
|
||||||
|
queueCap: number;
|
||||||
|
payloadCapBytes: number;
|
||||||
|
maintenanceIntervalMs: number;
|
||||||
|
retentionMode: ImmersionTrackingRetentionMode;
|
||||||
|
retentionPreset: ImmersionTrackingRetentionPreset;
|
||||||
|
retention: {
|
||||||
|
eventsDays: number;
|
||||||
|
telemetryDays: number;
|
||||||
|
sessionsDays: number;
|
||||||
|
dailyRollupsDays: number;
|
||||||
|
monthlyRollupsDays: number;
|
||||||
|
vacuumIntervalDays: number;
|
||||||
|
};
|
||||||
|
lifetimeSummaries: {
|
||||||
|
global: boolean;
|
||||||
|
anime: boolean;
|
||||||
|
media: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
stats: {
|
||||||
|
toggleKey: string;
|
||||||
|
markWatchedKey: string;
|
||||||
|
serverPort: number;
|
||||||
|
autoStartServer: boolean;
|
||||||
|
autoOpenBrowser: boolean;
|
||||||
|
};
|
||||||
|
logging: {
|
||||||
|
level: 'debug' | 'info' | 'warn' | 'error';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigValidationWarning {
|
||||||
|
path: string;
|
||||||
|
value: unknown;
|
||||||
|
fallback: unknown;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
235
src/types/integrations.ts
Normal file
235
src/types/integrations.ts
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import type { YoutubeTrackKind } from '../core/services/youtube/kinds';
|
||||||
|
|
||||||
|
export type JimakuLanguagePreference = 'ja' | 'en' | 'none';
|
||||||
|
export type { YoutubeTrackKind };
|
||||||
|
|
||||||
|
export interface YoutubeTrackOption {
|
||||||
|
id: string;
|
||||||
|
language: string;
|
||||||
|
sourceLanguage: string;
|
||||||
|
kind: YoutubeTrackKind;
|
||||||
|
label: string;
|
||||||
|
title?: string;
|
||||||
|
downloadUrl?: string;
|
||||||
|
fileExtension?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YoutubePickerOpenPayload {
|
||||||
|
sessionId: string;
|
||||||
|
url: string;
|
||||||
|
tracks: YoutubeTrackOption[];
|
||||||
|
defaultPrimaryTrackId: string | null;
|
||||||
|
defaultSecondaryTrackId: string | null;
|
||||||
|
hasTracks: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type YoutubePickerResolveRequest =
|
||||||
|
| {
|
||||||
|
sessionId: string;
|
||||||
|
action: 'continue-without-subtitles';
|
||||||
|
primaryTrackId: null;
|
||||||
|
secondaryTrackId: null;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
sessionId: string;
|
||||||
|
action: 'use-selected';
|
||||||
|
primaryTrackId: string | null;
|
||||||
|
secondaryTrackId: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface YoutubePickerResolveResult {
|
||||||
|
ok: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JimakuConfig {
|
||||||
|
apiKey?: string;
|
||||||
|
apiKeyCommand?: string;
|
||||||
|
apiBaseUrl?: string;
|
||||||
|
languagePreference?: JimakuLanguagePreference;
|
||||||
|
maxEntryResults?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnilistCharacterDictionaryEvictionPolicy = 'disable' | 'delete';
|
||||||
|
export type AnilistCharacterDictionaryProfileScope = 'all' | 'active';
|
||||||
|
export type AnilistCharacterDictionaryCollapsibleSectionKey =
|
||||||
|
| 'description'
|
||||||
|
| 'characterInformation'
|
||||||
|
| 'voicedBy';
|
||||||
|
|
||||||
|
export interface AnilistCharacterDictionaryCollapsibleSectionsConfig {
|
||||||
|
description?: boolean;
|
||||||
|
characterInformation?: boolean;
|
||||||
|
voicedBy?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnilistCharacterDictionaryConfig {
|
||||||
|
enabled?: boolean;
|
||||||
|
refreshTtlHours?: number;
|
||||||
|
maxLoaded?: number;
|
||||||
|
evictionPolicy?: AnilistCharacterDictionaryEvictionPolicy;
|
||||||
|
profileScope?: AnilistCharacterDictionaryProfileScope;
|
||||||
|
collapsibleSections?: AnilistCharacterDictionaryCollapsibleSectionsConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnilistConfig {
|
||||||
|
enabled?: boolean;
|
||||||
|
accessToken?: string;
|
||||||
|
characterDictionary?: AnilistCharacterDictionaryConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YomitanConfig {
|
||||||
|
externalProfilePath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JellyfinConfig {
|
||||||
|
enabled?: boolean;
|
||||||
|
serverUrl?: string;
|
||||||
|
username?: string;
|
||||||
|
deviceId?: string;
|
||||||
|
clientName?: string;
|
||||||
|
clientVersion?: string;
|
||||||
|
defaultLibraryId?: string;
|
||||||
|
remoteControlEnabled?: boolean;
|
||||||
|
remoteControlAutoConnect?: boolean;
|
||||||
|
autoAnnounce?: boolean;
|
||||||
|
remoteControlDeviceName?: string;
|
||||||
|
pullPictures?: boolean;
|
||||||
|
iconCacheDir?: string;
|
||||||
|
directPlayPreferred?: boolean;
|
||||||
|
directPlayContainers?: string[];
|
||||||
|
transcodeVideoCodec?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiscordPresenceConfig {
|
||||||
|
enabled?: boolean;
|
||||||
|
updateIntervalMs?: number;
|
||||||
|
debounceMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiFeatureConfig {
|
||||||
|
enabled?: boolean;
|
||||||
|
model?: string;
|
||||||
|
systemPrompt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiConfig {
|
||||||
|
enabled?: boolean;
|
||||||
|
apiKey?: string;
|
||||||
|
apiKeyCommand?: string;
|
||||||
|
baseUrl?: string;
|
||||||
|
model?: string;
|
||||||
|
systemPrompt?: string;
|
||||||
|
requestTimeoutMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YoutubeConfig {
|
||||||
|
primarySubLanguages?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YoutubeSubgenConfig {
|
||||||
|
whisperBin?: string;
|
||||||
|
whisperModel?: string;
|
||||||
|
whisperVadModel?: string;
|
||||||
|
whisperThreads?: number;
|
||||||
|
fixWithAi?: boolean;
|
||||||
|
ai?: AiFeatureConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatsConfig {
|
||||||
|
toggleKey?: string;
|
||||||
|
markWatchedKey?: string;
|
||||||
|
serverPort?: number;
|
||||||
|
autoStartServer?: boolean;
|
||||||
|
autoOpenBrowser?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ImmersionTrackingRetentionMode = 'preset' | 'advanced';
|
||||||
|
export type ImmersionTrackingRetentionPreset = 'minimal' | 'balanced' | 'deep-history';
|
||||||
|
|
||||||
|
export interface ImmersionTrackingConfig {
|
||||||
|
enabled?: boolean;
|
||||||
|
dbPath?: string;
|
||||||
|
batchSize?: number;
|
||||||
|
flushIntervalMs?: number;
|
||||||
|
queueCap?: number;
|
||||||
|
payloadCapBytes?: number;
|
||||||
|
maintenanceIntervalMs?: number;
|
||||||
|
retentionMode?: ImmersionTrackingRetentionMode;
|
||||||
|
retentionPreset?: ImmersionTrackingRetentionPreset;
|
||||||
|
retention?: {
|
||||||
|
eventsDays?: number;
|
||||||
|
telemetryDays?: number;
|
||||||
|
sessionsDays?: number;
|
||||||
|
dailyRollupsDays?: number;
|
||||||
|
monthlyRollupsDays?: number;
|
||||||
|
vacuumIntervalDays?: number;
|
||||||
|
};
|
||||||
|
lifetimeSummaries?: {
|
||||||
|
global?: boolean;
|
||||||
|
anime?: boolean;
|
||||||
|
media?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JimakuConfidence = 'high' | 'medium' | 'low';
|
||||||
|
|
||||||
|
export interface JimakuMediaInfo {
|
||||||
|
title: string;
|
||||||
|
season: number | null;
|
||||||
|
episode: number | null;
|
||||||
|
confidence: JimakuConfidence;
|
||||||
|
filename: string;
|
||||||
|
rawTitle: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JimakuSearchQuery {
|
||||||
|
query: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JimakuEntryFlags {
|
||||||
|
anime?: boolean;
|
||||||
|
movie?: boolean;
|
||||||
|
adult?: boolean;
|
||||||
|
external?: boolean;
|
||||||
|
unverified?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JimakuEntry {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
english_name?: string | null;
|
||||||
|
japanese_name?: string | null;
|
||||||
|
flags?: JimakuEntryFlags;
|
||||||
|
last_modified?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JimakuFilesQuery {
|
||||||
|
entryId: number;
|
||||||
|
episode?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JimakuFileEntry {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
size: number;
|
||||||
|
last_modified: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JimakuDownloadQuery {
|
||||||
|
entryId: number;
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JimakuApiError {
|
||||||
|
error: string;
|
||||||
|
code?: number;
|
||||||
|
retryAfter?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JimakuApiResponse<T> = { ok: true; data: T } | { ok: false; error: JimakuApiError };
|
||||||
|
|
||||||
|
export type JimakuDownloadResult =
|
||||||
|
| { ok: true; path: string }
|
||||||
|
| { ok: false; error: JimakuApiError };
|
||||||
31
src/types/runtime-options.ts
Normal file
31
src/types/runtime-options.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
export type RuntimeOptionId =
|
||||||
|
| 'anki.autoUpdateNewCards'
|
||||||
|
| 'subtitle.annotation.nPlusOne'
|
||||||
|
| 'subtitle.annotation.jlpt'
|
||||||
|
| 'subtitle.annotation.frequency'
|
||||||
|
| 'anki.kikuFieldGrouping'
|
||||||
|
| 'anki.nPlusOneMatchMode';
|
||||||
|
|
||||||
|
export type RuntimeOptionScope = 'ankiConnect' | 'subtitle';
|
||||||
|
|
||||||
|
export type RuntimeOptionValueType = 'boolean' | 'enum';
|
||||||
|
|
||||||
|
export type RuntimeOptionValue = boolean | string;
|
||||||
|
|
||||||
|
export interface RuntimeOptionState {
|
||||||
|
id: RuntimeOptionId;
|
||||||
|
label: string;
|
||||||
|
scope: RuntimeOptionScope;
|
||||||
|
valueType: RuntimeOptionValueType;
|
||||||
|
value: RuntimeOptionValue;
|
||||||
|
allowedValues: RuntimeOptionValue[];
|
||||||
|
requiresRestart: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuntimeOptionApplyResult {
|
||||||
|
ok: boolean;
|
||||||
|
option?: RuntimeOptionState;
|
||||||
|
osdMessage?: string;
|
||||||
|
requiresRestart?: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
394
src/types/runtime.ts
Normal file
394
src/types/runtime.ts
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
import type {
|
||||||
|
KikuFieldGroupingChoice,
|
||||||
|
KikuFieldGroupingRequestData,
|
||||||
|
KikuMergePreviewRequest,
|
||||||
|
KikuMergePreviewResponse,
|
||||||
|
} from './anki';
|
||||||
|
import type { ResolvedConfig, ShortcutsConfig } from './config';
|
||||||
|
import type {
|
||||||
|
JimakuApiResponse,
|
||||||
|
JimakuDownloadQuery,
|
||||||
|
JimakuDownloadResult,
|
||||||
|
JimakuEntry,
|
||||||
|
JimakuFileEntry,
|
||||||
|
JimakuFilesQuery,
|
||||||
|
JimakuMediaInfo,
|
||||||
|
JimakuSearchQuery,
|
||||||
|
YoutubePickerOpenPayload,
|
||||||
|
YoutubePickerResolveRequest,
|
||||||
|
YoutubePickerResolveResult,
|
||||||
|
} from './integrations';
|
||||||
|
import type {
|
||||||
|
SecondarySubMode,
|
||||||
|
SubtitleData,
|
||||||
|
SubtitlePosition,
|
||||||
|
SubtitleSidebarConfig,
|
||||||
|
SubtitleSidebarSnapshot,
|
||||||
|
SubtitleStyleConfig,
|
||||||
|
} from './subtitle';
|
||||||
|
import type {
|
||||||
|
RuntimeOptionApplyResult,
|
||||||
|
RuntimeOptionId,
|
||||||
|
RuntimeOptionState,
|
||||||
|
RuntimeOptionValue,
|
||||||
|
} from './runtime-options';
|
||||||
|
|
||||||
|
export interface WindowGeometry {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Keybinding {
|
||||||
|
key: string;
|
||||||
|
command: (string | number)[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MpvClient {
|
||||||
|
currentSubText: string;
|
||||||
|
currentVideoPath: string;
|
||||||
|
currentMediaTitle?: string | null;
|
||||||
|
currentTimePos: number;
|
||||||
|
currentSubStart: number;
|
||||||
|
currentSubEnd: number;
|
||||||
|
currentAudioStreamIndex: number | null;
|
||||||
|
requestProperty?: (name: string) => Promise<unknown>;
|
||||||
|
send(command: { command: unknown[]; request_id?: number }): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubsyncSourceTrack {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubsyncManualPayload {
|
||||||
|
sourceTracks: SubsyncSourceTrack[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubsyncManualRunRequest {
|
||||||
|
engine: 'alass' | 'ffsubsync';
|
||||||
|
sourceTrackId?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubsyncResult {
|
||||||
|
ok: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ControllerButtonBinding =
|
||||||
|
| 'none'
|
||||||
|
| 'select'
|
||||||
|
| 'buttonSouth'
|
||||||
|
| 'buttonEast'
|
||||||
|
| 'buttonNorth'
|
||||||
|
| 'buttonWest'
|
||||||
|
| 'leftShoulder'
|
||||||
|
| 'rightShoulder'
|
||||||
|
| 'leftStickPress'
|
||||||
|
| 'rightStickPress'
|
||||||
|
| 'leftTrigger'
|
||||||
|
| 'rightTrigger';
|
||||||
|
|
||||||
|
export type ControllerAxisBinding = 'leftStickX' | 'leftStickY' | 'rightStickX' | 'rightStickY';
|
||||||
|
export type ControllerTriggerInputMode = 'auto' | 'digital' | 'analog';
|
||||||
|
export type ControllerAxisDirection = 'negative' | 'positive';
|
||||||
|
export type ControllerDpadFallback = 'none' | 'horizontal' | 'vertical';
|
||||||
|
|
||||||
|
export interface ControllerNoneBinding {
|
||||||
|
kind: 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ControllerButtonInputBinding {
|
||||||
|
kind: 'button';
|
||||||
|
buttonIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ControllerAxisDirectionInputBinding {
|
||||||
|
kind: 'axis';
|
||||||
|
axisIndex: number;
|
||||||
|
direction: ControllerAxisDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ControllerAxisInputBinding {
|
||||||
|
kind: 'axis';
|
||||||
|
axisIndex: number;
|
||||||
|
dpadFallback?: ControllerDpadFallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ControllerDiscreteBindingConfig =
|
||||||
|
| ControllerButtonBinding
|
||||||
|
| ControllerNoneBinding
|
||||||
|
| ControllerButtonInputBinding
|
||||||
|
| ControllerAxisDirectionInputBinding;
|
||||||
|
|
||||||
|
export type ResolvedControllerDiscreteBinding =
|
||||||
|
| ControllerNoneBinding
|
||||||
|
| ControllerButtonInputBinding
|
||||||
|
| ControllerAxisDirectionInputBinding;
|
||||||
|
|
||||||
|
export type ControllerAxisBindingConfig =
|
||||||
|
| ControllerAxisBinding
|
||||||
|
| ControllerNoneBinding
|
||||||
|
| ControllerAxisInputBinding;
|
||||||
|
|
||||||
|
export type ResolvedControllerAxisBinding =
|
||||||
|
| ControllerNoneBinding
|
||||||
|
| {
|
||||||
|
kind: 'axis';
|
||||||
|
axisIndex: number;
|
||||||
|
dpadFallback: ControllerDpadFallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ControllerBindingsConfig {
|
||||||
|
toggleLookup?: ControllerDiscreteBindingConfig;
|
||||||
|
closeLookup?: ControllerDiscreteBindingConfig;
|
||||||
|
toggleKeyboardOnlyMode?: ControllerDiscreteBindingConfig;
|
||||||
|
mineCard?: ControllerDiscreteBindingConfig;
|
||||||
|
quitMpv?: ControllerDiscreteBindingConfig;
|
||||||
|
previousAudio?: ControllerDiscreteBindingConfig;
|
||||||
|
nextAudio?: ControllerDiscreteBindingConfig;
|
||||||
|
playCurrentAudio?: ControllerDiscreteBindingConfig;
|
||||||
|
toggleMpvPause?: ControllerDiscreteBindingConfig;
|
||||||
|
leftStickHorizontal?: ControllerAxisBindingConfig;
|
||||||
|
leftStickVertical?: ControllerAxisBindingConfig;
|
||||||
|
rightStickHorizontal?: ControllerAxisBindingConfig;
|
||||||
|
rightStickVertical?: ControllerAxisBindingConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolvedControllerBindingsConfig {
|
||||||
|
toggleLookup?: ResolvedControllerDiscreteBinding;
|
||||||
|
closeLookup?: ResolvedControllerDiscreteBinding;
|
||||||
|
toggleKeyboardOnlyMode?: ResolvedControllerDiscreteBinding;
|
||||||
|
mineCard?: ResolvedControllerDiscreteBinding;
|
||||||
|
quitMpv?: ResolvedControllerDiscreteBinding;
|
||||||
|
previousAudio?: ResolvedControllerDiscreteBinding;
|
||||||
|
nextAudio?: ResolvedControllerDiscreteBinding;
|
||||||
|
playCurrentAudio?: ResolvedControllerDiscreteBinding;
|
||||||
|
toggleMpvPause?: ResolvedControllerDiscreteBinding;
|
||||||
|
leftStickHorizontal?: ResolvedControllerAxisBinding;
|
||||||
|
leftStickVertical?: ResolvedControllerAxisBinding;
|
||||||
|
rightStickHorizontal?: ResolvedControllerAxisBinding;
|
||||||
|
rightStickVertical?: ResolvedControllerAxisBinding;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ControllerButtonIndicesConfig {
|
||||||
|
select?: number;
|
||||||
|
buttonSouth?: number;
|
||||||
|
buttonEast?: number;
|
||||||
|
buttonNorth?: number;
|
||||||
|
buttonWest?: number;
|
||||||
|
leftShoulder?: number;
|
||||||
|
rightShoulder?: number;
|
||||||
|
leftStickPress?: number;
|
||||||
|
rightStickPress?: number;
|
||||||
|
leftTrigger?: number;
|
||||||
|
rightTrigger?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ControllerConfig {
|
||||||
|
enabled?: boolean;
|
||||||
|
preferredGamepadId?: string;
|
||||||
|
preferredGamepadLabel?: string;
|
||||||
|
smoothScroll?: boolean;
|
||||||
|
scrollPixelsPerSecond?: number;
|
||||||
|
horizontalJumpPixels?: number;
|
||||||
|
stickDeadzone?: number;
|
||||||
|
triggerInputMode?: ControllerTriggerInputMode;
|
||||||
|
triggerDeadzone?: number;
|
||||||
|
repeatDelayMs?: number;
|
||||||
|
repeatIntervalMs?: number;
|
||||||
|
buttonIndices?: ControllerButtonIndicesConfig;
|
||||||
|
bindings?: ControllerBindingsConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ControllerPreferenceUpdate {
|
||||||
|
preferredGamepadId: string;
|
||||||
|
preferredGamepadLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ControllerConfigUpdate = ControllerConfig;
|
||||||
|
|
||||||
|
export interface ControllerDeviceInfo {
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
mapping: string;
|
||||||
|
connected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ControllerButtonSnapshot {
|
||||||
|
value: number;
|
||||||
|
pressed: boolean;
|
||||||
|
touched?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ControllerRuntimeSnapshot {
|
||||||
|
connectedGamepads: ControllerDeviceInfo[];
|
||||||
|
activeGamepadId: string | null;
|
||||||
|
rawAxes: number[];
|
||||||
|
rawButtons: ControllerButtonSnapshot[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MpvSubtitleRenderMetrics {
|
||||||
|
subPos: number;
|
||||||
|
subFontSize: number;
|
||||||
|
subScale: number;
|
||||||
|
subMarginY: number;
|
||||||
|
subMarginX: number;
|
||||||
|
subFont: string;
|
||||||
|
subSpacing: number;
|
||||||
|
subBold: boolean;
|
||||||
|
subItalic: boolean;
|
||||||
|
subBorderSize: number;
|
||||||
|
subShadowOffset: number;
|
||||||
|
subAssOverride: string;
|
||||||
|
subScaleByWindow: boolean;
|
||||||
|
subUseMargins: boolean;
|
||||||
|
osdHeight: number;
|
||||||
|
osdDimensions: {
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
ml: number;
|
||||||
|
mr: number;
|
||||||
|
mt: number;
|
||||||
|
mb: number;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OverlayLayer = 'visible';
|
||||||
|
|
||||||
|
export interface OverlayContentRect {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OverlayContentMeasurement {
|
||||||
|
layer: OverlayLayer;
|
||||||
|
measuredAtMs: number;
|
||||||
|
viewport: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
contentRect: OverlayContentRect | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MecabStatus {
|
||||||
|
available: boolean;
|
||||||
|
enabled: boolean;
|
||||||
|
path: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClipboardAppendResult {
|
||||||
|
ok: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigHotReloadPayload {
|
||||||
|
keybindings: Keybinding[];
|
||||||
|
subtitleStyle: SubtitleStyleConfig | null;
|
||||||
|
subtitleSidebar: Required<SubtitleSidebarConfig>;
|
||||||
|
secondarySubMode: SecondarySubMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResolvedControllerConfig = ResolvedConfig['controller'];
|
||||||
|
|
||||||
|
export interface ElectronAPI {
|
||||||
|
getOverlayLayer: () => 'visible' | 'modal' | null;
|
||||||
|
onSubtitle: (callback: (data: SubtitleData) => void) => void;
|
||||||
|
onVisibility: (callback: (visible: boolean) => void) => void;
|
||||||
|
onSubtitlePosition: (callback: (position: SubtitlePosition | null) => void) => void;
|
||||||
|
getOverlayVisibility: () => Promise<boolean>;
|
||||||
|
getCurrentSubtitle: () => Promise<SubtitleData>;
|
||||||
|
getCurrentSubtitleRaw: () => Promise<string>;
|
||||||
|
getCurrentSubtitleAss: () => Promise<string>;
|
||||||
|
getSubtitleSidebarSnapshot: () => Promise<SubtitleSidebarSnapshot>;
|
||||||
|
getPlaybackPaused: () => Promise<boolean | null>;
|
||||||
|
onSubtitleAss: (callback: (assText: string) => void) => void;
|
||||||
|
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => void;
|
||||||
|
openYomitanSettings: () => void;
|
||||||
|
recordYomitanLookup: () => void;
|
||||||
|
getSubtitlePosition: () => Promise<SubtitlePosition | null>;
|
||||||
|
saveSubtitlePosition: (position: SubtitlePosition) => void;
|
||||||
|
getMecabStatus: () => Promise<MecabStatus>;
|
||||||
|
setMecabEnabled: (enabled: boolean) => void;
|
||||||
|
sendMpvCommand: (command: (string | number)[]) => void;
|
||||||
|
getKeybindings: () => Promise<Keybinding[]>;
|
||||||
|
getConfiguredShortcuts: () => Promise<Required<ShortcutsConfig>>;
|
||||||
|
getStatsToggleKey: () => Promise<string>;
|
||||||
|
getMarkWatchedKey: () => Promise<string>;
|
||||||
|
markActiveVideoWatched: () => Promise<boolean>;
|
||||||
|
getControllerConfig: () => Promise<ResolvedControllerConfig>;
|
||||||
|
saveControllerConfig: (update: ControllerConfigUpdate) => Promise<void>;
|
||||||
|
saveControllerPreference: (update: ControllerPreferenceUpdate) => Promise<void>;
|
||||||
|
getJimakuMediaInfo: () => Promise<JimakuMediaInfo>;
|
||||||
|
jimakuSearchEntries: (query: JimakuSearchQuery) => Promise<JimakuApiResponse<JimakuEntry[]>>;
|
||||||
|
jimakuListFiles: (query: JimakuFilesQuery) => Promise<JimakuApiResponse<JimakuFileEntry[]>>;
|
||||||
|
jimakuDownloadFile: (query: JimakuDownloadQuery) => Promise<JimakuDownloadResult>;
|
||||||
|
quitApp: () => void;
|
||||||
|
toggleDevTools: () => void;
|
||||||
|
toggleOverlay: () => void;
|
||||||
|
toggleStatsOverlay: () => void;
|
||||||
|
getAnkiConnectStatus: () => Promise<boolean>;
|
||||||
|
setAnkiConnectEnabled: (enabled: boolean) => void;
|
||||||
|
clearAnkiConnectHistory: () => void;
|
||||||
|
onSecondarySub: (callback: (text: string) => void) => void;
|
||||||
|
onSecondarySubMode: (callback: (mode: SecondarySubMode) => void) => void;
|
||||||
|
getSecondarySubMode: () => Promise<SecondarySubMode>;
|
||||||
|
getCurrentSecondarySub: () => Promise<string>;
|
||||||
|
focusMainWindow: () => Promise<void>;
|
||||||
|
getSubtitleStyle: () => Promise<SubtitleStyleConfig | null>;
|
||||||
|
onSubsyncManualOpen: (callback: (payload: SubsyncManualPayload) => void) => void;
|
||||||
|
runSubsyncManual: (request: SubsyncManualRunRequest) => Promise<SubsyncResult>;
|
||||||
|
onKikuFieldGroupingRequest: (callback: (data: KikuFieldGroupingRequestData) => void) => void;
|
||||||
|
kikuBuildMergePreview: (request: KikuMergePreviewRequest) => Promise<KikuMergePreviewResponse>;
|
||||||
|
kikuFieldGroupingRespond: (choice: KikuFieldGroupingChoice) => void;
|
||||||
|
getRuntimeOptions: () => Promise<RuntimeOptionState[]>;
|
||||||
|
setRuntimeOptionValue: (
|
||||||
|
id: RuntimeOptionId,
|
||||||
|
value: RuntimeOptionValue,
|
||||||
|
) => Promise<RuntimeOptionApplyResult>;
|
||||||
|
cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1) => Promise<RuntimeOptionApplyResult>;
|
||||||
|
onRuntimeOptionsChanged: (callback: (options: RuntimeOptionState[]) => void) => void;
|
||||||
|
onOpenRuntimeOptions: (callback: () => void) => void;
|
||||||
|
onOpenJimaku: (callback: () => void) => void;
|
||||||
|
onOpenYoutubeTrackPicker: (callback: (payload: YoutubePickerOpenPayload) => void) => void;
|
||||||
|
onCancelYoutubeTrackPicker: (callback: () => void) => void;
|
||||||
|
onKeyboardModeToggleRequested: (callback: () => void) => void;
|
||||||
|
onLookupWindowToggleRequested: (callback: () => void) => void;
|
||||||
|
appendClipboardVideoToQueue: () => Promise<ClipboardAppendResult>;
|
||||||
|
youtubePickerResolve: (
|
||||||
|
request: YoutubePickerResolveRequest,
|
||||||
|
) => Promise<YoutubePickerResolveResult>;
|
||||||
|
notifyOverlayModalClosed: (
|
||||||
|
modal:
|
||||||
|
| 'runtime-options'
|
||||||
|
| 'subsync'
|
||||||
|
| 'jimaku'
|
||||||
|
| 'youtube-track-picker'
|
||||||
|
| 'kiku'
|
||||||
|
| 'controller-select'
|
||||||
|
| 'controller-debug'
|
||||||
|
| 'subtitle-sidebar',
|
||||||
|
) => void;
|
||||||
|
notifyOverlayModalOpened: (
|
||||||
|
modal:
|
||||||
|
| 'runtime-options'
|
||||||
|
| 'subsync'
|
||||||
|
| 'jimaku'
|
||||||
|
| 'youtube-track-picker'
|
||||||
|
| 'kiku'
|
||||||
|
| 'controller-select'
|
||||||
|
| 'controller-debug'
|
||||||
|
| 'subtitle-sidebar',
|
||||||
|
) => void;
|
||||||
|
reportOverlayContentBounds: (measurement: OverlayContentMeasurement) => void;
|
||||||
|
onConfigHotReload: (callback: (payload: ConfigHotReloadPayload) => void) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
electronAPI: ElectronAPI;
|
||||||
|
}
|
||||||
|
}
|
||||||
195
src/types/subtitle.ts
Normal file
195
src/types/subtitle.ts
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import type { SubtitleCue } from '../core/services/subtitle-cue-parser';
|
||||||
|
|
||||||
|
export enum PartOfSpeech {
|
||||||
|
noun = 'noun',
|
||||||
|
verb = 'verb',
|
||||||
|
i_adjective = 'i_adjective',
|
||||||
|
na_adjective = 'na_adjective',
|
||||||
|
particle = 'particle',
|
||||||
|
bound_auxiliary = 'bound_auxiliary',
|
||||||
|
symbol = 'symbol',
|
||||||
|
other = 'other',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Token {
|
||||||
|
word: string;
|
||||||
|
partOfSpeech: PartOfSpeech;
|
||||||
|
pos1: string;
|
||||||
|
pos2: string;
|
||||||
|
pos3: string;
|
||||||
|
pos4: string;
|
||||||
|
inflectionType: string;
|
||||||
|
inflectionForm: string;
|
||||||
|
headword: string;
|
||||||
|
katakanaReading: string;
|
||||||
|
pronunciation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MergedToken {
|
||||||
|
surface: string;
|
||||||
|
reading: string;
|
||||||
|
headword: string;
|
||||||
|
startPos: number;
|
||||||
|
endPos: number;
|
||||||
|
partOfSpeech: PartOfSpeech;
|
||||||
|
pos1?: string;
|
||||||
|
pos2?: string;
|
||||||
|
pos3?: string;
|
||||||
|
isMerged: boolean;
|
||||||
|
isKnown: boolean;
|
||||||
|
isNPlusOneTarget: boolean;
|
||||||
|
isNameMatch?: boolean;
|
||||||
|
jlptLevel?: JlptLevel;
|
||||||
|
frequencyRank?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FrequencyDictionaryLookup = (term: string) => number | null;
|
||||||
|
|
||||||
|
export type JlptLevel = 'N1' | 'N2' | 'N3' | 'N4' | 'N5';
|
||||||
|
|
||||||
|
export interface SubtitlePosition {
|
||||||
|
yPercent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubtitleStyle {
|
||||||
|
fontSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SecondarySubMode = 'hidden' | 'visible' | 'hover';
|
||||||
|
|
||||||
|
export interface SecondarySubConfig {
|
||||||
|
secondarySubLanguages?: string[];
|
||||||
|
autoLoadSecondarySub?: boolean;
|
||||||
|
defaultMode?: SecondarySubMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NPlusOneMatchMode = 'headword' | 'surface';
|
||||||
|
export type FrequencyDictionaryMatchMode = 'headword' | 'surface';
|
||||||
|
|
||||||
|
export interface SubtitleStyleConfig {
|
||||||
|
enableJlpt?: boolean;
|
||||||
|
preserveLineBreaks?: boolean;
|
||||||
|
autoPauseVideoOnHover?: boolean;
|
||||||
|
autoPauseVideoOnYomitanPopup?: boolean;
|
||||||
|
hoverTokenColor?: string;
|
||||||
|
hoverTokenBackgroundColor?: string;
|
||||||
|
nameMatchEnabled?: boolean;
|
||||||
|
nameMatchColor?: string;
|
||||||
|
fontFamily?: string;
|
||||||
|
fontSize?: number;
|
||||||
|
fontColor?: string;
|
||||||
|
fontWeight?: string | number;
|
||||||
|
fontStyle?: string;
|
||||||
|
lineHeight?: string | number;
|
||||||
|
letterSpacing?: string;
|
||||||
|
wordSpacing?: string | number;
|
||||||
|
fontKerning?: string;
|
||||||
|
textRendering?: string;
|
||||||
|
textShadow?: string;
|
||||||
|
backdropFilter?: string;
|
||||||
|
backgroundColor?: string;
|
||||||
|
nPlusOneColor?: string;
|
||||||
|
knownWordColor?: string;
|
||||||
|
jlptColors?: {
|
||||||
|
N1: string;
|
||||||
|
N2: string;
|
||||||
|
N3: string;
|
||||||
|
N4: string;
|
||||||
|
N5: string;
|
||||||
|
};
|
||||||
|
frequencyDictionary?: {
|
||||||
|
enabled?: boolean;
|
||||||
|
sourcePath?: string;
|
||||||
|
topX?: number;
|
||||||
|
mode?: FrequencyDictionaryMode;
|
||||||
|
matchMode?: FrequencyDictionaryMatchMode;
|
||||||
|
singleColor?: string;
|
||||||
|
bandedColors?: [string, string, string, string, string];
|
||||||
|
};
|
||||||
|
secondary?: {
|
||||||
|
fontFamily?: string;
|
||||||
|
fontSize?: number;
|
||||||
|
fontColor?: string;
|
||||||
|
fontWeight?: string | number;
|
||||||
|
fontStyle?: string;
|
||||||
|
lineHeight?: string | number;
|
||||||
|
letterSpacing?: string;
|
||||||
|
wordSpacing?: string | number;
|
||||||
|
fontKerning?: string;
|
||||||
|
textRendering?: string;
|
||||||
|
textShadow?: string;
|
||||||
|
backdropFilter?: string;
|
||||||
|
backgroundColor?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenPos1ExclusionConfig {
|
||||||
|
defaults?: string[];
|
||||||
|
add?: string[];
|
||||||
|
remove?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolvedTokenPos1ExclusionConfig {
|
||||||
|
defaults: string[];
|
||||||
|
add: string[];
|
||||||
|
remove: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenPos2ExclusionConfig {
|
||||||
|
defaults?: string[];
|
||||||
|
add?: string[];
|
||||||
|
remove?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolvedTokenPos2ExclusionConfig {
|
||||||
|
defaults: string[];
|
||||||
|
add: string[];
|
||||||
|
remove: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FrequencyDictionaryMode = 'single' | 'banded';
|
||||||
|
|
||||||
|
export type { SubtitleCue };
|
||||||
|
|
||||||
|
export type SubtitleSidebarLayout = 'overlay' | 'embedded';
|
||||||
|
|
||||||
|
export interface SubtitleSidebarConfig {
|
||||||
|
enabled?: boolean;
|
||||||
|
autoOpen?: boolean;
|
||||||
|
layout?: SubtitleSidebarLayout;
|
||||||
|
toggleKey?: string;
|
||||||
|
pauseVideoOnHover?: boolean;
|
||||||
|
autoScroll?: boolean;
|
||||||
|
maxWidth?: number;
|
||||||
|
opacity?: number;
|
||||||
|
backgroundColor?: string;
|
||||||
|
textColor?: string;
|
||||||
|
fontFamily?: string;
|
||||||
|
fontSize?: number;
|
||||||
|
timestampColor?: string;
|
||||||
|
activeLineColor?: string;
|
||||||
|
activeLineBackgroundColor?: string;
|
||||||
|
hoverLineBackgroundColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubtitleData {
|
||||||
|
text: string;
|
||||||
|
tokens: MergedToken[] | null;
|
||||||
|
startTime?: number | null;
|
||||||
|
endTime?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubtitleSidebarSnapshot {
|
||||||
|
cues: SubtitleCue[];
|
||||||
|
currentTimeSec?: number | null;
|
||||||
|
currentSubtitle: {
|
||||||
|
text: string;
|
||||||
|
startTime: number | null;
|
||||||
|
endTime: number | null;
|
||||||
|
};
|
||||||
|
config: Required<SubtitleSidebarConfig>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubtitleHoverTokenPayload {
|
||||||
|
tokenIndex: number | null;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user