mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
fix(shortcuts): gate feature-dependent bindings
Disable Anki-dependent shortcuts when AnkiConnect is off and require jellyfin.enabled for remote startup warmups to avoid initializing disabled integrations.
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
---
|
---
|
||||||
id: TASK-84
|
id: TASK-84
|
||||||
title: Gate feature-dependent keybindings behind config flags
|
title: Gate feature-dependent keybindings behind config flags
|
||||||
status: To Do
|
status: Done
|
||||||
assignee: []
|
assignee:
|
||||||
|
- opencode-task84-keybindings-gating
|
||||||
created_date: '2026-02-19 08:41'
|
created_date: '2026-02-19 08:41'
|
||||||
|
updated_date: '2026-02-22 01:35'
|
||||||
labels: []
|
labels: []
|
||||||
dependencies: []
|
dependencies: []
|
||||||
priority: medium
|
priority: medium
|
||||||
@@ -17,9 +19,43 @@ Ensure feature-specific keybindings only work when their related feature is enab
|
|||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
<!-- AC:BEGIN -->
|
<!-- AC:BEGIN -->
|
||||||
- [ ] #1 Feature-dependent keybindings are effectively disabled when their corresponding feature flag/config is off, with no user-facing error dialogs.
|
- [x] #1 Feature-dependent keybindings are effectively disabled when their corresponding feature flag/config is off, with no user-facing error dialogs.
|
||||||
- [ ] #2 When a feature is disabled in config, its related integration code is not loaded or initialized during startup (including Jellyfin as a concrete case).
|
- [x] #2 When a feature is disabled in config, its related integration code is not loaded or initialized during startup (including Jellyfin as a concrete case).
|
||||||
- [ ] #3 When a feature is enabled, existing keybinding behavior continues to work as expected.
|
- [x] #3 When a feature is enabled, existing keybinding behavior continues to work as expected.
|
||||||
- [ ] #4 Automated tests cover enabled and disabled paths for keybinding gating and disabled-integration loading behavior.
|
- [x] #4 Automated tests cover enabled and disabled paths for keybinding gating and disabled-integration loading behavior.
|
||||||
- [ ] #5 User-facing docs/config guidance explain that feature keybindings require the corresponding feature to be enabled.
|
- [x] #5 User-facing docs/config guidance explain that feature keybindings require the corresponding feature to be enabled.
|
||||||
<!-- AC:END -->
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
<!-- SECTION:PLAN:BEGIN -->
|
||||||
|
1) Add feature-gated shortcut resolution in `src/core/utils/shortcut-config.ts` so Anki-dependent shortcuts resolve to `null` when `ankiConnect.enabled` is false, with focused tests in `src/core/utils/shortcut-config.test.ts`.
|
||||||
|
2) Gate Jellyfin startup integration in `src/main/runtime/jellyfin-remote-session-lifecycle.ts` and startup warmup wiring in `src/main.ts` so remote session initialization requires `jellyfin.enabled`, `jellyfin.remoteControlEnabled`, and `jellyfin.remoteControlAutoConnect`.
|
||||||
|
3) Expand automated coverage for enabled/disabled paths (shortcut gating + Jellyfin startup no-op), then include new shortcut gating tests in `test:core:src`.
|
||||||
|
4) Update `docs/configuration.md` to clarify feature-dependent shortcuts require enabled features and Jellyfin remote autoconnect prerequisites.
|
||||||
|
5) Run verification (`bun test` focused suites, `bun run test:core:src`, `bun run docs:build`) and finalize TASK-84 notes/AC/final summary (no commit).
|
||||||
|
<!-- SECTION:PLAN:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
Plan captured in docs/plans/2026-02-22-task-84-feature-keybinding-gates.md. Proceeding immediately with execution via executing-plans flow per user request.
|
||||||
|
|
||||||
|
Implemented feature-dependent shortcut gating in `src/core/utils/shortcut-config.ts`: when `ankiConnect.enabled` is false, Anki/Kiku-dependent shortcuts resolve to `null` (`updateLastCardFromClipboard`, `triggerFieldGrouping`, `mineSentence`, `mineSentenceMultiple`, `markAudioCard`).
|
||||||
|
|
||||||
|
Added focused shortcut gating coverage in `src/core/utils/shortcut-config.test.ts` (disabled + enabled + normalization fallback), and wired this file into `test:core:src` in `package.json`.
|
||||||
|
|
||||||
|
Added disabled-integration startup guards for Jellyfin remote initialization: `createStartJellyfinRemoteSessionHandler` now early-returns when `jellyfin.enabled` is false, and startup warmup predicate in `src/main.ts` now requires `enabled && remoteControlEnabled && remoteControlAutoConnect`.
|
||||||
|
|
||||||
|
Extended Jellyfin startup tests in `src/main/runtime/jellyfin-remote-session-lifecycle.test.ts` and `src/main/runtime/startup-warmups.test.ts` to cover disabled and fully-enabled paths.
|
||||||
|
|
||||||
|
Updated docs in `docs/configuration.md` to clarify feature-dependent shortcuts require enabled integrations and Jellyfin remote autoconnect prerequisites.
|
||||||
|
|
||||||
|
Verification passed: `bun test src/core/utils/shortcut-config.test.ts src/main/runtime/jellyfin-remote-session-lifecycle.test.ts src/main/runtime/startup-warmups.test.ts src/core/services/overlay-shortcut-handler.test.ts`, `bun run test:core:src`, and `bun run docs:build`.
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Implemented config-driven feature gates for shortcuts and integration startup so feature-dependent behavior is disabled cleanly when the feature is off. Anki/Kiku-dependent overlay shortcuts now resolve to null when `ankiConnect.enabled` is false, Jellyfin remote startup now requires `jellyfin.enabled` in addition to remote-control flags, focused automated tests cover enabled/disabled paths, and configuration docs now explicitly describe these feature prerequisites.
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
|
|||||||
@@ -493,26 +493,26 @@ Jellyfin integration is optional and disabled by default. When enabled, SubMiner
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Values | Description |
|
| Option | Values | Description |
|
||||||
| -------------------------- | --------------- | --------------------------------------------------------------------------------------- |
|
| -------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||||
| `enabled` | `true`, `false` | Enable Jellyfin integration and CLI commands (default: `false`) |
|
| `enabled` | `true`, `false` | Enable Jellyfin integration and CLI commands (default: `false`) |
|
||||||
| `serverUrl` | string (URL) | Jellyfin server base URL |
|
| `serverUrl` | string (URL) | Jellyfin server base URL |
|
||||||
| `username` | string | Default username used by `--jellyfin-login` |
|
| `username` | string | Default username used by `--jellyfin-login` |
|
||||||
| `accessToken` | string | Optional explicit Jellyfin access token override; leave empty to use stored local token |
|
| `accessToken` | string | Optional explicit Jellyfin access token override; leave empty to use stored local token |
|
||||||
| `userId` | string | Jellyfin user id bound to token/session |
|
| `userId` | string | Jellyfin user id bound to token/session |
|
||||||
| `deviceId` | string | Client device id sent in auth headers (default: `subminer`) |
|
| `deviceId` | string | Client device id sent in auth headers (default: `subminer`) |
|
||||||
| `clientName` | string | Client name sent in auth headers (default: `SubMiner`) |
|
| `clientName` | string | Client name sent in auth headers (default: `SubMiner`) |
|
||||||
| `clientVersion` | string | Client version sent in auth headers (default: `0.1.0`) |
|
| `clientVersion` | string | Client version sent in auth headers (default: `0.1.0`) |
|
||||||
| `defaultLibraryId` | string | Default library id for `--jellyfin-items` when CLI value is omitted |
|
| `defaultLibraryId` | string | Default library id for `--jellyfin-items` when CLI value is omitted |
|
||||||
| `remoteControlEnabled` | `true`, `false` | Enable Jellyfin cast/remote-control session support |
|
| `remoteControlEnabled` | `true`, `false` | Enable Jellyfin cast/remote-control session support |
|
||||||
| `remoteControlAutoConnect` | `true`, `false` | Auto-connect Jellyfin remote session on app startup |
|
| `remoteControlAutoConnect` | `true`, `false` | Auto-connect Jellyfin remote session on app startup (requires `jellyfin.enabled` and `remoteControlEnabled`) |
|
||||||
| `autoAnnounce` | `true`, `false` | Auto-run cast-target visibility announce check on connect (default: `false`) |
|
| `autoAnnounce` | `true`, `false` | Auto-run cast-target visibility announce check on connect (default: `false`) |
|
||||||
| `remoteControlDeviceName` | string | Device name shown in Jellyfin cast/device lists |
|
| `remoteControlDeviceName` | string | Device name shown in Jellyfin cast/device lists |
|
||||||
| `pullPictures` | `true`, `false` | Enable poster/icon fetching for launcher Jellyfin pickers |
|
| `pullPictures` | `true`, `false` | Enable poster/icon fetching for launcher Jellyfin pickers |
|
||||||
| `iconCacheDir` | string | Cache directory for launcher-fetched Jellyfin poster icons |
|
| `iconCacheDir` | string | Cache directory for launcher-fetched Jellyfin poster icons |
|
||||||
| `directPlayPreferred` | `true`, `false` | Prefer direct stream URLs before transcoding |
|
| `directPlayPreferred` | `true`, `false` | Prefer direct stream URLs before transcoding |
|
||||||
| `directPlayContainers` | string[] | Container allowlist for direct play decisions |
|
| `directPlayContainers` | string[] | Container allowlist for direct play decisions |
|
||||||
| `transcodeVideoCodec` | string | Preferred transcode video codec fallback (default: `h264`) |
|
| `transcodeVideoCodec` | string | Preferred transcode video codec fallback (default: `h264`) |
|
||||||
|
|
||||||
When `jellyfin.accessToken` is empty, SubMiner uses the locally stored encrypted token saved from Jellyfin login/setup.
|
When `jellyfin.accessToken` is empty, SubMiner uses the locally stored encrypted token saved from Jellyfin login/setup.
|
||||||
|
|
||||||
@@ -534,6 +534,8 @@ Launcher subcommand equivalents:
|
|||||||
- `subminer jellyfin -p` opens play picker.
|
- `subminer jellyfin -p` opens play picker.
|
||||||
- `subminer jellyfin -d` starts cast discovery mode.
|
- `subminer jellyfin -d` starts cast discovery mode.
|
||||||
|
|
||||||
|
Jellyfin remote auto-connect runs only when all three are `true`: `jellyfin.enabled`, `jellyfin.remoteControlEnabled`, and `jellyfin.remoteControlAutoConnect`.
|
||||||
|
|
||||||
### Keybindings
|
### Keybindings
|
||||||
|
|
||||||
Add a `keybindings` array to configure keyboard shortcuts that send commands to mpv:
|
Add a `keybindings` array to configure keyboard shortcuts that send commands to mpv:
|
||||||
@@ -679,6 +681,8 @@ See `config.example.jsonc` for detailed configuration options.
|
|||||||
|
|
||||||
Set any shortcut to `null` to disable it.
|
Set any shortcut to `null` to disable it.
|
||||||
|
|
||||||
|
Feature-dependent shortcuts/keybindings only run when their related integration is enabled. For example, Anki/Kiku shortcuts require `ankiConnect.enabled` (and Kiku-specific behavior where applicable), and Jellyfin remote startup behavior requires Jellyfin to be enabled.
|
||||||
|
|
||||||
### Subtitle Position
|
### Subtitle Position
|
||||||
|
|
||||||
Set the initial vertical subtitle position (measured from the bottom of the screen):
|
Set the initial vertical subtitle position (measured from the bottom of the screen):
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"test:config:smoke:dist": "node --test dist/config/path-resolution.test.js",
|
"test:config:smoke:dist": "node --test dist/config/path-resolution.test.js",
|
||||||
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
||||||
"test:launcher:src": "bun test launcher/config.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/smoke.e2e.test.ts",
|
"test:launcher:src": "bun test launcher/config.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/smoke.e2e.test.ts",
|
||||||
"test:core:src": "bun test src/cli/args.test.ts src/cli/help.test.ts src/core/services/cli-command.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/config-hot-reload.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/renderer/error-recovery.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/x11-tracker.test.ts launcher/config.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts",
|
"test:core:src": "bun test src/cli/args.test.ts src/cli/help.test.ts src/core/services/cli-command.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/config-hot-reload.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/utils/shortcut-config.test.ts src/renderer/error-recovery.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/x11-tracker.test.ts launcher/config.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts",
|
||||||
"test:core:dist": "node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/config-hot-reload.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/renderer/error-recovery.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
"test:core:dist": "node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/config-hot-reload.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/renderer/error-recovery.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
||||||
"test:core:smoke:dist": "node --test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
"test:core:smoke:dist": "node --test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
||||||
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
|
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
|
||||||
|
|||||||
76
src/core/utils/shortcut-config.test.ts
Normal file
76
src/core/utils/shortcut-config.test.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import type { Config } from '../../types';
|
||||||
|
import { resolveConfiguredShortcuts } from './shortcut-config';
|
||||||
|
|
||||||
|
test('forces Anki-dependent shortcuts to null when AnkiConnect is explicitly disabled', () => {
|
||||||
|
const config: Config = {
|
||||||
|
ankiConnect: { enabled: false },
|
||||||
|
shortcuts: {
|
||||||
|
copySubtitle: 'Ctrl+KeyC',
|
||||||
|
updateLastCardFromClipboard: 'Ctrl+KeyU',
|
||||||
|
triggerFieldGrouping: 'Alt+KeyG',
|
||||||
|
mineSentence: 'Ctrl+Digit1',
|
||||||
|
mineSentenceMultiple: 'Ctrl+Digit2',
|
||||||
|
markAudioCard: 'Alt+KeyM',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const defaults: Config = {
|
||||||
|
shortcuts: {
|
||||||
|
updateLastCardFromClipboard: 'Alt+KeyL',
|
||||||
|
triggerFieldGrouping: 'Alt+KeyF',
|
||||||
|
mineSentence: 'KeyQ',
|
||||||
|
mineSentenceMultiple: 'KeyW',
|
||||||
|
markAudioCard: 'KeyE',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolved = resolveConfiguredShortcuts(config, defaults);
|
||||||
|
|
||||||
|
assert.equal(resolved.updateLastCardFromClipboard, null);
|
||||||
|
assert.equal(resolved.triggerFieldGrouping, null);
|
||||||
|
assert.equal(resolved.mineSentence, null);
|
||||||
|
assert.equal(resolved.mineSentenceMultiple, null);
|
||||||
|
assert.equal(resolved.markAudioCard, null);
|
||||||
|
assert.equal(resolved.copySubtitle, 'Ctrl+C');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps Anki-dependent shortcuts enabled and normalized when AnkiConnect is enabled', () => {
|
||||||
|
const config: Config = {
|
||||||
|
ankiConnect: { enabled: true },
|
||||||
|
shortcuts: {
|
||||||
|
updateLastCardFromClipboard: 'Ctrl+KeyU',
|
||||||
|
mineSentence: 'Ctrl+Digit1',
|
||||||
|
mineSentenceMultiple: 'Ctrl+Digit2',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const defaults: Config = {
|
||||||
|
shortcuts: {
|
||||||
|
triggerFieldGrouping: 'Alt+KeyG',
|
||||||
|
markAudioCard: 'Alt+KeyM',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolved = resolveConfiguredShortcuts(config, defaults);
|
||||||
|
|
||||||
|
assert.equal(resolved.updateLastCardFromClipboard, 'Ctrl+U');
|
||||||
|
assert.equal(resolved.triggerFieldGrouping, 'Alt+G');
|
||||||
|
assert.equal(resolved.mineSentence, 'Ctrl+1');
|
||||||
|
assert.equal(resolved.mineSentenceMultiple, 'Ctrl+2');
|
||||||
|
assert.equal(resolved.markAudioCard, 'Alt+M');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normalizes fallback shortcuts when AnkiConnect flag is unset', () => {
|
||||||
|
const config: Config = {};
|
||||||
|
const defaults: Config = {
|
||||||
|
shortcuts: {
|
||||||
|
mineSentence: 'KeyQ',
|
||||||
|
openRuntimeOptions: 'Digit9',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolved = resolveConfiguredShortcuts(config, defaults);
|
||||||
|
|
||||||
|
assert.equal(resolved.mineSentence, 'Q');
|
||||||
|
assert.equal(resolved.openRuntimeOptions, '9');
|
||||||
|
});
|
||||||
@@ -21,6 +21,8 @@ export function resolveConfiguredShortcuts(
|
|||||||
config: Config,
|
config: Config,
|
||||||
defaultConfig: Config,
|
defaultConfig: Config,
|
||||||
): ConfiguredShortcuts {
|
): ConfiguredShortcuts {
|
||||||
|
const isAnkiConnectDisabled = config.ankiConnect?.enabled === false;
|
||||||
|
|
||||||
const normalizeShortcut = (value: string | null | undefined): string | null | undefined => {
|
const normalizeShortcut = (value: string | null | undefined): string | null | undefined => {
|
||||||
if (typeof value !== 'string') return value;
|
if (typeof value !== 'string') return value;
|
||||||
return value.replace(/\bKey([A-Z])\b/g, '$1').replace(/\bDigit([0-9])\b/g, '$1');
|
return value.replace(/\bKey([A-Z])\b/g, '$1').replace(/\bDigit([0-9])\b/g, '$1');
|
||||||
@@ -42,20 +44,28 @@ export function resolveConfiguredShortcuts(
|
|||||||
config.shortcuts?.copySubtitleMultiple ?? defaultConfig.shortcuts?.copySubtitleMultiple,
|
config.shortcuts?.copySubtitleMultiple ?? defaultConfig.shortcuts?.copySubtitleMultiple,
|
||||||
),
|
),
|
||||||
updateLastCardFromClipboard: normalizeShortcut(
|
updateLastCardFromClipboard: normalizeShortcut(
|
||||||
config.shortcuts?.updateLastCardFromClipboard ??
|
isAnkiConnectDisabled
|
||||||
defaultConfig.shortcuts?.updateLastCardFromClipboard,
|
? null
|
||||||
|
: (config.shortcuts?.updateLastCardFromClipboard ??
|
||||||
|
defaultConfig.shortcuts?.updateLastCardFromClipboard),
|
||||||
),
|
),
|
||||||
triggerFieldGrouping: normalizeShortcut(
|
triggerFieldGrouping: normalizeShortcut(
|
||||||
config.shortcuts?.triggerFieldGrouping ?? defaultConfig.shortcuts?.triggerFieldGrouping,
|
isAnkiConnectDisabled
|
||||||
|
? null
|
||||||
|
: (config.shortcuts?.triggerFieldGrouping ?? defaultConfig.shortcuts?.triggerFieldGrouping),
|
||||||
),
|
),
|
||||||
triggerSubsync: normalizeShortcut(
|
triggerSubsync: normalizeShortcut(
|
||||||
config.shortcuts?.triggerSubsync ?? defaultConfig.shortcuts?.triggerSubsync,
|
config.shortcuts?.triggerSubsync ?? defaultConfig.shortcuts?.triggerSubsync,
|
||||||
),
|
),
|
||||||
mineSentence: normalizeShortcut(
|
mineSentence: normalizeShortcut(
|
||||||
config.shortcuts?.mineSentence ?? defaultConfig.shortcuts?.mineSentence,
|
isAnkiConnectDisabled
|
||||||
|
? null
|
||||||
|
: (config.shortcuts?.mineSentence ?? defaultConfig.shortcuts?.mineSentence),
|
||||||
),
|
),
|
||||||
mineSentenceMultiple: normalizeShortcut(
|
mineSentenceMultiple: normalizeShortcut(
|
||||||
config.shortcuts?.mineSentenceMultiple ?? defaultConfig.shortcuts?.mineSentenceMultiple,
|
isAnkiConnectDisabled
|
||||||
|
? null
|
||||||
|
: (config.shortcuts?.mineSentenceMultiple ?? defaultConfig.shortcuts?.mineSentenceMultiple),
|
||||||
),
|
),
|
||||||
multiCopyTimeoutMs:
|
multiCopyTimeoutMs:
|
||||||
config.shortcuts?.multiCopyTimeoutMs ?? defaultConfig.shortcuts?.multiCopyTimeoutMs ?? 5000,
|
config.shortcuts?.multiCopyTimeoutMs ?? defaultConfig.shortcuts?.multiCopyTimeoutMs ?? 5000,
|
||||||
@@ -63,7 +73,9 @@ export function resolveConfiguredShortcuts(
|
|||||||
config.shortcuts?.toggleSecondarySub ?? defaultConfig.shortcuts?.toggleSecondarySub,
|
config.shortcuts?.toggleSecondarySub ?? defaultConfig.shortcuts?.toggleSecondarySub,
|
||||||
),
|
),
|
||||||
markAudioCard: normalizeShortcut(
|
markAudioCard: normalizeShortcut(
|
||||||
config.shortcuts?.markAudioCard ?? defaultConfig.shortcuts?.markAudioCard,
|
isAnkiConnectDisabled
|
||||||
|
? null
|
||||||
|
: (config.shortcuts?.markAudioCard ?? defaultConfig.shortcuts?.markAudioCard),
|
||||||
),
|
),
|
||||||
openRuntimeOptions: normalizeShortcut(
|
openRuntimeOptions: normalizeShortcut(
|
||||||
config.shortcuts?.openRuntimeOptions ?? defaultConfig.shortcuts?.openRuntimeOptions,
|
config.shortcuts?.openRuntimeOptions ?? defaultConfig.shortcuts?.openRuntimeOptions,
|
||||||
|
|||||||
@@ -2233,7 +2233,12 @@ const {
|
|||||||
},
|
},
|
||||||
isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
|
isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
|
||||||
ensureYomitanExtensionLoaded: () => ensureYomitanExtensionLoaded().then(() => {}),
|
ensureYomitanExtensionLoaded: () => ensureYomitanExtensionLoaded().then(() => {}),
|
||||||
shouldAutoConnectJellyfinRemote: () => getResolvedConfig().jellyfin.remoteControlAutoConnect,
|
shouldAutoConnectJellyfinRemote: () => {
|
||||||
|
const jellyfin = getResolvedConfig().jellyfin;
|
||||||
|
return (
|
||||||
|
jellyfin.enabled && jellyfin.remoteControlEnabled && jellyfin.remoteControlAutoConnect
|
||||||
|
);
|
||||||
|
},
|
||||||
startJellyfinRemoteSession: () => startJellyfinRemoteSession(),
|
startJellyfinRemoteSession: () => startJellyfinRemoteSession(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
|
|
||||||
function createConfig(overrides?: Partial<Record<string, unknown>>) {
|
function createConfig(overrides?: Partial<Record<string, unknown>>) {
|
||||||
return {
|
return {
|
||||||
|
enabled: true,
|
||||||
remoteControlEnabled: true,
|
remoteControlEnabled: true,
|
||||||
remoteControlAutoConnect: true,
|
remoteControlAutoConnect: true,
|
||||||
serverUrl: 'http://localhost',
|
serverUrl: 'http://localhost',
|
||||||
@@ -21,6 +22,34 @@ function createConfig(overrides?: Partial<Record<string, unknown>>) {
|
|||||||
} as never;
|
} as never;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test('start handler no-ops when jellyfin integration is disabled', async () => {
|
||||||
|
let created = false;
|
||||||
|
const startRemote = createStartJellyfinRemoteSessionHandler({
|
||||||
|
getJellyfinConfig: () => createConfig({ enabled: false }),
|
||||||
|
getCurrentSession: () => null,
|
||||||
|
setCurrentSession: () => {},
|
||||||
|
createRemoteSessionService: () => {
|
||||||
|
created = true;
|
||||||
|
return {
|
||||||
|
start: () => {},
|
||||||
|
stop: () => {},
|
||||||
|
advertiseNow: async () => true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
defaultDeviceId: 'default-device',
|
||||||
|
defaultClientName: 'SubMiner',
|
||||||
|
defaultClientVersion: '1.0',
|
||||||
|
handlePlay: async () => {},
|
||||||
|
handlePlaystate: async () => {},
|
||||||
|
handleGeneralCommand: async () => {},
|
||||||
|
logInfo: () => {},
|
||||||
|
logWarn: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await startRemote();
|
||||||
|
assert.equal(created, false);
|
||||||
|
});
|
||||||
|
|
||||||
test('start handler no-ops when remote control is disabled', async () => {
|
test('start handler no-ops when remote control is disabled', async () => {
|
||||||
let created = false;
|
let created = false;
|
||||||
const startRemote = createStartJellyfinRemoteSessionHandler({
|
const startRemote = createStartJellyfinRemoteSessionHandler({
|
||||||
@@ -50,8 +79,11 @@ test('start handler no-ops when remote control is disabled', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('start handler creates, starts, and stores session', async () => {
|
test('start handler creates, starts, and stores session', async () => {
|
||||||
let storedSession: { start: () => void; stop: () => void; advertiseNow: () => Promise<boolean> } | null =
|
let storedSession: {
|
||||||
null;
|
start: () => void;
|
||||||
|
stop: () => void;
|
||||||
|
advertiseNow: () => Promise<boolean>;
|
||||||
|
} | null = null;
|
||||||
let started = false;
|
let started = false;
|
||||||
const infos: string[] = [];
|
const infos: string[] = [];
|
||||||
const startRemote = createStartJellyfinRemoteSessionHandler({
|
const startRemote = createStartJellyfinRemoteSessionHandler({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
type JellyfinRemoteConfig = {
|
type JellyfinRemoteConfig = {
|
||||||
|
enabled: boolean;
|
||||||
remoteControlEnabled: boolean;
|
remoteControlEnabled: boolean;
|
||||||
remoteControlAutoConnect: boolean;
|
remoteControlAutoConnect: boolean;
|
||||||
serverUrl: string;
|
serverUrl: string;
|
||||||
@@ -54,6 +55,7 @@ export function createStartJellyfinRemoteSessionHandler(deps: {
|
|||||||
}) {
|
}) {
|
||||||
return async (): Promise<void> => {
|
return async (): Promise<void> => {
|
||||||
const jellyfinConfig = deps.getJellyfinConfig();
|
const jellyfinConfig = deps.getJellyfinConfig();
|
||||||
|
if (jellyfinConfig.enabled === false) return;
|
||||||
if (jellyfinConfig.remoteControlEnabled === false) return;
|
if (jellyfinConfig.remoteControlEnabled === false) return;
|
||||||
if (jellyfinConfig.remoteControlAutoConnect === false) return;
|
if (jellyfinConfig.remoteControlAutoConnect === false) return;
|
||||||
if (!jellyfinConfig.serverUrl || !jellyfinConfig.accessToken || !jellyfinConfig.userId) return;
|
if (!jellyfinConfig.serverUrl || !jellyfinConfig.accessToken || !jellyfinConfig.userId) return;
|
||||||
@@ -87,7 +89,9 @@ export function createStartJellyfinRemoteSessionHandler(deps: {
|
|||||||
if (registered) {
|
if (registered) {
|
||||||
deps.logInfo('Jellyfin cast target is visible to server sessions.');
|
deps.logInfo('Jellyfin cast target is visible to server sessions.');
|
||||||
} else {
|
} else {
|
||||||
deps.logWarn('Jellyfin remote connected but device not visible in server sessions yet.');
|
deps.logWarn(
|
||||||
|
'Jellyfin remote connected but device not visible in server sessions yet.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ import {
|
|||||||
createStartBackgroundWarmupsHandler,
|
createStartBackgroundWarmupsHandler,
|
||||||
} from './startup-warmups';
|
} from './startup-warmups';
|
||||||
|
|
||||||
|
function shouldAutoConnectJellyfinRemote(config: {
|
||||||
|
enabled: boolean;
|
||||||
|
remoteControlEnabled: boolean;
|
||||||
|
remoteControlAutoConnect: boolean;
|
||||||
|
}): boolean {
|
||||||
|
return config.enabled && config.remoteControlEnabled && config.remoteControlAutoConnect;
|
||||||
|
}
|
||||||
|
|
||||||
test('launchBackgroundWarmupTask logs completion timing', async () => {
|
test('launchBackgroundWarmupTask logs completion timing', async () => {
|
||||||
const debugLogs: string[] = [];
|
const debugLogs: string[] = [];
|
||||||
const launchTask = createLaunchBackgroundWarmupTaskHandler({
|
const launchTask = createLaunchBackgroundWarmupTaskHandler({
|
||||||
@@ -41,7 +49,7 @@ test('startBackgroundWarmups no-ops when already started', () => {
|
|||||||
assert.equal(launches, 0);
|
assert.equal(launches, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('startBackgroundWarmups schedules base warmups and optional jellyfin warmup', () => {
|
test('startBackgroundWarmups does not schedule jellyfin warmup when jellyfin.enabled is false', () => {
|
||||||
const labels: string[] = [];
|
const labels: string[] = [];
|
||||||
let started = false;
|
let started = false;
|
||||||
const startWarmups = createStartBackgroundWarmupsHandler({
|
const startWarmups = createStartBackgroundWarmupsHandler({
|
||||||
@@ -56,7 +64,41 @@ test('startBackgroundWarmups schedules base warmups and optional jellyfin warmup
|
|||||||
createMecabTokenizerAndCheck: async () => {},
|
createMecabTokenizerAndCheck: async () => {},
|
||||||
ensureYomitanExtensionLoaded: async () => {},
|
ensureYomitanExtensionLoaded: async () => {},
|
||||||
prewarmSubtitleDictionaries: async () => {},
|
prewarmSubtitleDictionaries: async () => {},
|
||||||
shouldAutoConnectJellyfinRemote: () => true,
|
shouldAutoConnectJellyfinRemote: () =>
|
||||||
|
shouldAutoConnectJellyfinRemote({
|
||||||
|
enabled: false,
|
||||||
|
remoteControlEnabled: true,
|
||||||
|
remoteControlAutoConnect: true,
|
||||||
|
}),
|
||||||
|
startJellyfinRemoteSession: async () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
startWarmups();
|
||||||
|
assert.equal(started, true);
|
||||||
|
assert.deepEqual(labels, ['mecab', 'yomitan-extension', 'subtitle-dictionaries']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('startBackgroundWarmups schedules jellyfin warmup when all jellyfin flags are enabled', () => {
|
||||||
|
const labels: string[] = [];
|
||||||
|
let started = false;
|
||||||
|
const startWarmups = createStartBackgroundWarmupsHandler({
|
||||||
|
getStarted: () => started,
|
||||||
|
setStarted: (value) => {
|
||||||
|
started = value;
|
||||||
|
},
|
||||||
|
isTexthookerOnlyMode: () => false,
|
||||||
|
launchTask: (label) => {
|
||||||
|
labels.push(label);
|
||||||
|
},
|
||||||
|
createMecabTokenizerAndCheck: async () => {},
|
||||||
|
ensureYomitanExtensionLoaded: async () => {},
|
||||||
|
prewarmSubtitleDictionaries: async () => {},
|
||||||
|
shouldAutoConnectJellyfinRemote: () =>
|
||||||
|
shouldAutoConnectJellyfinRemote({
|
||||||
|
enabled: true,
|
||||||
|
remoteControlEnabled: true,
|
||||||
|
remoteControlAutoConnect: true,
|
||||||
|
}),
|
||||||
startJellyfinRemoteSession: async () => {},
|
startJellyfinRemoteSession: async () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user