* feat(controller): add hold + press combos to main bindings
Main Alt+C actions can now be learned as combos such as L1 + D-pad right.
A combo fires on each press while its hold button is down, and claims its
press input so token move and popup scroll ignore it until it is released.
A plain action on a hold button fires on release, unless another bound
button was pressed during the hold.
The save path needed two changes for combos to persist. The IPC validator
rebuilt bindings field by field and dropped `hold`, so Alt+C showed a combo
from its local copy until the next reload turned it back into a plain
button. Validator and config resolver now share one parser. Saves also
replace the controller section, since the deep merge in patchRawConfig kept
the old `hold` when a combo was rebound to one button.
Also fixes Learn on the (D-pad) rows, which threw after cancel() cleared
the capture target.
* test(launcher): wait for content in background child test
The test polled for the fake app's output files to exist, but the shell
creates a redirect target before writing to it. A poll landing in that gap
read an empty env file and failed with '' !== '1', as seen once in CI.
Wait until both files have content instead.
* fix(controller): keep a diagonal push from learning as a combo
Learn took the first axis of a diagonal stick push as the hold and the
second as the press, saving a combo that one axis could never trigger. This
reached main Alt+C rows when they moved to combo capture. Axes crossing the
threshold in the same poll as the first input now count as one movement. A
second axis moved in a later poll still records a combo.
* fix(controller): only treat simultaneous axes as one movement
The diagonal push guard also blocked an axis activated in the same poll as a
button, so L1 plus a stick direction could not record the stick until it was
released and moved again. Apply the guard only when the first input is an
axis, so that input pair records as a combo again.
- Add createControllerHintVisibility: use the dialog window's pad, else ask main which controller the overlay is reading, at most once per second while a dialog is open
- Hide shortcut tags when controller support is on but no controller is present
- Update the Anki integration docs and changelog fragments to match
- Remove the auto-jimaku example, now covered by the built-in Jimaku auto-open
- Drop its plugins docs section and changelog bullet
- List the keyboard-only `C` key in Keyboard shortcuts
- Point the configuration reference to Controller support for button defaults
- Show every pitch accent in each popup entry, drawn on the headword's reading
- Split headword furigana per kanji and add Yomitan-style definition tags
- Send controller connected and disabled messages over a new IPC channel so they follow ankiConnect.behavior.notificationType
- Remove the separate controller status toast from the overlay
- Only announce connected controllers when controller.enabled is on
- Announce a controller again after it reconnects, and update usage docs
mpv only sends the latest value of an observed property, and only after its
IPC thread has handled queued commands and events. A same-file
`loadfile replace` can therefore arrive with no `path=null`, so path
comparisons see no change, the autoplay gate stays released for that path and
drops every ready signal, and playback stays paused.
Track an `end-file` that mpv has not followed with a different path. If it is
still pending at `file-loaded`, replay the clear and read the path back, so
consumers see the same sequence as any other load. `file-loaded` waits for the
reply so plugins can read their media snapshot during the event.
Fixes#320
- Add `subtitleFilter` config with four presets: parentheses, brackets, speaker prefix and music symbols. Custom regex rules run after the presets.
- Filter primary text where it enters the app (live `sub-text` and parsed cues), so the overlay, sidebar, mining and stats all get the same filtered text.
- Changes hot-reload: the current line is read again and the active track is parsed again, using the filter config as the prefetch parse key.
- Add a custom rule editor in Settings under Behavior → Subtitle Filters that checks patterns as you type.
- Update the config example, the configuration docs and the architecture domains doc, and add a changelog fragment.
* fix(plugin): keep loading OSD off on warm playlist advance
- Track overlay_content_ready_seen when the app sends subminer-overlay-loading-ready
- Skip restarting the "Overlay loading" spinner for an overlay that already reported ready, so it no longer times out into "Overlay did not become ready" (#315)
- Reset the flag on stop, restart, start failure and shutdown
- Add start-gate tests for warm and not-yet-ready playlist advances
* fix(plugin): only report overlay ready once content has loaded
Hide paths dismissed the loading OSD with subminer-overlay-loading-ready,
which the plugin now remembers to skip the spinner on later playlist files.
A hide before the overlay had content would wrongly mark it ready. The app
now sends ready only when the overlay window content is ready, and a new
stop-only subminer-overlay-loading-dismissed message otherwise.
* fix(plugin): resync overlay readiness on dismissal and mpv connect
A dismissal now clears the plugin's readiness flag, since the app sends it
only while its overlay window has no content (e.g. a replaced window).
On mpv (re)connect the app resends subminer-overlay-loading-ready when the
overlay already has content, so a ready signal lost while mpv was away, or
sent before this mpv's plugin existed, still reaches the plugin.
* refactor(main): pass overlay readiness handler directly
- Generated sentence audio no longer overwrites Yomitan's ExpressionAudio on default configs
- fields.wordAudio still defaults to ExpressionAudio
- Regenerate config.example.jsonc and update the Anki and configuration docs
- Add changelog fragment
- uutils readlink (Ubuntu 26.04+) aborts at the first unreadable /proc/<pid>/exe, so the keepalive released the mount early
- Add mount_busy helper that readlinks each link individually
- Add changelog fragment
- Shorten the Hachidori backend fragment to its user-visible outcomes
- Move the fork submodule note into an internal fragment
- Split stats dashboard history-line mining into its own fixed fragment
- Reclassify the setup launcher label fragment as internal
- Drop the Electron/undici security fragment
* fix(subtitles): widen halfwidth katakana where subtitle text enters the app
Broadcast-derived Japanese subs write katakana halfwidth (アンタ, ダキニ), so
overlay lines mixed squashed and normal-width text. Normalize live mpv
sub-text and parsed cue text to fullwidth so display, lookups, mining,
and stats all see one form. The tokenizer reuses the shared helper.
* fix(subtitles): fold stray voiced marks and widen before cue dedup
- A lone halfwidth ゙ after fullwidth kana (ガ) now composes to ガ
instead of leaving a combining mark that broke name matching.
- Parsed cue text is widened before duplicate merging so ガク and ガク
on the same span collapse into one sidebar row.
- The YouTube track-load refresh widens the sub-text it reads directly.
* fix(stats): widen halfwidth katakana in stored lines and mined cards (#296)
Lines recorded before ingest widening still hold halfwidth katakana.
Occurrence and sentence-search queries widen line text on read, and the
mine-card route widens the sentence and word it receives, so the
dashboard and cards mined from history match newer rows. Word identity
columns are left as stored since the dashboard queries by them.
* fix(stats): match katakana in either width in sentence search
- Add narrowFullwidthKana, derived from widenHalfwidthKana so the two stay in sync
- Match subtitle text against the original, widened, and narrowed forms of each term so a search finds older lines stored as halfwidth
* feat(subtitles): generate Japanese subtitles during Jellyfin playback
- Read the item's original file via toJellyfinOriginalStreamUrl and write output to a temp dir
- Transcoded playback uses Jellyfin's audio index and only external references
- Show Jellyfin media by title and redact URL tokens in subprocess errors
- Update overlay copy, docs, and changelog fragment
* fix(subtitles): redact full stream tokens and reuse one Jellyfin output folder
* test(subtitles): make Jellyfin output path assertions platform-neutral
- Compare dirname/basename via node:path instead of POSIX-only regexes
- Assert the full sub-add command uses the generated output path
Drop 16 overlay preload methods with no caller (onSubtitleAss,
getCurrentSubtitleAss, onVisibility, getOverlayVisibility,
openYomitanSettings, quitApp, toggleOverlay, getMecabStatus,
setMecabEnabled, getKeybindings, saveControllerPreference,
getAnkiConnectStatus, setAnkiConnectEnabled, clearAnkiConnectHistory,
cycleRuntimeOption, appendClipboardVideoToQueue) along with their
IPC_CHANNELS keys, handlers, validators and dep plumbing.
With both ASS consumers gone, stop observing sub-text/ass in mpv and
remove the subtitle-ass-change event chain and appState.currentSubAssText.
The main-process functions these wrapped stay; tray, CLI, shortcuts and
session actions still use them.
* refactor: remove dead code and enable noUnusedLocals
- delete launcher/jimaku.ts and the launcher Args/config jimaku* fields
that only fed it (the SUBMINER_JIMAKU_* env vars had no effect)
- delete plugin/subminer/hover.lua; nothing has sent subminer-hover-token
since Overlay 2.0
- drop never-read anilist.characterDictionary.refreshTtlHours and
evictionPolicy (still hidden in the settings UI for old configs)
- drop unused root deps: eslint, @fontsource-variable/geist{,-mono}
(stats/ declares its own)
- delete the committed draft renderer-performance plan doc
- turn on noUnusedLocals for typecheck and remove what it surfaced,
including Windows focus-handoff values computed but never consumed
* docs(changes): add docs fragment and note retired hover messages
* test: add headless end-to-end harness and CI job
Boot the built app against a real mpv, a synthetic clip, a fixture Yomitan
dictionary, and a fake AnkiConnect inside a private Xvfb display, then assert
on the overlay DOM and the AnkiConnect requests. Adds the test:e2e lane, an
e2e session CLI for exploratory checks, and a three-platform CI job.
* ci: set e2e temp dir from a step
* test: make e2e session startup robust on slow machines
Bun caps node:test hooks at 5s, so boot the session inside the first test.
Keep pausing until the app's resume retries stop, read cue tokens in one
round trip, and resolve Electron through its package entry so CI downloads
the binary.
* test: harden e2e harness for CI runners
Fail fast when mpv cannot be spawned, read every Yomitan popup frame instead
of trusting target order, and put the chocolatey mpv directory on PATH.
* test: add e2e failure diagnostics and mark Windows job experimental
Report what the overlay showed when a cue never renders, dispose the session
when the overlay cannot be opened, skip popup frames that vanish mid-poll,
and turn off AniSkip lookups in the harness config.
* test: read the lookup popup entry from textContent
* test: capture renderer console and main-side subtitle state in e2e failures
* test: expose the Electron main-process inspector to the e2e harness
Adds session.mainProcess() and 'e2e eval main', and reports main's window
list alongside the overlay state when a cue never renders.
* test: tap subtitle sends and receipts in e2e failure diagnostics
* test: probe alternate main-to-renderer paths in e2e failure diagnostics
* test: select the visible overlay layer, not the primed modal window
Windows and macOS pre-create a hidden modal window that loads the same
renderer document, and the harness could attach to it instead of the
visible overlay. Drops the delivery taps that found this.
* ci: run the Windows e2e job as a normal matrix entry
* test: address review on e2e harness
Slice the app log at a byte offset before decoding, create the session state
file owner-only since it carries the inspector URL, and keep the cue failure
when inspector diagnostics are unavailable.
* test: create the e2e session state file exclusively
- Rewrite polish prompt to default to inline items, nesting only for several distinct changes
- Add length/density example and drop internal details from release bodies
- Tell prerelease mode to shorten long baseline bullets
- Update changes/README.md and prompt assertions in tests
- Install MeCab from the AUR (mecab-git) on Arch, since it is not in the official repos
- Rewrite the README feature blurbs, requirements table, and quick start so they match docs-site/installation.md
- Add fuse2, X11 window tools, whisper.cpp, and the thumbnail tools to the README requirements
- Drop the playlist browser and texthooker sections and the standalone launcher download steps
- Mention AniList as a guessit consumer in the installation guide