8 Commits

Author SHA1 Message Date
c24f99899b docs: add v0.11.1 changelog and expand installation guide
- Add v0.11.1 release entry with Wayland app-id and shortcut regression fixes
- Add "How the Pieces Fit Together" overview section to installation
- Add per-distro dependency install snippets (Arch, Ubuntu/Debian, Fedora, macOS, Windows)
- Add Windows prerequisites and getting-started steps
- Add First-Run Setup and Anki Setup sections; move Rofi theme to Optional Extras
- Expand Bun launcher requirement into explicit install step
- Reformat tables and callouts in usage.md for consistency
2026-04-04 15:28:13 -07:00
3aca581764 docs: replace GitHub callout blocks with plain text in README 2026-04-04 14:41:38 -07:00
ba540d09b2 docs: expand launcher and Windows guidance across README and docs-site
- Add downloads/release/license/TypeScript badges to README, replace static badge set
- Add macOS launcher install instructions (wget/curl + make install-macos)
- Add Windows experimental warning and bun run subminer clarification
- Update launcher-script.md: rename "wrapper" to "launcher", add Windows tip
- Update usage.md: add Windows mpv shortcut row, clarify shebang behavior per platform
- Update installation.md: mkdir -p, improve Linux/macOS launcher sections
2026-04-04 14:37:58 -07:00
6530d2ccbc Clarify install requirements and dependency checks
- Reclassify ffmpeg and MeCab as recommended or optional
- Add `subminer doctor` install verification step
- Refresh platform-specific install guidance and first-run tips
2026-04-04 12:51:37 -07:00
a784091ecb chore(release): finalize v0.11.1 prep 2026-04-04 00:45:47 -07:00
61c3e1e3c6 Change demo image link to GitHub asset
Updated SubMiner demo image link to a GitHub asset.
2026-04-04 00:34:13 -07:00
ce76a75630 chore: prep 0.11.1 release 2026-04-04 00:22:05 -07:00
52249db5b4 fix: restore linux modal shortcuts 2026-04-04 00:14:53 -07:00
28 changed files with 833 additions and 106 deletions

View File

@@ -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

View File

@@ -4,25 +4,21 @@
# SubMiner # SubMiner
## Turn mpv into a sentence-mining workstation.
Look up words with Yomitan, export to Anki in one key, track your immersion — all without leaving mpv. Look up words with Yomitan, export to Anki in one key, track your immersion — all without leaving mpv.
[![License: GPL v3](https://img.shields.io/badge/license-GPLv3-1a1a2e?style=flat-square)](https://www.gnu.org/licenses/gpl-3.0) [Installation](#quick-start) · [Requirements](#requirements) · [Usage](https://docs.subminer.moe/usage) · [Documentation](https://docs.subminer.moe)
[![Platform](https://img.shields.io/badge/platform-Linux%20·%20macOS%20·%20Windows-1a1a2e?style=flat-square)](https://github.com/ksyasuda/SubMiner)
[![Docs](https://img.shields.io/badge/docs-docs.subminer.moe-e6a817?style=flat-square)](https://docs.subminer.moe)
[![AUR](https://img.shields.io/aur/version/subminer-bin?style=flat-square&color=1a1a2e)](https://aur.archlinux.org/packages/subminer-bin)
[![SubMiner demo](./assets/minecard.webp)](./assets/minecard.mp4) [![Downloads](https://img.shields.io/github/downloads/ksyasuda/SubMiner/total?style=flat-square&color=1a1a2e)](https://github.com/ksyasuda/SubMiner/releases)
[![Release](https://img.shields.io/github/v/release/ksyasuda/SubMiner?style=flat-square&color=1a1a2e)](https://github.com/ksyasuda/SubMiner/releases/latest)
[![AUR](https://img.shields.io/aur/version/subminer-bin?style=flat-square&color=1a1a2e)](https://aur.archlinux.org/packages/subminer-bin)
[![Platform](https://img.shields.io/badge/platform-Linux%20·%20macOS%20·%20Windows-1a1a2e?style=flat-square)](https://github.com/ksyasuda/SubMiner)
[![License](https://img.shields.io/github/license/ksyasuda/SubMiner?style=flat-square&color=1a1a2e)](https://www.gnu.org/licenses/gpl-3.0)
[![TypeScript](https://img.shields.io/badge/TypeScript-1a1a2e?style=flat-square&logo=typescript&logoColor=3178c6)](https://www.typescriptlang.org)
[![SubMiner demo](./assets/minecard.webp)](https://github.com/user-attachments/assets/89e61895-e2b7-4b47-8d50-a35afe4132b2)
</div> </div>
## How It Works
SubMiner runs as an invisible Electron overlay on top of mpv. Subtitles render as an interactive layer. Move your cursor over any word and trigger a [Yomitan](https://github.com/yomidevs/yomitan) lookup. Press one key to snapshot the sentence, audio, and screenshot into Anki via AnkiConnect.
First-run setup requires the mpv plugin before it can finish. On Windows, the optional `SubMiner mpv` shortcut created during setup is the recommended playback entry point because it launches `mpv` with SubMiner's defaults directly, so you do not need an `mpv.conf` profile just to use it.
## Features ## Features
### Dictionary Lookups ### Dictionary Lookups
@@ -69,8 +65,6 @@ Local stats dashboard — watch time, anime library, vocabulary growth, mining t
Browse sibling episode files and the active mpv queue in one overlay modal. Open it with `Ctrl+Alt+P` to append episodes from the current directory, jump to queued items, remove entries, or reorder the playlist without leaving playback. Browse sibling episode files and the active mpv queue in one overlay modal. Open it with `Ctrl+Alt+P` to append episodes from the current directory, jump to queued items, remove entries, or reorder the playlist without leaving playback.
Managed local playback now reapplies your configured subtitle language priorities after mpv loads track metadata, so mixed subtitle sets can settle onto the expected primary and secondary tracks instead of staying on mpv's initial `sid=auto` guess.
<br> <br>
### Integrations ### Integrations
@@ -112,12 +106,15 @@ Managed local playback now reapplies your configured subtitle language prioritie
## Requirements ## Requirements
| | Required | Optional | | | Required | Recommended | Optional |
| -------------- | --------------------------------------- | ---------------------------------------------------------- | | -------------- | --------------------------------------- | ---------------------------------------- | ----------------------------------------------------------- |
| **Player** | [`mpv`](https://mpv.io) with IPC socket | — | | **Player** | [`mpv`](https://mpv.io) with IPC socket | — | — |
| **Processing** | `ffmpeg`, `mecab` + `mecab-ipadic` | `guessit` (AniSkip), `alass` / `ffsubsync` (subtitle sync) | | **Processing** | — | `ffmpeg` (audio clips & screenshots) | `mecab` + `mecab-ipadic` (annotation POS filtering), `guessit` (AniSkip), `alass` / `ffsubsync` (subtitle sync) |
| **Media** | — | `yt-dlp`, `chafa`, `ffmpegthumbnailer` | | **Media** | — | — | `yt-dlp`, `chafa`, `ffmpegthumbnailer` |
| **Selection** | — | `fzf` / `rofi` | | **Selection** | — | — | `fzf` / `rofi` |
> [!TIP]
> `ffmpeg` is not strictly required to run SubMiner, but without it audio clips and screenshots will not be attached to Anki cards. Most users will want it installed.
> [!NOTE] > [!NOTE]
> [`bun`](https://bun.sh) is required if building from source or using the CLI wrapper: `subminer`. Pre-built releases (AppImage, DMG, installer) do not require it. > [`bun`](https://bun.sh) is required if building from source or using the CLI wrapper: `subminer`. Pre-built releases (AppImage, DMG, installer) do not require it.
@@ -132,9 +129,9 @@ Managed local playback now reapplies your configured subtitle language prioritie
<summary><b>Arch Linux</b></summary> <summary><b>Arch Linux</b></summary>
```bash ```bash
paru -S --needed mpv ffmpeg mecab-git mecab-ipadic paru -S --needed mpv ffmpeg
# Optional # Optional
paru -S --needed yt-dlp fzf rofi chafa ffmpegthumbnailer xdotool xorg-xwininfo paru -S --needed mecab-git mecab-ipadic yt-dlp fzf rofi chafa ffmpegthumbnailer xdotool xorg-xwininfo
# Optional: subtitle sync (install at least one for subtitle syncing to work) # Optional: subtitle sync (install at least one for subtitle syncing to work)
paru -S --needed alass python-ffsubsync paru -S --needed alass python-ffsubsync
# X11 / XWAYLAND # X11 / XWAYLAND
@@ -147,9 +144,9 @@ paru -S --needed xdotool xorg-xwininfo
<summary><b>macOS</b></summary> <summary><b>macOS</b></summary>
```bash ```bash
brew install mpv ffmpeg mecab mecab-ipadic brew install mpv ffmpeg
# Optional # Optional
brew install yt-dlp fzf rofi chafa ffmpegthumbnailer brew install mecab mecab-ipadic yt-dlp fzf rofi chafa ffmpegthumbnailer
# Optional: subtitle sync (install at least one for subtitle syncing to work) # Optional: subtitle sync (install at least one for subtitle syncing to work)
brew install alass brew install alass
pip install ffsubsync pip install ffsubsync
@@ -164,7 +161,7 @@ Grant Accessibility permission to SubMiner in **System Settings > Privacy & Secu
Install [`mpv`](https://mpv.io/installation/) and [`ffmpeg`](https://ffmpeg.org/download.html) and ensure both are on your `PATH`. Install [`mpv`](https://mpv.io/installation/) and [`ffmpeg`](https://ffmpeg.org/download.html) and ensure both are on your `PATH`.
For MeCab, install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary. Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary for additional metadata enrichment.
</details> </details>
@@ -210,6 +207,16 @@ wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O ~
Download the latest DMG or ZIP from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) and drag `SubMiner.app` into `/Applications`. Download the latest DMG or ZIP from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) and drag `SubMiner.app` into `/Applications`.
Also download the `subminer` launcher (recommended):
```bash
sudo curl -fSL https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -o /usr/local/bin/subminer \
&& sudo chmod +x /usr/local/bin/subminer
```
> [!NOTE]
> The `subminer` launcher uses a [Bun](https://bun.sh) shebang. Make sure `bun` is on your `PATH`.
</details> </details>
<details> <details>
@@ -217,6 +224,10 @@ Download the latest DMG or ZIP from [GitHub Releases](https://github.com/ksyasud
Download the latest installer or portable `.zip` from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest). Make sure `mpv` is on your `PATH`. Download the latest installer or portable `.zip` from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest). Make sure `mpv` is on your `PATH`.
**Windows support is experimental.** Core features such as mining, annotations, and dictionary lookups work, but some functionality may be missing or unstable. Bug reports welcome.
**Note:** On Windows the `subminer` launcher requires [`bun`](https://bun.sh) and must be invoked with `bun run subminer` instead of running the script directly. The recommended alternative is the **SubMiner mpv** shortcut created during first-run setup — double-click it, drag files onto it, or run `SubMiner.exe --launch-mpv` from a terminal. See the [Windows mpv Shortcut](https://docs.subminer.moe/usage#windows-mpv-shortcut) section for details.
</details> </details>
<details> <details>
@@ -226,11 +237,20 @@ See the [build-from-source guide](https://docs.subminer.moe/installation#from-so
</details> </details>
### 2. First Launch ### 2. Check Dependencies
```bash
subminer doctor # verify mpv, ffmpeg, config, and socket
```
> [!NOTE]
> On Windows, use `bun run subminer doctor` or run `SubMiner.exe` directly — first-run setup will guide you through dependency checks.
### 3. First Launch
Run the app. On first launch SubMiner starts in the system tray, creates a default config, and opens a setup popup to finish config, install the mpv plugin, and configure Yomitan dictionaries. Run the app. On first launch SubMiner starts in the system tray, creates a default config, and opens a setup popup to finish config, install the mpv plugin, and configure Yomitan dictionaries.
### 3. Mine ### 4. Mine
```bash ```bash
subminer video.mkv # play video with overlay subminer video.mkv # play video with overlay
@@ -240,6 +260,8 @@ subminer stats -b # stats daemon in background
subminer stats -s # stop background stats daemon subminer stats -s # stop background stats daemon
``` ```
On **Windows**, the `subminer` script must be run with `bun run subminer` (e.g. `bun run subminer video.mkv`). The recommended alternative is the **SubMiner mpv** shortcut (created during setup) or `SubMiner.exe --launch-mpv`. Drag a video file onto the shortcut to play it, or double-click it to open mpv with SubMiner's defaults.
## Documentation ## Documentation
Full guides on configuration, Anki setup, Jellyfin, immersion tracking, and more: **[docs.subminer.moe](https://docs.subminer.moe)** Full guides on configuration, Anki setup, Jellyfin, immersion tracking, and more: **[docs.subminer.moe](https://docs.subminer.moe)**

View File

@@ -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 -->

View File

@@ -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 -->

View File

@@ -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 -->

View 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 -->

View File

@@ -1,6 +1,12 @@
# Changelog # Changelog
## v0.11.1 (2026-04-04)
- Fixed Linux packaged builds to 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`.
- Fixed Linux to restore 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 a playlist browser overlay modal for browsing sibling video files and the live mpv queue during playback, with a default `Ctrl+Alt+P` keybinding. - Added a playlist browser overlay modal for browsing sibling video files and the live mpv queue during playback, with a default `Ctrl+Alt+P` keybinding.
- Made mpv plugin installation mandatory in first-run setup (removed skip path); Finish stays disabled until the plugin is installed. - Made mpv plugin installation mandatory in first-run setup (removed skip path); Finish stays disabled until the plugin is installed.
- Fixed the Windows `SubMiner mpv` shortcut to launch mpv with required default args directly instead of requiring an `mpv.conf` profile named `subminer`. - Fixed the Windows `SubMiner mpv` shortcut to launch mpv with required default args directly instead of requiring an `mpv.conf` profile named `subminer`.
@@ -17,6 +23,7 @@
- Added a dedicated Subtitle Sidebar guide to the docs site with links from homepage and configuration docs. - Added a dedicated Subtitle Sidebar guide to the docs site with links from homepage and configuration docs.
## v0.10.0 (2026-03-29) ## v0.10.0 (2026-03-29)
- Fixed stats startup so the immersion tracker can run when `Bun.serve` is unavailable. - Fixed stats startup so the immersion tracker can run when `Bun.serve` is unavailable.
- Added a Node `http` fallback for Electron/runtime paths that do not expose Bun, so stats keeps working there too. - Added a Node `http` fallback for Electron/runtime paths that do not expose Bun, so stats keeps working there too.
- Updated Discord Rich Presence to the maintained `@xhayper/discord-rpc` wrapper. - Updated Discord Rich Presence to the maintained `@xhayper/discord-rpc` wrapper.
@@ -24,6 +31,7 @@
- Restored macOS mpv passthrough while the overlay subtitle sidebar is open so clicks outside the sidebar can refocus mpv and keep native keybindings working. - Restored macOS mpv passthrough while the overlay subtitle sidebar is open so clicks outside the sidebar can refocus mpv and keep native keybindings working.
## v0.9.3 (2026-03-25) ## v0.9.3 (2026-03-25)
- Moved YouTube primary subtitle language defaults to `youtube.primarySubLanguages`. - Moved YouTube primary subtitle language defaults to `youtube.primarySubLanguages`.
- Removed the placeholder YouTube subtitle retime step; downloaded primary subtitle tracks are now used directly. - Removed the placeholder YouTube subtitle retime step; downloaded primary subtitle tracks are now used directly.
- Removed the old internal YouTube retime helper and its tests. - Removed the old internal YouTube retime helper and its tests.
@@ -31,6 +39,7 @@
- Removed the legacy `youtubeSubgen.primarySubLanguages` config path from generated config and docs. - Removed the legacy `youtubeSubgen.primarySubLanguages` config path from generated config and docs.
## v0.9.2 (2026-03-25) ## v0.9.2 (2026-03-25)
- Fixed overlay pointer tracking so Windows click-through toggles immediately when the cursor enters or leaves subtitle regions. - Fixed overlay pointer tracking so Windows click-through toggles immediately when the cursor enters or leaves subtitle regions.
- Fixed Windows overlay window tracking on scaled displays by converting native tracked window bounds to Electron DIP coordinates. - Fixed Windows overlay window tracking on scaled displays by converting native tracked window bounds to Electron DIP coordinates.
- Fixed Windows direct `--youtube-play` startup so MPV boots reliably, stays paused until the app-owned subtitle flow is ready, and reuses an already-running SubMiner instance. - Fixed Windows direct `--youtube-play` startup so MPV boots reliably, stays paused until the app-owned subtitle flow is ready, and reuses an already-running SubMiner instance.
@@ -38,11 +47,13 @@
- Fixed `subminer <youtube-url>` on Linux so the YouTube playback flow waits for Yomitan to load before creating the overlay window. - Fixed `subminer <youtube-url>` on Linux so the YouTube playback flow waits for Yomitan to load before creating the overlay window.
## v0.9.1 (2026-03-24) ## v0.9.1 (2026-03-24)
- Reduced packaged release size by excluding duplicate `extraResources` payload and pruning docs, tests, sourcemaps, and other source-only files from Electron bundles. - Reduced packaged release size by excluding duplicate `extraResources` payload and pruning docs, tests, sourcemaps, and other source-only files from Electron bundles.
- Restored controller navigation and lookup/mining controls while the subtitle sidebar is open, while keeping true modal dialogs blocking controller actions. - Restored controller navigation and lookup/mining controls while the subtitle sidebar is open, while keeping true modal dialogs blocking controller actions.
- Fixed subtitle annotation clearing so explanatory contrast endings like `んですけど` are excluded consistently across the shared tokenizer filter and annotation stage. - Fixed subtitle annotation clearing so explanatory contrast endings like `んですけど` are excluded consistently across the shared tokenizer filter and annotation stage.
## v0.9.0 (2026-03-23) ## v0.9.0 (2026-03-23)
- Added an app-owned YouTube subtitle flow with absPlayer-style timedtext parsing that auto-loads the default primary subtitle plus a best-effort secondary at startup and resumes once the primary is ready. - Added an app-owned YouTube subtitle flow with absPlayer-style timedtext parsing that auto-loads the default primary subtitle plus a best-effort secondary at startup and resumes once the primary is ready.
- Added a manual YouTube subtitle picker on `Ctrl+Alt+C` so subtitle selection can be retried on demand during active YouTube playback. - Added a manual YouTube subtitle picker on `Ctrl+Alt+C` so subtitle selection can be retried on demand during active YouTube playback.
- Added yt-dlp metadata probing so YouTube playback and immersion tracking record canonical video title and channel metadata. - Added yt-dlp metadata probing so YouTube playback and immersion tracking record canonical video title and channel metadata.
@@ -57,6 +68,7 @@
- Reused existing authoritative YouTube subtitle tracks when present, fell back only for missing sides, and kept native mpv secondary subtitle rendering hidden so the overlay remains the visible secondary subtitle surface. - Reused existing authoritative YouTube subtitle tracks when present, fell back only for missing sides, and kept native mpv secondary subtitle rendering hidden so the overlay remains the visible secondary subtitle surface.
## v0.8.0 (2026-03-22) ## v0.8.0 (2026-03-22)
- Added a configurable subtitle sidebar feature (`subtitleSidebar`) with overlay/embedded rendering, click-to-seek cue list, and hot-reloadable visibility and behavior controls. - Added a configurable subtitle sidebar feature (`subtitleSidebar`) with overlay/embedded rendering, click-to-seek cue list, and hot-reloadable visibility and behavior controls.
- Added a rendered sidebar modal with cue list display, click-to-seek, active-cue highlighting, and embedded layout support. - Added a rendered sidebar modal with cue list display, click-to-seek, active-cue highlighting, and embedded layout support.
- Added sidebar snapshot plumbing between main and renderer for overlay/sidebar synchronization. - Added sidebar snapshot plumbing between main and renderer for overlay/sidebar synchronization.
@@ -68,6 +80,7 @@
- Prevented stale subtitle refreshes from regressing active-cue state. - Prevented stale subtitle refreshes from regressing active-cue state.
## v0.7.0 (2026-03-19) ## v0.7.0 (2026-03-19)
- Added a full local immersion dashboard release line with Overview, Library, Trends, Vocabulary, and Sessions drill-down views backed by SQLite tracking data. - Added a full local immersion dashboard release line with Overview, Library, Trends, Vocabulary, and Sessions drill-down views backed by SQLite tracking data.
- Added browser-first stats workflows: `subminer stats`, background stats daemon controls (`-b` / `-s`), stats cleanup, and dashboard-side mining actions with media enrichment. - Added browser-first stats workflows: `subminer stats`, background stats daemon controls (`-b` / `-s`), stats cleanup, and dashboard-side mining actions with media enrichment.
- Improved stats accuracy and scale handling with Yomitan token counts, full session timelines, known-word timeline fixes, cross-media vocabulary fixes, and clearer session charts. - Improved stats accuracy and scale handling with Yomitan token counts, full session timelines, known-word timeline fixes, cross-media vocabulary fixes, and clearer session charts.
@@ -77,16 +90,20 @@
- Excluded auxiliary-stem `そうだ` grammar tails (MeCab POS3 `助動詞語幹`) from subtitle annotation metadata so frequency, JLPT, and N+1 styling no longer bleed onto grammar-tail tokens. - Excluded auxiliary-stem `そうだ` grammar tails (MeCab POS3 `助動詞語幹`) from subtitle annotation metadata so frequency, JLPT, and N+1 styling no longer bleed onto grammar-tail tokens.
## v0.6.5 (2026-03-15) ## v0.6.5 (2026-03-15)
- Seeded the AUR checkout with the repo `.SRCINFO` template before rewriting metadata so tagged releases do not depend on prior AUR state. - Seeded the AUR checkout with the repo `.SRCINFO` template before rewriting metadata so tagged releases do not depend on prior AUR state.
## v0.6.4 (2026-03-15) ## v0.6.4 (2026-03-15)
- Reworked AUR metadata generation to update `.SRCINFO` directly instead of depending on runner `makepkg`, fixing tagged release publishing for `subminer-bin`. - Reworked AUR metadata generation to update `.SRCINFO` directly instead of depending on runner `makepkg`, fixing tagged release publishing for `subminer-bin`.
## v0.6.3 (2026-03-15) ## v0.6.3 (2026-03-15)
- Expanded `Alt+C` into an inline controller config/remap flow with preferred-controller saving and per-action learn mode for buttons, triggers, and stick directions. - Expanded `Alt+C` into an inline controller config/remap flow with preferred-controller saving and per-action learn mode for buttons, triggers, and stick directions.
- Automated `subminer-bin` AUR package updates from the tagged release workflow. - Automated `subminer-bin` AUR package updates from the tagged release workflow.
## v0.6.2 (2026-03-12) ## v0.6.2 (2026-03-12)
- Added `yomitan.externalProfilePath` so SubMiner can reuse another Electron app's Yomitan profile in read-only mode. - Added `yomitan.externalProfilePath` so SubMiner can reuse another Electron app's Yomitan profile in read-only mode.
- Reused external Yomitan dictionaries/settings without writing back to that profile. - Reused external Yomitan dictionaries/settings without writing back to that profile.
- Let launcher-managed playback honor external Yomitan config instead of forcing first-run setup. - Let launcher-managed playback honor external Yomitan config instead of forcing first-run setup.
@@ -94,6 +111,7 @@
- Let first-run setup complete without internal dictionaries while external Yomitan is configured, then require an internal dictionary again only if that external profile is later removed. - Let first-run setup complete without internal dictionaries while external Yomitan is configured, then require an internal dictionary again only if that external profile is later removed.
## v0.6.1 (2026-03-12) ## v0.6.1 (2026-03-12)
- Added Chrome Gamepad API controller support for keyboard-only overlay mode. - Added Chrome Gamepad API controller support for keyboard-only overlay mode.
- Added configurable controller bindings for lookup, mining, popup navigation, Yomitan audio, mpv pause, and d-pad fallback navigation. - Added configurable controller bindings for lookup, mining, popup navigation, Yomitan audio, mpv pause, and d-pad fallback navigation.
- Added smooth, slower popup scrolling for controller navigation. - Added smooth, slower popup scrolling for controller navigation.
@@ -103,11 +121,13 @@
- Added an enforced `verify:config-example` gate so checked-in example config artifacts cannot drift silently. - Added an enforced `verify:config-example` gate so checked-in example config artifacts cannot drift silently.
## v0.5.6 (2026-03-10) ## v0.5.6 (2026-03-10)
- Persisted merged character-dictionary MRU state as soon as a new retained set is built so revisits do not get dropped if later Yomitan import work fails. - Persisted merged character-dictionary MRU state as soon as a new retained set is built so revisits do not get dropped if later Yomitan import work fails.
- Fixed early Electron startup writing config and user data under a lowercase `~/.config/subminer` path instead of canonical `~/.config/SubMiner`. - Fixed early Electron startup writing config and user data under a lowercase `~/.config/subminer` path instead of canonical `~/.config/SubMiner`.
- Kept JLPT underline colors stable during Yomitan hover and selection states, even when tokens also use known, N+1, name-match, or frequency styling. - Kept JLPT underline colors stable during Yomitan hover and selection states, even when tokens also use known, N+1, name-match, or frequency styling.
## v0.5.1 (2026-03-09) ## v0.5.1 (2026-03-09)
- Removed the old YouTube subtitle-generation mode switch; YouTube playback now resolves subtitles before mpv starts. - Removed the old YouTube subtitle-generation mode switch; YouTube playback now resolves subtitles before mpv starts.
- Hardened YouTube AI subtitle fixing so fenced/text-only responses keep original cue timing. - Hardened YouTube AI subtitle fixing so fenced/text-only responses keep original cue timing.
- Skipped AniSkip during URL/YouTube playback where anime metadata cannot be resolved reliably. - Skipped AniSkip during URL/YouTube playback where anime metadata cannot be resolved reliably.
@@ -116,12 +136,14 @@
- Hardened the Windows signing/release workflow with SignPath retry handling for signed `.exe` and `.zip` artifacts. - Hardened the Windows signing/release workflow with SignPath retry handling for signed `.exe` and `.zip` artifacts.
## v0.5.0 (2026-03-08) ## v0.5.0 (2026-03-08)
- Added the initial packaged Windows release. - Added the initial packaged Windows release.
- Added Windows-native mpv window tracking, launcher/runtime plumbing, and packaged helper assets. - Added Windows-native mpv window tracking, launcher/runtime plumbing, and packaged helper assets.
- Improved close behavior so ending playback hides the visible overlay while the background app stays running. - Improved close behavior so ending playback hides the visible overlay while the background app stays running.
- Limited the native overlay outline/debug frame to debug mode on Windows. - Limited the native overlay outline/debug frame to debug mode on Windows.
## v0.3.0 (2026-03-05) ## v0.3.0 (2026-03-05)
- Added keyboard-driven Yomitan navigation and popup controls, including optional auto-pause. - Added keyboard-driven Yomitan navigation and popup controls, including optional auto-pause.
- Added subtitle/jump keyboard handling fixes for smoother subtitle playback control. - Added subtitle/jump keyboard handling fixes for smoother subtitle playback control.
- Improved Anki/Yomitan reliability with stronger Yomitan proxy syncing and safer extension refresh logic. - Improved Anki/Yomitan reliability with stronger Yomitan proxy syncing and safer extension refresh logic.
@@ -132,6 +154,7 @@
- Removed docs Plausible integration and cleaned associated tracker settings. - Removed docs Plausible integration and cleaned associated tracker settings.
## v0.2.3 (2026-03-02) ## v0.2.3 (2026-03-02)
- Added performance and tokenization optimizations (faster warmup, persistent MeCab usage, reduced enrichment lookups). - Added performance and tokenization optimizations (faster warmup, persistent MeCab usage, reduced enrichment lookups).
- Added subtitle controls for no-jump delay shifts. - Added subtitle controls for no-jump delay shifts.
- Improved subtitle highlight logic with priority and reliability fixes. - Improved subtitle highlight logic with priority and reliability fixes.
@@ -140,30 +163,36 @@
- Updated startup flow to load dictionaries asynchronously and unblock first tokenization sooner. - Updated startup flow to load dictionaries asynchronously and unblock first tokenization sooner.
## v0.2.2 (2026-03-01) ## v0.2.2 (2026-03-01)
- Improved subtitle highlighting reliability for frequency modes. - Improved subtitle highlighting reliability for frequency modes.
- Fixed Jellyfin misc info formatting cleanup. - Fixed Jellyfin misc info formatting cleanup.
- Version bump maintenance for 0.2.2. - Version bump maintenance for 0.2.2.
## v0.2.1 (2026-03-01) ## v0.2.1 (2026-03-01)
- Delivered Jellyfin and Subsync fixes from release patch cycle. - Delivered Jellyfin and Subsync fixes from release patch cycle.
- Version bump maintenance for 0.2.1. - Version bump maintenance for 0.2.1.
## v0.2.0 (2026-03-01) ## v0.2.0 (2026-03-01)
- Added task-related release work for the overlay 2.0 cycle. - Added task-related release work for the overlay 2.0 cycle.
- Introduced Overlay 2.0. - Introduced Overlay 2.0.
- Improved release automation reliability. - Improved release automation reliability.
## v0.1.2 (2026-02-24) ## v0.1.2 (2026-02-24)
- Added encrypted AniList token handling and default GNOME keyring support. - Added encrypted AniList token handling and default GNOME keyring support.
- Added launcher passthrough for password-store flows (Jellyfin path). - Added launcher passthrough for password-store flows (Jellyfin path).
- Updated docs for auth and integration behavior. - Updated docs for auth and integration behavior.
- Version bump maintenance for 0.1.2. - Version bump maintenance for 0.1.2.
## v0.1.1 (2026-02-23) ## v0.1.1 (2026-02-23)
- Fixed overlay modal focus handling (`grab input`) behavior. - Fixed overlay modal focus handling (`grab input`) behavior.
- Version bump maintenance for 0.1.1. - Version bump maintenance for 0.1.1.
## v0.1.0 (2026-02-23) ## v0.1.0 (2026-02-23)
- Bootstrapped Electron runtime, services, and composition model. - Bootstrapped Electron runtime, services, and composition model.
- Added runtime asset packaging and dependency vendoring. - Added runtime asset packaging and dependency vendoring.
- Added project docs baseline, setup guides, architecture notes, and submodule/runtime assets. - Added project docs baseline, setup guides, architecture notes, and submodule/runtime assets.

View File

@@ -1,17 +1,30 @@
# Installation # Installation
## How the Pieces Fit Together
SubMiner is an overlay that sits on top of mpv. It connects to mpv through an IPC socket, renders subtitles as interactive text using a bundled Yomitan dictionary engine, and optionally creates Anki flashcards via AnkiConnect.
To get a working setup you need:
1. **mpv** launched with an IPC socket so SubMiner can read subtitle data
2. **SubMiner** (the Electron overlay app)
3. **Dictionaries** imported into the bundled Yomitan instance (lookups won't work without at least one)
4. **Anki + AnkiConnect** _(optional but recommended)_ for card creation and enrichment
The `subminer` launcher script handles step 1 automatically. If you launch mpv yourself or from another tool, you must pass `--input-ipc-server=/tmp/subminer-socket` (or the equivalent named pipe on Windows) — without it the overlay will start but subtitles will never appear.
## Requirements ## Requirements
### System Dependencies ### System Dependencies
| Dependency | Required | Notes | | Dependency | Required | Notes |
| -------------------- | ---------- | -------------------------------------------------------- | | -------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Bun | Yes | Required for `subminer` wrapper and source workflows | | mpv | Yes | Must support IPC sockets (`--input-ipc-server`) |
| mpv | Yes | Must support IPC sockets (`--input-ipc-server`) | | Bun | For wrapper | Required for `subminer` CLI wrapper and source builds. Pre-built releases (AppImage, DMG, installer) work without it — only the `subminer` wrapper script needs Bun on `PATH`. |
| ffmpeg | For media | Audio extraction and screenshot generation | | ffmpeg | Recommended | Audio extraction and screenshot generation. Without it SubMiner still runs, but audio and image fields on Anki cards will be empty. |
| MeCab + mecab-ipadic | No | Optional Japanese metadata enrichment (not the primary tokenizer) | | MeCab + mecab-ipadic | No | Adds part-of-speech data used to filter particles out of N+1, JLPT, and frequency annotations. Without it annotations still render, but POS-based filtering is less precise. |
| fuse2 | Linux only | Required for AppImage | | fuse2 | Linux only | Required for AppImage |
| yt-dlp | No | Recommended for YouTube playback and subtitle extraction | | yt-dlp | No | Recommended for YouTube playback and subtitle extraction |
### Platform-Specific ### Platform-Specific
@@ -21,19 +34,77 @@
- Sway (uses `swaymsg`) - Sway (uses `swaymsg`)
- X11 (uses `xdotool` and `xwininfo`) - X11 (uses `xdotool` and `xwininfo`)
<details>
<summary><b>Arch Linux</b></summary>
```bash
sudo pacman -S --needed mpv ffmpeg
# Optional
sudo pacman -S --needed mecab mecab-ipadic yt-dlp fzf rofi chafa ffmpegthumbnailer
# Optional: subtitle sync (at least one needed for subtitle syncing)
paru -S --needed alass python-ffsubsync
# X11 / XWAYLAND
sudo pacman -S --needed xdotool xorg-xwininfo
```
</details>
<details>
<summary><b>Ubuntu / Debian</b></summary>
```bash
sudo apt install mpv ffmpeg
# Optional
sudo apt install mecab libmecab-dev mecab-ipadic-utf8 fzf rofi chafa ffmpegthumbnailer yt-dlp
# X11
sudo apt install xdotool x11-utils
# Optional: subtitle sync
pip install ffsubsync
# alass is not in apt — install via cargo: cargo install alass-cli
```
</details>
<details>
<summary><b>Fedora</b></summary>
```bash
sudo dnf install mpv ffmpeg
# Optional
sudo dnf install mecab mecab-ipadic fzf rofi chafa ffmpegthumbnailer yt-dlp
# X11
sudo dnf install xdotool xorg-x11-utils
# Optional: subtitle sync
pip install ffsubsync
# alass: cargo install alass-cli
```
</details>
**macOS** — macOS 10.13 or later. Accessibility permission required for window tracking. **macOS** — macOS 10.13 or later. Accessibility permission required for window tracking.
**Windows** — Windows 10 or later. Install `mpv`; keep it on `PATH` for auto-discovery or set `mpv.executablePath` in config if `mpv.exe` lives elsewhere. SubMiner's packaged build handles window tracking directly. ```bash
brew install mpv ffmpeg
# Optional but recommended for annotations
brew install mecab mecab-ipadic
# Optional
brew install yt-dlp fzf rofi chafa ffmpegthumbnailer
# Optional: subtitle sync
brew install alass
pip install ffsubsync
```
**Windows** — Windows 10 or later. Install [`mpv`](https://mpv.io/installation/) and [`ffmpeg`](https://ffmpeg.org/download.html) and ensure both are on `PATH`. Keep `mpv.exe` on `PATH` for auto-discovery or set `mpv.executablePath` in config if it lives elsewhere. SubMiner's packaged build handles window tracking directly. Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary.
### Optional Tools ### Optional Tools
| Tool | Purpose | | Tool | Purpose |
| ----------------- | ------------------------------------------------------------- | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| fzf | Terminal-based video picker (default) | | fzf | Terminal-based video picker (default) |
| rofi | GUI-based video picker | | rofi | GUI-based video picker |
| chafa | Thumbnail previews in fzf | | chafa | Thumbnail previews in fzf |
| ffmpegthumbnailer | Generate video thumbnails for picker | | ffmpegthumbnailer | Generate video thumbnails for picker |
| guessit | Better AniSkip title/season/episode parsing for file playback | | guessit | Better AniSkip title/season/episode parsing for file playback |
| alass | Subtitle sync engine (preferred) — must be on `PATH` or set `subsync.alass_path` in config; subtitle syncing is disabled without it or ffsubsync | | alass | Subtitle sync engine (preferred) — must be on `PATH` or set `subsync.alass_path` in config; subtitle syncing is disabled without it or ffsubsync |
| ffsubsync | Subtitle sync engine (fallback) — must be on `PATH` or set `subsync.ffsubsync_path` in config; subtitle syncing is disabled without it or alass | | ffsubsync | Subtitle sync engine (fallback) — must be on `PATH` or set `subsync.ffsubsync_path` in config; subtitle syncing is disabled without it or alass |
@@ -57,20 +128,31 @@ makepkg -si
### AppImage (Recommended) ### AppImage (Recommended)
Download the latest AppImage from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest): Download the latest AppImage and the `subminer` launcher from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest).
**Step 1 — Install Bun** (required for the launcher):
```bash ```bash
curl -fsSL https://bun.sh/install | bash
```
The `subminer` launcher uses a Bun shebang. The AppImage itself does **not** need Bun — only the launcher does. If you skip the launcher and run the AppImage directly (for example `SubMiner.AppImage --start`), you can skip this step, but you will need to configure `mpv.conf` with `input-ipc-server=/tmp/subminer-socket` manually.
**Step 2 — Download and install:**
```bash
mkdir -p ~/.local/bin
# Download and install AppImage # Download and install AppImage
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/SubMiner.AppImage -O ~/.local/bin/SubMiner.AppImage wget https://github.com/ksyasuda/SubMiner/releases/latest/download/SubMiner.AppImage -O ~/.local/bin/SubMiner.AppImage
chmod +x ~/.local/bin/SubMiner.AppImage chmod +x ~/.local/bin/SubMiner.AppImage
# Download subminer wrapper script # Download and install the subminer launcher (recommended)
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O ~/.local/bin/subminer wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O ~/.local/bin/subminer
chmod +x ~/.local/bin/subminer chmod +x ~/.local/bin/subminer
``` ```
The `subminer` wrapper uses a Bun shebang (`#!/usr/bin/env bun`), so [Bun](https://bun.sh) must be installed and available on `PATH`. The `subminer` launcher is the recommended way to use SubMiner on Linux. It ensures mpv is launched with the correct IPC socket and SubMiner defaults so you don't need to configure `mpv.conf` manually.
### From Source ### From Source
@@ -103,9 +185,33 @@ A **ZIP** artifact is also available as a fallback — unzip and drag `SubMiner.
Install dependencies using Homebrew: Install dependencies using Homebrew:
```bash ```bash
brew install mpv mecab mecab-ipadic brew install mpv ffmpeg
# Optional but recommended if you use N+1, JLPT, or frequency annotations
brew install mecab mecab-ipadic
``` ```
#### Install the `subminer` launcher (recommended)
The `subminer` launcher is the recommended way to use SubMiner on macOS. It launches mpv with the correct IPC socket and SubMiner defaults so you don't need to set up an `mpv.conf` profile manually.
Download it from the same [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) page:
```bash
sudo wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O /usr/local/bin/subminer
sudo chmod +x /usr/local/bin/subminer
```
Or with curl:
```bash
sudo curl -fSL https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -o /usr/local/bin/subminer
sudo chmod +x /usr/local/bin/subminer
```
::: warning Bun required for the launcher
The `subminer` launcher uses a Bun shebang (`#!/usr/bin/env bun`), so [Bun](https://bun.sh) must be installed and available on `PATH`. Install Bun if you haven't already: `curl -fsSL https://bun.sh/install | bash`.
:::
### From Source (macOS) ### From Source (macOS)
```bash ```bash
@@ -123,19 +229,44 @@ For unsigned local builds:
bun run build:mac:unsigned bun run build:mac:unsigned
``` ```
Build and install the launcher alongside the app:
```bash
make install-macos
```
This builds the `subminer` launcher into `dist/launcher/subminer` and installs it to `~/.local/bin/subminer` along with the app bundle and rofi theme. To install to `/usr/local/bin` instead (already on the default macOS `PATH`):
```bash
sudo make install-macos PREFIX=/usr/local
```
### Gatekeeper
If macOS blocks SubMiner on first launch, right-click the app and select **Open** to bypass the warning. Alternatively, remove the quarantine attribute:
```bash
xattr -d com.apple.quarantine /Applications/SubMiner.app
```
### Accessibility Permission ### Accessibility Permission
After launching SubMiner for the first time, grant accessibility permission: After launching SubMiner for the first time, grant accessibility permission:
1. Open **System Preferences****Security & Privacy****Privacy** tab 1. Open **System Settings****Privacy & Security****Accessibility**
2. Select **Accessibility** from the left sidebar 2. Enable SubMiner in the list (add it if it does not appear)
3. Add SubMiner to the list
Without this permission, window tracking will not work and the overlay won't follow the mpv window. Without this permission, window tracking will not work and the overlay won't follow the mpv window.
### macOS Usage Notes ### macOS Usage Notes
**Launching MPV with IPC:** **Launching with the `subminer` launcher (recommended):**
```bash
subminer video.mkv
```
The launcher handles the IPC socket and SubMiner defaults automatically. If you prefer to launch mpv manually:
```bash ```bash
mpv --input-ipc-server=/tmp/subminer-socket video.mkv mpv --input-ipc-server=/tmp/subminer-socket video.mkv
@@ -160,6 +291,17 @@ binary_path=/Applications/SubMiner.app/Contents/MacOS/subminer
## Windows ## Windows
> [!WARNING]
> **Windows support is experimental.** Core features — mining, annotations, and dictionary lookups — work, but some functionality may be missing or unstable. Bug reports welcome.
### Prerequisites
1. Install [`mpv`](https://mpv.io/installation/) and ensure `mpv.exe` is on `PATH`. If mpv is installed elsewhere, you can set `mpv.executablePath` in `config.jsonc` or use the first-run setup field to point at the executable.
2. Install [`ffmpeg`](https://ffmpeg.org/download.html) and add it to `PATH` — recommended for audio/screenshot extraction (without it, media fields on Anki cards will be empty).
3. _(Optional)_ Install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary for annotation POS filtering.
No compositor tools or window helpers are needed — native window tracking is built in on Windows.
### Installer (Recommended) ### Installer (Recommended)
Download the latest Windows installer from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest): Download the latest Windows installer from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest):
@@ -167,16 +309,24 @@ Download the latest Windows installer from [GitHub Releases](https://github.com/
- `SubMiner-<version>.exe` installs the app, Start menu shortcut, and default files under `Program Files` - `SubMiner-<version>.exe` installs the app, Start menu shortcut, and default files under `Program Files`
- `SubMiner-<version>.zip` is available as a portable fallback - `SubMiner-<version>.zip` is available as a portable fallback
Install `mpv` separately and ensure `mpv.exe` is on `PATH`. `ffmpeg` is still required for media extraction, and MeCab remains optional. ### Getting Started on Windows
### Windows Usage Notes 1. **Run `SubMiner.exe` once** — first-run setup creates `%APPDATA%\SubMiner\config.jsonc`, installs the mpv plugin, and opens Yomitan settings for dictionary import.
2. **Create the SubMiner mpv shortcut** _(recommended)_ — the setup popup offers to create a `SubMiner mpv` Start Menu and/or Desktop shortcut. This is the recommended way to launch playback on Windows.
3. **Play a video** — double-click the shortcut, drag a video file onto it, or run from a terminal:
- Launch `SubMiner.exe` once to let the first-run setup flow seed `%APPDATA%\\SubMiner\\config.jsonc`, require mpv plugin installation, and open bundled Yomitan settings. The optional `SubMiner mpv` Start Menu/Desktop shortcut can also be created during setup, and on Windows it is the recommended way to launch mpv playback with SubMiner defaults. ```powershell
- If `mpv.exe` is not on `PATH`, set `mpv.executablePath` in `config.jsonc` or use the first-run setup field to point at the executable. Leave it blank to keep PATH auto-discovery. & "C:\Program Files\SubMiner\SubMiner.exe" --launch-mpv "C:\Videos\episode 01.mkv"
- `SubMiner.exe --launch-mpv` and the optional `SubMiner mpv` shortcut pass SubMiner's default mpv socket/subtitle args directly and do not require an `mpv.conf` profile named `subminer`. ```
- First-run mpv plugin installs pin `binary_path` to the current `SubMiner.exe` automatically. Manual plugin configs can leave `binary_path` empty unless SubMiner is installed in a non-standard location.
- Windows plugin installs rewrite `socket_path` to `\\.\pipe\subminer-socket`; do not keep `/tmp/subminer-socket` on Windows. The shortcut and `--launch-mpv` pass SubMiner's default IPC socket and subtitle args directly — no `mpv.conf` profile is needed.
- Native window tracking is built in on Windows; no `xdotool`, `xwininfo`, or compositor-specific helper is required.
### Windows-Specific Notes
- The `subminer` launcher script requires [Bun](https://bun.sh) and must be invoked with `bun run subminer` on Windows since the shebang is not supported. The **SubMiner mpv** shortcut or `SubMiner.exe --launch-mpv` is the simpler alternative.
- First-run plugin installs pin `binary_path` to the current `SubMiner.exe` automatically. Manual plugin configs can leave `binary_path` empty unless SubMiner is in a non-standard location.
- Plugin installs rewrite `socket_path` to `\\.\pipe\subminer-socket` — do not keep `/tmp/subminer-socket` on Windows.
- Config is stored at `%APPDATA%\SubMiner\config.jsonc`.
### From Source (Windows) ### From Source (Windows)
@@ -184,10 +334,14 @@ Install `mpv` separately and ensure `mpv.exe` is on `PATH`. `ffmpeg` is still re
git clone https://github.com/ksyasuda/SubMiner.git git clone https://github.com/ksyasuda/SubMiner.git
cd SubMiner cd SubMiner
bun install bun install
# Windows requires building the texthooker-ui submodule manually before
# the main build (Linux/macOS handle this automatically during `bun run build`).
Set-Location vendor/texthooker-ui Set-Location vendor/texthooker-ui
bun install --frozen-lockfile bun install --frozen-lockfile
bun run build bun run build
Set-Location ../.. Set-Location ../..
bun run build:win bun run build:win
``` ```
@@ -220,29 +374,32 @@ cp /tmp/plugin/subminer.conf ~/.config/mpv/script-opts/
# make install-plugin # make install-plugin
``` ```
## Rofi Theme (Linux Only)
SubMiner ships a default rofi theme at `assets/themes/subminer.rasi`.
Install path (default auto-detected by `subminer`):
- Linux: `~/.local/share/SubMiner/themes/subminer.rasi`
- macOS: `~/Library/Application Support/SubMiner/themes/subminer.rasi`
```bash
mkdir -p ~/.local/share/SubMiner/themes
cp /tmp/assets/themes/subminer.rasi ~/.local/share/SubMiner/themes/subminer.rasi
```
Override with `SUBMINER_ROFI_THEME=/absolute/path/to/theme.rasi`.
See [MPV Plugin](/mpv-plugin) for the full configuration reference, keybindings, script messages, and binary auto-detection details. See [MPV Plugin](/mpv-plugin) for the full configuration reference, keybindings, script messages, and binary auto-detection details.
## Anki Setup (Recommended)
If you plan to mine Anki cards (the primary use case for most users):
1. Install [Anki](https://apps.ankiweb.net/).
2. Install the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on — open Anki, go to **Tools → Add-ons → Get Add-ons**, enter code `2055492159`.
3. Restart Anki and keep it running while using SubMiner.
AnkiConnect listens on `http://127.0.0.1:8765` by default. SubMiner will connect to it automatically with no extra config needed for basic card creation.
For enrichment configuration (sentence, audio, screenshot fields), see [Anki Integration](/anki-integration).
## Verify Installation ## Verify Installation
After installing, confirm SubMiner is working: After installing, run the built-in diagnostic to confirm dependencies are in place:
On Windows, replace `SubMiner.AppImage` with `SubMiner.exe` in the direct app commands below. ```bash
subminer doctor
```
This checks for the app binary, mpv, ffmpeg, config file, and socket path. Fix any failures before continuing.
> [!NOTE]
> On Windows, use `bun run subminer doctor` or run `SubMiner.exe` directly. Replace `SubMiner.AppImage` with `SubMiner.exe` in the direct app commands below.
```bash ```bash
# Play a file (default plugin config auto-starts visible overlay and waits for annotation readiness; first launch may open first-run setup popup) # Play a file (default plugin config auto-starts visible overlay and waits for annotation readiness; first launch may open first-run setup popup)
@@ -264,4 +421,38 @@ SubMiner.AppImage --help # Show all CLI options
You should see the overlay appear over mpv. If subtitles are loaded in the video, they will appear as interactive text in the overlay. You should see the overlay appear over mpv. If subtitles are loaded in the video, they will appear as interactive text in the overlay.
## First-Run Setup
On first launch SubMiner creates a default config file automatically and opens a setup popup. You do **not** need to create the config manually — SubMiner handles it.
The setup popup walks you through:
- **Config file**: auto-created at `~/.config/SubMiner/config.jsonc` (Linux/macOS) or `%APPDATA%\SubMiner\config.jsonc` (Windows)
- **mpv plugin**: install the bundled Lua plugin for in-player keybindings
- **Yomitan dictionaries**: import at least one dictionary so lookups work
- **Windows shortcut** _(Windows only)_: optionally create a `SubMiner mpv` Start Menu/Desktop shortcut
The `Finish setup` button stays disabled until the plugin is installed and at least one dictionary is imported. Once you finish, SubMiner will not show the popup again.
> [!TIP]
> You can re-open the setup popup at any time with `subminer --setup` or `SubMiner.AppImage --setup`.
## Optional Extras
### Rofi Theme (Linux Only)
SubMiner ships a default rofi theme at `assets/themes/subminer.rasi`.
Install path (default auto-detected by `subminer`):
- Linux: `~/.local/share/SubMiner/themes/subminer.rasi`
- macOS: `~/Library/Application Support/SubMiner/themes/subminer.rasi`
```bash
mkdir -p ~/.local/share/SubMiner/themes
cp /tmp/assets/themes/subminer.rasi ~/.local/share/SubMiner/themes/subminer.rasi
```
Override with `SUBMINER_ROFI_THEME=/absolute/path/to/theme.rasi`.
Next: [Usage](/usage) — learn about the `subminer` wrapper, keybindings, and YouTube playback. Next: [Usage](/usage) — learn about the `subminer` wrapper, keybindings, and YouTube playback.

View File

@@ -1,6 +1,10 @@
# Launcher Script # Launcher Script
The `subminer` wrapper script is an all-in-one launcher that handles video selection, mpv startup, and overlay management. It's a Bun script distributed alongside the AppImage. The `subminer` launcher is an all-in-one script that handles video selection, mpv startup, and overlay management. It is the recommended way to use SubMiner on Linux and macOS because it guarantees mpv is launched with the correct IPC socket and SubMiner defaults. It's a Bun script distributed as a release asset alongside the AppImage and DMG.
::: tip Windows users
On Windows the `subminer` script cannot run directly via shebang — use `bun run subminer` instead (e.g. `bun run subminer video.mkv`). The recommended alternative is the **SubMiner mpv** shortcut created during first-run setup, or `SubMiner.exe --launch-mpv`. See [Windows mpv Shortcut](/usage#windows-mpv-shortcut) for details.
:::
## Video Picker ## Video Picker

View File

@@ -4,6 +4,26 @@
> SubMiner requires the bundled Yomitan instance to have at least one dictionary imported for lookups to work. > SubMiner requires the bundled Yomitan instance to have at least one dictionary imported for lookups to work.
> See [Yomitan setup](#yomitan-setup) for details. > See [Yomitan setup](#yomitan-setup) for details.
::: tip Just finished first-run setup?
If you want Anki card enrichment (sentence, audio, screenshot), the only config you need is `ankiConnect` with your deck name and field names. Here is a minimal working example:
```jsonc
{
"ankiConnect": {
"enabled": true,
"deck": "Mining",
"fields": {
"sentence": "Sentence",
"audio": "SentenceAudio",
"image": "Picture",
},
},
}
```
Field names must match your Anki note type exactly (case-sensitive). See [Anki Integration](/anki-integration) for the full reference.
:::
## How It Works ## How It Works
1. SubMiner starts the overlay app in the background 1. SubMiner starts the overlay app in the background
@@ -14,14 +34,18 @@
6. Hover a word, then trigger Yomitan lookup with your configured lookup key/modifier to open the Yomitan popup 6. Hover a word, then trigger Yomitan lookup with your configured lookup key/modifier to open the Yomitan popup
7. Optional [subtitle annotations](/subtitle-annotations) (N+1, character-name, frequency, JLPT) highlight useful cues in real time 7. Optional [subtitle annotations](/subtitle-annotations) (N+1, character-name, frequency, JLPT) highlight useful cues in real time
There are two ways to use SubMiner: There are several ways to use SubMiner:
| Approach | Use when | How | > [!TIP]
| -------- | -------- | --- | > **New users: start with the `subminer` wrapper script** (or the **SubMiner mpv** shortcut on Windows). It handles mpv launch, IPC socket setup, and overlay lifecycle automatically so you don't need to configure anything in `mpv.conf`. You can add the mpv plugin later for in-player keybindings.
| **`subminer` script** | You want SubMiner to handle everything — launch mpv, set up the socket, start the overlay. The simplest path. | `subminer video.mkv` |
| **MPV plugin** | You launch mpv yourself or from another tool (file manager, Jellyfin, etc.). Requires `--input-ipc-server=/tmp/subminer-socket` in your mpv config. | Use `y` chord keybindings inside mpv |
You can use both — the plugin provides in-player controls, while the `subminer` script is convenient for direct playback. The `subminer` script runs directly via shebang (no `bun run` needed). | Approach | Use when | How |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| **`subminer` script** | You want SubMiner to handle everything — launch mpv, set up the socket, start the overlay. **The simplest path and recommended starting point.** | `subminer video.mkv` |
| **SubMiner mpv shortcut** (Windows) | The recommended Windows entry point. Created during first-run setup, launches mpv with SubMiner's defaults directly. | Double-click, drag a file onto it, or run `SubMiner.exe --launch-mpv` |
| **MPV plugin** (all platforms) | You launch mpv yourself or from another tool (file manager, Jellyfin, etc.). Requires `--input-ipc-server=/tmp/subminer-socket` in your mpv config. | Use `y` chord keybindings inside mpv |
You can use both — the plugin provides in-player controls, while the `subminer` script (or the Windows shortcut) is convenient for direct playback. The `subminer` script runs directly via shebang on Linux and macOS (no `bun run` needed); on Windows it must be invoked with `bun run subminer` since the shebang is not supported.
## Live Config Reload ## Live Config Reload
@@ -257,26 +281,26 @@ By default SubMiner uses the first connected controller. `Alt+C` opens the contr
### Default Button Mapping ### Default Button Mapping
| Button | Action | | Button | Action |
| ------ | ------ | | ----------------------- | --------------------------------------- |
| `A` (South) | Toggle lookup | | `A` (South) | Toggle lookup |
| `B` (East) | Close lookup | | `B` (East) | Close lookup |
| `Y` (North) | Toggle keyboard-only mode | | `Y` (North) | Toggle keyboard-only mode |
| `X` (West) | Mine card | | `X` (West) | Mine card |
| `L1` | Play current Yomitan audio | | `L1` | Play current Yomitan audio |
| `R1` | Next Yomitan audio track | | `R1` | Next Yomitan audio track |
| `L3` (left stick press) | Toggle mpv pause | | `L3` (left stick press) | Toggle mpv pause |
| `Select` / `Minus` | Quit mpv | | `Select` / `Minus` | Quit mpv |
| `L2` / `R2` | Unbound (available for custom bindings) | | `L2` / `R2` | Unbound (available for custom bindings) |
### Analog Controls ### Analog Controls
| Input | Action | | Input | Action |
| ----- | ------ | | --------------------- | --------------------------------------------- |
| Left stick horizontal | Move token selection left/right | | Left stick horizontal | Move token selection left/right |
| Left stick vertical | Scroll Yomitan popup | | Left stick vertical | Scroll Yomitan popup |
| Right stick vertical | Jump through Yomitan popup | | Right stick vertical | Jump through Yomitan popup |
| D-pad | Fallback for stick navigation when configured | | D-pad | Fallback for stick navigation when configured |
Learn mode ignores already-held inputs and waits for the next fresh button press or axis direction, which avoids accidental captures when you open the modal mid-input. Learn mode ignores already-held inputs and waits for the next fresh button press or axis direction, which avoids accidental captures when you open the modal mid-input.

View File

@@ -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",

View File

@@ -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',

View File

@@ -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);

View File

@@ -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;

View File

@@ -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) => {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 }),

View File

@@ -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' }),

View File

@@ -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' }),

View File

@@ -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',

View File

@@ -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),

View File

@@ -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('**/*'));

View File

@@ -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();

View File

@@ -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,

View File

@@ -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)
) { ) {

View File

@@ -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;