mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-09 03:13:32 -07:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
1280a30216
|
|||
|
a80ed72b2d
|
|||
| 4cc6c12dc7 | |||
| 425004879a | |||
| 76f99e6518 | |||
| f1e260e996 | |||
|
54e90754ef
|
|||
| 487143802a | |||
| e6a004ab8b | |||
| b510c54875 | |||
| e1ea464bc9 | |||
| b46b8dfa41 | |||
| ca067a6ccf | |||
| d719b346e0 | |||
|
a1da3dcdc8
|
|||
|
9927ef1581
|
|||
|
791c993870
|
|||
|
38dbce517c
|
|||
|
889dc9c009
|
|||
|
097021072d
|
|||
|
91c8eb8faf
|
|||
| eed0a6a243 | |||
| d33009d4a3 | |||
| 8d0535f3ca |
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [ksyasuda]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: sudacode
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -0,0 +1,105 @@
|
||||
name: Bug Report
|
||||
description: Report something that is broken or behaving incorrectly
|
||||
title: "[Bug]: "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to file a bug report! Please search [existing issues](https://github.com/ksyasuda/SubMiner/issues?q=is%3Aissue) first to avoid duplicates.
|
||||
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: A clear description of the bug, including what you expected to happen instead.
|
||||
placeholder: When I open the Yomitan popup, the overlay freezes...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Minimal, ordered steps that reliably trigger the bug.
|
||||
placeholder: |
|
||||
1. Launch `subminer`
|
||||
2. Play a video in MPV
|
||||
3. Hover a word and press ...
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Affected area
|
||||
description: Which part of SubMiner is affected?
|
||||
options:
|
||||
- Overlay / Yomitan popup
|
||||
- Anki mining
|
||||
- Subtitle annotations
|
||||
- Subtitle sidebar
|
||||
- Immersion tracking / stats
|
||||
- Launcher / CLI
|
||||
- MPV plugin
|
||||
- Jellyfin integration
|
||||
- Jimaku integration
|
||||
- AniList integration
|
||||
- YouTube integration
|
||||
- Character dictionary
|
||||
- WebSocket / texthooker API
|
||||
- Configuration
|
||||
- Documentation
|
||||
- Other / not sure
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating system
|
||||
options:
|
||||
- Linux
|
||||
- macOS
|
||||
- Windows
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: SubMiner version
|
||||
description: Output of `subminer --version`, or the release tag / commit you are running.
|
||||
placeholder: v0.15.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: compositor
|
||||
attributes:
|
||||
label: Compositor (Linux only)
|
||||
description: SubMiner's overlay supports Hyprland and sway. Name yours (and version if known). Leave blank on macOS / Windows.
|
||||
placeholder: Hyprland 0.55
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: mpv-version
|
||||
attributes:
|
||||
label: MPV version
|
||||
description: Output of `mpv --version` (first line).
|
||||
placeholder: mpv 0.38.0
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs / console output
|
||||
description: |
|
||||
Relevant logs. For verbose output, run `electron . --dev --log-level debug`.
|
||||
This will be rendered as code automatically — no backticks needed.
|
||||
render: shell
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Documentation
|
||||
url: https://docs.subminer.moe
|
||||
about: Setup, configuration, and feature docs — check here before filing an issue.
|
||||
- name: Troubleshooting guide
|
||||
url: https://docs.subminer.moe/troubleshooting
|
||||
about: Common problems and fixes (Hyprland rules, MPV detection, Anki connection, etc.).
|
||||
@@ -0,0 +1,59 @@
|
||||
name: Feature Request
|
||||
description: Suggest a new feature or an improvement to an existing one
|
||||
title: "[Feature]: "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for the idea! Please search [existing issues](https://github.com/ksyasuda/SubMiner/issues?q=is%3Aissue) first to avoid duplicates.
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem / motivation
|
||||
description: What problem are you trying to solve? What is missing or frustrating today?
|
||||
placeholder: When mining a card I have to manually switch to Anki because...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposal
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: Describe the feature or change you'd like to see.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Related area
|
||||
description: Which part of SubMiner does this relate to?
|
||||
options:
|
||||
- Overlay / Yomitan popup
|
||||
- Anki mining
|
||||
- Subtitle annotations
|
||||
- Subtitle sidebar
|
||||
- Immersion tracking / stats
|
||||
- Launcher / CLI
|
||||
- MPV plugin
|
||||
- Jellyfin integration
|
||||
- Jimaku integration
|
||||
- AniList integration
|
||||
- YouTube integration
|
||||
- Character dictionary
|
||||
- WebSocket / texthooker API
|
||||
- Configuration
|
||||
- Documentation
|
||||
- Other / new area
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives considered
|
||||
description: Any workarounds you currently use or other approaches you've thought about.
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,3 +1,36 @@
|
||||
<!--
|
||||
Thanks for contributing to SubMiner! Fill out the sections below.
|
||||
Keep it short — a couple of sentences per section is fine.
|
||||
-->
|
||||
|
||||
## Summary
|
||||
|
||||
<!-- What does this PR do and why? -->
|
||||
|
||||
## Type of change
|
||||
|
||||
<!-- Check all that apply. -->
|
||||
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature
|
||||
- [ ] Refactor / internal
|
||||
- [ ] Documentation
|
||||
- [ ] Other
|
||||
|
||||
## Related issues
|
||||
|
||||
<!-- e.g. "Closes #123". Delete if none. -->
|
||||
|
||||
## How was this tested?
|
||||
|
||||
<!--
|
||||
Describe verification. The default handoff gate is:
|
||||
bun run typecheck && bun run test:fast && bun run test:env && bun run build && bun run test:smoke:dist
|
||||
If docs-site/ changed, also: bun run docs:test && bun run docs:build
|
||||
-->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Added a changelog fragment in `changes/`, or this PR is labeled `skip-changelog`
|
||||
- [ ] Added a changelog fragment, or this PR is labeled `skip-changelog` (see [`changes/README.md`](../changes/README.md))
|
||||
- [ ] Docs updated in the same PR if behavior, defaults, flags, shortcuts, ports, or APIs changed
|
||||
- [ ] Relevant checks pass locally (typecheck, tests, build)
|
||||
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
name: appimage
|
||||
path: |
|
||||
release/*.AppImage
|
||||
release/*.yml
|
||||
release/latest*.yml
|
||||
release/*.blockmap
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -226,7 +226,7 @@ jobs:
|
||||
path: |
|
||||
release/*.dmg
|
||||
release/*.zip
|
||||
release/*.yml
|
||||
release/latest*.yml
|
||||
release/*.blockmap
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -279,7 +279,7 @@ jobs:
|
||||
path: |
|
||||
release/*.exe
|
||||
release/*.zip
|
||||
release/*.yml
|
||||
release/latest*.yml
|
||||
release/*.blockmap
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -353,7 +353,7 @@ jobs:
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/*.yml release/*.blockmap dist/launcher/subminer)
|
||||
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer)
|
||||
if [ "${#files[@]}" -eq 0 ]; then
|
||||
echo "No release artifacts found for checksum generation."
|
||||
exit 1
|
||||
@@ -389,7 +389,7 @@ jobs:
|
||||
release/*.exe
|
||||
release/*.zip
|
||||
release/*.tar.gz
|
||||
release/*.yml
|
||||
release/latest*.yml
|
||||
release/*.blockmap
|
||||
release/SHA256SUMS.txt
|
||||
dist/launcher/subminer
|
||||
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
name: appimage
|
||||
path: |
|
||||
release/*.AppImage
|
||||
release/*.yml
|
||||
release/latest*.yml
|
||||
release/*.blockmap
|
||||
|
||||
build-macos:
|
||||
@@ -216,7 +216,7 @@ jobs:
|
||||
path: |
|
||||
release/*.dmg
|
||||
release/*.zip
|
||||
release/*.yml
|
||||
release/latest*.yml
|
||||
release/*.blockmap
|
||||
|
||||
build-windows:
|
||||
@@ -268,7 +268,7 @@ jobs:
|
||||
path: |
|
||||
release/*.exe
|
||||
release/*.zip
|
||||
release/*.yml
|
||||
release/latest*.yml
|
||||
release/*.blockmap
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -342,7 +342,7 @@ jobs:
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/*.yml release/*.blockmap dist/launcher/subminer)
|
||||
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer)
|
||||
if [ "${#files[@]}" -eq 0 ]; then
|
||||
echo "No release artifacts found for checksum generation."
|
||||
exit 1
|
||||
@@ -396,7 +396,7 @@ jobs:
|
||||
release/*.exe
|
||||
release/*.zip
|
||||
release/*.tar.gz
|
||||
release/*.yml
|
||||
release/latest*.yml
|
||||
release/*.blockmap
|
||||
release/SHA256SUMS.txt
|
||||
dist/launcher/subminer
|
||||
|
||||
@@ -13,6 +13,8 @@ Start here, then leave this file.
|
||||
|
||||
`docs-site/` is user-facing. Do not treat it as the canonical internal source of truth.
|
||||
|
||||
`CLAUDE.md` is a symlink to this file — there is one project instruction file, not two.
|
||||
|
||||
## Quick Start
|
||||
|
||||
- Init workspace: `git submodule update --init --recursive`
|
||||
@@ -42,6 +44,20 @@ Start here, then leave this file.
|
||||
- Runtime-compat / dist-sensitive: `bun run test:runtime:compat`
|
||||
- Docs-only: `bun run docs:test`, then `bun run docs:build`
|
||||
|
||||
## Docs Upkeep
|
||||
|
||||
- Docs ship with the change, not after. If a change alters behavior, defaults, flags, shortcuts, ports, or APIs, update the matching docs in the same PR. Touching code without reconciling its docs is an incomplete change.
|
||||
- Source of truth for config defaults is the generated `config.example.jsonc`. Never write a default value into prose you didn't read from it — and don't restate the same default across multiple docs; cite/link to one place so there's a single thing to update.
|
||||
- Trigger map (touch left → update right):
|
||||
- `src/config/definitions/**` (schema/defaults/template) → `bun run generate:config-example`, then reconcile `docs-site/configuration.md` + any feature doc that cites that default
|
||||
- shortcuts/keybindings (`shortcuts.*`, `keybindings`, `stats.*Key`, `subtitleSidebar.toggleKey`, controller bindings) → `docs-site/shortcuts.md`
|
||||
- CLI flags/subcommands (`src/cli/args.ts`, `launcher/**`) → `docs-site/usage.md` + relevant integration doc
|
||||
- feature behavior (anki / jellyfin / jimaku / anilist / youtube / immersion / stats / websocket / sidebar / character-dictionary / annotations) → matching `docs-site/<feature>.md`
|
||||
- architecture / IPC / workflow / internal process → internal `docs/` (system of record)
|
||||
- feature set / requirements / install flow → `README.md`
|
||||
- Removing or renaming a config key: grep `docs-site/` and `docs/` for the old key and any value it documented; legacy/hidden keys (`LEGACY_HIDDEN_CONFIG_PATHS`) should not appear in user docs as current settings.
|
||||
- Verify after doc edits: `bun run verify:config-example` (if config touched), `bun run docs:test`, `bun run docs:build`.
|
||||
|
||||
## Sensitive Files
|
||||
|
||||
- Launcher source of truth: `launcher/*.ts`
|
||||
@@ -52,7 +68,8 @@ Start here, then leave this file.
|
||||
|
||||
## Release / PR Notes
|
||||
|
||||
- User-visible PRs need one fragment in `changes/*.md`
|
||||
- User-visible PRs need one fragment in `changes/*.md` — format and rules in [`changes/README.md`](./changes/README.md) (`type` + `area` keys required; apply the `skip-changelog` label to opt out)
|
||||
- User-visible docs changes get a `type: docs` fragment
|
||||
- CI enforces `bun run changelog:lint` and `bun run changelog:pr-check`
|
||||
- PR review helpers:
|
||||
- `gh pr view --json number,title,url --jq '"PR #\\(.number): \\(.title)\\n\\(.url)"'`
|
||||
@@ -63,4 +80,4 @@ Start here, then leave this file.
|
||||
- Use Codex background for long jobs; tmux only when persistence/interaction is required
|
||||
- CI red: `gh run list/view`, rerun, fix, repeat until green
|
||||
- TypeScript: keep files small; follow existing patterns
|
||||
- Swift: use workspace helper/daemon; validate `swift build` + tests
|
||||
- Only Swift is the `scripts/get-mpv-window-macos.swift` helper (macOS mpv window detection); validate via `bun test scripts/get-mpv-window-macos.test.ts`
|
||||
|
||||
+183
-4
@@ -1,10 +1,189 @@
|
||||
# Changelog
|
||||
|
||||
## v0.15.2 (2026-06-02)
|
||||
|
||||
### Changed
|
||||
- Yomitan: Updated the bundled Yomitan build to the latest vendored revision.
|
||||
|
||||
### Fixed
|
||||
- Anki - Animated AVIF: Clip timing no longer starts or ends early; word-audio lead-in and clip duration are now aligned to frame boundaries.
|
||||
- Overlay (Hyprland): Fixed fullscreen overlay alignment - modal, stats, and sidebar content no longer shift below the mpv window.
|
||||
- Overlay (macOS): Subtitle bars are now interactive immediately after autoplay starts with "wait for overlay to be ready" enabled, without requiring a manual click.
|
||||
- Overlay (macOS): Fixed overlay, subtitles, and subtitle sidebar staying hidden after a modal closes until the user clicked the mpv window; focus is now restored to mpv when the last modal closes, so playback shortcuts and the overlay reappear correctly - including in native fullscreen.
|
||||
|
||||
## v0.15.1 (2026-05-31)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Linux Overlay Stacking**: Fixed the overlay intermittently dropping behind mpv on KDE Plasma and other non-Hyprland/Sway Wayland sessions; restored subtitle hover, pause-on-hover, and Yomitan lookups on X11/XWayland; the overlay now correctly layers above/below mpv based on fullscreen state, yields to foreground windows (Settings, Yomitan, AniList, etc.), and avoids startup flashes and fullscreen transition glitches.
|
||||
- **Linux Overlay (Hyprland Lua)**: Fixed overlay placement on Hyprland 0.55+ when using a Lua-based config.
|
||||
- **Manual Overlay Startup**: Fixed manual visible-overlay startup from mpv - now correctly attaches to playback, keeps the window bounds synced with mpv, and primes current subtitles before showing.
|
||||
- **Playlist Transitions**: Reused the warm overlay when mpv advances to the next playlist item, avoiding a redundant tokenization pause and preserving visible subtitles across tracks.
|
||||
- **macOS Overlay**: Fixed the visible subtitle overlay staying click-through after pause-until-ready releases playback; restored mpv focus after closing modal windows so subtitles and keybinds resume without clicking the player.
|
||||
- **Mouse Keybindings**: Fixed keybinding capture and runtime handling for mouse buttons, including side buttons like `MBTN_BACK` and `MBTN_FORWARD`.
|
||||
- **Windows mpv Shortcut**: Fixed the Windows `SubMiner mpv` shortcut so videos attach to an already-running background app instead of spawning a second process.
|
||||
|
||||
### Docs
|
||||
|
||||
- **Troubleshooting**: Updated Hyprland overlay docs with current Lua (`hl.window_rule`) and legacy config syntax; added troubleshooting for KDE/Wayland and other non-Hyprland/Sway Wayland sessions; added a Character Dictionary troubleshooting section; added a "See Also" index linking each feature's troubleshooting page.
|
||||
|
||||
## v0.15.0 (2026-05-29)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- **Subsync:**
|
||||
- The `subsync.defaultMode` config option has been removed
|
||||
- Subsync now always opens the manual subtitle picker regardless of any previously set default mode
|
||||
- **N+1 Highlighting:**
|
||||
- N+1 highlighting now has its own dedicated `ankiConnect.nPlusOne.enabled` option, separate from known-word highlighting
|
||||
- It is no longer enabled automatically when known-word highlighting is on - enable it explicitly to keep N+1 annotations
|
||||
|
||||
### Added
|
||||
|
||||
- **Auto-Updater:**
|
||||
- Tray and `subminer -u` update checks with app update prompts
|
||||
- Launcher and Linux rofi theme auto-updates
|
||||
- Checksum verification and configurable notifications
|
||||
- Opt-in prerelease channel via `updates.channel: "prerelease"`
|
||||
- **Settings Window:**
|
||||
- New dedicated Settings window via `subminer --settings` or `subminer settings`, organized into Appearance, Behavior, Anki, Input, and Integration sections
|
||||
- Click-to-learn keybinding controls
|
||||
- AnkiConnect-backed deck, field, and note-type pickers that auto-fill from the configured Anki deck
|
||||
- Cross-category search
|
||||
- Live save for most options including subtitle CSS, stats keys, logging level, Jimaku, Subsync, and Anki mappings
|
||||
- AI and translation settings remain config-file only
|
||||
- **Inline Character Portraits:**
|
||||
- Optional AniList character portraits appear inline for name-matched subtitle text
|
||||
- Manual AniList overrides scoped per parent media directory so separate season folders maintain separate character dictionary selections
|
||||
- **Character Dictionary Manager:** New `Ctrl/Cmd+D` manager modal to remove, reorder, or override loaded entries.
|
||||
- **Log Export:** Sanitized log ZIP export from the tray menu and via `subminer logs -e`, with home-directory usernames redacted from exported contents.
|
||||
- **Launcher CLI:**
|
||||
- `subminer --version` / `subminer -v` prints the installed app version
|
||||
- `mpv.profile` config and Settings support passes a named mpv profile to managed launches
|
||||
- Bundled mpv plugin startup options are now configurable from SubMiner config
|
||||
- **First-Run Setup:**
|
||||
- Optional installer for Bun and the `subminer` CLI on Linux, macOS, and Windows
|
||||
- Windows `subminer.cmd` PATH shim so `subminer` works without manually adding `SubMiner.exe` to PATH
|
||||
- Setup recognizes existing Homebrew or user PATH installs and avoids writing into Homebrew-owned paths
|
||||
- Includes an Open SubMiner Settings button
|
||||
- Standalone setup app quits after completing, returning terminal control
|
||||
- **Primary Subtitle Visibility on Yomitan Popup:** New `subtitleStyle.primaryVisibleOnYomitanPopup` option keeps hover-mode primary subtitles visible while a Yomitan popup is open.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Subtitle Appearance Config:**
|
||||
- Primary and secondary subtitle appearance now use color controls plus CSS declaration editors, saved as `subtitleStyle.css`, `subtitleStyle.secondary.css`, and `subtitleSidebar.css`
|
||||
- Known-word and N+1 annotation colors moved to `subtitleStyle.knownWordColor` and `subtitleStyle.nPlusOneColor`
|
||||
- Subtitle font defaults updated to `Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP`
|
||||
- Existing configs migrate automatically; legacy Anki color keys still accepted with deprecation warnings
|
||||
- **Subtitle Style Defaults:**
|
||||
- Stronger outline-style text shadow
|
||||
- Thicker JLPT underlines
|
||||
- Frequency `topX` default raised to `10000`
|
||||
- **Character Dictionary:**
|
||||
- Entries scoped to the current AniList media for name matching and inline portraits
|
||||
- Generates Japanese-only name aliases so raw romanized/English aliases no longer surface as separate results
|
||||
- In-app AniList selector waits for an explicit search with the box prefilled from the current filename
|
||||
- `subtitleStyle.nameMatchEnabled` is now the sole switch for dictionary sync and builds
|
||||
- **Electron Runtime:** Updated from 39.8.6 to 42.2.0, returning SubMiner to a supported Electron release line.
|
||||
- **Jellyfin Setup:**
|
||||
- Removed the server presets dropdown
|
||||
- Setup now shows a single editable server URL field
|
||||
- **Jellyfin Cast Identity:**
|
||||
- Device identity now derived from the OS hostname and always reported as SubMiner
|
||||
- Previously configurable identity fields are ignored, preventing multiple installs from sharing a remote-session identity
|
||||
- **Startup Defaults:** Jellyfin remote-session startup warmup and character-name subtitle highlighting now default to off.
|
||||
- **Setup Appearance:** Removed the bundled mpv runtime plugin readiness card from the setup flow.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **AniList Progress:**
|
||||
- Progress updates fire correctly when playback reaches or skips past the watched threshold, using fresh mpv timing events
|
||||
- Season-specific results preferred for multi-season files, with a clear message when the matched season is not in Planning or Watching
|
||||
- Repeated missing-token checks no longer exhaust retry attempts or duplicate dead-letter entries
|
||||
- **Anki Mining:**
|
||||
- Sentence-audio padding is opt-in by default
|
||||
- Animated AVIF freeze-frame duration aligned to word audio length without double-counting
|
||||
- Multi-line sentence alignment fixed for repeated subtitle text
|
||||
- Kiku duplicate-card detection, auto-merge, modal acknowledgment race, and field/tag ordering corrected
|
||||
- YouTube playback cards use mpv's resolved stream URLs
|
||||
- Sentence cards refresh the secondary subtitle before saving
|
||||
- **Jellyfin Discovery:**
|
||||
- Startup, subtitle track selection, and duplicate ready-signal handling all fixed
|
||||
- Paused mpv no longer misreported as playing
|
||||
- Resume corrected when a remote play command sends `StartPositionTicks: 0` despite saved progress
|
||||
- **Jellyfin Remote:**
|
||||
- Tray checkbox stays in sync on Linux after tray, CLI, or startup changes
|
||||
- Remote controller visibility and progress sync fixed for seeks, stops, startup path changes, and Linux websocket reconnect windows
|
||||
- Play and Resume now behave correctly (Play from beginning, Resume from saved position)
|
||||
- Final progress reports reuse SubMiner's last known position when mpv resets on stop
|
||||
- Windows setup login flow fixed with an IPC bridge, immediate feedback, and a timeout with inline error for unreachable servers
|
||||
- **Overlay (macOS):**
|
||||
- Overlay hides when mpv loses focus, is minimized, or is no longer the foreground app
|
||||
- Stays stable through transient window geometry disappearances from macOS APIs and when clicking from the overlay back into mpv
|
||||
- Stats overlay opened inactive so it appears over fullscreen mpv without switching Spaces
|
||||
- Passthrough fixed so mpv controls stay clickable before hovering a subtitle bar
|
||||
- **Yomitan Sidebar:**
|
||||
- Playback stays paused for sidebar-opened Yomitan popups when auto-pause is enabled
|
||||
- Popups now open when startup races the Yomitan extension load
|
||||
- Sidebar mining cards use audio and images from the clicked sidebar line instead of the current primary subtitle
|
||||
- **Launcher:**
|
||||
- `subminer app` on Linux returns terminal control immediately
|
||||
- `subminer app --setup` opens the setup flow when SubMiner is already running in the background
|
||||
- **YouTube Playback:**
|
||||
- Selected subtitles downloaded to local temp files so the primary bar and sidebar read the same source, with cleanup on reload and quit
|
||||
- False load-failure notifications suppressed
|
||||
- Tray icon created on launcher-managed playback that attaches to an already-running process
|
||||
- **Shortcuts:**
|
||||
- Native mpv menu shortcuts disabled during managed macOS playback so configured SubMiner shortcuts work while mpv has focus
|
||||
- Custom session shortcuts including `stats.markWatchedKey` wired through mpv
|
||||
- Multi-line copy/mine overlay correctly focused so number keys choose the line count on macOS and Windows
|
||||
- **Controller Bindings:**
|
||||
- Controller config and debug shortcuts stay closed while controller support is disabled
|
||||
- Binding learn mode starts from the edit pencil
|
||||
- Remaps saved per controller profile
|
||||
- Binding badges also start learn mode
|
||||
- Row reset buttons restore individual bindings to defaults
|
||||
- **Logging:**
|
||||
- `logging.level` forwarded to launcher-started and Windows shortcut-started mpv sessions, covering mpv log verbosity, plugin logging, and plugin-launched app logging
|
||||
- `logging.rotation` (default 7 days) and per-component `logging.files` toggles added, with mpv logs disabled by default
|
||||
- Repeated IPC socket warning spam suppressed while waiting for mpv to recreate the socket
|
||||
- Windows mpv IPC, subtitle track, and Yomitan diagnostics added
|
||||
- **In-Player Stats:**
|
||||
- Layering fixed so delete confirmations, overlay modals, and update-check dialogs appear above the stats window
|
||||
- Jellyfin playback stats grouped by item metadata so watched episodes merge with matching local library titles and keep clean display names
|
||||
- **WebSocket Annotations:**
|
||||
- Annotation spans and token metadata stay on the annotation WebSocket
|
||||
- The regular subtitle WebSocket is plain-text only
|
||||
- **Subtitle Annotation Prefetching:** Cached colored annotations and character images ready sooner for live subtitle changes without delaying raw subtitle display.
|
||||
- **Windows Startup Errors:** Fatal startup failures now show a native error dialog and write details to the app log instead of exiting silently.
|
||||
|
||||
### Docs
|
||||
|
||||
- **Documentation Site:**
|
||||
- Published stable docs at the site root with current development docs under `/main/`
|
||||
- Fixed versioned docs navigation, archived page link handling, and local dev version routing
|
||||
- Documented all previously undocumented config options including `subtitleStyle.primaryDefaultMode`, `stats.markWatchedKey`, `immersionTracking.lifetimeSummaries.*`, and all seven `mpv.*` launcher options
|
||||
- Added Playback Startup Flow and Runtime Sockets diagrams to the architecture docs with cross-reference pointers in the MPV Plugin and Troubleshooting pages
|
||||
|
||||
<details>
|
||||
<summary>Internal changes</summary>
|
||||
|
||||
### Internal
|
||||
|
||||
- **Release Tooling:**
|
||||
- Release-note polishing treats pending fragments and reviewed prerelease notes as a cumulative final outcome, collapsing prerelease-only fixes into the final user-facing change
|
||||
- Prerelease generation reuses existing reviewed notes and merges only new fragment material
|
||||
- `make clean` preserves `release/prerelease-notes.md`
|
||||
- **Tests:** Removed stale Yomitan vendor source-inspection assertions for changes that were not shipped.
|
||||
|
||||
</details>
|
||||
|
||||
## v0.14.0 (2026-05-12)
|
||||
|
||||
### Added
|
||||
|
||||
- **Character Dictionary:** Added AniList-based character dictionary selection for resolving title mismatches — open it in-app with the new `Ctrl+Alt+A` shortcut or from the CLI with `subminer dictionary --candidates` / `--select`. Series-scoped overrides replace stale entries in the merged dictionary.
|
||||
- **Character Dictionary:** Added AniList-based character dictionary selection for resolving title mismatches - open it in-app with the new `Ctrl+Alt+A` shortcut or from the CLI with `subminer dictionary --candidates` / `--select`. Series-scoped overrides replace stale entries in the merged dictionary.
|
||||
- **Primary Subtitle Bar:** Added a `V` shortcut and mpv plugin binding to toggle the primary subtitle bar without affecting mpv's native subtitle visibility.
|
||||
- **Texthooker:** Added `subminer texthooker -o` and a tray menu item to open the local texthooker page in the default browser.
|
||||
|
||||
@@ -58,7 +237,7 @@
|
||||
### Internal
|
||||
|
||||
- Replaced the changelog renderer with an AI polish pass that merges related fragments and writes user-facing release notes. `CHANGELOG.md` keeps internal items in a collapsed `<details>` block; GitHub release notes omit them entirely.
|
||||
- Release CI no longer auto-builds pending `changes/*.md` fragments on tag. Tagging now fails fast if fragments remain — run `bun run changelog:build` (requires the `claude` CLI) and commit before tagging.
|
||||
- Release CI no longer auto-builds pending `changes/*.md` fragments on tag. Tagging now fails fast if fragments remain - run `bun run changelog:build` (requires the `claude` CLI) and commit before tagging.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -77,8 +256,8 @@
|
||||
- Stats: Episode detail hides card events whose Anki notes have been deleted, instead of showing phantom mining activity.
|
||||
- Stats: Trend and watch-time charts share a unified theme with horizontal gridlines and larger ticks for legibility.
|
||||
- Stats: Overview, Library, Trends, Sessions, and Vocabulary now use generic "title" wording so YouTube videos and anime live comfortably side by side in the dashboard.
|
||||
- Stats: Session timeline no longer plots seek-forward/seek-backward markers — they were too noisy on sessions with lots of rewinds.
|
||||
- Stats: Replaced the "Library — Per Day" section on the Stats → Trends page with a "Library — Summary" section. The new section shows a top-10 watch-time leaderboard chart and a sortable per-title table (watch time, videos, sessions, cards, words, lookups, lookups/100w, date range), all scoped to the current date range selector.
|
||||
- Stats: Session timeline no longer plots seek-forward/seek-backward markers - they were too noisy on sessions with lots of rewinds.
|
||||
- Stats: Replaced the "Library - Per Day" section on the Stats → Trends page with a "Library - Summary" section. The new section shows a top-10 watch-time leaderboard chart and a sortable per-title table (watch time, videos, sessions, cards, words, lookups, lookups/100w, date range), all scoped to the current date range selector.
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ How fragments turn into a release:
|
||||
|
||||
- At release time, `bun run changelog:build` (and `bun run changelog:prerelease-notes`) pipes every pending fragment through `claude -p` to merge related items, drop noise, and rewrite into a clean user-facing release body. Write fragments as raw, informative notes — don't worry about polished prose, deduping across PRs, or line-by-line phrasing. The polish step handles all of that.
|
||||
- The polish step treats pending fragments as the final release outcome, not prerelease history. If a feature is added and then renamed or fixed before the stable cut, ship the final feature bullet instead of separate prerelease-only breaking/fix entries.
|
||||
- GitHub release notes and prerelease notes use short top-level items with nested bullets for the change, user benefit, and any useful action note. The stable `CHANGELOG.md` can stay in compact single-line bullets.
|
||||
- `internal` fragments stay in `CHANGELOG.md` (inside a collapsed `<details>` block) but are dropped from the GitHub release notes entirely.
|
||||
- The polished `CHANGELOG.md` and `release/release-notes.md` are committed and reviewed before tagging — edit the Markdown by hand if Claude misses something.
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
type: fixed
|
||||
area: anilist
|
||||
|
||||
- Used fresh mpv time-position, duration, and subtitle timing events for AniList post-watch threshold checks so progress updates still fire when playback reaches or skips past the watched threshold.
|
||||
- Prefer season-specific AniList search results for multi-season files before falling back to the base title, and show a clear message when the matched season is not in Planning or Watching instead of silently queueing an impossible update.
|
||||
- Prevent repeated missing-token checks from rapidly exhausting AniList retry attempts or duplicating dead-letter entries for the same episode.
|
||||
@@ -1,8 +0,0 @@
|
||||
type: fixed
|
||||
area: anki
|
||||
|
||||
- Made sentence-audio padding opt-in by default, and kept animated AVIF freeze-frame duration aligned to the word audio length without double-counting sentence audio padding.
|
||||
- Kept multi-line sentence mining aligned when repeated subtitle text appears in the selected history range.
|
||||
- Fixed Kiku duplicate-card detection so local duplicate sentence cards trigger the manual modal or auto merge, modal-open acknowledgement races no longer cancel the flow, and merged fields follow Kiku's group ordering, sentence-audio, furigana, and tag semantics.
|
||||
- Fixed manual clipboard card updates from YouTube playback so generated audio and images use mpv's resolved stream URLs instead of the YouTube page URL.
|
||||
- Sentence cards now refresh the current secondary subtitle before saving, so the translation field uses the loaded subtitle instead of repeating the primary text.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: added
|
||||
area: updater
|
||||
|
||||
- Added tray and `subminer -u` update checks for SubMiner releases, including app update prompts, launcher and Linux rofi theme updates, checksum verification, configurable update notifications, and an opt-in prerelease channel. Set `updates.channel` to `"prerelease"` to receive beta/RC builds.
|
||||
@@ -1,8 +0,0 @@
|
||||
type: fixed
|
||||
area: character-dictionary
|
||||
|
||||
- Reused cached character-dictionary media matches so loading a title with an existing snapshot no longer sends another AniList search request.
|
||||
- Block the character dictionary manager when annotations are disabled, with a notice through the configured OSD/system notification surfaces.
|
||||
- Added surname honorific matches for Japanese localized character aliases embedded in AniList alternative names (e.g. Korean-source characters with Japanese names in parentheses), and refresh cached snapshots so those aliases are regenerated.
|
||||
- Use `subtitleStyle.nameMatchEnabled` as the only switch for character-dictionary sync/builds, hiding the legacy `anilist.characterDictionary.enabled` option.
|
||||
- Forward character dictionary manager session-action keybindings to the mpv plugin.
|
||||
@@ -1,6 +0,0 @@
|
||||
type: changed
|
||||
area: character-dictionary
|
||||
|
||||
- Character dictionary entries are now scoped to the current AniList media for name matching and inline portraits, and generate Japanese name aliases only so raw romanized/English aliases no longer surface as separate results.
|
||||
- Added a `Ctrl/Cmd+D` manager modal to remove, reorder, or override loaded dictionary entries.
|
||||
- The in-app AniList selector now waits for an explicit title search, with the search box prefilled from the current filename guess so you can edit it before choosing an override.
|
||||
@@ -1,7 +0,0 @@
|
||||
type: added
|
||||
area: subtitles
|
||||
|
||||
- Added optional inline AniList portraits for character-name subtitle matches, including automatic refresh of cached character dictionary snapshots that do not contain portrait data.
|
||||
- Scoped manual AniList overrides by parent media directory, so separate season folders can keep separate character dictionary selections.
|
||||
- Fixed large character dictionary imports by serving the merged ZIP through a local URL when supported, with a base64 fallback for older bundled Yomitan builds.
|
||||
- Allowed subtitle overlay data image sources so inline character portraits render instead of showing a broken image icon.
|
||||
@@ -1,6 +0,0 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- Controller config and debug shortcuts now stay closed while controller support is disabled and show a notice to enable `controller.enabled` manually.
|
||||
- Controller binding rows now start learn mode from the edit pencil, so clicking edit and pressing a controller button saves the remap.
|
||||
- Controller remaps are now saved per controller profile, binding badges also start learn mode, and row reset buttons restore individual bindings to their defaults.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: changed
|
||||
area: config
|
||||
|
||||
- Defaulted Jellyfin remote-session startup warmup and character-name subtitle highlighting to off.
|
||||
@@ -1,6 +0,0 @@
|
||||
type: docs
|
||||
area: docs
|
||||
|
||||
- Published stable docs at the site root with current development docs under `/main/`, and fixed versioned docs navigation so archived pages keep local links under the selected version, the version switcher no longer nests paths incorrectly, local dev version routes serve warmed archive files, and internal README files no longer break archived builds.
|
||||
- Documented all previously undocumented config options, including `subtitleStyle.primaryDefaultMode`, `stats.markWatchedKey`, `immersionTracking.lifetimeSummaries.*`, and all seven `mpv.*` launcher options.
|
||||
- Added a Playback Startup Flow diagram and a Runtime Sockets section/diagram to the architecture docs, with cross-reference pointers in the MPV Plugin and Troubleshooting pages.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: changed
|
||||
area: runtime
|
||||
|
||||
- Updated the bundled Electron runtime from 39.8.6 to 42.2.0, moving SubMiner back onto a supported Electron release line.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: fixed
|
||||
area: integrations
|
||||
|
||||
- Prevented Discord Rich Presence from falling back to Jellyfin stream URLs, and primed Jellyfin playback titles before loading tokenized streams so presence shows the show/episode title
|
||||
@@ -1,4 +0,0 @@
|
||||
type: changed
|
||||
area: setup
|
||||
|
||||
- Setup: Removed the bundled mpv runtime plugin readiness card; legacy mpv plugin removal still appears when needed.
|
||||
@@ -1,6 +0,0 @@
|
||||
type: fixed
|
||||
area: jellyfin
|
||||
|
||||
- Fixed Jellyfin discovery playback: the active item is no longer reloaded on startup, paused mpv is no longer misreported as playing, startup unpause no longer repeats after a manual pause or `y-t` toggle, duplicate ready signals no longer re-show the overlay, delayed Japanese subtitle selection is handled correctly, later-loading foreign tracks no longer steal the active Japanese track, and long-lived sidebar ffmpeg extractors no longer run against stream URLs.
|
||||
- Fixed discovery resume when a remote play command sends `StartPositionTicks: 0` despite saved progress on the item.
|
||||
- Kept Jellyfin picker library discovery working when the app log level is above info.
|
||||
@@ -1,8 +0,0 @@
|
||||
type: fixed
|
||||
area: jellyfin
|
||||
|
||||
- Keep the discovery tray checkbox in sync on Linux after tray, CLI, or startup remote-session changes, and restart stale discovery sessions when the server no longer lists the SubMiner cast target.
|
||||
- Fixed remote controller visibility and progress sync for mpv/SubMiner seek jumps, stopped sessions, startup path changes, and Linux websocket reconnect windows.
|
||||
- Kept Play and Resume distinct: Play starts from the beginning while Resume starts at the saved position, and final progress reports reuse SubMiner's last known position when mpv resets during stop.
|
||||
- Derived cast device identity from the OS hostname and always report the client as SubMiner, ignoring legacy configurable identity fields so multiple installs no longer share a remote-session identity.
|
||||
- Fixed the Windows setup login flow with an IPC bridge, immediate progress feedback, and a timeout with an inline error for unreachable servers.
|
||||
@@ -1,6 +0,0 @@
|
||||
type: fixed
|
||||
area: jellyfin
|
||||
|
||||
- Show the visible subtitle overlay automatically during Jellyfin playback so `subtitleStyle` appearance applies, and inject the bundled mpv plugin when SubMiner auto-launches mpv so mpv-side keybindings work without overlay focus.
|
||||
- Made the `y-t` overlay toggle reliable and sticky across stream redirects that change mpv's path, re-arming managed subtitle defaults on redirect so Japanese primary subtitles load, collapsing duplicate toggle events on Hyprland, and keeping passive Linux/Hyprland overlay shows from stealing keyboard focus from mpv.
|
||||
- Improved subtitle timing by preferring default embedded streams over external sidecars, stripping Jellyfin's server-selected stream from playback URLs, suppressing mpv auto-selection while SubMiner stages managed tracks, correcting clear Japanese-vs-English cue offsets, and restoring per-stream subtitle delay shifts. Track selection tolerates transient `track-list` read failures and numeric string track IDs on Linux.
|
||||
@@ -1,8 +0,0 @@
|
||||
type: fixed
|
||||
area: launcher
|
||||
|
||||
- Launcher-opened videos reuse an already-running background SubMiner instance, reapply preferred subtitles on warm launches, and close launcher-owned tray apps after playback ends.
|
||||
- Videos stay paused when attaching to a running background app until subtitle priming and tokenization readiness complete, with mpv plugin subtitle auto-selection moved to pre-load so launch-time choices are not reset.
|
||||
- `subminer settings` on macOS no longer emits Electron menu diagnostics and exits cleanly when the window is closed.
|
||||
- `subminer app` on Linux returns control to the terminal immediately, and Linux first-run launcher installs build with a valid Bun shebang.
|
||||
- `subminer app --setup` opens the setup flow when SubMiner is already running in the background.
|
||||
@@ -1,6 +0,0 @@
|
||||
type: added
|
||||
area: launcher
|
||||
|
||||
- Added `subminer --version` / `subminer -v` to print the installed app version.
|
||||
- Added `mpv.profile` config and Settings support for passing an mpv profile to SubMiner-managed mpv launches.
|
||||
- Made bundled mpv plugin startup options configurable from SubMiner config.
|
||||
@@ -1,5 +0,0 @@
|
||||
type: changed
|
||||
area: updater
|
||||
|
||||
- Linux tray "Check for Updates" now installs the new AppImage automatically via `electron-updater`, matching the macOS and Windows tray flow, instead of stopping at a "manual update required" dialog. AppImages managed by a system package (AUR `/opt/SubMiner/SubMiner.AppImage`) and non-AppImage launches (no `APPIMAGE` env) still fall back to the GitHub-asset flow.
|
||||
- Routed `electron-updater` HTTP through `/usr/bin/curl` on Linux and disabled differential downloads, matching the macOS path, so background update checks stay off Electron's network service.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: added
|
||||
area: logs
|
||||
|
||||
- Add sanitized log ZIP exports from the tray menu and `subminer logs -e`, with home-directory usernames redacted from exported log contents.
|
||||
@@ -1,7 +0,0 @@
|
||||
type: fixed
|
||||
area: logging
|
||||
|
||||
- Forward SubMiner `logging.level` into launcher-started and Windows shortcut-started mpv sessions, covering mpv log verbosity, plugin script logging, and plugin-launched app logging.
|
||||
- Added numeric `logging.rotation` (default 7 days of retained daily app, launcher, and mpv logs) and `logging.files` toggles per component, with mpv logs disabled by default unless explicitly enabled for debugging.
|
||||
- Added Windows mpv launch, IPC socket, subtitle track, and Yomitan/dictionary diagnostics, including expected/active IPC socket values when plugin auto-start skips on a socket mismatch.
|
||||
- Stop repeated mpv IPC socket warning spam while the app waits in the background for mpv to recreate the socket.
|
||||
@@ -1,5 +0,0 @@
|
||||
type: changed
|
||||
area: config
|
||||
|
||||
- `ankiConnect.nPlusOne.enabled` is no longer implicitly set to `true` when known-word highlighting is enabled; existing configs that already had N+1 highlighting keep it, but new configs leave it disabled unless `ankiConnect.nPlusOne.enabled` is set explicitly.
|
||||
- Updated known-word cache docs and examples to recommend expression/word fields and removed legacy-option references from user-facing config docs.
|
||||
@@ -1,8 +0,0 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- Primed the first startup subtitle before autoplay resumes so the overlay renders text before video playback begins.
|
||||
- Kept the visible overlay and subtitle stream alive after restarting SubMiner from the mpv `y-r` shortcut, with correct Linux bounds reapplication and user-paused playback preserved through readiness gates.
|
||||
- Fixed managed mpv startup so launcher-owned videos quit SubMiner when playback ends while background/tray sessions stay alive, with pause-until-ready waiting for overlay and tokenization readiness.
|
||||
- Fixed the subtitle sync modal on macOS so it no longer flashes and hides on the first attempt or leaves stale state after syncing.
|
||||
- Fixed Windows managed mpv launches from a background instance so the warm app receives the start command, retargets the new mpv socket, binds to the player window, and receives startup overlay options.
|
||||
@@ -1,7 +0,0 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- Refreshed overlay placement after leaving mpv fullscreen so the visible overlay stays aligned to the player on Hyprland.
|
||||
- Kept the visible overlay stacked above mpv after mpv regains focus from clicks or overlay movement, and suspended it while the in-player stats window is open, restoring it mouse-passive afterward.
|
||||
- Promoted SubMiner and Yomitan settings windows above the subtitle overlay on Hyprland instead of opening behind it, without hiding subtitles.
|
||||
- Hid the visible overlay as soon as the character dictionary modal opens, including while AniList lookup is loading or returns no results.
|
||||
@@ -1,7 +0,0 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- Hid the macOS visible overlay when mpv loses focus, is minimized, or is no longer the foreground target so other apps and Spaces are not covered, treating the frontmost mpv app as the focus signal.
|
||||
- Kept the overlay stable through transient window-tracking misses, when mpv remains frontmost but window geometry temporarily disappears from macOS APIs, and when clicking from the overlay back into mpv.
|
||||
- Kept the overlay correctly layered during stats mouse passthrough, opened the stats overlay inactive so it appears over fullscreen mpv without switching Spaces, and fixed passthrough so mpv controls stay clickable before hovering a subtitle bar.
|
||||
- Reduced window-tracker background work by preferring the compiled helper and slowing polls while mpv is stably focused.
|
||||
@@ -1,6 +0,0 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- Kept playback paused for Yomitan lookup popups opened from the subtitle sidebar when popup auto-pause is enabled.
|
||||
- Fixed Yomitan popups not opening when playback/overlay startup races the Yomitan extension load.
|
||||
- Fixed subtitle sidebar mining so Yomitan-enriched cards use audio and images from the clicked sidebar line instead of the current primary subtitle line.
|
||||
@@ -1,5 +0,0 @@
|
||||
type: fixed
|
||||
area: release
|
||||
|
||||
- Fixed macOS packaging so the compiled mpv window helper is built into `dist/scripts` and bundled, preventing the overlay from falling back to slow Swift source startup, and removed a stale Windows helper resource entry that produced harmless missing-file warnings.
|
||||
- Fixed one-shot `make clean build install` flows so install picks up the AppImage built earlier in the same make invocation.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: fixed
|
||||
area: websocket
|
||||
|
||||
- WebSocket: Kept the regular subtitle websocket plain-text only; annotation spans and token metadata now stay on the annotation websocket.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: added
|
||||
area: config
|
||||
|
||||
- Added `subtitleStyle.primaryVisibleOnYomitanPopup` to keep hover-mode primary subtitles visible while a Yomitan popup is open.
|
||||
@@ -1,5 +0,0 @@
|
||||
type: internal
|
||||
area: release
|
||||
|
||||
- Release-note polishing treats pending fragments and reviewed prerelease notes as a cumulative final outcome, collapsing prerelease-only fixes or breakages into the final user-facing change.
|
||||
- Prerelease note generation reuses existing reviewed notes and merges only new fragment material, and `make clean` preserves `release/prerelease-notes.md`.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: changed
|
||||
area: jellyfin
|
||||
|
||||
- Removed the Jellyfin setup server presets dropdown; setup now shows a single editable server URL field.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: internal
|
||||
area: tests
|
||||
|
||||
- Removed stale Yomitan vendor source-inspection assertions for changes that were not shipped.
|
||||
@@ -1,8 +0,0 @@
|
||||
type: added
|
||||
area: config
|
||||
|
||||
- Added a dedicated Settings window via `subminer --settings` or `subminer settings`, organized into Appearance, Behavior, Anki, Input, and Integration sections with click-to-learn keybinding controls (including the AniSkip button key) and AnkiConnect-backed deck, field, and note-type pickers.
|
||||
- Expanded live reload so Settings saves apply immediately for stats keys, logging level, Jimaku, Subsync, YouTube language defaults, Anki field mappings, sentence card model, and selected annotation/runtime options.
|
||||
- Settings search works across all categories, narrows on multi-word terms, and hides settings owned by richer editors.
|
||||
- The note-fields note type picker defaults to the configured Anki deck's note type, then exact `Kiku`, then exact `Lapis`, leaving it blank for manual selection otherwise.
|
||||
- AI and translation settings remain config-file only.
|
||||
@@ -1,7 +0,0 @@
|
||||
type: added
|
||||
area: setup
|
||||
|
||||
- Added optional first-run setup controls to install Bun and the `subminer` command-line launcher on Linux, macOS, and Windows, with a Windows `subminer.cmd` PATH shim so `subminer` works without manually adding `SubMiner.exe` to PATH.
|
||||
- Added an Open SubMiner Settings button to first-run setup and moved Finish to the right-side action slot.
|
||||
- First-run setup recognizes existing `subminer` installs in Homebrew or user PATH directories, while manual setup avoids writing into Homebrew-owned paths.
|
||||
- The standalone setup app quits after completing first-run setup, returning the terminal instead of leaving the process open.
|
||||
@@ -1,6 +0,0 @@
|
||||
type: fixed
|
||||
area: shortcuts
|
||||
|
||||
- Disabled native mpv menu shortcuts during managed macOS playback so configured SubMiner shortcuts work while mpv has focus.
|
||||
- Wired configured session shortcuts, including `stats.markWatchedKey`, through mpv so custom changes work while mpv has focus.
|
||||
- Focus the visible overlay when entering multi-line copy/mine selection so number keys choose the line count on macOS and Windows.
|
||||
@@ -1,5 +0,0 @@
|
||||
type: fixed
|
||||
area: stats
|
||||
|
||||
- Fixed in-player stats layering so delete confirmations, overlay modals, and update-check dialogs appear above the stats window.
|
||||
- Grouped Jellyfin playback stats under item metadata instead of stream URLs, so watched episodes merge with matching local library titles and keep clean display names.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: changed
|
||||
area: subtitles
|
||||
|
||||
- Subsync now always opens the manual picker and the `subsync.defaultMode` config/settings option has been removed.
|
||||
@@ -1,8 +0,0 @@
|
||||
type: changed
|
||||
area: config
|
||||
|
||||
- Primary and secondary subtitle appearance now use color controls plus CSS declaration editors, saved as `subtitleStyle.css` and `subtitleStyle.secondary.css`; sidebar appearance uses `subtitleSidebar.css`.
|
||||
- Moved known-word and N+1 annotation colors to `subtitleStyle.knownWordColor` and `subtitleStyle.nPlusOneColor`; legacy Anki color keys are still accepted with deprecation warnings.
|
||||
- Updated subtitle font defaults to `Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP`.
|
||||
- Existing configs are migrated automatically: legacy primary/secondary appearance options and hover token colors fold into `subtitleStyle.css`, and user config files are preserved during legacy compatibility handling.
|
||||
- Live Settings saves apply subtitle CSS declarations immediately to open video overlays, and the generated example config uses the same CSS declaration paths.
|
||||
@@ -1,5 +0,0 @@
|
||||
type: fixed
|
||||
area: subtitles
|
||||
|
||||
- Kept frequency highlighting for determiner-led noun compounds like `その場` while still filtering standalone determiners.
|
||||
- Fixed frequency annotations for Yomitan single-token compounds with internal particles such as `目の前`, while keeping pure grammar/kana helper spans unannotated.
|
||||
@@ -1,7 +0,0 @@
|
||||
type: fixed
|
||||
area: tray
|
||||
|
||||
- Kept the tray app running when closing tray-launched Yomitan settings, with a close-only menu so closing settings does not quit the tray, and an in-page close button on Hyprland where native window controls are unavailable.
|
||||
- Kept settings loading from blocking other tray actions, serialized copied Yomitan extension refreshes at startup, and disabled the embedded popup preview to avoid renderer hangs during sidebar navigation.
|
||||
- Fixed session help focus handling so the modal can close without mpv running.
|
||||
- Fixed the Windows tray "Open SubMiner Setup" action so it opens the setup window after first-run setup is complete.
|
||||
@@ -1,6 +0,0 @@
|
||||
type: fixed
|
||||
area: updater
|
||||
|
||||
- Linux: `subminer -u` performs release updates independently of any running tray app (reporting `up to date` without downloading when not newer), and update checks use GitHub release metadata/assets instead of the native Electron updater to avoid network-service crashes during startup.
|
||||
- macOS: update dialogs from `subminer -u` reliably appear in the foreground; builds that cannot apply native updates show a manual-install message instead of a restart prompt; `electron-updater` metadata and ZIP downloads route through `/usr/bin/curl` to avoid Electron network crashes while preserving the Squirrel install path; and metadata mismatches from conflicting ZIP filenames are resolved.
|
||||
- Windows: automatic updates keep the native `electron-updater`/NSIS install path while routing updater HTTP through main-process fetch, avoiding the delayed app exit after launch.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: fixed
|
||||
area: windows
|
||||
|
||||
- Windows startup failures now show a native error dialog and write fatal details to the SubMiner app log instead of exiting silently.
|
||||
@@ -1,6 +0,0 @@
|
||||
type: fixed
|
||||
area: youtube
|
||||
|
||||
- Downloaded selected YouTube primary subtitles to temporary local files so the primary bar and sidebar read the same source, with cleanup on reload and quit, and suppressed false load-failure notifications by re-checking live mpv subtitle state.
|
||||
- Launcher-managed playback commands create the tray icon even when attaching to an already-running process, and app-owned YouTube playback no longer lets the mpv plugin start a second SubMiner instance.
|
||||
- Logged Linux tray registration failures with a StatusNotifier/AppIndicator hint and documented the Hyprland tray-host requirement.
|
||||
@@ -380,7 +380,7 @@
|
||||
"word-spacing": "0", // Word spacing setting.
|
||||
"font-kerning": "normal", // Font kerning setting.
|
||||
"text-rendering": "geometricPrecision", // Text rendering setting.
|
||||
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
|
||||
"text-shadow": "-1px -1px 2px rgba(0,0,0,0.95), 1px -1px 2px rgba(0,0,0,0.95), -1px 1px 2px rgba(0,0,0,0.95), 1px 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5)", // Text shadow setting.
|
||||
"backdrop-filter": "blur(6px)", // Backdrop filter setting.
|
||||
"--subtitle-hover-token-color": "#f4dbd6", // Subtitle hover token color setting.
|
||||
"--subtitle-hover-token-background-color": "transparent" // Subtitle hover token background color setting.
|
||||
@@ -405,7 +405,7 @@
|
||||
"frequencyDictionary": {
|
||||
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
|
||||
"sourcePath": "", // Optional absolute path to a frequency dictionary directory. If empty, built-in discovery search paths are used.
|
||||
"topX": 1000, // Only color tokens with frequency rank <= topX (default: 1000).
|
||||
"topX": 10000, // Only color tokens with frequency rank <= topX (default: 10000).
|
||||
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
|
||||
"matchMode": "headword", // headword: frequency lookup uses dictionary form. surface: lookup uses subtitle-visible token text. Values: headword | surface
|
||||
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
|
||||
@@ -430,7 +430,7 @@
|
||||
"word-spacing": "0", // Word spacing setting.
|
||||
"font-kerning": "normal", // Font kerning setting.
|
||||
"text-rendering": "geometricPrecision", // Text rendering setting.
|
||||
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
|
||||
"text-shadow": "-1px -1px 2px rgba(0,0,0,0.95), 1px -1px 2px rgba(0,0,0,0.95), -1px 1px 2px rgba(0,0,0,0.95), 1px 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5)", // Text shadow setting.
|
||||
"backdrop-filter": "blur(6px)" // Backdrop filter setting.
|
||||
} // CSS declaration object applied to secondary subtitles after normal subtitle style defaults.
|
||||
} // Secondary setting.
|
||||
|
||||
@@ -4,7 +4,7 @@ SubMiner can sync your watch progress to [AniList](https://anilist.co) automatic
|
||||
|
||||
AniList data also powers two additional features: [cover art](#cover-art) for the stats dashboard and the [Character Dictionary](/character-dictionary) for in-overlay name lookup.
|
||||
|
||||
[AniList](https://anilist.co) is a free website for tracking which anime you have watched. An **access token** is a private key SubMiner stores so it can update your list on your behalf — you approve it once during setup, and you never paste a password into SubMiner.
|
||||
[AniList](https://anilist.co) is a free website for tracking which anime you have watched. An **access token** is a private key SubMiner stores so it can update your list on your behalf - you approve it once during setup, and you never paste a password into SubMiner.
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ This project is built primarily for [Kiku](https://kiku.youyoumu.my.id/) and [La
|
||||
::: tip New to these terms?
|
||||
- **Anki** is the flashcard app where your study cards live.
|
||||
- **AnkiConnect** is a free add-on that lets other programs (like SubMiner) talk to Anki over a local connection. SubMiner needs it installed to add or edit cards.
|
||||
- A **note type** (also called a "model") is the template that defines what a card looks like — for example the Kiku or Lapis templates many Japanese learners use.
|
||||
- A **note type** (also called a "model") is the template that defines what a card looks like - for example the Kiku or Lapis templates many Japanese learners use.
|
||||
- A **field** is one labeled slot in that template, such as `Sentence`, `Expression`, or `Picture`. SubMiner fills these fields when it mines a card.
|
||||
:::
|
||||
|
||||
@@ -22,9 +22,9 @@ AnkiConnect listens on `http://127.0.0.1:8765` by default. If you changed the po
|
||||
|
||||
When you add a word via Yomitan, SubMiner detects the new card and fills in the sentence, audio, image, and translation fields automatically. Two detection methods are available:
|
||||
|
||||
**Proxy mode** (default) — SubMiner runs a local *proxy*: a small middleman server that sits between Yomitan and Anki. Yomitan sends new cards to SubMiner, SubMiner enriches them, then passes them along to Anki. This makes enrichment instant.
|
||||
**Proxy mode** (default) - SubMiner runs a local *proxy*: a small middleman server that sits between Yomitan and Anki. Yomitan sends new cards to SubMiner, SubMiner enriches them, then passes them along to Anki. This makes enrichment instant.
|
||||
|
||||
**Polling mode** (fallback, when the proxy is disabled) — SubMiner asks AnkiConnect every few seconds whether any new cards were added, then enriches them. Simpler setup, but with a short delay (~3 seconds).
|
||||
**Polling mode** (fallback, when the proxy is disabled) - SubMiner asks AnkiConnect every few seconds whether any new cards were added, then enriches them. Simpler setup, but with a short delay (~3 seconds).
|
||||
|
||||
Use proxy mode if you want immediate enrichment. Use polling mode if your Yomitan instance is external (browser-based) or you prefer minimal configuration.
|
||||
|
||||
@@ -36,7 +36,7 @@ In both modes, the enrichment workflow is the same:
|
||||
4. Fills the translation field from the secondary subtitle or AI.
|
||||
5. Writes metadata to the miscInfo field.
|
||||
|
||||
Polling mode uses the query `"deck:<ankiConnect.deck>" added:1` to find recently added cards. If no deck is configured, it searches all decks.
|
||||
Polling mode uses the query `"deck:<ankiConnect.deck>" added:1` to find recently added cards. If no deck is configured, it searches all decks. In Settings, the AnkiConnect deck dropdown auto-fills from Yomitan's current mining deck when available, then falls back to the decks reported by AnkiConnect.
|
||||
Known-word sync scope is controlled by `ankiConnect.knownWords.decks`.
|
||||
|
||||
### Proxy Mode Setup (Yomitan / Texthooker)
|
||||
|
||||
+14
-14
@@ -34,7 +34,7 @@ plugin/
|
||||
src/
|
||||
ai/ # AI translation provider utilities (client, config)
|
||||
main-entry.ts # Background-mode bootstrap wrapper before loading main.js
|
||||
main.ts # Entry point — delegates to runtime composers/domain modules
|
||||
main.ts # Entry point - delegates to runtime composers/domain modules
|
||||
preload.ts # Electron preload bridge
|
||||
types.ts # Shared type definitions
|
||||
main/ # Main-process composition/runtime adapters
|
||||
@@ -226,17 +226,17 @@ Most runtime code follows a dependency-injection pattern:
|
||||
|
||||
The composition root (`src/main.ts`) delegates to focused modules in `src/main/` and `src/main/runtime/composers/`:
|
||||
|
||||
- `startup.ts` — argv/env processing and bootstrap flow
|
||||
- `app-lifecycle.ts` — Electron lifecycle event registration
|
||||
- `startup-lifecycle.ts` — app-ready initialization sequence
|
||||
- `state.ts` — centralized application runtime state container
|
||||
- `ipc-runtime.ts` — IPC channel registration and handler wiring
|
||||
- `cli-runtime.ts` — CLI command parsing and dispatch
|
||||
- `overlay-runtime.ts` — overlay window selection and modal state management
|
||||
- `subsync-runtime.ts` — subsync command orchestration
|
||||
- `runtime/composers/anilist-tracking-composer.ts` — AniList media tracking/probe/retry wiring
|
||||
- `runtime/composers/jellyfin-runtime-composer.ts` — Jellyfin config/client/playback/command/setup composition wiring
|
||||
- `runtime/composers/mpv-runtime-composer.ts` — MPV event/factory/tokenizer/warmup wiring
|
||||
- `startup.ts` - argv/env processing and bootstrap flow
|
||||
- `app-lifecycle.ts` - Electron lifecycle event registration
|
||||
- `startup-lifecycle.ts` - app-ready initialization sequence
|
||||
- `state.ts` - centralized application runtime state container
|
||||
- `ipc-runtime.ts` - IPC channel registration and handler wiring
|
||||
- `cli-runtime.ts` - CLI command parsing and dispatch
|
||||
- `overlay-runtime.ts` - overlay window selection and modal state management
|
||||
- `subsync-runtime.ts` - subsync command orchestration
|
||||
- `runtime/composers/anilist-tracking-composer.ts` - AniList media tracking/probe/retry wiring
|
||||
- `runtime/composers/jellyfin-runtime-composer.ts` - Jellyfin config/client/playback/command/setup composition wiring
|
||||
- `runtime/composers/mpv-runtime-composer.ts` - MPV event/factory/tokenizer/warmup wiring
|
||||
|
||||
Composer modules share contract conventions via `src/main/runtime/composers/contracts.ts`:
|
||||
|
||||
@@ -271,9 +271,9 @@ For domains migrated to reducer-style transitions (for example AniList token/que
|
||||
|
||||
## Playback Startup Flow
|
||||
|
||||
Before the app boots, something has to launch mpv, inject the plugin, and bring the overlay up. SubMiner-managed launches own this step — the `subminer` launcher, the app's own playback, and the packaged Windows shortcut all follow the same path. The launcher reads `config.jsonc`, spawns mpv with the IPC socket and the bundled plugin, and passes runtime settings as `--script-opts`. The plugin never reads a config file: the shipped `subminer.conf` is intentionally empty so command-line opts always win.
|
||||
Before the app boots, something has to launch mpv, inject the plugin, and bring the overlay up. SubMiner-managed launches own this step - the `subminer` launcher, the app's own playback, and the packaged Windows shortcut all follow the same path. The launcher reads `config.jsonc`, spawns mpv with the IPC socket and the bundled plugin, and passes runtime settings as `--script-opts`. The plugin never reads a config file: the shipped `subminer.conf` is intentionally empty so command-line opts always win.
|
||||
|
||||
Once mpv is up, exactly one of two triggers brings up the overlay. On a first launch the plugin's `file-loaded` hook self-starts the app once the socket is ready (because the launcher injected `auto_start=yes`). When the app is already running — or for explicit `--start-overlay` and YouTube flows — the launcher instead attaches over the control socket and suppresses the plugin's auto-start, so the two never fire together. Both converge on the same app bring-up, which then runs the Program Lifecycle below.
|
||||
Once mpv is up, exactly one of two triggers brings up the overlay. On a first launch the plugin's `file-loaded` hook self-starts the app once the socket is ready (because the launcher injected `auto_start=yes`). When the app is already running - or for explicit `--start-overlay` and YouTube flows - the launcher instead attaches over the control socket and suppresses the plugin's auto-start, so the two never fire together. Both converge on the same app bring-up, which then runs the Program Lifecycle below.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
|
||||
+187
-9
@@ -1,16 +1,194 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
## v0.15.2 (2026-06-02)
|
||||
|
||||
- **Character Dictionary:** Loaded entries are now scoped to the current AniList media for subtitle name matching and inline portraits. Added a character dictionary manager at `Ctrl/Cmd+D`; AniList overrides now live inside that manager instead of using a separate default shortcut.
|
||||
**Changed**
|
||||
- Yomitan: Updated the bundled Yomitan build to the latest vendored revision.
|
||||
|
||||
## v0.14.0 (2026-05-12)
|
||||
**Fixed**
|
||||
- Anki - Animated AVIF: Clip timing no longer starts or ends early; word-audio lead-in and clip duration are now aligned to frame boundaries.
|
||||
- Overlay (Hyprland): Fixed fullscreen overlay alignment - modal, stats, and sidebar content no longer shift below the mpv window.
|
||||
- Overlay (macOS): Subtitle bars are now interactive immediately after autoplay starts with "wait for overlay to be ready" enabled, without requiring a manual click.
|
||||
- Overlay (macOS): Fixed overlay, subtitles, and subtitle sidebar staying hidden after a modal closes until the user clicked the mpv window; focus is now restored to mpv when the last modal closes, so playback shortcuts and the overlay reappear correctly - including in native fullscreen.
|
||||
|
||||
SubMiner no longer requires a globally-installed mpv plugin. The bundled plugin is injected at runtime only when SubMiner launches mpv — through the `subminer` launcher, the app's managed launch, or the packaged Windows SubMiner mpv shortcut. When you open mpv on its own, SubMiner is not involved and the plugin is never loaded. If you have a legacy global SubMiner plugin under mpv's `scripts` directory, first-run setup detects it and prompts you to remove it before playback starts.
|
||||
## v0.15.1 (2026-05-31)
|
||||
|
||||
**Fixed**
|
||||
|
||||
- **Linux Overlay Stacking**: Fixed the overlay intermittently dropping behind mpv on KDE Plasma and other non-Hyprland/Sway Wayland sessions; restored subtitle hover, pause-on-hover, and Yomitan lookups on X11/XWayland; the overlay now correctly layers above/below mpv based on fullscreen state, yields to foreground windows (Settings, Yomitan, AniList, etc.), and avoids startup flashes and fullscreen transition glitches.
|
||||
- **Linux Overlay (Hyprland Lua)**: Fixed overlay placement on Hyprland 0.55+ when using a Lua-based config.
|
||||
- **Manual Overlay Startup**: Fixed manual visible-overlay startup from mpv - now correctly attaches to playback, keeps the window bounds synced with mpv, and primes current subtitles before showing.
|
||||
- **Playlist Transitions**: Reused the warm overlay when mpv advances to the next playlist item, avoiding a redundant tokenization pause and preserving visible subtitles across tracks.
|
||||
- **macOS Overlay**: Fixed the visible subtitle overlay staying click-through after pause-until-ready releases playback; restored mpv focus after closing modal windows so subtitles and keybinds resume without clicking the player.
|
||||
- **Mouse Keybindings**: Fixed keybinding capture and runtime handling for mouse buttons, including side buttons like `MBTN_BACK` and `MBTN_FORWARD`.
|
||||
- **Windows mpv Shortcut**: Fixed the Windows `SubMiner mpv` shortcut so videos attach to an already-running background app instead of spawning a second process.
|
||||
|
||||
**Docs**
|
||||
|
||||
- **Troubleshooting**: Updated Hyprland overlay docs with current Lua (`hl.window_rule`) and legacy config syntax; added troubleshooting for KDE/Wayland and other non-Hyprland/Sway Wayland sessions; added a Character Dictionary troubleshooting section; added a "See Also" index linking each feature's troubleshooting page.
|
||||
|
||||
## v0.15.0 (2026-05-29)
|
||||
|
||||
**Breaking Changes**
|
||||
|
||||
- **Subsync:**
|
||||
- The `subsync.defaultMode` config option has been removed
|
||||
- Subsync now always opens the manual subtitle picker regardless of any previously set default mode
|
||||
- **N+1 Highlighting:**
|
||||
- N+1 highlighting now has its own dedicated `ankiConnect.nPlusOne.enabled` option, separate from known-word highlighting
|
||||
- It is no longer enabled automatically when known-word highlighting is on - enable it explicitly to keep N+1 annotations
|
||||
|
||||
**Added**
|
||||
|
||||
- **Character Dictionary:** Added AniList-based character dictionary selection for resolving title mismatches — open it in-app with the new `Ctrl+Alt+A` shortcut or from the CLI with `subminer dictionary --candidates` / `--select`. Series-scoped overrides replace stale entries in the merged dictionary.
|
||||
- **Auto-Updater:**
|
||||
- Tray and `subminer -u` update checks with app update prompts
|
||||
- Launcher and Linux rofi theme auto-updates
|
||||
- Checksum verification and configurable notifications
|
||||
- Opt-in prerelease channel via `updates.channel: "prerelease"`
|
||||
- **Settings Window:**
|
||||
- New dedicated Settings window via `subminer --settings` or `subminer settings`, organized into Appearance, Behavior, Anki, Input, and Integration sections
|
||||
- Click-to-learn keybinding controls
|
||||
- AnkiConnect-backed deck, field, and note-type pickers that auto-fill from the configured Anki deck
|
||||
- Cross-category search
|
||||
- Live save for most options including subtitle CSS, stats keys, logging level, Jimaku, Subsync, and Anki mappings
|
||||
- AI and translation settings remain config-file only
|
||||
- **Inline Character Portraits:**
|
||||
- Optional AniList character portraits appear inline for name-matched subtitle text
|
||||
- Manual AniList overrides scoped per parent media directory so separate season folders maintain separate character dictionary selections
|
||||
- **Character Dictionary Manager:** New `Ctrl/Cmd+D` manager modal to remove, reorder, or override loaded entries.
|
||||
- **Log Export:** Sanitized log ZIP export from the tray menu and via `subminer logs -e`, with home-directory usernames redacted from exported contents.
|
||||
- **Launcher CLI:**
|
||||
- `subminer --version` / `subminer -v` prints the installed app version
|
||||
- `mpv.profile` config and Settings support passes a named mpv profile to managed launches
|
||||
- Bundled mpv plugin startup options are now configurable from SubMiner config
|
||||
- **First-Run Setup:**
|
||||
- Optional installer for Bun and the `subminer` CLI on Linux, macOS, and Windows
|
||||
- Windows `subminer.cmd` PATH shim so `subminer` works without manually adding `SubMiner.exe` to PATH
|
||||
- Setup recognizes existing Homebrew or user PATH installs and avoids writing into Homebrew-owned paths
|
||||
- Includes an Open SubMiner Settings button
|
||||
- Standalone setup app quits after completing, returning terminal control
|
||||
- **Primary Subtitle Visibility on Yomitan Popup:** New `subtitleStyle.primaryVisibleOnYomitanPopup` option keeps hover-mode primary subtitles visible while a Yomitan popup is open.
|
||||
|
||||
**Changed**
|
||||
|
||||
- **Subtitle Appearance Config:**
|
||||
- Primary and secondary subtitle appearance now use color controls plus CSS declaration editors, saved as `subtitleStyle.css`, `subtitleStyle.secondary.css`, and `subtitleSidebar.css`
|
||||
- Known-word and N+1 annotation colors moved to `subtitleStyle.knownWordColor` and `subtitleStyle.nPlusOneColor`
|
||||
- Subtitle font defaults updated to `Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP`
|
||||
- Existing configs migrate automatically; legacy Anki color keys still accepted with deprecation warnings
|
||||
- **Subtitle Style Defaults:**
|
||||
- Stronger outline-style text shadow
|
||||
- Thicker JLPT underlines
|
||||
- Frequency `topX` default raised to `10000`
|
||||
- **Character Dictionary:**
|
||||
- Entries scoped to the current AniList media for name matching and inline portraits
|
||||
- Generates Japanese-only name aliases so raw romanized/English aliases no longer surface as separate results
|
||||
- In-app AniList selector waits for an explicit search with the box prefilled from the current filename
|
||||
- `subtitleStyle.nameMatchEnabled` is now the sole switch for dictionary sync and builds
|
||||
- **Electron Runtime:** Updated from 39.8.6 to 42.2.0, returning SubMiner to a supported Electron release line.
|
||||
- **Jellyfin Setup:**
|
||||
- Removed the server presets dropdown
|
||||
- Setup now shows a single editable server URL field
|
||||
- **Jellyfin Cast Identity:**
|
||||
- Device identity now derived from the OS hostname and always reported as SubMiner
|
||||
- Previously configurable identity fields are ignored, preventing multiple installs from sharing a remote-session identity
|
||||
- **Startup Defaults:** Jellyfin remote-session startup warmup and character-name subtitle highlighting now default to off.
|
||||
- **Setup Appearance:** Removed the bundled mpv runtime plugin readiness card from the setup flow.
|
||||
|
||||
**Fixed**
|
||||
|
||||
- **AniList Progress:**
|
||||
- Progress updates fire correctly when playback reaches or skips past the watched threshold, using fresh mpv timing events
|
||||
- Season-specific results preferred for multi-season files, with a clear message when the matched season is not in Planning or Watching
|
||||
- Repeated missing-token checks no longer exhaust retry attempts or duplicate dead-letter entries
|
||||
- **Anki Mining:**
|
||||
- Sentence-audio padding is opt-in by default
|
||||
- Animated AVIF freeze-frame duration aligned to word audio length without double-counting
|
||||
- Multi-line sentence alignment fixed for repeated subtitle text
|
||||
- Kiku duplicate-card detection, auto-merge, modal acknowledgment race, and field/tag ordering corrected
|
||||
- YouTube playback cards use mpv's resolved stream URLs
|
||||
- Sentence cards refresh the secondary subtitle before saving
|
||||
- **Jellyfin Discovery:**
|
||||
- Startup, subtitle track selection, and duplicate ready-signal handling all fixed
|
||||
- Paused mpv no longer misreported as playing
|
||||
- Resume corrected when a remote play command sends `StartPositionTicks: 0` despite saved progress
|
||||
- **Jellyfin Remote:**
|
||||
- Tray checkbox stays in sync on Linux after tray, CLI, or startup changes
|
||||
- Remote controller visibility and progress sync fixed for seeks, stops, startup path changes, and Linux websocket reconnect windows
|
||||
- Play and Resume now behave correctly (Play from beginning, Resume from saved position)
|
||||
- Final progress reports reuse SubMiner's last known position when mpv resets on stop
|
||||
- Windows setup login flow fixed with an IPC bridge, immediate feedback, and a timeout with inline error for unreachable servers
|
||||
- **Overlay (macOS):**
|
||||
- Overlay hides when mpv loses focus, is minimized, or is no longer the foreground app
|
||||
- Stays stable through transient window geometry disappearances from macOS APIs and when clicking from the overlay back into mpv
|
||||
- Stats overlay opened inactive so it appears over fullscreen mpv without switching Spaces
|
||||
- Passthrough fixed so mpv controls stay clickable before hovering a subtitle bar
|
||||
- **Yomitan Sidebar:**
|
||||
- Playback stays paused for sidebar-opened Yomitan popups when auto-pause is enabled
|
||||
- Popups now open when startup races the Yomitan extension load
|
||||
- Sidebar mining cards use audio and images from the clicked sidebar line instead of the current primary subtitle
|
||||
- **Launcher:**
|
||||
- `subminer app` on Linux returns terminal control immediately
|
||||
- `subminer app --setup` opens the setup flow when SubMiner is already running in the background
|
||||
- **YouTube Playback:**
|
||||
- Selected subtitles downloaded to local temp files so the primary bar and sidebar read the same source, with cleanup on reload and quit
|
||||
- False load-failure notifications suppressed
|
||||
- Tray icon created on launcher-managed playback that attaches to an already-running process
|
||||
- **Shortcuts:**
|
||||
- Native mpv menu shortcuts disabled during managed macOS playback so configured SubMiner shortcuts work while mpv has focus
|
||||
- Custom session shortcuts including `stats.markWatchedKey` wired through mpv
|
||||
- Multi-line copy/mine overlay correctly focused so number keys choose the line count on macOS and Windows
|
||||
- **Controller Bindings:**
|
||||
- Controller config and debug shortcuts stay closed while controller support is disabled
|
||||
- Binding learn mode starts from the edit pencil
|
||||
- Remaps saved per controller profile
|
||||
- Binding badges also start learn mode
|
||||
- Row reset buttons restore individual bindings to defaults
|
||||
- **Logging:**
|
||||
- `logging.level` forwarded to launcher-started and Windows shortcut-started mpv sessions, covering mpv log verbosity, plugin logging, and plugin-launched app logging
|
||||
- `logging.rotation` (default 7 days) and per-component `logging.files` toggles added, with mpv logs disabled by default
|
||||
- Repeated IPC socket warning spam suppressed while waiting for mpv to recreate the socket
|
||||
- Windows mpv IPC, subtitle track, and Yomitan diagnostics added
|
||||
- **In-Player Stats:**
|
||||
- Layering fixed so delete confirmations, overlay modals, and update-check dialogs appear above the stats window
|
||||
- Jellyfin playback stats grouped by item metadata so watched episodes merge with matching local library titles and keep clean display names
|
||||
- **WebSocket Annotations:**
|
||||
- Annotation spans and token metadata stay on the annotation WebSocket
|
||||
- The regular subtitle WebSocket is plain-text only
|
||||
- **Subtitle Annotation Prefetching:** Cached colored annotations and character images ready sooner for live subtitle changes without delaying raw subtitle display.
|
||||
- **Windows Startup Errors:** Fatal startup failures now show a native error dialog and write details to the app log instead of exiting silently.
|
||||
|
||||
**Docs**
|
||||
|
||||
- **Documentation Site:**
|
||||
- Published stable docs at the site root with current development docs under `/main/`
|
||||
- Fixed versioned docs navigation, archived page link handling, and local dev version routing
|
||||
- Documented all previously undocumented config options including `subtitleStyle.primaryDefaultMode`, `stats.markWatchedKey`, `immersionTracking.lifetimeSummaries.*`, and all seven `mpv.*` launcher options
|
||||
- Added Playback Startup Flow and Runtime Sockets diagrams to the architecture docs with cross-reference pointers in the MPV Plugin and Troubleshooting pages
|
||||
|
||||
<details>
|
||||
<summary>Internal changes</summary>
|
||||
|
||||
**Internal**
|
||||
|
||||
- **Release Tooling:**
|
||||
- Release-note polishing treats pending fragments and reviewed prerelease notes as a cumulative final outcome, collapsing prerelease-only fixes into the final user-facing change
|
||||
- Prerelease generation reuses existing reviewed notes and merges only new fragment material
|
||||
- `make clean` preserves `release/prerelease-notes.md`
|
||||
- **Tests:** Removed stale Yomitan vendor source-inspection assertions for changes that were not shipped.
|
||||
|
||||
</details>
|
||||
|
||||
## Previous Versions
|
||||
|
||||
<details>
|
||||
<summary>v0.14.x</summary>
|
||||
|
||||
<h2>v0.14.0 (2026-05-12)</h2>
|
||||
|
||||
**Added**
|
||||
|
||||
- **Character Dictionary:** Added AniList-based character dictionary selection for resolving title mismatches - open it in-app with the new `Ctrl+Alt+A` shortcut or from the CLI with `subminer dictionary --candidates` / `--select`. Series-scoped overrides replace stale entries in the merged dictionary.
|
||||
- **Primary Subtitle Bar:** Added a `V` shortcut and mpv plugin binding to toggle the primary subtitle bar without affecting mpv's native subtitle visibility.
|
||||
- **Texthooker:** Added `subminer texthooker -o` and a tray menu item to open the local texthooker page in the default browser.
|
||||
|
||||
@@ -64,11 +242,11 @@ SubMiner no longer requires a globally-installed mpv plugin. The bundled plugin
|
||||
**Internal**
|
||||
|
||||
- Replaced the changelog renderer with an AI polish pass that merges related fragments and writes user-facing release notes. `CHANGELOG.md` keeps internal items in a collapsed `<details>` block; GitHub release notes omit them entirely.
|
||||
- Release CI no longer auto-builds pending `changes/*.md` fragments on tag. Tagging now fails fast if fragments remain — run `bun run changelog:build` (requires the `claude` CLI) and commit before tagging.
|
||||
- Release CI no longer auto-builds pending `changes/*.md` fragments on tag. Tagging now fails fast if fragments remain - run `bun run changelog:build` (requires the `claude` CLI) and commit before tagging.
|
||||
|
||||
</details>
|
||||
|
||||
## Previous Versions
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v0.12.x</summary>
|
||||
@@ -88,8 +266,8 @@ SubMiner no longer requires a globally-installed mpv plugin. The bundled plugin
|
||||
- Stats: Episode detail hides card events whose Anki notes have been deleted, instead of showing phantom mining activity.
|
||||
- Stats: Trend and watch-time charts share a unified theme with horizontal gridlines and larger ticks for legibility.
|
||||
- Stats: Overview, Library, Trends, Sessions, and Vocabulary now use generic "title" wording so YouTube videos and anime live comfortably side by side in the dashboard.
|
||||
- Stats: Session timeline no longer plots seek-forward/seek-backward markers — they were too noisy on sessions with lots of rewinds.
|
||||
- Stats: Replaced the "Library — Per Day" section on the Stats → Trends page with a "Library — Summary" section. The new section shows a top-10 watch-time leaderboard chart and a sortable per-title table (watch time, videos, sessions, cards, words, lookups, lookups/100w, date range), all scoped to the current date range selector.
|
||||
- Stats: Session timeline no longer plots seek-forward/seek-backward markers - they were too noisy on sessions with lots of rewinds.
|
||||
- Stats: Replaced the "Library - Per Day" section on the Stats → Trends page with a "Library - Summary" section. The new section shows a top-10 watch-time leaderboard chart and a sortable per-title table (watch time, videos, sessions, cards, words, lookups, lookups/100w, date range), all scoped to the current date range selector.
|
||||
|
||||
**Fixed**
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Character Dictionary
|
||||
|
||||
SubMiner can build a Yomitan-compatible character dictionary from [AniList](https://anilist.co) metadata so that character names in subtitles are recognized, highlighted, and enrichable with context — portraits, roles, voice actors, and biographical detail — without leaving the overlay. (AniList is an online anime/manga database; SubMiner pulls each show's character list from it.)
|
||||
SubMiner can build a Yomitan-compatible character dictionary from [AniList](https://anilist.co) metadata so that character names in subtitles are recognized, highlighted, and enrichable with context - portraits, roles, voice actors, and biographical detail - without leaving the overlay. (AniList is an online anime/manga database; SubMiner pulls each show's character list from it.)
|
||||
|
||||
This is helpful because proper names rarely appear in normal dictionaries, so character names would otherwise be flagged as "unknown" words and clutter your mining. Recognizing them keeps your N+1 highlighting focused on real vocabulary.
|
||||
|
||||
@@ -10,28 +10,25 @@ The dictionary is generated per-media, merged across your recently-watched title
|
||||
|
||||
The feature has three stages: **snapshot**, **merge**, and **match**.
|
||||
|
||||
1. **Snapshot** — When you start watching a new title, SubMiner queries the AniList GraphQL API for the media's character list. Each character's names, reading, role, description, birthday, voice actors, and portrait are fetched and saved as a local JSON snapshot in `character-dictionaries/snapshots/anilist-{mediaId}.json`. Images are downloaded and base64-encoded into the snapshot.
|
||||
1. **Snapshot** - When you start watching a new title, SubMiner queries the AniList GraphQL API for the media's character list. Each character's names, reading, role, description, birthday, voice actors, and portrait are fetched and saved as a local JSON snapshot in `character-dictionaries/snapshots/anilist-{mediaId}.json`. Images are downloaded and base64-encoded into the snapshot.
|
||||
|
||||
2. **Merge** — SubMiner maintains a most-recently-used list of media IDs (default: 3). Snapshots from those titles are merged into a single Yomitan ZIP — `character-dictionaries/merged.zip` — which is always named "SubMiner Character Dictionary" so Yomitan treats it as a single stable dictionary across rebuilds.
|
||||
2. **Merge** - SubMiner maintains a most-recently-used list of media IDs (default: 3). Snapshots from those titles are merged into a single Yomitan ZIP - `character-dictionaries/merged.zip` - which is always named "SubMiner Character Dictionary" so Yomitan treats it as a single stable dictionary across rebuilds.
|
||||
|
||||
3. **Match** — During subtitle rendering, Yomitan scans subtitle text against all loaded dictionaries including the character dictionary. SubMiner only accepts character entries for the current AniList media when that media ID is known, then flags matching tokens with `isNameMatch` and highlights them in the overlay with a distinct color.
|
||||
3. **Match** - During subtitle rendering, Yomitan scans subtitle text against all loaded dictionaries including the character dictionary. SubMiner only accepts character entries for the current AniList media when that media ID is known, then flags matching tokens with `isNameMatch` and highlights them in the overlay with a distinct color.
|
||||
|
||||
## Enabling the Feature
|
||||
|
||||
Character dictionary sync is disabled by default. To turn it on:
|
||||
|
||||
1. Authenticate with AniList (see [AniList Integration](/anilist-integration#setup)).
|
||||
2. Set `subtitleStyle.nameMatchEnabled` to `true` in your config or enable **Name Match Enabled** in Settings.
|
||||
3. Start watching — SubMiner will generate a snapshot for the current media and import the merged dictionary into Yomitan automatically.
|
||||
1. Enable **Name Match** in Settings → Subtitle Style, or set `subtitleStyle.nameMatchEnabled: true` in your config.
|
||||
2. Start watching - SubMiner queries AniList's public GraphQL API (no authentication required) and imports the merged dictionary into Yomitan automatically.
|
||||
3. Optionally enable **Name Match Images** (Settings → Subtitle Style) to show inline circular character portraits next to matched names in subtitles.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"anilist": {
|
||||
"enabled": true,
|
||||
"accessToken": "your-token",
|
||||
},
|
||||
"subtitleStyle": {
|
||||
"nameMatchEnabled": true,
|
||||
"nameMatchImagesEnabled": true, // optional - inline portraits
|
||||
},
|
||||
}
|
||||
```
|
||||
@@ -40,6 +37,10 @@ Character dictionary sync is disabled by default. To turn it on:
|
||||
The first sync for a media title takes a few seconds while character data and portraits are fetched from AniList. Subsequent launches reuse the cached media match and snapshot without a fresh AniList lookup.
|
||||
:::
|
||||
|
||||
::: info
|
||||
AniList character data is fetched via public GraphQL queries - no account or access token is needed. AniList authentication is only required for the separate [watch-progress sync](/anilist-integration) feature.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
If `yomitan.externalProfilePath` is set, SubMiner switches to read-only external-profile mode. In that mode SubMiner can reuse another app's installed Yomitan dictionaries/settings, but SubMiner's own character-dictionary features are fully disabled.
|
||||
:::
|
||||
@@ -59,7 +60,7 @@ A single character produces many searchable terms so that names are recognized r
|
||||
|
||||
- ア・リ・ス → アリス (combined), plus individual segments
|
||||
|
||||
**Honorific suffixes** — each base name is expanded with 15 common suffixes:
|
||||
**Honorific suffixes** - each base name is expanded with 15 common suffixes:
|
||||
|
||||
| Honorific | Reading |
|
||||
| --------- | ---------- |
|
||||
@@ -79,16 +80,16 @@ A single character produces many searchable terms so that names are recognized r
|
||||
| 社長 | しゃちょう |
|
||||
| 部長 | ぶちょう |
|
||||
|
||||
**Romanized names** — names stored in romaji on AniList are converted to kana aliases so they can match against Japanese subtitle text.
|
||||
**Romanized names** - names stored in romaji on AniList are converted to kana aliases so they can match against Japanese subtitle text.
|
||||
|
||||
This means a character like "太郎" generates entries for 太郎, 太郎さん, 太郎先生, 太郎君, 太郎ちゃん, and so on — all with correct readings.
|
||||
This means a character like "太郎" generates entries for 太郎, 太郎さん, 太郎先生, 太郎君, 太郎ちゃん, and so on - all with correct readings.
|
||||
|
||||
## Name Matching
|
||||
|
||||
Name matching runs inside Yomitan's scanning pipeline during subtitle tokenization.
|
||||
|
||||
1. Yomitan receives subtitle text and scans for dictionary matches.
|
||||
2. Entries from "SubMiner Character Dictionary" are checked with exact primary-source matching — the token must match the entry's `originalText` with `isPrimary: true` and `matchType: 'exact'`.
|
||||
2. Entries from "SubMiner Character Dictionary" are checked with exact primary-source matching - the token must match the entry's `originalText` with `isPrimary: true` and `matchType: 'exact'`.
|
||||
3. When the current AniList media ID is known, entries whose embedded media ID belongs to a different title are ignored for name matching and inline portraits.
|
||||
4. Matched tokens are flagged `isNameMatch: true` and forwarded to the renderer.
|
||||
5. If `subtitleStyle.nameMatchEnabled` is enabled, the renderer applies the name-match highlight color (default: `#f5bde6`).
|
||||
@@ -106,17 +107,36 @@ Name matches are visually distinct from [N+1 targeting, frequency highlighting,
|
||||
| `subtitleStyle.nameMatchImagesEnabled` | `false` | Show small AniList portraits beside names |
|
||||
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Highlight color for matched names |
|
||||
|
||||
## Inline Character Portraits
|
||||
|
||||
When `subtitleStyle.nameMatchImagesEnabled` is enabled, SubMiner injects a small circular portrait image directly into the subtitle line next to each matched character name.
|
||||
|
||||
Portraits are sourced from the local snapshot - they are embedded at snapshot-generation time and served from the cached ZIP, so no network request happens during playback. Images are downloaded from AniList CDN once per character and stored in `character-dictionaries/img/`.
|
||||
|
||||
If a snapshot was generated before portrait data was available (e.g. during an earlier version or offline sync), SubMiner detects the missing image data on the next media match and automatically refreshes the snapshot so portraits are included in the next merged dictionary build.
|
||||
|
||||
**To enable:**
|
||||
|
||||
- Settings → Subtitle Style → **Name Match Images**, or
|
||||
- `subtitleStyle.nameMatchImagesEnabled: true` in config.
|
||||
|
||||
The portrait size is controlled by the surrounding subtitle font size and renders as a circle clipped from the character's AniList cover image.
|
||||
|
||||
::: tip
|
||||
Inline portraits help you quickly associate names with faces while building vocabulary - especially useful for shows with large casts where you're still learning who's who.
|
||||
:::
|
||||
|
||||
## Dictionary Entries
|
||||
|
||||
Each character entry in the Yomitan dictionary includes structured content:
|
||||
|
||||
- **Name** — the matched Japanese name form
|
||||
- **Known names** — generated non-honorific Japanese aliases for that character, excluding raw romanized/English aliases from lookup results
|
||||
- **Role badge** — color-coded by role: main (score 100), supporting (90), side (80), background (70)
|
||||
- **Portrait** — character image from AniList, embedded in the ZIP
|
||||
- **Description** — biography text from AniList (collapsible)
|
||||
- **Character information** — age, birthday, gender, blood type (collapsible)
|
||||
- **Voiced by** — voice actor name and portrait (collapsible)
|
||||
- **Name** - the matched Japanese name form
|
||||
- **Known names** - generated non-honorific Japanese aliases for that character, excluding raw romanized/English aliases from lookup results
|
||||
- **Role badge** - color-coded by role: main (score 100), supporting (90), side (80), background (70)
|
||||
- **Portrait** - character image from AniList, embedded in the ZIP
|
||||
- **Description** - biography text from AniList (collapsible)
|
||||
- **Character information** - age, birthday, gender, blood type (collapsible)
|
||||
- **Voiced by** - voice actor name and portrait (collapsible)
|
||||
|
||||
The three collapsible sections can be configured to start open or closed:
|
||||
|
||||
@@ -140,12 +160,12 @@ When `subtitleStyle.nameMatchEnabled` is `true`, SubMiner runs an auto-sync rout
|
||||
|
||||
**Phases:**
|
||||
|
||||
1. **checking** — Is there already a cached snapshot for this media ID?
|
||||
2. **generating** — No cache hit: fetch characters from AniList GraphQL, download portraits (250ms throttle between image requests), save snapshot JSON.
|
||||
3. **syncing** — Add the media ID to the most-recently-used list. Evict old entries beyond `maxLoaded`.
|
||||
4. **building** — Merge active snapshots into a single Yomitan ZIP. A SHA-1 revision hash is computed from the media set — if it matches the previously imported revision, the import is skipped.
|
||||
5. **importing** — Push the ZIP into Yomitan. Waits for Yomitan mutation readiness (7-second timeout per operation).
|
||||
6. **ready** — Dictionary is live. Character names will match on the next subtitle line.
|
||||
1. **checking** - Is there already a cached snapshot for this media ID?
|
||||
2. **generating** - No cache hit: fetch characters from AniList GraphQL, download portraits (250ms throttle between image requests), save snapshot JSON.
|
||||
3. **syncing** - Add the media ID to the most-recently-used list. Evict old entries beyond `maxLoaded`.
|
||||
4. **building** - Merge active snapshots into a single Yomitan ZIP. A SHA-1 revision hash is computed from the media set - if it matches the previously imported revision, the import is skipped.
|
||||
5. **importing** - Push the ZIP into Yomitan. Waits for Yomitan mutation readiness (7-second timeout per operation).
|
||||
6. **ready** - Dictionary is live. Character names will match on the next subtitle line.
|
||||
|
||||
**State tracking** is persisted in `character-dictionaries/auto-sync-state.json`. AniList media matches are cached separately in `character-dictionaries/anilist-resolution-cache.json` so snapshot hits do not need another AniList search.
|
||||
|
||||
@@ -254,9 +274,9 @@ merged.zip
|
||||
|
||||
## Reference Implementation
|
||||
|
||||
SubMiner's character dictionary builder is inspired by the [Japanese Character Name Dictionary](https://github.com/bee-san/Japanese_Character_Name_Dictionary) project — a standalone Rust web service that generates Yomitan character dictionaries from AniList and VNDB data.
|
||||
SubMiner's character dictionary builder is inspired by the [Japanese Character Name Dictionary](https://github.com/bee-san/Japanese_Character_Name_Dictionary) project - a standalone Rust web service that generates Yomitan character dictionaries from AniList and VNDB data.
|
||||
|
||||
The reference implementation covers similar ground — name variant generation, honorific expansion, structured Yomitan content, portrait embedding — and additionally supports VNDB as a data source for visual novel characters. Key differences:
|
||||
The reference implementation covers similar ground - name variant generation, honorific expansion, structured Yomitan content, portrait embedding - and additionally supports VNDB as a data source for visual novel characters. Key differences:
|
||||
|
||||
| | SubMiner | Reference Implementation |
|
||||
| ---------------------- | -------------------------------------------- | ------------------------------------- |
|
||||
@@ -271,7 +291,7 @@ If you work with visual novels or want a standalone dictionary generator indepen
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Names not highlighting:** Confirm `subtitleStyle.nameMatchEnabled` is `true`. Check that the current media has an AniList entry — SubMiner needs a media ID to fetch characters.
|
||||
- **Names not highlighting:** Confirm `subtitleStyle.nameMatchEnabled` is `true`. Check that the current media has an AniList entry - SubMiner needs a media ID to fetch characters.
|
||||
- **Inline portraits missing:** Confirm `subtitleStyle.nameMatchImagesEnabled` is `true`. On the next character dictionary sync, SubMiner refreshes current-version snapshots that do not contain usable cached character portrait data. Portraits still require AniList to return an image and the image download to succeed.
|
||||
- **Sync seems stuck:** The auto-sync debounces for 800ms after media changes and throttles image downloads at 250ms per image. Large casts (50+ characters) take longer. Check the status bar for the current sync phase.
|
||||
- **Wrong characters showing:** Open the in-app character dictionary manager (`Ctrl/Cmd+D`) to remove/reorder loaded titles, then use **Override** to correct the active AniList match. You can also run `--dictionary-candidates`, then save the correct media with `--dictionary-select --dictionary-anilist-id <id>`. SubMiner ignores character entries from other loaded titles for subtitle name matching and inline portraits once the current media ID is known.
|
||||
@@ -280,6 +300,6 @@ If you work with visual novels or want a standalone dictionary generator indepen
|
||||
|
||||
## Related
|
||||
|
||||
- [Subtitle Annotations](/subtitle-annotations) — how name matches interact with N+1, frequency, and JLPT layers
|
||||
- [AniList Integration](/anilist-integration) — authentication, episode tracking, and AniList settings
|
||||
- [Configuration Reference](/configuration) — full config options
|
||||
- [Subtitle Annotations](/subtitle-annotations) - how name matches interact with N+1, frequency, and JLPT layers
|
||||
- [AniList Integration](/anilist-integration) - watch-progress sync and AniList authentication (separate from character dictionary)
|
||||
- [Configuration Reference](/configuration) - full config options
|
||||
|
||||
+29
-28
@@ -8,7 +8,7 @@ outline: [2, 3]
|
||||
import { withBase } from 'vitepress';
|
||||
</script>
|
||||
|
||||
SubMiner is configured through a single file (`config.jsonc`). Most settings are also editable from the in-app **Settings** window — you rarely need to edit the file by hand. This page is the full reference: it explains the Settings window, where the config file lives, and documents every option grouped by topic. New to SubMiner? The Quick Start below plus the [Settings window](#settings) cover everything most users need.
|
||||
SubMiner is configured through a single file (`config.jsonc`). Most settings are also editable from the in-app **Settings** window - you rarely need to edit the file by hand. This page is the full reference: it explains the Settings window, where the config file lives, and documents every option grouped by topic. New to SubMiner? The Quick Start below plus the [Settings window](#settings) cover everything most users need.
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -39,7 +39,7 @@ Then customize as needed using the sections below.
|
||||
|
||||
## Settings
|
||||
|
||||
SubMiner includes a dedicated **Settings** window accessible from the tray menu, the app `--settings` flag, or launcher commands such as `subminer --settings` and `subminer settings`. It is the primary way to configure SubMiner — all changes are written directly to `config.jsonc`, so manual file editing is not required for most users.
|
||||
SubMiner includes a dedicated **Settings** window accessible from the tray menu, the app `--settings` flag, or launcher commands such as `subminer --settings` and `subminer settings`. It is the primary way to configure SubMiner - all changes are written directly to `config.jsonc`, so manual file editing is not required for most users.
|
||||
|
||||
The Settings window groups options by workflow instead of mirroring the raw config-file shape:
|
||||
|
||||
@@ -52,7 +52,7 @@ The Settings window groups options by workflow instead of mirroring the raw conf
|
||||
- Tracking & App
|
||||
- Advanced
|
||||
|
||||
Each field still writes to its current `config.jsonc` path. For example, subtitle hover pause appears under **Behavior** / playback behavior, but saves to `subtitleStyle.autoPauseVideoOnHover`. Anki-aware fields can query AnkiConnect for deck names, note types, and field names, and keybinding fields use click-to-learn controls instead of raw text boxes.
|
||||
Each field still writes to its current `config.jsonc` path. For example, subtitle hover pause appears under **Behavior** / playback behavior, but saves to `subtitleStyle.autoPauseVideoOnHover`. Anki-aware fields can query AnkiConnect for deck names, note types, and field names. The AnkiConnect deck field also reads Yomitan's current mining deck and auto-fills an empty setting when one is found. Keybinding fields use click-to-learn controls instead of raw text boxes.
|
||||
|
||||
The Settings window preserves existing JSONC comments, trailing commas, and unrelated keys. Resetting a field removes the explicit config path so the built-in default applies.
|
||||
|
||||
@@ -96,8 +96,8 @@ SubMiner watches the active config file (`config.jsonc` or `config.json`) while
|
||||
|
||||
Hot-reloadable settings include subtitle appearance, sidebar controls, keybindings,
|
||||
logging level, selected source-language preferences, Jimaku/Subsync settings, and
|
||||
the Anki known-word, N+1, field, sentence-card, and Kiku options listed in the
|
||||
reference tables below.
|
||||
the Anki deck, known-word, N+1, field, sentence-card, and Kiku options listed
|
||||
in the reference tables below.
|
||||
|
||||
When these values change, SubMiner applies them live. Invalid config edits are rejected and the previous valid runtime config remains active.
|
||||
|
||||
@@ -105,7 +105,7 @@ Restart-required changes:
|
||||
|
||||
- Any other config sections still require restart.
|
||||
- Shared top-level `ai` provider settings still require restart.
|
||||
- AnkiConnect transport/proxy/media/deck/tag fields still require restart unless listed above.
|
||||
- AnkiConnect transport/proxy/media/tag fields still require restart unless listed above.
|
||||
- SubMiner shows an on-screen/system notification listing restart-required sections when they change.
|
||||
|
||||
### Configuration Options Overview
|
||||
@@ -223,7 +223,7 @@ Control whether the overlay automatically becomes visible when it connects to mp
|
||||
| -------------------- | --------------- | ----------------------------------------------------- |
|
||||
| `auto_start_overlay` | `true`, `false` | Auto-show overlay on mpv connection (default: `true`) |
|
||||
|
||||
When you launch through the SubMiner app or the `subminer` wrapper, the launcher reads these settings from this config and injects them into the mpv plugin at runtime — there is no separate plugin config file to edit. `auto_start_overlay` controls whether the visible overlay shows on auto-start. Two related keys in the `mpv` block tune startup behavior: `mpv.autoStartSubMiner` starts the overlay automatically when a file loads, and `mpv.pauseUntilOverlayReady` pauses mpv on visible auto-start until SubMiner signals overlay/tokenization readiness.
|
||||
When you launch through the SubMiner app or the `subminer` wrapper, the launcher reads these settings from this config and injects them into the mpv plugin at runtime - there is no separate plugin config file to edit. `auto_start_overlay` controls whether the visible overlay shows on auto-start. Two related keys in the `mpv` block tune startup behavior: `mpv.autoStartSubMiner` starts the overlay automatically when a file loads, and `mpv.pauseUntilOverlayReady` pauses mpv on visible auto-start until SubMiner signals overlay/tokenization readiness.
|
||||
|
||||
On Windows, packaged plugin installs also rewrite the plugin socket path to `\\.\pipe\subminer-socket`.
|
||||
|
||||
@@ -339,7 +339,7 @@ See `config.example.jsonc` for detailed configuration options.
|
||||
"word-spacing": "0",
|
||||
"font-kerning": "normal",
|
||||
"text-rendering": "geometricPrecision",
|
||||
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)",
|
||||
"text-shadow": "-1px -1px 2px rgba(0,0,0,0.95), 1px -1px 2px rgba(0,0,0,0.95), -1px 1px 2px rgba(0,0,0,0.95), 1px 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5)",
|
||||
"font-style": "normal",
|
||||
"backdrop-filter": "blur(6px)",
|
||||
"--subtitle-hover-token-color": "#f4dbd6",
|
||||
@@ -351,7 +351,7 @@ See `config.example.jsonc` for detailed configuration options.
|
||||
"color": "#cad3f5",
|
||||
"background-color": "transparent",
|
||||
"font-size": "24px",
|
||||
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)"
|
||||
"text-shadow": "-1px -1px 2px rgba(0,0,0,0.95), 1px -1px 2px rgba(0,0,0,0.95), -1px 1px 2px rgba(0,0,0,0.95), 1px 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -375,7 +375,7 @@ See `config.example.jsonc` for detailed configuration options.
|
||||
| `nPlusOneColor` | string | Hex color used for the single N+1 target subtitle highlight (default: `#c6a0f6`) |
|
||||
| `frequencyDictionary.enabled` | boolean | Enable frequency highlighting from dictionary lookups (`false` by default) |
|
||||
| `frequencyDictionary.sourcePath` | string | Path to a local frequency dictionary root. Leave empty or omit to use installed/default frequency-dictionary search paths. |
|
||||
| `frequencyDictionary.topX` | number | Only color tokens whose frequency rank is `<= topX` (`1000` by default) |
|
||||
| `frequencyDictionary.topX` | number | Only color tokens whose frequency rank is `<= topX` (`10000` by default) |
|
||||
| `frequencyDictionary.mode` | string | `"single"` or `"banded"` (`"single"` by default) |
|
||||
| `frequencyDictionary.matchMode` | string | `"headword"` or `"surface"` (`"headword"` by default) |
|
||||
| `frequencyDictionary.singleColor` | string | Color used for all highlighted tokens in single mode |
|
||||
@@ -526,9 +526,9 @@ The secondary-subtitle language list also acts as the fallback secondary-languag
|
||||
|
||||
**Display modes:**
|
||||
|
||||
- **hidden** — Secondary subtitles not shown
|
||||
- **visible** — Always visible at top of overlay
|
||||
- **hover** — Only visible when hovering over the subtitle area (default)
|
||||
- **hidden** - Secondary subtitles not shown
|
||||
- **visible** - Always visible at top of overlay
|
||||
- **hover** - Only visible when hovering over the subtitle area (default)
|
||||
|
||||
**See `config.example.jsonc`** for additional secondary subtitle configuration options.
|
||||
|
||||
@@ -571,13 +571,15 @@ See `config.example.jsonc` for detailed configuration options and more examples.
|
||||
{ "key": "ArrowRight", "command": ["seek", 5] },
|
||||
{ "key": "ArrowLeft", "command": ["seek", -5] },
|
||||
{ "key": "Shift+ArrowRight", "command": ["seek", 30] },
|
||||
{ "key": "MBTN_BACK", "command": ["sub-seek", -1] },
|
||||
{ "key": "MBTN_FORWARD", "command": ["sub-seek", 1] },
|
||||
{ "key": "KeyR", "command": ["script-binding", "immersive/auto-replay"] },
|
||||
{ "key": "KeyA", "command": ["script-message", "ankiconnect-add-note"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Key format:** Use `KeyboardEvent.code` values (`Space`, `ArrowRight`, `KeyR`, etc.) with optional modifiers (`Ctrl+`, `Alt+`, `Shift+`, `Meta+`).
|
||||
**Key format:** Use `KeyboardEvent.code` values (`Space`, `ArrowRight`, `KeyR`, etc.) with optional modifiers (`Ctrl+`, `Alt+`, `Shift+`, `Meta+`). Mouse buttons use mpv button names: `MBTN_LEFT`, `MBTN_MID`, `MBTN_RIGHT`, `MBTN_BACK`, and `MBTN_FORWARD`.
|
||||
|
||||
**Disable a default binding:** Set command to `null`:
|
||||
|
||||
@@ -897,8 +899,8 @@ Enable automatic Anki card creation and updates with media generation:
|
||||
},
|
||||
"ai": {
|
||||
"enabled": false,
|
||||
"model": "openai/gpt-4o-mini",
|
||||
"systemPrompt": "Translate mined sentence text only."
|
||||
"model": "",
|
||||
"systemPrompt": ""
|
||||
},
|
||||
"media": {
|
||||
"generateAudio": true,
|
||||
@@ -906,11 +908,11 @@ Enable automatic Anki card creation and updates with media generation:
|
||||
"imageType": "static",
|
||||
"imageFormat": "jpg",
|
||||
"imageQuality": 92,
|
||||
"imageMaxWidth": 1280,
|
||||
"imageMaxHeight": 720,
|
||||
"imageMaxWidth": 0,
|
||||
"imageMaxHeight": 0,
|
||||
"animatedFps": 10,
|
||||
"animatedMaxWidth": 640,
|
||||
"animatedMaxHeight": 360,
|
||||
"animatedMaxHeight": 0,
|
||||
"animatedCrf": 35,
|
||||
"audioPadding": 0,
|
||||
"fallbackDuration": 3,
|
||||
@@ -925,8 +927,8 @@ Enable automatic Anki card creation and updates with media generation:
|
||||
"pattern": "[SubMiner] %f (%t)"
|
||||
},
|
||||
"isLapis": {
|
||||
"enabled": true,
|
||||
"sentenceCardModel": "Japanese sentences"
|
||||
"enabled": false,
|
||||
"sentenceCardModel": "Lapis"
|
||||
},
|
||||
"isKiku": {
|
||||
"enabled": false,
|
||||
@@ -951,7 +953,7 @@ This example is intentionally compact. The option table below documents availabl
|
||||
| `proxy.port` | number | Bind port for local AnkiConnect proxy (default: `8766`) |
|
||||
| `proxy.upstreamUrl` | string (URL) | Upstream AnkiConnect URL that proxy forwards to (default: `http://127.0.0.1:8765`) |
|
||||
| `tags` | array of strings | Tags automatically added to cards mined/updated by SubMiner (default: `['SubMiner']`; set `[]` to disable automatic tagging). |
|
||||
| `ankiConnect.deck` | string | Restrict duplicate detection and card enrichment to this Anki deck. Leave empty to search all decks. |
|
||||
| `ankiConnect.deck` | string | Restrict duplicate detection and card enrichment to this Anki deck. Leave empty to search all decks. In Settings, this dropdown auto-fills from Yomitan's current mining deck when available. |
|
||||
| `fields.word` | string | Card field for mined word / expression text (default: `Expression`) |
|
||||
| `fields.audio` | string | Card field for audio files (default: `ExpressionAudio`) |
|
||||
| `fields.image` | string | Card field for images (default: `Picture`) |
|
||||
@@ -1099,8 +1101,8 @@ Set `openBrowser` to `false` to only print the URL without opening a browser.
|
||||
|
||||
Sync the active subtitle track from the overlay picker using `alass` or `ffsubsync`. Both are **optional external tools** that must be installed separately and available on your `PATH` (or configured via the path options below).
|
||||
|
||||
- [`alass`](https://github.com/kaegi/alass) — fast, audio-independent sync using a secondary subtitle as reference
|
||||
- [`ffsubsync`](https://github.com/smacke/ffsubsync) — audio-based sync using the video file as reference
|
||||
- [`alass`](https://github.com/kaegi/alass) - fast, audio-independent sync using a secondary subtitle as reference
|
||||
- [`ffsubsync`](https://github.com/smacke/ffsubsync) - audio-based sync using the video file as reference
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -1133,7 +1135,6 @@ AniList integration is opt-in and disabled by default. Enable it to allow SubMin
|
||||
"enabled": true,
|
||||
"accessToken": "",
|
||||
"characterDictionary": {
|
||||
"enabled": false,
|
||||
"maxLoaded": 3,
|
||||
"profileScope": "all",
|
||||
"collapsibleSections": {
|
||||
@@ -1472,9 +1473,9 @@ If `mpv.profile` is configured and the launcher also receives `--profile`, SubMi
|
||||
|
||||
Launch mode behavior:
|
||||
|
||||
- **`normal`** — mpv opens at its default window size with no extra flags.
|
||||
- **`maximized`** — mpv starts maximized via `--window-maximized=yes`, keeping taskbar access.
|
||||
- **`fullscreen`** — mpv starts in true fullscreen via `--fullscreen`.
|
||||
- **`normal`** - mpv opens at its default window size with no extra flags.
|
||||
- **`maximized`** - mpv starts maximized via `--window-maximized=yes`, keeping taskbar access.
|
||||
- **`fullscreen`** - mpv starts in true fullscreen via `--fullscreen`.
|
||||
|
||||
### YouTube Playback Settings
|
||||
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ Mine vocabulary cards from Yomitan or directly from subtitle lines. SubMiner aut
|
||||
|
||||
## Subtitle Download & Sync
|
||||
|
||||
Search and download subtitles from Jimaku, then retime them with alass or ffsubsync — all from within SubMiner.
|
||||
Search and download subtitles from Jimaku, then retime them with alass or ffsubsync - all from within SubMiner.
|
||||
|
||||
<!-- <video controls playsinline preload="metadata" :poster="withBase(`/assets/demos/subtitle-sync-poster.jpg?v=${v}`)">
|
||||
<source :src="withBase(`/assets/demos/subtitle-sync.webm?v=${v}`)" type="video/webm" />
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
SubMiner can log your watching and mining activity to a local SQLite database, then surface it in the built-in stats dashboard. Tracking is enabled by default and can be turned off if you do not want local analytics.
|
||||
|
||||
"Immersion" here means time spent watching and reading native Japanese content. **All data stays on your computer** — nothing is uploaded anywhere. (SQLite is just a single-file database; you do not need to install or manage anything.)
|
||||
"Immersion" here means time spent watching and reading native Japanese content. **All data stays on your computer** - nothing is uploaded anywhere. (SQLite is just a single-file database; you do not need to install or manage anything.)
|
||||
|
||||
When enabled, SubMiner records per-session statistics (watch time, subtitle lines seen, words encountered, cards mined) and maintains exact lifetime summary tables plus daily/monthly rollups. You can view that data in SubMiner's stats UI or query the database directly with any SQLite tool.
|
||||
|
||||
::: tip For most users
|
||||
Just leave tracking on and use the built-in [Stats Dashboard](#stats-dashboard). The retention, performance, SQL, and schema sections further down are reference material for advanced users who want to inspect or tune the database — you can safely skip them.
|
||||
Just leave tracking on and use the built-in [Stats Dashboard](#stats-dashboard). The retention, performance, SQL, and schema sections further down are reference material for advanced users who want to inspect or tune the database - you can safely skip them.
|
||||
:::
|
||||
|
||||
Episode completion for local `watched` state uses the shared `DEFAULT_MIN_WATCH_RATIO` (`85%`) value from `src/shared/watch-threshold.ts`.
|
||||
@@ -98,9 +98,9 @@ Stats server config lives under `stats`:
|
||||
|
||||
The Vocabulary tab's word detail panel shows example lines from your viewing history. Each example line with a valid source file offers three mining buttons:
|
||||
|
||||
- **Mine Word** — performs a full Yomitan dictionary lookup for the word (definition, reading, pitch accent, etc.) via a short-lived hidden helper, then enriches the card with sentence audio, a screenshot or animated AVIF clip, the highlighted sentence, and metadata extracted from the source video file. Requires Anki and Yomitan dictionaries to be loaded.
|
||||
- **Mine Sentence** — creates a sentence card directly with the `IsSentenceCard` flag set (for Lapis/Kiku workflows), along with audio, image, and translation from the secondary subtitle if available.
|
||||
- **Mine Audio** — creates an audio-only card with the `IsAudioCard` flag, attaching only the sentence audio clip.
|
||||
- **Mine Word** - performs a full Yomitan dictionary lookup for the word (definition, reading, pitch accent, etc.) via a short-lived hidden helper, then enriches the card with sentence audio, a screenshot or animated AVIF clip, the highlighted sentence, and metadata extracted from the source video file. Requires Anki and Yomitan dictionaries to be loaded.
|
||||
- **Mine Sentence** - creates a sentence card directly with the `IsSentenceCard` flag set (for Lapis/Kiku workflows), along with audio, image, and translation from the secondary subtitle if available.
|
||||
- **Mine Audio** - creates an audio-only card with the `IsAudioCard` flag, attaching only the sentence audio clip.
|
||||
|
||||
All three modes respect your `ankiConnect` config: deck, model, field mappings, media settings (static vs AVIF, quality, dimensions), audio padding, metadata pattern, and tags. Media generation runs in parallel for faster card creation.
|
||||
|
||||
@@ -281,11 +281,11 @@ LIMIT ?;
|
||||
|
||||
Core tables:
|
||||
|
||||
- `imm_videos` — video key/title/source metadata
|
||||
- `imm_sessions` — session UUID, video reference, timing/status, final denormalized totals
|
||||
- `imm_session_telemetry` — high-frequency session aggregates over time
|
||||
- `imm_session_events` — event stream with compact numeric event types
|
||||
- `imm_subtitle_lines` — persisted subtitle text and timing per session/video
|
||||
- `imm_videos` - video key/title/source metadata
|
||||
- `imm_sessions` - session UUID, video reference, timing/status, final denormalized totals
|
||||
- `imm_session_telemetry` - high-frequency session aggregates over time
|
||||
- `imm_session_events` - event stream with compact numeric event types
|
||||
- `imm_subtitle_lines` - persisted subtitle text and timing per session/video
|
||||
|
||||
Lifetime summary tables:
|
||||
|
||||
@@ -306,5 +306,5 @@ Vocabulary tables:
|
||||
|
||||
Media-art tables:
|
||||
|
||||
- `imm_media_art` — per-video cover metadata plus shared blob reference
|
||||
- `imm_cover_art_blobs` — deduplicated image bytes keyed by blob hash
|
||||
- `imm_media_art` - per-video cover metadata plus shared blob reference
|
||||
- `imm_cover_art_blobs` - deduplicated image bytes keyed by blob hash
|
||||
|
||||
+5
-5
@@ -24,7 +24,7 @@ features:
|
||||
src: /assets/mpv.svg
|
||||
alt: mpv icon
|
||||
title: Built for mpv
|
||||
details: Tracks subtitles via mpv IPC in real time. Launch with the wrapper script or the mpv plugin — no external bridge needed.
|
||||
details: Tracks subtitles via mpv IPC in real time. Launch with the wrapper script or the mpv plugin - no external bridge needed.
|
||||
link: /usage
|
||||
linkText: How it works
|
||||
- icon:
|
||||
@@ -45,14 +45,14 @@ features:
|
||||
src: /assets/highlight.svg
|
||||
alt: Highlight icon
|
||||
title: Reading Annotations
|
||||
details: N+1 targeting, character-name matching, frequency highlighting, and JLPT tagging — all layered on subtitle text in real time.
|
||||
details: N+1 targeting, character-name matching, frequency highlighting, and JLPT tagging - all layered on subtitle text in real time.
|
||||
link: /subtitle-annotations
|
||||
linkText: Annotation details
|
||||
- icon:
|
||||
src: /assets/video.svg
|
||||
alt: Video playback icon
|
||||
title: YouTube Playback
|
||||
details: Play YouTube URLs or ytsearch targets directly — SubMiner automatically selects and loads subtitles for the video.
|
||||
details: Play YouTube URLs or ytsearch targets directly - SubMiner automatically selects and loads subtitles for the video.
|
||||
link: /usage#youtube-playback
|
||||
linkText: YouTube playback
|
||||
- icon:
|
||||
@@ -66,14 +66,14 @@ features:
|
||||
src: /assets/subtitle-download.svg
|
||||
alt: Subtitle download icon
|
||||
title: Subtitle Download & Sync
|
||||
details: Search and pull subtitles from Jimaku, then retime subtitles with alass or ffsubsync — all from the overlay.
|
||||
details: Search and pull subtitles from Jimaku, then retime subtitles with alass or ffsubsync - all from the overlay.
|
||||
link: /jimaku-integration
|
||||
linkText: Jimaku integration
|
||||
- icon:
|
||||
src: /assets/tokenization.svg
|
||||
alt: Tracking chart icon
|
||||
title: Stats Dashboard
|
||||
details: Browse session history, streak calendars, vocabulary frequency, and per-series progress in a local dashboard — then mine cards straight from your viewing history.
|
||||
details: Browse session history, streak calendars, vocabulary frequency, and per-series progress in a local dashboard - then mine cards straight from your viewing history.
|
||||
link: /immersion-tracking
|
||||
linkText: Dashboard & tracking
|
||||
- icon:
|
||||
|
||||
+26
-26
@@ -1,12 +1,12 @@
|
||||
# Installation
|
||||
|
||||
SubMiner is a desktop app that draws an interactive layer — an **overlay** — on top of the [mpv](https://mpv.io) video player. As you watch native Japanese media, you can click or hover any word in the subtitles to look it up, then turn it into an Anki flashcard without pausing to switch apps. Building flashcards from real content you're watching is called **sentence mining**, and it's what SubMiner is built for. It bundles its own copy of **Yomitan** (a pop-up dictionary) and talks to **AnkiConnect** (an add-on that lets other programs add cards to Anki) so cards get filled in automatically.
|
||||
SubMiner is a desktop app that draws an interactive layer - an **overlay** - on top of the [mpv](https://mpv.io) video player. As you watch native Japanese media, you can click or hover any word in the subtitles to look it up, then turn it into an Anki flashcard without pausing to switch apps. Building flashcards from real content you're watching is called **sentence mining**, and it's what SubMiner is built for. It bundles its own copy of **Yomitan** (a pop-up dictionary) and talks to **AnkiConnect** (an add-on that lets other programs add cards to Anki) so cards get filled in automatically.
|
||||
|
||||
Three steps to get started:
|
||||
|
||||
1. **Install requirements** — mpv and a few optional extras
|
||||
2. **Install SubMiner** — from the AUR, or download from GitHub Releases
|
||||
3. **Launch the app** — first-run setup walks you through dictionaries, the launcher, and everything else
|
||||
1. **Install requirements** - mpv and a few optional extras
|
||||
2. **Install SubMiner** - from the AUR, or download from GitHub Releases
|
||||
3. **Launch the app** - first-run setup walks you through dictionaries, the launcher, and everything else
|
||||
|
||||
## 1. Install Requirements
|
||||
|
||||
@@ -29,14 +29,14 @@ Only **mpv** is strictly required to run SubMiner. Everything else enhances the
|
||||
|
||||
### Linux
|
||||
|
||||
**Window backend** — you need one of these depending on your compositor:
|
||||
**Window backend** - you need one of these depending on your compositor:
|
||||
|
||||
- **Hyprland** — native Wayland support (uses `hyprctl`)
|
||||
- **Sway** — native Wayland support (uses `swaymsg`)
|
||||
- **X11 / Xwayland** — for X11 sessions or any other Wayland compositor (uses `xdotool` and `xwininfo`)
|
||||
- **Hyprland** - native Wayland support (uses `hyprctl`)
|
||||
- **Sway** - native Wayland support (uses `swaymsg`)
|
||||
- **X11 / Xwayland** - for X11 sessions or any other Wayland compositor (uses `xdotool` and `xwininfo`)
|
||||
|
||||
::: warning Wayland support is compositor-specific
|
||||
Wayland has no universal API for window positioning — each compositor exposes its own IPC, so SubMiner needs a dedicated backend per compositor. Only Hyprland and Sway have native Wayland backends. If you run a different Wayland compositor (GNOME, KDE Plasma, river, etc.), both mpv **and** SubMiner must run under X11 or Xwayland. The `subminer` launcher handles this automatically when `--backend x11` is set or the X11 backend is auto-detected.
|
||||
Wayland has no universal API for window positioning - each compositor exposes its own IPC, so SubMiner needs a dedicated backend per compositor. Only Hyprland and Sway have native Wayland backends. If you run a different Wayland compositor (GNOME, KDE Plasma, river, etc.), both mpv **and** SubMiner must run under X11 or Xwayland. The `subminer` launcher handles this automatically when `--backend x11` is set or the X11 backend is auto-detected.
|
||||
:::
|
||||
|
||||
<details>
|
||||
@@ -69,7 +69,7 @@ sudo apt install yt-dlp fzf rofi chafa ffmpegthumbnailer
|
||||
sudo apt install xdotool x11-utils
|
||||
# Optional: subtitle sync
|
||||
pip install ffsubsync
|
||||
# alass is not in apt — install via cargo: cargo install alass-cli
|
||||
# alass is not in apt - install via cargo: cargo install alass-cli
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -94,7 +94,7 @@ pip install ffsubsync
|
||||
|
||||
### macOS
|
||||
|
||||
macOS 11 (Big Sur) or later. Accessibility permission — the macOS setting that lets one app observe and position another app's windows — is required so the overlay can follow the mpv window (see [step 2](#macos-dmg)).
|
||||
macOS 11 (Big Sur) or later. Accessibility permission - the macOS setting that lets one app observe and position another app's windows - is required so the overlay can follow the mpv window (see [step 2](#macos-dmg)).
|
||||
|
||||
```bash
|
||||
brew install mpv ffmpeg
|
||||
@@ -111,7 +111,7 @@ pip install ffsubsync
|
||||
|
||||
Windows 10 or later. Install [`mpv`](https://mpv.io/installation/) and [`ffmpeg`](https://ffmpeg.org/download.html) and ensure both are on `PATH`. Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary.
|
||||
|
||||
No compositor tools or window helpers are needed — native window tracking is built in.
|
||||
No compositor tools or window helpers are needed - native window tracking is built in.
|
||||
|
||||
## 2. Install SubMiner
|
||||
|
||||
@@ -172,8 +172,8 @@ If you prefer to install it manually, see [manual launcher install](#manual-laun
|
||||
|
||||
Download the latest installer from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest):
|
||||
|
||||
- `SubMiner-<version>.exe` — installer (recommended)
|
||||
- `SubMiner-<version>-win.zip` — portable fallback
|
||||
- `SubMiner-<version>.exe` - installer (recommended)
|
||||
- `SubMiner-<version>-win.zip` - portable fallback
|
||||
|
||||
Make sure `mpv.exe` is on your `PATH`, or set `mpv.executablePath` in the config during first-run setup.
|
||||
|
||||
@@ -240,18 +240,18 @@ subminer app --setup
|
||||
# Linux (AppImage directly)
|
||||
~/.local/bin/SubMiner.AppImage --setup
|
||||
|
||||
# macOS — launch SubMiner.app from /Applications, or:
|
||||
# macOS - launch SubMiner.app from /Applications, or:
|
||||
subminer app --setup
|
||||
```
|
||||
|
||||
On **Windows**, just run `SubMiner.exe` — the setup wizard opens automatically on first launch.
|
||||
On **Windows**, just run `SubMiner.exe` - the setup wizard opens automatically on first launch.
|
||||
|
||||
The setup wizard walks you through:
|
||||
|
||||
- **Config file** — auto-created at `~/.config/SubMiner/config.jsonc` (Linux/macOS) or `%APPDATA%\SubMiner\config.jsonc` (Windows)
|
||||
- **Yomitan dictionaries** — import at least one dictionary so word lookups work
|
||||
- **Bun + `subminer` launcher** _(optional)_ — installs the command-line launcher into a writable PATH directory
|
||||
- **Windows shortcut** _(Windows only)_ — create a `SubMiner mpv` Start Menu/Desktop shortcut
|
||||
- **Config file** - auto-created at `~/.config/SubMiner/config.jsonc` (Linux/macOS) or `%APPDATA%\SubMiner\config.jsonc` (Windows)
|
||||
- **Yomitan dictionaries** - import at least one dictionary so word lookups work
|
||||
- **Bun + `subminer` launcher** _(optional)_ - installs the command-line launcher into a writable PATH directory
|
||||
- **Windows shortcut** _(Windows only)_ - create a `SubMiner mpv` Start Menu/Desktop shortcut
|
||||
|
||||
The `Finish setup` button requires a config file and at least one Yomitan dictionary. Bun and the launcher are optional and never block setup completion.
|
||||
|
||||
@@ -268,7 +268,7 @@ subminer video.mkv
|
||||
|
||||
You should see the overlay appear over mpv. If subtitles are loaded, they will appear as interactive text in the overlay.
|
||||
|
||||
On **Windows**, the recommended way to play video is with the **SubMiner mpv** shortcut created during setup — double-click it, or drag a video file onto it.
|
||||
On **Windows**, the recommended way to play video is with the **SubMiner mpv** shortcut created during setup - double-click it, or drag a video file onto it.
|
||||
|
||||
### Verify Setup
|
||||
|
||||
@@ -285,7 +285,7 @@ This checks for the app binary, mpv, ffmpeg, config file, and socket path. Fix a
|
||||
If you plan to mine Anki cards:
|
||||
|
||||
1. Install [Anki](https://apps.ankiweb.net/)
|
||||
2. Install [AnkiConnect](https://ankiweb.net/shared/info/2055492159) — open Anki → **Tools → Add-ons → Get Add-ons** → enter code `2055492159`
|
||||
2. Install [AnkiConnect](https://ankiweb.net/shared/info/2055492159) - open Anki → **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 connects automatically with no extra config needed.
|
||||
@@ -310,9 +310,9 @@ The tray "Check for Updates" entry installs the new app automatically on Linux,
|
||||
|
||||
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.
|
||||
|
||||
The `subminer` launcher handles mpv IPC socket setup automatically. If you launch mpv yourself or from another tool, you must pass `--input-ipc-server=/tmp/subminer-socket` (or `\\.\pipe\subminer-socket` on Windows) — without it the overlay starts but subtitles won't appear.
|
||||
The `subminer` launcher handles mpv IPC socket setup automatically. If you launch mpv yourself or from another tool, you must pass `--input-ipc-server=/tmp/subminer-socket` (or `\\.\pipe\subminer-socket` on Windows) - without it the overlay starts but subtitles won't appear.
|
||||
|
||||
The bundled mpv plugin is injected at runtime automatically — you don't need to install it separately. It provides in-player keybindings (the `y` chord) for controlling the overlay from within mpv. See [MPV Plugin](/mpv-plugin) for the full keybinding and configuration reference.
|
||||
The bundled mpv plugin is injected at runtime automatically - you don't need to install it separately. It provides in-player keybindings (the `y` chord) for controlling the overlay from within mpv. See [MPV Plugin](/mpv-plugin) for the full keybinding and configuration reference.
|
||||
|
||||
## Platform Notes
|
||||
|
||||
@@ -330,7 +330,7 @@ Ensure `mecab` is available on your PATH when launching SubMiner.
|
||||
|
||||
- The **SubMiner mpv** shortcut is the recommended way to launch playback. It starts `mpv.exe` with the right IPC socket and subtitle defaults.
|
||||
- First-run setup adds only `%LOCALAPPDATA%\SubMiner\bin` to the HKCU user PATH. It does not add `SubMiner.exe` to PATH.
|
||||
- IPC socket on Windows is `\\.\pipe\subminer-socket` — do not use `/tmp/subminer-socket`.
|
||||
- IPC socket on Windows is `\\.\pipe\subminer-socket` - do not use `/tmp/subminer-socket`.
|
||||
- Config is stored at `%APPDATA%\SubMiner\config.jsonc`.
|
||||
|
||||
## Manual Launcher Install
|
||||
@@ -374,4 +374,4 @@ 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.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# IPC + Runtime Contracts
|
||||
|
||||
SubMiner's Electron app runs two isolated processes — main and renderer — that can only communicate through IPC channels. This boundary is intentional: the renderer is an untrusted surface (it loads Yomitan, renders user-controlled subtitle text, and runs in a Chromium sandbox), so every message crossing the bridge passes through a validation layer before it can reach domain logic.
|
||||
SubMiner's Electron app runs two isolated processes - main and renderer - that can only communicate through IPC channels. This boundary is intentional: the renderer is an untrusted surface (it loads Yomitan, renders user-controlled subtitle text, and runs in a Chromium sandbox), so every message crossing the bridge passes through a validation layer before it can reach domain logic.
|
||||
|
||||
The contract system enforces this by making channel names, payload shapes, and validators co-located and co-evolved. A change to any IPC surface touches the contract, the validator, the preload bridge, and the handler in the same commit — drift between any of those layers is treated as a bug.
|
||||
The contract system enforces this by making channel names, payload shapes, and validators co-located and co-evolved. A change to any IPC surface touches the contract, the validator, the preload bridge, and the handler in the same commit - drift between any of those layers is treated as a bug.
|
||||
|
||||
## Message Flow
|
||||
|
||||
@@ -38,7 +38,7 @@ flowchart TB
|
||||
|
||||
## Runtime Sockets
|
||||
|
||||
The renderer↔main bridge above lives *inside* the Electron app. A separate set of OS sockets connects the app to the other runtimes — mpv and the launcher/plugin. These carry no renderer payloads and bypass the contract/validator layer; they are command and property channels between processes.
|
||||
The renderer↔main bridge above lives *inside* the Electron app. A separate set of OS sockets connects the app to the other runtimes - mpv and the launcher/plugin. These carry no renderer payloads and bypass the contract/validator layer; they are command and property channels between processes.
|
||||
|
||||
- **mpv IPC socket** (`/tmp/subminer-socket`, or `\\.\pipe\subminer-socket` on Windows): the `MpvIpcClient` in the main process connects here to send JSON commands and subscribe to playback/subtitle properties via `observe_property`. Created by mpv's `--input-ipc-server`.
|
||||
- **App control socket** (`/tmp/subminer-control-<uid>-<hash>.sock`, or a named pipe on Windows): the launcher and the mpv plugin send CLI-style commands (`--start`, `--show-visible-overlay`, `--texthooker`) to a running app here. It also dedupes a second `subminer` invocation into the existing instance instead of launching twice.
|
||||
@@ -73,7 +73,7 @@ How these sockets are established during launch is covered in [Playback Startup
|
||||
| --- | --- |
|
||||
| `src/shared/ipc/contracts.ts` | Canonical channel names and payload type contracts. Single source of truth for both processes. |
|
||||
| `src/shared/ipc/validators.ts` | Runtime payload parsers and type guards. Every `invoke` payload is validated here before the handler runs. |
|
||||
| `src/preload.ts` | Renderer-side bridge. Exposes a typed API surface to the renderer — only approved channels are accessible. |
|
||||
| `src/preload.ts` | Renderer-side bridge. Exposes a typed API surface to the renderer - only approved channels are accessible. |
|
||||
| `src/main/ipc-runtime.ts` | Main-process handler registration and routing. Wires validated channels to domain handlers. |
|
||||
| `src/core/services/ipc.ts` | Service-level invoke handling. Applies guardrails (validation, error wrapping) before calling domain logic. |
|
||||
| `src/core/services/anki-jimaku-ipc.ts` | Integration-specific IPC boundary for Anki and Jimaku operations. |
|
||||
@@ -81,19 +81,19 @@ How these sockets are established during launch is covered in [Playback Startup
|
||||
|
||||
## Contract Rules
|
||||
|
||||
These rules exist to prevent a class of bugs where the renderer and main process silently disagree about message shapes — which surfaces as undefined fields, swallowed errors, or state corruption.
|
||||
These rules exist to prevent a class of bugs where the renderer and main process silently disagree about message shapes - which surfaces as undefined fields, swallowed errors, or state corruption.
|
||||
|
||||
- **Use shared constants.** Channel names come from `contracts.ts`, never ad-hoc literal strings. This makes channels greppable and refactor-safe.
|
||||
- **Validate before handling.** Every `invoke` payload passes through `validators.ts` before reaching domain logic. This catches shape drift at the boundary instead of deep inside a service.
|
||||
- **Return structured failures.** Handlers return `{ ok: false, error: string }` on failure rather than throwing. The renderer can always distinguish success from failure without try/catch.
|
||||
- **Keep payloads narrow.** Send only what the handler needs. Avoid passing entire state objects across the bridge — it couples the renderer to internal main-process structure.
|
||||
- **Keep payloads narrow.** Send only what the handler needs. Avoid passing entire state objects across the bridge - it couples the renderer to internal main-process structure.
|
||||
- **Co-evolve all layers.** When a payload shape changes, update `contracts.ts`, `validators.ts`, `preload.ts`, and the handler in the same commit. Partial updates are treated as bugs.
|
||||
|
||||
## Two Message Patterns
|
||||
|
||||
**Invoke (request/response):** The renderer calls a typed bridge method and awaits a result. The main process validates the payload, runs the handler, and returns a structured response. Used for operations where the renderer needs a result — lookups, config reads, mining actions.
|
||||
**Invoke (request/response):** The renderer calls a typed bridge method and awaits a result. The main process validates the payload, runs the handler, and returns a structured response. Used for operations where the renderer needs a result - lookups, config reads, mining actions.
|
||||
|
||||
**Fire-and-forget (send):** The renderer sends a message with no response. The main process validates and handles it silently. Malformed payloads are dropped. Used for notifications where the renderer doesn't need confirmation — UI state hints, focus events, position updates.
|
||||
**Fire-and-forget (send):** The renderer sends a message with no response. The main process validates and handles it silently. Malformed payloads are dropped. Used for notifications where the renderer doesn't need confirmation - UI state hints, focus events, position updates.
|
||||
|
||||
## Add a New IPC Action
|
||||
|
||||
@@ -108,7 +108,7 @@ These rules exist to prevent a class of bugs where the renderer and main process
|
||||
|
||||
- Prefer runtime/domain composition via `src/main/runtime/composers/*` and `src/main/runtime/domains/*`. IPC handlers should delegate to composers rather than containing orchestration logic.
|
||||
- Route shared mutable state updates through transition helpers in `src/main/state.ts` for migrated domains. Direct mutation from IPC handlers bypasses invariant checks.
|
||||
- Keep IPC handlers thin — they validate, delegate, and return. Business logic belongs in services.
|
||||
- Keep IPC handlers thin - they validate, delegate, and return. Business logic belongs in services.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Jellyfin Integration
|
||||
|
||||
[Jellyfin](https://jellyfin.org) is a free, self-hosted media server — think of it as your own private streaming service for video you own. If you keep your anime on a Jellyfin server, SubMiner can play episodes through mpv with the full mining overlay.
|
||||
[Jellyfin](https://jellyfin.org) is a free, self-hosted media server - think of it as your own private streaming service for video you own. If you keep your anime on a Jellyfin server, SubMiner can play episodes through mpv with the full mining overlay.
|
||||
|
||||
::: tip Who needs this?
|
||||
This page is only relevant if you already run (or have access to) a Jellyfin server. If you watch local files or YouTube, you can skip it. The in-app setup window (`subminer jellyfin`) is the easiest starting point.
|
||||
:::
|
||||
|
||||
SubMiner can act as a **cast-to-device target** for Jellyfin (similar to jellyfin-mpv-shim). Sign in once, turn on discovery, and SubMiner shows up in the "Play on…" / cast menu of any Jellyfin app — web, phone, or TV. Pick an episode, cast it to SubMiner, and it plays in SubMiner's mpv window with the full overlay and Yomitan click-to-lookup.
|
||||
SubMiner can act as a **cast-to-device target** for Jellyfin (similar to jellyfin-mpv-shim). Sign in once, turn on discovery, and SubMiner shows up in the "Play on…" / cast menu of any Jellyfin app - web, phone, or TV. Pick an episode, cast it to SubMiner, and it plays in SubMiner's mpv window with the full overlay and Yomitan click-to-lookup.
|
||||
|
||||
This is the recommended way to use Jellyfin with SubMiner. A terminal-only option is covered in [Launcher playback](#launcher-playback) at the end.
|
||||
|
||||
@@ -28,7 +28,7 @@ Open the tray menu and click **Configure Jellyfin**. In the window that opens, e
|
||||
|
||||
On success, SubMiner:
|
||||
|
||||
- saves an encrypted session token — your password is never stored,
|
||||
- saves an encrypted session token - your password is never stored,
|
||||
- turns the Jellyfin integration on, and
|
||||
- remembers the server and username for next time.
|
||||
|
||||
@@ -38,12 +38,12 @@ Reopen this window any time to switch servers or **Logout**.
|
||||
|
||||
Discovery is what makes SubMiner appear as a cast target. Two ways to enable it:
|
||||
|
||||
- **For the current session** — open the tray menu and tick **Jellyfin Discovery**. (This item appears once you've signed in.)
|
||||
- **Automatically on every launch** — already on by default. After your first sign-in, SubMiner auto-connects to Jellyfin at startup, so the cast target is ready without touching the tray. You can change this under [Settings](#settings).
|
||||
- **For the current session** - open the tray menu and tick **Jellyfin Discovery**. (This item appears once you've signed in.)
|
||||
- **Automatically on every launch** - already on by default. After your first sign-in, SubMiner auto-connects to Jellyfin at startup, so the cast target is ready without touching the tray. You can change this under [Settings](#settings).
|
||||
|
||||
### 4. Cast from any Jellyfin app
|
||||
|
||||
In the Jellyfin web UI or mobile app, start playing something, open the **cast / "Play on"** menu, and pick your device — SubMiner appears there named after your computer's hostname. Playback opens in SubMiner.
|
||||
In the Jellyfin web UI or mobile app, start playing something, open the **cast / "Play on"** menu, and pick your device - SubMiner appears there named after your computer's hostname. Playback opens in SubMiner.
|
||||
|
||||
From then on, pause / resume / seek / stop and audio or subtitle track changes you make in the Jellyfin app are mirrored in SubMiner, and your watch progress syncs back to Jellyfin (now-playing and resume position).
|
||||
|
||||
@@ -63,7 +63,7 @@ All Jellyfin options live under **Settings → Integrations → Jellyfin** (open
|
||||
| Setting | Default | What it does |
|
||||
| ------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Enabled** | Off | Turns the Jellyfin integration on. Switched on for you when you sign in. |
|
||||
| **Server Url** | — | Your Jellyfin server. Filled in when you sign in. |
|
||||
| **Server Url** | - | Your Jellyfin server. Filled in when you sign in. |
|
||||
| **Remote Control Enabled** | On | Lets SubMiner act as a cast target. |
|
||||
| **Remote Control Auto Connect** | On | Connects to Jellyfin at startup so discovery is automatic. Turn off if you'd rather start it from the tray each time. |
|
||||
| **Auto Announce** | Off | Re-broadcasts visibility on connect. Enable if your device is slow to appear in the cast menu. |
|
||||
@@ -88,14 +88,14 @@ See [Configuration](/configuration) for the full list (transcode codec, direct-p
|
||||
**SubMiner doesn't appear in the cast menu**
|
||||
|
||||
- Make sure SubMiner is running.
|
||||
- Make sure you're signed in — reopen **Configure Jellyfin** and log in again if your token expired.
|
||||
- Make sure you're signed in - reopen **Configure Jellyfin** and log in again if your token expired.
|
||||
- Make sure discovery is on (tray **Jellyfin Discovery**, or **Remote Control Auto Connect** in settings).
|
||||
- Make sure SubMiner and the Jellyfin client point at the same server.
|
||||
|
||||
**Casting starts but nothing plays**
|
||||
|
||||
- Confirm the item plays normally in another Jellyfin client.
|
||||
- If mpv was closed, give it a moment — SubMiner launches it on demand and retries.
|
||||
- If mpv was closed, give it a moment - SubMiner launches it on demand and retries.
|
||||
|
||||
**SubMiner keeps disconnecting**
|
||||
|
||||
@@ -104,7 +104,7 @@ See [Configuration](/configuration) for the full list (transcode codec, direct-p
|
||||
## Security notes
|
||||
|
||||
- The Jellyfin session (access token + user ID) is kept in SubMiner's local encrypted token storage. Your password is used only to log in and is never saved.
|
||||
- Treat the token storage and your `config.jsonc` as secrets — don't commit them.
|
||||
- Treat the token storage and your `config.jsonc` as secrets - don't commit them.
|
||||
- Advanced/headless: the `SUBMINER_JELLYFIN_ACCESS_TOKEN` and `SUBMINER_JELLYFIN_USER_ID` environment variables can supply a session without the sign-in window.
|
||||
|
||||
## Launcher playback
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Jimaku Integration
|
||||
|
||||
[Jimaku](https://jimaku.cc) is a community-driven subtitle repository for anime — a shared online library of subtitle files contributed by other learners. SubMiner integrates with the Jimaku API so you can search, browse, and download Japanese subtitle files directly from the overlay — no alt-tabbing or manual file management required. Downloaded subtitles are loaded into mpv immediately.
|
||||
[Jimaku](https://jimaku.cc) is a community-driven subtitle repository for anime - a shared online library of subtitle files contributed by other learners. SubMiner integrates with the Jimaku API so you can search, browse, and download Japanese subtitle files directly from the overlay - no alt-tabbing or manual file management required. Downloaded subtitles are loaded into mpv immediately.
|
||||
|
||||
::: tip Prerequisite: a free API key
|
||||
You need a Jimaku account and an API key (a personal access string) before this feature works. Create an account at [jimaku.cc](https://jimaku.cc), copy your key, and add it to your config as shown under [Configuration](#configuration) below. Without a key, the search modal will report "Jimaku API key not set."
|
||||
@@ -10,14 +10,14 @@ You need a Jimaku account and an API key (a personal access string) before this
|
||||
|
||||
The Jimaku integration runs through an in-overlay modal accessible via a keyboard shortcut (`Ctrl+Shift+J` by default).
|
||||
|
||||
When you open the modal, SubMiner parses the current video filename to extract a title, season, and episode number. Common naming conventions are supported — `S01E03`, `1x03`, `E03`, and dash-separated episode numbers all work. If the filename yields a high-confidence match (title + episode), SubMiner auto-searches immediately.
|
||||
When you open the modal, SubMiner parses the current video filename to extract a title, season, and episode number. Common naming conventions are supported - `S01E03`, `1x03`, `E03`, and dash-separated episode numbers all work. If the filename yields a high-confidence match (title + episode), SubMiner auto-searches immediately.
|
||||
|
||||
From there:
|
||||
|
||||
1. **Search** — SubMiner queries the Jimaku API with the parsed title. Results appear as a list of anime entries (Japanese and English names).
|
||||
2. **Browse entries** — Select an entry to load its available subtitle files, filtered by episode if one was detected.
|
||||
3. **Browse files** — Files show name, size, and last-modified date. If a language preference is configured, files are sorted accordingly (e.g., Japanese-tagged files first).
|
||||
4. **Download** — Selecting a file downloads it to the same directory as the video (or a temp directory for remote/streamed media) and loads it into mpv as a new subtitle track.
|
||||
1. **Search** - SubMiner queries the Jimaku API with the parsed title. Results appear as a list of anime entries (Japanese and English names).
|
||||
2. **Browse entries** - Select an entry to load its available subtitle files, filtered by episode if one was detected.
|
||||
3. **Browse files** - Files show name, size, and last-modified date. If a language preference is configured, files are sorted accordingly (e.g., Japanese-tagged files first).
|
||||
4. **Download** - Selecting a file downloads it to the same directory as the video (or a temp directory for remote/streamed media) and loads it into mpv as a new subtitle track.
|
||||
|
||||
If no files match the current episode filter, a "Show all files" button lets you broaden the search to all episodes for that entry.
|
||||
|
||||
@@ -48,8 +48,8 @@ Add a `jimaku` section to your `config.jsonc`:
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `jimaku.apiKey` | `string` | — | Jimaku API key (plaintext). Mutually exclusive with `apiKeyCommand`. |
|
||||
| `jimaku.apiKeyCommand` | `string` | — | Shell command that prints the API key to stdout. Useful for secret managers (e.g., `pass jimaku/api-key`). |
|
||||
| `jimaku.apiKey` | `string` | - | Jimaku API key (plaintext). Mutually exclusive with `apiKeyCommand`. |
|
||||
| `jimaku.apiKeyCommand` | `string` | - | Shell command that prints the API key to stdout. Useful for secret managers (e.g., `pass jimaku/api-key`). |
|
||||
| `jimaku.apiBaseUrl` | `string` | `"https://jimaku.cc"` | Base URL for the Jimaku API. Only change this if using a mirror or local instance. |
|
||||
| `jimaku.languagePreference` | `"ja"` \| `"en"` \| `"none"` | `"ja"` | Sort subtitle files by language tag. `"ja"` pushes Japanese-tagged files to the top; `"en"` does the same for English. `"none"` preserves the API order. |
|
||||
| `jimaku.maxEntryResults` | `number` | `10` | Maximum number of anime entries returned per search. |
|
||||
@@ -68,8 +68,8 @@ The keyboard shortcut is configured separately under `shortcuts`:
|
||||
|
||||
An API key is required to use the Jimaku integration. You can get one from [jimaku.cc](https://jimaku.cc). There are two ways to provide it:
|
||||
|
||||
- **`apiKey`** — set the key directly in config. Simple, but the key is stored in plaintext.
|
||||
- **`apiKeyCommand`** — a shell command that outputs the key. Runs with a 10-second timeout. Preferred if you use a secret manager like `pass`, `gpg`, or a keychain tool.
|
||||
- **`apiKey`** - set the key directly in config. Simple, but the key is stored in plaintext.
|
||||
- **`apiKeyCommand`** - a shell command that outputs the key. Runs with a 10-second timeout. Preferred if you use a secret manager like `pass`, `gpg`, or a keychain tool.
|
||||
|
||||
If both are set, `apiKey` takes priority.
|
||||
|
||||
@@ -79,8 +79,8 @@ SubMiner extracts media info from the current video path to pre-fill the search
|
||||
|
||||
- **Season + episode patterns:** `S01E03`, `1x03`
|
||||
- **Episode-only patterns:** `E03`, `EP03`, or dash-separated numbers like `Title - 03 -`
|
||||
- **Bracket tags:** `[SubGroup]`, `[1080p]`, `[HEVC]` — stripped before title extraction
|
||||
- **Year tags:** `(2024)` — stripped
|
||||
- **Bracket tags:** `[SubGroup]`, `[1080p]`, `[HEVC]` - stripped before title extraction
|
||||
- **Year tags:** `(2024)` - stripped
|
||||
- **Dots and underscores:** treated as spaces
|
||||
- **Remote/streamed URLs:** SubMiner checks URL query parameters (`title`, `name`, `q`) and path segments to extract a meaningful title
|
||||
|
||||
@@ -98,7 +98,7 @@ The Jimaku API has rate limits. If you see 429 errors, wait for the retry durati
|
||||
|
||||
**No entries found**
|
||||
|
||||
Try simplifying the title — remove season/episode qualifiers and search with just the anime name. Jimaku's search matches against its own database of anime titles, so the exact spelling matters.
|
||||
Try simplifying the title - remove season/episode qualifiers and search with just the anime name. Jimaku's search matches against its own database of anime titles, so the exact spelling matters.
|
||||
|
||||
**No files found for this episode**
|
||||
|
||||
@@ -110,6 +110,6 @@ Verify mpv is running and connected via IPC. SubMiner loads the subtitle by issu
|
||||
|
||||
## Related
|
||||
|
||||
- [Configuration Reference](/configuration#jimaku) — full config options
|
||||
- [Mining Workflow](/mining-workflow#jimaku-subtitle-search) — how Jimaku fits into the sentence mining loop
|
||||
- [Troubleshooting](/troubleshooting#jimaku) — additional error guidance
|
||||
- [Configuration Reference](/configuration#jimaku) - full config options
|
||||
- [Mining Workflow](/mining-workflow#jimaku-subtitle-search) - how Jimaku fits into the sentence mining loop
|
||||
- [Troubleshooting](/troubleshooting#jimaku) - additional error guidance
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
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 recommended way to launch playback is the **SubMiner mpv** shortcut created during first-run setup — double-click it, drag a file onto it, or run `SubMiner.exe --launch-mpv` from a terminal. See [Windows mpv Shortcut](/usage#windows-mpv-shortcut) for details.
|
||||
On Windows, the recommended way to launch playback is the **SubMiner mpv** shortcut created during first-run setup - double-click it, drag a file onto it, or run `SubMiner.exe --launch-mpv` from a terminal. See [Windows mpv Shortcut](/usage#windows-mpv-shortcut) for details.
|
||||
:::
|
||||
|
||||
## Video Picker
|
||||
@@ -121,4 +121,4 @@ With default plugin settings (`auto_start=yes`, `auto_start_visible_overlay=yes`
|
||||
|
||||
- Default log level is `info`
|
||||
- `--background` mode defaults to `warn` unless `--log-level` is explicitly set
|
||||
- `--dev` / `--debug` control app behavior, not logging verbosity — use `--log-level` for that
|
||||
- `--dev` / `--debug` control app behavior, not logging verbosity - use `--log-level` for that
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# Mining Workflow
|
||||
|
||||
This guide walks through the sentence mining loop — from watching a video to creating Anki cards with audio, screenshots, and context.
|
||||
This guide walks through the sentence mining loop - from watching a video to creating Anki cards with audio, screenshots, and context.
|
||||
|
||||
## Overview
|
||||
|
||||
*Sentence mining* means turning real sentences you encounter while watching native video into Anki flashcards, so you learn vocabulary in the context where you actually met it. SubMiner automates the tedious parts of that loop.
|
||||
|
||||
SubMiner runs as a transparent overlay on top of mpv (the video player). As subtitles play, the overlay displays them as interactive text. You hover a word, trigger a Yomitan dictionary lookup with your configured lookup key/modifier, then create an Anki card with a single action. SubMiner automatically attaches the sentence, an audio clip, and a screenshot to that card — no manual copy-pasting or screen capturing.
|
||||
SubMiner runs as a transparent overlay on top of mpv (the video player). As subtitles play, the overlay displays them as interactive text. You hover a word, trigger a Yomitan dictionary lookup with your configured lookup key/modifier, then create an Anki card with a single action. SubMiner automatically attaches the sentence, an audio clip, and a screenshot to that card - no manual copy-pasting or screen capturing.
|
||||
|
||||
> **Yomitan** is the popup dictionary that shows definitions when you hover or scan a word. **AnkiConnect** is the add-on that lets SubMiner talk to Anki. Both are set up during installation — see [Anki Integration](/anki-integration) if you have not configured them yet.
|
||||
> **Yomitan** is the popup dictionary that shows definitions when you hover or scan a word. **AnkiConnect** is the add-on that lets SubMiner talk to Anki. Both are set up during installation - see [Anki Integration](/anki-integration) if you have not configured them yet.
|
||||
|
||||
## Creating Anki Cards
|
||||
|
||||
@@ -39,7 +39,7 @@ If you prefer a hands-on approach (animecards-style), you can copy the current s
|
||||
1. Add a word via Yomitan as usual.
|
||||
2. Press `Ctrl/Cmd+C` to copy the current subtitle line to the clipboard.
|
||||
- For multiple lines: press `Ctrl/Cmd+Shift+C`, then a digit `1`–`9` to select how many recent subtitle lines to combine. The combined text is copied to the clipboard.
|
||||
3. Press `Ctrl/Cmd+V` to update the last-added card with the clipboard contents plus audio, image, and translation — the same fields auto-update would fill.
|
||||
3. Press `Ctrl/Cmd+V` to update the last-added card with the clipboard contents plus audio, image, and translation - the same fields auto-update would fill.
|
||||
|
||||
Manual clipboard updates always replace generated sentence audio, even when `ankiConnect.behavior.overwriteAudio` is disabled. The word audio field is left unchanged because the word itself does not change in this flow.
|
||||
|
||||
@@ -61,7 +61,7 @@ Create a standalone sentence card without going through Yomitan:
|
||||
The sentence card uses the note type configured in `isLapis.sentenceCardModel` and always maps sentence/audio to `Sentence` and `SentenceAudio`.
|
||||
|
||||
::: warning Requires Lapis/Kiku note type
|
||||
Sentence card creation requires a [Lapis](https://github.com/donkuri/lapis) or [Kiku](https://github.com/youyoumu/kiku) compatible note type and `ankiConnect.isLapis.enabled: true` in your config. See [Anki Integration — Sentence Cards](/anki-integration#sentence-cards-lapis) for setup.
|
||||
Sentence card creation requires a [Lapis](https://github.com/donkuri/lapis) or [Kiku](https://github.com/youyoumu/kiku) compatible note type and `ankiConnect.isLapis.enabled: true` in your config. See [Anki Integration - Sentence Cards](/anki-integration#sentence-cards-lapis) for setup.
|
||||
:::
|
||||
|
||||
### 4. Mark as Audio Card
|
||||
@@ -69,7 +69,7 @@ Sentence card creation requires a [Lapis](https://github.com/donkuri/lapis) or [
|
||||
After adding a word via Yomitan, press the audio card shortcut to overwrite the audio with a longer clip spanning the full subtitle timing.
|
||||
|
||||
::: warning Requires Lapis/Kiku note type
|
||||
Audio card marking requires a [Lapis](https://github.com/donkuri/lapis) or [Kiku](https://github.com/youyoumu/kiku) compatible note type and `ankiConnect.isLapis.enabled: true` in your config. See [Anki Integration — Sentence Cards](/anki-integration#sentence-cards-lapis) for setup.
|
||||
Audio card marking requires a [Lapis](https://github.com/donkuri/lapis) or [Kiku](https://github.com/youyoumu/kiku) compatible note type and `ankiConnect.isLapis.enabled: true` in your config. See [Anki Integration - Sentence Cards](/anki-integration#sentence-cards-lapis) for setup.
|
||||
:::
|
||||
|
||||
### Field Grouping (Kiku)
|
||||
@@ -82,11 +82,11 @@ If you mine the same word from different sentences, SubMiner can merge the cards
|
||||
- **Auto mode** (`ankiConnect.isKiku.fieldGrouping: "auto"`): Merges automatically. Both sentences, audio clips, and images are combined into the existing card. The duplicate is optionally deleted.
|
||||
- **Manual mode** (`ankiConnect.isKiku.fieldGrouping: "manual"`): A modal appears showing both cards side by side. You choose which card to keep and preview the merged result before confirming.
|
||||
|
||||
See [Anki Integration — Field Grouping](/anki-integration#field-grouping-kiku) for configuration options, merge behavior, and modal keyboard shortcuts.
|
||||
See [Anki Integration - Field Grouping](/anki-integration#field-grouping-kiku) for configuration options, merge behavior, and modal keyboard shortcuts.
|
||||
|
||||
## Overlay Model
|
||||
|
||||
SubMiner uses one overlay window with modal surfaces. It carries two subtitle bars — a primary reading bar and a secondary translation/context bar — plus modal dialogs that open on top.
|
||||
SubMiner uses one overlay window with modal surfaces. It carries two subtitle bars - a primary reading bar and a secondary translation/context bar - plus modal dialogs that open on top.
|
||||
|
||||
Toggle the entire overlay window with `Alt+Shift+O` (global) or `y-t` (mpv plugin).
|
||||
|
||||
@@ -99,14 +99,14 @@ The primary bar renders subtitles as tokenized hoverable word spans. Each word i
|
||||
- Auto pause/resume while the Yomitan popup is open (enabled by default via `subtitleStyle.autoPauseVideoOnYomitanPopup`)
|
||||
- Right-click to pause/resume
|
||||
- Right-click + drag to reposition subtitles
|
||||
- **Reading annotations** — known words, N+1 targets, character-name matches, JLPT levels, and frequency hits can all be visually highlighted
|
||||
- **Reading annotations** - known words, N+1 targets, character-name matches, JLPT levels, and frequency hits can all be visually highlighted
|
||||
|
||||
### Secondary Subtitle Bar
|
||||
|
||||
The secondary bar is a compact top-strip region in the same overlay window. It shows a secondary subtitle track (typically English) for translation/context while keeping the primary reading flow below. It is useful for:
|
||||
|
||||
- Quick comprehension checks without leaving the mining flow.
|
||||
- Auto-populating the translation field on mined cards — when a card is created, SubMiner uses the secondary subtitle text as the translation field value (unless AI translation is configured to override it).
|
||||
- Auto-populating the translation field on mined cards - when a card is created, SubMiner uses the secondary subtitle text as the translation field value (unless AI translation is configured to override it).
|
||||
|
||||
It is controlled by `secondarySub` configuration and shares its lifecycle with the main overlay window. Cycle which track feeds it with `Shift+J`.
|
||||
|
||||
@@ -114,9 +114,9 @@ It is controlled by `secondarySub` configuration and shares its lifecycle with t
|
||||
|
||||
Both the primary and secondary subtitle bars share the same three visibility modes, and each can be changed independently at runtime:
|
||||
|
||||
- **Hidden** — the bar is not shown.
|
||||
- **Visible** — the bar is always shown.
|
||||
- **Hover** — the bar is revealed only while you hover over the overlay.
|
||||
- **Hidden** - the bar is not shown.
|
||||
- **Visible** - the bar is always shown.
|
||||
- **Hover** - the bar is revealed only while you hover over the overlay.
|
||||
|
||||
By default the **primary** bar is `visible` (`subtitleStyle.primaryDefaultMode`) and the **secondary** bar is `hover` (`secondarySub.defaultMode`).
|
||||
|
||||
@@ -133,7 +133,7 @@ Jimaku search, field-grouping, runtime options, and manual subsync open as modal
|
||||
|
||||
## Looking Up Words
|
||||
|
||||
1. Hover over the subtitle area — the overlay activates pointer events.
|
||||
1. Hover over the subtitle area - the overlay activates pointer events.
|
||||
2. Hover the word you want. SubMiner keeps per-token boundaries so Yomitan can target that token cleanly.
|
||||
3. Trigger Yomitan lookup with your configured lookup key/modifier (for example `Shift` if that is how your Yomitan profile is set up).
|
||||
4. Yomitan opens its lookup popup for the hovered token.
|
||||
@@ -143,17 +143,17 @@ Jimaku search, field-grouping, runtime options, and manual subsync open as modal
|
||||
|
||||
With a gamepad connected and keyboard-only mode enabled, the full mining loop works without a mouse or keyboard:
|
||||
|
||||
1. **Navigate** — push the left stick left/right to move the token highlight across subtitle words.
|
||||
2. **Look up** — press `A` to trigger Yomitan lookup on the highlighted word.
|
||||
3. **Browse the popup** — push the left stick up/down to smooth-scroll through the Yomitan popup, or use the right stick for larger jumps.
|
||||
4. **Cycle audio** — press `R1` to move to the next dictionary audio entry, `L1` to play the current one.
|
||||
5. **Mine** — press `X` to create an Anki card for the current sentence (same as `Ctrl+S`).
|
||||
6. **Close** — press `B` to dismiss the Yomitan popup and return to subtitle navigation.
|
||||
7. **Pause/resume** — press `L3` (left stick click) to toggle mpv pause at any time.
|
||||
1. **Navigate** - push the left stick left/right to move the token highlight across subtitle words.
|
||||
2. **Look up** - press `A` to trigger Yomitan lookup on the highlighted word.
|
||||
3. **Browse the popup** - push the left stick up/down to smooth-scroll through the Yomitan popup, or use the right stick for larger jumps.
|
||||
4. **Cycle audio** - press `R1` to move to the next dictionary audio entry, `L1` to play the current one.
|
||||
5. **Mine** - press `X` to create an Anki card for the current sentence (same as `Ctrl+S`).
|
||||
6. **Close** - press `B` to dismiss the Yomitan popup and return to subtitle navigation.
|
||||
7. **Pause/resume** - press `L3` (left stick click) to toggle mpv pause at any time.
|
||||
|
||||
After controller support is enabled, the controller and keyboard can be used interchangeably — switching mid-session is seamless. Toggle keyboard-only mode on or off with `Y` on the controller.
|
||||
After controller support is enabled, the controller and keyboard can be used interchangeably - switching mid-session is seamless. Toggle keyboard-only mode on or off with `Y` on the controller.
|
||||
|
||||
See [Usage — Controller Support](/usage#controller-support) for setup details and [Configuration — Controller Support](/configuration#controller-support) for the full mapping and tuning options.
|
||||
See [Usage - Controller Support](/usage#controller-support) for setup details and [Configuration - Controller Support](/configuration#controller-support) for the full mapping and tuning options.
|
||||
|
||||
## Subtitle Sync (Subsync)
|
||||
|
||||
@@ -166,11 +166,11 @@ If your subtitle file is out of sync with the audio, SubMiner can resynchronize
|
||||
|
||||
For remote streams, including Jellyfin playback, the modal only offers alass. Jellyfin subtitle URLs are cached as temporary subtitle files so alass can read them, but the video stream is not downloaded. ffsubsync needs direct access to the local media file and is unavailable for stream URLs.
|
||||
|
||||
Install the sync tools separately — see [Troubleshooting](/troubleshooting#subtitle-sync-subsync) if the tools are not found.
|
||||
Install the sync tools separately - see [Troubleshooting](/troubleshooting#subtitle-sync-subsync) if the tools are not found.
|
||||
|
||||
## Texthooker
|
||||
|
||||
SubMiner runs a local HTTP server at `http://127.0.0.1:5174` (configurable port) that serves a texthooker UI. This allows external tools — such as a browser-based Yomitan instance — to receive subtitle text in real time.
|
||||
SubMiner runs a local HTTP server at `http://127.0.0.1:5174` (configurable port) that serves a texthooker UI. This allows external tools - such as a browser-based Yomitan instance - to receive subtitle text in real time.
|
||||
|
||||
The texthooker page displays the current subtitle and updates as new lines arrive. This is useful if you prefer to do lookups in a browser rather than through the overlay's built-in Yomitan.
|
||||
|
||||
@@ -180,8 +180,8 @@ If you want to build your own browser client, websocket consumer, or automation
|
||||
|
||||
These features support the mining loop but have their own dedicated pages:
|
||||
|
||||
- **[Jimaku subtitle search](/jimaku-integration)** — search and download anime subtitle files directly from the overlay (`Ctrl+Shift+J` by default), then load them into mpv.
|
||||
- **[N+1 word highlighting](/subtitle-annotations#n1-word-highlighting)** — cross-reference your Anki decks to highlight known words, making true N+1 sentences (exactly one unknown word) easy to spot during immersion.
|
||||
- **[Immersion tracking](/immersion-tracking)** — log watching and mining activity to a local database and view session times, words seen, and cards mined in the built-in stats dashboard.
|
||||
- **[Jimaku subtitle search](/jimaku-integration)** - search and download anime subtitle files directly from the overlay (`Ctrl+Shift+J` by default), then load them into mpv.
|
||||
- **[N+1 word highlighting](/subtitle-annotations#n1-word-highlighting)** - cross-reference your Anki decks to highlight known words, making true N+1 sentences (exactly one unknown word) easy to spot during immersion.
|
||||
- **[Immersion tracking](/immersion-tracking)** - log watching and mining activity to a local database and view session times, words seen, and cards mined in the built-in stats dashboard.
|
||||
|
||||
Next: [Anki Integration](/anki-integration) — field mapping, media generation, and card enrichment configuration.
|
||||
Next: [Anki Integration](/anki-integration) - field mapping, media generation, and card enrichment configuration.
|
||||
|
||||
+13
-13
@@ -2,7 +2,7 @@
|
||||
|
||||
**What this is:** mpv is the video player SubMiner overlays subtitles on. The SubMiner mpv plugin is a small Lua script that runs *inside* mpv and gives you in-player keybindings to control the SubMiner overlay (start/stop/toggle, skip intro, etc.) without leaving the player window.
|
||||
|
||||
**Who needs this page:** Most users never touch the plugin directly — SubMiner-managed launches (the app, the `subminer` launcher, or the Windows shortcut) inject the bundled plugin automatically for that session, so there is nothing to install into mpv's global `scripts` directory. Read on if you launch mpv from another tool and want SubMiner's in-player controls, or you want to script mpv against SubMiner.
|
||||
**Who needs this page:** Most users never touch the plugin directly - SubMiner-managed launches (the app, the `subminer` launcher, or the Windows shortcut) inject the bundled plugin automatically for that session, so there is nothing to install into mpv's global `scripts` directory. Read on if you launch mpv from another tool and want SubMiner's in-player controls, or you want to script mpv against SubMiner.
|
||||
|
||||
The plugin ships as a modular Lua package under `plugin/subminer/` (entry point `init.lua`, which loads `main.lua` and sibling modules). Earlier releases shipped a single global `main.lua`; runtime loading replaces it.
|
||||
|
||||
@@ -27,7 +27,7 @@ input-ipc-server=\\.\pipe\subminer-socket
|
||||
|
||||
## Keybindings
|
||||
|
||||
Most plugin actions use a `y` chord prefix — press `y`, then the second key (a "chord"):
|
||||
Most plugin actions use a `y` chord prefix - press `y`, then the second key (a "chord"):
|
||||
|
||||
| Chord | Action |
|
||||
| ---------------- | -------------------------------------- |
|
||||
@@ -48,7 +48,7 @@ The bare `v` binding is a forced mpv binding. It overrides mpv's default primary
|
||||
|
||||
## Shared Shortcuts (Session Bindings)
|
||||
|
||||
The `y-*` chords above are built into the plugin. Everything else you configure under [`shortcuts.*`](/shortcuts) — plus any custom [`keybindings`](/configuration) and the stats toggle/mark-watched keys — is **injected into mpv at runtime**, so the same shortcut works both inside mpv and in the SubMiner overlay. You do not edit any mpv config to enable them.
|
||||
The `y-*` chords above are built into the plugin. Everything else you configure under [`shortcuts.*`](/shortcuts) - plus any custom [`keybindings`](/configuration) and the stats toggle/mark-watched keys - is **injected into mpv at runtime**, so the same shortcut works both inside mpv and in the SubMiner overlay. You do not edit any mpv config to enable them.
|
||||
|
||||
How it works:
|
||||
|
||||
@@ -58,11 +58,11 @@ How it works:
|
||||
|
||||
Because the bindings come from the same configuration the overlay uses, you maintain one set of shortcuts for both surfaces.
|
||||
|
||||
Live updates: changing a shortcut in the app rewrites `session-bindings.json` and sends the plugin a `subminer-reload-session-bindings` script message, so mpv re-registers the bindings immediately — no mpv restart required.
|
||||
Live updates: changing a shortcut in the app rewrites `session-bindings.json` and sends the plugin a `subminer-reload-session-bindings` script message, so mpv re-registers the bindings immediately - no mpv restart required.
|
||||
|
||||
Notes:
|
||||
|
||||
- Accelerators are normalized per platform — `CommandOrControl` resolves to `Cmd` on macOS and `Ctrl` elsewhere.
|
||||
- Accelerators are normalized per platform - `CommandOrControl` resolves to `Cmd` on macOS and `Ctrl` elsewhere.
|
||||
- Multi-line actions (`copySubtitleMultiple`, `mineSentenceMultiple`) register temporary `1`–`9` digit follow-up bindings after the trigger key, with `Esc` to cancel.
|
||||
- If two shortcuts compile to the same key, or an accelerator can't be mapped to an mpv key, the app logs a warning and skips that binding instead of registering a broken one.
|
||||
|
||||
@@ -110,14 +110,14 @@ Packaged Windows plugin installs also rewrite `socket_path` to `\\.\pipe\submine
|
||||
|
||||
When `backend=auto`, the plugin detects the window manager:
|
||||
|
||||
1. **macOS** — detected via platform or `OSTYPE`.
|
||||
2. **Hyprland** — detected via `HYPRLAND_INSTANCE_SIGNATURE`.
|
||||
3. **Sway** — detected via `SWAYSOCK`.
|
||||
4. **X11** — detected via `XDG_SESSION_TYPE=x11` or `DISPLAY`.
|
||||
5. **Fallback** — defaults to X11 with a warning.
|
||||
1. **macOS** - detected via platform or `OSTYPE`.
|
||||
2. **Hyprland** - detected via `HYPRLAND_INSTANCE_SIGNATURE`.
|
||||
3. **Sway** - detected via `SWAYSOCK`.
|
||||
4. **X11** - detected via `XDG_SESSION_TYPE=x11` or `DISPLAY`.
|
||||
5. **Fallback** - defaults to X11 with a warning.
|
||||
|
||||
::: tip Wayland is compositor-specific
|
||||
Native Wayland support is only available for Hyprland and Sway. If you use a different Wayland compositor, auto-detection will fall back to X11 — both mpv and SubMiner must be running under Xwayland, and `xdotool` and `xwininfo` must be installed.
|
||||
Native Wayland support is only available for Hyprland and Sway. If you use a different Wayland compositor, auto-detection will fall back to X11 - both mpv and SubMiner must be running under Xwayland, and `xdotool` and `xwininfo` must be installed.
|
||||
:::
|
||||
|
||||
## Script Messages
|
||||
@@ -163,7 +163,7 @@ script-message subminer-start backend=hyprland socket=/custom/path texthooker=no
|
||||
|
||||
## Lifecycle
|
||||
|
||||
For how the plugin's auto-start fits into the full launch sequence — including when the launcher starts the overlay instead of the plugin — see [Playback Startup Flow](./architecture#playback-startup-flow).
|
||||
For how the plugin's auto-start fits into the full launch sequence - including when the launcher starts the overlay instead of the plugin - see [Playback Startup Flow](./architecture#playback-startup-flow).
|
||||
|
||||
- **File loaded**: If `auto_start=yes`, the plugin starts the overlay, then defers AniSkip lookup until after startup delay.
|
||||
- **Auto-start pause gate**: If `auto_start_visible_overlay=yes` and `auto_start_pause_until_ready=yes`, launcher starts mpv paused and the plugin resumes playback after SubMiner reports tokenization-ready (with timeout fallback).
|
||||
@@ -181,4 +181,4 @@ The plugin is useful when you:
|
||||
- Want on-demand overlay control without the wrapper.
|
||||
- Use mpv's built-in file browser or playlist features.
|
||||
|
||||
You can install both — the plugin provides chord keybindings for convenience, while the wrapper handles the full lifecycle.
|
||||
You can install both - the plugin provides chord keybindings for convenience, while the wrapper handles the full lifecycle.
|
||||
|
||||
@@ -380,7 +380,7 @@
|
||||
"word-spacing": "0", // Word spacing setting.
|
||||
"font-kerning": "normal", // Font kerning setting.
|
||||
"text-rendering": "geometricPrecision", // Text rendering setting.
|
||||
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
|
||||
"text-shadow": "-1px -1px 2px rgba(0,0,0,0.95), 1px -1px 2px rgba(0,0,0,0.95), -1px 1px 2px rgba(0,0,0,0.95), 1px 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5)", // Text shadow setting.
|
||||
"backdrop-filter": "blur(6px)", // Backdrop filter setting.
|
||||
"--subtitle-hover-token-color": "#f4dbd6", // Subtitle hover token color setting.
|
||||
"--subtitle-hover-token-background-color": "transparent" // Subtitle hover token background color setting.
|
||||
@@ -405,7 +405,7 @@
|
||||
"frequencyDictionary": {
|
||||
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
|
||||
"sourcePath": "", // Optional absolute path to a frequency dictionary directory. If empty, built-in discovery search paths are used.
|
||||
"topX": 1000, // Only color tokens with frequency rank <= topX (default: 1000).
|
||||
"topX": 10000, // Only color tokens with frequency rank <= topX (default: 10000).
|
||||
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
|
||||
"matchMode": "headword", // headword: frequency lookup uses dictionary form. surface: lookup uses subtitle-visible token text. Values: headword | surface
|
||||
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
|
||||
@@ -430,7 +430,7 @@
|
||||
"word-spacing": "0", // Word spacing setting.
|
||||
"font-kerning": "normal", // Font kerning setting.
|
||||
"text-rendering": "geometricPrecision", // Text rendering setting.
|
||||
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
|
||||
"text-shadow": "-1px -1px 2px rgba(0,0,0,0.95), 1px -1px 2px rgba(0,0,0,0.95), -1px 1px 2px rgba(0,0,0,0.95), 1px 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5)", // Text shadow setting.
|
||||
"backdrop-filter": "blur(6px)" // Backdrop filter setting.
|
||||
} // CSS declaration object applied to secondary subtitles after normal subtitle style defaults.
|
||||
} // Secondary setting.
|
||||
|
||||
+12
-7
@@ -1,12 +1,12 @@
|
||||
# Keyboard Shortcuts
|
||||
|
||||
This page is the complete reference for every keystroke SubMiner responds to. If you are just getting started, focus on the **Mining Shortcuts** and **Overlay Controls** sections — those cover the day-to-day mining loop. The rest can wait until you need them.
|
||||
This page is the complete reference for every keystroke SubMiner responds to. If you are just getting started, focus on the **Mining Shortcuts** and **Overlay Controls** sections - those cover the day-to-day mining loop. The rest can wait until you need them.
|
||||
|
||||
A few terms used throughout:
|
||||
|
||||
- **Overlay** — the transparent SubMiner window that sits on top of mpv and shows the interactive subtitles. Most shortcuts only work while this window has focus (click the video once if a shortcut seems to do nothing).
|
||||
- **`Ctrl/Cmd`** — use `Ctrl` on Windows/Linux and `Cmd` (⌘) on macOS. In the config file this is written as `CommandOrControl`.
|
||||
- **Accelerator** — Electron's name for a shortcut string like `Alt+Shift+O`.
|
||||
- **Overlay** - the transparent SubMiner window that sits on top of mpv and shows the interactive subtitles. Most shortcuts only work while this window has focus (click the video once if a shortcut seems to do nothing).
|
||||
- **`Ctrl/Cmd`** - use `Ctrl` on Windows/Linux and `Cmd` (⌘) on macOS. In the config file this is written as `CommandOrControl`.
|
||||
- **Accelerator** - Electron's name for a shortcut string like `Alt+Shift+O`.
|
||||
|
||||
All shortcuts are configurable in `config.jsonc` under `shortcuts` and `keybindings`. Set any shortcut to `null` to disable it.
|
||||
|
||||
@@ -67,7 +67,7 @@ These control playback and subtitle display. They require overlay window focus.
|
||||
| `Right-click + drag` | Reposition subtitles (on subtitle area) |
|
||||
| `Ctrl/Cmd+A` | Append clipboard video path to mpv playlist |
|
||||
|
||||
These keybindings can be overridden or disabled via the `keybindings` config array. The playlist browser opens a split overlay modal with sibling video files on the left and the live mpv playlist on the right.
|
||||
The mpv-command rows above (`Space`, `F`, `J`, `Shift+J`, the seek/sub-seek/sub-delay keys, replay/play-next, and quit) are merged from the `keybindings` config array and can be remapped or disabled there. `V`, `Ctrl/Cmd+A`, and the mouse actions are built-in overlay behaviors and are not part of the `keybindings` array. The playlist browser opens a split overlay modal with sibling video files on the left and the live mpv playlist on the right.
|
||||
|
||||
On macOS managed playback, SubMiner disables mpv's menu-bar shortcuts so configured SubMiner shortcuts like `Cmd+Shift+O` reach the mpv plugin instead of opening native mpv menu actions.
|
||||
|
||||
@@ -86,6 +86,7 @@ Mouse-hover playback behavior is configured separately from shortcuts: `subtitle
|
||||
| `Ctrl+Alt+S` | Open subtitle sync (subsync) modal | `shortcuts.triggerSubsync` |
|
||||
| `\` | Toggle subtitle sidebar | `subtitleSidebar.toggleKey` |
|
||||
| `` ` `` | Toggle stats overlay | `stats.toggleKey` |
|
||||
| `W` | Mark current video watched and advance to next in queue | `stats.markWatchedKey` |
|
||||
|
||||
The stats toggle is handled inside the focused visible overlay window. It is configurable through the top-level `stats.toggleKey` setting and defaults to `Backquote`.
|
||||
|
||||
@@ -104,7 +105,7 @@ Controller input only drives the overlay while keyboard-only mode is enabled. Th
|
||||
|
||||
## MPV Plugin Chords
|
||||
|
||||
When the mpv plugin is installed, all commands use a `y` chord prefix — press `y`, then the second key within 1 second.
|
||||
When the mpv plugin is installed, all commands use a `y` chord prefix - press `y`, then the second key within 1 second.
|
||||
|
||||
| Chord | Action |
|
||||
| ----- | -------------------------------------- |
|
||||
@@ -151,9 +152,13 @@ The `keybindings` array overrides or extends the overlay's built-in key handling
|
||||
"keybindings": [
|
||||
{ "key": "f", "command": ["cycle", "fullscreen"] },
|
||||
{ "key": "m", "command": ["cycle", "mute"] },
|
||||
{ "key": "MBTN_BACK", "command": ["sub-seek", -1] },
|
||||
{ "key": "MBTN_FORWARD", "command": ["sub-seek", 1] },
|
||||
{ "key": "Space", "command": null }, // disable default Space → pause
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Both `shortcuts`, `keybindings`, and `subtitleSidebar` are [hot-reloadable](/configuration#hot-reload-behavior) — changes take effect without restarting SubMiner.
|
||||
Mouse keybinding names are `MBTN_LEFT`, `MBTN_MID`, `MBTN_RIGHT`, `MBTN_BACK`, and `MBTN_FORWARD`.
|
||||
|
||||
Both `shortcuts`, `keybindings`, and `subtitleSidebar` are [hot-reloadable](/configuration#hot-reload-behavior) - changes take effect without restarting SubMiner.
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
SubMiner annotates subtitle tokens in real time as they appear in the overlay. Four annotation layers work together to surface useful context while you watch: **N+1 highlighting**, **character-name highlighting**, **frequency highlighting**, and **JLPT tagging**.
|
||||
|
||||
All four are opt-in and configured under `subtitleStyle`, `ankiConnect.knownWords`, and `ankiConnect.nPlusOne` in your config. They apply independently — you can enable any combination.
|
||||
All four are opt-in and configured under `subtitleStyle`, `ankiConnect.knownWords`, and `ankiConnect.nPlusOne` in your config. They apply independently - you can enable any combination.
|
||||
|
||||
::: tip Tokenization
|
||||
SubMiner's primary tokenizer is Yomitan itself - subtitle text is tokenized based entirely on the dictionaries you have installed in Yomitan. Installing many large dictionaries can increase noise and slow down lookups, so be selective about which dictionaries you install and their priority order.
|
||||
:::
|
||||
|
||||
Before any of those layers render, SubMiner strips annotation metadata from tokens that are usually just subtitle glue or annotation noise. Standalone particles, auxiliaries, adnominals, common explanatory endings like `んです` / `のだ`, merged trailing quote-particle forms like `...って`, auxiliary-stem grammar tails like `そうだ` (MeCab POS3 `助動詞語幹`), repeated kana interjections, and similar non-lexical helper tokens remain hoverable in the subtitle text, but they render as plain tokens without known-word, N+1, frequency, JLPT, or name-match annotation styling.
|
||||
|
||||
@@ -39,7 +43,7 @@ Set `refreshMinutes` to `1440` (24 hours) for daily sync if your Anki collection
|
||||
|
||||
## Character-Name Highlighting
|
||||
|
||||
Character-name matches are built from the active merged SubMiner character dictionary, which auto-syncs character data from AniList for your recently-watched titles. When the current AniList media ID is known, SubMiner ignores loaded entries from other titles for subtitle name matching and inline portraits. Matching names are highlighted in subtitles and become available for hover-driven Yomitan character profiles — portraits, roles, voice actors, and biographical detail.
|
||||
Character-name matches are built from the active merged SubMiner character dictionary, which auto-syncs character data from AniList for your recently-watched titles. When the current AniList media ID is known, SubMiner ignores loaded entries from other titles for subtitle name matching and inline portraits. Matching names are highlighted in subtitles and become available for hover-driven Yomitan character profiles - portraits, roles, voice actors, and biographical detail.
|
||||
|
||||
**How it works:**
|
||||
|
||||
@@ -60,12 +64,12 @@ For full details on dictionary generation, name variant expansion, auto-sync lif
|
||||
|
||||
## Frequency Highlighting
|
||||
|
||||
Frequency highlighting colors tokens based on how common they are, using dictionary frequency rank data. This helps you spot high-value vocabulary at a glance.
|
||||
Frequency highlighting colors tokens based on how common they are, using dictionary frequency rank data. This helps you spot high-value vocabulary at a glance. Frequency ranks are sourced from the **highest-ranked frequency dictionary** installed in Yomitan - other frequency dictionaries are not consulted.
|
||||
|
||||
**Modes:**
|
||||
|
||||
- **Single** — all highlighted tokens share one color (`singleColor`).
|
||||
- **Banded** — tokens are assigned to five color bands from most common to least common within the `topX` window.
|
||||
- **Single** - all highlighted tokens share one color (`singleColor`).
|
||||
- **Banded** - tokens are assigned to five color bands from most common to least common within the `topX` window.
|
||||
|
||||
SubMiner looks up each token's `frequencyRank` from `term_meta_bank_*.json` files. Only tokens with a positive rank at or below `topX` are highlighted.
|
||||
|
||||
@@ -74,7 +78,7 @@ SubMiner looks up each token's `frequencyRank` from `term_meta_bank_*.json` file
|
||||
| Option | Default | Description |
|
||||
| ------------------------------------------------ | ------------ | ---------------------------------------------------------------- |
|
||||
| `subtitleStyle.frequencyDictionary.enabled` | `false` | Enable frequency highlighting |
|
||||
| `subtitleStyle.frequencyDictionary.topX` | `1000` | Max frequency rank to highlight |
|
||||
| `subtitleStyle.frequencyDictionary.topX` | `10000` | Max frequency rank to highlight |
|
||||
| `subtitleStyle.frequencyDictionary.mode` | `"single"` | `"single"` or `"banded"` |
|
||||
| `subtitleStyle.frequencyDictionary.matchMode` | `"headword"` | `"headword"` or `"surface"` |
|
||||
| `subtitleStyle.frequencyDictionary.singleColor` | `#f5a97f` | Color for single mode |
|
||||
@@ -130,14 +134,14 @@ All annotation layers can be toggled at runtime via the mpv command menu without
|
||||
- `subtitleStyle.enableJlpt` (`On` / `Off`)
|
||||
- `subtitleStyle.frequencyDictionary.enabled` (`On` / `Off`)
|
||||
|
||||
Toggles only apply to new subtitle lines after the change — the currently displayed line is not re-tokenized in place.
|
||||
Toggles only apply to new subtitle lines after the change - the currently displayed line is not re-tokenized in place.
|
||||
|
||||
## Rendering Priority
|
||||
|
||||
When multiple annotations apply to the same token, the visual priority is:
|
||||
|
||||
1. **N+1 target** (highest) — the single unknown word in an N+1 sentence
|
||||
2. **Character-name match** — dictionary-driven character-name token styling
|
||||
3. **Known-word color** — already-learned token tint
|
||||
4. **Frequency highlight** — common-word coloring (not applied when N+1/character-name/known-word already matched)
|
||||
5. **JLPT underline** — level-based underline (stacks with the above since it uses underline rather than text color)
|
||||
1. **N+1 target** (highest) - the single unknown word in an N+1 sentence
|
||||
2. **Character-name match** - dictionary-driven character-name token styling
|
||||
3. **Known-word color** - already-learned token tint
|
||||
4. **Frequency highlight** - common-word coloring (not applied when N+1/character-name/known-word already matched)
|
||||
5. **JLPT underline** - level-based underline (stacks with the above since it uses underline rather than text color)
|
||||
|
||||
@@ -10,7 +10,7 @@ When SubMiner parses the active subtitle source into a cue list, the sidebar bec
|
||||
|
||||
- The active cue is highlighted and kept in view as playback advances (when `autoScroll` is `true`).
|
||||
- Clicking any cue seeks mpv to that timestamp.
|
||||
- The sidebar stays synchronized with the overlay — media transitions and subtitle source changes update both simultaneously.
|
||||
- The sidebar stays synchronized with the overlay - media transitions and subtitle source changes update both simultaneously.
|
||||
|
||||
The sidebar only appears when a parsed cue list is available. External subtitle sources that SubMiner cannot parse (for example, embedded ASS tracks rendered directly by mpv) will not populate the sidebar.
|
||||
|
||||
@@ -18,9 +18,9 @@ The sidebar only appears when a parsed cue list is available. External subtitle
|
||||
|
||||
Two layout modes are available via `subtitleSidebar.layout`:
|
||||
|
||||
**`overlay`** (default) — The sidebar floats over mpv as a panel. It does not affect the player window size or position.
|
||||
**`overlay`** (default) - The sidebar floats over mpv as a panel. It does not affect the player window size or position.
|
||||
|
||||
**`embedded`** — Reserves space on the right side of the player and shifts the video area to mimic a split-pane layout. Useful if you want the cue list visible without it covering the video. If you see unexpected positioning in your environment, switch back to `overlay` to isolate the issue.
|
||||
**`embedded`** - Reserves space on the right side of the player and shifts the video area to mimic a split-pane layout. Useful if you want the cue list visible without it covering the video. If you see unexpected positioning in your environment, switch back to `overlay` to isolate the issue.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -35,30 +35,45 @@ Enable and configure the sidebar under `subtitleSidebar` in your config file:
|
||||
"toggleKey": "Backslash",
|
||||
"pauseVideoOnHover": true,
|
||||
"autoScroll": true,
|
||||
"fontFamily": "\"M PLUS 1\", \"Noto Sans CJK JP\", sans-serif",
|
||||
"fontSize": 16
|
||||
"css": {
|
||||
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP",
|
||||
"color": "#cad3f5",
|
||||
"background-color": "rgba(73, 77, 100, 0.9)",
|
||||
"font-size": "16px",
|
||||
"opacity": "0.95",
|
||||
"--subtitle-sidebar-max-width": "420px",
|
||||
"--subtitle-sidebar-timestamp-color": "#a5adcb",
|
||||
"--subtitle-sidebar-active-line-color": "#f5bde6",
|
||||
"--subtitle-sidebar-active-background-color": "rgba(138, 173, 244, 0.22)",
|
||||
"--subtitle-sidebar-hover-background-color": "rgba(54, 58, 79, 0.84)"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| --------------------------- | ------- | ------------ | -------------------------------------------------------------------------------------------------- |
|
||||
| `enabled` | boolean | `true` | Enable subtitle sidebar support |
|
||||
| `autoOpen` | boolean | `false` | Open the sidebar automatically on overlay startup |
|
||||
| `layout` | string | `"overlay"` | `"overlay"` floats over mpv; `"embedded"` reserves right-side player space |
|
||||
| `toggleKey` | string | `"Backslash"` | `KeyboardEvent.code` for the toggle shortcut |
|
||||
| `pauseVideoOnHover` | boolean | `true` | Pause playback while hovering the cue list |
|
||||
| `autoScroll` | boolean | `true` | Keep the active cue in view during playback |
|
||||
| `maxWidth` | number | `420` | Maximum sidebar width in CSS pixels |
|
||||
| `opacity` | number | `0.95` | Sidebar opacity between `0` and `1` |
|
||||
| `backgroundColor` | string | `rgba(73, 77, 100, 0.9)` | Sidebar shell background color |
|
||||
| `textColor` | string | `#cad3f5` | Default cue text color |
|
||||
| `fontFamily` | string | `Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP` | CSS `font-family` applied to cue text |
|
||||
| `fontSize` | number | `16` | Base cue font size in CSS pixels |
|
||||
| `timestampColor` | string | `#a5adcb` | Cue timestamp color |
|
||||
| `activeLineColor` | string | `#f5bde6` | Active cue text color |
|
||||
| `activeLineBackgroundColor` | string | `rgba(138, 173, 244, 0.22)` | Active cue background color |
|
||||
| `hoverLineBackgroundColor` | string | `rgba(54, 58, 79, 0.84)` | Hovered cue background color |
|
||||
Styling lives under the `css` object, using CSS property names and CSS custom properties (the same pattern as `subtitleStyle.css`).
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| ------------------- | ------- | ------------- | -------------------------------------------------------------------------- |
|
||||
| `enabled` | boolean | `true` | Enable subtitle sidebar support |
|
||||
| `autoOpen` | boolean | `false` | Open the sidebar automatically on overlay startup |
|
||||
| `layout` | string | `"overlay"` | `"overlay"` floats over mpv; `"embedded"` reserves right-side player space |
|
||||
| `toggleKey` | string | `"Backslash"` | `KeyboardEvent.code` for the toggle shortcut |
|
||||
| `pauseVideoOnHover` | boolean | `true` | Pause playback while hovering the cue list |
|
||||
| `autoScroll` | boolean | `true` | Keep the active cue in view during playback |
|
||||
|
||||
| `css` property | Default | Description |
|
||||
| ------------------------------------------- | --------------------------- | ---------------------------- |
|
||||
| `font-family` | `Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP` | Cue text font family |
|
||||
| `color` | `#cad3f5` | Default cue text color |
|
||||
| `background-color` | `rgba(73, 77, 100, 0.9)` | Sidebar shell background color |
|
||||
| `font-size` | `16px` | Base cue font size |
|
||||
| `opacity` | `0.95` | Sidebar opacity between `0` and `1` |
|
||||
| `--subtitle-sidebar-max-width` | `420px` | Maximum sidebar width |
|
||||
| `--subtitle-sidebar-timestamp-color` | `#a5adcb` | Cue timestamp color |
|
||||
| `--subtitle-sidebar-active-line-color` | `#f5bde6` | Active cue text color |
|
||||
| `--subtitle-sidebar-active-background-color`| `rgba(138, 173, 244, 0.22)` | Active cue background color |
|
||||
| `--subtitle-sidebar-hover-background-color` | `rgba(54, 58, 79, 0.84)` | Hovered cue background color |
|
||||
|
||||
## Keyboard Shortcut
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Troubleshooting
|
||||
|
||||
Common issues and how to resolve them. Most problems fall into one of a few buckets — the overlay shows but subtitles don't (see [MPV Connection](#mpv-connection)), cards aren't being created or come out empty (see [AnkiConnect](#ankiconnect)), or word lookups don't appear (see [Yomitan](#yomitan)). If an error message popped up on screen, search this page for the exact text — most headings below are quoted error strings.
|
||||
Common issues and how to resolve them. Most problems fall into one of a few buckets - the overlay shows but subtitles don't (see [MPV Connection](#mpv-connection)), cards aren't being created or come out empty (see [AnkiConnect](#ankiconnect)), or word lookups don't appear (see [Yomitan](#yomitan)). If an error message popped up on screen, search this page for the exact text - most headings below are quoted error strings.
|
||||
|
||||
## MPV Connection
|
||||
|
||||
@@ -18,7 +18,7 @@ If the overlay never appears at all, see [Playback Startup Flow](./architecture#
|
||||
|
||||
## Logging and App Mode
|
||||
|
||||
- Default log output is `info`.
|
||||
- Default log output is `warn`.
|
||||
- Use `--log-level` for more/less output.
|
||||
- Use `--dev`/`--debug` only to force app/dev mode (for example to get dev behavior from the overlay/app); they do not change log verbosity.
|
||||
- You can combine both, for example `SubMiner.AppImage --start --dev --log-level debug`, when you need maximum diagnostics.
|
||||
@@ -46,7 +46,7 @@ If the overlay never appears at all, see [Playback Startup Flow](./architecture#
|
||||
|
||||
2. Reduce rendering pressure:
|
||||
|
||||
- lower `subtitleStyle.fontSize`
|
||||
- lower `subtitleStyle.css["font-size"]`
|
||||
- keep overlay complexity minimal during heavy CPU periods
|
||||
|
||||
3. Reduce media overhead:
|
||||
@@ -66,7 +66,9 @@ If the overlay never appears at all, see [Playback Startup Flow](./architecture#
|
||||
```json
|
||||
{
|
||||
"subtitleStyle": {
|
||||
"fontSize": 30,
|
||||
"css": {
|
||||
"font-size": "30px"
|
||||
},
|
||||
"enableJlpt": false,
|
||||
"frequencyDictionary": {
|
||||
"enabled": false
|
||||
@@ -95,12 +97,12 @@ If the overlay never appears at all, see [Playback Startup Flow](./architecture#
|
||||
|
||||
- Confirm only one SubMiner instance is running.
|
||||
- Check whether bottlenecks are `ffmpeg`, `yt-dlp`, or sync tooling in system monitor.
|
||||
- Use `info` logs by default; keep `debug` for targeted diagnosis.
|
||||
- Keep the default `warn` level for normal use; raise to `info` or `debug` only for targeted diagnosis.
|
||||
- Reproduce once with `SubMiner.AppImage --start --log-level debug` and open DevTools (`y` then `d`) if freezes recur.
|
||||
|
||||
**"Failed to parse MPV message"**
|
||||
|
||||
Logged when a malformed JSON line arrives from the mpv socket. Usually harmless — SubMiner skips the bad line and continues. If it happens constantly, check that nothing else is writing to the same socket path.
|
||||
Logged when a malformed JSON line arrives from the mpv socket. Usually harmless - SubMiner skips the bad line and continues. If it happens constantly, check that nothing else is writing to the same socket path.
|
||||
|
||||
## Updates
|
||||
|
||||
@@ -130,7 +132,7 @@ The detected launcher is installed in a protected path such as `/usr/local/bin/s
|
||||
|
||||
**"AnkiConnect: unable to connect"**
|
||||
|
||||
First confirm you've completed the [Anki Integration prerequisites](/anki-integration#prerequisites) — Anki must be running with the AnkiConnect add-on installed.
|
||||
First confirm you've completed the [Anki Integration prerequisites](/anki-integration#prerequisites) - Anki must be running with the AnkiConnect add-on installed.
|
||||
|
||||
SubMiner connects to the active Anki endpoint:
|
||||
|
||||
@@ -146,7 +148,7 @@ SubMiner retries with exponential backoff (up to 5 s) and suppresses repeated er
|
||||
|
||||
**Cards are created but fields are empty**
|
||||
|
||||
Field names in your config must match your Anki note type exactly (case-sensitive). Check `ankiConnect.fields` — for example, if your note type uses `SentenceAudio` but your config says `Audio`, the field will not be populated.
|
||||
Field names in your config must match your Anki note type exactly (case-sensitive). Check `ankiConnect.fields` - for example, if your note type uses `SentenceAudio` but your config says `Audio`, the field will not be populated.
|
||||
|
||||
See [Anki Integration](/anki-integration) for the full field mapping reference.
|
||||
|
||||
@@ -167,7 +169,7 @@ Shown when SubMiner tries to update a card that no longer exists, or when AnkiCo
|
||||
|
||||
**Overlay appears but clicks pass through / cannot interact**
|
||||
|
||||
- Make sure you are hovering over subtitle text — the overlay only becomes interactive when the cursor is over a subtitle.
|
||||
- Make sure you are hovering over subtitle text - the overlay only becomes interactive when the cursor is over a subtitle.
|
||||
- On macOS/Windows: toggle the overlay off and back on (`Alt+Shift+O`) to re-enable pointer events.
|
||||
- On Linux: mouse event handling is unreliable in some Electron/compositor combinations. If clicks consistently fail, toggle the overlay off, click the underlying mpv window, then toggle it back on.
|
||||
|
||||
@@ -206,7 +208,7 @@ If you installed from the AppImage and see this error, the package may be incomp
|
||||
|
||||
**Yomitan lookup popup does not appear when hovering words or triggering lookup**
|
||||
|
||||
- Verify Yomitan loaded successfully — check the terminal output for "Loaded Yomitan extension".
|
||||
- Verify Yomitan loaded successfully - check the terminal output for "Loaded Yomitan extension".
|
||||
- Yomitan requires dictionaries to be installed. Open Yomitan settings (`Alt+Shift+Y` or `SubMiner.AppImage --yomitan`) and confirm at least one dictionary is imported.
|
||||
- If `yomitan.externalProfilePath` is set, import/check dictionaries in the external app/profile instead. SubMiner treats that profile as read-only and does not open its own Yomitan settings window.
|
||||
- If the overlay shows subtitles but hover lookup never resolves on tokens, the tokenizer may have failed. See the MeCab section below.
|
||||
@@ -230,6 +232,15 @@ Japanese word boundaries depend on Yomitan parser output. If segmentation seems
|
||||
- Verify Yomitan dictionaries are installed and active.
|
||||
- Note that CJK characters without spaces are segmented using parser heuristics, which is not always perfect.
|
||||
|
||||
## Character Dictionary
|
||||
|
||||
Character names from AniList are matched and highlighted in subtitles via the bundled Yomitan. See [Character Dictionary](/character-dictionary) for setup and the full troubleshooting list - the most common issues:
|
||||
|
||||
- **Names not highlighting:** Confirm `subtitleStyle.nameMatchEnabled` is `true`, and that the current media resolved to an AniList entry (SubMiner needs a media ID to fetch characters). No AniList account or token is required - character data uses public GraphQL queries.
|
||||
- **Inline portraits missing:** Confirm `subtitleStyle.nameMatchImagesEnabled` is `true`. Portraits also require AniList to return an image and the download to succeed during snapshot generation.
|
||||
- **Wrong characters showing:** Open the in-app manager (`Ctrl/Cmd+D`) and use **Override** to pin the correct AniList match for the series.
|
||||
- **Feature unavailable:** If `yomitan.externalProfilePath` is set, SubMiner runs in read-only external-profile mode and its character-dictionary features are disabled.
|
||||
|
||||
## Media Generation
|
||||
|
||||
**"FFmpeg not found"**
|
||||
@@ -258,7 +269,7 @@ Global shortcuts (`Alt+Shift+O`, `Alt+Shift+Y`) may conflict with other applicat
|
||||
|
||||
- Check your DE/WM keybinding settings for conflicts.
|
||||
- Change the shortcut in your config under `shortcuts.toggleVisibleOverlayGlobal`.
|
||||
- On Wayland, global shortcut registration has limitations depending on the compositor. Only Hyprland and Sway are supported natively — see the [Hyprland](#hyprland) section below for shortcut passthrough rules. Other Wayland compositors require X11/Xwayland.
|
||||
- On Wayland, global shortcut registration has limitations depending on the compositor. Only Hyprland and Sway are supported natively - see the [Hyprland](#hyprland) section below for shortcut passthrough rules. Other Wayland compositors require X11/Xwayland.
|
||||
|
||||
**Overlay keybindings not working**
|
||||
|
||||
@@ -315,18 +326,35 @@ The Jimaku API has rate limits. If you see 429 errors, wait for the retry durati
|
||||
|
||||
### Linux
|
||||
|
||||
- **Wayland (Hyprland/Sway only)**: Native Wayland support is limited to Hyprland and Sway. Window tracking uses compositor-specific commands (`hyprctl` / `swaymsg`). If these are not on `PATH`, tracking will fail silently. Other Wayland compositors are not supported — both mpv and SubMiner must run under X11 or Xwayland instead.
|
||||
- **X11 / Xwayland**: Requires `xdotool` and `xwininfo`. If missing, the overlay cannot track the mpv window position. This is the required backend for any Wayland compositor other than Hyprland or Sway — both mpv and SubMiner must be running under X11/Xwayland for window tracking to work.
|
||||
- **Wayland (Hyprland/Sway only)**: Native Wayland support is limited to Hyprland and Sway. Window tracking uses compositor-specific commands (`hyprctl` / `swaymsg`). If these are not on `PATH`, tracking will fail silently. Other Wayland compositors (KDE Plasma, GNOME, …) are not supported natively - both mpv and SubMiner must run under X11 or Xwayland instead. On those sessions SubMiner forces XWayland automatically for itself and for every mpv it launches (see [KDE Plasma & other Wayland compositors](#kde-plasma--other-wayland-compositors)).
|
||||
- **X11 / Xwayland**: Requires `xdotool`, `xprop`, and `xwininfo`. If missing, the overlay cannot track the mpv window position. This is the required backend for any Wayland compositor other than Hyprland or Sway - both mpv and SubMiner must be running under X11/Xwayland for window tracking _and_ for the overlay to stay above mpv (Wayland forbids clients from controlling window stacking). SubMiner uses a managed X11 overlay while mpv is windowed, switches to an override-redirect X11 overlay while tracked mpv is fullscreen, and hides/releases that overlay when another X11/Xwayland app takes focus. The visible overlay stays hidden until SubMiner has tracked mpv geometry, so startup should not create a display-sized fallback overlay while tokenization warms up.
|
||||
- **Tray icon missing**: SubMiner creates an Electron tray icon in `--background` mode, but Linux trays require a StatusNotifier/AppIndicator host. Hyprland does not provide one by itself; enable a tray in Waybar, Hyprpanel, or another panel. If Electron cannot register the tray, SubMiner logs a warning that mentions the missing tray host.
|
||||
- **Mouse passthrough**: On Linux, Electron's mouse passthrough is unreliable. SubMiner keeps pointer events enabled, meaning you may need to toggle the overlay off to interact with mpv controls underneath.
|
||||
- **Mouse passthrough**: On Linux X11/Xwayland, SubMiner uses `xdotool` to poll the cursor and only enables overlay input while the cursor is over subtitle or popup regions. Outside those regions, pointer input passes through to mpv. Native Wayland compositors other than Hyprland/Sway cannot provide the stacking control SubMiner needs.
|
||||
|
||||
### Hyprland
|
||||
|
||||
SubMiner's overlay is a transparent, frameless, always-on-top Electron window. Hyprland needs window rules to keep it transparent and borderless, and `pass` bindings to forward global shortcuts to SubMiner.
|
||||
SubMiner's overlay is a transparent, frameless Electron window that must be kept above mpv. SubMiner tries to apply the floating, borderless, no-shadow, and no-blur properties itself each time it places the overlay. It detects Hyprland's active config provider and uses Lua `hl.dsp.window.*` dispatchers for recent Hyprland Lua configs, or the legacy dispatcher syntax for older hyprlang configs. On many configurations that is enough, but if your Hyprland version doesn't honor those runtime dispatches - or a broad rule in your config forces opacity/blur on every window - add explicit window rules so the overlay is exempt. You also need `pass` bindings to forward global shortcuts to SubMiner (see below).
|
||||
|
||||
**Overlay is not transparent or has a visible border**
|
||||
|
||||
Add these window rules to your `hyprland.conf`:
|
||||
Add a window rule matching SubMiner's window class. Recent Hyprland uses the Lua config format:
|
||||
|
||||
```lua
|
||||
hl.window_rule({
|
||||
match = { class = "^SubMiner$" },
|
||||
float = true,
|
||||
border_size = 0,
|
||||
xray = false,
|
||||
no_shadow = true,
|
||||
no_blur = true,
|
||||
no_dim = true,
|
||||
opaque = true,
|
||||
dim_around = false,
|
||||
opacity = "1.0 override 1.0 override",
|
||||
})
|
||||
```
|
||||
|
||||
On older Hyprland releases that still use the hyprlang config (`hyprland.conf`), use the equivalent `windowrule` lines:
|
||||
|
||||
```ini
|
||||
windowrule = float on, match:class SubMiner
|
||||
@@ -336,7 +364,7 @@ windowrule = no_shadow on, match:class SubMiner
|
||||
windowrule = no_blur on, match:class SubMiner
|
||||
```
|
||||
|
||||
Without `xray off override`, the compositor may render the transparent overlay incorrectly — you might see a solid background or visual artifacts instead of the mpv video underneath.
|
||||
If you still see a solid background or visual artifacts instead of the mpv video underneath, the culprit is almost always a global opacity/blur rule applying to the overlay - the `opaque`/`opacity` and `no_blur` fields above override it.
|
||||
|
||||
**Global shortcuts not working**
|
||||
|
||||
@@ -357,7 +385,53 @@ SubMiner watches mpv's `fullscreen` property and refreshes the overlay geometry
|
||||
|
||||
For more details, see the Hyprland docs on [global keybinds](https://wiki.hypr.land/Configuring/Binds/#global-keybinds) and [window rules](https://wiki.hypr.land/Configuring/Window-Rules/).
|
||||
|
||||
### KDE Plasma & other Wayland compositors
|
||||
|
||||
On any Wayland session that is not Hyprland or Sway (KDE Plasma, GNOME, and others), the overlay can only stay above mpv when both processes run under **XWayland** - the Wayland protocol forbids clients from controlling window stacking, so the overlay's "always on top" becomes a no-op on a native Wayland surface.
|
||||
|
||||
SubMiner handles this automatically:
|
||||
|
||||
- It launches its own window under XWayland (it sets `--ozone-platform-hint=x11`).
|
||||
- Every mpv it launches (via the `subminer` launcher, Jellyfin, or YouTube) is pinned to XWayland too - Wayland environment hints are stripped and an X11 GPU context (`--gpu-context=x11egl,x11`) is applied.
|
||||
- While mpv is windowed, the overlay is a managed X11 window owned by the tracked mpv window (`WM_TRANSIENT_FOR`), so it stays above mpv while other foreground X11/Xwayland apps can still cover both windows.
|
||||
- While tracked mpv is fullscreen, SubMiner swaps the visible overlay to a focusable-false X11 override-redirect window. That path can stay above the active fullscreen mpv window without requiring a KDE/KWin-specific rule, and SubMiner hides/releases it when mpv is no longer the active X11/Xwayland window.
|
||||
- The visible overlay is shown inactive on Linux, so normal hover should not steal keyboard focus from mpv.
|
||||
- During startup and fullscreen transitions, SubMiner waits for tracked mpv geometry before showing the visible overlay and skips the fullscreen restack hide/show path after mpv leaves fullscreen. That avoids a temporary full-screen overlay or black window while the subtitle tokenizer and Yomitan warmups finish.
|
||||
- If the subtitle sidebar is open during a windowed/fullscreen transition, SubMiner restores it on the replacement overlay window. Subtitle hit regions are also refreshed as soon as the first measured subtitle line is reported, so hover and Yomitan lookup should work on the first visible line.
|
||||
|
||||
Requirements: `xdotool`, `xprop`, and `xwininfo` must be installed. SubMiner uses root `_NET_ACTIVE_WINDOW` from `xprop` for focus detection and falls back to `xdotool getactivewindow` when that signal is unavailable.
|
||||
|
||||
**Overlay sits behind mpv / pause-on-hover and Yomitan stop working**
|
||||
|
||||
This almost always means mpv came up as a **native Wayland** window that the XWayland overlay cannot cover. It happens when mpv is launched **manually** (your own command), because SubMiner can only force XWayland on the mpv processes it launches itself. Fix it one of these ways:
|
||||
|
||||
- Launch playback through SubMiner (the `subminer` launcher or the tray), which forces XWayland for you, or
|
||||
- Force XWayland in your own mpv invocation, e.g. `mpv --gpu-context=x11egl …`, or launch with `WAYLAND_DISPLAY= mpv …`, or set `gpu-context=x11egl` in your `mpv.conf`.
|
||||
|
||||
To confirm mpv is on XWayland, `xdotool search --class mpv` should return a window id (a native Wayland mpv returns nothing).
|
||||
|
||||
**Overlay stays above an unrelated foreground app**
|
||||
|
||||
SubMiner can only detect focus for X11/Xwayland windows in this mode. If a native Wayland app covers mpv but the overlay stays visible, run that app under Xwayland too or use Hyprland/Sway native support. Generic X11 cannot observe native Wayland foreground windows.
|
||||
|
||||
### macOS
|
||||
|
||||
- **Accessibility permission**: Required for window tracking. Grant it in System Settings > Privacy & Security > Accessibility.
|
||||
- **Gatekeeper**: If macOS blocks SubMiner, right-click the app and select "Open" to bypass the warning, or remove the quarantine attribute: `xattr -d com.apple.quarantine /path/to/SubMiner.app`
|
||||
|
||||
## See Also
|
||||
|
||||
Feature-specific issues are covered in each feature's own page:
|
||||
|
||||
- [Anki Integration](/anki-integration) - card creation, field mapping, and AnkiConnect setup
|
||||
- [AniList Integration](/anilist-integration) - watch-progress sync and authentication
|
||||
- [Character Dictionary](/character-dictionary) - AniList character name matching and inline portraits
|
||||
- [Jellyfin Integration](/jellyfin-integration) - remote playback and library connection
|
||||
- [Jimaku Integration](/jimaku-integration) - subtitle fetching and API rate limits
|
||||
- [YouTube Integration](/youtube-integration) - subtitle generation and playback
|
||||
- [Immersion Tracking](/immersion-tracking) - telemetry and session logging
|
||||
- [WebSocket / Texthooker API](/websocket-texthooker-api) - external texthooker clients
|
||||
- [Subtitle Annotations](/subtitle-annotations) - N+1, frequency, JLPT, and name-match layers
|
||||
- [Subtitle Sidebar](/subtitle-sidebar) - sidebar navigation and behavior
|
||||
- [Configuration Reference](/configuration) - full config options
|
||||
- [Shortcuts](/shortcuts) - keybinding reference
|
||||
|
||||
+9
-9
@@ -8,7 +8,7 @@ Play a video with SubMiner:
|
||||
subminer video.mkv
|
||||
```
|
||||
|
||||
On **Windows**, use the **SubMiner mpv** shortcut created during first-run setup — double-click it, or drag a video file onto it.
|
||||
On **Windows**, use the **SubMiner mpv** shortcut created during first-run setup - double-click it, or drag a video file onto it.
|
||||
|
||||
That's the simplest way to get started. The `subminer` launcher handles mpv, the IPC socket, and the overlay automatically.
|
||||
|
||||
@@ -41,20 +41,20 @@ Field names must match your Anki note type exactly (case-sensitive). See [Anki I
|
||||
When you launch SubMiner, it wires up mpv and the overlay for you:
|
||||
|
||||
1. SubMiner starts the overlay app in the background
|
||||
2. mpv runs with an **IPC socket** at `/tmp/subminer-socket` — a small local channel two programs use to talk to each other, so the overlay can ask mpv what subtitle is on screen right now
|
||||
2. mpv runs with an **IPC socket** at `/tmp/subminer-socket` - a small local channel two programs use to talk to each other, so the overlay can ask mpv what subtitle is on screen right now
|
||||
3. The overlay connects and subscribes to subtitle changes
|
||||
|
||||
From there, subtitles render as interactive, hoverable word spans and you mine cards directly from the overlay. For the overlay anatomy and the full mining loop — word lookup, card creation, annotations — see [Mining Workflow](/mining-workflow).
|
||||
From there, subtitles render as interactive, hoverable word spans and you mine cards directly from the overlay. For the overlay anatomy and the full mining loop - word lookup, card creation, annotations - see [Mining Workflow](/mining-workflow).
|
||||
|
||||
### Ways to Launch
|
||||
|
||||
| Approach | Use when | How |
|
||||
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| **`subminer` launcher** | You want SubMiner to handle everything — launch mpv, set up the socket, start the overlay. **Recommended for most users.** | `subminer video.mkv` |
|
||||
| **`subminer` launcher** | You want SubMiner to handle everything - launch mpv, set up the socket, start the overlay. **Recommended for most users.** | `subminer video.mkv` |
|
||||
| **SubMiner mpv shortcut** (Windows) | The recommended Windows entry point. Created during first-run setup, launches mpv with SubMiner's defaults. | Double-click, drag a file onto it, or run `SubMiner.exe --launch-mpv` |
|
||||
| **mpv plugin** (all platforms) | Bundled and injected at runtime. Provides `y` chord keybindings for controlling the overlay from within mpv. No manual install needed. | Automatic when using the launcher or shortcut |
|
||||
|
||||
The mpv plugin is always available — it's bundled with SubMiner and injected at runtime. If you launch mpv yourself (without the launcher), pass `--input-ipc-server=/tmp/subminer-socket` in your mpv config for the overlay to connect.
|
||||
The mpv plugin is always available - it's bundled with SubMiner and injected at runtime. If you launch mpv yourself (without the launcher), pass `--input-ipc-server=/tmp/subminer-socket` in your mpv config for the overlay to connect.
|
||||
|
||||
## Live Config Reload
|
||||
|
||||
@@ -116,7 +116,7 @@ subminer dictionary --candidates /path/to/file.mkv
|
||||
subminer dictionary --select 21355 /path/to/file.mkv
|
||||
subminer texthooker # Launch texthooker-only mode
|
||||
subminer texthooker -o # Launch texthooker and open it in your browser
|
||||
subminer app --anilist # Pass args directly to SubMiner binary (example: AniList login flow)
|
||||
subminer app --anilist-setup # Pass args directly to SubMiner binary (example: AniList login flow)
|
||||
|
||||
# Direct packaged app control
|
||||
SubMiner.AppImage --background # Start in background (tray + IPC wait, minimal logs)
|
||||
@@ -301,7 +301,7 @@ SubMiner supports gamepad/controller input for couch-friendly usage via the Chro
|
||||
1. Connect a controller before or after launching SubMiner.
|
||||
2. Set `controller.enabled` to `true` in your config.
|
||||
3. Press `Alt+C` in the overlay by default to pick the controller you want to save and remap any action inline.
|
||||
4. Enable keyboard-only mode — press `Y` on the controller (default binding) or use the overlay keybinding.
|
||||
4. Enable keyboard-only mode - press `Y` on the controller (default binding) or use the overlay keybinding.
|
||||
5. Click the binding badge, edit pencil, or `Learn` on the overlay action you want, then press the matching button, trigger, or stick direction on the controller.
|
||||
6. Use the left stick to navigate subtitle tokens and scroll the popup; use the right stick vertically for popup page jumps.
|
||||
7. Press `A` to look up the selected word, `X` to mine a card, `B` to close the popup.
|
||||
@@ -333,7 +333,7 @@ By default SubMiner uses the first connected controller after controller support
|
||||
|
||||
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.
|
||||
|
||||
All button and axis mappings are configurable under the `controller` config block. Learned remaps are saved under `controller.profiles` for the selected controller id. See [Configuration — Controller Support](/configuration#controller-support) for the full options.
|
||||
All button and axis mappings are configurable under the `controller` config block. Learned remaps are saved under `controller.profiles` for the selected controller id. See [Configuration - Controller Support](/configuration#controller-support) for the full options.
|
||||
|
||||
## Keybindings
|
||||
|
||||
@@ -361,4 +361,4 @@ Hovering over subtitle text pauses mpv by default; leaving resumes it. Yomitan p
|
||||
- Drop video files onto the overlay to replace current playback.
|
||||
- Hold `Shift` while dropping to append to the playlist instead.
|
||||
|
||||
Next: [Mining Workflow](/mining-workflow) — word lookup, card creation, and the full mining loop.
|
||||
Next: [Mining Workflow](/mining-workflow) - word lookup, card creation, and the full mining loop.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# WebSocket / Texthooker API & Integration
|
||||
|
||||
**Who this page is for:** developers and tinkerers who want to consume SubMiner's live subtitle stream from their own tools — a browser tab, an automation script, or another mpv plugin. If you just want subtitles in a browser tab for Yomitan, skip to [Texthooker Integration Guide](#texthooker-integration-guide); the rest is reference for building custom clients.
|
||||
**Who this page is for:** developers and tinkerers who want to consume SubMiner's live subtitle stream from their own tools - a browser tab, an automation script, or another mpv plugin. If you just want subtitles in a browser tab for Yomitan, skip to [Texthooker Integration Guide](#texthooker-integration-guide); the rest is reference for building custom clients.
|
||||
|
||||
A *texthooker* is a page/tool that receives the text currently on screen so a dictionary extension (like Yomitan) can look words up. SubMiner ships its own texthooker UI and also broadcasts subtitle text over local WebSockets that any client can connect to.
|
||||
|
||||
@@ -24,7 +24,7 @@ This page documents those integration points and shows how to build custom consu
|
||||
|
||||
## Enable and Configure the Services
|
||||
|
||||
SubMiner's integration ports are configured in `config.jsonc`.
|
||||
SubMiner's integration ports are configured in `config.jsonc`. All three services are **off by default** - the block below shows the values to set to turn them on.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@@ -45,12 +45,12 @@ SubMiner's integration ports are configured in `config.jsonc`.
|
||||
|
||||
### How startup behaves
|
||||
|
||||
- `websocket.enabled: "auto"` starts the basic subtitle websocket unless SubMiner detects the external `mpv_websocket` plugin.
|
||||
- `annotationWebsocket` is independent from `websocket` and stays enabled unless you explicitly disable it.
|
||||
- `texthooker.launchAtStartup` starts the local HTTP UI automatically.
|
||||
- `websocket.enabled` defaults to `false`. Set it to `"auto"` to start the basic subtitle websocket unless SubMiner detects the external `mpv_websocket` plugin, or `true` to always start it.
|
||||
- `annotationWebsocket.enabled` defaults to `false` and is independent from `websocket`. Set it to `true` to start the annotated stream.
|
||||
- `texthooker.launchAtStartup` defaults to `false`. Set it to `true` to start the local HTTP UI automatically.
|
||||
- `texthooker.openBrowser` controls whether SubMiner opens the texthooker page in your browser when it starts.
|
||||
|
||||
If you use the [mpv plugin](/mpv-plugin), it can also start a texthooker-only helper process. The launcher derives the plugin's texthooker setting from your SubMiner config (`texthooker.launchAtStartup`) and injects it at runtime — there is no plugin config file to edit.
|
||||
If you use the [mpv plugin](/mpv-plugin), it can also start a texthooker-only helper process. The launcher derives the plugin's texthooker setting from your SubMiner config (`texthooker.launchAtStartup`) and injects it at runtime - there is no plugin config file to edit.
|
||||
|
||||
## Developer API Documentation
|
||||
|
||||
@@ -368,7 +368,7 @@ ws.on('message', async (raw) => {
|
||||
## Related Pages
|
||||
|
||||
- [Configuration](/configuration#websocket-server)
|
||||
- [Mining Workflow — Texthooker](/mining-workflow#texthooker)
|
||||
- [Mining Workflow - Texthooker](/mining-workflow#texthooker)
|
||||
- [MPV Plugin](/mpv-plugin)
|
||||
- [Launcher Script](/launcher-script)
|
||||
- [Anki Integration](/anki-integration#proxy-mode-setup-yomitan--texthooker)
|
||||
|
||||
@@ -4,8 +4,8 @@ SubMiner auto-loads Japanese subtitles when you play a YouTube URL, giving you t
|
||||
|
||||
## Requirements
|
||||
|
||||
- **[yt-dlp](https://github.com/yt-dlp/yt-dlp)** must be installed and on your `PATH`. yt-dlp is a free command-line tool that reads YouTube video and subtitle info; SubMiner calls it behind the scenes. (`PATH` is the list of folders your system searches for programs — most installers add yt-dlp to it automatically. If yours did not, set `SUBMINER_YTDLP_BIN` to the full path of the yt-dlp binary.)
|
||||
- mpv with `--input-ipc-server` configured (handled automatically when you launch playback through the `subminer` launcher — no manual setup needed).
|
||||
- **[yt-dlp](https://github.com/yt-dlp/yt-dlp)** must be installed and on your `PATH`. yt-dlp is a free command-line tool that reads YouTube video and subtitle info; SubMiner calls it behind the scenes. (`PATH` is the list of folders your system searches for programs - most installers add yt-dlp to it automatically. If yours did not, set `SUBMINER_YTDLP_BIN` to the full path of the yt-dlp binary.)
|
||||
- mpv with `--input-ipc-server` configured (handled automatically when you launch playback through the `subminer` launcher - no manual setup needed).
|
||||
|
||||
## How It Works
|
||||
|
||||
@@ -32,7 +32,7 @@ flowchart TD
|
||||
C[Track discovery]:::action
|
||||
D{Auto or manual selection?}:::step
|
||||
E[Auto-select best tracks]:::action
|
||||
F[Manual picker — Ctrl+Alt+C]:::action
|
||||
F[Manual picker - Ctrl+Alt+C]:::action
|
||||
G[Download subtitle files]:::action
|
||||
H[Convert TimedText to VTT]:::enrich
|
||||
I[Normalize auto-caption duplicates]:::enrich
|
||||
|
||||
+3
-3
@@ -33,7 +33,7 @@
|
||||
`bun run build`
|
||||
When validating auto-update metadata, also run the relevant platform package
|
||||
build and confirm `release/` contains the generated updater metadata
|
||||
(`*.yml`) and blockmaps (`*.blockmap`).
|
||||
(`latest*.yml`) and blockmaps (`*.blockmap`).
|
||||
8. If `docs-site/` changed, also run:
|
||||
`bun run docs:test`
|
||||
`bun run docs:build`
|
||||
@@ -55,7 +55,7 @@
|
||||
`bun run test:env`
|
||||
`bun run build`
|
||||
When validating packaged updater output, confirm the platform build writes
|
||||
`*.yml` and `*.blockmap` files under `release/`.
|
||||
`latest*.yml` and `*.blockmap` files under `release/`.
|
||||
5. Commit the prerelease prep (package.json version bump + the generated
|
||||
`release/prerelease-notes.md`). CI does not regenerate notes — it uses the
|
||||
committed file — so review it before committing. If you add more
|
||||
@@ -87,7 +87,7 @@ Notes:
|
||||
- Keep Cloudflare Pages Git auto-deploy disabled for `docs.subminer.moe`. Production docs are direct-uploaded by Wrangler from GitHub Actions with `--branch main`.
|
||||
- AUR publish is best-effort: the workflow retries transient SSH clone/push failures, then warns and leaves the GitHub Release green if AUR still fails. Follow up with a manual `git push aur master` from the AUR checkout when needed.
|
||||
- Required GitHub Actions secret: `AUR_SSH_PRIVATE_KEY`. Add the matching public key to your AUR account before relying on the automation.
|
||||
- Release and prerelease workflows upload updater metadata (`*.yml`) and blockmaps (`*.blockmap`) alongside platform artifacts. Do not remove those files while `electron-updater` is enabled.
|
||||
- Release and prerelease workflows upload updater metadata (`latest*.yml`) and blockmaps (`*.blockmap`) alongside platform artifacts. Do not remove those files while `electron-updater` is enabled.
|
||||
- macOS tray app updates use the standard `electron-updater`/Squirrel path. Keep `latest-mac.yml`, the macOS `SubMiner-<version>-mac.zip`, and ZIP blockmap published; Squirrel uses the ZIP payload even when the DMG remains the user-facing installer.
|
||||
- macOS update metadata and full ZIP downloads are routed through `/usr/bin/curl` before Squirrel installation to avoid Electron main-process network crashes on update checks.
|
||||
- Windows tray app updates use the standard `electron-updater`/NSIS path. Keep `latest.yml`, the Windows NSIS installer, and installer blockmap published; updater HTTP is routed through main-process fetch to avoid Electron main-process network crashes during update checks.
|
||||
|
||||
@@ -19,6 +19,7 @@ The desktop app keeps `src/main.ts` as composition root and pushes behavior into
|
||||
|
||||
- [Domains](./domains.md) - who owns what
|
||||
- [Layering](./layering.md) - how modules should depend on each other
|
||||
- [Subtitle Overlay Priming](./subtitle-overlay-priming.md) - visible-overlay subtitle startup flow
|
||||
- Public contributor summary: [`docs-site/architecture.md`](../../docs-site/architecture.md)
|
||||
|
||||
## Current Shape
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<!-- read_when: changing visible overlay startup, Linux/X11 overlay window shape, mpv subtitle callbacks, or subtitle tokenization emission -->
|
||||
|
||||
# Subtitle Overlay Priming
|
||||
|
||||
Status: active
|
||||
Last verified: 2026-06-01
|
||||
Owner: Kyle Yasuda
|
||||
Read when: debugging subtitle state or blank Linux/X11 overlay windows when the visible overlay is shown or recreated
|
||||
|
||||
Visible-overlay subtitle priming fills the overlay from mpv's current subtitle properties before
|
||||
waiting for the next live mpv subtitle event. This avoids a stale or blank overlay when the user
|
||||
manually shows the visible overlay while playback is already sitting on a subtitle.
|
||||
|
||||
On Linux/X11, visible-overlay show and later mpv bounds refreshes restore the Electron window shape
|
||||
to the full current overlay bounds. Electron's `BrowserWindow.setShape()` applies a bounding shape,
|
||||
not an input-only region; stale shapes can leave a mapped 1920x1080 overlay with smaller X11 shape
|
||||
extents such as `800x600+0+0`, so renderer and websocket subtitle state are correct while bottom
|
||||
subtitles do not draw.
|
||||
|
||||
## Entry Points
|
||||
|
||||
- `src/main.ts` calls `primeCurrentSubtitleForVisibleOverlay()` when manual visible-overlay show
|
||||
paths run.
|
||||
- `src/main.ts` calls `restoreVisibleOverlayWindowShapeForShow()` before visible-overlay show
|
||||
actions on Linux, and `resetVisibleOverlayInputState()` restores a full shape instead of applying
|
||||
an empty shape.
|
||||
- `src/main.ts` also restores the Linux/X11 shape after applying mpv overlay bounds, so a newly
|
||||
created 800x600 hidden Electron window cannot keep clipping after it is resized to mpv geometry.
|
||||
- `primeCurrentSubtitleForVisibleOverlay()` delegates to
|
||||
`primeVisibleOverlaySubtitleFromMpv()` in `src/main/runtime/current-subtitle-snapshot.ts`.
|
||||
- `restoreVisibleOverlayWindowShapeForShow()` delegates to `restoreLinuxOverlayWindowShape()` in
|
||||
`src/main/runtime/linux-overlay-window-shape.ts`.
|
||||
- Inputs are callback deps, not globals: `getMpvClient`, `setCurrentSubText`,
|
||||
`getCurrentSubtitleData`, `consumeCachedSubtitle`, `onSubtitleChange`,
|
||||
`refreshCurrentSubtitle`, `emitSubtitle`, optional secondary-subtitle callbacks, and `logDebug`.
|
||||
|
||||
## Primary Subtitle Flow
|
||||
|
||||
1. Read the connected mpv client through `getMpvClient()`. Exit if no connected client.
|
||||
2. Request mpv `sub-text`. On failure, log a
|
||||
`[visible-overlay-subtitle-prime] failed to read sub-text` debug line and exit.
|
||||
3. Normalize non-string `sub-text` to `''`, then call `setCurrentSubText(text)` so app state
|
||||
matches mpv before any overlay emission.
|
||||
4. Empty text: call `onSubtitleChange(text)`, emit `{ text, tokens: null }`, then prime secondary
|
||||
subtitles.
|
||||
5. Current cached payload: if `getCurrentSubtitleData()?.text === text`, call
|
||||
`emitSubtitle(payload)` and `refreshCurrentSubtitle(text)`, then prime secondary subtitles.
|
||||
6. Tokenization cache hit: call `consumeCachedSubtitle(text)`, `onSubtitleChange(text)`, and
|
||||
`emitSubtitle(cachedPayload)`, then prime secondary subtitles.
|
||||
7. Cache miss: call `refreshCurrentSubtitle(text)` and let normal tokenization emit the final
|
||||
payload.
|
||||
|
||||
In `src/main.ts`, both `onSubtitleChange` and `refreshCurrentSubtitle` pause
|
||||
`subtitlePrefetchService`, notify it with `onSeek(lastObservedTimePos)`, and then call the matching
|
||||
`subtitleProcessingController` method. This gives the visible overlay priority over background
|
||||
prefetch work and re-centers prefetch around the live playback time.
|
||||
|
||||
## Emitted State
|
||||
|
||||
- `emitSubtitle(payload)` maps to `emitSubtitlePayload(payload)`, which sends the normal
|
||||
annotated subtitle payload to overlay windows and subtitle websocket listeners.
|
||||
- Secondary priming reads mpv `secondary-sub-text`, stores it in
|
||||
`mpvClient.currentSecondarySubText`, and broadcasts `secondary-subtitle:set` to overlay windows.
|
||||
- If secondary `requestProperty` fails, the primary flow stays complete and only a debug line is
|
||||
written.
|
||||
|
||||
## Linux/X11 Window Shape
|
||||
|
||||
- `restoreLinuxOverlayWindowShape()` reads `BrowserWindow.getBounds()` and calls `setShape()` with
|
||||
one full-window rectangle: `{ x: 0, y: 0, width, height }`.
|
||||
- Restore the shape after `setBounds()`/mpv geometry updates, not only before showing the overlay.
|
||||
Manual startup can create the hidden overlay at Electron's default 800x600 size before the window
|
||||
tracker applies the real mpv bounds.
|
||||
- Do not use `setShape([])` as a passive reset for the visible overlay. On the tested X11/XWayland
|
||||
path, empty or stale bounding shapes produced invisible or clipped subtitles even though the
|
||||
overlay window remained mapped above mpv.
|
||||
- Pointer pass-through should continue to use `setIgnoreMouseEvents(true, { forward: true })` and
|
||||
the Linux cursor-poll fallback, not bounding-shape clipping.
|
||||
|
||||
## Config And Migration
|
||||
|
||||
No config or schema migration. This workflow reuses existing mpv properties, overlay IPC events,
|
||||
subtitle tokenization cache, and prefetch controls.
|
||||
@@ -13,6 +13,7 @@ Read when: finding internal docs or checking verification status
|
||||
| Architecture index | `docs/architecture/README.md` | active | 2026-05-23 | top-level runtime map |
|
||||
| Domain ownership | `docs/architecture/domains.md` | active | 2026-05-23 | runtime and feature ownership |
|
||||
| Layering rules | `docs/architecture/layering.md` | active | 2026-05-23 | dependency direction and smells |
|
||||
| Subtitle overlay priming | `docs/architecture/subtitle-overlay-priming.md` | active | 2026-06-01 | visible-overlay subtitle startup flow |
|
||||
| KB rules | `docs/knowledge-base/README.md` | active | 2026-05-23 | maintenance policy |
|
||||
| Core beliefs | `docs/knowledge-base/core-beliefs.md` | active | 2026-03-13 | agent-first principles |
|
||||
| Quality scorecard | `docs/knowledge-base/quality.md` | active | 2026-03-13 | quality grades and gaps |
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getAppControlSocketPath } from '../src/shared/app-control';
|
||||
import { withProcessExitIntercept } from './test-support/exit-intercept.js';
|
||||
import {
|
||||
buildConfiguredMpvDefaultArgs,
|
||||
buildRuntimeExtraScriptOptParts,
|
||||
buildMpvBackendArgs,
|
||||
buildMpvEnv,
|
||||
cleanupPlaybackSession,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
runAppCommandCaptureOutput,
|
||||
resolveLauncherRuntimePluginPath,
|
||||
resolveLauncherRuntimePluginPlan,
|
||||
shouldResolveAniSkipMetadataForLaunch,
|
||||
shouldResolveAniSkipMetadata,
|
||||
stopOverlay,
|
||||
startOverlay,
|
||||
@@ -374,6 +376,43 @@ test('resolveLauncherRuntimePluginPlan reports missing bundled plugin when no in
|
||||
assert.match(plan.errorMessage ?? '', /Packaged mpv plugin assets were not found/);
|
||||
});
|
||||
|
||||
test('buildRuntimeExtraScriptOptParts marks launcher-owned startup pause gate', () => {
|
||||
assert.deepEqual(
|
||||
buildRuntimeExtraScriptOptParts('/tmp/video.mkv', 'file', {
|
||||
startPaused: true,
|
||||
runtimePluginConfig: {
|
||||
socketPath: '/tmp/subminer.sock',
|
||||
binaryPath: '',
|
||||
backend: 'auto',
|
||||
autoStart: true,
|
||||
autoStartVisibleOverlay: true,
|
||||
autoStartPauseUntilReady: true,
|
||||
texthookerEnabled: false,
|
||||
aniskipEnabled: true,
|
||||
aniskipButtonKey: 'TAB',
|
||||
},
|
||||
}),
|
||||
['subminer-auto_start_pause_until_ready_owns_initial_pause=yes'],
|
||||
);
|
||||
});
|
||||
|
||||
test('shouldResolveAniSkipMetadataForLaunch respects disabled runtime plugin AniSkip', () => {
|
||||
assert.equal(
|
||||
shouldResolveAniSkipMetadataForLaunch('/tmp/video.mkv', 'file', undefined, {
|
||||
socketPath: '/tmp/subminer.sock',
|
||||
binaryPath: '',
|
||||
backend: 'auto',
|
||||
autoStart: true,
|
||||
autoStartVisibleOverlay: true,
|
||||
autoStartPauseUntilReady: true,
|
||||
texthookerEnabled: false,
|
||||
aniskipEnabled: false,
|
||||
aniskipButtonKey: 'TAB',
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('launchTexthookerOnly exits non-zero when app binary cannot be spawned', () => {
|
||||
const error = withProcessExitIntercept(() => {
|
||||
launchTexthookerOnly('/definitely-missing-subminer-binary', makeArgs());
|
||||
|
||||
+61
-51
@@ -5,6 +5,12 @@ import net from 'node:net';
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { buildMpvLaunchModeArgs } from '../src/shared/mpv-launch-mode.js';
|
||||
import { buildMpvLoggingArgs } from '../src/shared/mpv-logging-args.js';
|
||||
import {
|
||||
MPV_X11_BACKEND_ARGS,
|
||||
applyX11EnvOverrides,
|
||||
getLinuxDesktopEnv,
|
||||
shouldForceX11MpvBackend as shouldForceX11MpvBackendForBackend,
|
||||
} from '../src/shared/mpv-x11-backend.js';
|
||||
import {
|
||||
isAppControlServerAvailable as checkAppControlServerAvailable,
|
||||
sendAppControlCommand,
|
||||
@@ -458,39 +464,8 @@ export function detectBackend(
|
||||
fail('Could not detect display backend');
|
||||
}
|
||||
|
||||
type LinuxDesktopEnv = {
|
||||
xdgCurrentDesktop: string;
|
||||
xdgSessionDesktop: string;
|
||||
hasWayland: boolean;
|
||||
};
|
||||
|
||||
function getLinuxDesktopEnv(env: NodeJS.ProcessEnv): LinuxDesktopEnv {
|
||||
const xdgCurrentDesktop = (env.XDG_CURRENT_DESKTOP || '').toLowerCase();
|
||||
const xdgSessionDesktop = (env.XDG_SESSION_DESKTOP || '').toLowerCase();
|
||||
const xdgSessionType = (env.XDG_SESSION_TYPE || '').toLowerCase();
|
||||
return {
|
||||
xdgCurrentDesktop,
|
||||
xdgSessionDesktop,
|
||||
hasWayland: Boolean(env.WAYLAND_DISPLAY) || xdgSessionType === 'wayland',
|
||||
};
|
||||
}
|
||||
|
||||
function shouldForceX11MpvBackend(args: Pick<Args, 'backend'>, env: NodeJS.ProcessEnv): boolean {
|
||||
if (process.platform !== 'linux' || !env.DISPLAY?.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const linuxDesktopEnv = getLinuxDesktopEnv(env);
|
||||
const supportedWaylandBackend =
|
||||
Boolean(env.HYPRLAND_INSTANCE_SIGNATURE || env.SWAYSOCK) ||
|
||||
linuxDesktopEnv.xdgCurrentDesktop.includes('hyprland') ||
|
||||
linuxDesktopEnv.xdgCurrentDesktop.includes('sway') ||
|
||||
linuxDesktopEnv.xdgSessionDesktop.includes('hyprland') ||
|
||||
linuxDesktopEnv.xdgSessionDesktop.includes('sway');
|
||||
return (
|
||||
args.backend === 'x11' ||
|
||||
(args.backend === 'auto' && linuxDesktopEnv.hasWayland && !supportedWaylandBackend)
|
||||
);
|
||||
return shouldForceX11MpvBackendForBackend(args.backend, env);
|
||||
}
|
||||
|
||||
function resolveAppBinaryCandidate(candidate: string, pathModule: PathModule = path): string {
|
||||
@@ -862,6 +837,50 @@ export function shouldResolveAniSkipMetadata(
|
||||
return !isYoutubeTarget(target);
|
||||
}
|
||||
|
||||
type StartMpvOptions = {
|
||||
startPaused?: boolean;
|
||||
disableYoutubeSubtitleAutoLoad?: boolean;
|
||||
runtimePluginPath?: string | null;
|
||||
runtimePluginConfig?: PluginRuntimeConfig;
|
||||
};
|
||||
|
||||
export function shouldResolveAniSkipMetadataForLaunch(
|
||||
target: string,
|
||||
targetKind: 'file' | 'url',
|
||||
preloadedSubtitles?: { primaryPath?: string; secondaryPath?: string },
|
||||
runtimePluginConfig?: PluginRuntimeConfig,
|
||||
): boolean {
|
||||
if (runtimePluginConfig?.aniskipEnabled === false) {
|
||||
return false;
|
||||
}
|
||||
return shouldResolveAniSkipMetadata(target, targetKind, preloadedSubtitles);
|
||||
}
|
||||
|
||||
export function buildRuntimeExtraScriptOptParts(
|
||||
target: string,
|
||||
targetKind: 'file' | 'url',
|
||||
options?: Pick<
|
||||
StartMpvOptions,
|
||||
'startPaused' | 'disableYoutubeSubtitleAutoLoad' | 'runtimePluginConfig'
|
||||
>,
|
||||
): string[] {
|
||||
const launcherOwnsAutoplayReadyInitialPause =
|
||||
options?.startPaused === true &&
|
||||
options.runtimePluginConfig?.autoStart === true &&
|
||||
options.runtimePluginConfig.autoStartVisibleOverlay === true &&
|
||||
options.runtimePluginConfig.autoStartPauseUntilReady === true;
|
||||
return [
|
||||
...(launcherOwnsAutoplayReadyInitialPause
|
||||
? ['subminer-auto_start_pause_until_ready_owns_initial_pause=yes']
|
||||
: []),
|
||||
...(targetKind === 'url' &&
|
||||
isYoutubeTarget(target) &&
|
||||
options?.disableYoutubeSubtitleAutoLoad === true
|
||||
? ['subminer-auto_start_pause_until_ready=no']
|
||||
: []),
|
||||
];
|
||||
}
|
||||
|
||||
export async function startMpv(
|
||||
target: string,
|
||||
targetKind: 'file' | 'url',
|
||||
@@ -869,12 +888,7 @@ export async function startMpv(
|
||||
socketPath: string,
|
||||
appPath: string,
|
||||
preloadedSubtitles?: { primaryPath?: string; secondaryPath?: string },
|
||||
options?: {
|
||||
startPaused?: boolean;
|
||||
disableYoutubeSubtitleAutoLoad?: boolean;
|
||||
runtimePluginPath?: string | null;
|
||||
runtimePluginConfig?: PluginRuntimeConfig;
|
||||
},
|
||||
options?: StartMpvOptions,
|
||||
): Promise<void> {
|
||||
if (targetKind === 'file' && (!fs.existsSync(target) || !fs.statSync(target).isFile())) {
|
||||
fail(`Video file not found: ${target}`);
|
||||
@@ -932,15 +946,15 @@ export async function startMpv(
|
||||
if (options?.startPaused) {
|
||||
mpvArgs.push('--pause=yes');
|
||||
}
|
||||
const aniSkipMetadata = shouldResolveAniSkipMetadata(target, targetKind, preloadedSubtitles)
|
||||
const aniSkipMetadata = shouldResolveAniSkipMetadataForLaunch(
|
||||
target,
|
||||
targetKind,
|
||||
preloadedSubtitles,
|
||||
options?.runtimePluginConfig,
|
||||
)
|
||||
? await resolveAniSkipMetadataForFile(target)
|
||||
: null;
|
||||
const extraScriptOpts =
|
||||
targetKind === 'url' &&
|
||||
isYoutubeTarget(target) &&
|
||||
options?.disableYoutubeSubtitleAutoLoad === true
|
||||
? ['subminer-auto_start_pause_until_ready=no']
|
||||
: [];
|
||||
const extraScriptOpts = buildRuntimeExtraScriptOptParts(target, targetKind, options);
|
||||
const runtimeScriptOpts = options?.runtimePluginConfig
|
||||
? buildPluginRuntimeScriptOptParts(options.runtimePluginConfig, appPath)
|
||||
: [`subminer-binary_path=${appPath}`, `subminer-socket_path=${socketPath}`];
|
||||
@@ -1344,11 +1358,7 @@ export function buildMpvEnv(
|
||||
return env;
|
||||
}
|
||||
|
||||
delete env.WAYLAND_DISPLAY;
|
||||
delete env.HYPRLAND_INSTANCE_SIGNATURE;
|
||||
delete env.SWAYSOCK;
|
||||
env.XDG_SESSION_TYPE = 'x11';
|
||||
return env;
|
||||
return applyX11EnvOverrides(env);
|
||||
}
|
||||
|
||||
export function buildMpvBackendArgs(
|
||||
@@ -1358,7 +1368,7 @@ export function buildMpvBackendArgs(
|
||||
if (!shouldForceX11MpvBackend(args, baseEnv)) {
|
||||
return [];
|
||||
}
|
||||
return ['--vo=gpu', '--gpu-api=opengl', '--gpu-context=x11egl,x11'];
|
||||
return [...MPV_X11_BACKEND_ARGS];
|
||||
}
|
||||
|
||||
export function buildConfiguredMpvDefaultArgs(
|
||||
|
||||
@@ -559,6 +559,7 @@ test(
|
||||
socketPath: smokeCase.socketPath,
|
||||
autoStartSubMiner: true,
|
||||
pauseUntilOverlayReady: true,
|
||||
aniskipEnabled: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -582,6 +583,10 @@ test(
|
||||
assert.equal(result.status, unixSocketDenied ? 3 : 0);
|
||||
assert.equal(Array.isArray(mpvFirstArgs), true);
|
||||
assert.equal((mpvFirstArgs as string[]).includes('--pause=yes'), true);
|
||||
assert.match(
|
||||
(mpvFirstArgs as string[]).find((arg) => arg.startsWith('--script-opts=')) ?? '',
|
||||
/subminer-auto_start_pause_until_ready_owns_initial_pause=yes/,
|
||||
);
|
||||
assert.match(result.stdout, /pause mpv until overlay and tokenization are ready/i);
|
||||
});
|
||||
},
|
||||
|
||||
+3
-3
File diff suppressed because one or more lines are too long
@@ -2,6 +2,7 @@ local M = {}
|
||||
|
||||
local AUTO_START_SOCKET_RETRY_DELAY_SECONDS = 0.2
|
||||
local AUTO_START_SOCKET_RETRY_MAX_ATTEMPTS = 25
|
||||
local WARM_END_FILE_HIDE_DELAY_SECONDS = 0.25
|
||||
|
||||
function M.create(ctx)
|
||||
local mp = ctx.mp
|
||||
@@ -58,6 +59,40 @@ function M.create(ctx)
|
||||
end)
|
||||
end
|
||||
|
||||
local function clear_pending_visible_overlay_hide()
|
||||
local timer = state.pending_visible_overlay_hide_timer
|
||||
if timer and timer.kill then
|
||||
timer:kill()
|
||||
end
|
||||
state.pending_visible_overlay_hide_timer = nil
|
||||
state.pending_visible_overlay_hide_generation = (state.pending_visible_overlay_hide_generation or 0) + 1
|
||||
end
|
||||
|
||||
local resolve_auto_start_visible_overlay_enabled
|
||||
|
||||
local function hide_visible_overlay_after_end_file()
|
||||
if state.visible_overlay_requested == true and not resolve_auto_start_visible_overlay_enabled() then
|
||||
return
|
||||
end
|
||||
if not state.auto_play_ready_signal_seen then
|
||||
process.hide_visible_overlay()
|
||||
return
|
||||
end
|
||||
|
||||
clear_pending_visible_overlay_hide()
|
||||
local generation = (state.pending_visible_overlay_hide_generation or 0) + 1
|
||||
state.pending_visible_overlay_hide_generation = generation
|
||||
state.pending_visible_overlay_hide_timer = mp.add_timeout(WARM_END_FILE_HIDE_DELAY_SECONDS, function()
|
||||
if state.pending_visible_overlay_hide_generation ~= generation then
|
||||
return
|
||||
end
|
||||
state.pending_visible_overlay_hide_timer = nil
|
||||
if state.overlay_running then
|
||||
process.hide_visible_overlay()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function resolve_auto_start_enabled()
|
||||
local raw_auto_start = opts.auto_start
|
||||
if raw_auto_start == nil then
|
||||
@@ -69,6 +104,14 @@ function M.create(ctx)
|
||||
return options_helper.coerce_bool(raw_auto_start, false)
|
||||
end
|
||||
|
||||
resolve_auto_start_visible_overlay_enabled = function()
|
||||
local raw_visible_overlay = opts.auto_start_visible_overlay
|
||||
if raw_visible_overlay == nil then
|
||||
raw_visible_overlay = opts["auto-start-visible-overlay"]
|
||||
end
|
||||
return options_helper.coerce_bool(raw_visible_overlay, false)
|
||||
end
|
||||
|
||||
local function next_auto_start_retry_generation()
|
||||
state.auto_start_retry_generation = (state.auto_start_retry_generation or 0) + 1
|
||||
return state.auto_start_retry_generation
|
||||
@@ -103,6 +146,11 @@ function M.create(ctx)
|
||||
return true
|
||||
end
|
||||
|
||||
local function should_rearm_pause_until_ready(same_media_loaded)
|
||||
return not same_media_loaded
|
||||
and not (state.overlay_running and state.auto_play_ready_signal_seen == true)
|
||||
end
|
||||
|
||||
local function start_overlay_when_socket_ready(generation, media_identity, same_media_loaded, attempt)
|
||||
if generation ~= state.auto_start_retry_generation then
|
||||
return
|
||||
@@ -137,7 +185,7 @@ function M.create(ctx)
|
||||
process.start_overlay({
|
||||
auto_start_trigger = true,
|
||||
socket_path = opts.socket_path,
|
||||
rearm_pause_until_ready = not same_media_loaded,
|
||||
rearm_pause_until_ready = should_rearm_pause_until_ready(same_media_loaded),
|
||||
})
|
||||
-- Give the overlay process a moment to initialize before querying AniSkip.
|
||||
schedule_aniskip_fetch("overlay-start", 0.8)
|
||||
@@ -155,6 +203,7 @@ function M.create(ctx)
|
||||
end
|
||||
|
||||
local function on_file_loaded()
|
||||
clear_pending_visible_overlay_hide()
|
||||
local media_identity = resolve_media_identity()
|
||||
local media_title = resolve_media_title()
|
||||
local retry_generation = next_auto_start_retry_generation()
|
||||
@@ -242,6 +291,8 @@ function M.create(ctx)
|
||||
aniskip.clear_aniskip_state()
|
||||
hover.clear_hover_overlay()
|
||||
process.disarm_auto_play_ready_gate()
|
||||
clear_pending_visible_overlay_hide()
|
||||
state.auto_play_ready_signal_seen = false
|
||||
state.current_media_identity = nil
|
||||
state.current_media_title = nil
|
||||
state.pending_reload_media_identity = nil
|
||||
@@ -277,7 +328,7 @@ function M.create(ctx)
|
||||
state.app_managed_playback_pending = false
|
||||
state.app_managed_playback_active = false
|
||||
if state.overlay_running and reason ~= "quit" then
|
||||
process.hide_visible_overlay()
|
||||
hide_visible_overlay_after_end_file()
|
||||
end
|
||||
end)
|
||||
mp.register_event("shutdown", function()
|
||||
|
||||
@@ -33,6 +33,7 @@ function M.load(options_lib, default_socket_path)
|
||||
auto_start = false,
|
||||
auto_start_visible_overlay = false,
|
||||
auto_start_pause_until_ready = true,
|
||||
auto_start_pause_until_ready_owns_initial_pause = false,
|
||||
auto_start_pause_until_ready_timeout_seconds = 15,
|
||||
osd_messages = true,
|
||||
log_level = "info",
|
||||
|
||||
@@ -39,6 +39,9 @@ function M.create(ctx)
|
||||
end
|
||||
return "show-visible-overlay"
|
||||
end
|
||||
if state.visible_overlay_requested == true then
|
||||
return nil
|
||||
end
|
||||
return "hide-visible-overlay"
|
||||
end
|
||||
|
||||
@@ -50,6 +53,25 @@ function M.create(ctx)
|
||||
return options_helper.coerce_bool(raw_pause_until_ready, false)
|
||||
end
|
||||
|
||||
local function resolve_pause_until_ready_owns_initial_pause()
|
||||
local raw_owns_initial_pause = opts.auto_start_pause_until_ready_owns_initial_pause
|
||||
if raw_owns_initial_pause == nil then
|
||||
raw_owns_initial_pause = opts["auto-start-pause-until-ready-owns-initial-pause"]
|
||||
end
|
||||
return options_helper.coerce_bool(raw_owns_initial_pause, false)
|
||||
end
|
||||
|
||||
local function consume_pause_until_ready_initial_pause_ownership()
|
||||
if state.auto_play_ready_initial_pause_ownership_consumed then
|
||||
return false
|
||||
end
|
||||
if not resolve_pause_until_ready_owns_initial_pause() then
|
||||
return false
|
||||
end
|
||||
state.auto_play_ready_initial_pause_ownership_consumed = true
|
||||
return true
|
||||
end
|
||||
|
||||
local function resolve_texthooker_enabled(override_value)
|
||||
if override_value ~= nil then
|
||||
return options_helper.coerce_bool(override_value, false)
|
||||
@@ -260,7 +282,8 @@ function M.create(ctx)
|
||||
clear_auto_play_ready_osd_timer()
|
||||
end
|
||||
if not was_armed then
|
||||
state.auto_play_ready_should_resume_playback = mp.get_property_native("pause") ~= true
|
||||
state.auto_play_ready_should_resume_playback = consume_pause_until_ready_initial_pause_ownership()
|
||||
or mp.get_property_native("pause") ~= true
|
||||
end
|
||||
state.auto_play_ready_gate_armed = true
|
||||
mp.set_property_native("pause", true)
|
||||
@@ -290,6 +313,7 @@ function M.create(ctx)
|
||||
end
|
||||
|
||||
local function notify_auto_play_ready()
|
||||
state.auto_play_ready_signal_seen = true
|
||||
local released_ready_gate = release_auto_play_ready_gate("tokenization-ready")
|
||||
local force_ready_overlay_restore = state.force_ready_overlay_restore == true
|
||||
state.force_ready_overlay_restore = false
|
||||
@@ -601,6 +625,7 @@ function M.create(ctx)
|
||||
end
|
||||
|
||||
state.overlay_running = false
|
||||
state.auto_play_ready_signal_seen = false
|
||||
subminer_log("error", "process", "Overlay start failed after retries: " .. reason)
|
||||
show_osd("Overlay start failed")
|
||||
release_auto_play_ready_gate("overlay-start-failed")
|
||||
@@ -653,6 +678,7 @@ function M.create(ctx)
|
||||
|
||||
state.overlay_running = false
|
||||
state.texthooker_running = false
|
||||
state.auto_play_ready_signal_seen = false
|
||||
disarm_auto_play_ready_gate()
|
||||
show_osd("Stopped")
|
||||
end
|
||||
@@ -709,6 +735,14 @@ function M.create(ctx)
|
||||
end)
|
||||
return
|
||||
end
|
||||
if not state.overlay_running then
|
||||
state.suppress_ready_overlay_restore = false
|
||||
disarm_auto_play_ready_gate({ resume_playback = false })
|
||||
start_overlay({
|
||||
show_visible_overlay = true,
|
||||
})
|
||||
return
|
||||
end
|
||||
state.suppress_ready_overlay_restore = true
|
||||
disarm_auto_play_ready_gate({ resume_playback = false })
|
||||
|
||||
@@ -773,6 +807,7 @@ function M.create(ctx)
|
||||
|
||||
state.overlay_running = false
|
||||
state.texthooker_running = false
|
||||
state.auto_play_ready_signal_seen = false
|
||||
state.suppress_ready_overlay_restore = false
|
||||
state.force_ready_overlay_restore = true
|
||||
disarm_auto_play_ready_gate({ resume_playback = false })
|
||||
@@ -795,6 +830,7 @@ function M.create(ctx)
|
||||
}, function(success, result, error)
|
||||
if not success or (result and result.status ~= 0) then
|
||||
state.overlay_running = false
|
||||
state.auto_play_ready_signal_seen = false
|
||||
subminer_log(
|
||||
"error",
|
||||
"process",
|
||||
|
||||
@@ -24,6 +24,11 @@ local KEY_NAME_MAP = {
|
||||
BracketLeft = "[",
|
||||
BracketRight = "]",
|
||||
Backquote = "`",
|
||||
MBTN_LEFT = "MBTN_LEFT",
|
||||
MBTN_MID = "MBTN_MID",
|
||||
MBTN_RIGHT = "MBTN_RIGHT",
|
||||
MBTN_BACK = "MBTN_BACK",
|
||||
MBTN_FORWARD = "MBTN_FORWARD",
|
||||
}
|
||||
|
||||
local MODIFIER_MAP = {
|
||||
|
||||
@@ -33,6 +33,10 @@ function M.new()
|
||||
auto_play_ready_should_resume_playback = false,
|
||||
auto_play_ready_timeout = nil,
|
||||
auto_play_ready_osd_timer = nil,
|
||||
auto_play_ready_signal_seen = false,
|
||||
auto_play_ready_initial_pause_ownership_consumed = false,
|
||||
pending_visible_overlay_hide_timer = nil,
|
||||
pending_visible_overlay_hide_generation = 0,
|
||||
suppress_ready_overlay_restore = false,
|
||||
force_ready_overlay_restore = false,
|
||||
visible_overlay_requested = nil,
|
||||
|
||||
+79
-47
@@ -3,39 +3,48 @@
|
||||
## Highlights
|
||||
### Added
|
||||
|
||||
- **Settings Window:** A dedicated Settings window is now available via `subminer --settings` or `subminer settings`, organized into Appearance, Behavior, Anki, Input, and Integration sections. Includes click-to-learn keybinding controls, AnkiConnect-backed deck/field/note-type pickers, and live reload for stats keys, logging level, Jimaku, Subsync, YouTube language defaults, Anki field mappings, sentence card model, and selected annotation/runtime options. Settings search works across all categories and narrows on multi-word terms. AI and translation settings remain config-file only.
|
||||
- **Settings Window:** A dedicated Settings window is now available via `subminer --settings` or `subminer settings`, organized into Appearance, Behavior, Anki, Input, and Integration sections.
|
||||
- Includes click-to-learn keybinding controls, an AnkiConnect deck dropdown that auto-fills from Yomitan's current mining deck, and AnkiConnect-backed deck, field, and note-type pickers.
|
||||
- Live-saves changes for subtitle CSS declarations, stats keys, logging level, Anki field mappings, sentence card model, and other annotation and runtime options; search narrows across all categories including on multi-word terms. AI and translation settings remain config-file only.
|
||||
|
||||
- **Auto-Updater:** SubMiner can now check for and apply updates from the system tray or by running `subminer -u`, with checksum verification, configurable update notifications, and an opt-in prerelease channel. The `subminer` launcher and Linux rofi theme update automatically. Set `updates.channel` to `"prerelease"` to receive beta and RC builds.
|
||||
- **Auto-Updater:** SubMiner can now check for and apply updates from the system tray or by running `subminer -u`, with checksum verification and configurable update notifications.
|
||||
- The `subminer` launcher and Linux rofi theme update automatically alongside the app.
|
||||
- Set `updates.channel` to `"prerelease"` to receive beta and RC builds.
|
||||
|
||||
- **First-Run Setup:** A new optional setup flow installs Bun and the `subminer` command-line launcher on Linux, macOS, and Windows, with an Open SubMiner Settings button on completion. Windows users get a `subminer.cmd` PATH shim so `subminer` works in any terminal without manually adding `SubMiner.exe` to PATH. Setup recognizes existing `subminer` installs in Homebrew or user PATH directories, avoids writing into Homebrew-owned paths, and quits the standalone setup app on completion.
|
||||
- **First-Run Setup:** A new optional setup flow installs Bun and the `subminer` command-line launcher on Linux, macOS, and Windows.
|
||||
- Windows users get a `subminer.cmd` PATH shim so `subminer` works in any terminal without manually adding `SubMiner.exe` to PATH.
|
||||
- Setup recognizes existing `subminer` installs in Homebrew or user PATH directories and avoids writing into Homebrew-owned paths. An Open SubMiner Settings button is included on completion; the standalone setup app quits after finishing.
|
||||
|
||||
- **Launcher:** `subminer --version` / `subminer -v` now prints the installed app version. The new `mpv.profile` config option passes an mpv profile to SubMiner-managed mpv launches. Bundled mpv plugin startup options are now configurable from SubMiner config.
|
||||
|
||||
- **Character Portraits:** Character-name subtitle matches can now show optional inline AniList character portraits. Manual AniList title overrides are scoped per media directory so separate season folders keep independent character dictionary selections.
|
||||
- **Character Portraits:** Character-name subtitle matches can now show optional inline AniList character portraits.
|
||||
- Manual AniList title overrides are scoped per media directory so separate season folders keep independent character dictionary selections.
|
||||
|
||||
- **Log Export:** Sanitized log ZIP archives can be exported from the tray menu or by running `subminer logs -e`, with home-directory usernames redacted from the exported contents.
|
||||
|
||||
- **Logging Configuration:** SubMiner's logging level is now forwarded into launcher-started and Windows shortcut-started mpv sessions, controlling mpv log verbosity and plugin script logging. The new `logging.rotation` config sets daily log retention (default 7 days), and `logging.files` toggles let you enable or disable per-component log files; mpv logs are off by default unless explicitly enabled for debugging.
|
||||
- **Logging Configuration:** SubMiner's logging level is now forwarded into launcher-started and Windows shortcut-started mpv sessions, controlling mpv log verbosity and plugin script logging.
|
||||
- The new `logging.rotation` config sets daily log retention (default 7 days). `logging.files` toggles let you enable or disable per-component log files; mpv logs are off by default unless explicitly enabled.
|
||||
|
||||
- **Yomitan Popup Visibility:** The new `subtitleStyle.primaryVisibleOnYomitanPopup` option keeps hover-mode primary subtitles visible while a Yomitan lookup popup is open.
|
||||
|
||||
- **Launcher:** `subminer --version` / `subminer -v` now prints the installed app version. The new `mpv.profile` config option passes an mpv profile to SubMiner-managed mpv launches, and bundled mpv plugin startup options are now configurable from SubMiner config.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Subtitle Appearance:** Primary and secondary subtitle appearance now use color controls plus CSS declaration editors, saved as `subtitleStyle.css` and `subtitleStyle.secondary.css`. Sidebar appearance is configured via `subtitleSidebar.css`. The default subtitle font stack is updated to `Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP`. Existing configs are migrated automatically.
|
||||
- **Subtitle Appearance:** Primary and secondary subtitle appearance now use color controls plus CSS declaration editors, saved as `subtitleStyle.css` and `subtitleStyle.secondary.css`; sidebar appearance uses `subtitleSidebar.css`.
|
||||
- Default font stack updated to `Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP`; default text shadow is stronger, JLPT underlines are thicker, and the frequency `topX` threshold defaults to `10000`.
|
||||
- Existing configs are migrated automatically: legacy appearance options and hover token colors fold into `subtitleStyle.css`, and user config files are preserved.
|
||||
|
||||
- **Known-Word Colors:** Known-word and N+1 annotation colors moved to `subtitleStyle.knownWordColor` and `subtitleStyle.nPlusOneColor`. Legacy Anki color keys remain accepted with deprecation warnings. N+1 highlighting is preserved for configs that already had it enabled; new configs leave it disabled unless `ankiConnect.nPlusOne.enabled` is set explicitly.
|
||||
- **Known-Word Colors:** Known-word and N+1 annotation colors moved to `subtitleStyle.knownWordColor` and `subtitleStyle.nPlusOneColor`. Legacy Anki color keys remain accepted with deprecation warnings.
|
||||
- N+1 highlighting is preserved for configs that already had it enabled; new configs leave it disabled unless `ankiConnect.nPlusOne.enabled` is set explicitly.
|
||||
|
||||
- **Character Dictionary:** A new `Ctrl/Cmd+D` manager modal lets you remove, reorder, or override loaded dictionary entries. Entries are scoped to the current AniList media and generate Japanese name aliases only, so raw romanized or English aliases no longer appear as separate results. The in-app AniList title selector now waits for an explicit search rather than triggering automatically; the search box is prefilled from the current filename guess. The manager is blocked with a notice when character dictionary annotations are disabled, and `subtitleStyle.nameMatchEnabled` is the sole switch for enabling name matching and dictionary builds.
|
||||
- **Character Dictionary:** Entries are now scoped to the current AniList media and generate Japanese name aliases only, so raw romanized or English aliases no longer appear as separate results.
|
||||
- A new `Ctrl/Cmd+D` manager modal lets you remove, reorder, or override loaded dictionary entries.
|
||||
- The in-app AniList title selector now waits for an explicit search rather than triggering automatically; the search box is prefilled from the current filename guess.
|
||||
|
||||
- **Linux Updater:** Tray "Check for Updates" now installs the new AppImage automatically via `electron-updater`, matching the macOS and Windows update flow. System-package-managed AppImages (e.g. AUR `/opt/SubMiner`) and non-AppImage launches fall back to the GitHub-asset flow.
|
||||
- **Linux Updater:** Tray "Check for Updates" now installs the new AppImage automatically via `electron-updater`, matching the macOS and Windows update flow. System-package-managed AppImages and non-AppImage launches fall back to the GitHub-asset flow.
|
||||
|
||||
- **Subsync:** The subtitle sync dialog now always opens the manual picker; the `subsync.defaultMode` config option has been removed.
|
||||
|
||||
- **Jellyfin:** The server presets dropdown in Jellyfin setup is replaced by a single editable server URL field.
|
||||
|
||||
- **AniSkip:** The key binding setting now uses click-to-learn key capture instead of raw text entry.
|
||||
|
||||
- **Setup:** The bundled mpv runtime plugin readiness card is removed from first-run setup; the legacy mpv plugin removal notice still appears when needed.
|
||||
- **Jellyfin Setup:** The server presets dropdown is replaced by a single editable server URL field.
|
||||
|
||||
- **Defaults:** Jellyfin remote-session startup warmup and character-name subtitle highlighting now default to off.
|
||||
|
||||
@@ -43,73 +52,96 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- **macOS Overlay:** Significantly improved overlay focus and stability: the overlay hides when mpv loses focus, is minimized, or is no longer the foreground target; stays stable through transient window-tracking misses; remains correctly layered during stats mouse passthrough; opens over fullscreen mpv without switching Spaces; and stays stable when mpv remains frontmost but window geometry temporarily disappears from macOS APIs. Passthrough is fixed so mpv controls stay clickable before hovering a subtitle bar. The overlay stays stable when clicking from the overlay back into mpv. Background tracking overhead is reduced while mpv is stably focused. The compiled mpv window helper is now correctly bundled, preventing the overlay from falling back to slower startup on first launch.
|
||||
- **macOS Overlay:** Significantly improved overlay focus and stability across a range of scenarios.
|
||||
- The overlay hides when mpv loses focus, is minimized, or is no longer the foreground target; stays stable through transient window-tracking misses; remains correctly layered during stats mouse passthrough; and opens over fullscreen mpv without switching Spaces.
|
||||
- Passthrough is fixed so mpv controls stay clickable before hovering a subtitle bar. The compiled mpv window helper is now correctly bundled, preventing the overlay from falling back to a slower startup path on first launch.
|
||||
|
||||
- **Linux/Hyprland Overlay:** Overlay placement refreshes after leaving mpv fullscreen so the visible overlay stays aligned to the player. The visible overlay remains stacked above mpv after mpv regains focus from clicks, and is suspended while the in-player stats window is open. Settings windows (SubMiner and Yomitan) now open above the subtitle overlay on Hyprland instead of behind it. The overlay is hidden immediately when the character dictionary modal opens, including while AniList lookup is in progress or returns no results.
|
||||
- **Linux/Hyprland Overlay:** Overlay placement refreshes after leaving mpv fullscreen so the visible overlay stays aligned to the player.
|
||||
- The overlay stays stacked above mpv after click-to-focus events and is suspended while the in-player stats window is open.
|
||||
- Settings windows (SubMiner and Yomitan) now open above the subtitle overlay; the overlay hides immediately when the character dictionary modal opens, including while AniList lookup is in progress.
|
||||
|
||||
- **Jellyfin Playback:** Resolved a wide range of Jellyfin discovery and playback issues: the active item is no longer reloaded during startup, paused mpv is no longer misreported as playing, startup unpause no longer repeats after a manual pause or `y-t` toggle, duplicate ready signals no longer re-show the overlay, and long-lived sidebar ffmpeg extractors no longer run against stream URLs. Discovery now correctly handles delayed Japanese subtitle selection and prevents later-loading foreign tracks from stealing the active Japanese track. Discovery resume correctly handles `StartPositionTicks: 0` for items with saved progress.
|
||||
- **Jellyfin Playback:** Resolved a wide range of discovery and playback issues: the active item is no longer reloaded during startup, paused mpv is no longer misreported as playing, startup unpause no longer repeats after a manual pause or `y-t` toggle, and duplicate ready signals no longer re-show the overlay.
|
||||
- Discovery now correctly handles delayed Japanese subtitle selection and prevents later-loading foreign tracks from stealing the active Japanese track.
|
||||
- Discovery resume correctly handles `StartPositionTicks: 0` for items with saved progress.
|
||||
|
||||
- **Jellyfin Subtitles:** Improved subtitle timing by preferring default embedded streams over external sidecars, stripping Jellyfin's server-selected stream from playback URLs, suppressing mpv auto-selection while SubMiner stages managed tracks, and automatically correcting clear Japanese-vs-English cue timeline offsets. Per-stream subtitle delay shifts are restored on load. Track selection now tolerates transient `track-list` read failures and numeric string track IDs on Linux.
|
||||
- **Jellyfin Subtitles:** Improved subtitle timing by preferring default embedded streams over external sidecars, stripping Jellyfin's server-selected stream from playback URLs, suppressing mpv auto-selection while SubMiner stages managed tracks, and automatically correcting Japanese-vs-English cue timeline offsets.
|
||||
- Per-stream subtitle delay shifts are restored on load. Track selection now tolerates transient `track-list` read failures and numeric string track IDs on Linux.
|
||||
|
||||
- **Jellyfin Overlay:** The visible subtitle overlay now shows automatically during Jellyfin playback so `subtitleStyle` appearance applies. The bundled mpv plugin is injected when SubMiner auto-launches mpv for Jellyfin so mpv-side keybindings work without overlay focus. The `y-t` overlay toggle is reliable and remains sticky across stream redirects. Passive Linux/Hyprland overlay shows no longer steal keyboard focus from mpv.
|
||||
- **Jellyfin Overlay:** The visible subtitle overlay now shows automatically during Jellyfin playback so `subtitleStyle` appearance applies, and the bundled mpv plugin is injected when SubMiner auto-launches mpv so mpv-side keybindings work without overlay focus.
|
||||
- The `y-t` overlay toggle is reliable and remains sticky across stream redirects.
|
||||
- Passive Linux/Hyprland overlay shows no longer steal keyboard focus from mpv.
|
||||
|
||||
- **Jellyfin Remote Progress:** Fixed progress sync for mpv/SubMiner seek jumps, stopped sessions, startup path changes, and Linux websocket reconnect windows. Play and Resume are now distinct: Play starts from the beginning while Resume starts at the saved position. Final progress reports use SubMiner's last known position when mpv resets during stop.
|
||||
- **Jellyfin Remote Progress:** Fixed progress sync for mpv/SubMiner seek jumps, stopped sessions, startup path changes, and Linux websocket reconnect windows.
|
||||
- Play and Resume are now distinct: Play starts from the beginning while Resume starts at the saved position.
|
||||
- Final progress reports use SubMiner's last known position when mpv resets during stop.
|
||||
|
||||
- **Jellyfin Identity:** Cast device identity is now derived from the OS hostname. Multiple SubMiner installs no longer share the same remote-session identity, and SubMiner always reports itself as the client regardless of legacy configurable identity fields.
|
||||
- **Jellyfin Identity:** Cast device identity is now derived from the OS hostname. Multiple SubMiner installs no longer share the same remote-session identity.
|
||||
|
||||
- **Jellyfin Tray:** The discovery tray checkbox stays in sync on Linux after tray, CLI, or startup remote-session changes. Stale discovery sessions restart automatically when the server no longer lists the SubMiner cast target. Library discovery works correctly when the app log level is set above info.
|
||||
- **Jellyfin Tray:** The discovery tray checkbox stays in sync on Linux after tray, CLI, or startup remote-session changes. Stale discovery sessions restart automatically when the server no longer lists the SubMiner cast target.
|
||||
|
||||
- **Jellyfin Setup:** Fixed the Jellyfin setup login flow on Windows: login now uses an IPC bridge with immediate progress feedback, and unreachable servers time out with an inline error instead of hanging.
|
||||
- **Jellyfin Setup:** Fixed the Windows login flow with an IPC bridge and immediate progress feedback; unreachable servers time out with an inline error instead of hanging.
|
||||
|
||||
- **Subtitle Sync Modal:** Fixed a macOS issue where opening the subtitle sync modal would flash and disappear on the first attempt, or leave stale state after syncing.
|
||||
- **AniList Progress:** Threshold checks now use fresh playback position data so updates fire correctly when playback reaches or skips past the watched threshold.
|
||||
- Season-specific results are preferred for multi-season files, with a clear message when the matched season is not in Planning or Watching status.
|
||||
- Repeated missing-token checks no longer exhaust AniList retry attempts or create duplicate dead-letter entries for the same episode.
|
||||
|
||||
- **Controller:** Controller config and debug shortcuts now stay closed while controller support is disabled, with a notice to enable `controller.enabled`. Learn mode can be entered from the edit pencil or binding badge, remaps are saved per controller profile, and individual bindings can be reset to their defaults.
|
||||
- **Anki:** Sentence-audio padding is now opt-in by default; animated AVIF freeze-frame duration is correctly aligned to word audio length without double-counting padding.
|
||||
- Multi-line sentence mining stays aligned for repeated subtitle text; Kiku duplicate-card detection and merge flow are fixed; clipboard card updates from YouTube use mpv's resolved stream URLs; sentence cards refresh the secondary subtitle before saving.
|
||||
- Known-word cache append is fixed when no default Anki mining deck is configured but multiple known-word deck field mappings are present.
|
||||
|
||||
- **AniList Progress:** Progress threshold checks now use fresh playback position data so updates fire correctly when playback reaches or skips past the watched threshold. Season-specific results are preferred for multi-season files, and a clear message is shown when the matched season is not in Planning or Watching status. Repeated missing-token checks no longer rapidly exhaust AniList retry attempts or create duplicate dead-letter entries for the same episode.
|
||||
- **YouTube:** Primary subtitles are downloaded to temporary local files so the primary bar and sidebar read the same source, with cleanup on reload and quit.
|
||||
- False load-failure notifications are suppressed. Launcher-managed playback creates the tray icon when attaching to an already-running process, and app-owned playback no longer lets the mpv plugin start a second SubMiner instance.
|
||||
|
||||
- **Anki:** Sentence-audio padding is now opt-in by default. When padding is configured, animated AVIF freeze-frame duration is correctly aligned to the word audio length without double-counting sentence audio padding. Multi-line sentence mining stays aligned when repeated subtitle text appears in the selected history range. Manual clipboard card updates from YouTube playback now use mpv's resolved stream URLs for generated audio and images. Sentence cards now refresh the current secondary subtitle before saving so the translation field contains the loaded subtitle rather than repeating the primary text. Kiku duplicate-card detection correctly groups fields, modal-open acknowledgement races no longer cancel the merge flow, and merged fields follow Kiku's group ordering, sentence-audio, furigana, and tag semantics.
|
||||
- **Character Dictionary:** Surname honorifics are now matched for Japanese localized aliases embedded in AniList alternative names; cached snapshots are regenerated to include them.
|
||||
- Cached media matches are reused when loading a title with an existing snapshot, avoiding redundant AniList search requests. Manager keyboard shortcuts are correctly forwarded to the mpv plugin.
|
||||
|
||||
- **YouTube:** Primary subtitles are now downloaded to temporary local files so the primary bar and sidebar read the same source, with cleanup on reload and quit. False subtitle load failure notifications are suppressed after SubMiner confirms the selected track loaded. Launcher-managed playback commands create the tray icon even when attaching to an already-running process, and app-owned YouTube playback no longer lets the mpv plugin start a second SubMiner instance.
|
||||
- **Updater:** Update checks are more stable across platforms: Linux uses GitHub release metadata; `subminer -u` can update independently of the tray app; macOS update dialogs reliably appear in the foreground.
|
||||
- Builds that cannot apply native updates show a manual-install message instead of a restart prompt. Windows retains the native NSIS update path while routing updater HTTP through the main process.
|
||||
|
||||
- **Character Dictionary:** Surname honorifics are now matched for Japanese localized aliases embedded in AniList alternative names (e.g. Korean-source characters whose Japanese name appears in parentheses), and cached snapshots are regenerated to include them. Cached media matches are reused when loading a title with an existing snapshot, avoiding redundant AniList search requests on repeat visits. Character dictionary manager keyboard shortcuts are now correctly forwarded to the mpv plugin.
|
||||
- **Setup - macOS:** First-run setup recognizes existing `subminer` installs in Homebrew or user PATH directories and avoids writing into Homebrew-owned paths.
|
||||
- `subminer app --setup` opens the setup flow even when SubMiner is already running. The standalone setup app quits after completing first-run setup, and `subminer settings` exits cleanly when the window is closed.
|
||||
|
||||
- **Updater:** Update checks are more stable across platforms: Linux uses GitHub release metadata instead of the native Electron updater; `subminer -u` can update independently of the tray app; macOS update dialogs reliably appear in the foreground; builds that cannot apply native updates show a manual-install message instead of a restart prompt; Windows retains the native NSIS update path while routing updater HTTP through the main process; and macOS updater metadata mismatches from conflicting ZIP filenames are resolved.
|
||||
- **Tray App:** Fixed several lifecycle issues: the tray stays running when Yomitan settings are closed; a close-only menu prevents accidentally quitting the tray app; an in-page close button is available on Hyprland where native window controls are unavailable.
|
||||
- Settings loading no longer blocks other tray actions; the embedded popup preview is disabled to prevent renderer hangs during sidebar navigation; extension refreshes at startup are serialized; the session help modal closes correctly without mpv running.
|
||||
- On Windows, "Open SubMiner Setup" now correctly opens the setup window after first-run setup is complete.
|
||||
|
||||
- **Setup - macOS:** First-run setup now recognizes existing `subminer` installs in Homebrew or user PATH directories, and manual setup avoids writing into Homebrew-owned paths. `subminer app --setup` opens the setup flow even when SubMiner is already running in the background. The standalone setup app quits after completing first-run setup, and `subminer settings` exits cleanly when the window is closed.
|
||||
|
||||
- **Tray App:** Fixed several lifecycle issues with tray-launched Yomitan settings: the tray stays running when settings are closed; settings loading no longer blocks other tray actions; a close-only menu prevents accidentally quitting the tray app; an in-page close button is available on Hyprland where native window controls are unavailable; the embedded popup preview is disabled to prevent renderer hangs during sidebar navigation; extension refreshes at startup are serialized to prevent race conditions; and the session help modal can close correctly without mpv running. On Windows, the tray "Open SubMiner Setup" action now correctly opens the setup window after first-run setup is complete.
|
||||
|
||||
- **Launcher:** Launcher-opened videos reuse an already-running background SubMiner instance and correctly reapply preferred subtitles on warm launches. Videos stay paused when attaching to a running background app until subtitle priming and tokenization readiness complete. Launcher-owned tray apps close after playback ends. `subminer settings` on macOS no longer emits Electron menu diagnostics. Linux first-run launcher installs now build with a valid Bun shebang. `subminer app` on Linux returns control to the terminal immediately. On Windows, managed mpv launches from a background SubMiner instance correctly retarget the new mpv socket, bind to the player window, and receive startup overlay options.
|
||||
- **Launcher:** Launcher-opened videos reuse an already-running background SubMiner instance and correctly reapply preferred subtitles on warm launches. Videos stay paused until subtitle priming and tokenization readiness complete.
|
||||
- `subminer settings` on macOS no longer emits Electron menu diagnostics and exits cleanly when the window is closed. Linux first-run launcher installs build with a valid Bun shebang; `subminer app` on Linux returns control to the terminal immediately.
|
||||
- On Windows, managed mpv launches from a background instance correctly retarget the new mpv socket, bind to the player window, and receive startup overlay options.
|
||||
|
||||
- **Playback:** The first subtitle is primed before autoplay resumes so the overlay renders text before video playback begins. Launcher-owned videos quit SubMiner when playback ends while background and tray sessions stay alive.
|
||||
- The visible overlay and subtitle stream stay alive after restarting SubMiner from the `y-r` shortcut, with correct Linux bounds reapplication and user-paused playback preserved through readiness gates.
|
||||
|
||||
- **Subtitle Frequency:** Frequency highlighting is preserved for determiner-led noun compounds like `その場` while standalone determiners are still filtered. Frequency annotations are also corrected for Yomitan single-token compounds with internal particles such as `目の前`, while pure grammar and kana helper spans remain unannotated.
|
||||
- **Subtitle Frequency:** Frequency highlighting is preserved for determiner-led noun compounds like `その場` while standalone determiners are still filtered. Annotations are corrected for Yomitan single-token compounds with internal particles like `目の前`.
|
||||
|
||||
- **Shortcuts:** Native mpv menu shortcuts are disabled during managed macOS playback so configured SubMiner shortcuts also work while mpv has focus. Session shortcuts including `stats.markWatchedKey` are correctly wired through mpv. The visible overlay receives focus when entering multi-line copy/mine selection so number keys work on macOS and Windows.
|
||||
- **Subtitle Annotation Prefetch:** Cached annotations and character images are ready for more live subtitle changes without delaying raw subtitle display.
|
||||
|
||||
- **Overlay Restart:** The visible overlay and subtitle stream stay alive after restarting SubMiner from the `y-r` shortcut, with correct bounds reapplication on Linux and user-paused playback preserved through readiness gates.
|
||||
- **Shortcuts:** Native mpv menu shortcuts are disabled during managed macOS playback so SubMiner shortcuts also work while mpv has focus. Session shortcuts including `stats.markWatchedKey` are correctly wired through mpv. The visible overlay receives focus when entering multi-line copy/mine selection so number keys work on macOS and Windows.
|
||||
|
||||
- **Stats:** In-player stats layering is fixed so delete confirmations, overlay modals, and update-check dialogs appear above the stats window. Jellyfin playback stats are grouped under item metadata instead of stream URLs, so watched episodes merge with matching local library titles and display clean names.
|
||||
- **Stats:** In-player stats layering is fixed so delete confirmations, overlay modals, and update-check dialogs appear above the stats window. Jellyfin playback stats are grouped under item metadata so watched episodes merge with matching local library titles and display clean names.
|
||||
|
||||
- **Sidebar:** Yomitan lookup popups opened from the subtitle sidebar now correctly pause playback when popup auto-pause is enabled. Yomitan-enriched cards mined from the sidebar now use audio and images from the clicked subtitle line rather than the current primary line.
|
||||
- **Sidebar:** Yomitan lookup popups opened from the subtitle sidebar now correctly pause playback when popup auto-pause is enabled. Mined cards use audio and images from the clicked subtitle line rather than the current primary line.
|
||||
|
||||
- **Controller:** Config and debug shortcuts stay closed while controller support is disabled, with a notice to enable `controller.enabled`. Learn mode can be entered from the edit pencil or binding badge; remaps are saved per controller profile, and individual bindings can be reset to their defaults.
|
||||
|
||||
- **Discord Rich Presence:** Presence no longer falls back to Jellyfin stream URLs; Jellyfin playback titles are primed before loading tokenized streams so presence shows the show/episode title.
|
||||
|
||||
- **WebSocket:** The regular subtitle WebSocket now sends plain text only; annotation spans and token metadata are sent exclusively on the annotation WebSocket.
|
||||
|
||||
- **Windows:** Startup failures now show a native error dialog and write fatal details to the app log instead of exiting silently.
|
||||
- **Windows Startup:** Fatal startup errors now show a native error dialog and write details to the app log instead of exiting silently.
|
||||
|
||||
- **Yomitan:** Fixed Yomitan popups not opening when overlay startup races the Yomitan extension load.
|
||||
- **Yomitan:** Fixed popups not opening when overlay startup races the Yomitan extension load.
|
||||
|
||||
- **Settings:** Search now works across all categories, narrows correctly on multi-word terms, and hides settings with dedicated editors. Live saves for subtitle CSS declarations apply immediately to open overlays. Legacy subtitle appearance options and hover token colors are automatically migrated into `subtitleStyle.css`. The note-fields note type picker defaults to the configured Anki deck's note type, then `Kiku`, then `Lapis`, leaving it blank for manual selection otherwise. User config files are preserved during legacy config compatibility handling. The generated example config uses the same CSS declaration paths written by the Settings window.
|
||||
- **Subtitle Sync Modal:** Fixed a macOS issue where the modal would flash and disappear on the first attempt, or leave stale state after syncing.
|
||||
|
||||
### Docs
|
||||
|
||||
- **Versioned Docs:** Stable docs are now published at the site root with current development docs under `/main/`. Fixed versioned docs navigation so archived pages keep local links under the selected version, the version switcher no longer nests paths incorrectly, local dev version routes serve warmed archive files instead of redirecting to production, and internal README files no longer break archived builds.
|
||||
- **Versioned Docs:** Stable docs are now published at the site root with current development docs under `/main/`.
|
||||
- Fixed versioned docs navigation so archived pages keep local links under the selected version, the version switcher no longer nests paths incorrectly, and local dev version routes serve warmed archive files instead of redirecting to production.
|
||||
|
||||
- **Configuration Reference:** All previously undocumented config options are now covered, including `subtitleStyle.primaryDefaultMode`, `stats.markWatchedKey`, `immersionTracking.lifetimeSummaries.*`, and all seven `mpv.*` launcher options. Updated known-word cache docs and examples to recommend expression/word fields.
|
||||
|
||||
- **Architecture Docs:** Added a Playback Startup Flow diagram showing how managed launches inject the plugin, establish the IPC socket, and bring up the overlay via the two convergent triggers. Added a Runtime Sockets section and diagram to the IPC + Runtime Contracts page, with cross-reference pointers in the MPV Plugin and Troubleshooting pages.
|
||||
- **Architecture Docs:** Added a Playback Startup Flow diagram and a Runtime Sockets section and diagram to the IPC + Runtime Contracts page, with cross-reference pointers in the MPV Plugin and Troubleshooting pages.
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
## Highlights
|
||||
### Changed
|
||||
|
||||
- **Yomitan**: Updated the bundled Yomitan to the latest revision.
|
||||
- Picks up the newest lookup improvements and fixes from the SubMiner Yomitan fork.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Anki / Animated AVIF**: Clips with word audio no longer start or end early.
|
||||
- Clip boundaries are now snapped to the nearest AVIF frame edge, keeping audio lead-in and playback in sync.
|
||||
|
||||
- **macOS Overlay**: Resolved several interactivity and focus issues triggered by autoplay and modal windows.
|
||||
- After autoplay starts with "wait for overlay to be ready" enabled, subtitles are immediately hoverable and Yomitan lookups work - no longer require an extra click to activate.
|
||||
- After any modal closes (Settings, Stats, sidebar, etc.), the overlay and subtitles reappear automatically and mpv keyboard shortcuts (pause, seek, etc.) are restored to mpv right away, including in native fullscreen.
|
||||
|
||||
- **Hyprland Fullscreen Overlay**: Fixed overlay alignment when mpv is fullscreen on Hyprland.
|
||||
- Compositor client bounds are now verified before positioning, so the stats panel, modals, and subtitle sidebar no longer shift below the mpv window.
|
||||
|
||||
## Installation
|
||||
|
||||
See the README and docs/installation guide for full setup steps.
|
||||
|
||||
## Assets
|
||||
|
||||
- Linux: `SubMiner.AppImage`
|
||||
- macOS: `SubMiner-*.dmg` and `SubMiner-*.zip`
|
||||
- Windows: `SubMiner-*.exe` and `SubMiner-*-win.zip`
|
||||
- Optional extras: `subminer-assets.tar.gz` and the `subminer` launcher
|
||||
|
||||
Note: the `subminer` wrapper script uses Bun (`#!/usr/bin/env bun`), so `bun` must be installed and on `PATH`.
|
||||
@@ -43,6 +43,14 @@ function fragmentTypesInPrompt(input: string): string[] {
|
||||
.map((line) => line.slice('type: '.length).trim());
|
||||
}
|
||||
|
||||
function assertReleaseNotesPromptRequestsNestedBullets(input: string): void {
|
||||
assert.match(input, /In MODE: release-notes, use short top-level change bullets/);
|
||||
assert.match(input, /Nested bullets should cover the change, user benefit, and any user action/);
|
||||
assert.match(input, /Do not require the exact nested labels/);
|
||||
assert.match(input, /Keep nested bullets short, concrete, and readable by non-technical users/);
|
||||
assert.match(input, /Avoid paragraph-style release-note bullets/);
|
||||
}
|
||||
|
||||
function defaultPolishedBody(input: string): string {
|
||||
const mode = modeFromPrompt(input);
|
||||
const types = fragmentTypesInPrompt(input);
|
||||
@@ -171,6 +179,7 @@ test('writeChangelogArtifacts ignores README, groups fragments by type, writes r
|
||||
assert.match(releaseNotes, /## Highlights\n### Added\n- Polished: added entry\./);
|
||||
assert.match(releaseNotes, /### Fixed\n- Polished: fixed entry\./);
|
||||
assert.match(releaseNotes, /## Installation\n\nSee the README and docs\/installation guide/);
|
||||
assert.match(releaseNotes, /- Windows: `SubMiner-\*\.exe` and `SubMiner-\*-win\.zip`/);
|
||||
} finally {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
}
|
||||
@@ -437,6 +446,12 @@ test('writeChangelogArtifacts prompts Claude to summarize the final stable outco
|
||||
/Multiple fixes within the same prerelease cycle should collapse into one current-state bullet/,
|
||||
);
|
||||
}
|
||||
|
||||
const releaseNotesPrompt = stub.calls.find(
|
||||
(call) => modeFromPrompt(call.input) === 'release-notes',
|
||||
);
|
||||
assert.ok(releaseNotesPrompt, 'expected a release-notes Claude invocation');
|
||||
assertReleaseNotesPromptRequestsNestedBullets(releaseNotesPrompt.input);
|
||||
} finally {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
}
|
||||
@@ -706,6 +721,7 @@ test('writePrereleaseNotesForVersion prompts Claude to revise stale prerelease b
|
||||
prompt,
|
||||
/Multiple fixes within the same prerelease cycle should collapse into one current-state bullet/,
|
||||
);
|
||||
assertReleaseNotesPromptRequestsNestedBullets(prompt);
|
||||
} finally {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -260,14 +260,18 @@ You will receive a list of FRAGMENT entries below. Each fragment has metadata (t
|
||||
|
||||
</details>
|
||||
Do not include the Internal section at all in MODE: release-notes; internal fragments will not be present in the input for that mode.
|
||||
4. Each bullet should:
|
||||
- Lead with a short feature/area name in title case followed by a colon, e.g. "Playlist browser:", "Windows overlay:", "Stats dashboard:". Pick the name from the fragment's bullet content, not the raw 'area:' slug.
|
||||
4. Each top-level change item should:
|
||||
- Lead with a short feature/area name in title case. Pick the name from the fragment's bullet content, not the raw 'area:' slug.
|
||||
- Be written in user-facing language. Drop implementation jargon, internal class names, file paths, and PR numbers.
|
||||
- Be merged with related bullets when possible. If five fragments all touch Windows overlay z-order/focus/restore, write one or two bullets that summarize the overall improvement instead of five.
|
||||
- Drop bullets that only describe PR housekeeping, CodeRabbit follow-ups, or test-only changes that don't affect users.
|
||||
- Preserve the substance of breaking changes that remain breaking after applying the Release Outcome Rules. Do not soften or omit them.
|
||||
5. Do not invent features. Every bullet must be grounded in the input fragments.
|
||||
6. Do not include the version heading (## v...) — that wrapper is added by the caller.
|
||||
5. In MODE: changelog, each item may be a conventional single-level bullet, e.g. "- Playlist Browser: Adds faster saved-show browsing."
|
||||
6. In MODE: release-notes, use short top-level change bullets with two or three nested bullets when an item needs explanation.
|
||||
Nested bullets should cover the change, user benefit, and any user action or compatibility note when useful. Do not require the exact nested labels; natural phrasing is fine. Omit the action bullet when no action is needed.
|
||||
Keep nested bullets short, concrete, and readable by non-technical users. Avoid paragraph-style release-note bullets.
|
||||
7. Do not invent features. Every bullet must be grounded in the input fragments.
|
||||
8. Do not include the version heading (## v...) — that wrapper is added by the caller.
|
||||
|
||||
The input begins below.
|
||||
|
||||
@@ -485,6 +489,7 @@ function renderReleaseNotes(
|
||||
'',
|
||||
'- Linux: `SubMiner.AppImage`',
|
||||
'- macOS: `SubMiner-*.dmg` and `SubMiner-*.zip`',
|
||||
'- Windows: `SubMiner-*.exe` and `SubMiner-*-win.zip`',
|
||||
'- Optional extras: `subminer-assets.tar.gz` and the `subminer` launcher',
|
||||
'',
|
||||
'Note: the `subminer` wrapper script uses Bun (`#!/usr/bin/env bun`), so `bun` must be installed and on `PATH`.',
|
||||
|
||||
@@ -229,6 +229,14 @@ local ctx = {
|
||||
actionType = "mpv-command",
|
||||
command = { "quit" },
|
||||
},
|
||||
{
|
||||
key = {
|
||||
code = "MBTN_BACK",
|
||||
modifiers = {},
|
||||
},
|
||||
actionType = "mpv-command",
|
||||
command = { "sub-seek", -1 },
|
||||
},
|
||||
{
|
||||
key = {
|
||||
code = "KeyW",
|
||||
@@ -317,6 +325,7 @@ local expected_mpv_bindings = {
|
||||
{ keys = "L", command = { "sub-seek", 1 } },
|
||||
{ keys = "q", command = { "quit" } },
|
||||
{ keys = "Ctrl+w", command = { "quit" } },
|
||||
{ keys = "MBTN_BACK", command = { "sub-seek", -1 } },
|
||||
}
|
||||
|
||||
for _, expected in ipairs(expected_mpv_bindings) do
|
||||
|
||||
@@ -13,6 +13,7 @@ local function run_plugin_scenario(config)
|
||||
property_sets = {},
|
||||
periodic_timers = {},
|
||||
timeouts = {},
|
||||
timeout_handles = {},
|
||||
}
|
||||
|
||||
local function make_mp_stub()
|
||||
@@ -139,15 +140,17 @@ local function run_plugin_scenario(config)
|
||||
recorded.timeouts[#recorded.timeouts + 1] = seconds
|
||||
local timeout = {
|
||||
killed = false,
|
||||
callback = callback,
|
||||
}
|
||||
function timeout:kill()
|
||||
self.killed = true
|
||||
end
|
||||
|
||||
local delay = tonumber(seconds) or 0
|
||||
if callback and delay < 5 then
|
||||
if callback and delay < 5 and not config.defer_timeouts then
|
||||
callback()
|
||||
end
|
||||
recorded.timeout_handles[#recorded.timeout_handles + 1] = timeout
|
||||
return timeout
|
||||
end
|
||||
|
||||
@@ -612,6 +615,15 @@ local function fire_event(recorded, name, ...)
|
||||
end
|
||||
end
|
||||
|
||||
local function fire_pending_timeouts(recorded)
|
||||
for _, timeout in ipairs(recorded.timeout_handles or {}) do
|
||||
if not timeout.killed and timeout.callback then
|
||||
timeout.killed = true
|
||||
timeout.callback()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function fire_observer(recorded, name, value)
|
||||
local listeners = recorded.observers[name] or {}
|
||||
for _, listener in ipairs(listeners) do
|
||||
@@ -647,13 +659,88 @@ do
|
||||
assert_true(recorded ~= nil, "plugin failed to load for cold-start scenario: " .. tostring(err))
|
||||
assert_true(recorded.script_messages["subminer-start"] ~= nil, "subminer-start script message not registered")
|
||||
recorded.script_messages["subminer-start"]("texthooker=no")
|
||||
assert_true(find_start_call(recorded.async_calls) ~= nil, "expected cold-start to invoke --start command when process is absent")
|
||||
assert_true(
|
||||
find_start_call(recorded.async_calls) ~= nil,
|
||||
"expected cold-start to invoke --start command when process is absent"
|
||||
)
|
||||
assert_true(
|
||||
not has_sync_command(recorded.sync_calls, "ps"),
|
||||
"expected cold-start start command to avoid synchronous process list scan"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local scenario = {
|
||||
process_list = "",
|
||||
defer_timeouts = true,
|
||||
option_overrides = {
|
||||
binary_path = binary_path,
|
||||
auto_start = "yes",
|
||||
auto_start_visible_overlay = "yes",
|
||||
auto_start_pause_until_ready = "yes",
|
||||
socket_path = "/tmp/subminer-socket",
|
||||
},
|
||||
input_ipc_server = "/tmp/subminer-socket",
|
||||
path = "/media/episode-01.mkv",
|
||||
media_title = "Episode 1",
|
||||
files = {
|
||||
[binary_path] = true,
|
||||
},
|
||||
}
|
||||
local recorded, err = run_plugin_scenario(scenario)
|
||||
assert_true(recorded ~= nil, "plugin failed to load for warm playlist visibility scenario: " .. tostring(err))
|
||||
fire_event(recorded, "file-loaded")
|
||||
recorded.script_messages["subminer-autoplay-ready"]()
|
||||
fire_event(recorded, "end-file", { reason = "eof" })
|
||||
scenario.path = "/media/episode-02.mkv"
|
||||
scenario.media_title = "Episode 2"
|
||||
fire_event(recorded, "file-loaded")
|
||||
fire_pending_timeouts(recorded)
|
||||
assert_true(
|
||||
count_control_calls(recorded.async_calls, "--hide-visible-overlay") == 0,
|
||||
"warm playlist advance should cancel the end-file hide before it hides the next video's overlay"
|
||||
)
|
||||
assert_true(
|
||||
count_start_calls(recorded.async_calls) == 1,
|
||||
"warm playlist visibility reuse should not issue another --start command"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local scenario = {
|
||||
process_list = "",
|
||||
option_overrides = {
|
||||
binary_path = binary_path,
|
||||
auto_start = "yes",
|
||||
auto_start_visible_overlay = "no",
|
||||
auto_start_pause_until_ready = "yes",
|
||||
socket_path = "/tmp/subminer-socket",
|
||||
},
|
||||
input_ipc_server = "/tmp/subminer-socket",
|
||||
path = "/media/manual-episode-01.mkv",
|
||||
media_title = "Manual Episode 1",
|
||||
files = {
|
||||
[binary_path] = true,
|
||||
},
|
||||
}
|
||||
local recorded, err = run_plugin_scenario(scenario)
|
||||
assert_true(recorded ~= nil, "plugin failed to load for manual warm playlist visibility scenario: " .. tostring(err))
|
||||
recorded.script_messages["subminer-toggle"]()
|
||||
recorded.script_messages["subminer-autoplay-ready"]()
|
||||
fire_event(recorded, "end-file", { reason = "eof" })
|
||||
scenario.path = "/media/manual-episode-02.mkv"
|
||||
scenario.media_title = "Manual Episode 2"
|
||||
fire_event(recorded, "file-loaded")
|
||||
assert_true(
|
||||
count_control_calls(recorded.async_calls, "--hide-visible-overlay") == 0,
|
||||
"manual visible overlay should remain visible across warm playlist auto-start reattach"
|
||||
)
|
||||
assert_true(
|
||||
count_start_calls(recorded.async_calls) == 1,
|
||||
"manual warm playlist visibility reuse should not issue another --start command"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local scenario = {
|
||||
process_list = "",
|
||||
@@ -714,13 +801,13 @@ do
|
||||
"new media after prior playback should reuse the running overlay"
|
||||
)
|
||||
assert_true(
|
||||
count_property_set(recorded.property_sets, "pause", true) == 2,
|
||||
"new media after prior playback should re-arm pause-until-ready"
|
||||
count_property_set(recorded.property_sets, "pause", true) == 1,
|
||||
"new media after prior ready playback should not re-arm pause-until-ready"
|
||||
)
|
||||
recorded.script_messages["subminer-autoplay-ready"]()
|
||||
assert_true(
|
||||
count_property_set(recorded.property_sets, "pause", false) == 2,
|
||||
"new media after prior playback should resume only after readiness"
|
||||
count_property_set(recorded.property_sets, "pause", false) == 1,
|
||||
"new media after prior ready playback should not wait for another readiness signal"
|
||||
)
|
||||
end
|
||||
|
||||
@@ -1800,6 +1887,61 @@ do
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local recorded, err = run_plugin_scenario({
|
||||
process_list = "",
|
||||
option_overrides = {
|
||||
binary_path = binary_path,
|
||||
auto_start = "yes",
|
||||
auto_start_visible_overlay = "yes",
|
||||
auto_start_pause_until_ready = "yes",
|
||||
auto_start_pause_until_ready_owns_initial_pause = "yes",
|
||||
socket_path = "/tmp/subminer-socket",
|
||||
},
|
||||
input_ipc_server = "/tmp/subminer-socket",
|
||||
media_title = "Random Movie",
|
||||
paused = true,
|
||||
files = {
|
||||
[binary_path] = true,
|
||||
},
|
||||
})
|
||||
assert_true(recorded ~= nil, "plugin failed to load for launcher-owned pause-until-ready scenario: " .. tostring(err))
|
||||
fire_event(recorded, "file-loaded")
|
||||
assert_true(recorded.script_messages["subminer-autoplay-ready"] ~= nil, "subminer-autoplay-ready script message not registered")
|
||||
recorded.script_messages["subminer-autoplay-ready"]()
|
||||
assert_true(
|
||||
has_property_set(recorded.property_sets, "pause", false),
|
||||
"launcher-owned initial pause should resume when autoplay-ready arrives"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local recorded, err = run_plugin_scenario({
|
||||
process_list = "",
|
||||
option_overrides = {
|
||||
binary_path = binary_path,
|
||||
auto_start = "yes",
|
||||
auto_start_visible_overlay = "yes",
|
||||
auto_start_pause_until_ready = "yes",
|
||||
auto_start_pause_until_ready_owns_initial_pause = "yes",
|
||||
auto_start_pause_until_ready_timeout_seconds = 0.1,
|
||||
socket_path = "/tmp/subminer-socket",
|
||||
},
|
||||
input_ipc_server = "/tmp/subminer-socket",
|
||||
media_title = "Random Movie",
|
||||
paused = true,
|
||||
files = {
|
||||
[binary_path] = true,
|
||||
},
|
||||
})
|
||||
assert_true(recorded ~= nil, "plugin failed to load for launcher-owned pause timeout scenario: " .. tostring(err))
|
||||
fire_event(recorded, "file-loaded")
|
||||
assert_true(
|
||||
has_property_set(recorded.property_sets, "pause", false),
|
||||
"launcher-owned initial pause should resume when autoplay-ready timeout fires"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local recorded, err = run_plugin_scenario({
|
||||
process_list = "",
|
||||
@@ -1992,7 +2134,9 @@ do
|
||||
option_overrides = {
|
||||
binary_path = binary_path,
|
||||
auto_start = "no",
|
||||
socket_path = "/tmp/subminer-socket",
|
||||
},
|
||||
input_ipc_server = "/tmp/subminer-socket",
|
||||
files = {
|
||||
[binary_path] = true,
|
||||
},
|
||||
@@ -2000,9 +2144,30 @@ do
|
||||
assert_true(recorded ~= nil, "plugin failed to load for manual toggle command scenario: " .. tostring(err))
|
||||
assert_true(recorded.script_messages["subminer-toggle"] ~= nil, "subminer-toggle script message not registered")
|
||||
recorded.script_messages["subminer-toggle"]()
|
||||
local start_call = find_start_call(recorded.async_calls)
|
||||
assert_true(
|
||||
count_control_calls(recorded.async_calls, "--toggle-visible-overlay") == 1,
|
||||
"script-message toggle should issue explicit visible-overlay toggle command"
|
||||
start_call ~= nil,
|
||||
"first manual toggle from a stopped overlay should start SubMiner with mpv attachment"
|
||||
)
|
||||
assert_true(
|
||||
call_has_arg(start_call, "--managed-playback"),
|
||||
"first manual toggle should attach managed playback so subtitles reach the overlay"
|
||||
)
|
||||
assert_true(
|
||||
call_has_arg(start_call, "--socket") and call_has_arg(start_call, "/tmp/subminer-socket"),
|
||||
"first manual toggle should pass the active mpv socket to SubMiner"
|
||||
)
|
||||
assert_true(
|
||||
call_has_arg(start_call, "--show-visible-overlay"),
|
||||
"first manual toggle should start directly into visible overlay state"
|
||||
)
|
||||
assert_true(
|
||||
not call_has_arg(start_call, "--hide-visible-overlay"),
|
||||
"first manual toggle should not start hidden"
|
||||
)
|
||||
assert_true(
|
||||
count_control_calls(recorded.async_calls, "--toggle-visible-overlay") == 0,
|
||||
"first manual toggle should not issue a bare visible-overlay toggle before mpv is attached"
|
||||
)
|
||||
assert_true(
|
||||
count_control_calls(recorded.async_calls, "--toggle") == 0,
|
||||
|
||||
@@ -19,6 +19,7 @@ interface IntegrationTestContext {
|
||||
function createIntegrationTestContext(
|
||||
options: {
|
||||
highlightEnabled?: boolean;
|
||||
nPlusOneEnabled?: boolean;
|
||||
onFindNotes?: () => Promise<number[]>;
|
||||
onNotesInfo?: () => Promise<unknown[]>;
|
||||
stateDirPrefix?: string;
|
||||
@@ -59,6 +60,12 @@ function createIntegrationTestContext(
|
||||
knownWords: {
|
||||
highlightEnabled: options.highlightEnabled ?? true,
|
||||
},
|
||||
nPlusOne:
|
||||
options.nPlusOneEnabled === undefined
|
||||
? undefined
|
||||
: {
|
||||
enabled: options.nPlusOneEnabled,
|
||||
},
|
||||
},
|
||||
{} as never,
|
||||
{} as never,
|
||||
@@ -161,6 +168,47 @@ test('AnkiIntegration.refreshKnownWordCache bypasses stale checks', async () =>
|
||||
}
|
||||
});
|
||||
|
||||
test('AnkiIntegration.refreshKnownWordCache notifies annotation cache listeners', async () => {
|
||||
const ctx = createIntegrationTestContext({
|
||||
stateDirPrefix: 'subminer-anki-integration-refresh-notify-',
|
||||
});
|
||||
let notifications = 0;
|
||||
|
||||
try {
|
||||
ctx.integration.setKnownWordCacheUpdatedCallback(() => {
|
||||
notifications += 1;
|
||||
});
|
||||
|
||||
await ctx.integration.refreshKnownWordCache();
|
||||
|
||||
assert.equal(notifications, 1);
|
||||
} finally {
|
||||
cleanupIntegrationTestContext(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
test('AnkiIntegration.refreshKnownWordCache notifies when n+1 is enabled without highlights', async () => {
|
||||
const ctx = createIntegrationTestContext({
|
||||
highlightEnabled: false,
|
||||
nPlusOneEnabled: true,
|
||||
stateDirPrefix: 'subminer-anki-integration-nplusone-notify-',
|
||||
});
|
||||
let notifications = 0;
|
||||
|
||||
try {
|
||||
ctx.integration.setKnownWordCacheUpdatedCallback(() => {
|
||||
notifications += 1;
|
||||
});
|
||||
|
||||
await ctx.integration.refreshKnownWordCache();
|
||||
|
||||
assert.equal(ctx.calls.findNotes, 1);
|
||||
assert.equal(notifications, 1);
|
||||
} finally {
|
||||
cleanupIntegrationTestContext(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
test('AnkiIntegration.refreshKnownWordCache skips work when highlight mode is disabled', async () => {
|
||||
const ctx = createIntegrationTestContext({
|
||||
highlightEnabled: false,
|
||||
|
||||
@@ -526,7 +526,9 @@ export class AnkiIntegration {
|
||||
}
|
||||
|
||||
private isKnownWordCacheEnabled(): boolean {
|
||||
return this.config.knownWords?.highlightEnabled === true;
|
||||
return (
|
||||
this.config.knownWords?.highlightEnabled === true || this.config.nPlusOne?.enabled === true
|
||||
);
|
||||
}
|
||||
|
||||
private getConfiguredAnkiTags(): string[] {
|
||||
@@ -549,7 +551,11 @@ export class AnkiIntegration {
|
||||
}
|
||||
|
||||
async refreshKnownWordCache(): Promise<void> {
|
||||
return this.knownWordCache.refresh(true);
|
||||
const shouldNotify = this.isKnownWordCacheEnabled();
|
||||
await this.knownWordCache.refresh(true);
|
||||
if (shouldNotify) {
|
||||
this.notifyKnownWordCacheUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
private appendKnownWordsFromNoteInfo(noteInfo: NoteInfo): void {
|
||||
|
||||
@@ -520,6 +520,36 @@ test('KnownWordCacheManager uses the current deck fields for immediate append',
|
||||
}
|
||||
});
|
||||
|
||||
test('KnownWordCacheManager uses all configured deck fields for immediate append without a current deck', () => {
|
||||
const config: AnkiConnectConfig = {
|
||||
deck: '',
|
||||
fields: {
|
||||
word: 'Expression',
|
||||
},
|
||||
knownWords: {
|
||||
highlightEnabled: true,
|
||||
decks: {
|
||||
'Kaishi 1.5k': ['Word'],
|
||||
Minecraft: ['Expression', 'Word'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const { manager, cleanup } = createKnownWordCacheHarness(config);
|
||||
|
||||
try {
|
||||
manager.appendFromNoteInfo({
|
||||
noteId: 1,
|
||||
fields: {
|
||||
Expression: { value: '別人' },
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(manager.isKnownWord('別人'), true);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('KnownWordCacheManager reports immediate append cache clears as mutations', () => {
|
||||
const config: AnkiConnectConfig = {
|
||||
fields: {
|
||||
|
||||
@@ -326,7 +326,18 @@ export class KnownWordCacheManager {
|
||||
: null;
|
||||
|
||||
if (!selectedDeckEntry) {
|
||||
return null;
|
||||
const configuredFields = trimmedDeckEntries.flatMap(([, fields]) =>
|
||||
Array.isArray(fields) ? fields : [],
|
||||
);
|
||||
const normalizedFields = [
|
||||
...new Set(
|
||||
configuredFields
|
||||
.map(String)
|
||||
.map((field) => field.trim())
|
||||
.filter((field) => field.length > 0),
|
||||
),
|
||||
];
|
||||
return normalizedFields.length > 0 ? normalizedFields : this.getDefaultKnownWordFields();
|
||||
}
|
||||
|
||||
const deckFields = selectedDeckEntry[1];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user