mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-04 06:12:06 -07:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
a784091ecb
|
|||
| 61c3e1e3c6 | |||
|
ce76a75630
|
|||
|
52249db5b4
|
@@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v0.11.1 (2026-04-04)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Release: Linux packaged builds now expose the canonical `SubMiner` app identity to Electron's startup metadata so native Wayland compositors stop reporting the window class/app-id as lowercase `subminer`.
|
||||||
|
- Linux: Linux now restores the runtime options, Jimaku, and Subsync shortcuts after the Electron 39 regression by routing those actions through the overlay's mpv/IPC shortcut path.
|
||||||
|
|
||||||
## v0.11.0 (2026-04-03)
|
## v0.11.0 (2026-04-03)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Look up words with Yomitan, export to Anki in one key, track your immersion —
|
|||||||
[](https://docs.subminer.moe)
|
[](https://docs.subminer.moe)
|
||||||
[](https://aur.archlinux.org/packages/subminer-bin)
|
[](https://aur.archlinux.org/packages/subminer-bin)
|
||||||
|
|
||||||
[](./assets/minecard.mp4)
|
[](https://github.com/user-attachments/assets/89e61895-e2b7-4b47-8d50-a35afe4132b2)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
id: TASK-279
|
||||||
|
title: Prepare v0.11.2 release notes and verify release gate
|
||||||
|
status: In Progress
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-04-04 07:38'
|
||||||
|
labels: []
|
||||||
|
dependencies: []
|
||||||
|
priority: high
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Generate release metadata for the pending changelog fragments, review the resulting changelog/release notes output, and run the documented local release gate so the repo is ready for a v0.11.2 cut if checks pass.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [ ] #1 CHANGELOG.md and release/release-notes.md are generated for v0.11.2 using the pending changes fragments.
|
||||||
|
- [ ] #2 The documented local release gate for release prep is run and the pass/fail state is recorded with any blockers.
|
||||||
|
- [ ] #3 Any release-prep file updates needed for the generated notes are left in the worktree for review without tagging or pushing.
|
||||||
|
<!-- AC:END -->
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
id: TASK-276
|
||||||
|
title: Restore canonical SubMiner Linux app-id metadata
|
||||||
|
status: Done
|
||||||
|
assignee:
|
||||||
|
- codex
|
||||||
|
created_date: '2026-04-04 06:31'
|
||||||
|
updated_date: '2026-04-04 06:34'
|
||||||
|
labels:
|
||||||
|
- bug
|
||||||
|
- linux
|
||||||
|
- electron
|
||||||
|
dependencies: []
|
||||||
|
priority: medium
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Fix the Linux/Wayland packaged app metadata so the OS-facing desktop/app-id metadata stays canonical `SubMiner` instead of the lowercase npm package name `subminer`. Add regression coverage around packaged metadata so future Electron/runtime changes do not silently reintroduce the lowercase class/app-id behavior.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 Top-level package metadata provides the canonical capitalized app name used by Electron runtime bootstrap on Linux
|
||||||
|
- [x] #2 Packaged Linux app metadata resolves to `SubMiner`/`SubMiner.desktop` instead of lowercase `subminer`
|
||||||
|
- [x] #3 Regression coverage fails before the fix and passes after it
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
<!-- SECTION:PLAN:BEGIN -->
|
||||||
|
1. Add a focused packaging/runtime regression test that reads the packaged app metadata source and asserts the Linux Electron bootstrap-visible fields resolve to canonical `SubMiner` / `SubMiner.desktop`.
|
||||||
|
2. Run the targeted test first to capture the failing pre-fix state.
|
||||||
|
3. Update top-level package metadata in `package.json` with the canonical Electron runtime-facing fields needed for Linux bootstrap.
|
||||||
|
4. Re-run the targeted test and a lightweight packaging validation to confirm the packaged metadata now stays canonical.
|
||||||
|
5. Record verification notes and complete the task if all acceptance criteria pass.
|
||||||
|
<!-- SECTION:PLAN:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
Added a regression in `src/release-workflow.test.ts` asserting top-level `productName` and `desktopName` stay canonical for Linux Electron runtime bootstrap. Verified the new test failed before the fix because both fields were missing from top-level package metadata.
|
||||||
|
|
||||||
|
Updated top-level `package.json` metadata with `productName: SubMiner` and `desktopName: SubMiner.desktop` so packaged `app.asar` exposes the canonical Linux startup identity Electron reads before app code runs.
|
||||||
|
|
||||||
|
Verification passed with `bun test src/release-workflow.test.ts`, `bun run build && ./node_modules/.bin/electron-builder --linux dir --publish never`, packaged `release/linux-unpacked/resources/app.asar` inspection showing `{ name: subminer, productName: SubMiner, desktopName: SubMiner.desktop }`, and `bun run changelog:lint`.
|
||||||
|
|
||||||
|
Ran the full default handoff gate after the targeted/package verification: `bun run typecheck`, `bun run test:fast`, `bun run test:env`, and `bun run test:smoke:dist` all passed.
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Restored canonical Linux/Wayland app identity metadata by adding top-level Electron runtime fields to `package.json`: `productName: SubMiner` and `desktopName: SubMiner.desktop`. This fixes the packaged app metadata Electron reads before user code runs, so native Wayland compositors no longer need to derive the app-id/class from the lowercase npm package name alone.
|
||||||
|
|
||||||
|
Added a regression test in `src/release-workflow.test.ts` that asserts the runtime-visible top-level metadata stays canonical. The new test was run first and failed before the fix because `productName` was missing, then passed after the metadata update.
|
||||||
|
|
||||||
|
Verification: `bun test src/release-workflow.test.ts`; `bun run build && ./node_modules/.bin/electron-builder --linux dir --publish never`; inspected `release/linux-unpacked/resources/app.asar` and confirmed `productName: SubMiner` plus `desktopName: SubMiner.desktop`; `bun run changelog:lint`. Added changelog fragment `changes/2026-04-04-linux-app-id-metadata.md`.
|
||||||
|
|
||||||
|
Full default handoff gate also passed: `bun run typecheck`; `bun run test:fast`; `bun run test:env`; `bun run test:smoke:dist`.
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
id: TASK-277
|
||||||
|
title: Restore Linux shortcut-backed modal actions after Electron 39 upgrade
|
||||||
|
status: Done
|
||||||
|
assignee:
|
||||||
|
- codex
|
||||||
|
created_date: '2026-04-04 06:49'
|
||||||
|
updated_date: '2026-04-04 07:08'
|
||||||
|
labels:
|
||||||
|
- bug
|
||||||
|
- linux
|
||||||
|
- electron
|
||||||
|
- shortcuts
|
||||||
|
dependencies: []
|
||||||
|
priority: high
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Linux builds on Electron 39 no longer open the runtime options, Jimaku, or Subsync flows from their configured shortcuts (`Ctrl/Cmd+Shift+O`, `Ctrl+Shift+J`, `Ctrl+Alt+S`). Other renderer-driven modals like session help, subtitle sidebar, and playlist browser still work, which points to the Linux `globalShortcut` / overlay shortcut path rather than modal rendering in general. Investigate the Electron 39 regression and restore these Linux shortcut-triggered actions without regressing macOS or Windows behavior.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 On Linux, the configured runtime options, Jimaku, and Subsync shortcuts trigger their existing actions again under Electron 39.
|
||||||
|
- [x] #2 The fix is covered by automated tests around the Linux shortcut/backend behavior that regressed.
|
||||||
|
- [x] #3 A changelog fragment is added for the Linux shortcut regression fix.
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
<!-- SECTION:PLAN:BEGIN -->
|
||||||
|
1. Extend the special-command / mpv IPC command path to cover Jimaku alongside the existing runtime-options and subsync commands.
|
||||||
|
2. Add tests for IPC command dispatch and any keybinding/session-help metadata affected by the new special command.
|
||||||
|
3. Route Linux modal-opening shortcuts through the working mpv/IPC command path instead of depending on Electron globalShortcut delivery for those actions.
|
||||||
|
4. Re-run targeted tests, then the handoff verification gate, and update the changelog/task summary to reflect the final root cause and fix.
|
||||||
|
<!-- SECTION:PLAN:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
User approved plan; proceeding with test-first implementation.
|
||||||
|
|
||||||
|
Root cause: Linux startup unconditionally enabled Electron's GlobalShortcutsPortal path, so Electron 39 X11 sessions were routed through the portal-backed globalShortcut path even though that path should only be used for Wayland-style launches. The affected runtime-options, Jimaku, and Subsync actions all depend on that shortcut path, while renderer-driven modals did not regress.
|
||||||
|
|
||||||
|
User retested after the portal gating fix; issue persists. Updating investigation scope from portal selection alone to Linux overlay shortcut delivery/registration under Electron 39.
|
||||||
|
|
||||||
|
Revised fix path: Linux renderer now matches configured overlay shortcuts for runtime options, subsync, and Jimaku locally, then dispatches the existing mpv/IPC special-command route instead of depending on Electron globalShortcut delivery.
|
||||||
|
|
||||||
|
Added Jimaku mpv special command plumbing through IPC/runtime deps and exposed an overlay-runtime helper for opening the Jimaku modal from that IPC path.
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Restored Linux shortcut-backed modal actions by routing runtime options, Subsync, and Jimaku through the working mpv/IPC special-command path. Added renderer-side Linux shortcut matching for configured overlay shortcuts, threaded a new Jimaku special command through IPC/runtime deps, refreshed configured shortcuts on hot reload, and covered the regression with IPC/runtime keyboard tests. Verification: bun test src/core/services/ipc-command.test.ts src/renderer/handlers/keyboard.test.ts src/main/runtime/ipc-mpv-command-main-deps.test.ts; bun run typecheck; bun run test:fast; bun run test:env; bun run build; bun run test:smoke:dist.
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
67
backlog/tasks/task-278 - Prepare-patch-release-0.11.1.md
Normal file
67
backlog/tasks/task-278 - Prepare-patch-release-0.11.1.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
id: TASK-278
|
||||||
|
title: Prepare patch release 0.11.1
|
||||||
|
status: Done
|
||||||
|
assignee:
|
||||||
|
- '@codex'
|
||||||
|
created_date: '2026-04-04 07:16'
|
||||||
|
updated_date: '2026-04-04 07:41'
|
||||||
|
labels:
|
||||||
|
- release
|
||||||
|
dependencies: []
|
||||||
|
priority: medium
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Bump SubMiner from 0.11.0 to 0.11.1, run the local release checklist, and confirm whether the branch is ready for tagging/publishing.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 package.json is bumped to 0.11.1 without overwriting unrelated local metadata edits.
|
||||||
|
- [x] #2 Release prep checks are run and summarized, including changelog/build verification and local build/test gates.
|
||||||
|
- [x] #3 Any remaining release blockers are called out explicitly.
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
<!-- SECTION:PLAN:BEGIN -->
|
||||||
|
1. Generate release metadata with `bun run changelog:build --version 0.11.1 --date 2026-04-04`.
|
||||||
|
2. Review the resulting `CHANGELOG.md` and `release/release-notes.md` content for the 0.11.1 section.
|
||||||
|
3. Rerun the documented local release gate: `bun run changelog:check --version 0.11.1`, `bun run verify:config-example`, `bun run typecheck`, `bun run test:fast`, `bun run test:env`, and `bun run build`.
|
||||||
|
4. Record results, remaining blockers, and ready-for-tagging status in the task notes/final summary.
|
||||||
|
<!-- SECTION:PLAN:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
Bumped package.json from 0.11.0 to 0.11.1 while preserving the existing local productName/desktopName edits.
|
||||||
|
|
||||||
|
Fixed the Linux shortcut changelog fragment metadata so changelog lint now passes and the previously failing CI cause is addressed locally.
|
||||||
|
|
||||||
|
Release checks run: bun run changelog:lint, bun run changelog:check --version 0.11.1, bun run verify:config-example, bun run typecheck, bun run test:fast, bun run test:env, bun run build. All passed except changelog:check, which is expected until pending fragments are built into CHANGELOG.md/release/release-notes.md.
|
||||||
|
|
||||||
|
Current release blocker: pending changelog fragments /changes/2026-04-04-linux-app-id-metadata.md and /changes/2026-04-04-linux-shortcut-portal-regression.md still need bun run changelog:build --version 0.11.1 --date 2026-04-04 before tagging.
|
||||||
|
|
||||||
|
Reopened to complete the remaining release-prep work: generate changelog/release notes artifacts, rerun the documented release gate, and confirm ready-for-tagging status.
|
||||||
|
|
||||||
|
Completed the remaining release-prep step with `bun run changelog:build --version 0.11.1 --date 2026-04-04`, which prepended `CHANGELOG.md`, generated `release/release-notes.md`, and removed the two pending `changes/*.md` fragments.
|
||||||
|
|
||||||
|
Reran the release gate after changelog generation: `bun run changelog:check --version 0.11.1`, `bun run verify:config-example`, `bun run typecheck`, `bun run test:fast`, `bun run test:env`, and `bun run build`; all passed.
|
||||||
|
|
||||||
|
Extra confidence checks also passed: `bun run changelog:lint`, `bun run test:smoke:dist`, and `gh run list --workflow CI --limit 5` shows the two latest `main` CI runs succeeded on 2026-04-04 after the earlier pre-fix failure.
|
||||||
|
|
||||||
|
`release/release-notes.md` is intentionally generated under the gitignored `release/` directory, so readiness evidence in git status is `CHANGELOG.md` plus deletion of the released change fragments.
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Finished the 0.11.1 release prep by generating the changelog artifacts from the pending Linux fix fragments and re-running the full documented local release gate. `CHANGELOG.md` now contains the `v0.11.1 (2026-04-04)` section, `release/release-notes.md` was regenerated in the ignored `release/` directory, and the released `changes/2026-04-04-linux-app-id-metadata.md` plus `changes/2026-04-04-linux-shortcut-portal-regression.md` fragments were removed.
|
||||||
|
|
||||||
|
Verification results: `bun run changelog:check --version 0.11.1`, `bun run verify:config-example`, `bun run typecheck`, `bun run test:fast`, `bun run test:env`, `bun run build`, `bun run changelog:lint`, and `bun run test:smoke:dist` all passed locally. Remote CI also looks green for the latest release-prep head: `gh run list --workflow CI --limit 5` showed successful `main` runs for `chore: prep 0.11.1 release` and `Change demo image link to GitHub asset` on 2026-04-04, with the earlier `fix: restore linux modal shortcuts` failure already superseded by later green runs.
|
||||||
|
|
||||||
|
Remaining manual release step: commit the generated release-prep changes if desired, tag `v0.11.1`, and push the commit plus tag when ready.
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "subminer",
|
"name": "subminer",
|
||||||
"version": "0.11.0",
|
"productName": "SubMiner",
|
||||||
|
"desktopName": "SubMiner.desktop",
|
||||||
|
"version": "0.11.1",
|
||||||
"description": "All-in-one sentence mining overlay with AnkiConnect and dictionary integration",
|
"description": "All-in-one sentence mining overlay with AnkiConnect and dictionary integration",
|
||||||
"packageManager": "bun@1.3.5",
|
"packageManager": "bun@1.3.5",
|
||||||
"main": "dist/main-entry.js",
|
"main": "dist/main-entry.js",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export interface ConfigTemplateSection {
|
|||||||
export const SPECIAL_COMMANDS = {
|
export const SPECIAL_COMMANDS = {
|
||||||
SUBSYNC_TRIGGER: '__subsync-trigger',
|
SUBSYNC_TRIGGER: '__subsync-trigger',
|
||||||
RUNTIME_OPTIONS_OPEN: '__runtime-options-open',
|
RUNTIME_OPTIONS_OPEN: '__runtime-options-open',
|
||||||
|
JIMAKU_OPEN: '__jimaku-open',
|
||||||
RUNTIME_OPTION_CYCLE_PREFIX: '__runtime-option-cycle:',
|
RUNTIME_OPTION_CYCLE_PREFIX: '__runtime-option-cycle:',
|
||||||
REPLAY_SUBTITLE: '__replay-subtitle',
|
REPLAY_SUBTITLE: '__replay-subtitle',
|
||||||
PLAY_NEXT_SUBTITLE: '__play-next-subtitle',
|
PLAY_NEXT_SUBTITLE: '__play-next-subtitle',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ function createOptions(overrides: Partial<Parameters<typeof handleMpvCommandFrom
|
|||||||
specialCommands: {
|
specialCommands: {
|
||||||
SUBSYNC_TRIGGER: '__subsync-trigger',
|
SUBSYNC_TRIGGER: '__subsync-trigger',
|
||||||
RUNTIME_OPTIONS_OPEN: '__runtime-options-open',
|
RUNTIME_OPTIONS_OPEN: '__runtime-options-open',
|
||||||
|
JIMAKU_OPEN: '__jimaku-open',
|
||||||
RUNTIME_OPTION_CYCLE_PREFIX: '__runtime-option-cycle:',
|
RUNTIME_OPTION_CYCLE_PREFIX: '__runtime-option-cycle:',
|
||||||
REPLAY_SUBTITLE: '__replay-subtitle',
|
REPLAY_SUBTITLE: '__replay-subtitle',
|
||||||
PLAY_NEXT_SUBTITLE: '__play-next-subtitle',
|
PLAY_NEXT_SUBTITLE: '__play-next-subtitle',
|
||||||
@@ -24,6 +25,9 @@ function createOptions(overrides: Partial<Parameters<typeof handleMpvCommandFrom
|
|||||||
openRuntimeOptionsPalette: () => {
|
openRuntimeOptionsPalette: () => {
|
||||||
calls.push('runtime-options');
|
calls.push('runtime-options');
|
||||||
},
|
},
|
||||||
|
openJimaku: () => {
|
||||||
|
calls.push('jimaku');
|
||||||
|
},
|
||||||
openYoutubeTrackPicker: () => {
|
openYoutubeTrackPicker: () => {
|
||||||
calls.push('youtube-picker');
|
calls.push('youtube-picker');
|
||||||
},
|
},
|
||||||
@@ -114,6 +118,14 @@ test('handleMpvCommandFromIpc dispatches special youtube picker open command', (
|
|||||||
assert.deepEqual(osd, []);
|
assert.deepEqual(osd, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('handleMpvCommandFromIpc dispatches special jimaku open command', () => {
|
||||||
|
const { options, calls, sentCommands, osd } = createOptions();
|
||||||
|
handleMpvCommandFromIpc(['__jimaku-open'], options);
|
||||||
|
assert.deepEqual(calls, ['jimaku']);
|
||||||
|
assert.deepEqual(sentCommands, []);
|
||||||
|
assert.deepEqual(osd, []);
|
||||||
|
});
|
||||||
|
|
||||||
test('handleMpvCommandFromIpc dispatches special playlist browser open command', async () => {
|
test('handleMpvCommandFromIpc dispatches special playlist browser open command', async () => {
|
||||||
const { options, calls, sentCommands, osd } = createOptions();
|
const { options, calls, sentCommands, osd } = createOptions();
|
||||||
handleMpvCommandFromIpc(['__playlist-browser-open'], options);
|
handleMpvCommandFromIpc(['__playlist-browser-open'], options);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface HandleMpvCommandFromIpcOptions {
|
|||||||
specialCommands: {
|
specialCommands: {
|
||||||
SUBSYNC_TRIGGER: string;
|
SUBSYNC_TRIGGER: string;
|
||||||
RUNTIME_OPTIONS_OPEN: string;
|
RUNTIME_OPTIONS_OPEN: string;
|
||||||
|
JIMAKU_OPEN: string;
|
||||||
RUNTIME_OPTION_CYCLE_PREFIX: string;
|
RUNTIME_OPTION_CYCLE_PREFIX: string;
|
||||||
REPLAY_SUBTITLE: string;
|
REPLAY_SUBTITLE: string;
|
||||||
PLAY_NEXT_SUBTITLE: string;
|
PLAY_NEXT_SUBTITLE: string;
|
||||||
@@ -19,6 +20,7 @@ export interface HandleMpvCommandFromIpcOptions {
|
|||||||
};
|
};
|
||||||
triggerSubsyncFromConfig: () => void;
|
triggerSubsyncFromConfig: () => void;
|
||||||
openRuntimeOptionsPalette: () => void;
|
openRuntimeOptionsPalette: () => void;
|
||||||
|
openJimaku: () => void;
|
||||||
openYoutubeTrackPicker: () => void | Promise<void>;
|
openYoutubeTrackPicker: () => void | Promise<void>;
|
||||||
openPlaylistBrowser: () => void | Promise<void>;
|
openPlaylistBrowser: () => void | Promise<void>;
|
||||||
runtimeOptionsCycle: (id: RuntimeOptionId, direction: 1 | -1) => RuntimeOptionApplyResult;
|
runtimeOptionsCycle: (id: RuntimeOptionId, direction: 1 | -1) => RuntimeOptionApplyResult;
|
||||||
@@ -94,6 +96,11 @@ export function handleMpvCommandFromIpc(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (first === options.specialCommands.JIMAKU_OPEN) {
|
||||||
|
options.openJimaku();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (first === options.specialCommands.YOUTUBE_PICKER_OPEN) {
|
if (first === options.specialCommands.YOUTUBE_PICKER_OPEN) {
|
||||||
void options.openYoutubeTrackPicker();
|
void options.openYoutubeTrackPicker();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -4191,6 +4191,7 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
|
|||||||
mpvCommandMainDeps: {
|
mpvCommandMainDeps: {
|
||||||
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||||
|
openJimaku: () => overlayModalRuntime.openJimaku(),
|
||||||
openYoutubeTrackPicker: () => openYoutubeTrackPickerFromPlayback(),
|
openYoutubeTrackPicker: () => openYoutubeTrackPickerFromPlayback(),
|
||||||
openPlaylistBrowser: () => openPlaylistBrowser(),
|
openPlaylistBrowser: () => openPlaylistBrowser(),
|
||||||
cycleRuntimeOption: (id, direction) => {
|
cycleRuntimeOption: (id, direction) => {
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ export interface MpvCommandRuntimeServiceDepsParams {
|
|||||||
runtimeOptionsCycle: HandleMpvCommandFromIpcOptions['runtimeOptionsCycle'];
|
runtimeOptionsCycle: HandleMpvCommandFromIpcOptions['runtimeOptionsCycle'];
|
||||||
triggerSubsyncFromConfig: HandleMpvCommandFromIpcOptions['triggerSubsyncFromConfig'];
|
triggerSubsyncFromConfig: HandleMpvCommandFromIpcOptions['triggerSubsyncFromConfig'];
|
||||||
openRuntimeOptionsPalette: HandleMpvCommandFromIpcOptions['openRuntimeOptionsPalette'];
|
openRuntimeOptionsPalette: HandleMpvCommandFromIpcOptions['openRuntimeOptionsPalette'];
|
||||||
|
openJimaku: HandleMpvCommandFromIpcOptions['openJimaku'];
|
||||||
openYoutubeTrackPicker: HandleMpvCommandFromIpcOptions['openYoutubeTrackPicker'];
|
openYoutubeTrackPicker: HandleMpvCommandFromIpcOptions['openYoutubeTrackPicker'];
|
||||||
openPlaylistBrowser: HandleMpvCommandFromIpcOptions['openPlaylistBrowser'];
|
openPlaylistBrowser: HandleMpvCommandFromIpcOptions['openPlaylistBrowser'];
|
||||||
showMpvOsd: HandleMpvCommandFromIpcOptions['showMpvOsd'];
|
showMpvOsd: HandleMpvCommandFromIpcOptions['showMpvOsd'];
|
||||||
@@ -368,6 +369,7 @@ export function createMpvCommandRuntimeServiceDeps(
|
|||||||
specialCommands: params.specialCommands,
|
specialCommands: params.specialCommands,
|
||||||
triggerSubsyncFromConfig: params.triggerSubsyncFromConfig,
|
triggerSubsyncFromConfig: params.triggerSubsyncFromConfig,
|
||||||
openRuntimeOptionsPalette: params.openRuntimeOptionsPalette,
|
openRuntimeOptionsPalette: params.openRuntimeOptionsPalette,
|
||||||
|
openJimaku: params.openJimaku,
|
||||||
openYoutubeTrackPicker: params.openYoutubeTrackPicker,
|
openYoutubeTrackPicker: params.openYoutubeTrackPicker,
|
||||||
openPlaylistBrowser: params.openPlaylistBrowser,
|
openPlaylistBrowser: params.openPlaylistBrowser,
|
||||||
runtimeOptionsCycle: params.runtimeOptionsCycle,
|
runtimeOptionsCycle: params.runtimeOptionsCycle,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type MpvPropertyClientLike = {
|
|||||||
export interface MpvCommandFromIpcRuntimeDeps {
|
export interface MpvCommandFromIpcRuntimeDeps {
|
||||||
triggerSubsyncFromConfig: () => void;
|
triggerSubsyncFromConfig: () => void;
|
||||||
openRuntimeOptionsPalette: () => void;
|
openRuntimeOptionsPalette: () => void;
|
||||||
|
openJimaku: () => void;
|
||||||
openYoutubeTrackPicker: () => void | Promise<void>;
|
openYoutubeTrackPicker: () => void | Promise<void>;
|
||||||
openPlaylistBrowser: () => void | Promise<void>;
|
openPlaylistBrowser: () => void | Promise<void>;
|
||||||
cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1) => RuntimeOptionApplyResult;
|
cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1) => RuntimeOptionApplyResult;
|
||||||
@@ -35,6 +36,7 @@ export function handleMpvCommandFromIpcRuntime(
|
|||||||
specialCommands: SPECIAL_COMMANDS,
|
specialCommands: SPECIAL_COMMANDS,
|
||||||
triggerSubsyncFromConfig: deps.triggerSubsyncFromConfig,
|
triggerSubsyncFromConfig: deps.triggerSubsyncFromConfig,
|
||||||
openRuntimeOptionsPalette: deps.openRuntimeOptionsPalette,
|
openRuntimeOptionsPalette: deps.openRuntimeOptionsPalette,
|
||||||
|
openJimaku: deps.openJimaku,
|
||||||
openYoutubeTrackPicker: deps.openYoutubeTrackPicker,
|
openYoutubeTrackPicker: deps.openYoutubeTrackPicker,
|
||||||
openPlaylistBrowser: deps.openPlaylistBrowser,
|
openPlaylistBrowser: deps.openPlaylistBrowser,
|
||||||
runtimeOptionsCycle: deps.cycleRuntimeOption,
|
runtimeOptionsCycle: deps.cycleRuntimeOption,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface OverlayModalRuntime {
|
|||||||
},
|
},
|
||||||
) => boolean;
|
) => boolean;
|
||||||
openRuntimeOptionsPalette: () => void;
|
openRuntimeOptionsPalette: () => void;
|
||||||
|
openJimaku: () => void;
|
||||||
handleOverlayModalClosed: (modal: OverlayHostedModal) => void;
|
handleOverlayModalClosed: (modal: OverlayHostedModal) => void;
|
||||||
notifyOverlayModalOpened: (modal: OverlayHostedModal) => void;
|
notifyOverlayModalOpened: (modal: OverlayHostedModal) => void;
|
||||||
waitForModalOpen: (modal: OverlayHostedModal, timeoutMs: number) => Promise<boolean>;
|
waitForModalOpen: (modal: OverlayHostedModal, timeoutMs: number) => Promise<boolean>;
|
||||||
@@ -307,6 +308,12 @@ export function createOverlayModalRuntimeService(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openJimaku = (): void => {
|
||||||
|
sendToActiveOverlayWindow('jimaku:open', undefined, {
|
||||||
|
restoreOnModalClose: 'jimaku',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleOverlayModalClosed = (modal: OverlayHostedModal): void => {
|
const handleOverlayModalClosed = (modal: OverlayHostedModal): void => {
|
||||||
if (!restoreVisibleOverlayOnModalClose.has(modal)) return;
|
if (!restoreVisibleOverlayOnModalClose.has(modal)) return;
|
||||||
restoreVisibleOverlayOnModalClose.delete(modal);
|
restoreVisibleOverlayOnModalClose.delete(modal);
|
||||||
@@ -379,6 +386,7 @@ export function createOverlayModalRuntimeService(
|
|||||||
return {
|
return {
|
||||||
sendToActiveOverlayWindow,
|
sendToActiveOverlayWindow,
|
||||||
openRuntimeOptionsPalette,
|
openRuntimeOptionsPalette,
|
||||||
|
openJimaku,
|
||||||
handleOverlayModalClosed,
|
handleOverlayModalClosed,
|
||||||
notifyOverlayModalOpened,
|
notifyOverlayModalOpened,
|
||||||
waitForModalOpen,
|
waitForModalOpen,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ test('composeIpcRuntimeHandlers returns callable IPC handlers and registration b
|
|||||||
mpvCommandMainDeps: {
|
mpvCommandMainDeps: {
|
||||||
triggerSubsyncFromConfig: async () => {},
|
triggerSubsyncFromConfig: async () => {},
|
||||||
openRuntimeOptionsPalette: () => {},
|
openRuntimeOptionsPalette: () => {},
|
||||||
|
openJimaku: () => {},
|
||||||
openYoutubeTrackPicker: () => {},
|
openYoutubeTrackPicker: () => {},
|
||||||
openPlaylistBrowser: () => {},
|
openPlaylistBrowser: () => {},
|
||||||
cycleRuntimeOption: () => ({ ok: true }),
|
cycleRuntimeOption: () => ({ ok: true }),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ test('ipc bridge action main deps builders map callbacks', async () => {
|
|||||||
buildMpvCommandDeps: () => ({
|
buildMpvCommandDeps: () => ({
|
||||||
triggerSubsyncFromConfig: async () => {},
|
triggerSubsyncFromConfig: async () => {},
|
||||||
openRuntimeOptionsPalette: () => {},
|
openRuntimeOptionsPalette: () => {},
|
||||||
|
openJimaku: () => {},
|
||||||
openYoutubeTrackPicker: () => {},
|
openYoutubeTrackPicker: () => {},
|
||||||
openPlaylistBrowser: () => {},
|
openPlaylistBrowser: () => {},
|
||||||
cycleRuntimeOption: () => ({ ok: false as const, error: 'x' }),
|
cycleRuntimeOption: () => ({ ok: false as const, error: 'x' }),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ test('handle mpv command handler forwards command and built deps', () => {
|
|||||||
const deps = {
|
const deps = {
|
||||||
triggerSubsyncFromConfig: () => {},
|
triggerSubsyncFromConfig: () => {},
|
||||||
openRuntimeOptionsPalette: () => {},
|
openRuntimeOptionsPalette: () => {},
|
||||||
|
openJimaku: () => {},
|
||||||
openYoutubeTrackPicker: () => {},
|
openYoutubeTrackPicker: () => {},
|
||||||
openPlaylistBrowser: () => {},
|
openPlaylistBrowser: () => {},
|
||||||
cycleRuntimeOption: () => ({ ok: false as const, error: 'x' }),
|
cycleRuntimeOption: () => ({ ok: false as const, error: 'x' }),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ test('ipc mpv command main deps builder maps callbacks', () => {
|
|||||||
const deps = createBuildMpvCommandFromIpcRuntimeMainDepsHandler({
|
const deps = createBuildMpvCommandFromIpcRuntimeMainDepsHandler({
|
||||||
triggerSubsyncFromConfig: () => calls.push('subsync'),
|
triggerSubsyncFromConfig: () => calls.push('subsync'),
|
||||||
openRuntimeOptionsPalette: () => calls.push('palette'),
|
openRuntimeOptionsPalette: () => calls.push('palette'),
|
||||||
|
openJimaku: () => calls.push('jimaku'),
|
||||||
openYoutubeTrackPicker: () => {
|
openYoutubeTrackPicker: () => {
|
||||||
calls.push('youtube-picker');
|
calls.push('youtube-picker');
|
||||||
},
|
},
|
||||||
@@ -28,6 +29,7 @@ test('ipc mpv command main deps builder maps callbacks', () => {
|
|||||||
|
|
||||||
deps.triggerSubsyncFromConfig();
|
deps.triggerSubsyncFromConfig();
|
||||||
deps.openRuntimeOptionsPalette();
|
deps.openRuntimeOptionsPalette();
|
||||||
|
deps.openJimaku();
|
||||||
void deps.openYoutubeTrackPicker();
|
void deps.openYoutubeTrackPicker();
|
||||||
void deps.openPlaylistBrowser();
|
void deps.openPlaylistBrowser();
|
||||||
assert.deepEqual(deps.cycleRuntimeOption('anki.nPlusOneMatchMode', 1), { ok: false, error: 'x' });
|
assert.deepEqual(deps.cycleRuntimeOption('anki.nPlusOneMatchMode', 1), { ok: false, error: 'x' });
|
||||||
@@ -42,6 +44,7 @@ test('ipc mpv command main deps builder maps callbacks', () => {
|
|||||||
assert.deepEqual(calls, [
|
assert.deepEqual(calls, [
|
||||||
'subsync',
|
'subsync',
|
||||||
'palette',
|
'palette',
|
||||||
|
'jimaku',
|
||||||
'youtube-picker',
|
'youtube-picker',
|
||||||
'playlist-browser',
|
'playlist-browser',
|
||||||
'osd:hello',
|
'osd:hello',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export function createBuildMpvCommandFromIpcRuntimeMainDepsHandler(
|
|||||||
return (): MpvCommandFromIpcRuntimeDeps => ({
|
return (): MpvCommandFromIpcRuntimeDeps => ({
|
||||||
triggerSubsyncFromConfig: () => deps.triggerSubsyncFromConfig(),
|
triggerSubsyncFromConfig: () => deps.triggerSubsyncFromConfig(),
|
||||||
openRuntimeOptionsPalette: () => deps.openRuntimeOptionsPalette(),
|
openRuntimeOptionsPalette: () => deps.openRuntimeOptionsPalette(),
|
||||||
|
openJimaku: () => deps.openJimaku(),
|
||||||
openYoutubeTrackPicker: () => deps.openYoutubeTrackPicker(),
|
openYoutubeTrackPicker: () => deps.openYoutubeTrackPicker(),
|
||||||
openPlaylistBrowser: () => deps.openPlaylistBrowser(),
|
openPlaylistBrowser: () => deps.openPlaylistBrowser(),
|
||||||
cycleRuntimeOption: (id, direction) => deps.cycleRuntimeOption(id, direction),
|
cycleRuntimeOption: (id, direction) => deps.cycleRuntimeOption(id, direction),
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ const makefilePath = resolve(__dirname, '../Makefile');
|
|||||||
const makefile = readFileSync(makefilePath, 'utf8');
|
const makefile = readFileSync(makefilePath, 'utf8');
|
||||||
const packageJsonPath = resolve(__dirname, '../package.json');
|
const packageJsonPath = resolve(__dirname, '../package.json');
|
||||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as {
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as {
|
||||||
|
desktopName?: string;
|
||||||
|
productName?: string;
|
||||||
scripts: Record<string, string>;
|
scripts: Record<string, string>;
|
||||||
build?: {
|
build?: {
|
||||||
files?: string[];
|
files?: string[];
|
||||||
@@ -75,6 +77,11 @@ test('release package scripts disable implicit electron-builder publishing', ()
|
|||||||
assert.match(packageJson.scripts['build:win:unsigned'] ?? '', /build-win-unsigned\.mjs/);
|
assert.match(packageJson.scripts['build:win:unsigned'] ?? '', /build-win-unsigned\.mjs/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('top-level package metadata keeps Linux Electron runtime app identity canonical', () => {
|
||||||
|
assert.equal(packageJson.productName, 'SubMiner');
|
||||||
|
assert.equal(packageJson.desktopName, 'SubMiner.desktop');
|
||||||
|
});
|
||||||
|
|
||||||
test('release packaging keeps default file inclusion and excludes large source-only trees explicitly', () => {
|
test('release packaging keeps default file inclusion and excludes large source-only trees explicitly', () => {
|
||||||
const files = packageJson.build?.files ?? [];
|
const files = packageJson.build?.files ?? [];
|
||||||
assert.ok(files.includes('**/*'));
|
assert.ok(files.includes('**/*'));
|
||||||
|
|||||||
@@ -53,6 +53,21 @@ function installKeyboardTestGlobals() {
|
|||||||
let playbackPausedResponse: boolean | null = false;
|
let playbackPausedResponse: boolean | null = false;
|
||||||
let statsToggleKey = 'Backquote';
|
let statsToggleKey = 'Backquote';
|
||||||
let markWatchedKey = 'KeyW';
|
let markWatchedKey = 'KeyW';
|
||||||
|
let configuredShortcuts = {
|
||||||
|
copySubtitle: '',
|
||||||
|
copySubtitleMultiple: '',
|
||||||
|
updateLastCardFromClipboard: '',
|
||||||
|
triggerFieldGrouping: '',
|
||||||
|
triggerSubsync: 'Ctrl+Alt+S',
|
||||||
|
mineSentence: '',
|
||||||
|
mineSentenceMultiple: '',
|
||||||
|
multiCopyTimeoutMs: 1000,
|
||||||
|
toggleSecondarySub: '',
|
||||||
|
markAudioCard: '',
|
||||||
|
openRuntimeOptions: 'CommandOrControl+Shift+O',
|
||||||
|
openJimaku: 'Ctrl+Shift+J',
|
||||||
|
toggleVisibleOverlayGlobal: '',
|
||||||
|
};
|
||||||
let markActiveVideoWatchedResult = true;
|
let markActiveVideoWatchedResult = true;
|
||||||
let markActiveVideoWatchedCalls = 0;
|
let markActiveVideoWatchedCalls = 0;
|
||||||
let statsToggleOverlayCalls = 0;
|
let statsToggleOverlayCalls = 0;
|
||||||
@@ -138,6 +153,7 @@ function installKeyboardTestGlobals() {
|
|||||||
},
|
},
|
||||||
electronAPI: {
|
electronAPI: {
|
||||||
getKeybindings: async () => [],
|
getKeybindings: async () => [],
|
||||||
|
getConfiguredShortcuts: async () => configuredShortcuts,
|
||||||
sendMpvCommand: (command: Array<string | number>) => {
|
sendMpvCommand: (command: Array<string | number>) => {
|
||||||
mpvCommands.push(command);
|
mpvCommands.push(command);
|
||||||
},
|
},
|
||||||
@@ -273,6 +289,9 @@ function installKeyboardTestGlobals() {
|
|||||||
setMarkWatchedKey: (value: string) => {
|
setMarkWatchedKey: (value: string) => {
|
||||||
markWatchedKey = value;
|
markWatchedKey = value;
|
||||||
},
|
},
|
||||||
|
setConfiguredShortcuts: (value: typeof configuredShortcuts) => {
|
||||||
|
configuredShortcuts = value;
|
||||||
|
},
|
||||||
setMarkActiveVideoWatchedResult: (value: boolean) => {
|
setMarkActiveVideoWatchedResult: (value: boolean) => {
|
||||||
markActiveVideoWatchedResult = value;
|
markActiveVideoWatchedResult = value;
|
||||||
},
|
},
|
||||||
@@ -315,6 +334,7 @@ function createKeyboardHandlerHarness() {
|
|||||||
overlay: testGlobals.overlay,
|
overlay: testGlobals.overlay,
|
||||||
},
|
},
|
||||||
platform: {
|
platform: {
|
||||||
|
isLinuxPlatform: false,
|
||||||
shouldToggleMouseIgnore: false,
|
shouldToggleMouseIgnore: false,
|
||||||
isMacOSPlatform: false,
|
isMacOSPlatform: false,
|
||||||
isModalLayer: false,
|
isModalLayer: false,
|
||||||
@@ -765,6 +785,51 @@ test('youtube picker: unhandled keys still dispatch mpv keybindings', async () =
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('linux overlay shortcut: Ctrl+Alt+S dispatches subsync special command locally', async () => {
|
||||||
|
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||||
|
|
||||||
|
try {
|
||||||
|
ctx.platform.isLinuxPlatform = true;
|
||||||
|
await handlers.setupMpvInputForwarding();
|
||||||
|
|
||||||
|
testGlobals.dispatchKeydown({ key: 's', code: 'KeyS', ctrlKey: true, altKey: true });
|
||||||
|
|
||||||
|
assert.deepEqual(testGlobals.mpvCommands, [['__subsync-trigger']]);
|
||||||
|
} finally {
|
||||||
|
testGlobals.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('linux overlay shortcut: Ctrl+Shift+J dispatches jimaku special command locally', async () => {
|
||||||
|
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||||
|
|
||||||
|
try {
|
||||||
|
ctx.platform.isLinuxPlatform = true;
|
||||||
|
await handlers.setupMpvInputForwarding();
|
||||||
|
|
||||||
|
testGlobals.dispatchKeydown({ key: 'J', code: 'KeyJ', ctrlKey: true, shiftKey: true });
|
||||||
|
|
||||||
|
assert.deepEqual(testGlobals.mpvCommands, [['__jimaku-open']]);
|
||||||
|
} finally {
|
||||||
|
testGlobals.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('linux overlay shortcut: CommandOrControl+Shift+O dispatches runtime options locally', async () => {
|
||||||
|
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||||
|
|
||||||
|
try {
|
||||||
|
ctx.platform.isLinuxPlatform = true;
|
||||||
|
await handlers.setupMpvInputForwarding();
|
||||||
|
|
||||||
|
testGlobals.dispatchKeydown({ key: 'O', code: 'KeyO', ctrlKey: true, shiftKey: true });
|
||||||
|
|
||||||
|
assert.deepEqual(testGlobals.mpvCommands, [['__runtime-options-open']]);
|
||||||
|
} finally {
|
||||||
|
testGlobals.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('keyboard mode: h moves left when popup is closed', async () => {
|
test('keyboard mode: h moves left when popup is closed', async () => {
|
||||||
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
|
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Keybinding } from '../../types';
|
import { SPECIAL_COMMANDS } from '../../config/definitions';
|
||||||
|
import type { Keybinding, ShortcutsConfig } from '../../types';
|
||||||
import type { RendererContext } from '../context';
|
import type { RendererContext } from '../context';
|
||||||
import {
|
import {
|
||||||
YOMITAN_POPUP_HIDDEN_EVENT,
|
YOMITAN_POPUP_HIDDEN_EVENT,
|
||||||
@@ -35,6 +36,7 @@ export function createKeyboardHandlers(
|
|||||||
// Timeout for the modal chord capture window (e.g. Y followed by H/K).
|
// Timeout for the modal chord capture window (e.g. Y followed by H/K).
|
||||||
const CHORD_TIMEOUT_MS = 1000;
|
const CHORD_TIMEOUT_MS = 1000;
|
||||||
const KEYBOARD_SELECTED_WORD_CLASS = 'keyboard-selected';
|
const KEYBOARD_SELECTED_WORD_CLASS = 'keyboard-selected';
|
||||||
|
const linuxOverlayShortcutCommands = new Map<string, (string | number)[]>();
|
||||||
let pendingSelectionAnchorAfterSubtitleSeek: 'start' | 'end' | null = null;
|
let pendingSelectionAnchorAfterSubtitleSeek: 'start' | 'end' | null = null;
|
||||||
let pendingLookupRefreshAfterSubtitleSeek = false;
|
let pendingLookupRefreshAfterSubtitleSeek = false;
|
||||||
let resetSelectionToStartOnNextSubtitleSync = false;
|
let resetSelectionToStartOnNextSubtitleSync = false;
|
||||||
@@ -74,6 +76,117 @@ export function createKeyboardHandlers(
|
|||||||
return parts.join('+');
|
return parts.join('+');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function acceleratorToKeyToken(token: string): string | null {
|
||||||
|
const normalized = token.trim();
|
||||||
|
if (!normalized) return null;
|
||||||
|
if (/^[a-z]$/i.test(normalized)) {
|
||||||
|
return `Key${normalized.toUpperCase()}`;
|
||||||
|
}
|
||||||
|
if (/^[0-9]$/.test(normalized)) {
|
||||||
|
return `Digit${normalized}`;
|
||||||
|
}
|
||||||
|
const exactMap: Record<string, string> = {
|
||||||
|
space: 'Space',
|
||||||
|
tab: 'Tab',
|
||||||
|
enter: 'Enter',
|
||||||
|
return: 'Enter',
|
||||||
|
esc: 'Escape',
|
||||||
|
escape: 'Escape',
|
||||||
|
up: 'ArrowUp',
|
||||||
|
down: 'ArrowDown',
|
||||||
|
left: 'ArrowLeft',
|
||||||
|
right: 'ArrowRight',
|
||||||
|
backspace: 'Backspace',
|
||||||
|
delete: 'Delete',
|
||||||
|
slash: 'Slash',
|
||||||
|
backslash: 'Backslash',
|
||||||
|
minus: 'Minus',
|
||||||
|
plus: 'Equal',
|
||||||
|
equal: 'Equal',
|
||||||
|
comma: 'Comma',
|
||||||
|
period: 'Period',
|
||||||
|
quote: 'Quote',
|
||||||
|
semicolon: 'Semicolon',
|
||||||
|
bracketleft: 'BracketLeft',
|
||||||
|
bracketright: 'BracketRight',
|
||||||
|
backquote: 'Backquote',
|
||||||
|
};
|
||||||
|
const lower = normalized.toLowerCase();
|
||||||
|
if (exactMap[lower]) return exactMap[lower];
|
||||||
|
if (/^key[a-z]$/i.test(normalized) || /^digit[0-9]$/i.test(normalized)) {
|
||||||
|
return normalized[0]!.toUpperCase() + normalized.slice(1);
|
||||||
|
}
|
||||||
|
if (/^arrow(?:up|down|left|right)$/i.test(normalized)) {
|
||||||
|
return normalized[0]!.toUpperCase() + normalized.slice(1);
|
||||||
|
}
|
||||||
|
if (/^f\d{1,2}$/i.test(normalized)) {
|
||||||
|
return normalized.toUpperCase();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function acceleratorToKeyString(accelerator: string): string | null {
|
||||||
|
const normalized = accelerator
|
||||||
|
.replace(/\s+/g, '')
|
||||||
|
.replace(/cmdorctrl/gi, 'CommandOrControl');
|
||||||
|
if (!normalized) return null;
|
||||||
|
const parts = normalized.split('+').filter(Boolean);
|
||||||
|
const keyToken = parts.pop();
|
||||||
|
if (!keyToken) return null;
|
||||||
|
|
||||||
|
const eventParts: string[] = [];
|
||||||
|
for (const modifier of parts) {
|
||||||
|
const lower = modifier.toLowerCase();
|
||||||
|
if (lower === 'ctrl' || lower === 'control') {
|
||||||
|
eventParts.push('Ctrl');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (lower === 'alt' || lower === 'option') {
|
||||||
|
eventParts.push('Alt');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (lower === 'shift') {
|
||||||
|
eventParts.push('Shift');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (lower === 'meta' || lower === 'super' || lower === 'command' || lower === 'cmd') {
|
||||||
|
eventParts.push('Meta');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (lower === 'commandorcontrol') {
|
||||||
|
eventParts.push(ctx.platform.isMacOSPlatform ? 'Meta' : 'Ctrl');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedKey = acceleratorToKeyToken(keyToken);
|
||||||
|
if (!normalizedKey) return null;
|
||||||
|
eventParts.push(normalizedKey);
|
||||||
|
return eventParts.join('+');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateConfiguredShortcuts(shortcuts: Required<ShortcutsConfig>): void {
|
||||||
|
linuxOverlayShortcutCommands.clear();
|
||||||
|
const bindings: Array<[string | null, (string | number)[]]> = [
|
||||||
|
[shortcuts.triggerSubsync, [SPECIAL_COMMANDS.SUBSYNC_TRIGGER]],
|
||||||
|
[shortcuts.openRuntimeOptions, [SPECIAL_COMMANDS.RUNTIME_OPTIONS_OPEN]],
|
||||||
|
[shortcuts.openJimaku, [SPECIAL_COMMANDS.JIMAKU_OPEN]],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [accelerator, command] of bindings) {
|
||||||
|
if (!accelerator) continue;
|
||||||
|
const keyString = acceleratorToKeyString(accelerator);
|
||||||
|
if (keyString) {
|
||||||
|
linuxOverlayShortcutCommands.set(keyString, command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshConfiguredShortcuts(): Promise<void> {
|
||||||
|
updateConfiguredShortcuts(await window.electronAPI.getConfiguredShortcuts());
|
||||||
|
}
|
||||||
|
|
||||||
function dispatchYomitanPopupKeydown(
|
function dispatchYomitanPopupKeydown(
|
||||||
key: string,
|
key: string,
|
||||||
code: string,
|
code: string,
|
||||||
@@ -779,12 +892,14 @@ export function createKeyboardHandlers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setupMpvInputForwarding(): Promise<void> {
|
async function setupMpvInputForwarding(): Promise<void> {
|
||||||
const [keybindings, statsToggleKey, markWatchedKey] = await Promise.all([
|
const [keybindings, shortcuts, statsToggleKey, markWatchedKey] = await Promise.all([
|
||||||
window.electronAPI.getKeybindings(),
|
window.electronAPI.getKeybindings(),
|
||||||
|
window.electronAPI.getConfiguredShortcuts(),
|
||||||
window.electronAPI.getStatsToggleKey(),
|
window.electronAPI.getStatsToggleKey(),
|
||||||
window.electronAPI.getMarkWatchedKey(),
|
window.electronAPI.getMarkWatchedKey(),
|
||||||
]);
|
]);
|
||||||
updateKeybindings(keybindings);
|
updateKeybindings(keybindings);
|
||||||
|
updateConfiguredShortcuts(shortcuts);
|
||||||
ctx.state.statsToggleKey = statsToggleKey;
|
ctx.state.statsToggleKey = statsToggleKey;
|
||||||
ctx.state.markWatchedKey = markWatchedKey;
|
ctx.state.markWatchedKey = markWatchedKey;
|
||||||
syncKeyboardTokenSelection();
|
syncKeyboardTokenSelection();
|
||||||
@@ -982,6 +1097,14 @@ export function createKeyboardHandlers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const keyString = keyEventToString(e);
|
const keyString = keyEventToString(e);
|
||||||
|
const linuxOverlayCommand = ctx.platform.isLinuxPlatform
|
||||||
|
? linuxOverlayShortcutCommands.get(keyString)
|
||||||
|
: undefined;
|
||||||
|
if (linuxOverlayCommand) {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatchConfiguredMpvCommand(linuxOverlayCommand);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const command = ctx.state.keybindingsMap.get(keyString);
|
const command = ctx.state.keybindingsMap.get(keyString);
|
||||||
|
|
||||||
if (command) {
|
if (command) {
|
||||||
@@ -1015,6 +1138,7 @@ export function createKeyboardHandlers(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
setupMpvInputForwarding,
|
setupMpvInputForwarding,
|
||||||
|
refreshConfiguredShortcuts,
|
||||||
updateKeybindings,
|
updateKeybindings,
|
||||||
syncKeyboardTokenSelection,
|
syncKeyboardTokenSelection,
|
||||||
handleSubtitleContentUpdated,
|
handleSubtitleContentUpdated,
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ function describeCommand(command: (string | number)[]): string {
|
|||||||
}
|
}
|
||||||
if (first === SPECIAL_COMMANDS.SUBSYNC_TRIGGER) return 'Open subtitle sync controls';
|
if (first === SPECIAL_COMMANDS.SUBSYNC_TRIGGER) return 'Open subtitle sync controls';
|
||||||
if (first === SPECIAL_COMMANDS.RUNTIME_OPTIONS_OPEN) return 'Open runtime options';
|
if (first === SPECIAL_COMMANDS.RUNTIME_OPTIONS_OPEN) return 'Open runtime options';
|
||||||
|
if (first === SPECIAL_COMMANDS.JIMAKU_OPEN) return 'Open jimaku';
|
||||||
if (first === SPECIAL_COMMANDS.PLAYLIST_BROWSER_OPEN) return 'Open playlist browser';
|
if (first === SPECIAL_COMMANDS.PLAYLIST_BROWSER_OPEN) return 'Open playlist browser';
|
||||||
if (first === SPECIAL_COMMANDS.REPLAY_SUBTITLE) return 'Replay current subtitle';
|
if (first === SPECIAL_COMMANDS.REPLAY_SUBTITLE) return 'Replay current subtitle';
|
||||||
if (first === SPECIAL_COMMANDS.PLAY_NEXT_SUBTITLE) return 'Play next subtitle';
|
if (first === SPECIAL_COMMANDS.PLAY_NEXT_SUBTITLE) return 'Play next subtitle';
|
||||||
@@ -165,6 +166,7 @@ function sectionForCommand(command: (string | number)[]): string {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
first === SPECIAL_COMMANDS.RUNTIME_OPTIONS_OPEN ||
|
first === SPECIAL_COMMANDS.RUNTIME_OPTIONS_OPEN ||
|
||||||
|
first === SPECIAL_COMMANDS.JIMAKU_OPEN ||
|
||||||
first === SPECIAL_COMMANDS.PLAYLIST_BROWSER_OPEN ||
|
first === SPECIAL_COMMANDS.PLAYLIST_BROWSER_OPEN ||
|
||||||
first.startsWith(SPECIAL_COMMANDS.RUNTIME_OPTION_CYCLE_PREFIX)
|
first.startsWith(SPECIAL_COMMANDS.RUNTIME_OPTION_CYCLE_PREFIX)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -621,6 +621,7 @@ async function init(): Promise<void> {
|
|||||||
window.electronAPI.onConfigHotReload((payload: ConfigHotReloadPayload) => {
|
window.electronAPI.onConfigHotReload((payload: ConfigHotReloadPayload) => {
|
||||||
runGuarded('config:hot-reload', () => {
|
runGuarded('config:hot-reload', () => {
|
||||||
keyboardHandlers.updateKeybindings(payload.keybindings);
|
keyboardHandlers.updateKeybindings(payload.keybindings);
|
||||||
|
void keyboardHandlers.refreshConfiguredShortcuts();
|
||||||
subtitleRenderer.applySubtitleStyle(payload.subtitleStyle);
|
subtitleRenderer.applySubtitleStyle(payload.subtitleStyle);
|
||||||
subtitleRenderer.updateSecondarySubMode(payload.secondarySubMode);
|
subtitleRenderer.updateSecondarySubMode(payload.secondarySubMode);
|
||||||
ctx.state.subtitleSidebarConfig = payload.subtitleSidebar;
|
ctx.state.subtitleSidebarConfig = payload.subtitleSidebar;
|
||||||
|
|||||||
Reference in New Issue
Block a user