mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-01 06:22:44 -08:00
run prettier
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
<!-- BACKLOG.MD MCP GUIDELINES START -->
|
<!-- BACKLOG.MD MCP GUIDELINES START -->
|
||||||
|
|
||||||
<CRITICAL_INSTRUCTION>
|
<CRITICAL_INSTRUCTION>
|
||||||
@@ -17,6 +16,7 @@ This project uses Backlog.md MCP for all task and project management activities.
|
|||||||
- **When to read it**: BEFORE creating tasks, or when you're unsure whether to track work
|
- **When to read it**: BEFORE creating tasks, or when you're unsure whether to track work
|
||||||
|
|
||||||
These guides cover:
|
These guides cover:
|
||||||
|
|
||||||
- Decision framework for when to create tasks
|
- Decision framework for when to create tasks
|
||||||
- Search-first workflow to avoid duplicates
|
- Search-first workflow to avoid duplicates
|
||||||
- Links to detailed guides for task creation, execution, and finalization
|
- Links to detailed guides for task creation, execution, and finalization
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ cp /tmp/plugin/subminer.conf ~/.config/mpv/script-opts/
|
|||||||
mkdir -p ~/.config/SubMiner && cp /tmp/config.example.jsonc ~/.config/SubMiner/config.jsonc
|
mkdir -p ~/.config/SubMiner && cp /tmp/config.example.jsonc ~/.config/SubMiner/config.jsonc
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### 3. Set up Yomitan Dictionaries
|
### 3. Set up Yomitan Dictionaries
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
* Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.
|
* Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.
|
||||||
*/
|
*/
|
||||||
{
|
{
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Overlay Auto-Start
|
// Overlay Auto-Start
|
||||||
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles.
|
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles.
|
||||||
@@ -17,7 +16,7 @@
|
|||||||
// Control whether browser opens automatically for texthooker.
|
// Control whether browser opens automatically for texthooker.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"texthooker": {
|
"texthooker": {
|
||||||
"openBrowser": true // Open browser setting. Values: true | false
|
"openBrowser": true, // Open browser setting. Values: true | false
|
||||||
}, // Control whether browser opens automatically for texthooker.
|
}, // Control whether browser opens automatically for texthooker.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -27,7 +26,7 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
"websocket": {
|
"websocket": {
|
||||||
"enabled": "auto", // Built-in subtitle websocket server mode. Values: auto | true | false
|
"enabled": "auto", // Built-in subtitle websocket server mode. Values: auto | true | false
|
||||||
"port": 6677 // Built-in subtitle websocket server port.
|
"port": 6677, // Built-in subtitle websocket server port.
|
||||||
}, // Built-in WebSocket server broadcasts subtitle text to connected clients.
|
}, // Built-in WebSocket server broadcasts subtitle text to connected clients.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -36,7 +35,7 @@
|
|||||||
// Set to debug for full runtime diagnostics.
|
// Set to debug for full runtime diagnostics.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"logging": {
|
"logging": {
|
||||||
"level": "info" // Minimum log level for runtime logging. Values: debug | info | warn | error
|
"level": "info", // Minimum log level for runtime logging. Values: debug | info | warn | error
|
||||||
}, // Controls logging verbosity. Keep this as an object; do not replace with a bare string.
|
}, // Controls logging verbosity. Keep this as an object; do not replace with a bare string.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -57,7 +56,7 @@
|
|||||||
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
|
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
|
||||||
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting.
|
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting.
|
||||||
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting.
|
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting.
|
||||||
"openJimaku": "Ctrl+Shift+J" // Open jimaku setting.
|
"openJimaku": "Ctrl+Shift+J", // Open jimaku setting.
|
||||||
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -77,7 +76,7 @@
|
|||||||
"secondarySub": {
|
"secondarySub": {
|
||||||
"secondarySubLanguages": [], // Secondary sub languages setting.
|
"secondarySubLanguages": [], // Secondary sub languages setting.
|
||||||
"autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false
|
"autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false
|
||||||
"defaultMode": "hover" // Default mode setting.
|
"defaultMode": "hover", // Default mode setting.
|
||||||
}, // Dual subtitle track options.
|
}, // Dual subtitle track options.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -88,7 +87,7 @@
|
|||||||
"defaultMode": "auto", // Subsync default mode. Values: auto | manual
|
"defaultMode": "auto", // Subsync default mode. Values: auto | manual
|
||||||
"alass_path": "", // Alass path setting.
|
"alass_path": "", // Alass path setting.
|
||||||
"ffsubsync_path": "", // Ffsubsync path setting.
|
"ffsubsync_path": "", // Ffsubsync path setting.
|
||||||
"ffmpeg_path": "" // Ffmpeg path setting.
|
"ffmpeg_path": "", // Ffmpeg path setting.
|
||||||
}, // Subsync engine and executable paths.
|
}, // Subsync engine and executable paths.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -96,7 +95,7 @@
|
|||||||
// Initial vertical subtitle position from the bottom.
|
// Initial vertical subtitle position from the bottom.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"subtitlePosition": {
|
"subtitlePosition": {
|
||||||
"yPercent": 10 // Y percent setting.
|
"yPercent": 10, // Y percent setting.
|
||||||
}, // Initial vertical subtitle position from the bottom.
|
}, // Initial vertical subtitle position from the bottom.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -129,7 +128,7 @@
|
|||||||
"N2": "#f5a97f", // N2 setting.
|
"N2": "#f5a97f", // N2 setting.
|
||||||
"N3": "#f9e2af", // N3 setting.
|
"N3": "#f9e2af", // N3 setting.
|
||||||
"N4": "#a6e3a1", // N4 setting.
|
"N4": "#a6e3a1", // N4 setting.
|
||||||
"N5": "#8aadf4" // N5 setting.
|
"N5": "#8aadf4", // N5 setting.
|
||||||
}, // Jlpt colors setting.
|
}, // Jlpt colors setting.
|
||||||
"frequencyDictionary": {
|
"frequencyDictionary": {
|
||||||
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
|
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
|
||||||
@@ -138,13 +137,7 @@
|
|||||||
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
|
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
|
||||||
"matchMode": "headword", // Frequency lookup text selection mode. Values: headword | surface
|
"matchMode": "headword", // Frequency lookup text selection mode. Values: headword | surface
|
||||||
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
|
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
|
||||||
"bandedColors": [
|
"bandedColors": ["#ed8796", "#f5a97f", "#f9e2af", "#a6e3a1", "#8aadf4"], // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
|
||||||
"#ed8796",
|
|
||||||
"#f5a97f",
|
|
||||||
"#f9e2af",
|
|
||||||
"#a6e3a1",
|
|
||||||
"#8aadf4"
|
|
||||||
] // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
|
|
||||||
}, // Frequency dictionary setting.
|
}, // Frequency dictionary setting.
|
||||||
"secondary": {
|
"secondary": {
|
||||||
"fontFamily": "Manrope, Inter", // Font family setting.
|
"fontFamily": "Manrope, Inter", // Font family setting.
|
||||||
@@ -159,8 +152,8 @@
|
|||||||
"backgroundColor": "transparent", // Background color setting.
|
"backgroundColor": "transparent", // Background color setting.
|
||||||
"backdropFilter": "blur(6px)", // Backdrop filter setting.
|
"backdropFilter": "blur(6px)", // Backdrop filter setting.
|
||||||
"fontWeight": "normal", // Font weight setting.
|
"fontWeight": "normal", // Font weight setting.
|
||||||
"fontStyle": "normal" // Font style setting.
|
"fontStyle": "normal", // Font style setting.
|
||||||
} // Secondary setting.
|
}, // Secondary setting.
|
||||||
}, // Primary and secondary subtitle styling.
|
}, // Primary and secondary subtitle styling.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -177,17 +170,15 @@
|
|||||||
"enabled": false, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false
|
"enabled": false, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false
|
||||||
"host": "127.0.0.1", // Bind host for local AnkiConnect proxy.
|
"host": "127.0.0.1", // Bind host for local AnkiConnect proxy.
|
||||||
"port": 8766, // Bind port for local AnkiConnect proxy.
|
"port": 8766, // Bind port for local AnkiConnect proxy.
|
||||||
"upstreamUrl": "http://127.0.0.1:8765" // Upstream AnkiConnect URL proxied by local AnkiConnect proxy.
|
"upstreamUrl": "http://127.0.0.1:8765", // Upstream AnkiConnect URL proxied by local AnkiConnect proxy.
|
||||||
}, // Proxy setting.
|
}, // Proxy setting.
|
||||||
"tags": [
|
"tags": ["SubMiner"], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
||||||
"SubMiner"
|
|
||||||
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
|
||||||
"fields": {
|
"fields": {
|
||||||
"audio": "ExpressionAudio", // Audio setting.
|
"audio": "ExpressionAudio", // Audio setting.
|
||||||
"image": "Picture", // Image setting.
|
"image": "Picture", // Image setting.
|
||||||
"sentence": "Sentence", // Sentence setting.
|
"sentence": "Sentence", // Sentence setting.
|
||||||
"miscInfo": "MiscInfo", // Misc info setting.
|
"miscInfo": "MiscInfo", // Misc info setting.
|
||||||
"translation": "SelectionText" // Translation setting.
|
"translation": "SelectionText", // Translation setting.
|
||||||
}, // Fields setting.
|
}, // Fields setting.
|
||||||
"ai": {
|
"ai": {
|
||||||
"enabled": false, // Enabled setting. Values: true | false
|
"enabled": false, // Enabled setting. Values: true | false
|
||||||
@@ -196,7 +187,7 @@
|
|||||||
"model": "openai/gpt-4o-mini", // Model setting.
|
"model": "openai/gpt-4o-mini", // Model setting.
|
||||||
"baseUrl": "https://openrouter.ai/api", // Base url setting.
|
"baseUrl": "https://openrouter.ai/api", // Base url setting.
|
||||||
"targetLanguage": "English", // Target language setting.
|
"targetLanguage": "English", // Target language setting.
|
||||||
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations." // System prompt setting.
|
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // System prompt setting.
|
||||||
}, // Ai setting.
|
}, // Ai setting.
|
||||||
"media": {
|
"media": {
|
||||||
"generateAudio": true, // Generate audio setting. Values: true | false
|
"generateAudio": true, // Generate audio setting. Values: true | false
|
||||||
@@ -209,7 +200,7 @@
|
|||||||
"animatedCrf": 35, // Animated crf setting.
|
"animatedCrf": 35, // Animated crf setting.
|
||||||
"audioPadding": 0.5, // Audio padding setting.
|
"audioPadding": 0.5, // Audio padding setting.
|
||||||
"fallbackDuration": 3, // Fallback duration setting.
|
"fallbackDuration": 3, // Fallback duration setting.
|
||||||
"maxMediaDuration": 30 // Max media duration setting.
|
"maxMediaDuration": 30, // Max media duration setting.
|
||||||
}, // Media setting.
|
}, // Media setting.
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"overwriteAudio": true, // Overwrite audio setting. Values: true | false
|
"overwriteAudio": true, // Overwrite audio setting. Values: true | false
|
||||||
@@ -217,7 +208,7 @@
|
|||||||
"mediaInsertMode": "append", // Media insert mode setting.
|
"mediaInsertMode": "append", // Media insert mode setting.
|
||||||
"highlightWord": true, // Highlight word setting. Values: true | false
|
"highlightWord": true, // Highlight word setting. Values: true | false
|
||||||
"notificationType": "osd", // Notification type setting.
|
"notificationType": "osd", // Notification type setting.
|
||||||
"autoUpdateNewCards": true // Automatically update newly added cards. Values: true | false
|
"autoUpdateNewCards": true, // Automatically update newly added cards. Values: true | false
|
||||||
}, // Behavior setting.
|
}, // Behavior setting.
|
||||||
"nPlusOne": {
|
"nPlusOne": {
|
||||||
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
|
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
|
||||||
@@ -226,20 +217,20 @@
|
|||||||
"decks": [], // Decks used for N+1 known-word cache scope. Supports one or more deck names.
|
"decks": [], // Decks used for N+1 known-word cache scope. Supports one or more deck names.
|
||||||
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3).
|
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3).
|
||||||
"nPlusOne": "#c6a0f6", // Color used for the single N+1 target token highlight.
|
"nPlusOne": "#c6a0f6", // Color used for the single N+1 target token highlight.
|
||||||
"knownWord": "#a6da95" // Color used for legacy known-word highlights.
|
"knownWord": "#a6da95", // Color used for legacy known-word highlights.
|
||||||
}, // N plus one setting.
|
}, // N plus one setting.
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"pattern": "[SubMiner] %f (%t)" // Pattern setting.
|
"pattern": "[SubMiner] %f (%t)", // Pattern setting.
|
||||||
}, // Metadata setting.
|
}, // Metadata setting.
|
||||||
"isLapis": {
|
"isLapis": {
|
||||||
"enabled": false, // Enabled setting. Values: true | false
|
"enabled": false, // Enabled setting. Values: true | false
|
||||||
"sentenceCardModel": "Japanese sentences" // Sentence card model setting.
|
"sentenceCardModel": "Japanese sentences", // Sentence card model setting.
|
||||||
}, // Is lapis setting.
|
}, // Is lapis setting.
|
||||||
"isKiku": {
|
"isKiku": {
|
||||||
"enabled": false, // Enabled setting. Values: true | false
|
"enabled": false, // Enabled setting. Values: true | false
|
||||||
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
|
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
|
||||||
"deleteDuplicateInAuto": true // Delete duplicate in auto setting. Values: true | false
|
"deleteDuplicateInAuto": true, // Delete duplicate in auto setting. Values: true | false
|
||||||
} // Is kiku setting.
|
}, // Is kiku setting.
|
||||||
}, // Automatic Anki updates and media generation options.
|
}, // Automatic Anki updates and media generation options.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -249,7 +240,7 @@
|
|||||||
"jimaku": {
|
"jimaku": {
|
||||||
"apiBaseUrl": "https://jimaku.cc", // Api base url setting.
|
"apiBaseUrl": "https://jimaku.cc", // Api base url setting.
|
||||||
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
|
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
|
||||||
"maxEntryResults": 10 // Maximum Jimaku search results returned.
|
"maxEntryResults": 10, // Maximum Jimaku search results returned.
|
||||||
}, // Jimaku API configuration and defaults.
|
}, // Jimaku API configuration and defaults.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -260,10 +251,7 @@
|
|||||||
"mode": "automatic", // YouTube subtitle generation mode for the launcher script. Values: automatic | preprocess | off
|
"mode": "automatic", // YouTube subtitle generation mode for the launcher script. Values: automatic | preprocess | off
|
||||||
"whisperBin": "", // Path to whisper.cpp CLI used as fallback transcription engine.
|
"whisperBin": "", // Path to whisper.cpp CLI used as fallback transcription engine.
|
||||||
"whisperModel": "", // Path to whisper model used for fallback transcription.
|
"whisperModel": "", // Path to whisper model used for fallback transcription.
|
||||||
"primarySubLanguages": [
|
"primarySubLanguages": ["ja", "jpn"], // Comma-separated primary subtitle language priority used by the launcher.
|
||||||
"ja",
|
|
||||||
"jpn"
|
|
||||||
] // Comma-separated primary subtitle language priority used by the launcher.
|
|
||||||
}, // Defaults for subminer YouTube subtitle extraction/transcription mode.
|
}, // Defaults for subminer YouTube subtitle extraction/transcription mode.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -272,7 +260,7 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
"anilist": {
|
"anilist": {
|
||||||
"enabled": false, // Enable AniList post-watch progress updates. Values: true | false
|
"enabled": false, // Enable AniList post-watch progress updates. Values: true | false
|
||||||
"accessToken": "" // Optional explicit AniList access token override; leave empty to use locally stored token from setup.
|
"accessToken": "", // Optional explicit AniList access token override; leave empty to use locally stored token from setup.
|
||||||
}, // Anilist API credentials and update behavior.
|
}, // Anilist API credentials and update behavior.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -296,16 +284,8 @@
|
|||||||
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
|
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
|
||||||
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
|
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
|
||||||
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
|
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
|
||||||
"directPlayContainers": [
|
"directPlayContainers": ["mkv", "mp4", "webm", "mov", "flac", "mp3", "aac"], // Container allowlist for direct play decisions.
|
||||||
"mkv",
|
"transcodeVideoCodec": "h264", // Preferred transcode video codec when direct play is unavailable.
|
||||||
"mp4",
|
|
||||||
"webm",
|
|
||||||
"mov",
|
|
||||||
"flac",
|
|
||||||
"mp3",
|
|
||||||
"aac"
|
|
||||||
], // Container allowlist for direct play decisions.
|
|
||||||
"transcodeVideoCodec": "h264" // Preferred transcode video codec when direct play is unavailable.
|
|
||||||
}, // Optional Jellyfin integration for auth, browsing, and playback launch.
|
}, // Optional Jellyfin integration for auth, browsing, and playback launch.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -316,7 +296,7 @@
|
|||||||
"discordPresence": {
|
"discordPresence": {
|
||||||
"enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false
|
"enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false
|
||||||
"updateIntervalMs": 3000, // Minimum interval between presence payload updates.
|
"updateIntervalMs": 3000, // Minimum interval between presence payload updates.
|
||||||
"debounceMs": 750 // Debounce delay used to collapse bursty presence updates.
|
"debounceMs": 750, // Debounce delay used to collapse bursty presence updates.
|
||||||
}, // Optional Discord Rich Presence activity card updates for current playback/study session.
|
}, // Optional Discord Rich Presence activity card updates for current playback/study session.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -338,7 +318,7 @@
|
|||||||
"telemetryDays": 30, // Telemetry retention window in days.
|
"telemetryDays": 30, // Telemetry retention window in days.
|
||||||
"dailyRollupsDays": 365, // Daily rollup retention window in days.
|
"dailyRollupsDays": 365, // Daily rollup retention window in days.
|
||||||
"monthlyRollupsDays": 1825, // Monthly rollup retention window in days.
|
"monthlyRollupsDays": 1825, // Monthly rollup retention window in days.
|
||||||
"vacuumIntervalDays": 7 // Minimum days between VACUUM runs.
|
"vacuumIntervalDays": 7, // Minimum days between VACUUM runs.
|
||||||
} // Retention setting.
|
}, // Retention setting.
|
||||||
} // Enable/disable immersion tracking.
|
}, // Enable/disable immersion tracking.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ test('inferAniSkipMetadataForFile falls back to anime directory title when filen
|
|||||||
|
|
||||||
test('buildSubminerScriptOpts includes aniskip metadata fields', () => {
|
test('buildSubminerScriptOpts includes aniskip metadata fields', () => {
|
||||||
const opts = buildSubminerScriptOpts('/tmp/SubMiner.AppImage', '/tmp/subminer.sock', {
|
const opts = buildSubminerScriptOpts('/tmp/SubMiner.AppImage', '/tmp/subminer.sock', {
|
||||||
title: 'Frieren: Beyond Journey\'s End',
|
title: "Frieren: Beyond Journey's End",
|
||||||
season: 1,
|
season: 1,
|
||||||
episode: 5,
|
episode: 5,
|
||||||
source: 'guessit',
|
source: 'guessit',
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ function toPositiveInt(value: unknown): number | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function detectEpisodeFromName(baseName: string): number | null {
|
function detectEpisodeFromName(baseName: string): number | null {
|
||||||
const patterns = [/[Ss]\d+[Ee](\d{1,3})/, /(?:^|[\s._-])[Ee][Pp]?[\s._-]*(\d{1,3})(?:$|[\s._-])/, /[-\s](\d{1,3})$/];
|
const patterns = [
|
||||||
|
/[Ss]\d+[Ee](\d{1,3})/,
|
||||||
|
/(?:^|[\s._-])[Ee][Pp]?[\s._-]*(\d{1,3})(?:$|[\s._-])/,
|
||||||
|
/[-\s](\d{1,3})$/,
|
||||||
|
];
|
||||||
for (const pattern of patterns) {
|
for (const pattern of patterns) {
|
||||||
const match = baseName.match(pattern);
|
const match = baseName.match(pattern);
|
||||||
if (!match || !match[1]) continue;
|
if (!match || !match[1]) continue;
|
||||||
@@ -171,7 +175,11 @@ export function inferAniSkipMetadataForFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeScriptOptValue(value: string): string {
|
function sanitizeScriptOptValue(value: string): string {
|
||||||
return value.replace(/,/g, ' ').replace(/[\r\n]/g, ' ').replace(/\s+/g, ' ').trim();
|
return value
|
||||||
|
.replace(/,/g, ' ')
|
||||||
|
.replace(/[\r\n]/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildSubminerScriptOpts(
|
export function buildSubminerScriptOpts(
|
||||||
|
|||||||
@@ -4,11 +4,9 @@ import { execFileSync } from 'node:child_process';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
test('launcher root help lists subcommands', () => {
|
test('launcher root help lists subcommands', () => {
|
||||||
const output = execFileSync(
|
const output = execFileSync('bun', ['run', path.join(process.cwd(), 'launcher/main.ts'), '-h'], {
|
||||||
'bun',
|
encoding: 'utf8',
|
||||||
['run', path.join(process.cwd(), 'launcher/main.ts'), '-h'],
|
});
|
||||||
{ encoding: 'utf8' },
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.match(output, /Commands:/);
|
assert.match(output, /Commands:/);
|
||||||
assert.match(output, /jellyfin\|jf/);
|
assert.match(output, /jellyfin\|jf/);
|
||||||
|
|||||||
@@ -182,7 +182,8 @@ export function parseCliPrograms(
|
|||||||
server: typeof options.server === 'string' ? options.server : undefined,
|
server: typeof options.server === 'string' ? options.server : undefined,
|
||||||
username: typeof options.username === 'string' ? options.username : undefined,
|
username: typeof options.username === 'string' ? options.username : undefined,
|
||||||
password: typeof options.password === 'string' ? options.password : undefined,
|
password: typeof options.password === 'string' ? options.password : undefined,
|
||||||
passwordStore: typeof options.passwordStore === 'string' ? options.passwordStore : undefined,
|
passwordStore:
|
||||||
|
typeof options.passwordStore === 'string' ? options.passwordStore : undefined,
|
||||||
logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined,
|
logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,10 +22,14 @@ function withTempDir<T>(fn: (dir: string) => T): T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function runLauncher(argv: string[], env: NodeJS.ProcessEnv): RunResult {
|
function runLauncher(argv: string[], env: NodeJS.ProcessEnv): RunResult {
|
||||||
const result = spawnSync(process.execPath, ['run', path.join(process.cwd(), 'launcher/main.ts'), ...argv], {
|
const result = spawnSync(
|
||||||
|
process.execPath,
|
||||||
|
['run', path.join(process.cwd(), 'launcher/main.ts'), ...argv],
|
||||||
|
{
|
||||||
env,
|
env,
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
});
|
},
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
status: result.status,
|
status: result.status,
|
||||||
stdout: result.stdout || '',
|
stdout: result.stdout || '',
|
||||||
@@ -225,10 +229,7 @@ test('jellyfin setup forwards password-store to app command', () => {
|
|||||||
SUBMINER_APPIMAGE_PATH: appPath,
|
SUBMINER_APPIMAGE_PATH: appPath,
|
||||||
SUBMINER_TEST_CAPTURE: capturePath,
|
SUBMINER_TEST_CAPTURE: capturePath,
|
||||||
};
|
};
|
||||||
const result = runLauncher(
|
const result = runLauncher(['jf', 'setup', '--password-store', 'gnome-libsecret'], env);
|
||||||
['jf', 'setup', '--password-store', 'gnome-libsecret'],
|
|
||||||
env,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.equal(result.status, 0);
|
assert.equal(result.status, 0);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
|
|||||||
@@ -475,8 +475,7 @@ export function startMpv(
|
|||||||
if (preloadedSubtitles?.secondaryPath) {
|
if (preloadedSubtitles?.secondaryPath) {
|
||||||
mpvArgs.push(`--sub-file=${preloadedSubtitles.secondaryPath}`);
|
mpvArgs.push(`--sub-file=${preloadedSubtitles.secondaryPath}`);
|
||||||
}
|
}
|
||||||
const aniSkipMetadata =
|
const aniSkipMetadata = targetKind === 'file' ? inferAniSkipMetadataForFile(target) : null;
|
||||||
targetKind === 'file' ? inferAniSkipMetadataForFile(target) : null;
|
|
||||||
const scriptOpts = buildSubminerScriptOpts(appPath, socketPath, aniSkipMetadata);
|
const scriptOpts = buildSubminerScriptOpts(appPath, socketPath, aniSkipMetadata);
|
||||||
if (aniSkipMetadata) {
|
if (aniSkipMetadata) {
|
||||||
log(
|
log(
|
||||||
|
|||||||
@@ -31,11 +31,7 @@ test('parseArgs maps jellyfin play action and log-level override', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('parseArgs forwards jellyfin password-store option', () => {
|
test('parseArgs forwards jellyfin password-store option', () => {
|
||||||
const parsed = parseArgs(
|
const parsed = parseArgs(['jf', 'setup', '--password-store', 'gnome-libsecret'], 'subminer', {});
|
||||||
['jf', 'setup', '--password-store', 'gnome-libsecret'],
|
|
||||||
'subminer',
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.equal(parsed.jellyfin, true);
|
assert.equal(parsed.jellyfin, true);
|
||||||
assert.equal(parsed.passwordStore, 'gnome-libsecret');
|
assert.equal(parsed.passwordStore, 'gnome-libsecret');
|
||||||
|
|||||||
@@ -239,7 +239,8 @@ export class AnkiIntegration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createProxyServer(): AnkiConnectProxyServer {
|
private createProxyServer(): AnkiConnectProxyServer {
|
||||||
const { AnkiConnectProxyServer } = require('./anki-integration/anki-connect-proxy') as typeof import('./anki-integration/anki-connect-proxy');
|
const { AnkiConnectProxyServer } =
|
||||||
|
require('./anki-integration/anki-connect-proxy') as typeof import('./anki-integration/anki-connect-proxy');
|
||||||
return new AnkiConnectProxyServer({
|
return new AnkiConnectProxyServer({
|
||||||
shouldAutoUpdateNewCards: () => this.config.behavior?.autoUpdateNewCards !== false,
|
shouldAutoUpdateNewCards: () => this.config.behavior?.autoUpdateNewCards !== false,
|
||||||
processNewCard: (noteId: number) => this.processNewCard(noteId),
|
processNewCard: (noteId: number) => this.processNewCard(noteId),
|
||||||
|
|||||||
@@ -27,9 +27,11 @@ test('proxy enqueues addNote result for enrichment', async () => {
|
|||||||
logError: () => undefined,
|
logError: () => undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
(proxy as unknown as {
|
(
|
||||||
|
proxy as unknown as {
|
||||||
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
|
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
|
||||||
}).maybeEnqueueFromRequest(
|
}
|
||||||
|
).maybeEnqueueFromRequest(
|
||||||
{ action: 'addNote' },
|
{ action: 'addNote' },
|
||||||
Buffer.from(JSON.stringify({ result: 42, error: null }), 'utf8'),
|
Buffer.from(JSON.stringify({ result: 42, error: null }), 'utf8'),
|
||||||
);
|
);
|
||||||
@@ -50,9 +52,11 @@ test('proxy enqueues addNote bare numeric response for enrichment', async () =>
|
|||||||
logError: () => undefined,
|
logError: () => undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
(proxy as unknown as {
|
(
|
||||||
|
proxy as unknown as {
|
||||||
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
|
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
|
||||||
}).maybeEnqueueFromRequest({ action: 'addNote' }, Buffer.from('42', 'utf8'));
|
}
|
||||||
|
).maybeEnqueueFromRequest({ action: 'addNote' }, Buffer.from('42', 'utf8'));
|
||||||
|
|
||||||
await waitForCondition(() => processed.length === 1);
|
await waitForCondition(() => processed.length === 1);
|
||||||
assert.deepEqual(processed, [42]);
|
assert.deepEqual(processed, [42]);
|
||||||
@@ -71,9 +75,11 @@ test('proxy de-duplicates addNotes IDs within the same response', async () => {
|
|||||||
logError: () => undefined,
|
logError: () => undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
(proxy as unknown as {
|
(
|
||||||
|
proxy as unknown as {
|
||||||
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
|
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
|
||||||
}).maybeEnqueueFromRequest(
|
}
|
||||||
|
).maybeEnqueueFromRequest(
|
||||||
{ action: 'addNotes' },
|
{ action: 'addNotes' },
|
||||||
Buffer.from(JSON.stringify({ result: [101, 102, 101, null], error: null }), 'utf8'),
|
Buffer.from(JSON.stringify({ result: [101, 102, 101, null], error: null }), 'utf8'),
|
||||||
);
|
);
|
||||||
@@ -94,17 +100,15 @@ test('proxy enqueues note IDs from multi action addNote/addNotes results', async
|
|||||||
logError: () => undefined,
|
logError: () => undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
(proxy as unknown as {
|
(
|
||||||
|
proxy as unknown as {
|
||||||
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
|
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
|
||||||
}).maybeEnqueueFromRequest(
|
}
|
||||||
|
).maybeEnqueueFromRequest(
|
||||||
{
|
{
|
||||||
action: 'multi',
|
action: 'multi',
|
||||||
params: {
|
params: {
|
||||||
actions: [
|
actions: [{ action: 'version' }, { action: 'addNote' }, { action: 'addNotes' }],
|
||||||
{ action: 'version' },
|
|
||||||
{ action: 'addNote' },
|
|
||||||
{ action: 'addNotes' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Buffer.from(JSON.stringify({ result: [6, 777, [888, 777, null]], error: null }), 'utf8'),
|
Buffer.from(JSON.stringify({ result: [6, 777, [888, 777, null]], error: null }), 'utf8'),
|
||||||
@@ -126,9 +130,11 @@ test('proxy enqueues note IDs from bare multi action results', async () => {
|
|||||||
logError: () => undefined,
|
logError: () => undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
(proxy as unknown as {
|
(
|
||||||
|
proxy as unknown as {
|
||||||
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
|
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
|
||||||
}).maybeEnqueueFromRequest(
|
}
|
||||||
|
).maybeEnqueueFromRequest(
|
||||||
{
|
{
|
||||||
action: 'multi',
|
action: 'multi',
|
||||||
params: {
|
params: {
|
||||||
@@ -154,17 +160,15 @@ test('proxy enqueues note IDs from multi action envelope results', async () => {
|
|||||||
logError: () => undefined,
|
logError: () => undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
(proxy as unknown as {
|
(
|
||||||
|
proxy as unknown as {
|
||||||
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
|
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
|
||||||
}).maybeEnqueueFromRequest(
|
}
|
||||||
|
).maybeEnqueueFromRequest(
|
||||||
{
|
{
|
||||||
action: 'multi',
|
action: 'multi',
|
||||||
params: {
|
params: {
|
||||||
actions: [
|
actions: [{ action: 'version' }, { action: 'addNote' }, { action: 'addNotes' }],
|
||||||
{ action: 'version' },
|
|
||||||
{ action: 'addNote' },
|
|
||||||
{ action: 'addNotes' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Buffer.from(
|
Buffer.from(
|
||||||
@@ -196,9 +200,11 @@ test('proxy skips auto-enrichment when auto-update is disabled', async () => {
|
|||||||
logError: () => undefined,
|
logError: () => undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
(proxy as unknown as {
|
(
|
||||||
|
proxy as unknown as {
|
||||||
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
|
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
|
||||||
}).maybeEnqueueFromRequest(
|
}
|
||||||
|
).maybeEnqueueFromRequest(
|
||||||
{ action: 'addNote' },
|
{ action: 'addNote' },
|
||||||
Buffer.from(JSON.stringify({ result: 303, error: null }), 'utf8'),
|
Buffer.from(JSON.stringify({ result: 303, error: null }), 'utf8'),
|
||||||
);
|
);
|
||||||
@@ -219,9 +225,11 @@ test('proxy ignores addNote when upstream response reports error', async () => {
|
|||||||
logError: () => undefined,
|
logError: () => undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
(proxy as unknown as {
|
(
|
||||||
|
proxy as unknown as {
|
||||||
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
|
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
|
||||||
}).maybeEnqueueFromRequest(
|
}
|
||||||
|
).maybeEnqueueFromRequest(
|
||||||
{ action: 'addNote' },
|
{ action: 'addNote' },
|
||||||
Buffer.from(JSON.stringify({ result: 123, error: 'duplicate' }), 'utf8'),
|
Buffer.from(JSON.stringify({ result: 123, error: 'duplicate' }), 'utf8'),
|
||||||
);
|
);
|
||||||
@@ -248,9 +256,11 @@ test('proxy does not fallback-enqueue latest note for multi requests without add
|
|||||||
logError: () => undefined,
|
logError: () => undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
(proxy as unknown as {
|
(
|
||||||
|
proxy as unknown as {
|
||||||
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
|
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
|
||||||
}).maybeEnqueueFromRequest(
|
}
|
||||||
|
).maybeEnqueueFromRequest(
|
||||||
{
|
{
|
||||||
action: 'multi',
|
action: 'multi',
|
||||||
params: {
|
params: {
|
||||||
@@ -283,9 +293,11 @@ test('proxy fallback-enqueues latest note for addNote responses without note IDs
|
|||||||
logError: () => undefined,
|
logError: () => undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
(proxy as unknown as {
|
(
|
||||||
|
proxy as unknown as {
|
||||||
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
|
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
|
||||||
}).maybeEnqueueFromRequest(
|
}
|
||||||
|
).maybeEnqueueFromRequest(
|
||||||
{ action: 'addNote' },
|
{ action: 'addNote' },
|
||||||
Buffer.from(JSON.stringify({ result: 0, error: null }), 'utf8'),
|
Buffer.from(JSON.stringify({ result: 0, error: null }), 'utf8'),
|
||||||
);
|
);
|
||||||
@@ -304,13 +316,15 @@ test('proxy detects self-referential loop configuration', () => {
|
|||||||
logError: () => undefined,
|
logError: () => undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = (proxy as unknown as {
|
const result = (
|
||||||
|
proxy as unknown as {
|
||||||
isSelfReferentialProxy: (options: {
|
isSelfReferentialProxy: (options: {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
upstreamUrl: string;
|
upstreamUrl: string;
|
||||||
}) => boolean;
|
}) => boolean;
|
||||||
}).isSelfReferentialProxy({
|
}
|
||||||
|
).isSelfReferentialProxy({
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 8766,
|
port: 8766,
|
||||||
upstreamUrl: 'http://localhost:8766',
|
upstreamUrl: 'http://localhost:8766',
|
||||||
|
|||||||
@@ -175,7 +175,9 @@ export class AnkiConnectProxyServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const action =
|
const action =
|
||||||
typeof requestJson.action === 'string' ? requestJson.action : String(requestJson.action ?? '');
|
typeof requestJson.action === 'string'
|
||||||
|
? requestJson.action
|
||||||
|
: String(requestJson.action ?? '');
|
||||||
if (action !== 'addNote' && action !== 'addNotes' && action !== 'multi') {
|
if (action !== 'addNote' && action !== 'addNotes' && action !== 'multi') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,11 @@ test('findDuplicateNote checks both source expression/word values when both fiel
|
|||||||
if (query.includes('昨日は雨だった。')) {
|
if (query.includes('昨日は雨だった。')) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
if (query.includes('"Word:雨"') || query.includes('"word:雨"') || query.includes('"Expression:雨"')) {
|
if (
|
||||||
|
query.includes('"Word:雨"') ||
|
||||||
|
query.includes('"word:雨"') ||
|
||||||
|
query.includes('"Expression:雨"')
|
||||||
|
) {
|
||||||
return [200];
|
return [200];
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -32,9 +32,7 @@ export async function findDuplicateNote(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const deckValue = deps.getDeck();
|
const deckValue = deps.getDeck();
|
||||||
const queryPrefixes = deckValue
|
const queryPrefixes = deckValue ? [`"deck:${escapeAnkiSearchValue(deckValue)}" `, ''] : [''];
|
||||||
? [`"deck:${escapeAnkiSearchValue(deckValue)}" `, '']
|
|
||||||
: [''];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const noteIds = new Set<number>();
|
const noteIds = new Set<number>();
|
||||||
|
|||||||
@@ -112,12 +112,7 @@ export class FieldGroupingWorkflow {
|
|||||||
const keepNoteId = choice.keepNoteId;
|
const keepNoteId = choice.keepNoteId;
|
||||||
const deleteNoteId = choice.deleteNoteId;
|
const deleteNoteId = choice.deleteNoteId;
|
||||||
|
|
||||||
await this.performMerge(
|
await this.performMerge(keepNoteId, deleteNoteId, expression, choice.deleteDuplicate);
|
||||||
keepNoteId,
|
|
||||||
deleteNoteId,
|
|
||||||
expression,
|
|
||||||
choice.deleteDuplicate,
|
|
||||||
);
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.deps.logError('Field grouping manual merge failed:', (error as Error).message);
|
this.deps.logError('Field grouping manual merge failed:', (error as Error).message);
|
||||||
|
|||||||
@@ -51,18 +51,10 @@ function createWorkflowHarness() {
|
|||||||
return out;
|
return out;
|
||||||
},
|
},
|
||||||
findDuplicateNote: async (_expression, _excludeNoteId, _noteInfo) => null,
|
findDuplicateNote: async (_expression, _excludeNoteId, _noteInfo) => null,
|
||||||
handleFieldGroupingAuto: async (
|
handleFieldGroupingAuto: async (_originalNoteId, _newNoteId, _newNoteInfo, _expression) =>
|
||||||
_originalNoteId,
|
undefined,
|
||||||
_newNoteId,
|
handleFieldGroupingManual: async (_originalNoteId, _newNoteId, _newNoteInfo, _expression) =>
|
||||||
_newNoteInfo,
|
false,
|
||||||
_expression,
|
|
||||||
) => undefined,
|
|
||||||
handleFieldGroupingManual: async (
|
|
||||||
_originalNoteId,
|
|
||||||
_newNoteId,
|
|
||||||
_newNoteInfo,
|
|
||||||
_expression,
|
|
||||||
) => false,
|
|
||||||
processSentence: (text: string, _noteFields: Record<string, string>) => text,
|
processSentence: (text: string, _noteFields: Record<string, string>) => text,
|
||||||
resolveConfiguredFieldName: (noteInfo: NoteUpdateWorkflowNoteInfo, preferred?: string) => {
|
resolveConfiguredFieldName: (noteInfo: NoteUpdateWorkflowNoteInfo, preferred?: string) => {
|
||||||
if (!preferred) return null;
|
if (!preferred) return null;
|
||||||
|
|||||||
@@ -748,6 +748,9 @@ test('runtime options registry is centralized', () => {
|
|||||||
const ids = RUNTIME_OPTION_REGISTRY.map((entry) => entry.id);
|
const ids = RUNTIME_OPTION_REGISTRY.map((entry) => entry.id);
|
||||||
assert.deepEqual(ids, [
|
assert.deepEqual(ids, [
|
||||||
'anki.autoUpdateNewCards',
|
'anki.autoUpdateNewCards',
|
||||||
|
'subtitle.annotation.nPlusOne',
|
||||||
|
'subtitle.annotation.jlpt',
|
||||||
|
'subtitle.annotation.frequency',
|
||||||
'anki.nPlusOneMatchMode',
|
'anki.nPlusOneMatchMode',
|
||||||
'anki.kikuFieldGrouping',
|
'anki.kikuFieldGrouping',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ test('domain registry builders each contribute entries to composed registry', ()
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('default keybindings include primary and secondary subtitle track cycling on J keys', () => {
|
test('default keybindings include primary and secondary subtitle track cycling on J keys', () => {
|
||||||
const keybindingMap = new Map(DEFAULT_KEYBINDINGS.map((binding) => [binding.key, binding.command]));
|
const keybindingMap = new Map(
|
||||||
|
DEFAULT_KEYBINDINGS.map((binding) => [binding.key, binding.command]),
|
||||||
|
);
|
||||||
assert.deepEqual(keybindingMap.get('KeyJ'), ['cycle', 'sid']);
|
assert.deepEqual(keybindingMap.get('KeyJ'), ['cycle', 'sid']);
|
||||||
assert.deepEqual(keybindingMap.get('Shift+KeyJ'), ['cycle', 'secondary-sid']);
|
assert.deepEqual(keybindingMap.get('Shift+KeyJ'), ['cycle', 'secondary-sid']);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ export function buildIntegrationConfigOptionRegistry(
|
|||||||
defaultConfig: ResolvedConfig,
|
defaultConfig: ResolvedConfig,
|
||||||
runtimeOptionRegistry: RuntimeOptionRegistryEntry[],
|
runtimeOptionRegistry: RuntimeOptionRegistryEntry[],
|
||||||
): ConfigOptionRegistryEntry[] {
|
): ConfigOptionRegistryEntry[] {
|
||||||
|
const runtimeOptionById = new Map(runtimeOptionRegistry.map((entry) => [entry.id, entry]));
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
path: 'ankiConnect.enabled',
|
path: 'ankiConnect.enabled',
|
||||||
@@ -54,7 +56,7 @@ export function buildIntegrationConfigOptionRegistry(
|
|||||||
kind: 'boolean',
|
kind: 'boolean',
|
||||||
defaultValue: defaultConfig.ankiConnect.behavior.autoUpdateNewCards,
|
defaultValue: defaultConfig.ankiConnect.behavior.autoUpdateNewCards,
|
||||||
description: 'Automatically update newly added cards.',
|
description: 'Automatically update newly added cards.',
|
||||||
runtime: runtimeOptionRegistry[0],
|
runtime: runtimeOptionById.get('anki.autoUpdateNewCards'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'ankiConnect.nPlusOne.matchMode',
|
path: 'ankiConnect.nPlusOne.matchMode',
|
||||||
@@ -105,7 +107,7 @@ export function buildIntegrationConfigOptionRegistry(
|
|||||||
enumValues: ['auto', 'manual', 'disabled'],
|
enumValues: ['auto', 'manual', 'disabled'],
|
||||||
defaultValue: defaultConfig.ankiConnect.isKiku.fieldGrouping,
|
defaultValue: defaultConfig.ankiConnect.isKiku.fieldGrouping,
|
||||||
description: 'Kiku duplicate-card field grouping mode.',
|
description: 'Kiku duplicate-card field grouping mode.',
|
||||||
runtime: runtimeOptionRegistry[1],
|
runtime: runtimeOptionById.get('anki.kikuFieldGrouping'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'jimaku.languagePreference',
|
path: 'jimaku.languagePreference',
|
||||||
|
|||||||
@@ -19,6 +19,42 @@ export function buildRuntimeOptionRegistry(
|
|||||||
behavior: { autoUpdateNewCards: value === true },
|
behavior: { autoUpdateNewCards: value === true },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'subtitle.annotation.nPlusOne',
|
||||||
|
path: 'ankiConnect.nPlusOne.highlightEnabled',
|
||||||
|
label: 'N+1 Annotation',
|
||||||
|
scope: 'subtitle',
|
||||||
|
valueType: 'boolean',
|
||||||
|
allowedValues: [true, false],
|
||||||
|
defaultValue: defaultConfig.ankiConnect.nPlusOne.highlightEnabled,
|
||||||
|
requiresRestart: false,
|
||||||
|
formatValueForOsd: (value) => (value === true ? 'On' : 'Off'),
|
||||||
|
toAnkiPatch: () => ({}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'subtitle.annotation.jlpt',
|
||||||
|
path: 'subtitleStyle.enableJlpt',
|
||||||
|
label: 'JLPT Annotation',
|
||||||
|
scope: 'subtitle',
|
||||||
|
valueType: 'boolean',
|
||||||
|
allowedValues: [true, false],
|
||||||
|
defaultValue: defaultConfig.subtitleStyle.enableJlpt,
|
||||||
|
requiresRestart: false,
|
||||||
|
formatValueForOsd: (value) => (value === true ? 'On' : 'Off'),
|
||||||
|
toAnkiPatch: () => ({}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'subtitle.annotation.frequency',
|
||||||
|
path: 'subtitleStyle.frequencyDictionary.enabled',
|
||||||
|
label: 'Frequency Annotation',
|
||||||
|
scope: 'subtitle',
|
||||||
|
valueType: 'boolean',
|
||||||
|
allowedValues: [true, false],
|
||||||
|
defaultValue: defaultConfig.subtitleStyle.frequencyDictionary.enabled,
|
||||||
|
requiresRestart: false,
|
||||||
|
formatValueForOsd: (value) => (value === true ? 'On' : 'Off'),
|
||||||
|
toAnkiPatch: () => ({}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'anki.nPlusOneMatchMode',
|
id: 'anki.nPlusOneMatchMode',
|
||||||
path: 'ankiConnect.nPlusOne.matchMode',
|
path: 'ankiConnect.nPlusOne.matchMode',
|
||||||
|
|||||||
@@ -111,7 +111,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
|||||||
frequencyDictionary: {
|
frequencyDictionary: {
|
||||||
...resolved.subtitleStyle.frequencyDictionary,
|
...resolved.subtitleStyle.frequencyDictionary,
|
||||||
...(isObject((src.subtitleStyle as { frequencyDictionary?: unknown }).frequencyDictionary)
|
...(isObject((src.subtitleStyle as { frequencyDictionary?: unknown }).frequencyDictionary)
|
||||||
? ((src.subtitleStyle as { frequencyDictionary?: unknown }).frequencyDictionary as ResolvedConfig['subtitleStyle']['frequencyDictionary'])
|
? ((src.subtitleStyle as { frequencyDictionary?: unknown })
|
||||||
|
.frequencyDictionary as ResolvedConfig['subtitleStyle']['frequencyDictionary'])
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
@@ -152,7 +153,9 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hoverTokenColor = asColor((src.subtitleStyle as { hoverTokenColor?: unknown }).hoverTokenColor);
|
const hoverTokenColor = asColor(
|
||||||
|
(src.subtitleStyle as { hoverTokenColor?: unknown }).hoverTokenColor,
|
||||||
|
);
|
||||||
if (hoverTokenColor !== undefined) {
|
if (hoverTokenColor !== undefined) {
|
||||||
resolved.subtitleStyle.hoverTokenColor = hoverTokenColor;
|
resolved.subtitleStyle.hoverTokenColor = hoverTokenColor;
|
||||||
} else if ((src.subtitleStyle as { hoverTokenColor?: unknown }).hoverTokenColor !== undefined) {
|
} else if ((src.subtitleStyle as { hoverTokenColor?: unknown }).hoverTokenColor !== undefined) {
|
||||||
@@ -174,7 +177,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
|||||||
(src.subtitleStyle as { hoverTokenBackgroundColor?: unknown }).hoverTokenBackgroundColor !==
|
(src.subtitleStyle as { hoverTokenBackgroundColor?: unknown }).hoverTokenBackgroundColor !==
|
||||||
undefined
|
undefined
|
||||||
) {
|
) {
|
||||||
resolved.subtitleStyle.hoverTokenBackgroundColor = fallbackSubtitleStyleHoverTokenBackgroundColor;
|
resolved.subtitleStyle.hoverTokenBackgroundColor =
|
||||||
|
fallbackSubtitleStyleHoverTokenBackgroundColor;
|
||||||
warn(
|
warn(
|
||||||
'subtitleStyle.hoverTokenBackgroundColor',
|
'subtitleStyle.hoverTokenBackgroundColor',
|
||||||
(src.subtitleStyle as { hoverTokenBackgroundColor?: unknown }).hoverTokenBackgroundColor,
|
(src.subtitleStyle as { hoverTokenBackgroundColor?: unknown }).hoverTokenBackgroundColor,
|
||||||
@@ -208,7 +212,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
|||||||
if (sourcePath !== undefined) {
|
if (sourcePath !== undefined) {
|
||||||
resolved.subtitleStyle.frequencyDictionary.sourcePath = sourcePath;
|
resolved.subtitleStyle.frequencyDictionary.sourcePath = sourcePath;
|
||||||
} else if ((frequencyDictionary as { sourcePath?: unknown }).sourcePath !== undefined) {
|
} else if ((frequencyDictionary as { sourcePath?: unknown }).sourcePath !== undefined) {
|
||||||
resolved.subtitleStyle.frequencyDictionary.sourcePath = fallbackFrequencyDictionary.sourcePath;
|
resolved.subtitleStyle.frequencyDictionary.sourcePath =
|
||||||
|
fallbackFrequencyDictionary.sourcePath;
|
||||||
warn(
|
warn(
|
||||||
'subtitleStyle.frequencyDictionary.sourcePath',
|
'subtitleStyle.frequencyDictionary.sourcePath',
|
||||||
(frequencyDictionary as { sourcePath?: unknown }).sourcePath,
|
(frequencyDictionary as { sourcePath?: unknown }).sourcePath,
|
||||||
@@ -260,7 +265,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
|||||||
if (singleColor !== undefined) {
|
if (singleColor !== undefined) {
|
||||||
resolved.subtitleStyle.frequencyDictionary.singleColor = singleColor;
|
resolved.subtitleStyle.frequencyDictionary.singleColor = singleColor;
|
||||||
} else if ((frequencyDictionary as { singleColor?: unknown }).singleColor !== undefined) {
|
} else if ((frequencyDictionary as { singleColor?: unknown }).singleColor !== undefined) {
|
||||||
resolved.subtitleStyle.frequencyDictionary.singleColor = fallbackFrequencyDictionary.singleColor;
|
resolved.subtitleStyle.frequencyDictionary.singleColor =
|
||||||
|
fallbackFrequencyDictionary.singleColor;
|
||||||
warn(
|
warn(
|
||||||
'subtitleStyle.frequencyDictionary.singleColor',
|
'subtitleStyle.frequencyDictionary.singleColor',
|
||||||
(frequencyDictionary as { singleColor?: unknown }).singleColor,
|
(frequencyDictionary as { singleColor?: unknown }).singleColor,
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ function humanizeKey(key: string): string {
|
|||||||
|
|
||||||
function buildInlineOptionComment(path: string, value: unknown): string {
|
function buildInlineOptionComment(path: string, value: unknown): string {
|
||||||
const registryEntry = OPTION_REGISTRY_BY_PATH.get(path);
|
const registryEntry = OPTION_REGISTRY_BY_PATH.get(path);
|
||||||
const baseDescription = registryEntry?.description ?? TOP_LEVEL_SECTION_DESCRIPTION_BY_KEY.get(path);
|
const baseDescription =
|
||||||
|
registryEntry?.description ?? TOP_LEVEL_SECTION_DESCRIPTION_BY_KEY.get(path);
|
||||||
const description =
|
const description =
|
||||||
baseDescription && baseDescription.trim().length > 0
|
baseDescription && baseDescription.trim().length > 0
|
||||||
? normalizeCommentText(baseDescription)
|
? normalizeCommentText(baseDescription)
|
||||||
|
|||||||
@@ -132,16 +132,15 @@ export function createAnilistTokenStore(
|
|||||||
}
|
}
|
||||||
return decrypted;
|
return decrypted;
|
||||||
}
|
}
|
||||||
if (
|
if (typeof parsed.plaintextToken === 'string' && parsed.plaintextToken.trim().length > 0) {
|
||||||
typeof parsed.plaintextToken === 'string' &&
|
|
||||||
parsed.plaintextToken.trim().length > 0
|
|
||||||
) {
|
|
||||||
if (storage.isEncryptionAvailable()) {
|
if (storage.isEncryptionAvailable()) {
|
||||||
if (!isSafeStorageUsable()) {
|
if (!isSafeStorageUsable()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const plaintext = parsed.plaintextToken.trim();
|
const plaintext = parsed.plaintextToken.trim();
|
||||||
notifyUser('AniList token plaintext fallback payload found. Migrating to encrypted storage.');
|
notifyUser(
|
||||||
|
'AniList token plaintext fallback payload found. Migrating to encrypted storage.',
|
||||||
|
);
|
||||||
this.saveToken(plaintext);
|
this.saveToken(plaintext);
|
||||||
return plaintext;
|
return plaintext;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -283,8 +283,7 @@ export function handleCliCommand(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldStart =
|
const shouldStart = args.start || args.toggle || args.toggleVisibleOverlay;
|
||||||
args.start || args.toggle || args.toggleVisibleOverlay;
|
|
||||||
const needsOverlayRuntime = commandNeedsOverlayRuntime(args);
|
const needsOverlayRuntime = commandNeedsOverlayRuntime(args);
|
||||||
const shouldInitializeOverlayRuntime = needsOverlayRuntime || args.start;
|
const shouldInitializeOverlayRuntime = needsOverlayRuntime || args.start;
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ test('createFrequencyDictionaryLookup aggregates duplicate-term logs into a sing
|
|||||||
|
|
||||||
assert.equal(lookup('猫'), 100);
|
assert.equal(lookup('猫'), 100);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
logs.filter((entry) => entry.includes('Frequency dictionary ignored 2 duplicate term entries')).length,
|
logs.filter((entry) => entry.includes('Frequency dictionary ignored 2 duplicate term entries'))
|
||||||
|
.length,
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function parsePositiveFrequencyString(value: string): number | null {
|
|||||||
const chunks = numericPrefix.split(',');
|
const chunks = numericPrefix.split(',');
|
||||||
const normalizedNumber =
|
const normalizedNumber =
|
||||||
chunks.length <= 1
|
chunks.length <= 1
|
||||||
? chunks[0] ?? ''
|
? (chunks[0] ?? '')
|
||||||
: chunks.slice(1).every((chunk) => /^\d{3}$/.test(chunk))
|
: chunks.slice(1).every((chunk) => /^\d{3}$/.test(chunk))
|
||||||
? chunks.join('')
|
? chunks.join('')
|
||||||
: (chunks[0] ?? '');
|
: (chunks[0] ?? '');
|
||||||
|
|||||||
@@ -62,10 +62,7 @@ export {
|
|||||||
updateOverlayWindowBounds,
|
updateOverlayWindowBounds,
|
||||||
} from './overlay-window';
|
} from './overlay-window';
|
||||||
export { initializeOverlayRuntime } from './overlay-runtime-init';
|
export { initializeOverlayRuntime } from './overlay-runtime-init';
|
||||||
export {
|
export { setVisibleOverlayVisible, updateVisibleOverlayVisibility } from './overlay-visibility';
|
||||||
setVisibleOverlayVisible,
|
|
||||||
updateVisibleOverlayVisibility,
|
|
||||||
} from './overlay-visibility';
|
|
||||||
export {
|
export {
|
||||||
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
||||||
MpvIpcClient,
|
MpvIpcClient,
|
||||||
|
|||||||
@@ -150,7 +150,12 @@ test('registerIpcHandlers rejects malformed runtime-option payloads', async () =
|
|||||||
});
|
});
|
||||||
const validResult = await setHandler!({}, 'anki.autoUpdateNewCards', true);
|
const validResult = await setHandler!({}, 'anki.autoUpdateNewCards', true);
|
||||||
assert.deepEqual(validResult, { ok: true });
|
assert.deepEqual(validResult, { ok: true });
|
||||||
assert.deepEqual(calls, [{ id: 'anki.autoUpdateNewCards', value: true }]);
|
const validSubtitleAnnotationResult = await setHandler!({}, 'subtitle.annotation.jlpt', false);
|
||||||
|
assert.deepEqual(validSubtitleAnnotationResult, { ok: true });
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
{ id: 'anki.autoUpdateNewCards', value: true },
|
||||||
|
{ id: 'subtitle.annotation.jlpt', value: false },
|
||||||
|
]);
|
||||||
|
|
||||||
const cycleHandler = handlers.handle.get(IPC_CHANNELS.request.cycleRuntimeOption);
|
const cycleHandler = handlers.handle.get(IPC_CHANNELS.request.cycleRuntimeOption);
|
||||||
assert.ok(cycleHandler);
|
assert.ok(cycleHandler);
|
||||||
@@ -215,9 +220,7 @@ test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => {
|
|||||||
|
|
||||||
handlers.on.get(IPC_CHANNELS.command.saveSubtitlePosition)!({}, { yPercent: 'bad' });
|
handlers.on.get(IPC_CHANNELS.command.saveSubtitlePosition)!({}, { yPercent: 'bad' });
|
||||||
handlers.on.get(IPC_CHANNELS.command.saveSubtitlePosition)!({}, { yPercent: 42 });
|
handlers.on.get(IPC_CHANNELS.command.saveSubtitlePosition)!({}, { yPercent: 42 });
|
||||||
assert.deepEqual(saves, [
|
assert.deepEqual(saves, [{ yPercent: 42 }]);
|
||||||
{ yPercent: 42 },
|
|
||||||
]);
|
|
||||||
|
|
||||||
handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'not-a-modal');
|
handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'not-a-modal');
|
||||||
handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'subsync');
|
handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'subsync');
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ export function createJellyfinTokenStore(
|
|||||||
}
|
}
|
||||||
const decrypted = safeStorage.decryptString(encrypted).trim();
|
const decrypted = safeStorage.decryptString(encrypted).trim();
|
||||||
const session = JSON.parse(decrypted) as Partial<JellyfinStoredSession>;
|
const session = JSON.parse(decrypted) as Partial<JellyfinStoredSession>;
|
||||||
const accessToken = typeof session.accessToken === 'string' ? session.accessToken.trim() : '';
|
const accessToken =
|
||||||
|
typeof session.accessToken === 'string' ? session.accessToken.trim() : '';
|
||||||
const userId = typeof session.userId === 'string' ? session.userId.trim() : '';
|
const userId = typeof session.userId === 'string' ? session.userId.trim() : '';
|
||||||
if (!accessToken || !userId) return null;
|
if (!accessToken || !userId) return null;
|
||||||
return { accessToken, userId };
|
return { accessToken, userId };
|
||||||
@@ -88,7 +89,9 @@ export function createJellyfinTokenStore(
|
|||||||
(typeof parsed.encryptedToken === 'string' && parsed.encryptedToken.length > 0) ||
|
(typeof parsed.encryptedToken === 'string' && parsed.encryptedToken.length > 0) ||
|
||||||
(typeof parsed.plaintextToken === 'string' && parsed.plaintextToken.trim().length > 0)
|
(typeof parsed.plaintextToken === 'string' && parsed.plaintextToken.trim().length > 0)
|
||||||
) {
|
) {
|
||||||
logger.warn('Ignoring legacy Jellyfin token-only store payload because userId is missing.');
|
logger.warn(
|
||||||
|
'Ignoring legacy Jellyfin token-only store payload because userId is missing.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to read Jellyfin session store.', error);
|
logger.error('Failed to read Jellyfin session store.', error);
|
||||||
|
|||||||
@@ -34,10 +34,7 @@ export function sendToVisibleOverlayRuntime<T extends string>(options: {
|
|||||||
|
|
||||||
const isLoading = typeof webContents.isLoading === 'function' ? webContents.isLoading() : false;
|
const isLoading = typeof webContents.isLoading === 'function' ? webContents.isLoading() : false;
|
||||||
const currentURL = typeof webContents.getURL === 'function' ? webContents.getURL() : '';
|
const currentURL = typeof webContents.getURL === 'function' ? webContents.getURL() : '';
|
||||||
const isReady =
|
const isReady = !isLoading && currentURL !== '' && currentURL !== 'about:blank';
|
||||||
!isLoading &&
|
|
||||||
currentURL !== '' &&
|
|
||||||
currentURL !== 'about:blank';
|
|
||||||
|
|
||||||
if (!isReady) {
|
if (!isReady) {
|
||||||
if (typeof webContents.once !== 'function') {
|
if (typeof webContents.once !== 'function') {
|
||||||
|
|||||||
@@ -85,7 +85,9 @@ export function parseClipboardVideoPath(text: string): string | null {
|
|||||||
return isSupportedVideoPath(unquoted) ? unquoted : null;
|
return isSupportedVideoPath(unquoted) ? unquoted : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function collectDroppedVideoPaths(dataTransfer: DropDataTransferLike | null | undefined): string[] {
|
export function collectDroppedVideoPaths(
|
||||||
|
dataTransfer: DropDataTransferLike | null | undefined,
|
||||||
|
): string[] {
|
||||||
if (!dataTransfer) return [];
|
if (!dataTransfer) return [];
|
||||||
|
|
||||||
const out: string[] = [];
|
const out: string[] = [];
|
||||||
|
|||||||
@@ -117,13 +117,9 @@ test('runtime-option broadcast still uses expected channel', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
let state = false;
|
let state = false;
|
||||||
const changed = setOverlayDebugVisualizationEnabledRuntime(
|
const changed = setOverlayDebugVisualizationEnabledRuntime(state, true, (enabled) => {
|
||||||
state,
|
|
||||||
true,
|
|
||||||
(enabled) => {
|
|
||||||
state = enabled;
|
state = enabled;
|
||||||
},
|
});
|
||||||
);
|
|
||||||
assert.equal(changed, true);
|
assert.equal(changed, true);
|
||||||
assert.equal(state, true);
|
assert.equal(state, true);
|
||||||
assert.deepEqual(broadcasts, [['runtime-options:changed', []]]);
|
assert.deepEqual(broadcasts, [['runtime-options:changed', []]]);
|
||||||
|
|||||||
@@ -41,8 +41,7 @@ test('initializeOverlayRuntime skips Anki integration when ankiConnect.enabled i
|
|||||||
setIntegrationCalls += 1;
|
setIntegrationCalls += 1;
|
||||||
},
|
},
|
||||||
showDesktopNotification: () => {},
|
showDesktopNotification: () => {},
|
||||||
createFieldGroupingCallback: () =>
|
createFieldGroupingCallback: () => async () => ({
|
||||||
async () => ({
|
|
||||||
keepNoteId: 1,
|
keepNoteId: 1,
|
||||||
deleteNoteId: 2,
|
deleteNoteId: 2,
|
||||||
deleteDuplicate: false,
|
deleteDuplicate: false,
|
||||||
@@ -96,8 +95,7 @@ test('initializeOverlayRuntime starts Anki integration when ankiConnect.enabled
|
|||||||
setIntegrationCalls += 1;
|
setIntegrationCalls += 1;
|
||||||
},
|
},
|
||||||
showDesktopNotification: () => {},
|
showDesktopNotification: () => {},
|
||||||
createFieldGroupingCallback: () =>
|
createFieldGroupingCallback: () => async () => ({
|
||||||
async () => ({
|
|
||||||
keepNoteId: 3,
|
keepNoteId: 3,
|
||||||
deleteNoteId: 4,
|
deleteNoteId: 4,
|
||||||
deleteDuplicate: false,
|
deleteDuplicate: false,
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ type CreateAnkiIntegrationArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function createDefaultAnkiIntegration(args: CreateAnkiIntegrationArgs): AnkiIntegrationLike {
|
function createDefaultAnkiIntegration(args: CreateAnkiIntegrationArgs): AnkiIntegrationLike {
|
||||||
const { AnkiIntegration } = require('../../anki-integration') as typeof import('../../anki-integration');
|
const { AnkiIntegration } =
|
||||||
|
require('../../anki-integration') as typeof import('../../anki-integration');
|
||||||
return new AnkiIntegration(
|
return new AnkiIntegration(
|
||||||
args.config,
|
args.config,
|
||||||
args.subtitleTimingTracker as never,
|
args.subtitleTimingTracker as never,
|
||||||
@@ -76,7 +77,10 @@ export function initializeOverlayRuntime(options: {
|
|||||||
options.registerGlobalShortcuts();
|
options.registerGlobalShortcuts();
|
||||||
|
|
||||||
const createWindowTrackerHandler = options.createWindowTracker ?? createWindowTracker;
|
const createWindowTrackerHandler = options.createWindowTracker ?? createWindowTracker;
|
||||||
const windowTracker = createWindowTrackerHandler(options.backendOverride, options.getMpvSocketPath());
|
const windowTracker = createWindowTrackerHandler(
|
||||||
|
options.backendOverride,
|
||||||
|
options.getMpvSocketPath(),
|
||||||
|
);
|
||||||
options.setWindowTracker(windowTracker);
|
options.setWindowTracker(windowTracker);
|
||||||
if (windowTracker) {
|
if (windowTracker) {
|
||||||
windowTracker.onGeometryChange = (geometry: WindowGeometry) => {
|
windowTracker.onGeometryChange = (geometry: WindowGeometry) => {
|
||||||
|
|||||||
@@ -138,3 +138,35 @@ test('subtitle processing refresh can use explicit text override', async () => {
|
|||||||
|
|
||||||
assert.deepEqual(emitted, [{ text: 'initial', tokens: [] }]);
|
assert.deepEqual(emitted, [{ text: 'initial', tokens: [] }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('subtitle processing cache invalidation only affects future subtitle events', async () => {
|
||||||
|
const emitted: SubtitleData[] = [];
|
||||||
|
const callsByText = new Map<string, number>();
|
||||||
|
const controller = createSubtitleProcessingController({
|
||||||
|
tokenizeSubtitle: async (text) => {
|
||||||
|
callsByText.set(text, (callsByText.get(text) ?? 0) + 1);
|
||||||
|
return { text, tokens: [] };
|
||||||
|
},
|
||||||
|
emitSubtitle: (payload) => emitted.push(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.onSubtitleChange('same');
|
||||||
|
await flushMicrotasks();
|
||||||
|
controller.onSubtitleChange('other');
|
||||||
|
await flushMicrotasks();
|
||||||
|
controller.onSubtitleChange('same');
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
assert.equal(callsByText.get('same'), 1);
|
||||||
|
assert.equal(emitted.length, 3);
|
||||||
|
|
||||||
|
controller.invalidateTokenizationCache();
|
||||||
|
assert.equal(emitted.length, 3);
|
||||||
|
|
||||||
|
controller.onSubtitleChange('different');
|
||||||
|
await flushMicrotasks();
|
||||||
|
controller.onSubtitleChange('same');
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
assert.equal(callsByText.get('same'), 2);
|
||||||
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface SubtitleProcessingControllerDeps {
|
|||||||
export interface SubtitleProcessingController {
|
export interface SubtitleProcessingController {
|
||||||
onSubtitleChange: (text: string) => void;
|
onSubtitleChange: (text: string) => void;
|
||||||
refreshCurrentSubtitle: (textOverride?: string) => void;
|
refreshCurrentSubtitle: (textOverride?: string) => void;
|
||||||
|
invalidateTokenizationCache: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSubtitleProcessingController(
|
export function createSubtitleProcessingController(
|
||||||
@@ -126,5 +127,8 @@ export function createSubtitleProcessingController(
|
|||||||
refreshRequested = true;
|
refreshRequested = true;
|
||||||
processLatest();
|
processLatest();
|
||||||
},
|
},
|
||||||
|
invalidateTokenizationCache: () => {
|
||||||
|
tokenizationCache.clear();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,12 @@ test('annotateTokens known-word match mode uses headword vs surface', () => {
|
|||||||
|
|
||||||
test('annotateTokens excludes frequency for particle/bound_auxiliary and pos1 exclusions', () => {
|
test('annotateTokens excludes frequency for particle/bound_auxiliary and pos1 exclusions', () => {
|
||||||
const tokens = [
|
const tokens = [
|
||||||
makeToken({ surface: 'は', headword: 'は', partOfSpeech: PartOfSpeech.particle, frequencyRank: 3 }),
|
makeToken({
|
||||||
|
surface: 'は',
|
||||||
|
headword: 'は',
|
||||||
|
partOfSpeech: PartOfSpeech.particle,
|
||||||
|
frequencyRank: 3,
|
||||||
|
}),
|
||||||
makeToken({
|
makeToken({
|
||||||
surface: 'です',
|
surface: 'です',
|
||||||
headword: 'です',
|
headword: 'です',
|
||||||
|
|||||||
@@ -7,12 +7,7 @@ import {
|
|||||||
DEFAULT_ANNOTATION_POS2_EXCLUSION_CONFIG,
|
DEFAULT_ANNOTATION_POS2_EXCLUSION_CONFIG,
|
||||||
resolveAnnotationPos2ExclusionSet,
|
resolveAnnotationPos2ExclusionSet,
|
||||||
} from '../../../token-pos2-exclusions';
|
} from '../../../token-pos2-exclusions';
|
||||||
import {
|
import { JlptLevel, MergedToken, NPlusOneMatchMode, PartOfSpeech } from '../../../types';
|
||||||
JlptLevel,
|
|
||||||
MergedToken,
|
|
||||||
NPlusOneMatchMode,
|
|
||||||
PartOfSpeech,
|
|
||||||
} from '../../../types';
|
|
||||||
import { shouldIgnoreJlptByTerm, shouldIgnoreJlptForMecabPos1 } from '../jlpt-token-filter';
|
import { shouldIgnoreJlptByTerm, shouldIgnoreJlptForMecabPos1 } from '../jlpt-token-filter';
|
||||||
|
|
||||||
const KATAKANA_TO_HIRAGANA_OFFSET = 0x60;
|
const KATAKANA_TO_HIRAGANA_OFFSET = 0x60;
|
||||||
@@ -67,10 +62,7 @@ function normalizePos1Tag(pos1: string | undefined): string {
|
|||||||
return typeof pos1 === 'string' ? pos1.trim() : '';
|
return typeof pos1 === 'string' ? pos1.trim() : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function isExcludedByTagSet(
|
function isExcludedByTagSet(normalizedTag: string, exclusions: ReadonlySet<string>): boolean {
|
||||||
normalizedTag: string,
|
|
||||||
exclusions: ReadonlySet<string>,
|
|
||||||
): boolean {
|
|
||||||
if (!normalizedTag) {
|
if (!normalizedTag) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -350,10 +342,7 @@ function isLikelyFrequencyNoiseToken(token: MergedToken): boolean {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (shouldIgnoreJlptByTerm(trimmedCandidate) || shouldIgnoreJlptByTerm(normalizedCandidate)) {
|
||||||
shouldIgnoreJlptByTerm(trimmedCandidate) ||
|
|
||||||
shouldIgnoreJlptByTerm(normalizedCandidate)
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,8 +431,7 @@ export function annotateTokens(
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const frequencyEnabled = options.frequencyEnabled !== false;
|
const frequencyEnabled = options.frequencyEnabled !== false;
|
||||||
const frequencyMarkedTokens =
|
const frequencyMarkedTokens = frequencyEnabled
|
||||||
frequencyEnabled
|
|
||||||
? applyFrequencyMarking(knownMarkedTokens, pos1Exclusions, pos2Exclusions)
|
? applyFrequencyMarking(knownMarkedTokens, pos1Exclusions, pos2Exclusions)
|
||||||
: knownMarkedTokens.map((token) => ({
|
: knownMarkedTokens.map((token) => ({
|
||||||
...token,
|
...token,
|
||||||
|
|||||||
@@ -116,7 +116,9 @@ class ParserEnrichmentWorkerRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleWorkerFailure(error: Error): void {
|
private handleWorkerFailure(error: Error): void {
|
||||||
logger.debug(`Parser enrichment worker unavailable, falling back to main thread: ${error.message}`);
|
logger.debug(
|
||||||
|
`Parser enrichment worker unavailable, falling back to main thread: ${error.message}`,
|
||||||
|
);
|
||||||
for (const pending of this.pending.values()) {
|
for (const pending of this.pending.values()) {
|
||||||
pending.reject(error);
|
pending.reject(error);
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/main.ts
10
src/main.ts
@@ -1256,10 +1256,7 @@ function getResolvedConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getRuntimeBooleanOption(
|
function getRuntimeBooleanOption(
|
||||||
id:
|
id: 'subtitle.annotation.nPlusOne' | 'subtitle.annotation.jlpt' | 'subtitle.annotation.frequency',
|
||||||
| 'subtitle.annotation.nPlusOne'
|
|
||||||
| 'subtitle.annotation.jlpt'
|
|
||||||
| 'subtitle.annotation.frequency',
|
|
||||||
fallback: boolean,
|
fallback: boolean,
|
||||||
): boolean {
|
): boolean {
|
||||||
const value = appState.runtimeOptionsManager?.getOptionValue(id);
|
const value = appState.runtimeOptionsManager?.getOptionValue(id);
|
||||||
@@ -2318,7 +2315,10 @@ const {
|
|||||||
getResolvedConfig().ankiConnect.nPlusOne.minSentenceWords,
|
getResolvedConfig().ankiConnect.nPlusOne.minSentenceWords,
|
||||||
getJlptLevel: (text) => appState.jlptLevelLookup(text),
|
getJlptLevel: (text) => appState.jlptLevelLookup(text),
|
||||||
getJlptEnabled: () =>
|
getJlptEnabled: () =>
|
||||||
getRuntimeBooleanOption('subtitle.annotation.jlpt', getResolvedConfig().subtitleStyle.enableJlpt),
|
getRuntimeBooleanOption(
|
||||||
|
'subtitle.annotation.jlpt',
|
||||||
|
getResolvedConfig().subtitleStyle.enableJlpt,
|
||||||
|
),
|
||||||
getFrequencyDictionaryEnabled: () =>
|
getFrequencyDictionaryEnabled: () =>
|
||||||
getRuntimeBooleanOption(
|
getRuntimeBooleanOption(
|
||||||
'subtitle.annotation.frequency',
|
'subtitle.annotation.frequency',
|
||||||
|
|||||||
@@ -214,9 +214,13 @@ test('handleOverlayModalClosed hides modal window only after all pending modals
|
|||||||
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||||
restoreOnModalClose: 'runtime-options',
|
restoreOnModalClose: 'runtime-options',
|
||||||
});
|
});
|
||||||
runtime.sendToActiveOverlayWindow('subsync:open-manual', { sourceTracks: [] }, {
|
runtime.sendToActiveOverlayWindow(
|
||||||
|
'subsync:open-manual',
|
||||||
|
{ sourceTracks: [] },
|
||||||
|
{
|
||||||
restoreOnModalClose: 'subsync',
|
restoreOnModalClose: 'subsync',
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
runtime.handleOverlayModalClosed('runtime-options');
|
runtime.handleOverlayModalClosed('runtime-options');
|
||||||
assert.equal(window.getHideCount(), 0);
|
assert.equal(window.getHideCount(), 0);
|
||||||
@@ -267,9 +271,13 @@ test('modal runtime notifies callers when modal input state becomes active/inact
|
|||||||
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||||
restoreOnModalClose: 'runtime-options',
|
restoreOnModalClose: 'runtime-options',
|
||||||
});
|
});
|
||||||
runtime.sendToActiveOverlayWindow('subsync:open-manual', { sourceTracks: [] }, {
|
runtime.sendToActiveOverlayWindow(
|
||||||
|
'subsync:open-manual',
|
||||||
|
{ sourceTracks: [] },
|
||||||
|
{
|
||||||
restoreOnModalClose: 'subsync',
|
restoreOnModalClose: 'subsync',
|
||||||
});
|
},
|
||||||
|
);
|
||||||
assert.deepEqual(state, []);
|
assert.deepEqual(state, []);
|
||||||
runtime.notifyOverlayModalOpened('runtime-options');
|
runtime.notifyOverlayModalOpened('runtime-options');
|
||||||
assert.deepEqual(state, [true]);
|
assert.deepEqual(state, [true]);
|
||||||
@@ -352,9 +360,13 @@ test('handleOverlayModalClosed hides modal window for single kiku modal', () =>
|
|||||||
setModalWindowBounds: () => {},
|
setModalWindowBounds: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
runtime.sendToActiveOverlayWindow('kiku:field-grouping-open', { test: true }, {
|
runtime.sendToActiveOverlayWindow(
|
||||||
|
'kiku:field-grouping-open',
|
||||||
|
{ test: true },
|
||||||
|
{
|
||||||
restoreOnModalClose: 'kiku',
|
restoreOnModalClose: 'kiku',
|
||||||
});
|
},
|
||||||
|
);
|
||||||
runtime.handleOverlayModalClosed('kiku');
|
runtime.handleOverlayModalClosed('kiku');
|
||||||
|
|
||||||
assert.equal(window.getHideCount(), 1);
|
assert.equal(window.getHideCount(), 1);
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import type {
|
|||||||
createMaybeProbeAnilistDurationHandler,
|
createMaybeProbeAnilistDurationHandler,
|
||||||
} from './anilist-media-guess';
|
} from './anilist-media-guess';
|
||||||
|
|
||||||
type MaybeProbeAnilistDurationMainDeps = Parameters<typeof createMaybeProbeAnilistDurationHandler>[0];
|
type MaybeProbeAnilistDurationMainDeps = Parameters<
|
||||||
|
typeof createMaybeProbeAnilistDurationHandler
|
||||||
|
>[0];
|
||||||
type EnsureAnilistMediaGuessMainDeps = Parameters<typeof createEnsureAnilistMediaGuessHandler>[0];
|
type EnsureAnilistMediaGuessMainDeps = Parameters<typeof createEnsureAnilistMediaGuessHandler>[0];
|
||||||
|
|
||||||
export function createBuildMaybeProbeAnilistDurationMainDepsHandler(
|
export function createBuildMaybeProbeAnilistDurationMainDepsHandler(
|
||||||
@@ -19,13 +21,17 @@ export function createBuildMaybeProbeAnilistDurationMainDepsHandler(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBuildEnsureAnilistMediaGuessMainDepsHandler(deps: EnsureAnilistMediaGuessMainDeps) {
|
export function createBuildEnsureAnilistMediaGuessMainDepsHandler(
|
||||||
|
deps: EnsureAnilistMediaGuessMainDeps,
|
||||||
|
) {
|
||||||
return (): EnsureAnilistMediaGuessMainDeps => ({
|
return (): EnsureAnilistMediaGuessMainDeps => ({
|
||||||
getState: () => deps.getState(),
|
getState: () => deps.getState(),
|
||||||
setState: (state) => deps.setState(state),
|
setState: (state) => deps.setState(state),
|
||||||
resolveMediaPathForJimaku: (currentMediaPath) => deps.resolveMediaPathForJimaku(currentMediaPath),
|
resolveMediaPathForJimaku: (currentMediaPath) =>
|
||||||
|
deps.resolveMediaPathForJimaku(currentMediaPath),
|
||||||
getCurrentMediaPath: () => deps.getCurrentMediaPath(),
|
getCurrentMediaPath: () => deps.getCurrentMediaPath(),
|
||||||
getCurrentMediaTitle: () => deps.getCurrentMediaTitle(),
|
getCurrentMediaTitle: () => deps.getCurrentMediaTitle(),
|
||||||
guessAnilistMediaInfo: (mediaPath, mediaTitle) => deps.guessAnilistMediaInfo(mediaPath, mediaTitle),
|
guessAnilistMediaInfo: (mediaPath, mediaTitle) =>
|
||||||
|
deps.guessAnilistMediaInfo(mediaPath, mediaTitle),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ import type {
|
|||||||
createSetAnilistMediaGuessRuntimeStateHandler,
|
createSetAnilistMediaGuessRuntimeStateHandler,
|
||||||
} from './anilist-media-state';
|
} from './anilist-media-state';
|
||||||
|
|
||||||
type GetCurrentAnilistMediaKeyMainDeps = Parameters<typeof createGetCurrentAnilistMediaKeyHandler>[0];
|
type GetCurrentAnilistMediaKeyMainDeps = Parameters<
|
||||||
type ResetAnilistMediaTrackingMainDeps = Parameters<typeof createResetAnilistMediaTrackingHandler>[0];
|
typeof createGetCurrentAnilistMediaKeyHandler
|
||||||
|
>[0];
|
||||||
|
type ResetAnilistMediaTrackingMainDeps = Parameters<
|
||||||
|
typeof createResetAnilistMediaTrackingHandler
|
||||||
|
>[0];
|
||||||
type GetAnilistMediaGuessRuntimeStateMainDeps = Parameters<
|
type GetAnilistMediaGuessRuntimeStateMainDeps = Parameters<
|
||||||
typeof createGetAnilistMediaGuessRuntimeStateHandler
|
typeof createGetAnilistMediaGuessRuntimeStateHandler
|
||||||
>[0];
|
>[0];
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ export function createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler(
|
|||||||
hasAttemptedUpdateKey: (key: string) => deps.hasAttemptedUpdateKey(key),
|
hasAttemptedUpdateKey: (key: string) => deps.hasAttemptedUpdateKey(key),
|
||||||
processNextAnilistRetryUpdate: () => deps.processNextAnilistRetryUpdate(),
|
processNextAnilistRetryUpdate: () => deps.processNextAnilistRetryUpdate(),
|
||||||
refreshAnilistClientSecretState: () => deps.refreshAnilistClientSecretState(),
|
refreshAnilistClientSecretState: () => deps.refreshAnilistClientSecretState(),
|
||||||
enqueueRetry: (key: string, title: string, episode: number) => deps.enqueueRetry(key, title, episode),
|
enqueueRetry: (key: string, title: string, episode: number) =>
|
||||||
|
deps.enqueueRetry(key, title, episode),
|
||||||
markRetryFailure: (key: string, message: string) => deps.markRetryFailure(key, message),
|
markRetryFailure: (key: string, message: string) => deps.markRetryFailure(key, message),
|
||||||
markRetrySuccess: (key: string) => deps.markRetrySuccess(key),
|
markRetrySuccess: (key: string) => deps.markRetrySuccess(key),
|
||||||
refreshRetryQueueState: () => deps.refreshRetryQueueState(),
|
refreshRetryQueueState: () => deps.refreshRetryQueueState(),
|
||||||
|
|||||||
@@ -64,7 +64,11 @@ export function createProcessNextAnilistRetryUpdateHandler(deps: {
|
|||||||
return { ok: false, message: 'AniList token unavailable for queued retry.' };
|
return { ok: false, message: 'AniList token unavailable for queued retry.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await deps.updateAnilistPostWatchProgress(accessToken, queued.title, queued.episode);
|
const result = await deps.updateAnilistPostWatchProgress(
|
||||||
|
accessToken,
|
||||||
|
queued.title,
|
||||||
|
queued.episode,
|
||||||
|
);
|
||||||
if (result.status === 'updated' || result.status === 'skipped') {
|
if (result.status === 'updated' || result.status === 'skipped') {
|
||||||
deps.markSuccess(queued.key);
|
deps.markSuccess(queued.key);
|
||||||
deps.rememberAttemptedUpdateKey(queued.key);
|
deps.rememberAttemptedUpdateKey(queued.key);
|
||||||
@@ -166,7 +170,11 @@ export function createMaybeRunAnilistPostWatchUpdateHandler(deps: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await deps.updateAnilistPostWatchProgress(accessToken, guess.title, guess.episode);
|
const result = await deps.updateAnilistPostWatchProgress(
|
||||||
|
accessToken,
|
||||||
|
guess.title,
|
||||||
|
guess.episode,
|
||||||
|
);
|
||||||
if (result.status === 'updated') {
|
if (result.status === 'updated') {
|
||||||
deps.rememberAttemptedUpdateKey(attemptKey);
|
deps.rememberAttemptedUpdateKey(attemptKey);
|
||||||
deps.markRetrySuccess(attemptKey);
|
deps.markRetrySuccess(attemptKey);
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ export function createBuildHandleAnilistSetupProtocolUrlMainDepsHandler(
|
|||||||
deps: HandleAnilistSetupProtocolUrlMainDeps,
|
deps: HandleAnilistSetupProtocolUrlMainDeps,
|
||||||
) {
|
) {
|
||||||
return (): HandleAnilistSetupProtocolUrlMainDeps => ({
|
return (): HandleAnilistSetupProtocolUrlMainDeps => ({
|
||||||
consumeAnilistSetupTokenFromUrl: (rawUrl: string) => deps.consumeAnilistSetupTokenFromUrl(rawUrl),
|
consumeAnilistSetupTokenFromUrl: (rawUrl: string) =>
|
||||||
|
deps.consumeAnilistSetupTokenFromUrl(rawUrl),
|
||||||
logWarn: (message: string, details: unknown) => deps.logWarn(message, details),
|
logWarn: (message: string, details: unknown) => deps.logWarn(message, details),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,11 +66,7 @@ export function createRegisterSubminerProtocolClientHandler(deps: {
|
|||||||
getArgv: () => string[];
|
getArgv: () => string[];
|
||||||
execPath: string;
|
execPath: string;
|
||||||
resolvePath: (value: string) => string;
|
resolvePath: (value: string) => string;
|
||||||
setAsDefaultProtocolClient: (
|
setAsDefaultProtocolClient: (scheme: string, path?: string, args?: string[]) => boolean;
|
||||||
scheme: string,
|
|
||||||
path?: string,
|
|
||||||
args?: string[],
|
|
||||||
) => boolean;
|
|
||||||
logWarn: (message: string, details?: unknown) => void;
|
logWarn: (message: string, details?: unknown) => void;
|
||||||
}) {
|
}) {
|
||||||
return (): void => {
|
return (): void => {
|
||||||
|
|||||||
@@ -259,7 +259,8 @@ test('open anilist setup handler no-ops when existing setup window focused', ()
|
|||||||
|
|
||||||
test('open anilist setup handler wires navigation, fallback, and lifecycle', () => {
|
test('open anilist setup handler wires navigation, fallback, and lifecycle', () => {
|
||||||
let openHandler: ((params: { url: string }) => { action: 'deny' }) | null = null;
|
let openHandler: ((params: { url: string }) => { action: 'deny' }) | null = null;
|
||||||
let willNavigateHandler: ((event: { preventDefault: () => void }, url: string) => void) | null = null;
|
let willNavigateHandler: ((event: { preventDefault: () => void }, url: string) => void) | null =
|
||||||
|
null;
|
||||||
let didNavigateHandler: ((event: unknown, url: string) => void) | null = null;
|
let didNavigateHandler: ((event: unknown, url: string) => void) | null = null;
|
||||||
let didFinishLoadHandler: (() => void) | null = null;
|
let didFinishLoadHandler: (() => void) | null = null;
|
||||||
let didFailLoadHandler:
|
let didFailLoadHandler:
|
||||||
@@ -276,7 +277,12 @@ test('open anilist setup handler wires navigation, fallback, and lifecycle', ()
|
|||||||
openHandler = handler;
|
openHandler = handler;
|
||||||
},
|
},
|
||||||
on: (
|
on: (
|
||||||
event: 'will-navigate' | 'will-redirect' | 'did-navigate' | 'did-fail-load' | 'did-finish-load',
|
event:
|
||||||
|
| 'will-navigate'
|
||||||
|
| 'will-redirect'
|
||||||
|
| 'did-navigate'
|
||||||
|
| 'did-fail-load'
|
||||||
|
| 'did-finish-load',
|
||||||
handler: (...args: any[]) => void,
|
handler: (...args: any[]) => void,
|
||||||
) => {
|
) => {
|
||||||
if (event === 'will-navigate') willNavigateHandler = handler as never;
|
if (event === 'will-navigate') willNavigateHandler = handler as never;
|
||||||
|
|||||||
@@ -126,7 +126,11 @@ export function createAnilistSetupDidNavigateHandler(deps: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createAnilistSetupDidFailLoadHandler(deps: {
|
export function createAnilistSetupDidFailLoadHandler(deps: {
|
||||||
onLoadFailure: (details: { errorCode: number; errorDescription: string; validatedURL: string }) => void;
|
onLoadFailure: (details: {
|
||||||
|
errorCode: number;
|
||||||
|
errorDescription: string;
|
||||||
|
validatedURL: string;
|
||||||
|
}) => void;
|
||||||
}) {
|
}) {
|
||||||
return (details: { errorCode: number; errorDescription: string; validatedURL: string }): void => {
|
return (details: { errorCode: number; errorDescription: string; validatedURL: string }): void => {
|
||||||
deps.onLoadFailure(details);
|
deps.onLoadFailure(details);
|
||||||
@@ -175,7 +179,11 @@ export function createAnilistSetupFallbackHandler(deps: {
|
|||||||
logWarn: (message: string) => void;
|
logWarn: (message: string) => void;
|
||||||
}) {
|
}) {
|
||||||
return {
|
return {
|
||||||
onLoadFailure: (details: { errorCode: number; errorDescription: string; validatedURL: string }) => {
|
onLoadFailure: (details: {
|
||||||
|
errorCode: number;
|
||||||
|
errorDescription: string;
|
||||||
|
validatedURL: string;
|
||||||
|
}) => {
|
||||||
deps.logError('AniList setup window failed to load', details);
|
deps.logError('AniList setup window failed to load', details);
|
||||||
deps.openSetupInBrowser();
|
deps.openSetupInBrowser();
|
||||||
if (!deps.setupWindow.isDestroyed()) {
|
if (!deps.setupWindow.isDestroyed()) {
|
||||||
@@ -298,12 +306,7 @@ export function createOpenAnilistSetupWindowHandler<TWindow extends AnilistSetup
|
|||||||
});
|
});
|
||||||
setupWindow.webContents.on(
|
setupWindow.webContents.on(
|
||||||
'did-fail-load',
|
'did-fail-load',
|
||||||
(
|
(_event: unknown, errorCode: number, errorDescription: string, validatedURL: string) => {
|
||||||
_event: unknown,
|
|
||||||
errorCode: number,
|
|
||||||
errorDescription: string,
|
|
||||||
validatedURL: string,
|
|
||||||
) => {
|
|
||||||
handleDidFailLoad({
|
handleDidFailLoad({
|
||||||
errorCode,
|
errorCode,
|
||||||
errorDescription,
|
errorDescription,
|
||||||
|
|||||||
@@ -65,9 +65,7 @@ export function findAnilistSetupDeepLinkArgvUrl(argv: readonly string[]): string
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function consumeAnilistSetupCallbackUrl(
|
export function consumeAnilistSetupCallbackUrl(deps: ConsumeAnilistSetupCallbackUrlDeps): boolean {
|
||||||
deps: ConsumeAnilistSetupCallbackUrlDeps,
|
|
||||||
): boolean {
|
|
||||||
const token = extractAnilistAccessTokenFromUrl(deps.rawUrl);
|
const token = extractAnilistAccessTokenFromUrl(deps.rawUrl);
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ type ConfigWithAnilistToken = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createRefreshAnilistClientSecretStateHandler<TConfig extends ConfigWithAnilistToken>(deps: {
|
export function createRefreshAnilistClientSecretStateHandler<
|
||||||
|
TConfig extends ConfigWithAnilistToken,
|
||||||
|
>(deps: {
|
||||||
getResolvedConfig: () => TConfig;
|
getResolvedConfig: () => TConfig;
|
||||||
isAnilistTrackingEnabled: (config: TConfig) => boolean;
|
isAnilistTrackingEnabled: (config: TConfig) => boolean;
|
||||||
getCachedAccessToken: () => string | null;
|
getCachedAccessToken: () => string | null;
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ export function createBuildUpdateLastCardFromClipboardMainDepsHandler<TAnki>(dep
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBuildRefreshKnownWordCacheMainDepsHandler(deps: RefreshKnownWordCacheMainDeps) {
|
export function createBuildRefreshKnownWordCacheMainDepsHandler(
|
||||||
|
deps: RefreshKnownWordCacheMainDeps,
|
||||||
|
) {
|
||||||
return (): RefreshKnownWordCacheMainDeps => ({
|
return (): RefreshKnownWordCacheMainDeps => ({
|
||||||
getAnkiIntegration: () => deps.getAnkiIntegration(),
|
getAnkiIntegration: () => deps.getAnkiIntegration(),
|
||||||
missingIntegrationMessage: deps.missingIntegrationMessage,
|
missingIntegrationMessage: deps.missingIntegrationMessage,
|
||||||
@@ -42,8 +44,10 @@ export function createBuildTriggerFieldGroupingMainDepsHandler<TAnki>(deps: {
|
|||||||
return () => ({
|
return () => ({
|
||||||
getAnkiIntegration: () => deps.getAnkiIntegration(),
|
getAnkiIntegration: () => deps.getAnkiIntegration(),
|
||||||
showMpvOsd: (text: string) => deps.showMpvOsd(text),
|
showMpvOsd: (text: string) => deps.showMpvOsd(text),
|
||||||
triggerFieldGroupingCore: (options: { ankiIntegration: TAnki; showMpvOsd: (text: string) => void }) =>
|
triggerFieldGroupingCore: (options: {
|
||||||
deps.triggerFieldGroupingCore(options),
|
ankiIntegration: TAnki;
|
||||||
|
showMpvOsd: (text: string) => void;
|
||||||
|
}) => deps.triggerFieldGroupingCore(options),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,8 +62,10 @@ export function createBuildMarkLastCardAsAudioCardMainDepsHandler<TAnki>(deps: {
|
|||||||
return () => ({
|
return () => ({
|
||||||
getAnkiIntegration: () => deps.getAnkiIntegration(),
|
getAnkiIntegration: () => deps.getAnkiIntegration(),
|
||||||
showMpvOsd: (text: string) => deps.showMpvOsd(text),
|
showMpvOsd: (text: string) => deps.showMpvOsd(text),
|
||||||
markLastCardAsAudioCardCore: (options: { ankiIntegration: TAnki; showMpvOsd: (text: string) => void }) =>
|
markLastCardAsAudioCardCore: (options: {
|
||||||
deps.markLastCardAsAudioCardCore(options),
|
ankiIntegration: TAnki;
|
||||||
|
showMpvOsd: (text: string) => void;
|
||||||
|
}) => deps.markLastCardAsAudioCardCore(options),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,8 +52,7 @@ export function createBuildOnWillQuitCleanupDepsHandler(deps: {
|
|||||||
destroyTray: () => deps.destroyTray(),
|
destroyTray: () => deps.destroyTray(),
|
||||||
stopConfigHotReload: () => deps.stopConfigHotReload(),
|
stopConfigHotReload: () => deps.stopConfigHotReload(),
|
||||||
restorePreviousSecondarySubVisibility: () => deps.restorePreviousSecondarySubVisibility(),
|
restorePreviousSecondarySubVisibility: () => deps.restorePreviousSecondarySubVisibility(),
|
||||||
restoreMpvSubVisibility: () =>
|
restoreMpvSubVisibility: () => deps.restoreMpvSubVisibility(),
|
||||||
deps.restoreMpvSubVisibility(),
|
|
||||||
unregisterAllGlobalShortcuts: () => deps.unregisterAllGlobalShortcuts(),
|
unregisterAllGlobalShortcuts: () => deps.unregisterAllGlobalShortcuts(),
|
||||||
stopSubtitleWebsocket: () => deps.stopSubtitleWebsocket(),
|
stopSubtitleWebsocket: () => deps.stopSubtitleWebsocket(),
|
||||||
stopTexthookerService: () => deps.stopTexthookerService(),
|
stopTexthookerService: () => deps.stopTexthookerService(),
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import type { AppReadyRuntimeDepsFactoryInput } from '../app-lifecycle';
|
import type { AppReadyRuntimeDepsFactoryInput } from '../app-lifecycle';
|
||||||
|
|
||||||
export function createBuildAppReadyRuntimeMainDepsHandler(
|
export function createBuildAppReadyRuntimeMainDepsHandler(deps: AppReadyRuntimeDepsFactoryInput) {
|
||||||
deps: AppReadyRuntimeDepsFactoryInput,
|
|
||||||
) {
|
|
||||||
return (): AppReadyRuntimeDepsFactoryInput => ({
|
return (): AppReadyRuntimeDepsFactoryInput => ({
|
||||||
loadSubtitlePosition: deps.loadSubtitlePosition,
|
loadSubtitlePosition: deps.loadSubtitlePosition,
|
||||||
resolveKeybindings: deps.resolveKeybindings,
|
resolveKeybindings: deps.resolveKeybindings,
|
||||||
@@ -27,8 +25,7 @@ export function createBuildAppReadyRuntimeMainDepsHandler(
|
|||||||
prewarmSubtitleDictionaries: deps.prewarmSubtitleDictionaries,
|
prewarmSubtitleDictionaries: deps.prewarmSubtitleDictionaries,
|
||||||
startBackgroundWarmups: deps.startBackgroundWarmups,
|
startBackgroundWarmups: deps.startBackgroundWarmups,
|
||||||
texthookerOnlyMode: deps.texthookerOnlyMode,
|
texthookerOnlyMode: deps.texthookerOnlyMode,
|
||||||
shouldAutoInitializeOverlayRuntimeFromConfig:
|
shouldAutoInitializeOverlayRuntimeFromConfig: deps.shouldAutoInitializeOverlayRuntimeFromConfig,
|
||||||
deps.shouldAutoInitializeOverlayRuntimeFromConfig,
|
|
||||||
initializeOverlayRuntime: deps.initializeOverlayRuntime,
|
initializeOverlayRuntime: deps.initializeOverlayRuntime,
|
||||||
handleInitialArgs: deps.handleInitialArgs,
|
handleInitialArgs: deps.handleInitialArgs,
|
||||||
onCriticalConfigErrors: deps.onCriticalConfigErrors,
|
onCriticalConfigErrors: deps.onCriticalConfigErrors,
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ test('open yomitan settings main deps map async open callbacks', async () => {
|
|||||||
const extension = { id: 'ext' };
|
const extension = { id: 'ext' };
|
||||||
const deps = createBuildOpenYomitanSettingsMainDepsHandler({
|
const deps = createBuildOpenYomitanSettingsMainDepsHandler({
|
||||||
ensureYomitanExtensionLoaded: async () => extension,
|
ensureYomitanExtensionLoaded: async () => extension,
|
||||||
openYomitanSettingsWindow: ({ yomitanExt }) => calls.push(`open:${(yomitanExt as { id: string }).id}`),
|
openYomitanSettingsWindow: ({ yomitanExt }) =>
|
||||||
|
calls.push(`open:${(yomitanExt as { id: string }).id}`),
|
||||||
getExistingWindow: () => currentWindow,
|
getExistingWindow: () => currentWindow,
|
||||||
setWindow: (window) => {
|
setWindow: (window) => {
|
||||||
currentWindow = window;
|
currentWindow = window;
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import { createCliCommandContext } from './cli-command-context';
|
|||||||
import { createBuildCliCommandContextDepsHandler } from './cli-command-context-deps';
|
import { createBuildCliCommandContextDepsHandler } from './cli-command-context-deps';
|
||||||
import { createBuildCliCommandContextMainDepsHandler } from './cli-command-context-main-deps';
|
import { createBuildCliCommandContextMainDepsHandler } from './cli-command-context-main-deps';
|
||||||
|
|
||||||
type CliCommandContextMainDeps = Parameters<
|
type CliCommandContextMainDeps = Parameters<typeof createBuildCliCommandContextMainDepsHandler>[0];
|
||||||
typeof createBuildCliCommandContextMainDepsHandler
|
|
||||||
>[0];
|
|
||||||
|
|
||||||
export function createCliCommandContextFactory(deps: CliCommandContextMainDeps) {
|
export function createCliCommandContextFactory(deps: CliCommandContextMainDeps) {
|
||||||
const buildCliCommandContextMainDepsHandler = createBuildCliCommandContextMainDepsHandler(deps);
|
const buildCliCommandContextMainDepsHandler = createBuildCliCommandContextMainDepsHandler(deps);
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ function createDeps() {
|
|||||||
triggerFieldGrouping: async () => {},
|
triggerFieldGrouping: async () => {},
|
||||||
triggerSubsyncFromConfig: async () => {},
|
triggerSubsyncFromConfig: async () => {},
|
||||||
markLastCardAsAudioCard: async () => {},
|
markLastCardAsAudioCard: async () => {},
|
||||||
getAnilistStatus: () => ({} as never),
|
getAnilistStatus: () => ({}) as never,
|
||||||
clearAnilistToken: () => {},
|
clearAnilistToken: () => {},
|
||||||
openAnilistSetup: () => {},
|
openAnilistSetup: () => {},
|
||||||
openJellyfinSetup: () => {},
|
openJellyfinSetup: () => {},
|
||||||
getAnilistQueueStatus: () => ({} as never),
|
getAnilistQueueStatus: () => ({}) as never,
|
||||||
retryAnilistQueueNow: async () => ({ ok: true, message: 'ok' }),
|
retryAnilistQueueNow: async () => ({ ok: true, message: 'ok' }),
|
||||||
runJellyfinCommand: async () => {},
|
runJellyfinCommand: async () => {},
|
||||||
openYomitanSettings: () => {},
|
openYomitanSettings: () => {},
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ export function createCliCommandRuntimeHandler<TCliContext>(deps: {
|
|||||||
cliContext: TCliContext,
|
cliContext: TCliContext,
|
||||||
) => void;
|
) => void;
|
||||||
}) {
|
}) {
|
||||||
const handleTexthookerOnlyModeTransitionHandler =
|
const handleTexthookerOnlyModeTransitionHandler = createHandleTexthookerOnlyModeTransitionHandler(
|
||||||
createHandleTexthookerOnlyModeTransitionHandler(
|
|
||||||
createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler(
|
createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler(
|
||||||
deps.handleTexthookerOnlyModeTransitionMainDeps,
|
deps.handleTexthookerOnlyModeTransitionMainDeps,
|
||||||
)(),
|
)(),
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ export type AppendClipboardVideoToQueueRuntimeDeps = {
|
|||||||
sendMpvCommand: (command: (string | number)[]) => void;
|
sendMpvCommand: (command: (string | number)[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function appendClipboardVideoToQueueRuntime(
|
export function appendClipboardVideoToQueueRuntime(deps: AppendClipboardVideoToQueueRuntimeDeps): {
|
||||||
deps: AppendClipboardVideoToQueueRuntimeDeps,
|
ok: boolean;
|
||||||
): { ok: boolean; message: string } {
|
message: string;
|
||||||
|
} {
|
||||||
const mpvClient = deps.getMpvClient();
|
const mpvClient = deps.getMpvClient();
|
||||||
if (!mpvClient || !mpvClient.connected) {
|
if (!mpvClient || !mpvClient.connected) {
|
||||||
return { ok: false, message: 'MPV is not connected.' };
|
return { ok: false, message: 'MPV is not connected.' };
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ test('composeJellyfinRemoteHandlers returns callable jellyfin remote handlers',
|
|||||||
let lastProgressAt = 0;
|
let lastProgressAt = 0;
|
||||||
const composed = composeJellyfinRemoteHandlers({
|
const composed = composeJellyfinRemoteHandlers({
|
||||||
getConfiguredSession: () => null,
|
getConfiguredSession: () => null,
|
||||||
getClientInfo: () => ({ clientName: 'SubMiner', clientVersion: 'test', deviceId: 'dev' }) as never,
|
getClientInfo: () =>
|
||||||
|
({ clientName: 'SubMiner', clientVersion: 'test', deviceId: 'dev' }) as never,
|
||||||
getJellyfinConfig: () => ({ enabled: false }) as never,
|
getJellyfinConfig: () => ({ enabled: false }) as never,
|
||||||
playJellyfinItem: async () => {},
|
playJellyfinItem: async () => {},
|
||||||
logWarn: () => {},
|
logWarn: () => {},
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ export function createConfigDerivedRuntime(deps: ConfigDerivedRuntimeDeps): {
|
|||||||
deps.defaultJimakuLanguagePreference,
|
deps.defaultJimakuLanguagePreference,
|
||||||
),
|
),
|
||||||
getJimakuMaxEntryResults: () =>
|
getJimakuMaxEntryResults: () =>
|
||||||
getJimakuMaxEntryResultsCore(() => deps.getResolvedConfig(), deps.defaultJimakuMaxEntryResults),
|
getJimakuMaxEntryResultsCore(
|
||||||
|
() => deps.getResolvedConfig(),
|
||||||
|
deps.defaultJimakuMaxEntryResults,
|
||||||
|
),
|
||||||
resolveJimakuApiKey: () => resolveJimakuApiKeyCore(() => deps.getResolvedConfig()),
|
resolveJimakuApiKey: () => resolveJimakuApiKeyCore(() => deps.getResolvedConfig()),
|
||||||
jimakuFetchJson: <T>(
|
jimakuFetchJson: <T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ test('config hot reload applied main deps builder maps callbacks', () => {
|
|||||||
test('config hot reload runtime main deps builder maps runtime callbacks', () => {
|
test('config hot reload runtime main deps builder maps runtime callbacks', () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const deps = createBuildConfigHotReloadRuntimeMainDepsHandler({
|
const deps = createBuildConfigHotReloadRuntimeMainDepsHandler({
|
||||||
getCurrentConfig: () => ({ id: 1 } as never as ResolvedConfig),
|
getCurrentConfig: () => ({ id: 1 }) as never as ResolvedConfig,
|
||||||
reloadConfigStrict: () =>
|
reloadConfigStrict: () =>
|
||||||
({
|
({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -144,5 +144,10 @@ test('config hot reload runtime main deps builder maps runtime callbacks', () =>
|
|||||||
deps.onRestartRequired([]);
|
deps.onRestartRequired([]);
|
||||||
deps.onInvalidConfig('bad');
|
deps.onInvalidConfig('bad');
|
||||||
deps.onValidationWarnings('/tmp/config.jsonc', []);
|
deps.onValidationWarnings('/tmp/config.jsonc', []);
|
||||||
assert.deepEqual(calls, ['hot-reload', 'restart-required', 'invalid-config', 'validation-warnings']);
|
assert.deepEqual(calls, [
|
||||||
|
'hot-reload',
|
||||||
|
'restart-required',
|
||||||
|
'invalid-config',
|
||||||
|
'validation-warnings',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import type {
|
|||||||
ConfigHotReloadRuntimeDeps,
|
ConfigHotReloadRuntimeDeps,
|
||||||
} from '../../core/services/config-hot-reload';
|
} from '../../core/services/config-hot-reload';
|
||||||
import type { ReloadConfigStrictResult } from '../../config';
|
import type { ReloadConfigStrictResult } from '../../config';
|
||||||
import type { ConfigHotReloadPayload, ConfigValidationWarning, ResolvedConfig, SecondarySubMode } from '../../types';
|
import type {
|
||||||
|
ConfigHotReloadPayload,
|
||||||
|
ConfigValidationWarning,
|
||||||
|
ResolvedConfig,
|
||||||
|
SecondarySubMode,
|
||||||
|
} from '../../types';
|
||||||
import type { createConfigHotReloadMessageHandler } from './config-hot-reload-handlers';
|
import type { createConfigHotReloadMessageHandler } from './config-hot-reload-handlers';
|
||||||
|
|
||||||
type ConfigWatchListener = (eventType: string, filename: string | null) => void;
|
type ConfigWatchListener = (eventType: string, filename: string | null) => void;
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ test('jlpt dictionary runtime main deps builder maps search paths and log prefix
|
|||||||
const deps = createBuildJlptDictionaryRuntimeMainDepsHandler({
|
const deps = createBuildJlptDictionaryRuntimeMainDepsHandler({
|
||||||
isJlptEnabled: () => true,
|
isJlptEnabled: () => true,
|
||||||
getDictionaryRoots: () => ['/root/a'],
|
getDictionaryRoots: () => ['/root/a'],
|
||||||
getJlptDictionarySearchPaths: ({ getDictionaryRoots }) => getDictionaryRoots().map((path) => `${path}/jlpt`),
|
getJlptDictionarySearchPaths: ({ getDictionaryRoots }) =>
|
||||||
|
getDictionaryRoots().map((path) => `${path}/jlpt`),
|
||||||
setJlptLevelLookup: () => calls.push('set-lookup'),
|
setJlptLevelLookup: () => calls.push('set-lookup'),
|
||||||
logInfo: (message) => calls.push(`log:${message}`),
|
logInfo: (message) => calls.push(`log:${message}`),
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -12,20 +12,24 @@ test('get configured shortcuts main deps map config resolver inputs', () => {
|
|||||||
const build = createBuildGetConfiguredShortcutsMainDepsHandler({
|
const build = createBuildGetConfiguredShortcutsMainDepsHandler({
|
||||||
getResolvedConfig: () => config,
|
getResolvedConfig: () => config,
|
||||||
defaultConfig: defaults,
|
defaultConfig: defaults,
|
||||||
resolveConfiguredShortcuts: (nextConfig, nextDefaults) => ({ nextConfig, nextDefaults }) as never,
|
resolveConfiguredShortcuts: (nextConfig, nextDefaults) =>
|
||||||
|
({ nextConfig, nextDefaults }) as never,
|
||||||
});
|
});
|
||||||
|
|
||||||
const deps = build();
|
const deps = build();
|
||||||
assert.equal(deps.getResolvedConfig(), config);
|
assert.equal(deps.getResolvedConfig(), config);
|
||||||
assert.equal(deps.defaultConfig, defaults);
|
assert.equal(deps.defaultConfig, defaults);
|
||||||
assert.deepEqual(deps.resolveConfiguredShortcuts(config, defaults), { nextConfig: config, nextDefaults: defaults });
|
assert.deepEqual(deps.resolveConfiguredShortcuts(config, defaults), {
|
||||||
|
nextConfig: config,
|
||||||
|
nextDefaults: defaults,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('register global shortcuts main deps map callbacks and flags', () => {
|
test('register global shortcuts main deps map callbacks and flags', () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const mainWindow = { id: 'main' };
|
const mainWindow = { id: 'main' };
|
||||||
const build = createBuildRegisterGlobalShortcutsMainDepsHandler({
|
const build = createBuildRegisterGlobalShortcutsMainDepsHandler({
|
||||||
getConfiguredShortcuts: () => ({ copySubtitle: 's' } as never),
|
getConfiguredShortcuts: () => ({ copySubtitle: 's' }) as never,
|
||||||
registerGlobalShortcutsCore: () => calls.push('register'),
|
registerGlobalShortcutsCore: () => calls.push('register'),
|
||||||
toggleVisibleOverlay: () => calls.push('toggle-visible'),
|
toggleVisibleOverlay: () => calls.push('toggle-visible'),
|
||||||
openYomitanSettings: () => calls.push('open-yomitan'),
|
openYomitanSettings: () => calls.push('open-yomitan'),
|
||||||
|
|||||||
@@ -32,15 +32,17 @@ export function createGlobalShortcutsRuntimeHandlers(deps: {
|
|||||||
const getConfiguredShortcutsMainDeps = createBuildGetConfiguredShortcutsMainDepsHandler(
|
const getConfiguredShortcutsMainDeps = createBuildGetConfiguredShortcutsMainDepsHandler(
|
||||||
deps.getConfiguredShortcutsMainDeps,
|
deps.getConfiguredShortcutsMainDeps,
|
||||||
)();
|
)();
|
||||||
const getConfiguredShortcutsHandler =
|
const getConfiguredShortcutsHandler = createGetConfiguredShortcutsHandler(
|
||||||
createGetConfiguredShortcutsHandler(getConfiguredShortcutsMainDeps);
|
getConfiguredShortcutsMainDeps,
|
||||||
|
);
|
||||||
const getConfiguredShortcuts = () => getConfiguredShortcutsHandler();
|
const getConfiguredShortcuts = () => getConfiguredShortcutsHandler();
|
||||||
|
|
||||||
const registerGlobalShortcutsMainDeps = createBuildRegisterGlobalShortcutsMainDepsHandler(
|
const registerGlobalShortcutsMainDeps = createBuildRegisterGlobalShortcutsMainDepsHandler(
|
||||||
deps.buildRegisterGlobalShortcutsMainDeps(getConfiguredShortcuts),
|
deps.buildRegisterGlobalShortcutsMainDeps(getConfiguredShortcuts),
|
||||||
)();
|
)();
|
||||||
const registerGlobalShortcutsHandler =
|
const registerGlobalShortcutsHandler = createRegisterGlobalShortcutsHandler(
|
||||||
createRegisterGlobalShortcutsHandler(registerGlobalShortcutsMainDeps);
|
registerGlobalShortcutsMainDeps,
|
||||||
|
);
|
||||||
const registerGlobalShortcuts = () => registerGlobalShortcutsHandler();
|
const registerGlobalShortcuts = () => registerGlobalShortcutsHandler();
|
||||||
|
|
||||||
const refreshGlobalAndOverlayShortcutsMainDeps =
|
const refreshGlobalAndOverlayShortcutsMainDeps =
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ import type { RegisterGlobalShortcutsServiceOptions } from '../../core/services/
|
|||||||
export function createGetConfiguredShortcutsHandler(deps: {
|
export function createGetConfiguredShortcutsHandler(deps: {
|
||||||
getResolvedConfig: () => Config;
|
getResolvedConfig: () => Config;
|
||||||
defaultConfig: Config;
|
defaultConfig: Config;
|
||||||
resolveConfiguredShortcuts: (
|
resolveConfiguredShortcuts: (config: Config, defaultConfig: Config) => ConfiguredShortcuts;
|
||||||
config: Config,
|
|
||||||
defaultConfig: Config,
|
|
||||||
) => ConfiguredShortcuts;
|
|
||||||
}) {
|
}) {
|
||||||
return (): ConfiguredShortcuts =>
|
return (): ConfiguredShortcuts =>
|
||||||
deps.resolveConfiguredShortcuts(deps.getResolvedConfig(), deps.defaultConfig);
|
deps.resolveConfiguredShortcuts(deps.getResolvedConfig(), deps.defaultConfig);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { createBuildImmersionTrackerStartupMainDepsHandler } from './immersion-s
|
|||||||
test('immersion tracker startup main deps builder maps callbacks', () => {
|
test('immersion tracker startup main deps builder maps callbacks', () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const deps = createBuildImmersionTrackerStartupMainDepsHandler({
|
const deps = createBuildImmersionTrackerStartupMainDepsHandler({
|
||||||
getResolvedConfig: () => ({ immersionTracking: { enabled: true } } as never),
|
getResolvedConfig: () => ({ immersionTracking: { enabled: true } }) as never,
|
||||||
getConfiguredDbPath: () => '/tmp/immersion.db',
|
getConfiguredDbPath: () => '/tmp/immersion.db',
|
||||||
createTrackerService: () => {
|
createTrackerService: () => {
|
||||||
calls.push('create');
|
calls.push('create');
|
||||||
@@ -21,9 +21,12 @@ test('immersion tracker startup main deps builder maps callbacks', () => {
|
|||||||
|
|
||||||
assert.deepEqual(deps.getResolvedConfig(), { immersionTracking: { enabled: true } });
|
assert.deepEqual(deps.getResolvedConfig(), { immersionTracking: { enabled: true } });
|
||||||
assert.equal(deps.getConfiguredDbPath(), '/tmp/immersion.db');
|
assert.equal(deps.getConfiguredDbPath(), '/tmp/immersion.db');
|
||||||
assert.deepEqual(deps.createTrackerService({ dbPath: '/tmp/immersion.db', policy: {} as never }), {
|
assert.deepEqual(
|
||||||
|
deps.createTrackerService({ dbPath: '/tmp/immersion.db', policy: {} as never }),
|
||||||
|
{
|
||||||
id: 'tracker',
|
id: 'tracker',
|
||||||
});
|
},
|
||||||
|
);
|
||||||
deps.setTracker(null);
|
deps.setTracker(null);
|
||||||
assert.equal(deps.getMpvClient()?.connected, true);
|
assert.equal(deps.getMpvClient()?.connected, true);
|
||||||
deps.seedTrackerFromCurrentMedia();
|
deps.seedTrackerFromCurrentMedia();
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ test('initial args handler no-ops without initial args', () => {
|
|||||||
test('initial args handler ensures tray in background mode', () => {
|
test('initial args handler ensures tray in background mode', () => {
|
||||||
let ensuredTray = false;
|
let ensuredTray = false;
|
||||||
const handleInitialArgs = createHandleInitialArgsHandler({
|
const handleInitialArgs = createHandleInitialArgsHandler({
|
||||||
getInitialArgs: () => ({ start: true } as never),
|
getInitialArgs: () => ({ start: true }) as never,
|
||||||
isBackgroundMode: () => true,
|
isBackgroundMode: () => true,
|
||||||
ensureTray: () => {
|
ensureTray: () => {
|
||||||
ensuredTray = true;
|
ensuredTray = true;
|
||||||
@@ -44,7 +44,7 @@ test('initial args handler auto-connects mpv when needed', () => {
|
|||||||
let connectCalls = 0;
|
let connectCalls = 0;
|
||||||
let logged = false;
|
let logged = false;
|
||||||
const handleInitialArgs = createHandleInitialArgsHandler({
|
const handleInitialArgs = createHandleInitialArgsHandler({
|
||||||
getInitialArgs: () => ({ start: true } as never),
|
getInitialArgs: () => ({ start: true }) as never,
|
||||||
isBackgroundMode: () => false,
|
isBackgroundMode: () => false,
|
||||||
ensureTray: () => {},
|
ensureTray: () => {},
|
||||||
isTexthookerOnlyMode: () => false,
|
isTexthookerOnlyMode: () => false,
|
||||||
@@ -69,7 +69,7 @@ test('initial args handler auto-connects mpv when needed', () => {
|
|||||||
test('initial args handler forwards args to cli handler', () => {
|
test('initial args handler forwards args to cli handler', () => {
|
||||||
const seenSources: string[] = [];
|
const seenSources: string[] = [];
|
||||||
const handleInitialArgs = createHandleInitialArgsHandler({
|
const handleInitialArgs = createHandleInitialArgsHandler({
|
||||||
getInitialArgs: () => ({ start: true } as never),
|
getInitialArgs: () => ({ start: true }) as never,
|
||||||
isBackgroundMode: () => false,
|
isBackgroundMode: () => false,
|
||||||
ensureTray: () => {},
|
ensureTray: () => {},
|
||||||
isTexthookerOnlyMode: () => false,
|
isTexthookerOnlyMode: () => false,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { createInitialArgsRuntimeHandler } from './initial-args-runtime-handler'
|
|||||||
test('initial args runtime handler composes main deps and runs initial command flow', () => {
|
test('initial args runtime handler composes main deps and runs initial command flow', () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const handleInitialArgs = createInitialArgsRuntimeHandler({
|
const handleInitialArgs = createInitialArgsRuntimeHandler({
|
||||||
getInitialArgs: () => ({ start: true } as never),
|
getInitialArgs: () => ({ start: true }) as never,
|
||||||
isBackgroundMode: () => true,
|
isBackgroundMode: () => true,
|
||||||
ensureTray: () => calls.push('tray'),
|
ensureTray: () => calls.push('tray'),
|
||||||
isTexthookerOnlyMode: () => false,
|
isTexthookerOnlyMode: () => false,
|
||||||
@@ -20,5 +20,10 @@ test('initial args runtime handler composes main deps and runs initial command f
|
|||||||
|
|
||||||
handleInitialArgs();
|
handleInitialArgs();
|
||||||
|
|
||||||
assert.deepEqual(calls, ['tray', 'log:Auto-connecting MPV client for immersion tracking', 'connect', 'cli:initial']);
|
assert.deepEqual(calls, [
|
||||||
|
'tray',
|
||||||
|
'log:Auto-connecting MPV client for immersion tracking',
|
||||||
|
'connect',
|
||||||
|
'cli:initial',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { MpvCommandFromIpcRuntimeDeps } from '../ipc-mpv-command';
|
import type { MpvCommandFromIpcRuntimeDeps } from '../ipc-mpv-command';
|
||||||
|
|
||||||
export function createBuildMpvCommandFromIpcRuntimeMainDepsHandler(deps: MpvCommandFromIpcRuntimeDeps) {
|
export function createBuildMpvCommandFromIpcRuntimeMainDepsHandler(
|
||||||
|
deps: MpvCommandFromIpcRuntimeDeps,
|
||||||
|
) {
|
||||||
return (): MpvCommandFromIpcRuntimeDeps => ({
|
return (): MpvCommandFromIpcRuntimeDeps => ({
|
||||||
triggerSubsyncFromConfig: () => deps.triggerSubsyncFromConfig(),
|
triggerSubsyncFromConfig: () => deps.triggerSubsyncFromConfig(),
|
||||||
openRuntimeOptionsPalette: () => deps.openRuntimeOptionsPalette(),
|
openRuntimeOptionsPalette: () => deps.openRuntimeOptionsPalette(),
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { createHandleMpvCommandFromIpcHandler, createRunSubsyncManualFromIpcHandler } from './ipc-bridge-actions';
|
import {
|
||||||
|
createHandleMpvCommandFromIpcHandler,
|
||||||
|
createRunSubsyncManualFromIpcHandler,
|
||||||
|
} from './ipc-bridge-actions';
|
||||||
import {
|
import {
|
||||||
createBuildHandleMpvCommandFromIpcMainDepsHandler,
|
createBuildHandleMpvCommandFromIpcMainDepsHandler,
|
||||||
createBuildRunSubsyncManualFromIpcMainDepsHandler,
|
createBuildRunSubsyncManualFromIpcMainDepsHandler,
|
||||||
@@ -22,10 +25,10 @@ export function createIpcRuntimeHandlers<TRequest, TResult>(deps: {
|
|||||||
handleMpvCommandFromIpcMainDeps,
|
handleMpvCommandFromIpcMainDeps,
|
||||||
);
|
);
|
||||||
|
|
||||||
const runSubsyncManualFromIpcMainDeps =
|
const runSubsyncManualFromIpcMainDeps = createBuildRunSubsyncManualFromIpcMainDepsHandler<
|
||||||
createBuildRunSubsyncManualFromIpcMainDepsHandler<TRequest, TResult>(
|
TRequest,
|
||||||
deps.runSubsyncManualFromIpcDeps,
|
TResult
|
||||||
)();
|
>(deps.runSubsyncManualFromIpcDeps)();
|
||||||
const runSubsyncManualFromIpc = createRunSubsyncManualFromIpcHandler<TRequest, TResult>(
|
const runSubsyncManualFromIpc = createRunSubsyncManualFromIpcHandler<TRequest, TResult>(
|
||||||
runSubsyncManualFromIpcMainDeps,
|
runSubsyncManualFromIpcMainDeps,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -94,7 +94,11 @@ export function createHandleJellyfinListCommands(deps: {
|
|||||||
if (!args.jellyfinItemId) {
|
if (!args.jellyfinItemId) {
|
||||||
throw new Error('Missing --jellyfin-item-id for --jellyfin-subtitles.');
|
throw new Error('Missing --jellyfin-item-id for --jellyfin-subtitles.');
|
||||||
}
|
}
|
||||||
const tracks = await deps.listJellyfinSubtitleTracks(session, clientInfo, args.jellyfinItemId);
|
const tracks = await deps.listJellyfinSubtitleTracks(
|
||||||
|
session,
|
||||||
|
clientInfo,
|
||||||
|
args.jellyfinItemId,
|
||||||
|
);
|
||||||
if (tracks.length === 0) {
|
if (tracks.length === 0) {
|
||||||
deps.logInfo('No Jellyfin subtitle tracks found for item.');
|
deps.logInfo('No Jellyfin subtitle tracks found for item.');
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
import type {
|
import type { createHandleJellyfinAuthCommands } from './jellyfin-cli-auth';
|
||||||
createHandleJellyfinAuthCommands,
|
import type { createHandleJellyfinListCommands } from './jellyfin-cli-list';
|
||||||
} from './jellyfin-cli-auth';
|
import type { createHandleJellyfinPlayCommand } from './jellyfin-cli-play';
|
||||||
import type {
|
import type { createHandleJellyfinRemoteAnnounceCommand } from './jellyfin-cli-remote-announce';
|
||||||
createHandleJellyfinListCommands,
|
|
||||||
} from './jellyfin-cli-list';
|
|
||||||
import type {
|
|
||||||
createHandleJellyfinPlayCommand,
|
|
||||||
} from './jellyfin-cli-play';
|
|
||||||
import type {
|
|
||||||
createHandleJellyfinRemoteAnnounceCommand,
|
|
||||||
} from './jellyfin-cli-remote-announce';
|
|
||||||
|
|
||||||
type HandleJellyfinAuthCommandsMainDeps = Parameters<typeof createHandleJellyfinAuthCommands>[0];
|
type HandleJellyfinAuthCommandsMainDeps = Parameters<typeof createHandleJellyfinAuthCommands>[0];
|
||||||
type HandleJellyfinListCommandsMainDeps = Parameters<typeof createHandleJellyfinListCommands>[0];
|
type HandleJellyfinListCommandsMainDeps = Parameters<typeof createHandleJellyfinListCommands>[0];
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import type {
|
|||||||
createGetResolvedJellyfinConfigHandler,
|
createGetResolvedJellyfinConfigHandler,
|
||||||
} from './jellyfin-client-info';
|
} from './jellyfin-client-info';
|
||||||
|
|
||||||
type GetResolvedJellyfinConfigMainDeps = Parameters<typeof createGetResolvedJellyfinConfigHandler>[0];
|
type GetResolvedJellyfinConfigMainDeps = Parameters<
|
||||||
|
typeof createGetResolvedJellyfinConfigHandler
|
||||||
|
>[0];
|
||||||
type GetJellyfinClientInfoMainDeps = Parameters<typeof createGetJellyfinClientInfoHandler>[0];
|
type GetJellyfinClientInfoMainDeps = Parameters<typeof createGetJellyfinClientInfoHandler>[0];
|
||||||
|
|
||||||
export function createBuildGetResolvedJellyfinConfigMainDepsHandler(
|
export function createBuildGetResolvedJellyfinConfigMainDepsHandler(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
test('get resolved jellyfin config returns jellyfin section from resolved config', () => {
|
test('get resolved jellyfin config returns jellyfin section from resolved config', () => {
|
||||||
const jellyfin = { url: 'https://jellyfin.local' } as never;
|
const jellyfin = { url: 'https://jellyfin.local' } as never;
|
||||||
const getConfig = createGetResolvedJellyfinConfigHandler({
|
const getConfig = createGetResolvedJellyfinConfigHandler({
|
||||||
getResolvedConfig: () => ({ jellyfin } as never),
|
getResolvedConfig: () => ({ jellyfin }) as never,
|
||||||
loadStoredSession: () => null,
|
loadStoredSession: () => null,
|
||||||
getEnv: () => undefined,
|
getEnv: () => undefined,
|
||||||
});
|
});
|
||||||
@@ -68,8 +68,7 @@ test('get resolved jellyfin config uses stored user id when env token set withou
|
|||||||
},
|
},
|
||||||
}) as never,
|
}) as never,
|
||||||
loadStoredSession: () => ({ accessToken: 'stored-token', userId: 'stored-user' }),
|
loadStoredSession: () => ({ accessToken: 'stored-token', userId: 'stored-user' }),
|
||||||
getEnv: (key: string) =>
|
getEnv: (key: string) => (key === 'SUBMINER_JELLYFIN_ACCESS_TOKEN' ? 'env-token' : undefined),
|
||||||
key === 'SUBMINER_JELLYFIN_ACCESS_TOKEN' ? 'env-token' : undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(getConfig(), {
|
assert.deepEqual(getConfig(), {
|
||||||
@@ -81,7 +80,7 @@ test('get resolved jellyfin config uses stored user id when env token set withou
|
|||||||
|
|
||||||
test('jellyfin client info resolves defaults when fields are missing', () => {
|
test('jellyfin client info resolves defaults when fields are missing', () => {
|
||||||
const getClientInfo = createGetJellyfinClientInfoHandler({
|
const getClientInfo = createGetJellyfinClientInfoHandler({
|
||||||
getResolvedJellyfinConfig: () => ({ clientName: '', clientVersion: '', deviceId: '' } as never),
|
getResolvedJellyfinConfig: () => ({ clientName: '', clientVersion: '', deviceId: '' }) as never,
|
||||||
getDefaultJellyfinConfig: () =>
|
getDefaultJellyfinConfig: () =>
|
||||||
({
|
({
|
||||||
clientName: 'SubMiner',
|
clientName: 'SubMiner',
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import type { createPlayJellyfinItemInMpvHandler } from './jellyfin-playback-lau
|
|||||||
|
|
||||||
type PlayJellyfinItemInMpvMainDeps = Parameters<typeof createPlayJellyfinItemInMpvHandler>[0];
|
type PlayJellyfinItemInMpvMainDeps = Parameters<typeof createPlayJellyfinItemInMpvHandler>[0];
|
||||||
|
|
||||||
export function createBuildPlayJellyfinItemInMpvMainDepsHandler(deps: PlayJellyfinItemInMpvMainDeps) {
|
export function createBuildPlayJellyfinItemInMpvMainDepsHandler(
|
||||||
|
deps: PlayJellyfinItemInMpvMainDeps,
|
||||||
|
) {
|
||||||
return (): PlayJellyfinItemInMpvMainDeps => ({
|
return (): PlayJellyfinItemInMpvMainDeps => ({
|
||||||
ensureMpvConnectedForPlayback: () => deps.ensureMpvConnectedForPlayback(),
|
ensureMpvConnectedForPlayback: () => deps.ensureMpvConnectedForPlayback(),
|
||||||
getMpvClient: () => deps.getMpvClient(),
|
getMpvClient: () => deps.getMpvClient(),
|
||||||
|
|||||||
@@ -136,6 +136,8 @@ test('createHandleJellyfinRemoteGeneralCommand mutates active playback indices',
|
|||||||
assert.equal(playback.subtitleStreamIndex, null);
|
assert.equal(playback.subtitleStreamIndex, null);
|
||||||
assert.ok(calls.includes('progress:true'));
|
assert.ok(calls.includes('progress:true'));
|
||||||
assert.ok(
|
assert.ok(
|
||||||
calls.some((entry) => entry.includes('Ignoring unsupported Jellyfin GeneralCommand: UnsupportedCommand')),
|
calls.some((entry) =>
|
||||||
|
entry.includes('Ignoring unsupported Jellyfin GeneralCommand: UnsupportedCommand'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ export type JellyfinRemoteProgressReporterDeps = {
|
|||||||
logDebug: (message: string, error: unknown) => void;
|
logDebug: (message: string, error: unknown) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createReportJellyfinRemoteProgressHandler(deps: JellyfinRemoteProgressReporterDeps) {
|
export function createReportJellyfinRemoteProgressHandler(
|
||||||
|
deps: JellyfinRemoteProgressReporterDeps,
|
||||||
|
) {
|
||||||
return async (force = false): Promise<void> => {
|
return async (force = false): Promise<void> => {
|
||||||
const playback = deps.getActivePlayback();
|
const playback = deps.getActivePlayback();
|
||||||
if (!playback) return;
|
if (!playback) return;
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ import type {
|
|||||||
createStopJellyfinRemoteSessionHandler,
|
createStopJellyfinRemoteSessionHandler,
|
||||||
} from './jellyfin-remote-session-lifecycle';
|
} from './jellyfin-remote-session-lifecycle';
|
||||||
|
|
||||||
type StartJellyfinRemoteSessionMainDeps = Parameters<typeof createStartJellyfinRemoteSessionHandler>[0];
|
type StartJellyfinRemoteSessionMainDeps = Parameters<
|
||||||
type StopJellyfinRemoteSessionMainDeps = Parameters<typeof createStopJellyfinRemoteSessionHandler>[0];
|
typeof createStartJellyfinRemoteSessionHandler
|
||||||
|
>[0];
|
||||||
|
type StopJellyfinRemoteSessionMainDeps = Parameters<
|
||||||
|
typeof createStopJellyfinRemoteSessionHandler
|
||||||
|
>[0];
|
||||||
|
|
||||||
export function createBuildStartJellyfinRemoteSessionMainDepsHandler(
|
export function createBuildStartJellyfinRemoteSessionMainDepsHandler(
|
||||||
deps: StartJellyfinRemoteSessionMainDeps,
|
deps: StartJellyfinRemoteSessionMainDeps,
|
||||||
|
|||||||
@@ -16,7 +16,11 @@ test('open jellyfin setup window main deps builder maps callbacks', async () =>
|
|||||||
accessToken: 'token',
|
accessToken: 'token',
|
||||||
userId: 'uid',
|
userId: 'uid',
|
||||||
}),
|
}),
|
||||||
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'dev' }),
|
getJellyfinClientInfo: () => ({
|
||||||
|
clientName: 'SubMiner',
|
||||||
|
clientVersion: '1.0',
|
||||||
|
deviceId: 'dev',
|
||||||
|
}),
|
||||||
saveStoredSession: () => calls.push('save'),
|
saveStoredSession: () => calls.push('save'),
|
||||||
patchJellyfinConfig: () => calls.push('patch'),
|
patchJellyfinConfig: () => calls.push('patch'),
|
||||||
logInfo: (message) => calls.push(`info:${message}`),
|
logInfo: (message) => calls.push(`info:${message}`),
|
||||||
@@ -38,12 +42,15 @@ test('open jellyfin setup window main deps builder maps callbacks', async () =>
|
|||||||
username: 'u',
|
username: 'u',
|
||||||
password: 'p',
|
password: 'p',
|
||||||
});
|
});
|
||||||
assert.deepEqual(await deps.authenticateWithPassword('s', 'u', 'p', deps.getJellyfinClientInfo()), {
|
assert.deepEqual(
|
||||||
|
await deps.authenticateWithPassword('s', 'u', 'p', deps.getJellyfinClientInfo()),
|
||||||
|
{
|
||||||
serverUrl: 'http://127.0.0.1:8096',
|
serverUrl: 'http://127.0.0.1:8096',
|
||||||
username: 'alice',
|
username: 'alice',
|
||||||
accessToken: 'token',
|
accessToken: 'token',
|
||||||
userId: 'uid',
|
userId: 'uid',
|
||||||
});
|
},
|
||||||
|
);
|
||||||
deps.saveStoredSession({ accessToken: 'token', userId: 'uid' });
|
deps.saveStoredSession({ accessToken: 'token', userId: 'uid' });
|
||||||
deps.patchJellyfinConfig({
|
deps.patchJellyfinConfig({
|
||||||
serverUrl: 'http://127.0.0.1:8096',
|
serverUrl: 'http://127.0.0.1:8096',
|
||||||
@@ -57,5 +64,13 @@ test('open jellyfin setup window main deps builder maps callbacks', async () =>
|
|||||||
deps.clearSetupWindow();
|
deps.clearSetupWindow();
|
||||||
deps.setSetupWindow({} as never);
|
deps.setSetupWindow({} as never);
|
||||||
assert.equal(deps.encodeURIComponent('a b'), 'a%20b');
|
assert.equal(deps.encodeURIComponent('a b'), 'a%20b');
|
||||||
assert.deepEqual(calls, ['save', 'patch', 'info:ok', 'error:bad', 'osd:toast', 'clear', 'set-window']);
|
assert.deepEqual(calls, [
|
||||||
|
'save',
|
||||||
|
'patch',
|
||||||
|
'info:ok',
|
||||||
|
'error:bad',
|
||||||
|
'osd:toast',
|
||||||
|
'clear',
|
||||||
|
'set-window',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,7 +50,11 @@ test('createHandleJellyfinSetupSubmissionHandler applies successful login', asyn
|
|||||||
accessToken: 'token',
|
accessToken: 'token',
|
||||||
userId: 'uid',
|
userId: 'uid',
|
||||||
}),
|
}),
|
||||||
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }),
|
getJellyfinClientInfo: () => ({
|
||||||
|
clientName: 'SubMiner',
|
||||||
|
clientVersion: '1.0',
|
||||||
|
deviceId: 'did',
|
||||||
|
}),
|
||||||
saveStoredSession: (session) => {
|
saveStoredSession: (session) => {
|
||||||
savedSession = session;
|
savedSession = session;
|
||||||
calls.push('save');
|
calls.push('save');
|
||||||
@@ -86,7 +90,11 @@ test('createHandleJellyfinSetupSubmissionHandler reports failure to OSD', async
|
|||||||
authenticateWithPassword: async () => {
|
authenticateWithPassword: async () => {
|
||||||
throw new Error('bad credentials');
|
throw new Error('bad credentials');
|
||||||
},
|
},
|
||||||
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }),
|
getJellyfinClientInfo: () => ({
|
||||||
|
clientName: 'SubMiner',
|
||||||
|
clientVersion: '1.0',
|
||||||
|
deviceId: 'did',
|
||||||
|
}),
|
||||||
saveStoredSession: () => calls.push('save'),
|
saveStoredSession: () => calls.push('save'),
|
||||||
patchJellyfinConfig: () => calls.push('patch'),
|
patchJellyfinConfig: () => calls.push('patch'),
|
||||||
logInfo: () => calls.push('info'),
|
logInfo: () => calls.push('info'),
|
||||||
@@ -180,7 +188,11 @@ test('createOpenJellyfinSetupWindowHandler no-ops when existing setup window is
|
|||||||
authenticateWithPassword: async () => {
|
authenticateWithPassword: async () => {
|
||||||
throw new Error('should not auth');
|
throw new Error('should not auth');
|
||||||
},
|
},
|
||||||
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }),
|
getJellyfinClientInfo: () => ({
|
||||||
|
clientName: 'SubMiner',
|
||||||
|
clientVersion: '1.0',
|
||||||
|
deviceId: 'did',
|
||||||
|
}),
|
||||||
saveStoredSession: () => {},
|
saveStoredSession: () => {},
|
||||||
patchJellyfinConfig: () => {},
|
patchJellyfinConfig: () => {},
|
||||||
logInfo: () => {},
|
logInfo: () => {},
|
||||||
@@ -196,14 +208,18 @@ test('createOpenJellyfinSetupWindowHandler no-ops when existing setup window is
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window lifecycle', async () => {
|
test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window lifecycle', async () => {
|
||||||
let willNavigateHandler: ((event: { preventDefault: () => void }, url: string) => void) | null = null;
|
let willNavigateHandler: ((event: { preventDefault: () => void }, url: string) => void) | null =
|
||||||
|
null;
|
||||||
let closedHandler: (() => void) | null = null;
|
let closedHandler: (() => void) | null = null;
|
||||||
let prevented = false;
|
let prevented = false;
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const fakeWindow = {
|
const fakeWindow = {
|
||||||
focus: () => {},
|
focus: () => {},
|
||||||
webContents: {
|
webContents: {
|
||||||
on: (event: 'will-navigate', handler: (event: { preventDefault: () => void }, url: string) => void) => {
|
on: (
|
||||||
|
event: 'will-navigate',
|
||||||
|
handler: (event: { preventDefault: () => void }, url: string) => void,
|
||||||
|
) => {
|
||||||
if (event === 'will-navigate') {
|
if (event === 'will-navigate') {
|
||||||
willNavigateHandler = handler;
|
willNavigateHandler = handler;
|
||||||
}
|
}
|
||||||
@@ -233,7 +249,11 @@ test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window li
|
|||||||
accessToken: 'token',
|
accessToken: 'token',
|
||||||
userId: 'uid',
|
userId: 'uid',
|
||||||
}),
|
}),
|
||||||
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }),
|
getJellyfinClientInfo: () => ({
|
||||||
|
clientName: 'SubMiner',
|
||||||
|
clientVersion: '1.0',
|
||||||
|
deviceId: 'did',
|
||||||
|
}),
|
||||||
saveStoredSession: () => calls.push('save'),
|
saveStoredSession: () => calls.push('save'),
|
||||||
patchJellyfinConfig: () => calls.push('patch'),
|
patchJellyfinConfig: () => calls.push('patch'),
|
||||||
logInfo: () => calls.push('info'),
|
logInfo: () => calls.push('info'),
|
||||||
@@ -249,7 +269,9 @@ test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window li
|
|||||||
assert.ok(closedHandler);
|
assert.ok(closedHandler);
|
||||||
assert.deepEqual(calls.slice(0, 2), ['load:data-url', 'set-window']);
|
assert.deepEqual(calls.slice(0, 2), ['load:data-url', 'set-window']);
|
||||||
|
|
||||||
const navHandler = willNavigateHandler as ((event: { preventDefault: () => void }, url: string) => void) | null;
|
const navHandler = willNavigateHandler as
|
||||||
|
| ((event: { preventDefault: () => void }, url: string) => void)
|
||||||
|
| null;
|
||||||
if (!navHandler) {
|
if (!navHandler) {
|
||||||
throw new Error('missing will-navigate handler');
|
throw new Error('missing will-navigate handler');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ export function parseJellyfinSetupSubmissionUrl(rawUrl: string): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createHandleJellyfinSetupSubmissionHandler(deps: {
|
export function createHandleJellyfinSetupSubmissionHandler(deps: {
|
||||||
parseSubmissionUrl: (rawUrl: string) => { server: string; username: string; password: string } | null;
|
parseSubmissionUrl: (
|
||||||
|
rawUrl: string,
|
||||||
|
) => { server: string; username: string; password: string } | null;
|
||||||
authenticateWithPassword: (
|
authenticateWithPassword: (
|
||||||
server: string,
|
server: string,
|
||||||
username: string,
|
username: string,
|
||||||
@@ -179,20 +181,22 @@ export function createHandleJellyfinSetupWindowClosedHandler(deps: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createHandleJellyfinSetupWindowOpenedHandler(deps: {
|
export function createHandleJellyfinSetupWindowOpenedHandler(deps: { setSetupWindow: () => void }) {
|
||||||
setSetupWindow: () => void;
|
|
||||||
}) {
|
|
||||||
return (): void => {
|
return (): void => {
|
||||||
deps.setSetupWindow();
|
deps.setSetupWindow();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createOpenJellyfinSetupWindowHandler<TWindow extends JellyfinSetupWindowLike>(deps: {
|
export function createOpenJellyfinSetupWindowHandler<
|
||||||
|
TWindow extends JellyfinSetupWindowLike,
|
||||||
|
>(deps: {
|
||||||
maybeFocusExistingSetupWindow: () => boolean;
|
maybeFocusExistingSetupWindow: () => boolean;
|
||||||
createSetupWindow: () => TWindow;
|
createSetupWindow: () => TWindow;
|
||||||
getResolvedJellyfinConfig: () => { serverUrl?: string | null; username?: string | null };
|
getResolvedJellyfinConfig: () => { serverUrl?: string | null; username?: string | null };
|
||||||
buildSetupFormHtml: (defaultServer: string, defaultUser: string) => string;
|
buildSetupFormHtml: (defaultServer: string, defaultUser: string) => string;
|
||||||
parseSubmissionUrl: (rawUrl: string) => { server: string; username: string; password: string } | null;
|
parseSubmissionUrl: (
|
||||||
|
rawUrl: string,
|
||||||
|
) => { server: string; username: string; password: string } | null;
|
||||||
authenticateWithPassword: (
|
authenticateWithPassword: (
|
||||||
server: string,
|
server: string,
|
||||||
username: string,
|
username: string,
|
||||||
@@ -258,9 +262,7 @@ export function createOpenJellyfinSetupWindowHandler<TWindow extends JellyfinSet
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
void setupWindow.loadURL(
|
void setupWindow.loadURL(`data:text/html;charset=utf-8,${deps.encodeURIComponent(formHtml)}`);
|
||||||
`data:text/html;charset=utf-8,${deps.encodeURIComponent(formHtml)}`,
|
|
||||||
);
|
|
||||||
setupWindow.on('closed', () => {
|
setupWindow.on('closed', () => {
|
||||||
handleWindowClosed();
|
handleWindowClosed();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -117,13 +117,7 @@ export function createPreloadJellyfinExternalSubtitlesHandler(deps: {
|
|||||||
seenUrls.add(track.deliveryUrl);
|
seenUrls.add(track.deliveryUrl);
|
||||||
const labelBase = (track.title || track.language || '').trim();
|
const labelBase = (track.title || track.language || '').trim();
|
||||||
const label = labelBase || `Jellyfin Subtitle ${track.index}`;
|
const label = labelBase || `Jellyfin Subtitle ${track.index}`;
|
||||||
deps.sendMpvCommand([
|
deps.sendMpvCommand(['sub-add', track.deliveryUrl, 'cached', label, track.language || '']);
|
||||||
'sub-add',
|
|
||||||
track.deliveryUrl,
|
|
||||||
'cached',
|
|
||||||
label,
|
|
||||||
track.language || '',
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await deps.wait(250);
|
await deps.wait(250);
|
||||||
|
|||||||
@@ -39,8 +39,10 @@ export function createCopyCurrentSubtitleHandler<TSubtitleTimingTracker>(deps: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createHandleMineSentenceDigitHandler<TSubtitleTimingTracker, TAnkiIntegration>(
|
export function createHandleMineSentenceDigitHandler<
|
||||||
deps: {
|
TSubtitleTimingTracker,
|
||||||
|
TAnkiIntegration,
|
||||||
|
>(deps: {
|
||||||
getSubtitleTimingTracker: () => TSubtitleTimingTracker;
|
getSubtitleTimingTracker: () => TSubtitleTimingTracker;
|
||||||
getAnkiIntegration: () => TAnkiIntegration;
|
getAnkiIntegration: () => TAnkiIntegration;
|
||||||
getCurrentSecondarySubText: () => string | undefined;
|
getCurrentSecondarySubText: () => string | undefined;
|
||||||
@@ -58,8 +60,7 @@ export function createHandleMineSentenceDigitHandler<TSubtitleTimingTracker, TAn
|
|||||||
onCardsMined: (count: number) => void;
|
onCardsMined: (count: number) => void;
|
||||||
},
|
},
|
||||||
) => void;
|
) => void;
|
||||||
},
|
}) {
|
||||||
) {
|
|
||||||
return (count: number): void => {
|
return (count: number): void => {
|
||||||
deps.handleMineSentenceDigitCore(count, {
|
deps.handleMineSentenceDigitCore(count, {
|
||||||
subtitleTimingTracker: deps.getSubtitleTimingTracker(),
|
subtitleTimingTracker: deps.getSubtitleTimingTracker(),
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ test('mpv runtime service main deps builder maps state and callbacks', () => {
|
|||||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
class FakeClient {
|
class FakeClient {
|
||||||
constructor(public socketPath: string, public options: unknown) {}
|
constructor(
|
||||||
|
public socketPath: string,
|
||||||
|
public options: unknown,
|
||||||
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const build = createBuildMpvClientRuntimeServiceFactoryDepsHandler({
|
const build = createBuildMpvClientRuntimeServiceFactoryDepsHandler({
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ export function createBuildMpvClientRuntimeServiceFactoryDepsHandler<
|
|||||||
setOverlayVisible: (visible: boolean) => deps.setOverlayVisible(visible),
|
setOverlayVisible: (visible: boolean) => deps.setOverlayVisible(visible),
|
||||||
isVisibleOverlayVisible: () => deps.isVisibleOverlayVisible(),
|
isVisibleOverlayVisible: () => deps.isVisibleOverlayVisible(),
|
||||||
getReconnectTimer: () => deps.getReconnectTimer(),
|
getReconnectTimer: () => deps.getReconnectTimer(),
|
||||||
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => deps.setReconnectTimer(timer),
|
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) =>
|
||||||
|
deps.setReconnectTimer(timer),
|
||||||
},
|
},
|
||||||
bindEventHandlers: (client: TClient) => deps.bindEventHandlers(client),
|
bindEventHandlers: (client: TClient) => deps.bindEventHandlers(client),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ export function createBuildApplyJellyfinMpvDefaultsMainDepsHandler(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBuildGetDefaultSocketPathMainDepsHandler(
|
export function createBuildGetDefaultSocketPathMainDepsHandler(deps: GetDefaultSocketPathMainDeps) {
|
||||||
deps: GetDefaultSocketPathMainDeps,
|
|
||||||
) {
|
|
||||||
return (): GetDefaultSocketPathMainDeps => ({
|
return (): GetDefaultSocketPathMainDeps => ({
|
||||||
platform: deps.platform,
|
platform: deps.platform,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -97,8 +97,7 @@ export function createBindMpvMainEventHandlersHandler(deps: {
|
|||||||
const handleMpvMediaPathChange = createHandleMpvMediaPathChangeHandler({
|
const handleMpvMediaPathChange = createHandleMpvMediaPathChangeHandler({
|
||||||
updateCurrentMediaPath: (path) => deps.updateCurrentMediaPath(path),
|
updateCurrentMediaPath: (path) => deps.updateCurrentMediaPath(path),
|
||||||
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
|
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
|
||||||
restoreMpvSubVisibility: () =>
|
restoreMpvSubVisibility: () => deps.restoreMpvSubVisibility(),
|
||||||
deps.restoreMpvSubVisibility(),
|
|
||||||
getCurrentAnilistMediaKey: () => deps.getCurrentAnilistMediaKey(),
|
getCurrentAnilistMediaKey: () => deps.getCurrentAnilistMediaKey(),
|
||||||
resetAnilistMediaTracking: (mediaKey) => deps.resetAnilistMediaTracking(mediaKey),
|
resetAnilistMediaTracking: (mediaKey) => deps.resetAnilistMediaTracking(mediaKey),
|
||||||
maybeProbeAnilistDuration: (mediaKey) => deps.maybeProbeAnilistDuration(mediaKey),
|
maybeProbeAnilistDuration: (mediaKey) => deps.maybeProbeAnilistDuration(mediaKey),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { createUpdateMpvSubtitleRenderMetricsHandler } from './mpv-subtitle-render-metrics';
|
import type { createUpdateMpvSubtitleRenderMetricsHandler } from './mpv-subtitle-render-metrics';
|
||||||
|
|
||||||
type UpdateMpvSubtitleRenderMetricsMainDeps = Parameters<typeof createUpdateMpvSubtitleRenderMetricsHandler>[0];
|
type UpdateMpvSubtitleRenderMetricsMainDeps = Parameters<
|
||||||
|
typeof createUpdateMpvSubtitleRenderMetricsHandler
|
||||||
|
>[0];
|
||||||
|
|
||||||
export function createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler(
|
export function createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler(
|
||||||
deps: UpdateMpvSubtitleRenderMetricsMainDeps,
|
deps: UpdateMpvSubtitleRenderMetricsMainDeps,
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ test('numeric shortcut runtime main deps builder maps callbacks', () => {
|
|||||||
},
|
},
|
||||||
})();
|
})();
|
||||||
|
|
||||||
assert.equal(deps.globalShortcut.register('1', () => {}), true);
|
assert.equal(
|
||||||
|
deps.globalShortcut.register('1', () => {}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
deps.globalShortcut.unregister('1');
|
deps.globalShortcut.unregister('1');
|
||||||
deps.showMpvOsd('x');
|
deps.showMpvOsd('x');
|
||||||
deps.setTimer(() => calls.push('tick'), 1000);
|
deps.setTimer(() => calls.push('tick'), 1000);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { NumericShortcutRuntimeOptions } from '../../core/services/numeric-shortcut';
|
import type { NumericShortcutRuntimeOptions } from '../../core/services/numeric-shortcut';
|
||||||
|
|
||||||
export function createBuildNumericShortcutRuntimeMainDepsHandler(deps: NumericShortcutRuntimeOptions) {
|
export function createBuildNumericShortcutRuntimeMainDepsHandler(
|
||||||
|
deps: NumericShortcutRuntimeOptions,
|
||||||
|
) {
|
||||||
return (): NumericShortcutRuntimeOptions => ({
|
return (): NumericShortcutRuntimeOptions => ({
|
||||||
globalShortcut: deps.globalShortcut,
|
globalShortcut: deps.globalShortcut,
|
||||||
showMpvOsd: (text: string) => deps.showMpvOsd(text),
|
showMpvOsd: (text: string) => deps.showMpvOsd(text),
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ import type {
|
|||||||
createStartNumericShortcutSessionHandler,
|
createStartNumericShortcutSessionHandler,
|
||||||
} from './numeric-shortcut-session-handlers';
|
} from './numeric-shortcut-session-handlers';
|
||||||
|
|
||||||
type CancelNumericShortcutSessionMainDeps = Parameters<typeof createCancelNumericShortcutSessionHandler>[0];
|
type CancelNumericShortcutSessionMainDeps = Parameters<
|
||||||
type StartNumericShortcutSessionMainDeps = Parameters<typeof createStartNumericShortcutSessionHandler>[0];
|
typeof createCancelNumericShortcutSessionHandler
|
||||||
|
>[0];
|
||||||
|
type StartNumericShortcutSessionMainDeps = Parameters<
|
||||||
|
typeof createStartNumericShortcutSessionHandler
|
||||||
|
>[0];
|
||||||
|
|
||||||
export function createBuildCancelNumericShortcutSessionMainDepsHandler(
|
export function createBuildCancelNumericShortcutSessionMainDepsHandler(
|
||||||
deps: CancelNumericShortcutSessionMainDeps,
|
deps: CancelNumericShortcutSessionMainDeps,
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ export function createNumericShortcutSessionRuntimeHandlers(deps: {
|
|||||||
const cancelPendingMultiCopyMainDeps = createBuildCancelNumericShortcutSessionMainDepsHandler({
|
const cancelPendingMultiCopyMainDeps = createBuildCancelNumericShortcutSessionMainDepsHandler({
|
||||||
session: deps.multiCopySession,
|
session: deps.multiCopySession,
|
||||||
})();
|
})();
|
||||||
const cancelPendingMultiCopyHandler =
|
const cancelPendingMultiCopyHandler = createCancelNumericShortcutSessionHandler(
|
||||||
createCancelNumericShortcutSessionHandler(cancelPendingMultiCopyMainDeps);
|
cancelPendingMultiCopyMainDeps,
|
||||||
|
);
|
||||||
|
|
||||||
const startPendingMultiCopyMainDeps = createBuildStartNumericShortcutSessionMainDepsHandler({
|
const startPendingMultiCopyMainDeps = createBuildStartNumericShortcutSessionMainDepsHandler({
|
||||||
session: deps.multiCopySession,
|
session: deps.multiCopySession,
|
||||||
@@ -32,8 +33,9 @@ export function createNumericShortcutSessionRuntimeHandlers(deps: {
|
|||||||
cancelled: 'Cancelled',
|
cancelled: 'Cancelled',
|
||||||
},
|
},
|
||||||
})();
|
})();
|
||||||
const startPendingMultiCopyHandler =
|
const startPendingMultiCopyHandler = createStartNumericShortcutSessionHandler(
|
||||||
createStartNumericShortcutSessionHandler(startPendingMultiCopyMainDeps);
|
startPendingMultiCopyMainDeps,
|
||||||
|
);
|
||||||
|
|
||||||
const cancelPendingMineSentenceMultipleMainDeps =
|
const cancelPendingMineSentenceMultipleMainDeps =
|
||||||
createBuildCancelNumericShortcutSessionMainDepsHandler({
|
createBuildCancelNumericShortcutSessionMainDepsHandler({
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ export function createBuildOverlayContentMeasurementStoreMainDepsHandler(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBuildOverlayModalRuntimeMainDepsHandler(
|
export function createBuildOverlayModalRuntimeMainDepsHandler(deps: OverlayWindowResolver) {
|
||||||
deps: OverlayWindowResolver,
|
|
||||||
) {
|
|
||||||
return (): OverlayWindowResolver => ({
|
return (): OverlayWindowResolver => ({
|
||||||
getMainWindow: () => deps.getMainWindow(),
|
getMainWindow: () => deps.getMainWindow(),
|
||||||
getModalWindow: () => deps.getModalWindow(),
|
getModalWindow: () => deps.getModalWindow(),
|
||||||
|
|||||||
@@ -35,12 +35,15 @@ test('overlay main action main deps builders map callbacks', () => {
|
|||||||
showMpvOsd: (text) => calls.push(`osd:${text}`),
|
showMpvOsd: (text) => calls.push(`osd:${text}`),
|
||||||
sendMpvCommand: (command) => calls.push(`cmd:${command.join(':')}`),
|
sendMpvCommand: (command) => calls.push(`cmd:${command.join(':')}`),
|
||||||
})();
|
})();
|
||||||
assert.deepEqual(append.appendClipboardVideoToQueueRuntime({
|
assert.deepEqual(
|
||||||
|
append.appendClipboardVideoToQueueRuntime({
|
||||||
getMpvClient: () => ({ connected: true }),
|
getMpvClient: () => ({ connected: true }),
|
||||||
readClipboardText: () => '/tmp/v.mkv',
|
readClipboardText: () => '/tmp/v.mkv',
|
||||||
showMpvOsd: () => {},
|
showMpvOsd: () => {},
|
||||||
sendMpvCommand: () => {},
|
sendMpvCommand: () => {},
|
||||||
}), { ok: true, message: 'ok' });
|
}),
|
||||||
|
{ ok: true, message: 'ok' },
|
||||||
|
);
|
||||||
assert.equal(append.readClipboardText(), '/tmp/v.mkv');
|
assert.equal(append.readClipboardText(), '/tmp/v.mkv');
|
||||||
assert.equal(typeof append.getMpvClient(), 'object');
|
assert.equal(typeof append.getMpvClient(), 'object');
|
||||||
append.showMpvOsd('queued');
|
append.showMpvOsd('queued');
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import type {
|
|||||||
type SetOverlayVisibleMainDeps = Parameters<typeof createSetOverlayVisibleHandler>[0];
|
type SetOverlayVisibleMainDeps = Parameters<typeof createSetOverlayVisibleHandler>[0];
|
||||||
type ToggleOverlayMainDeps = Parameters<typeof createToggleOverlayHandler>[0];
|
type ToggleOverlayMainDeps = Parameters<typeof createToggleOverlayHandler>[0];
|
||||||
type HandleOverlayModalClosedMainDeps = Parameters<typeof createHandleOverlayModalClosedHandler>[0];
|
type HandleOverlayModalClosedMainDeps = Parameters<typeof createHandleOverlayModalClosedHandler>[0];
|
||||||
type AppendClipboardVideoToQueueMainDeps = Parameters<typeof createAppendClipboardVideoToQueueHandler>[0];
|
type AppendClipboardVideoToQueueMainDeps = Parameters<
|
||||||
|
typeof createAppendClipboardVideoToQueueHandler
|
||||||
|
>[0];
|
||||||
|
|
||||||
export function createBuildSetOverlayVisibleMainDepsHandler(deps: SetOverlayVisibleMainDeps) {
|
export function createBuildSetOverlayVisibleMainDepsHandler(deps: SetOverlayVisibleMainDeps) {
|
||||||
return (): SetOverlayVisibleMainDeps => ({
|
return (): SetOverlayVisibleMainDeps => ({
|
||||||
@@ -34,7 +36,8 @@ export function createBuildAppendClipboardVideoToQueueMainDepsHandler(
|
|||||||
deps: AppendClipboardVideoToQueueMainDeps,
|
deps: AppendClipboardVideoToQueueMainDeps,
|
||||||
) {
|
) {
|
||||||
return (): AppendClipboardVideoToQueueMainDeps => ({
|
return (): AppendClipboardVideoToQueueMainDeps => ({
|
||||||
appendClipboardVideoToQueueRuntime: (options) => deps.appendClipboardVideoToQueueRuntime(options),
|
appendClipboardVideoToQueueRuntime: (options) =>
|
||||||
|
deps.appendClipboardVideoToQueueRuntime(options),
|
||||||
getMpvClient: () => deps.getMpvClient(),
|
getMpvClient: () => deps.getMpvClient(),
|
||||||
readClipboardText: () => deps.readClipboardText(),
|
readClipboardText: () => deps.readClipboardText(),
|
||||||
showMpvOsd: (text: string) => deps.showMpvOsd(text),
|
showMpvOsd: (text: string) => deps.showMpvOsd(text),
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ export function createSetOverlayVisibleHandler(deps: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createToggleOverlayHandler(deps: {
|
export function createToggleOverlayHandler(deps: { toggleVisibleOverlay: () => void }) {
|
||||||
toggleVisibleOverlay: () => void;
|
|
||||||
}) {
|
|
||||||
return (): void => {
|
return (): void => {
|
||||||
deps.toggleVisibleOverlay();
|
deps.toggleVisibleOverlay();
|
||||||
};
|
};
|
||||||
@@ -26,9 +24,10 @@ export function createHandleOverlayModalClosedHandler(deps: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createAppendClipboardVideoToQueueHandler(deps: {
|
export function createAppendClipboardVideoToQueueHandler(deps: {
|
||||||
appendClipboardVideoToQueueRuntime: (
|
appendClipboardVideoToQueueRuntime: (options: AppendClipboardVideoToQueueRuntimeDeps) => {
|
||||||
options: AppendClipboardVideoToQueueRuntimeDeps,
|
ok: boolean;
|
||||||
) => { ok: boolean; message: string };
|
message: string;
|
||||||
|
};
|
||||||
getMpvClient: () => AppendClipboardVideoToQueueRuntimeDeps['getMpvClient'] extends () => infer T
|
getMpvClient: () => AppendClipboardVideoToQueueRuntimeDeps['getMpvClient'] extends () => infer T
|
||||||
? T
|
? T
|
||||||
: never;
|
: never;
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ function parseSubVisibility(value: unknown): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function createEnsureOverlayMpvSubtitlesHiddenHandler(deps: {
|
export function createEnsureOverlayMpvSubtitlesHiddenHandler(deps: {
|
||||||
getMpvClient: () => MpvVisibilityClient | null;
|
getMpvClient: () => MpvVisibilityClient | null;
|
||||||
getSavedSubVisibility: () => boolean | null;
|
getSavedSubVisibility: () => boolean | null;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user