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,4 +1,4 @@
|
||||
import type { AnkiConnectConfig } from './types';
|
||||
import type { AnkiConnectConfig } from './types/anki';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function getConfiguredWordFieldName(config?: Pick<AnkiConnectConfig, 'fields'> | null): string {
|
||||
export function getConfiguredWordFieldName(
|
||||
config?: Pick<AnkiConnectConfig, 'fields'> | null,
|
||||
): string {
|
||||
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';
|
||||
|
||||
const DEFAULT_AI_SYSTEM_PROMPT =
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { DEFAULT_ANKI_CONNECT_CONFIG } from '../config';
|
||||
import type { AnkiConnectConfig } from '../types';
|
||||
import type { AnkiConnectConfig } from '../types/anki';
|
||||
|
||||
type NoteInfoLike = {
|
||||
noteId: number;
|
||||
@@ -36,9 +36,7 @@ export function extractSoundFilenames(value: string): string[] {
|
||||
}
|
||||
|
||||
function shouldSyncAnimatedImageToWordAudio(config: Pick<AnkiConnectConfig, 'media'>): boolean {
|
||||
return (
|
||||
config.media?.imageType === 'avif' && config.media?.syncAnimatedImageToWordAudio !== false
|
||||
);
|
||||
return config.media?.imageType === 'avif' && config.media?.syncAnimatedImageToWordAudio !== false;
|
||||
}
|
||||
|
||||
export async function probeAudioDurationSeconds(
|
||||
|
||||
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { CardCreationService } from './card-creation';
|
||||
import type { AnkiConnectConfig } from '../types';
|
||||
import type { AnkiConnectConfig } from '../types/anki';
|
||||
|
||||
test('CardCreationService counts locally created sentence cards', async () => {
|
||||
const minedCards: Array<{ count: number; noteIds?: number[] }> = [];
|
||||
|
||||
@@ -3,10 +3,11 @@ import {
|
||||
getConfiguredWordFieldName,
|
||||
getPreferredWordValueFromExtractedFields,
|
||||
} from '../anki-field-config';
|
||||
import { AiConfig, AnkiConnectConfig } from '../types';
|
||||
import { AnkiConnectConfig } from '../types/anki';
|
||||
import { createLogger } from '../logger';
|
||||
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 { resolveMediaGenerationInputPath } from './media-source';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AnkiConnectConfig } from '../types';
|
||||
import { AnkiConnectConfig } from '../types/anki';
|
||||
import { getConfiguredWordFieldName } from '../anki-field-config';
|
||||
|
||||
interface FieldGroupingMergeMedia {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { FieldGroupingWorkflow } from './field-grouping-workflow';
|
||||
import type { KikuDuplicateCardInfo, KikuFieldGroupingChoice } from '../types';
|
||||
import type { KikuDuplicateCardInfo, KikuFieldGroupingChoice } from '../types/anki';
|
||||
|
||||
type NoteInfo = {
|
||||
noteId: number;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { KikuDuplicateCardInfo, KikuFieldGroupingChoice } from '../types';
|
||||
import { KikuDuplicateCardInfo, KikuFieldGroupingChoice } from '../types/anki';
|
||||
import { getPreferredWordValueFromExtractedFields } from '../anki-field-config';
|
||||
|
||||
export interface FieldGroupingWorkflowNoteInfo {
|
||||
@@ -181,7 +181,8 @@ export class FieldGroupingWorkflow {
|
||||
return {
|
||||
noteId: noteInfo.noteId,
|
||||
expression:
|
||||
getPreferredWordValueFromExtractedFields(fields, this.deps.getConfig()) || fallbackExpression,
|
||||
getPreferredWordValueFromExtractedFields(fields, this.deps.getConfig()) ||
|
||||
fallbackExpression,
|
||||
sentencePreview: this.deps.truncateSentence(
|
||||
fields[(sentenceCardConfig.sentenceField || 'sentence').toLowerCase()] ||
|
||||
(isOriginal ? '' : this.deps.getCurrentSubtitleText() || ''),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { KikuMergePreviewResponse } from '../types';
|
||||
import { KikuMergePreviewResponse } from '../types/anki';
|
||||
import { createLogger } from '../logger';
|
||||
import { getPreferredWordValueFromExtractedFields } from '../anki-field-config';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import type { AnkiConnectConfig } from '../types';
|
||||
import type { AnkiConnectConfig } from '../types/anki';
|
||||
import { KnownWordCacheManager } from './known-word-cache';
|
||||
|
||||
async function waitForCondition(
|
||||
@@ -351,10 +351,7 @@ test('KnownWordCacheManager preserves cache state key captured before refresh wo
|
||||
scope: string;
|
||||
words: string[];
|
||||
};
|
||||
assert.equal(
|
||||
persisted.scope,
|
||||
'{"refreshMinutes":1,"scope":"is:note","fieldsWord":"Word"}',
|
||||
);
|
||||
assert.equal(persisted.scope, '{"refreshMinutes":1,"scope":"is:note","fieldsWord":"Word"}');
|
||||
assert.deepEqual(persisted.words, ['猫']);
|
||||
} finally {
|
||||
fs.rmSync(stateDir, { recursive: true, force: true });
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from 'path';
|
||||
|
||||
import { DEFAULT_ANKI_CONNECT_CONFIG } from '../config';
|
||||
import { getConfiguredWordFieldName } from '../anki-field-config';
|
||||
import { AnkiConnectConfig } from '../types';
|
||||
import { AnkiConnectConfig } from '../types/anki';
|
||||
import { createLogger } from '../logger';
|
||||
|
||||
const log = createLogger('anki').child('integration.known-word-cache');
|
||||
@@ -316,9 +316,9 @@ export class KnownWordCacheManager {
|
||||
const currentDeck = this.deps.getConfig().deck?.trim();
|
||||
const selectedDeckEntry =
|
||||
currentDeck !== undefined && currentDeck.length > 0
|
||||
? trimmedDeckEntries.find(([deckName]) => deckName === currentDeck) ?? null
|
||||
? (trimmedDeckEntries.find(([deckName]) => deckName === currentDeck) ?? null)
|
||||
: trimmedDeckEntries.length === 1
|
||||
? trimmedDeckEntries[0] ?? null
|
||||
? (trimmedDeckEntries[0] ?? null)
|
||||
: null;
|
||||
|
||||
if (!selectedDeckEntry) {
|
||||
@@ -329,7 +329,10 @@ export class KnownWordCacheManager {
|
||||
if (Array.isArray(deckFields)) {
|
||||
const normalizedFields = [
|
||||
...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) {
|
||||
@@ -353,7 +356,14 @@ export class KnownWordCacheManager {
|
||||
continue;
|
||||
}
|
||||
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({
|
||||
query: `deck:"${escapeAnkiSearchValue(trimmedDeckName)}"`,
|
||||
@@ -402,7 +412,10 @@ export class KnownWordCacheManager {
|
||||
private async fetchKnownWordNoteFieldsById(): Promise<Map<number, string[]>> {
|
||||
const scopes = this.getKnownWordQueryScopes();
|
||||
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) {
|
||||
const noteIds = (await this.deps.client.findNotes(scope.query, {
|
||||
@@ -414,10 +427,7 @@ export class KnownWordCacheManager {
|
||||
continue;
|
||||
}
|
||||
const existingFields = noteFieldsById.get(noteId) ?? [];
|
||||
noteFieldsById.set(
|
||||
noteId,
|
||||
[...new Set([...existingFields, ...scope.fields])],
|
||||
);
|
||||
noteFieldsById.set(noteId, [...new Set([...existingFields, ...scope.fields])]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isRemoteMediaPath } from '../jimaku/utils';
|
||||
import type { MpvClient } from '../types';
|
||||
import type { MpvClient } from '../types/runtime';
|
||||
|
||||
export type MediaGenerationKind = 'audio' | 'video';
|
||||
|
||||
@@ -50,7 +50,7 @@ function resolvePreferredUrlFromMpvEdlSource(
|
||||
|
||||
// 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.
|
||||
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(
|
||||
|
||||
@@ -2,7 +2,7 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { DEFAULT_ANKI_CONNECT_CONFIG } from '../config';
|
||||
import type { AnkiConnectConfig } from '../types';
|
||||
import type { AnkiConnectConfig } from '../types/anki';
|
||||
import { AnkiIntegrationRuntime } from './runtime';
|
||||
|
||||
function createRuntime(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DEFAULT_ANKI_CONNECT_CONFIG } from '../config';
|
||||
import type { AnkiConnectConfig } from '../types';
|
||||
import type { AnkiConnectConfig } from '../types/anki';
|
||||
import {
|
||||
getKnownWordCacheLifecycleConfig,
|
||||
getKnownWordCacheRefreshIntervalMinutes,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NotificationOptions } from '../types';
|
||||
import { NotificationOptions } from '../types/anki';
|
||||
|
||||
export interface UiFeedbackState {
|
||||
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 { IMMERSION_DEFAULT_CONFIG } from './definitions/defaults-immersion';
|
||||
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<
|
||||
ResolvedConfig,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ResolvedConfig } from '../../types';
|
||||
import { ResolvedConfig } from '../../types/config';
|
||||
|
||||
export const IMMERSION_DEFAULT_CONFIG: Pick<ResolvedConfig, 'immersionTracking'> = {
|
||||
immersionTracking: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ResolvedConfig } from '../../types';
|
||||
import { ResolvedConfig } from '../../types/config';
|
||||
|
||||
export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
||||
ResolvedConfig,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ResolvedConfig } from '../../types.js';
|
||||
import { ResolvedConfig } from '../../types/config.js';
|
||||
|
||||
export const STATS_DEFAULT_CONFIG: Pick<ResolvedConfig, 'stats'> = {
|
||||
stats: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ResolvedConfig } from '../../types';
|
||||
import { ResolvedConfig } from '../../types/config';
|
||||
|
||||
export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle' | 'subtitleSidebar'> = {
|
||||
subtitleStyle: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ResolvedConfig } from '../../types';
|
||||
import { ResolvedConfig } from '../../types/config';
|
||||
import { ConfigOptionRegistryEntry } from './shared';
|
||||
|
||||
export function buildCoreConfigOptionRegistry(
|
||||
@@ -263,7 +263,8 @@ export function buildCoreConfigOptionRegistry(
|
||||
{
|
||||
path: `controller.bindings.${binding.id}.axisIndex`,
|
||||
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.',
|
||||
},
|
||||
{
|
||||
@@ -293,7 +294,8 @@ export function buildCoreConfigOptionRegistry(
|
||||
{
|
||||
path: `controller.bindings.${binding.id}.axisIndex`,
|
||||
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.',
|
||||
},
|
||||
{
|
||||
@@ -302,7 +304,8 @@ export function buildCoreConfigOptionRegistry(
|
||||
enumValues: ['none', 'horizontal', 'vertical'],
|
||||
defaultValue:
|
||||
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';
|
||||
|
||||
export function buildImmersionConfigOptionRegistry(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ResolvedConfig } from '../../types';
|
||||
import { ResolvedConfig } from '../../types/config';
|
||||
import { ConfigOptionRegistryEntry, RuntimeOptionRegistryEntry } from './shared';
|
||||
|
||||
export function buildIntegrationConfigOptionRegistry(
|
||||
@@ -369,13 +369,15 @@ export function buildIntegrationConfigOptionRegistry(
|
||||
path: 'youtubeSubgen.whisperBin',
|
||||
kind: 'string',
|
||||
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',
|
||||
kind: 'string',
|
||||
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',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ResolvedConfig } from '../../types.js';
|
||||
import { ResolvedConfig } from '../../types/config.js';
|
||||
import { ConfigOptionRegistryEntry } from './shared.js';
|
||||
|
||||
export function buildStatsConfigOptionRegistry(
|
||||
@@ -15,7 +15,8 @@ export function buildStatsConfigOptionRegistry(
|
||||
path: 'stats.markWatchedKey',
|
||||
kind: 'string',
|
||||
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',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ResolvedConfig } from '../../types';
|
||||
import { ResolvedConfig } from '../../types/config';
|
||||
import { ConfigOptionRegistryEntry } from './shared';
|
||||
|
||||
export function buildSubtitleConfigOptionRegistry(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ResolvedConfig } from '../../types';
|
||||
import { ResolvedConfig } from '../../types/config';
|
||||
import { RuntimeOptionRegistryEntry } from './shared';
|
||||
|
||||
export function buildRuntimeOptionRegistry(
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {
|
||||
AnkiConnectConfig,
|
||||
ResolvedConfig,
|
||||
import type { AnkiConnectConfig } from '../../types/anki';
|
||||
import type { ResolvedConfig } from '../../types/config';
|
||||
import type {
|
||||
RuntimeOptionId,
|
||||
RuntimeOptionScope,
|
||||
RuntimeOptionValue,
|
||||
RuntimeOptionValueType,
|
||||
} from '../../types';
|
||||
} from '../../types/runtime-options';
|
||||
|
||||
export type ConfigValueKind = 'boolean' | 'number' | 'string' | 'enum' | 'array' | 'object';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as fs from 'fs';
|
||||
import { RawConfig } from '../types';
|
||||
import { RawConfig } from '../types/config';
|
||||
import { parseConfigContent } from './parse';
|
||||
|
||||
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 { createResolveContext } from './resolve/context';
|
||||
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 { createWarningCollector } from '../warnings';
|
||||
import { isObject } from './shared';
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
ControllerDiscreteBindingConfig,
|
||||
ResolvedControllerAxisBinding,
|
||||
ResolvedControllerDiscreteBinding,
|
||||
} from '../../types';
|
||||
} from '../../types/runtime';
|
||||
import { ResolveContext } from './context';
|
||||
import { asBoolean, asNumber, asString, isObject } from './shared';
|
||||
|
||||
@@ -27,7 +27,12 @@ const CONTROLLER_BUTTON_BINDINGS = [
|
||||
'rightTrigger',
|
||||
] 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> = {
|
||||
leftStickX: 0,
|
||||
@@ -98,7 +103,9 @@ function parseDiscreteBindingObject(value: unknown): ResolvedControllerDiscreteB
|
||||
return { kind: 'none' };
|
||||
}
|
||||
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 }
|
||||
: null;
|
||||
}
|
||||
@@ -121,7 +128,11 @@ function parseAxisBindingObject(
|
||||
return { kind: 'none' };
|
||||
}
|
||||
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;
|
||||
}
|
||||
if (value.dpadFallback !== undefined && !isControllerDpadFallback(value.dpadFallback)) {
|
||||
@@ -368,7 +379,9 @@ export function applyCoreDomainConfig(context: ResolveContext): void {
|
||||
const legacyValue = asString(bindingValue);
|
||||
if (
|
||||
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(
|
||||
legacyValue as ControllerButtonBinding,
|
||||
@@ -401,7 +414,9 @@ export function applyCoreDomainConfig(context: ResolveContext): void {
|
||||
const legacyValue = asString(bindingValue);
|
||||
if (
|
||||
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(
|
||||
legacyValue as ControllerAxisBinding,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ResolveContext } from './context';
|
||||
import { ImmersionTrackingRetentionMode, ImmersionTrackingRetentionPreset } from '../../types';
|
||||
import {
|
||||
ImmersionTrackingRetentionMode,
|
||||
ImmersionTrackingRetentionPreset,
|
||||
} from '../../types/integrations';
|
||||
import { asBoolean, asNumber, asString, isObject } from './shared';
|
||||
|
||||
const DEFAULT_RETENTION_MODE: ImmersionTrackingRetentionMode = 'preset';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ResolvedConfig } from '../../types';
|
||||
import { ResolvedConfig } from '../../types/config';
|
||||
import { ResolveContext } from './context';
|
||||
import {
|
||||
asBoolean,
|
||||
@@ -467,7 +467,9 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
||||
);
|
||||
if (pauseVideoOnHover !== undefined) {
|
||||
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;
|
||||
warn(
|
||||
'subtitleSidebar.pauseVideoOnHover',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as fs from 'fs';
|
||||
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 { ConfigPaths, loadRawConfig, loadRawConfigStrict } from './load';
|
||||
import { resolveConfig } from './resolve';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ResolvedConfig } from '../types';
|
||||
import { ResolvedConfig } from '../types/config';
|
||||
import {
|
||||
CONFIG_OPTION_REGISTRY,
|
||||
CONFIG_TEMPLATE_SECTIONS,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ConfigValidationWarning } from '../types';
|
||||
import { ConfigValidationWarning } from '../types/config';
|
||||
|
||||
export interface WarningCollector {
|
||||
warnings: ConfigValidationWarning[];
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { RuntimeOptionApplyResult, RuntimeOptionId, RuntimeOptionValue } from '../../types';
|
||||
import {
|
||||
RuntimeOptionApplyResult,
|
||||
RuntimeOptionId,
|
||||
RuntimeOptionValue,
|
||||
} from '../../types/runtime-options';
|
||||
|
||||
export interface RuntimeOptionsManagerLike {
|
||||
setOptionValue: (id: RuntimeOptionId, value: RuntimeOptionValue) => RuntimeOptionApplyResult;
|
||||
|
||||
@@ -2,9 +2,9 @@ import type {
|
||||
AnkiConnectConfig,
|
||||
KikuFieldGroupingChoice,
|
||||
KikuFieldGroupingRequestData,
|
||||
WindowGeometry,
|
||||
} from '../../types';
|
||||
} from '../../types/anki';
|
||||
import type { BrowserWindow } from 'electron';
|
||||
import type { WindowGeometry } from '../../types/runtime';
|
||||
import type { BaseWindowTracker } from '../../window-trackers';
|
||||
|
||||
type OverlayRuntimeOptions = {
|
||||
|
||||
@@ -16,14 +16,14 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { AnkiConnectConfig } from './types/anki';
|
||||
import {
|
||||
AnkiConnectConfig,
|
||||
RuntimeOptionApplyResult,
|
||||
RuntimeOptionId,
|
||||
RuntimeOptionState,
|
||||
RuntimeOptionValue,
|
||||
SubtitleStyleConfig,
|
||||
} from './types';
|
||||
} from './types/runtime-options';
|
||||
import { SubtitleStyleConfig } from './types/subtitle';
|
||||
import { RUNTIME_OPTION_REGISTRY, RuntimeOptionRegistryEntry } from './config';
|
||||
|
||||
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 = [
|
||||
'runtime-options',
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { KikuFieldGroupingChoice, KikuMergePreviewRequest } from '../../types/anki';
|
||||
import type {
|
||||
ControllerConfigUpdate,
|
||||
ControllerPreferenceUpdate,
|
||||
JimakuDownloadQuery,
|
||||
JimakuFilesQuery,
|
||||
JimakuSearchQuery,
|
||||
KikuFieldGroupingChoice,
|
||||
KikuMergePreviewRequest,
|
||||
RuntimeOptionId,
|
||||
RuntimeOptionValue,
|
||||
SubtitlePosition,
|
||||
SubsyncManualRunRequest,
|
||||
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';
|
||||
|
||||
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 (typeof value.sessionId !== 'string' || !value.sessionId.trim()) 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,
|
||||
};
|
||||
}
|
||||
if (value.primaryTrackId !== null && value.primaryTrackId !== undefined && typeof value.primaryTrackId !== 'string') {
|
||||
if (
|
||||
value.primaryTrackId !== null &&
|
||||
value.primaryTrackId !== undefined &&
|
||||
typeof value.primaryTrackId !== 'string'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
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