Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d7c80f2e4 | |||
|
2f17859b7b
|
|||
|
9c7e02cbf0
|
|||
|
5f320edab5
|
3
.github/workflows/ci.yml
vendored
@@ -46,6 +46,9 @@ jobs:
|
||||
# Keep explicit typecheck for fast fail before full build/bundle.
|
||||
run: bun run typecheck
|
||||
|
||||
- name: Verify generated config examples
|
||||
run: bun run verify:config-example
|
||||
|
||||
- name: Test suite (source)
|
||||
run: bun run test:fast
|
||||
|
||||
|
||||
3
.github/workflows/release.yml
vendored
@@ -295,6 +295,9 @@ jobs:
|
||||
- name: Enforce generated launcher workflow
|
||||
run: bash scripts/verify-generated-launcher.sh
|
||||
|
||||
- name: Verify generated config examples
|
||||
run: bun run verify:config-example
|
||||
|
||||
- name: Package optional assets bundle
|
||||
run: |
|
||||
tar -czf "release/subminer-assets.tar.gz" \
|
||||
|
||||
79
AGENTS.md
@@ -1,11 +1,55 @@
|
||||
# AGENTS.MD
|
||||
|
||||
## Quick Start
|
||||
|
||||
- Read [`docs-site/development.md`](./docs-site/development.md) and [`docs-site/architecture.md`](./docs-site/architecture.md) before substantial changes; follow them unless task requires deviation.
|
||||
- Init workspace: `git submodule update --init --recursive`.
|
||||
- Install deps: `make deps` or `bun install` plus `(cd vendor/texthooker-ui && bun install --frozen-lockfile)`.
|
||||
- Fast dev loop: `make dev-watch`.
|
||||
- Full local run: `bun run dev`.
|
||||
- Verbose Electron debug: `electron . --start --dev --log-level debug`.
|
||||
|
||||
## Build / Test
|
||||
|
||||
- Use repo package manager/runtime only: Bun (`packageManager: bun@1.3.5`).
|
||||
- Default handoff gate:
|
||||
`bun run typecheck`
|
||||
`bun run test:fast`
|
||||
`bun run test:env`
|
||||
`bun run build`
|
||||
`bun run test:smoke:dist`
|
||||
- If `docs-site/` changed, also run:
|
||||
`bun run docs:test`
|
||||
`bun run docs:build`
|
||||
- Formatting: prefer `make pretty` and `bun run format:check:src`; use `bun run format` only intentionally.
|
||||
- Keep verification observable; capture failing command + exact error in notes/handoff.
|
||||
|
||||
## Change-Specific Checks
|
||||
|
||||
- Config/schema/defaults changes: run `bun run test:config`; if config template/defaults changed, run `bun run generate:config-example`.
|
||||
- Launcher/plugin changes: run `bun run test:launcher` or `bun run test:env`; use `bun run test:launcher:smoke:src` for focused launcher e2e checks.
|
||||
- Runtime-compat or compiled/dist-sensitive changes: run `bun run test:runtime:compat`.
|
||||
- Docs-only changes: at least `bun run docs:test` if docs behavior/assertions changed; `bun run docs:build` before handoff.
|
||||
|
||||
## Generated / Sensitive Files
|
||||
|
||||
- Launcher source of truth: `launcher/*.ts`.
|
||||
- Generated launcher artifact: `dist/launcher/subminer`; never hand-edit it.
|
||||
- Repo-root `./subminer` is stale artifact path; do not revive/use it.
|
||||
- `bun run build` rebuilds bundled Yomitan from `vendor/subminer-yomitan`; check submodules before debugging build failures.
|
||||
- Avoid changing packaging/signing identifiers (`build.appId`, mac entitlements, signing-related settings) unless task explicitly requires it.
|
||||
|
||||
## Docs
|
||||
|
||||
- Docs site lives in-repo under [`docs-site/`](./docs-site/).
|
||||
- Update docs for new/breaking behavior; no ship with stale docs.
|
||||
- Make sure [`docs-site/changelog.md`](./docs-site/changelog.md) is updated on each release.
|
||||
|
||||
## PR Feedback
|
||||
|
||||
- Active PR: `gh pr view --json number,title,url --jq '"PR #\\(.number): \\(.title)\\n\\(.url)"'`.
|
||||
- PR comments: `gh pr view …` + `gh api …/comments --paginate`.
|
||||
- Replies: cite fix + file/line; resolve threads only after fix lands.
|
||||
- When merging a PR: thank the contributor in `CHANGELOG.md`.
|
||||
|
||||
## Changelog
|
||||
|
||||
@@ -21,49 +65,16 @@
|
||||
- Release prep: `bun run changelog:build`, review `CHANGELOG.md` + `release/release-notes.md`, commit generated changelog + fragment deletions, then tag.
|
||||
- Release CI expects committed changelog entry already present; do not rely on tag job to invent notes.
|
||||
|
||||
## Docs
|
||||
|
||||
- The documentation for this project lives in a separate sister directory: [`../subminer-docs/`](https://github.com/ksyasuda/subminer-docs)
|
||||
- Read the [`../subminer-docs/development.md`](https://github.com/ksyasuda/subminer-docs/blob/main/development.md) and [`../subminer-docs/architecture.md`](https://github.com/ksyasuda/subminer-docs/blob/main/architecture.md) before making changes and follow the guidelines unless there is a specific reason to deviate
|
||||
- Make sure the changelog [`../subminer-docs/changelog.md`](https://github.com/ksyasuda/subminer-docs/blob/main/changelog.md) is updated on each release
|
||||
- Ensure the docs are kept up to date if any new or breaking changes are introduced
|
||||
- If the sister repo does not exist at `../subminer-docs`, then do not attempt
|
||||
to update the docs and reference the files from GitHub if needed
|
||||
|
||||
## Flow & Runtime
|
||||
|
||||
- Use repo’s package manager/runtime; no swaps w/o approval.
|
||||
- Use Codex background for long jobs; tmux only for interactive/persistent (debugger/server).
|
||||
|
||||
## Build / Test
|
||||
|
||||
- Before handoff: run full gate (lint/typecheck/tests/docs).
|
||||
- CI red: `gh run list/view`, rerun, fix, push, repeat til green.
|
||||
- Keep it observable (logs, panes, tails, MCP/browser tools).
|
||||
|
||||
## Git
|
||||
|
||||
- Safe by default: `git status/diff/log`. Push only when user asks.
|
||||
- `git checkout` ok for PR review / explicit request.
|
||||
- Branch changes require user consent.
|
||||
- Destructive ops forbidden unless explicit (`reset --hard`, `clean`, `restore`, `rm`, …).
|
||||
- Don’t delete/rename unexpected stuff; stop + ask.
|
||||
- No repo-wide S/R scripts; keep edits small/reviewable.
|
||||
- Avoid manual `git stash`; if Git auto-stashes during pull/rebase, that’s fine (hint, not hard guardrail).
|
||||
- If user types a command (“pull and push”), that’s consent for that command.
|
||||
- No amend unless asked.
|
||||
- Big review: `git --no-pager diff --color=never`.
|
||||
- Multi-agent: check `git status/diff` before edits; ship small commits.
|
||||
|
||||
## Language/Stack Notes
|
||||
|
||||
- Swift: use workspace helper/daemon; validate `swift build` + tests; keep concurrency attrs right.
|
||||
- TypeScript: use repo PM; keep files small; follow existing patterns.
|
||||
|
||||
## macOS Permissions / Signing (TCC)
|
||||
|
||||
- Never re-sign / ad-hoc sign / change bundle ID as “debug” without explicit ok (can mess TCC).
|
||||
|
||||
<!-- BACKLOG.MD MCP GUIDELINES START -->
|
||||
|
||||
<CRITICAL_INSTRUCTION>
|
||||
|
||||
11
CHANGELOG.md
@@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## v0.6.0 (2026-03-12)
|
||||
|
||||
### Added
|
||||
- Overlay: Added Chrome Gamepad API controller support for keyboard-only overlay mode, including configurable logical bindings for lookup, mining, popup navigation, Yomitan audio, mpv pause, d-pad fallback navigation, and slower smooth popup scrolling.
|
||||
- Overlay: Added `Alt+C` controller selection and `Alt+Shift+C` controller debug modals, with preferred controller persistence and live raw input inspection.
|
||||
- Overlay: Added a transient in-overlay controller-detected indicator when a controller is first found.
|
||||
- Overlay: Fixed stale keyboard-only token highlight cleanup when keyboard-only mode turns off or the Yomitan popup closes.
|
||||
|
||||
### Internal
|
||||
- Config: add an enforced `verify:config-example` gate so checked-in example config artifacts cannot drift silently
|
||||
|
||||
## v0.5.6 (2026-03-10)
|
||||
|
||||
### Fixed
|
||||
|
||||
1
Makefile
@@ -165,7 +165,6 @@ generate-config: ensure-bun
|
||||
@bun run electron . --generate-config
|
||||
|
||||
generate-example-config: ensure-bun
|
||||
@bun run build
|
||||
@bun run generate:config-example
|
||||
|
||||
dev-start: ensure-bun
|
||||
|
||||
18
README.md
@@ -35,6 +35,22 @@ SubMiner is an Electron overlay that sits on top of mpv. It turns your video pla
|
||||
|
||||
### 1. Install
|
||||
|
||||
**Arch Linux (AUR):**
|
||||
|
||||
Install [`subminer-bin`](https://aur.archlinux.org/packages/subminer-bin) from the AUR. It installs the packaged AppImage plus the `subminer` wrapper:
|
||||
|
||||
```bash
|
||||
paru -S subminer-bin
|
||||
```
|
||||
|
||||
Or manually:
|
||||
|
||||
```bash
|
||||
git clone https://aur.archlinux.org/subminer-bin.git
|
||||
cd subminer-bin
|
||||
makepkg -si
|
||||
```
|
||||
|
||||
**Linux (AppImage):**
|
||||
|
||||
```bash
|
||||
@@ -102,7 +118,7 @@ Windows builds use native window tracking and do not require the Linux composito
|
||||
|
||||
## Documentation
|
||||
|
||||
For full guides on configuration, Anki, Jellyfin, and more, see [docs.subminer.moe](https://docs.subminer.moe).
|
||||
For full guides on configuration, Anki, Jellyfin, and more, see [docs.subminer.moe](https://docs.subminer.moe). The VitePress source for that site lives in [`docs-site/`](./docs-site/).
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
id: TASK-155
|
||||
title: Move user docs site back into main repo
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: '2026-03-10 19:20'
|
||||
updated_date: '2026-03-10 19:38'
|
||||
labels: []
|
||||
dependencies: []
|
||||
priority: medium
|
||||
ordinal: 15500
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
|
||||
Move the standalone VitePress docs site from the sibling `../subminer-docs` checkout back into the main `SubMiner` repo so docs can be updated alongside code and local tooling can reference one repository.
|
||||
|
||||
Scope:
|
||||
|
||||
- import the tracked docs-site source into a dedicated in-repo subdirectory
|
||||
- update scripts/tests/docs instructions that assume a sibling `../subminer-docs` checkout
|
||||
- preserve Cloudflare Pages deployability from a repo subdirectory
|
||||
- verify the app repo and docs site both still build/test from the new layout
|
||||
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
<!-- AC:BEGIN -->
|
||||
|
||||
- [x] #1 The user-facing VitePress docs source lives inside the `SubMiner` repo in a dedicated subdirectory.
|
||||
- [x] #2 First-party scripts/tests/docs no longer require `../subminer-docs` for normal operation.
|
||||
- [x] #3 In-repo docs instructions include the Cloudflare Pages subdirectory deploy settings.
|
||||
- [x] #4 Verification covers the relocated docs site build/tests plus affected app-repo checks.
|
||||
|
||||
<!-- AC:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
|
||||
Imported the VitePress site into `docs-site/` inside the main repo and updated project instructions, docs contributor guidance, generator logic, and regression tests to treat that in-repo directory as the docs source of truth.
|
||||
|
||||
Added root proxy scripts for `docs:dev`, `docs:build`, `docs:preview`, and `docs:test`, repointed config-example generation to `docs-site/public/config.example.jsonc`, switched docs edit links to the main `SubMiner` repo, and documented the Cloudflare Pages subdirectory settings (`docs-site` root, `.vitepress/dist` output, `docs-site/**` watch path).
|
||||
|
||||
Verified with `bun run format:check:src`, `bun run typecheck`, `bun run docs:test`, `bun run docs:build`, `bun run test:config:src`, and `bun run test:fast`.
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
id: TASK-156
|
||||
title: Fix docs-site Plausible geo attribution through analytics worker
|
||||
status: In Progress
|
||||
assignee: []
|
||||
created_date: '2026-03-11 02:19'
|
||||
updated_date: '2026-03-11 02:44'
|
||||
labels:
|
||||
- docs-site
|
||||
- analytics
|
||||
dependencies: []
|
||||
priority: medium
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Investigate and fix missing city/region data for docs.subminer.moe visits in Plausible. The docs site already proxies analytics events to worker.subminer.moe, so the remaining work is to verify and correct the worker-side forwarding contract Plausible needs for geolocation.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 The analytics worker forwards Plausible event requests in a way that preserves the original client IP information needed for location attribution.
|
||||
- [ ] #2 The docs-site analytics flow remains proxied through worker.subminer.moe after the fix.
|
||||
- [ ] #3 Coverage or documentation records the worker-side header/forwarding requirement for Plausible geo reporting.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Worker implementation lives in `/home/sudacode/projects/blog-proxy`, not in the SubMiner repo.
|
||||
|
||||
Patched `worker.js` to forward client IP via `x-forwarded-for`/`x-real-ip` from `cf-connecting-ip` (fallbacks retained) and added `worker.test.js` regression coverage.
|
||||
|
||||
Local verification in `blog-proxy`: `node --test worker.test.js` passes. Deployment not performed in this session.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
id: TASK-157
|
||||
title: Fix Cloudflare Pages watch path for docs-site
|
||||
status: In Progress
|
||||
assignee: []
|
||||
created_date: '2026-03-10 20:15'
|
||||
updated_date: '2026-03-10 20:15'
|
||||
labels:
|
||||
- docs-site
|
||||
- cloudflare
|
||||
dependencies: []
|
||||
priority: medium
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Cloudflare Pages skipped a docs-site deployment after the docs repo moved into the main `SubMiner` repository. The documented/configured watch path uses `docs-site/**`, but Pages monorepo watch paths use a single `*` wildcard pattern. Correct the documented setting and leave a regression test so future repo moves or docs rewrites do not reintroduce the bad pattern.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 Docs contributor guidance points Cloudflare Pages watch paths at `docs-site/*`, not `docs-site/**`.
|
||||
- [ ] #2 Regression coverage fails if the docs revert to the incorrect watch-path string.
|
||||
- [ ] #3 Implementation notes record that the Cloudflare dashboard setting must be updated manually and the docs deploy retriggered.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Added docs regression coverage in `docs-site/docs-sync.test.ts` for the Pages watch-path string, then corrected the Cloudflare Pages instructions in `docs-site/README.md` and `docs-site/development.md`.
|
||||
|
||||
Manual follow-up still required outside git: update the Cloudflare Pages project include path from `docs-site/**` to `docs-site/*`, then trigger a fresh deployment against `main`.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
id: TASK-158
|
||||
title: Enforce generated config example drift checks
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: '2026-03-10 20:35'
|
||||
updated_date: '2026-03-10 20:35'
|
||||
labels:
|
||||
- config
|
||||
- docs-site
|
||||
dependencies: []
|
||||
priority: medium
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
The generated `config.example.jsonc` artifact is covered by generation-path tests, but there is no hard gate that fails when the checked-in example drifts from the canonical template. The in-repo docs-site copy can also drift silently.
|
||||
|
||||
Scope:
|
||||
|
||||
- add a first-party verification path that compares generated config-example content against committed artifacts
|
||||
- fail fast when repo-root `config.example.jsonc` is stale or missing
|
||||
- fail fast when `docs-site/public/config.example.jsonc` is stale or missing, when the docs site exists
|
||||
- wire the verification into the normal gate and release flow
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 Automated verification fails when repo-root `config.example.jsonc` is missing or stale.
|
||||
- [x] #2 Automated verification fails when in-repo docs-site `public/config.example.jsonc` is missing or stale, when docs-site exists.
|
||||
- [x] #3 CI/release or equivalent project gates run the verification automatically.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Added `src/verify-config-example.ts`, which renders the canonical config template and compares it against the checked-in repo-root `config.example.jsonc` plus `docs-site/public/config.example.jsonc` when the docs site exists.
|
||||
|
||||
Wired the new verification into `package.json` as `bun run verify:config-example`, added regression coverage for missing and stale artifacts, and enforced the new check in both CI and release workflows.
|
||||
|
||||
Regenerated the checked-in config example artifacts so the new gate passes in the repo-local docs-site layout, and documented the release-step expectation in `docs/RELEASING.md`.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
id: TASK-159
|
||||
title: Add overlay controller support for keyboard-only mode
|
||||
status: Done
|
||||
assignee:
|
||||
- codex
|
||||
created_date: '2026-03-11 00:30'
|
||||
updated_date: '2026-03-11 04:05'
|
||||
labels:
|
||||
- enhancement
|
||||
- renderer
|
||||
- overlay
|
||||
- input
|
||||
dependencies:
|
||||
- TASK-86
|
||||
references:
|
||||
- src/renderer/handlers/keyboard.ts
|
||||
- src/renderer/renderer.ts
|
||||
- src/renderer/state.ts
|
||||
- src/renderer/index.html
|
||||
- src/renderer/style.css
|
||||
- src/preload.ts
|
||||
- src/types.ts
|
||||
- src/config/definitions/defaults-core.ts
|
||||
- src/config/definitions/options-core.ts
|
||||
- src/config/definitions/template-sections.ts
|
||||
- config.example.jsonc
|
||||
priority: medium
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
|
||||
Add Chrome Gamepad API support to the visible overlay as a supplement to keyboard-only mode. By default SubMiner should bind to the first available controller, allow the user to pick and persist a preferred controller, expose a raw-input debug modal, and map controller actions onto the existing keyboard-only/Yomitan flow without breaking keyboard input. Also fix the current keyboard-only cleanup bug so the selected-token highlight clears when keyboard-only mode turns off or when the Yomitan popup closes.
|
||||
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
<!-- AC:BEGIN -->
|
||||
|
||||
- [x] #1 Controller input is ignored unless keyboard-only mode is enabled, except the controller binding for toggling keyboard-only mode itself.
|
||||
- [x] #2 Default logical mappings work: smooth popup scroll, token selection, lookup toggle/close, mining, Yomitan audio navigation/play, and mpv play/pause.
|
||||
- [x] #3 Controller config supports named logical bindings plus tuning knobs (preferred controller, deadzones, smooth-scroll speed/repeat), not raw axis/button maps.
|
||||
- [x] #4 `Alt+C` opens a controller selection modal listing connected controllers; saving a choice persists the preferred controller for next launch.
|
||||
- [x] #5 `Alt+Shift+C` opens a debug modal showing live raw controller axes/buttons as seen by SubMiner.
|
||||
- [x] #6 Keyboard-only selection highlight clears immediately when keyboard-only mode is disabled or the Yomitan popup closes.
|
||||
- [x] #7 Renderer/config regression tests cover controller gating, mappings, modal behavior, persisted selection, and highlight cleanup.
|
||||
- [x] #8 Docs/config example describe the controller feature and new shortcuts.
|
||||
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Added renderer-side gamepad polling and logical action mapping in `src/renderer/handlers/gamepad-controller.ts`.
|
||||
- Added controller select/debug modals, persisted preferred-controller IPC, and top-level `controller` config defaults/schema/template output.
|
||||
- Added a transient in-overlay controller status indicator when a controller is first detected.
|
||||
- Tuned controller defaults and routing after live testing: d-pad fallback navigation, slower repeat timing, DOM-backed popup-open detection, and direct pixel scroll/audio-source popup bridge commands.
|
||||
- Reused existing keyboard-only lookup/mining/navigation flows so controller input stays a supplement to keyboard-only mode instead of a parallel input path.
|
||||
- Verified keyboard-only highlight cleanup on mode-off and popup-close paths with renderer tests.
|
||||
|
||||
## Verification
|
||||
|
||||
- `bun test src/config/config.test.ts src/config/definitions/domain-registry.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/handlers/gamepad-controller.test.ts src/renderer/modals/controller-select.test.ts src/renderer/modals/controller-debug.test.ts src/core/services/ipc.test.ts`
|
||||
- `bun test src/main/runtime/composers/ipc-runtime-composer.test.ts`
|
||||
- `bun run generate:config-example`
|
||||
- `bun run typecheck`
|
||||
- `bun run docs:test`
|
||||
- `bun run test:fast`
|
||||
- `bun run test:env`
|
||||
- `bun run build`
|
||||
- `bun run docs:build`
|
||||
- `bun run test:smoke:dist`
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
id: TASK-159
|
||||
title: Create SubMiner automated testing skill for agents
|
||||
status: Done
|
||||
assignee:
|
||||
- codex
|
||||
created_date: '2026-03-11 05:55'
|
||||
updated_date: '2026-03-11 06:13'
|
||||
labels:
|
||||
- tooling
|
||||
- testing
|
||||
- skills
|
||||
dependencies: []
|
||||
priority: medium
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Design and implement a SubMiner-specific skill that agents can use to verify code changes with automated checks across launcher, mpv, overlay, and related workflows.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 Define the skill trigger surface and intended use cases for SubMiner change verification.
|
||||
- [x] #2 Implement a SubMiner-specific skill package with concise workflow guidance and any required helper scripts or references.
|
||||
- [x] #3 Document how agents should run automated verification for common SubMiner change types, including launcher/mpv/overlay-sensitive changes.
|
||||
- [x] #4 Verify the skill itself is usable in this repo context.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
1. Add a design doc at `docs/plans/2026-03-10-subminer-change-verification-design.md` capturing the approved skill contract, lane selection rules, helper script design, and trigger/workflow guidance.
|
||||
2. Create an in-repo source package for the new SubMiner-specific verification skill with `SKILL.md` plus helper shell scripts for diff classification and coordinated verification/artifact capture.
|
||||
3. Implement cheap-first verification logic: map changed paths to verification lanes, run repo-native commands, capture stdout/stderr and metadata under `.tmp/skill-verification/<timestamp>/`, and report pass/fail/skipped states with artifact paths.
|
||||
4. Encode repo-specific heuristics for docs/config, launcher/plugin, runtime/dist, and optional real-GUI escalation guidance without making GUI launch the default.
|
||||
5. Verify the skill locally by running the classifier/verifier against representative paths and confirming artifact generation and summaries.
|
||||
6. Optionally install the finished skill to `~/.codex/skills/subminer-change-verification/` after user approval because that target is outside the writable sandbox.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Planned outputs: SKILL.md plus small helper shell scripts for diff classification and coordinated verification/artifact capture. Default to repo-native commands; only escalate to real GUI/mpv verification when affected paths or requested behavior require it.
|
||||
|
||||
2026-03-10: Brainstorming completed with user approval for a SubMiner-specific automated verification skill. Chosen shape: auto-triggering, cheap-first verification workflow with optional real GUI/mpv escalation and artifact capture.
|
||||
|
||||
2026-03-10: Added design doc at docs/plans/2026-03-10-subminer-change-verification-design.md and created the in-repo skill source at tools/skills/subminer-change-verification/.
|
||||
|
||||
2026-03-10: Verified classifier output with representative launcher/runtime/config/docs paths and ran the verifier in dry-run mode plus one real config lane (`bun run test:config`). Artifacts written under .tmp/skill-verification/20260310-231320 and .tmp/skill-verification/20260310-231326.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Implemented a SubMiner-specific change verification skill source with a concise SKILL.md, a diff classifier, and a cheap-first verifier that captures artifacts under `.tmp/skill-verification/`. Verified the flow with representative path classification, a dry-run multi-lane plan, and a passing real config verification run.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
id: TASK-160
|
||||
title: Create repo-local scrum master orchestration skill
|
||||
status: Done
|
||||
assignee:
|
||||
- codex
|
||||
created_date: '2026-03-11 06:32'
|
||||
updated_date: '2026-03-11 06:45'
|
||||
labels:
|
||||
- skills
|
||||
- workflow
|
||||
- backlog
|
||||
- subagents
|
||||
- automation
|
||||
dependencies: []
|
||||
priority: high
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Design and implement a repo-local skill that can turn incoming requests/issues into well-scoped Backlog tasks, then coordinate one or more subagents to implement and verify the resulting work using the repo's established skills and verification workflow.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 Define the trigger surface, responsibilities, and limits of the scrum master skill for request intake, backlog updates, and execution orchestration.
|
||||
- [x] #2 Implement the skill package with concise guidance for intake, task search/create/update, planning, and subagent dispatch.
|
||||
- [x] #3 Specify how the skill coordinates with existing repo workflows, including Backlog.md requirements and SubMiner change verification.
|
||||
- [x] #4 Verify the skill is usable in this repo context for at least one representative request flow.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
1. Add a design doc at `docs/plans/2026-03-10-subminer-scrum-master-design.md` capturing the approved contract, backlog decision rules, orchestration policy, and verification handoff.
|
||||
2. Create the repo-local skill at `.agents/skills/subminer-scrum-master/` with a concise `SKILL.md`.
|
||||
3. Keep v1 instruction-heavy: backlog-or-not decision first, search/create/update task structure when needed, require planning before dispatch, use parent + subtasks for multi-part work, dispatch conservatively with explicit ownership, and require `subminer-change-verification` before handoff.
|
||||
4. Include representative flows for trivial no-ticket work, single-task implementation, and multi-part parent+subtask execution.
|
||||
5. Verify the skill in-repo with one representative request flow and update `TASK-160` with results.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
V1 will be instruction-heavy rather than script-heavy. The skill will decide whether backlog is warranted, record plans before dispatch, create parent+subtasks when needed, dispatch conservatively with explicit ownership, and require `subminer-change-verification` before handoff.
|
||||
|
||||
2026-03-10: Added design doc at docs/plans/2026-03-10-subminer-scrum-master-design.md and created the repo-local skill at .agents/skills/subminer-scrum-master/.
|
||||
|
||||
2026-03-10: Verified the skill against a representative request flow ('Fix launcher auto-start pause bug and make sure it is verified') by checking that the instructions cover backlog decisioning, planning-before-dispatch, single-task execution, explicit worker ownership, and verification via subminer-change-verification.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Implemented a repo-local `subminer-scrum-master` skill that assesses whether backlog tracking is needed, manages task structure and planning when appropriate, dispatches subagents conservatively, and requires verification before handoff. Verified the skill in-repo against a representative launcher bug workflow.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
id: TASK-161
|
||||
title: Add Arch Linux PKGBUILD and .SRCINFO for SubMiner release artifacts
|
||||
status: Done
|
||||
assignee:
|
||||
- codex
|
||||
created_date: '2026-03-11 07:50'
|
||||
updated_date: '2026-03-11 07:56'
|
||||
labels:
|
||||
- packaging
|
||||
- linux
|
||||
- docs
|
||||
dependencies: []
|
||||
references:
|
||||
- package.json
|
||||
- .github/workflows/release.yml
|
||||
- README.md
|
||||
documentation:
|
||||
- docs-site/development.md
|
||||
- docs-site/installation.md
|
||||
- docs/RELEASING.md
|
||||
priority: medium
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Add repo-maintained Arch packaging metadata so Arch/AUR users can install the packaged SubMiner release artifacts without relying on npm. Cover the binary package flow that matches the current GitHub Releases distribution model and document the install path for Arch users.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 A dedicated Arch packaging directory exists with a PKGBUILD and matching .SRCINFO for the SubMiner binary release flow.
|
||||
- [x] #2 The package installs the shipped Linux release artifact and wrapper in paths consistent with current runtime auto-detection expectations.
|
||||
- [x] #3 Package metadata declares the required Arch runtime dependencies for the packaged workflow.
|
||||
- [x] #4 The packaging files remain isolated from the main app/release/docs surfaces so they can be moved to a separate repository later.
|
||||
- [x] #5 The packaging metadata is validated with an appropriate local Arch packaging check or an explicitly documented limitation if the tool is unavailable in this environment.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
1. Add isolated Arch packaging under packaging/arch/subminer-bin for the binary release flow that matches current GitHub Releases artifacts.
|
||||
2. Install the Linux AppImage to /opt/SubMiner/SubMiner.AppImage, add PATH symlinks for SubMiner.AppImage and subminer, and ship optional plugin/theme/config assets under /usr/share/subminer.
|
||||
3. Generate and commit matching .SRCINFO metadata in the same packaging directory.
|
||||
4. Validate the PKGBUILD metadata locally with shell parsing plus makepkg --printsrcinfo and namcap if available.
|
||||
5. Leave the new files isolated so they can be moved to a separate packaging repository later.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
User requested no commit and no broad repo integration; keep the Arch packaging files isolated in a dedicated directory for later extraction.
|
||||
|
||||
Created isolated Arch packaging under packaging/arch/subminer-bin with PKGBUILD and generated .SRCINFO only; no docs or release workflow changes.
|
||||
|
||||
Validation run: bash -n PKGBUILD, makepkg --printsrcinfo > .SRCINFO, namcap PKGBUILD.
|
||||
|
||||
PKGBUILD uses current v0.5.6 GitHub release assets and recorded SHA-256 values for SubMiner-0.5.6.AppImage, subminer, and subminer-assets.tar.gz.
|
||||
|
||||
Per user follow-up, moved packaging/arch/subminer-bin out of the repo to /home/sudacode/packages/maintaining/subminer-bin for separate maintenance. Repo docs/workflows were still left untouched.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Added an isolated Arch Linux packaging directory at packaging/arch/subminer-bin containing a `subminer-bin` PKGBUILD and generated `.SRCINFO`. The package installs the current GitHub release AppImage to `/opt/SubMiner/SubMiner.AppImage`, adds PATH access for `SubMiner.AppImage` and `subminer`, and ships optional plugin/theme/config assets under `/usr/share/subminer`. Verified locally with `bash -n PKGBUILD`, `makepkg --printsrcinfo`, and `namcap PKGBUILD`.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
id: TASK-162
|
||||
title: Normalize packaged Linux paths to canonical SubMiner directories
|
||||
status: In Progress
|
||||
assignee:
|
||||
- codex
|
||||
created_date: '2026-03-11 08:28'
|
||||
updated_date: '2026-03-11 08:29'
|
||||
labels:
|
||||
- linux
|
||||
- packaging
|
||||
- docs
|
||||
dependencies: []
|
||||
references:
|
||||
- launcher/mpv.ts
|
||||
- launcher/picker.ts
|
||||
- plugin/subminer/binary.lua
|
||||
- plugin/subminer.conf
|
||||
- docs-site/installation.md
|
||||
- docs-site/launcher-script.md
|
||||
- README.md
|
||||
priority: medium
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Align packaged Linux path conventions so system-installed assets use canonical `SubMiner` directories and match runtime auto-detection. Cover AppImage binary discovery, rofi theme discovery/docs, and related path references while preserving lowercase names only for the launcher wrapper, rofi theme filename, and mpv Lua plugin/conf.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 Launcher/runtime path discovery prefers canonical packaged Linux locations that use `SubMiner` casing for shared data and config directories.
|
||||
- [ ] #2 Tests cover the expected packaged Linux discovery paths for the AppImage and rofi theme search behavior.
|
||||
- [ ] #3 User-facing docs reference the canonical packaged Linux locations consistently.
|
||||
- [ ] #4 Lowercase names remain only where intentionally required for the launcher wrapper, rofi theme filename, and mpv Lua plugin/conf.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
1. Add failing launcher tests for canonical packaged Linux discovery paths: /usr/lib/subminer/SubMiner.AppImage via PATH symlink flow and /usr/share/SubMiner/themes/subminer.rasi for rofi theme lookup.
|
||||
2. Update launcher runtime path discovery to prefer canonical packaged Linux shared-data locations using SubMiner casing.
|
||||
3. Update plugin auto-detection comments and binary search defaults so packaged Linux paths stay consistent with launcher/runtime expectations.
|
||||
4. Update user-facing docs to reference canonical SubMiner-cased config/share paths while keeping lowercase names only for the launcher wrapper, rofi theme filename, and mpv Lua plugin/conf.
|
||||
5. Run targeted launcher tests plus docs checks.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
id: TASK-163
|
||||
title: 'Resolve current lint, format, and style check failures'
|
||||
status: Done
|
||||
assignee:
|
||||
- Codex
|
||||
created_date: '2026-03-11 08:48'
|
||||
updated_date: '2026-03-11 08:49'
|
||||
labels:
|
||||
- maintenance
|
||||
- tooling
|
||||
dependencies: []
|
||||
references:
|
||||
- /home/sudacode/projects/japanese/SubMiner/package.json
|
||||
- /home/sudacode/projects/japanese/SubMiner/docs-site/development.md
|
||||
- /home/sudacode/projects/japanese/SubMiner/docs-site/architecture.md
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Run the repo's maintained static/style gates, fix any failures they report, and leave the working tree with those checks passing without disturbing unrelated in-progress work.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 The repo's maintained static/style commands for this cleanup are identified and recorded.
|
||||
- [x] #2 Any failures from those commands are fixed in-scope in the codebase.
|
||||
- [x] #3 The same static/style commands pass after the fixes.
|
||||
- [x] #4 Verification results and any skipped checks are documented before handoff.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
1. Run the maintained static/style gates for this repo: `bun run typecheck` and `bun run format:check:src`.
|
||||
2. Inspect each failure and patch only the files implicated by the failing command, preserving unrelated user changes.
|
||||
3. Re-run the failing gate after each fix until both commands pass.
|
||||
4. Run one final consolidated verification pass, record exact commands/results, and note any intentionally skipped broader gates outside lint/style scope.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Identified maintained commands from repo docs/scripts: `bun run typecheck` and `bun run format:check:src`. `changelog:lint` exists but is release-fragment specific, not a general lint/style gate for this request.
|
||||
|
||||
Ran `bun run typecheck` at 2026-03-11 01:48 PDT: passed.
|
||||
|
||||
Ran `bun run format:check:src` at 2026-03-11 01:48 PDT: failed on `src/release-workflow.test.ts`.
|
||||
|
||||
Applied a scoped Prettier rewrite with `./node_modules/.bin/prettier --write src/release-workflow.test.ts`.
|
||||
|
||||
Re-ran `bun run format:check:src` at 2026-03-11 01:49 PDT: passed.
|
||||
|
||||
Re-ran `bun run typecheck` at 2026-03-11 01:49 PDT: passed.
|
||||
|
||||
Skipped broader test/build/docs gates because the user asked specifically for lint/format/style cleanup and the only required change was a formatting-only update in one test file.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Resolved the repo's current static/style failure by reformatting `src/release-workflow.test.ts` to match the maintained Prettier scope. The only code change was line wrapping in the `generate:config-example` assertion; behavior did not change.
|
||||
|
||||
Verification run:
|
||||
- `bun run typecheck`
|
||||
- `bun run format:check:src`
|
||||
- `./node_modules/.bin/prettier --write src/release-workflow.test.ts`
|
||||
- `bun run format:check:src`
|
||||
- `bun run typecheck`
|
||||
|
||||
Result: both maintained gates requested for this cleanup now pass. Broader build/test/docs lanes were intentionally not run because this task was limited to lint/format/style checks and required a formatting-only fix.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
id: TASK-164
|
||||
title: Run maintained test gate and fix failing regressions
|
||||
status: Done
|
||||
assignee:
|
||||
- Codex
|
||||
created_date: '2026-03-11 08:52'
|
||||
updated_date: '2026-03-11 08:54'
|
||||
labels:
|
||||
- maintenance
|
||||
- testing
|
||||
dependencies: []
|
||||
references:
|
||||
- /home/sudacode/projects/japanese/SubMiner/package.json
|
||||
- /home/sudacode/projects/japanese/SubMiner/docs-site/development.md
|
||||
- /home/sudacode/projects/japanese/SubMiner/docs-site/architecture.md
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Execute the repo's maintained test verification flow, fix any regressions surfaced by those commands, and leave the requested test gate passing without disturbing unrelated in-progress work.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 The maintained test commands chosen for this pass are recorded in the task plan.
|
||||
- [x] #2 Any test failures encountered in that gate are fixed in-scope with appropriate regression coverage when needed.
|
||||
- [x] #3 The same maintained test gate passes after the fixes.
|
||||
- [x] #4 Verification results, skips, and remaining risks are documented before handoff.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
1. Run the maintained verification gate documented for local handoff: `bun run test:fast`, `bun run test:env`, `bun run build`, and `bun run test:smoke:dist`.
|
||||
2. Stop at the first failing command, inspect the exact failure, and use a red/green loop with the smallest targeted failing test before writing any production-code fix.
|
||||
3. Re-run the targeted failing test, then the original failing command, and continue through the remaining gate.
|
||||
4. Record results, any skips, and residual risks before handoff.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Using the docs-site recommended local gate as the maintained test/build verification flow for this request.
|
||||
|
||||
Ran `bun run test:fast` at 2026-03-11 01:52 PDT: passed.
|
||||
|
||||
Ran `bun run test:env` at 2026-03-11 01:53 PDT: passed.
|
||||
|
||||
Ran `bun run build` at 2026-03-11 01:53 PDT: passed.
|
||||
|
||||
Ran `bun run test:smoke:dist` at 2026-03-11 01:53 PDT: passed.
|
||||
|
||||
No failing tests surfaced in the maintained gate, so no production-code or test changes were required in this pass.
|
||||
|
||||
Working tree still includes unrelated user edits plus the previously-applied formatting change in `src/release-workflow.test.ts`; this task did not add new source changes.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Executed the repo's maintained local verification gate from the development docs: `bun run test:fast`, `bun run test:env`, `bun run build`, and `bun run test:smoke:dist`.
|
||||
|
||||
Result: every command passed. No failing tests surfaced, so no additional fixes or new regression tests were required for this task.
|
||||
|
||||
Working tree note: existing unrelated user changes remain in place, along with the prior formatting-only change to `src/release-workflow.test.ts` from TASK-163. This task introduced no new code changes.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
4
changes/aur-install-docs.md
Normal file
@@ -0,0 +1,4 @@
|
||||
type: docs
|
||||
area: install
|
||||
|
||||
- Added Arch Linux AUR install docs for `subminer-bin` in the README and installation guide.
|
||||
4
changes/config-example-drift-check.md
Normal file
@@ -0,0 +1,4 @@
|
||||
type: internal
|
||||
area: config
|
||||
|
||||
- add an enforced `verify:config-example` gate so checked-in example config artifacts cannot drift silently
|
||||
7
changes/controller-overlay-support.md
Normal file
@@ -0,0 +1,7 @@
|
||||
type: added
|
||||
area: overlay
|
||||
|
||||
- Added Chrome Gamepad API controller support for keyboard-only overlay mode, including configurable logical bindings for lookup, mining, popup navigation, Yomitan audio, mpv pause, d-pad fallback navigation, and slower smooth popup scrolling.
|
||||
- Added `Alt+C` controller selection and `Alt+Shift+C` controller debug modals, with preferred controller persistence and live raw input inspection.
|
||||
- Added a transient in-overlay controller-detected indicator when a controller is first found.
|
||||
- Fixed stale keyboard-only token highlight cleanup when keyboard-only mode turns off or the Yomitan popup closes.
|
||||
@@ -5,6 +5,7 @@
|
||||
* Copy to %APPDATA%/SubMiner/config.jsonc on Windows, or $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) on Linux/macOS.
|
||||
*/
|
||||
{
|
||||
|
||||
// ==========================================
|
||||
// Overlay Auto-Start
|
||||
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles.
|
||||
@@ -17,7 +18,7 @@
|
||||
// ==========================================
|
||||
"texthooker": {
|
||||
"launchAtStartup": true, // Launch texthooker server automatically when SubMiner starts. Values: true | false
|
||||
"openBrowser": true, // Open browser setting. Values: true | false
|
||||
"openBrowser": true // Open browser setting. Values: true | false
|
||||
}, // Configure texthooker startup launch and browser opening behavior.
|
||||
|
||||
// ==========================================
|
||||
@@ -27,7 +28,7 @@
|
||||
// ==========================================
|
||||
"websocket": {
|
||||
"enabled": "auto", // Built-in subtitle websocket server mode. Values: auto | true | false
|
||||
"port": 6677, // Built-in subtitle websocket server port.
|
||||
"port": 6677 // Built-in subtitle websocket server port.
|
||||
}, // Built-in WebSocket server broadcasts subtitle text to connected clients.
|
||||
|
||||
// ==========================================
|
||||
@@ -37,7 +38,7 @@
|
||||
// ==========================================
|
||||
"annotationWebsocket": {
|
||||
"enabled": true, // Annotated subtitle websocket server enabled state. Values: true | false
|
||||
"port": 6678, // Annotated subtitle websocket server port.
|
||||
"port": 6678 // Annotated subtitle websocket server port.
|
||||
}, // Dedicated annotated subtitle websocket for bundled texthooker and token-aware clients.
|
||||
|
||||
// ==========================================
|
||||
@@ -46,9 +47,58 @@
|
||||
// Set to debug for full runtime diagnostics.
|
||||
// ==========================================
|
||||
"logging": {
|
||||
"level": "info", // Minimum log level for runtime logging. Values: debug | info | warn | error
|
||||
"level": "info" // Minimum log level for runtime logging. Values: debug | info | warn | error
|
||||
}, // Controls logging verbosity.
|
||||
|
||||
// ==========================================
|
||||
// Controller Support
|
||||
// Gamepad support for the visible overlay while keyboard-only mode is active.
|
||||
// Use the selection modal to save a preferred controller by id for future launches.
|
||||
// Trigger input mode can be auto, digital-only, or analog-thresholded depending on the controller.
|
||||
// Override controller.buttonIndices when your pad reports non-standard raw button numbers.
|
||||
// ==========================================
|
||||
"controller": {
|
||||
"enabled": true, // Enable overlay controller support through the Chrome Gamepad API. Values: true | false
|
||||
"preferredGamepadId": "", // Preferred controller id saved from the controller selection modal.
|
||||
"preferredGamepadLabel": "", // Preferred controller display label saved for diagnostics.
|
||||
"smoothScroll": true, // Use smooth scrolling for controller-driven popup scroll input. Values: true | false
|
||||
"scrollPixelsPerSecond": 900, // Base popup scroll speed for controller stick input.
|
||||
"horizontalJumpPixels": 160, // Popup page-jump distance for controller jump input.
|
||||
"stickDeadzone": 0.2, // Deadzone applied to controller stick axes.
|
||||
"triggerInputMode": "auto", // How controller triggers are interpreted: auto, pressed-only, or thresholded analog. Values: auto | digital | analog
|
||||
"triggerDeadzone": 0.5, // Minimum analog trigger value required when trigger input uses auto or analog mode.
|
||||
"repeatDelayMs": 320, // Delay before repeating held controller actions.
|
||||
"repeatIntervalMs": 120, // Repeat interval for held controller actions.
|
||||
"buttonIndices": {
|
||||
"select": 6, // Raw button index used for the controller select/minus/back button.
|
||||
"buttonSouth": 0, // Raw button index used for controller south/A button input.
|
||||
"buttonEast": 1, // Raw button index used for controller east/B button input.
|
||||
"buttonWest": 2, // Raw button index used for controller west/X button input.
|
||||
"buttonNorth": 3, // Raw button index used for controller north/Y button input.
|
||||
"leftShoulder": 4, // Raw button index used for controller left shoulder input.
|
||||
"rightShoulder": 5, // Raw button index used for controller right shoulder input.
|
||||
"leftStickPress": 9, // Raw button index used for controller L3 input.
|
||||
"rightStickPress": 10, // Raw button index used for controller R3 input.
|
||||
"leftTrigger": 6, // Raw button index used for controller L2 input.
|
||||
"rightTrigger": 7 // Raw button index used for controller R2 input.
|
||||
}, // Button indices setting.
|
||||
"bindings": {
|
||||
"toggleLookup": "buttonSouth", // Controller binding for toggling lookup. Values: none | select | buttonSouth | buttonEast | buttonNorth | buttonWest | leftShoulder | rightShoulder | leftStickPress | rightStickPress | leftTrigger | rightTrigger
|
||||
"closeLookup": "buttonEast", // Controller binding for closing lookup. Values: none | select | buttonSouth | buttonEast | buttonNorth | buttonWest | leftShoulder | rightShoulder | leftStickPress | rightStickPress | leftTrigger | rightTrigger
|
||||
"toggleKeyboardOnlyMode": "buttonNorth", // Controller binding for toggling keyboard-only mode. Values: none | select | buttonSouth | buttonEast | buttonNorth | buttonWest | leftShoulder | rightShoulder | leftStickPress | rightStickPress | leftTrigger | rightTrigger
|
||||
"mineCard": "buttonWest", // Controller binding for mining the active card. Values: none | select | buttonSouth | buttonEast | buttonNorth | buttonWest | leftShoulder | rightShoulder | leftStickPress | rightStickPress | leftTrigger | rightTrigger
|
||||
"quitMpv": "select", // Controller binding for quitting mpv. Values: none | select | buttonSouth | buttonEast | buttonNorth | buttonWest | leftShoulder | rightShoulder | leftStickPress | rightStickPress | leftTrigger | rightTrigger
|
||||
"previousAudio": "none", // Controller binding for previous Yomitan audio. Values: none | select | buttonSouth | buttonEast | buttonNorth | buttonWest | leftShoulder | rightShoulder | leftStickPress | rightStickPress | leftTrigger | rightTrigger
|
||||
"nextAudio": "rightShoulder", // Controller binding for next Yomitan audio. Values: none | select | buttonSouth | buttonEast | buttonNorth | buttonWest | leftShoulder | rightShoulder | leftStickPress | rightStickPress | leftTrigger | rightTrigger
|
||||
"playCurrentAudio": "leftShoulder", // Controller binding for playing the current Yomitan audio. Values: none | select | buttonSouth | buttonEast | buttonNorth | buttonWest | leftShoulder | rightShoulder | leftStickPress | rightStickPress | leftTrigger | rightTrigger
|
||||
"toggleMpvPause": "leftStickPress", // Controller binding for toggling mpv play/pause. Values: none | select | buttonSouth | buttonEast | buttonNorth | buttonWest | leftShoulder | rightShoulder | leftStickPress | rightStickPress | leftTrigger | rightTrigger
|
||||
"leftStickHorizontal": "leftStickX", // Axis binding used for left/right token selection. Values: leftStickX | leftStickY | rightStickX | rightStickY
|
||||
"leftStickVertical": "leftStickY", // Axis binding used for primary popup scrolling. Values: leftStickX | leftStickY | rightStickX | rightStickY
|
||||
"rightStickHorizontal": "rightStickX", // Axis binding reserved for alternate right-stick mappings. Values: leftStickX | leftStickY | rightStickX | rightStickY
|
||||
"rightStickVertical": "rightStickY" // Axis binding used for popup page jumps. Values: leftStickX | leftStickY | rightStickX | rightStickY
|
||||
} // Bindings setting.
|
||||
}, // Gamepad support for the visible overlay while keyboard-only mode is active.
|
||||
|
||||
// ==========================================
|
||||
// Startup Warmups
|
||||
// Background warmup controls for MeCab, Yomitan, dictionaries, and Jellyfin session.
|
||||
@@ -60,7 +110,7 @@
|
||||
"mecab": true, // Warm up MeCab tokenizer at startup. Values: true | false
|
||||
"yomitanExtension": true, // Warm up Yomitan extension at startup. Values: true | false
|
||||
"subtitleDictionaries": true, // Warm up subtitle dictionaries at startup. Values: true | false
|
||||
"jellyfinRemoteSession": true, // Warm up Jellyfin remote session at startup. Values: true | false
|
||||
"jellyfinRemoteSession": true // Warm up Jellyfin remote session at startup. Values: true | false
|
||||
}, // Background warmup controls for MeCab, Yomitan, dictionaries, and Jellyfin session.
|
||||
|
||||
// ==========================================
|
||||
@@ -81,7 +131,7 @@
|
||||
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
|
||||
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting.
|
||||
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting.
|
||||
"openJimaku": "Ctrl+Shift+J", // Open jimaku setting.
|
||||
"openJimaku": "Ctrl+Shift+J" // Open jimaku setting.
|
||||
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||
|
||||
// ==========================================
|
||||
@@ -101,7 +151,7 @@
|
||||
"secondarySub": {
|
||||
"secondarySubLanguages": [], // Secondary sub languages setting.
|
||||
"autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false
|
||||
"defaultMode": "hover", // Default mode setting.
|
||||
"defaultMode": "hover" // Default mode setting.
|
||||
}, // Dual subtitle track options.
|
||||
|
||||
// ==========================================
|
||||
@@ -113,7 +163,7 @@
|
||||
"alass_path": "", // Alass path setting.
|
||||
"ffsubsync_path": "", // Ffsubsync path setting.
|
||||
"ffmpeg_path": "", // Ffmpeg path setting.
|
||||
"replace": true, // Replace the active subtitle file when sync completes. Values: true | false
|
||||
"replace": true // Replace the active subtitle file when sync completes. Values: true | false
|
||||
}, // Subsync engine and executable paths.
|
||||
|
||||
// ==========================================
|
||||
@@ -121,7 +171,7 @@
|
||||
// Initial vertical subtitle position from the bottom.
|
||||
// ==========================================
|
||||
"subtitlePosition": {
|
||||
"yPercent": 10, // Y percent setting.
|
||||
"yPercent": 10 // Y percent setting.
|
||||
}, // Initial vertical subtitle position from the bottom.
|
||||
|
||||
// ==========================================
|
||||
@@ -158,7 +208,7 @@
|
||||
"N2": "#f5a97f", // N2 setting.
|
||||
"N3": "#f9e2af", // N3 setting.
|
||||
"N4": "#a6e3a1", // N4 setting.
|
||||
"N5": "#8aadf4", // N5 setting.
|
||||
"N5": "#8aadf4" // N5 setting.
|
||||
}, // Jlpt colors setting.
|
||||
"frequencyDictionary": {
|
||||
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
|
||||
@@ -167,7 +217,13 @@
|
||||
"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`.
|
||||
"bandedColors": ["#ed8796", "#f5a97f", "#f9e2af", "#8bd5ca", "#8aadf4"], // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
|
||||
"bandedColors": [
|
||||
"#ed8796",
|
||||
"#f5a97f",
|
||||
"#f9e2af",
|
||||
"#8bd5ca",
|
||||
"#8aadf4"
|
||||
] // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
|
||||
}, // Frequency dictionary setting.
|
||||
"secondary": {
|
||||
"fontFamily": "Inter, Noto Sans, Helvetica Neue, sans-serif", // Font family setting.
|
||||
@@ -182,8 +238,8 @@
|
||||
"backgroundColor": "rgba(20, 22, 34, 0.78)", // Background color setting.
|
||||
"backdropFilter": "blur(6px)", // Backdrop filter setting.
|
||||
"fontWeight": "600", // Font weight setting.
|
||||
"fontStyle": "normal", // Font style setting.
|
||||
}, // Secondary setting.
|
||||
"fontStyle": "normal" // Font style setting.
|
||||
} // Secondary setting.
|
||||
}, // Primary and secondary subtitle styling.
|
||||
|
||||
// ==========================================
|
||||
@@ -197,7 +253,7 @@
|
||||
"model": "openai/gpt-4o-mini", // Model setting.
|
||||
"baseUrl": "https://openrouter.ai/api", // Base URL for the shared OpenAI-compatible AI provider.
|
||||
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // System prompt setting.
|
||||
"requestTimeoutMs": 15000, // Timeout in milliseconds for shared AI provider requests.
|
||||
"requestTimeoutMs": 15000 // Timeout in milliseconds for shared AI provider requests.
|
||||
}, // Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
|
||||
|
||||
// ==========================================
|
||||
@@ -215,20 +271,22 @@
|
||||
"enabled": true, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false
|
||||
"host": "127.0.0.1", // Bind host for local AnkiConnect proxy.
|
||||
"port": 8766, // Bind port for local AnkiConnect proxy.
|
||||
"upstreamUrl": "http://127.0.0.1:8765", // Upstream AnkiConnect URL proxied by local AnkiConnect proxy.
|
||||
"upstreamUrl": "http://127.0.0.1:8765" // Upstream AnkiConnect URL proxied by local AnkiConnect proxy.
|
||||
}, // Proxy setting.
|
||||
"tags": ["SubMiner"], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
||||
"tags": [
|
||||
"SubMiner"
|
||||
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
||||
"fields": {
|
||||
"audio": "ExpressionAudio", // Audio setting.
|
||||
"image": "Picture", // Image setting.
|
||||
"sentence": "Sentence", // Sentence setting.
|
||||
"miscInfo": "MiscInfo", // Misc info setting.
|
||||
"translation": "SelectionText", // Translation setting.
|
||||
"translation": "SelectionText" // Translation setting.
|
||||
}, // Fields setting.
|
||||
"ai": {
|
||||
"enabled": false, // Enable AI provider usage for Anki translation/enrichment flows. Values: true | false
|
||||
"model": "", // Optional model override for Anki AI translation/enrichment flows.
|
||||
"systemPrompt": "", // Optional system prompt override for Anki AI translation/enrichment flows.
|
||||
"systemPrompt": "" // Optional system prompt override for Anki AI translation/enrichment flows.
|
||||
}, // Ai setting.
|
||||
"media": {
|
||||
"generateAudio": true, // Generate audio setting. Values: true | false
|
||||
@@ -241,7 +299,7 @@
|
||||
"animatedCrf": 35, // Animated crf setting.
|
||||
"audioPadding": 0.5, // Audio padding setting.
|
||||
"fallbackDuration": 3, // Fallback duration setting.
|
||||
"maxMediaDuration": 30, // Max media duration setting.
|
||||
"maxMediaDuration": 30 // Max media duration setting.
|
||||
}, // Media setting.
|
||||
"behavior": {
|
||||
"overwriteAudio": true, // Overwrite audio setting. Values: true | false
|
||||
@@ -249,7 +307,7 @@
|
||||
"mediaInsertMode": "append", // Media insert mode setting.
|
||||
"highlightWord": true, // Highlight word setting. Values: true | false
|
||||
"notificationType": "osd", // Notification type setting.
|
||||
"autoUpdateNewCards": true, // Automatically update newly added cards. Values: true | false
|
||||
"autoUpdateNewCards": true // Automatically update newly added cards. Values: true | false
|
||||
}, // Behavior setting.
|
||||
"nPlusOne": {
|
||||
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
|
||||
@@ -258,20 +316,20 @@
|
||||
"decks": [], // Decks used for N+1 known-word cache scope. Supports one or more deck names.
|
||||
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3).
|
||||
"nPlusOne": "#c6a0f6", // Color used for the single N+1 target token highlight.
|
||||
"knownWord": "#a6da95", // Color used for legacy known-word highlights.
|
||||
"knownWord": "#a6da95" // Color used for legacy known-word highlights.
|
||||
}, // N plus one setting.
|
||||
"metadata": {
|
||||
"pattern": "[SubMiner] %f (%t)", // Pattern setting.
|
||||
"pattern": "[SubMiner] %f (%t)" // Pattern setting.
|
||||
}, // Metadata setting.
|
||||
"isLapis": {
|
||||
"enabled": false, // Enabled setting. Values: true | false
|
||||
"sentenceCardModel": "Japanese sentences", // Sentence card model setting.
|
||||
"sentenceCardModel": "Japanese sentences" // Sentence card model setting.
|
||||
}, // Is lapis setting.
|
||||
"isKiku": {
|
||||
"enabled": false, // Enabled setting. Values: true | false
|
||||
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
|
||||
"deleteDuplicateInAuto": true, // Delete duplicate in auto setting. Values: true | false
|
||||
}, // Is kiku setting.
|
||||
"deleteDuplicateInAuto": true // Delete duplicate in auto setting. Values: true | false
|
||||
} // Is kiku setting.
|
||||
}, // Automatic Anki updates and media generation options.
|
||||
|
||||
// ==========================================
|
||||
@@ -281,7 +339,7 @@
|
||||
"jimaku": {
|
||||
"apiBaseUrl": "https://jimaku.cc", // Api base url setting.
|
||||
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
|
||||
"maxEntryResults": 10, // Maximum Jimaku search results returned.
|
||||
"maxEntryResults": 10 // Maximum Jimaku search results returned.
|
||||
}, // Jimaku API configuration and defaults.
|
||||
|
||||
// ==========================================
|
||||
@@ -296,9 +354,12 @@
|
||||
"fixWithAi": false, // Use shared AI provider to post-process whisper-generated YouTube subtitles. Values: true | false
|
||||
"ai": {
|
||||
"model": "", // Optional model override for YouTube subtitle AI post-processing.
|
||||
"systemPrompt": "", // Optional system prompt override for YouTube subtitle AI post-processing.
|
||||
"systemPrompt": "" // Optional system prompt override for YouTube subtitle AI post-processing.
|
||||
}, // Ai setting.
|
||||
"primarySubLanguages": ["ja", "jpn"], // Comma-separated primary subtitle language priority used by the launcher.
|
||||
"primarySubLanguages": [
|
||||
"ja",
|
||||
"jpn"
|
||||
] // Comma-separated primary subtitle language priority used by the launcher.
|
||||
}, // Defaults for SubMiner YouTube subtitle generation.
|
||||
|
||||
// ==========================================
|
||||
@@ -319,9 +380,9 @@
|
||||
"collapsibleSections": {
|
||||
"description": false, // Open the Description section by default in character dictionary glossary entries. Values: true | false
|
||||
"characterInformation": false, // Open the Character Information section by default in character dictionary glossary entries. Values: true | false
|
||||
"voicedBy": false, // Open the Voiced by section by default in character dictionary glossary entries. Values: true | false
|
||||
}, // Collapsible sections setting.
|
||||
}, // Character dictionary setting.
|
||||
"voicedBy": false // Open the Voiced by section by default in character dictionary glossary entries. Values: true | false
|
||||
} // Collapsible sections setting.
|
||||
} // Character dictionary setting.
|
||||
}, // Anilist API credentials and update behavior.
|
||||
|
||||
// ==========================================
|
||||
@@ -345,8 +406,16 @@
|
||||
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
|
||||
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
|
||||
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
|
||||
"directPlayContainers": ["mkv", "mp4", "webm", "mov", "flac", "mp3", "aac"], // Container allowlist for direct play decisions.
|
||||
"transcodeVideoCodec": "h264", // Preferred transcode video codec when direct play is unavailable.
|
||||
"directPlayContainers": [
|
||||
"mkv",
|
||||
"mp4",
|
||||
"webm",
|
||||
"mov",
|
||||
"flac",
|
||||
"mp3",
|
||||
"aac"
|
||||
], // Container allowlist for direct play decisions.
|
||||
"transcodeVideoCodec": "h264" // Preferred transcode video codec when direct play is unavailable.
|
||||
}, // Optional Jellyfin integration for auth, browsing, and playback launch.
|
||||
|
||||
// ==========================================
|
||||
@@ -357,7 +426,7 @@
|
||||
"discordPresence": {
|
||||
"enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false
|
||||
"updateIntervalMs": 3000, // Minimum interval between presence payload updates.
|
||||
"debounceMs": 750, // Debounce delay used to collapse bursty presence updates.
|
||||
"debounceMs": 750 // Debounce delay used to collapse bursty presence updates.
|
||||
}, // Optional Discord Rich Presence activity card updates for current playback/study session.
|
||||
|
||||
// ==========================================
|
||||
@@ -379,7 +448,7 @@
|
||||
"telemetryDays": 30, // Telemetry retention window in days.
|
||||
"dailyRollupsDays": 365, // Daily rollup retention window in days.
|
||||
"monthlyRollupsDays": 1825, // Monthly rollup retention window in days.
|
||||
"vacuumIntervalDays": 7, // Minimum days between VACUUM runs.
|
||||
}, // Retention setting.
|
||||
}, // Enable/disable immersion tracking.
|
||||
"vacuumIntervalDays": 7 // Minimum days between VACUUM runs.
|
||||
} // Retention setting.
|
||||
} // Enable/disable immersion tracking.
|
||||
}
|
||||
|
||||
8
docs-site/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
.vitepress/cache/
|
||||
.vitepress/dist/
|
||||
.DS_Store
|
||||
.codex/
|
||||
.agents/
|
||||
**/CLAUDE.md
|
||||
.claude/*
|
||||
119
docs-site/.vitepress/config.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
export default {
|
||||
title: 'SubMiner Docs',
|
||||
description:
|
||||
'SubMiner: an MPV immersion-mining overlay with Yomitan and AnkiConnect integration.',
|
||||
head: [
|
||||
['link', { rel: 'icon', href: '/favicon.ico', sizes: 'any' }],
|
||||
[
|
||||
'link',
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
href: '/favicon-32x32.png',
|
||||
sizes: '32x32',
|
||||
},
|
||||
],
|
||||
[
|
||||
'link',
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
href: '/favicon-16x16.png',
|
||||
sizes: '16x16',
|
||||
},
|
||||
],
|
||||
[
|
||||
'link',
|
||||
{
|
||||
rel: 'apple-touch-icon',
|
||||
href: '/apple-touch-icon.png',
|
||||
sizes: '180x180',
|
||||
},
|
||||
],
|
||||
],
|
||||
appearance: 'dark',
|
||||
cleanUrls: true,
|
||||
metaChunk: true,
|
||||
sitemap: { hostname: 'https://docs.subminer.moe' },
|
||||
lastUpdated: true,
|
||||
srcExclude: ['subagents/**'],
|
||||
markdown: {
|
||||
theme: {
|
||||
light: 'catppuccin-latte',
|
||||
dark: 'catppuccin-macchiato',
|
||||
},
|
||||
},
|
||||
themeConfig: {
|
||||
logo: {
|
||||
light: '/assets/SubMiner.png',
|
||||
dark: '/assets/SubMiner.png',
|
||||
},
|
||||
siteTitle: 'SubMiner Docs',
|
||||
nav: [
|
||||
{ text: 'Home', link: '/' },
|
||||
{ text: 'Get Started', link: '/installation' },
|
||||
{ text: 'Mining', link: '/mining-workflow' },
|
||||
{ text: 'Configuration', link: '/configuration' },
|
||||
{ text: 'Changelog', link: '/changelog' },
|
||||
{ text: 'Troubleshooting', link: '/troubleshooting' },
|
||||
],
|
||||
sidebar: [
|
||||
{
|
||||
text: 'Getting Started',
|
||||
items: [
|
||||
{ text: 'Overview', link: '/' },
|
||||
{ text: 'Installation', link: '/installation' },
|
||||
{ text: 'Usage', link: '/usage' },
|
||||
{ text: 'Mining Workflow', link: '/mining-workflow' },
|
||||
{ text: 'Launcher Script', link: '/launcher-script' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Reference',
|
||||
items: [
|
||||
{ text: 'Configuration', link: '/configuration' },
|
||||
{ text: 'Keyboard Shortcuts', link: '/shortcuts' },
|
||||
{ text: 'Subtitle Annotations', link: '/subtitle-annotations' },
|
||||
{ text: 'Immersion Tracking', link: '/immersion-tracking' },
|
||||
{ text: 'Troubleshooting', link: '/troubleshooting' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Integrations',
|
||||
items: [
|
||||
{ text: 'MPV Plugin', link: '/mpv-plugin' },
|
||||
{ text: 'Anki', link: '/anki-integration' },
|
||||
{ text: 'Jellyfin', link: '/jellyfin-integration' },
|
||||
{ text: 'Jimaku', link: '/configuration#jimaku' },
|
||||
{ text: 'AniList', link: '/configuration#anilist' },
|
||||
{ text: 'Character Dictionary', link: '/character-dictionary' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Development',
|
||||
items: [
|
||||
{ text: 'Building & Testing', link: '/development' },
|
||||
{ text: 'Architecture', link: '/architecture' },
|
||||
{ text: 'IPC + Runtime Contracts', link: '/ipc-contracts' },
|
||||
{ text: 'Changelog', link: '/changelog' },
|
||||
],
|
||||
},
|
||||
],
|
||||
search: {
|
||||
provider: 'local',
|
||||
},
|
||||
footer: {
|
||||
message: 'Released under the GPL-3.0 License.',
|
||||
copyright: 'Copyright © 2026-present sudacode',
|
||||
},
|
||||
editLink: {
|
||||
pattern: 'https://github.com/ksyasuda/SubMiner/edit/main/docs-site/:path',
|
||||
text: 'Edit this page on GitHub',
|
||||
},
|
||||
outline: { level: [2, 3], label: 'On this page' },
|
||||
externalLinkIcon: true,
|
||||
docFooter: { prev: 'Previous', next: 'Next' },
|
||||
returnToTopLabel: 'Back to top',
|
||||
socialLinks: [{ icon: 'github', link: 'https://github.com/ksyasuda/SubMiner' }],
|
||||
},
|
||||
};
|
||||
18
docs-site/.vitepress/theme/TuiLayout.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup>
|
||||
import DefaultTheme from 'vitepress/theme';
|
||||
import StatusLine from './components/StatusLine.vue';
|
||||
import BlinkingCursor from './components/BlinkingCursor.vue';
|
||||
|
||||
const { Layout } = DefaultTheme;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout>
|
||||
<template #home-hero-info-after>
|
||||
<BlinkingCursor />
|
||||
</template>
|
||||
<template #layout-bottom>
|
||||
<StatusLine />
|
||||
</template>
|
||||
</Layout>
|
||||
</template>
|
||||
3
docs-site/.vitepress/theme/components/BlinkingCursor.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<span class="tui-cursor" aria-hidden="true">█</span>
|
||||
</template>
|
||||
48
docs-site/.vitepress/theme/components/StatusLine.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup>
|
||||
import { useRoute, useData } from 'vitepress';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const route = useRoute();
|
||||
const { page, frontmatter } = useData();
|
||||
|
||||
const mode = computed(() => {
|
||||
const layout = frontmatter.value.layout;
|
||||
if (layout === 'home') return 'HOME';
|
||||
return 'NORMAL';
|
||||
});
|
||||
|
||||
const filePath = computed(() => {
|
||||
const path = route.path;
|
||||
return path === '/' ? 'index.md' : `${path.replace(/^\//, '')}.md`;
|
||||
});
|
||||
|
||||
const section = computed(() => {
|
||||
const path = route.path;
|
||||
if (path === '/') return 'root';
|
||||
const parts = path.split('/').filter(Boolean);
|
||||
return parts[0] || 'root';
|
||||
});
|
||||
|
||||
const lastUpdated = computed(() => {
|
||||
if (!page.value.lastUpdated) return '';
|
||||
const date = new Date(page.value.lastUpdated);
|
||||
return date.toISOString().slice(0, 10);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="tui-statusline" aria-label="Page info">
|
||||
<div class="tui-statusline__left">
|
||||
<span class="tui-statusline__mode" :data-mode="mode">{{ mode }}</span>
|
||||
<span class="tui-statusline__sep"></span>
|
||||
<span class="tui-statusline__file">{{ filePath }}</span>
|
||||
</div>
|
||||
<div class="tui-statusline__right">
|
||||
<span class="tui-statusline__section">{{ section }}</span>
|
||||
<span class="tui-statusline__sep"></span>
|
||||
<span v-if="lastUpdated" class="tui-statusline__date">{{ lastUpdated }}</span>
|
||||
<span v-if="lastUpdated" class="tui-statusline__sep"></span>
|
||||
<span class="tui-statusline__branch">GPL-3.0</span>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
222
docs-site/.vitepress/theme/index.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import DefaultTheme from 'vitepress/theme';
|
||||
import { useRoute } from 'vitepress';
|
||||
import { nextTick, onMounted, watch } from 'vue';
|
||||
import '@catppuccin/vitepress/theme/macchiato/mauve.css';
|
||||
import './tui-theme.css';
|
||||
import './mermaid-modal.css';
|
||||
import TuiLayout from './TuiLayout.vue';
|
||||
|
||||
let mermaidLoader: Promise<any> | null = null;
|
||||
let plausibleTrackerInitialized = false;
|
||||
const MERMAID_MODAL_ID = 'mermaid-diagram-modal';
|
||||
const PLAUSIBLE_DOMAIN = 'subminer.moe';
|
||||
const PLAUSIBLE_ENDPOINT = 'https://worker.subminer.moe/api/event';
|
||||
|
||||
async function initPlausibleTracker() {
|
||||
if (typeof window === 'undefined' || plausibleTrackerInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { init } = await import('@plausible-analytics/tracker');
|
||||
init({
|
||||
domain: PLAUSIBLE_DOMAIN,
|
||||
endpoint: PLAUSIBLE_ENDPOINT,
|
||||
outboundLinks: true,
|
||||
fileDownloads: true,
|
||||
formSubmissions: true,
|
||||
captureOnLocalhost: false,
|
||||
});
|
||||
plausibleTrackerInitialized = true;
|
||||
}
|
||||
|
||||
function closeMermaidModal() {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = document.getElementById(MERMAID_MODAL_ID);
|
||||
if (!modal) {
|
||||
return;
|
||||
}
|
||||
|
||||
modal.classList.remove('is-open');
|
||||
document.body.classList.remove('mermaid-modal-open');
|
||||
}
|
||||
|
||||
function ensureMermaidModal(): HTMLDivElement {
|
||||
const existing = document.getElementById(MERMAID_MODAL_ID);
|
||||
if (existing) {
|
||||
return existing as HTMLDivElement;
|
||||
}
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.id = MERMAID_MODAL_ID;
|
||||
modal.className = 'mermaid-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="mermaid-modal__backdrop" data-mermaid-close="true"></div>
|
||||
<div class="mermaid-modal__dialog" role="dialog" aria-modal="true" aria-label="Expanded Mermaid diagram">
|
||||
<button class="mermaid-modal__close" type="button" aria-label="Close Mermaid diagram">Close</button>
|
||||
<div class="mermaid-modal__content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.addEventListener('click', (event) => {
|
||||
const target = event.target as HTMLElement | null;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.closest('[data-mermaid-close="true"]') || target.closest('.mermaid-modal__close')) {
|
||||
closeMermaidModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape' && modal.classList.contains('is-open')) {
|
||||
closeMermaidModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(modal);
|
||||
return modal;
|
||||
}
|
||||
|
||||
function openMermaidModal(sourceNode: HTMLElement) {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = ensureMermaidModal();
|
||||
const content = modal.querySelector<HTMLDivElement>('.mermaid-modal__content');
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
content.replaceChildren(sourceNode.cloneNode(true));
|
||||
modal.classList.add('is-open');
|
||||
document.body.classList.add('mermaid-modal-open');
|
||||
}
|
||||
|
||||
function attachMermaidInteractions(nodes: HTMLElement[]) {
|
||||
for (const node of nodes) {
|
||||
if (node.dataset.mermaidInteractive === 'true') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const svg = node.querySelector<HTMLElement>('svg');
|
||||
if (!svg) {
|
||||
continue;
|
||||
}
|
||||
|
||||
node.classList.add('mermaid-interactive');
|
||||
node.setAttribute('role', 'button');
|
||||
node.setAttribute('tabindex', '0');
|
||||
node.setAttribute('aria-label', 'Open Mermaid diagram in full view');
|
||||
|
||||
const open = () => openMermaidModal(svg);
|
||||
node.addEventListener('click', open);
|
||||
node.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
open();
|
||||
}
|
||||
});
|
||||
|
||||
node.dataset.mermaidInteractive = 'true';
|
||||
}
|
||||
}
|
||||
|
||||
async function getMermaid() {
|
||||
if (!mermaidLoader) {
|
||||
mermaidLoader = import('mermaid').then((module) => {
|
||||
const mermaid = module.default;
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: 'loose',
|
||||
theme: 'base',
|
||||
themeVariables: {
|
||||
background: '#24273a',
|
||||
primaryColor: '#363a4f',
|
||||
primaryTextColor: '#cad3f5',
|
||||
primaryBorderColor: '#c6a0f6',
|
||||
secondaryColor: '#494d64',
|
||||
secondaryTextColor: '#cad3f5',
|
||||
secondaryBorderColor: '#b7bdf8',
|
||||
tertiaryColor: '#5b6078',
|
||||
tertiaryTextColor: '#cad3f5',
|
||||
tertiaryBorderColor: '#8aadf4',
|
||||
lineColor: '#939ab7',
|
||||
textColor: '#cad3f5',
|
||||
mainBkg: '#363a4f',
|
||||
nodeBorder: '#c6a0f6',
|
||||
clusterBkg: '#1e2030',
|
||||
clusterBorder: '#494d64',
|
||||
edgeLabelBackground: '#24273a',
|
||||
labelTextColor: '#cad3f5',
|
||||
},
|
||||
});
|
||||
return mermaid;
|
||||
});
|
||||
}
|
||||
return mermaidLoader;
|
||||
}
|
||||
|
||||
async function renderMermaidBlocks() {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const blocks = Array.from(document.querySelectorAll<HTMLElement>('div.language-mermaid'));
|
||||
if (blocks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mermaid = await getMermaid();
|
||||
const nodes: HTMLElement[] = [];
|
||||
|
||||
for (const block of blocks) {
|
||||
if (block.dataset.mermaidRendered === 'true') {
|
||||
continue;
|
||||
}
|
||||
const code = block.querySelector('pre code');
|
||||
const source = code?.textContent?.trim();
|
||||
if (!source) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mount = document.createElement('div');
|
||||
mount.className = 'mermaid';
|
||||
mount.textContent = source;
|
||||
|
||||
block.replaceChildren(mount);
|
||||
block.dataset.mermaidRendered = 'true';
|
||||
nodes.push(mount);
|
||||
}
|
||||
|
||||
if (nodes.length > 0) {
|
||||
await mermaid.run({ nodes });
|
||||
attachMermaidInteractions(nodes);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
Layout: TuiLayout,
|
||||
extends: DefaultTheme,
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
const render = () => {
|
||||
nextTick(() => {
|
||||
renderMermaidBlocks().catch((error) => {
|
||||
console.error('Failed to render Mermaid diagram:', error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initPlausibleTracker().catch((error) => {
|
||||
console.error('Failed to initialize Plausible tracker:', error);
|
||||
});
|
||||
render();
|
||||
});
|
||||
watch(() => route.path, render);
|
||||
},
|
||||
};
|
||||
80
docs-site/.vitepress/theme/mermaid-modal.css
Normal file
@@ -0,0 +1,80 @@
|
||||
.mermaid-interactive {
|
||||
cursor: zoom-in;
|
||||
transition: outline-color 180ms ease;
|
||||
}
|
||||
|
||||
.mermaid-interactive:focus-visible {
|
||||
outline: 2px solid var(--vp-c-brand-1);
|
||||
outline-offset: 4px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.mermaid-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 200;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mermaid-modal.is-open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mermaid-modal__backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.mermaid-modal__dialog {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: 4vh auto;
|
||||
width: min(96vw, 1800px);
|
||||
max-height: 92vh;
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 0;
|
||||
background: var(--vp-c-bg);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 24px 64px rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mermaid-modal__close {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: 16px;
|
||||
margin-top: 12px;
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 0;
|
||||
padding: 4px 10px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
font-family: var(--tui-font-mono);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: border-color 180ms ease, color 180ms ease;
|
||||
}
|
||||
|
||||
.mermaid-modal__close:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.mermaid-modal__content {
|
||||
overflow: auto;
|
||||
max-height: calc(92vh - 56px);
|
||||
padding: 8px 16px 16px;
|
||||
}
|
||||
|
||||
.mermaid-modal__content svg {
|
||||
max-width: none;
|
||||
width: max-content;
|
||||
height: auto;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
body.mermaid-modal-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
637
docs-site/.vitepress/theme/tui-theme.css
Normal file
@@ -0,0 +1,637 @@
|
||||
@import '@fontsource/jetbrains-mono/400.css';
|
||||
@import '@fontsource/jetbrains-mono/500.css';
|
||||
@import '@fontsource/jetbrains-mono/600.css';
|
||||
@import '@fontsource/jetbrains-mono/700.css';
|
||||
@import '@fontsource/manrope/400.css';
|
||||
@import '@fontsource/manrope/500.css';
|
||||
@import '@fontsource/manrope/600.css';
|
||||
@import '@fontsource/manrope/700.css';
|
||||
@import '@fontsource/manrope/800.css';
|
||||
|
||||
:root {
|
||||
--tui-font-mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace;
|
||||
--tui-font-body: 'Manrope', system-ui, sans-serif;
|
||||
--tui-transition: 180ms ease;
|
||||
}
|
||||
|
||||
:root {
|
||||
--vp-font-family-base: var(--tui-font-body);
|
||||
--vp-font-family-mono: var(--tui-font-mono);
|
||||
}
|
||||
|
||||
/* === Selection === */
|
||||
::selection {
|
||||
background: hsla(267, 83%, 80%, 0.22);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
/* === Scrollbar === */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--vp-c-divider);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
/* === Global transitions === */
|
||||
a,
|
||||
button,
|
||||
.VPFeature,
|
||||
.VPNavBarMenuLink,
|
||||
.VPSidebarItem .text {
|
||||
transition: color var(--tui-transition), background var(--tui-transition),
|
||||
border-color var(--tui-transition), opacity var(--tui-transition);
|
||||
}
|
||||
|
||||
/* === Nav bar === */
|
||||
.VPNav .VPNavBarTitle .title {
|
||||
font-family: var(--tui-font-mono);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.VPNav .VPNavBarMenuLink {
|
||||
font-family: var(--tui-font-mono);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.VPNav .VPNavBar {
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
backdrop-filter: blur(12px) saturate(1.2);
|
||||
-webkit-backdrop-filter: blur(12px) saturate(1.2);
|
||||
}
|
||||
|
||||
.VPNav .VPNavBar:not(.has-sidebar) {
|
||||
background: hsla(232, 23%, 18%, 0.82);
|
||||
}
|
||||
|
||||
.VPNav .VPNavBar.has-sidebar .content {
|
||||
backdrop-filter: blur(12px) saturate(1.2);
|
||||
-webkit-backdrop-filter: blur(12px) saturate(1.2);
|
||||
}
|
||||
|
||||
/* === Sidebar === */
|
||||
.VPSidebar .VPSidebarItem .text {
|
||||
font-family: var(--tui-font-mono);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.VPSidebar .VPSidebarItem.is-active.is-link > .item .text::before {
|
||||
content: '> ';
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.VPSidebar .VPSidebarItem.is-link:not(.is-active) > .item .text::before {
|
||||
content: ' ';
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.VPSidebar .VPSidebarItem.is-link:not(.is-active) > .item:hover .text::before {
|
||||
content: '> ';
|
||||
color: var(--vp-c-text-3);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.VPSidebar .VPSidebarItem.is-link:not(.is-active) > .item:hover .text {
|
||||
color: var(--vp-c-text-1) !important;
|
||||
}
|
||||
|
||||
.VPSidebar .VPSidebarItem.level-0 > .item .text {
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.VPSidebar .VPSidebarItem.level-0 > .item .text::before {
|
||||
content: '# ';
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.VPSidebar .VPSidebarItem.level-0 .items {
|
||||
border-left: 1px solid var(--vp-c-divider);
|
||||
margin-left: 6px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
/* === Headings === */
|
||||
.vp-doc h1,
|
||||
.vp-doc h2,
|
||||
.vp-doc h3,
|
||||
.vp-doc h4 {
|
||||
font-family: var(--tui-font-mono);
|
||||
}
|
||||
|
||||
.vp-doc h1 {
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.vp-doc h1::before {
|
||||
content: '> ';
|
||||
color: var(--vp-c-brand-1);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.vp-doc h2 {
|
||||
border-bottom: none;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.vp-doc h2::after {
|
||||
content: '';
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
height: 1px;
|
||||
background: repeating-linear-gradient(
|
||||
to right,
|
||||
var(--vp-c-divider) 0,
|
||||
var(--vp-c-divider) 1ch,
|
||||
transparent 1ch,
|
||||
transparent 1.5ch
|
||||
);
|
||||
}
|
||||
|
||||
/* === Inline code === */
|
||||
.vp-doc :not(pre) > code {
|
||||
border-radius: 0;
|
||||
padding: 2px 6px;
|
||||
font-size: 0.88em;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
/* === Code blocks === */
|
||||
.vp-doc div[class*='language-'] {
|
||||
border-radius: 0;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-alt) !important;
|
||||
}
|
||||
|
||||
.vp-doc div[class*='language-']::before {
|
||||
font-family: var(--tui-font-mono);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.vp-doc div[class*='language-'] > button.copy {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* === Tables === */
|
||||
.vp-doc table {
|
||||
border-collapse: collapse;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.vp-doc table th {
|
||||
font-family: var(--tui-font-mono);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.vp-doc table td,
|
||||
.vp-doc table th {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.vp-doc table tr:hover td {
|
||||
background: hsla(232, 23%, 18%, 0.4);
|
||||
}
|
||||
|
||||
/* === Links === */
|
||||
.vp-doc a {
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid hsla(267, 83%, 80%, 0.3);
|
||||
transition: border-color var(--tui-transition), color var(--tui-transition);
|
||||
}
|
||||
|
||||
.vp-doc a:hover {
|
||||
border-bottom-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
/* === Custom containers === */
|
||||
.vp-doc .custom-block {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 0;
|
||||
padding: 16px 20px;
|
||||
margin: 16px 0;
|
||||
position: relative;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.vp-doc .custom-block::before {
|
||||
font-family: var(--tui-font-mono);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 12px;
|
||||
transform: translateY(-50%);
|
||||
padding: 0 6px;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.vp-doc .custom-block.tip,
|
||||
.vp-doc .custom-block.info,
|
||||
.vp-doc .custom-block.warning,
|
||||
.vp-doc .custom-block.danger,
|
||||
.vp-doc .custom-block.details {
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
.vp-doc .custom-block.tip { border-color: var(--vp-c-brand-1); }
|
||||
.vp-doc .custom-block.tip::before { content: '-- tip'; color: var(--vp-c-brand-1); }
|
||||
|
||||
.vp-doc .custom-block.info { border-color: var(--vp-c-brand-2); }
|
||||
.vp-doc .custom-block.info::before { content: '-- info'; color: var(--vp-c-brand-2); }
|
||||
|
||||
.vp-doc .custom-block.warning { border-color: var(--vp-c-warning-1); }
|
||||
.vp-doc .custom-block.warning::before { content: '-- warning'; color: var(--vp-c-warning-1); }
|
||||
|
||||
.vp-doc .custom-block.danger { border-color: var(--vp-c-danger-1); }
|
||||
.vp-doc .custom-block.danger::before { content: '-- danger'; color: var(--vp-c-danger-1); }
|
||||
|
||||
.vp-doc .custom-block.details { border-color: var(--vp-c-divider); }
|
||||
.vp-doc .custom-block.details::before { content: '-- details'; color: var(--vp-c-text-2); }
|
||||
|
||||
.vp-doc .custom-block .custom-block-title {
|
||||
font-family: var(--tui-font-mono);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* === Blockquotes === */
|
||||
.vp-doc blockquote {
|
||||
border-left: 2px solid var(--vp-c-divider);
|
||||
padding: 4px 0 4px 16px;
|
||||
margin: 16px 0;
|
||||
color: var(--vp-c-text-2);
|
||||
font-style: normal;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.vp-doc blockquote::before {
|
||||
content: '│';
|
||||
position: absolute;
|
||||
left: -2px;
|
||||
top: 4px;
|
||||
color: var(--vp-c-brand-1);
|
||||
font-family: var(--tui-font-mono);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* === Horizontal rules === */
|
||||
.vp-doc hr {
|
||||
border: none;
|
||||
height: 1px;
|
||||
margin: 24px 0;
|
||||
background: repeating-linear-gradient(
|
||||
to right,
|
||||
var(--vp-c-divider) 0,
|
||||
var(--vp-c-divider) 1ch,
|
||||
transparent 1ch,
|
||||
transparent 1.5ch
|
||||
);
|
||||
}
|
||||
|
||||
/* === Lists === */
|
||||
.vp-doc ul > li::marker {
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.vp-doc ol > li::marker {
|
||||
font-family: var(--tui-font-mono);
|
||||
font-size: 0.85em;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
/* === Focus-visible === */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--vp-c-brand-1);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.vp-doc a:focus-visible {
|
||||
outline-offset: 3px;
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
/* === Feature icon hover === */
|
||||
.VPFeatures .VPFeature .icon {
|
||||
transition: transform 280ms ease;
|
||||
}
|
||||
|
||||
.VPFeatures .VPFeature:hover .icon {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* === Blinking cursor === */
|
||||
.tui-cursor {
|
||||
display: inline-block;
|
||||
color: var(--vp-c-brand-1);
|
||||
animation: tui-blink 1s step-end infinite;
|
||||
font-family: var(--tui-font-mono);
|
||||
font-size: 0.9em;
|
||||
vertical-align: baseline;
|
||||
margin-left: 2px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@keyframes tui-blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
|
||||
/* === Statusline === */
|
||||
.tui-statusline {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
font-family: var(--tui-font-mono);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
color: var(--vp-c-text-2);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tui-statusline__left,
|
||||
.tui-statusline__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.tui-statusline__mode {
|
||||
padding: 0 10px;
|
||||
font-weight: 700;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--vp-c-bg);
|
||||
background: var(--vp-c-brand-1);
|
||||
line-height: 28px;
|
||||
margin-left: -12px;
|
||||
}
|
||||
|
||||
.tui-statusline__mode[data-mode="HOME"] {
|
||||
background: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
.tui-statusline__sep {
|
||||
display: inline-block;
|
||||
margin: 0 2px;
|
||||
color: var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.tui-statusline__sep::after {
|
||||
content: '|';
|
||||
}
|
||||
|
||||
.tui-statusline__file {
|
||||
padding: 0 8px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.tui-statusline__section {
|
||||
padding: 0 8px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.tui-statusline__date {
|
||||
padding: 0 8px;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.tui-statusline__branch {
|
||||
padding: 0 10px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-bg);
|
||||
background: var(--vp-c-brand-3, var(--vp-c-brand-1));
|
||||
line-height: 28px;
|
||||
margin-right: -12px;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-bottom: 28px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.tui-statusline__section,
|
||||
.tui-statusline__date {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.VPHome .VPHero::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* === Hero === */
|
||||
.VPHero .name {
|
||||
font-family: var(--tui-font-mono) !important;
|
||||
}
|
||||
|
||||
.VPHero .clip {
|
||||
-webkit-text-fill-color: var(--vp-c-brand-1) !important;
|
||||
}
|
||||
|
||||
.VPHero .text {
|
||||
font-family: var(--tui-font-mono) !important;
|
||||
font-size: 2rem !important;
|
||||
}
|
||||
|
||||
.VPHero .tagline {
|
||||
font-family: var(--tui-font-body);
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.VPHero .actions .action .VPButton {
|
||||
font-family: var(--tui-font-mono);
|
||||
font-size: 13px;
|
||||
border-radius: 8px;
|
||||
text-transform: lowercase;
|
||||
transition: all var(--tui-transition);
|
||||
}
|
||||
|
||||
.VPHero .actions .action .VPButton:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.VPHero .actions .action .VPButton::before {
|
||||
content: '[';
|
||||
opacity: 0.5;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.VPHero .actions .action .VPButton::after {
|
||||
content: ']';
|
||||
opacity: 0.5;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.VPHero .image-container .image-bg {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* === Features === */
|
||||
.VPFeatures .VPFeature {
|
||||
border-radius: 8px !important;
|
||||
border: 1px solid var(--vp-c-divider) !important;
|
||||
transition: border-color var(--tui-transition), background var(--tui-transition),
|
||||
transform var(--tui-transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.VPFeatures .VPFeature::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: var(--vp-c-brand-1);
|
||||
transition: width 280ms ease;
|
||||
}
|
||||
|
||||
.VPFeatures .VPFeature:hover {
|
||||
border-color: var(--vp-c-brand-1) !important;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.VPFeatures .VPFeature:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.VPFeatures .VPFeature .title {
|
||||
font-family: var(--tui-font-mono);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.VPFeatures .VPFeature .details {
|
||||
font-family: var(--tui-font-body);
|
||||
}
|
||||
|
||||
/* === Outline === */
|
||||
.VPDocAsideOutline .outline-title {
|
||||
font-family: var(--tui-font-mono);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-link {
|
||||
font-family: var(--tui-font-mono);
|
||||
font-size: 12px;
|
||||
transition: color var(--tui-transition);
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-link.active {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-link.active::before {
|
||||
content: '> ';
|
||||
}
|
||||
|
||||
/* === Doc footer === */
|
||||
.VPDocFooter .edit-link-button,
|
||||
.VPDocFooter .last-updated-text {
|
||||
font-family: var(--tui-font-mono);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.VPDocFooter .pager-link {
|
||||
border-radius: 0;
|
||||
transition: border-color var(--tui-transition);
|
||||
}
|
||||
|
||||
.VPDocFooter .pager-link:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.VPDocFooter .pager-link .title {
|
||||
font-family: var(--tui-font-mono);
|
||||
}
|
||||
|
||||
.VPDocFooter .pager-link .desc {
|
||||
font-family: var(--tui-font-mono);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* === Search === */
|
||||
.VPLocalSearchBox .search-input {
|
||||
font-family: var(--tui-font-mono);
|
||||
}
|
||||
|
||||
/* === Background texture === */
|
||||
.VPDoc,
|
||||
.VPHome {
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.015'/%3E%3C/svg%3E");
|
||||
background-repeat: repeat;
|
||||
background-size: 200px 200px;
|
||||
}
|
||||
|
||||
/* === Hero glow === */
|
||||
.VPHome .VPHero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.VPHome .VPHero::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -120px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 600px;
|
||||
height: 400px;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
hsla(267, 83%, 80%, 0.06) 0%,
|
||||
transparent 70%
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* === Footer === */
|
||||
.VPFooter {
|
||||
font-family: var(--tui-font-mono);
|
||||
font-size: 12px;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
36
docs-site/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# SubMiner Docs
|
||||
|
||||
In-repo VitePress documentation source for SubMiner.
|
||||
|
||||
## Local development
|
||||
|
||||
```bash
|
||||
bun --cwd docs-site install
|
||||
bun run docs:dev
|
||||
```
|
||||
|
||||
Build and preview:
|
||||
|
||||
```bash
|
||||
bun run docs:build
|
||||
bun run docs:preview
|
||||
bun run docs:test
|
||||
```
|
||||
|
||||
Direct package commands still work from `docs-site/` if you prefer:
|
||||
|
||||
```bash
|
||||
cd docs-site
|
||||
bun install
|
||||
bun run docs:dev
|
||||
```
|
||||
|
||||
## Cloudflare Pages
|
||||
|
||||
- Git repo: `ksyasuda/SubMiner`
|
||||
- Root directory: `docs-site`
|
||||
- Build command: `bun run docs:build`
|
||||
- Build output directory: `.vitepress/dist`
|
||||
- Build watch paths: `docs-site/*`
|
||||
|
||||
Cloudflare Pages watch paths use a single `*` wildcard for monorepo subdirectories. `docs-site/*` matches nested files under the docs site; `docs-site/**` can cause docs-only pushes to be skipped.
|
||||
362
docs-site/anki-integration.md
Normal file
@@ -0,0 +1,362 @@
|
||||
# Anki Integration
|
||||
|
||||
SubMiner uses the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on to create and update Anki cards with sentence context, audio, and screenshots.
|
||||
This project is built primarily for [Kiku](https://kiku.youyoumu.my.id/) and [Lapis](https://github.com/donkuri/lapis) note types, including sentence-card and field-grouping behavior.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Install [Anki](https://apps.ankiweb.net/).
|
||||
2. Install the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on (code: `2055492159`).
|
||||
3. Keep Anki running while using SubMiner.
|
||||
|
||||
AnkiConnect listens on `http://127.0.0.1:8765` by default. If you changed the port in AnkiConnect's settings, update `ankiConnect.url` in your SubMiner config.
|
||||
|
||||
## Auto-Enrichment Transport
|
||||
|
||||
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** — SubMiner runs a local AnkiConnect-compatible proxy and intercepts card creation instantly. Recommended when possible.
|
||||
|
||||
**Polling mode** (default) — SubMiner polls AnkiConnect every few seconds for newly added cards. 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.
|
||||
|
||||
In both modes, the enrichment workflow is the same:
|
||||
|
||||
1. Checks if a duplicate expression already exists (for field grouping).
|
||||
2. Updates the sentence field with the current subtitle.
|
||||
3. Generates and uploads audio and image media.
|
||||
4. Fills the translation field from the secondary subtitle or AI.
|
||||
5. Writes metadata to the miscInfo field.
|
||||
|
||||
Polling mode uses the query `"deck:<your-deck>" added:1` to find recently added cards. If no deck is configured, it searches all decks.
|
||||
|
||||
### Proxy Mode Setup (Yomitan / Texthooker)
|
||||
|
||||
```jsonc
|
||||
"ankiConnect": {
|
||||
"url": "http://127.0.0.1:8765", // real AnkiConnect
|
||||
"proxy": {
|
||||
"enabled": true,
|
||||
"host": "127.0.0.1",
|
||||
"port": 8766,
|
||||
"upstreamUrl": "http://127.0.0.1:8765"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then point Yomitan/clients to `http://127.0.0.1:8766` instead of `8765`.
|
||||
|
||||
When SubMiner loads the bundled Yomitan extension, it also attempts to update the **default Yomitan profile** (`profiles[0].options.anki.server`) to the active SubMiner endpoint:
|
||||
|
||||
- proxy URL when `ankiConnect.proxy.enabled` is `true`
|
||||
- direct `ankiConnect.url` when proxy mode is disabled
|
||||
|
||||
To avoid clobbering custom setups, this auto-update only changes the default profile when its current server is blank or the stock Yomitan default (`http://127.0.0.1:8765`).
|
||||
|
||||
For browser-based Yomitan or other external clients (for example Texthooker in a normal browser profile), set their Anki server to the same proxy URL separately: `http://127.0.0.1:8766` (or your configured `proxy.host` + `proxy.port`).
|
||||
|
||||
### Browser/Yomitan external setup (separate profile)
|
||||
|
||||
If you want SubMiner to use proxy mode without touching your main/default Yomitan profile, create or select a separate Yomitan profile just for SubMiner and set its Anki server to the proxy URL.
|
||||
|
||||
That profile isolation gives you both benefits:
|
||||
|
||||
- SubMiner can auto-enrich immediately via proxy.
|
||||
- Your default Yomitan profile keeps its existing Anki server setting.
|
||||
|
||||
In Yomitan, go to Settings → Profile and:
|
||||
|
||||
1. Create a profile for SubMiner (or choose one dedicated profile).
|
||||
2. Open Anki settings for that profile.
|
||||
3. Set server to `http://127.0.0.1:8766` (or your configured proxy URL).
|
||||
4. Save and make that profile active when using SubMiner.
|
||||
|
||||
This is only for non-bundled, external/browser Yomitan or other clients. The bundled profile auto-update logic only targets `profiles[0]` when it is blank or still default.
|
||||
|
||||
### Proxy Troubleshooting (quick checks)
|
||||
|
||||
If auto-enrichment appears to do nothing:
|
||||
|
||||
1. Confirm proxy listener is running while SubMiner is active:
|
||||
|
||||
```bash
|
||||
ss -ltnp | rg 8766
|
||||
```
|
||||
|
||||
2. Confirm requests can pass through the proxy:
|
||||
|
||||
```bash
|
||||
curl -sS http://127.0.0.1:8766 \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{"action":"version","version":2}'
|
||||
```
|
||||
|
||||
3. Check both log sinks:
|
||||
|
||||
- Launcher/mpv-integrated log: `~/.cache/SubMiner/mp.log`
|
||||
- App runtime log: `~/.config/SubMiner/logs/SubMiner-YYYY-MM-DD.log`
|
||||
|
||||
4. Ensure config JSONC is valid and logging shape is correct:
|
||||
|
||||
```jsonc
|
||||
"logging": {
|
||||
"level": "debug"
|
||||
}
|
||||
```
|
||||
|
||||
`"logging": "debug"` is invalid for current schema and can break reload/start behavior.
|
||||
|
||||
## Field Mapping
|
||||
|
||||
SubMiner maps its data to your Anki note fields. Configure these under `ankiConnect.fields`:
|
||||
|
||||
```jsonc
|
||||
"ankiConnect": {
|
||||
"fields": {
|
||||
"audio": "ExpressionAudio", // audio clip from the video
|
||||
"image": "Picture", // screenshot or animated clip
|
||||
"sentence": "Sentence", // subtitle text
|
||||
"miscInfo": "MiscInfo", // metadata (filename, timestamp)
|
||||
"translation": "SelectionText" // secondary sub or AI translation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Field names must match your Anki note type exactly (case-sensitive). If a configured field does not exist on the note type, SubMiner skips it without error.
|
||||
|
||||
### Minimal Config
|
||||
|
||||
If you only want sentence and audio on your cards:
|
||||
|
||||
```jsonc
|
||||
"ankiConnect": {
|
||||
"enabled": true,
|
||||
"fields": {
|
||||
"sentence": "Sentence",
|
||||
"audio": "ExpressionAudio"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Media Generation
|
||||
|
||||
SubMiner uses FFmpeg to generate audio and image media from the video. FFmpeg must be installed and on `PATH`.
|
||||
|
||||
### Audio
|
||||
|
||||
Audio is extracted from the video file using the subtitle's start and end timestamps, with configurable padding added before and after.
|
||||
|
||||
```jsonc
|
||||
"ankiConnect": {
|
||||
"media": {
|
||||
"generateAudio": true,
|
||||
"audioPadding": 0.5, // seconds before and after subtitle timing
|
||||
"maxMediaDuration": 30 // cap total duration in seconds
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Output format: MP3 at 44100 Hz. If the video has multiple audio streams, SubMiner uses the active stream.
|
||||
|
||||
The audio is uploaded to Anki's media folder and inserted as `[sound:audio_<timestamp>.mp3]`.
|
||||
|
||||
### Screenshots (Static)
|
||||
|
||||
A single frame is captured at the current playback position.
|
||||
|
||||
```jsonc
|
||||
"ankiConnect": {
|
||||
"media": {
|
||||
"generateImage": true,
|
||||
"imageType": "static",
|
||||
"imageFormat": "jpg", // "jpg", "png", or "webp"
|
||||
"imageQuality": 92, // 1–100
|
||||
"imageMaxWidth": null, // optional, preserves aspect ratio
|
||||
"imageMaxHeight": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Animated Clips (AVIF)
|
||||
|
||||
Instead of a static screenshot, SubMiner can generate an animated AVIF covering the subtitle duration.
|
||||
|
||||
```jsonc
|
||||
"ankiConnect": {
|
||||
"media": {
|
||||
"generateImage": true,
|
||||
"imageType": "avif",
|
||||
"animatedFps": 10,
|
||||
"animatedMaxWidth": 640,
|
||||
"animatedMaxHeight": null,
|
||||
"animatedCrf": 35 // 0–63, lower = better quality
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Animated AVIF requires an AV1 encoder (`libaom-av1`, `libsvtav1`, or `librav1e`) in your FFmpeg build. Generation timeout is 60 seconds.
|
||||
|
||||
### Behavior Options
|
||||
|
||||
```jsonc
|
||||
"ankiConnect": {
|
||||
"behavior": {
|
||||
"overwriteAudio": true, // replace existing audio, or append
|
||||
"overwriteImage": true, // replace existing image, or append
|
||||
"mediaInsertMode": "append", // "append" or "prepend" to field content
|
||||
"autoUpdateNewCards": true, // auto-update when new card detected
|
||||
"notificationType": "osd" // "osd", "system", "both", or "none"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## AI Translation
|
||||
|
||||
SubMiner can auto-translate the mined sentence and fill the translation field.
|
||||
Secondary subtitle text still wins when present. AI translation is only attempted when `ankiConnect.ai.enabled` is `true` and no secondary subtitle exists.
|
||||
|
||||
```jsonc
|
||||
"ai": {
|
||||
"enabled": true,
|
||||
"apiKey": "sk-...",
|
||||
"apiKeyCommand": "",
|
||||
"baseUrl": "https://openrouter.ai/api",
|
||||
"requestTimeoutMs": 15000
|
||||
},
|
||||
"ankiConnect": {
|
||||
"ai": {
|
||||
"enabled": true,
|
||||
"model": "openai/gpt-4o-mini",
|
||||
"systemPrompt": "Translate mined sentence text only."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ankiConnect.ai` controls feature-local enablement plus optional `model` / `systemPrompt` overrides.
|
||||
Provider credentials and request transport settings live in top-level `ai`.
|
||||
|
||||
Translation priority:
|
||||
|
||||
1. If a secondary subtitle is available, use it as the translation.
|
||||
2. If `ankiConnect.ai.enabled` is `true` and top-level `ai.enabled` is `true`, call the shared AI provider.
|
||||
3. If AI translation fails and no secondary subtitle exists, fall back to the original sentence text.
|
||||
|
||||
The built-in translation request asks for English output by default. Customize that behavior through `ankiConnect.ai.systemPrompt`.
|
||||
|
||||
## Sentence Cards (Lapis)
|
||||
|
||||
SubMiner can create standalone sentence cards (without a word/expression) using a separate note type. This is designed for use with [Lapis](https://github.com/donkuri/Lapis) and similar sentence-focused note types.
|
||||
|
||||
```jsonc
|
||||
"ankiConnect": {
|
||||
"isLapis": {
|
||||
"enabled": true,
|
||||
"sentenceCardModel": "Japanese sentences"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Trigger with the mine sentence shortcut (`Ctrl/Cmd+S` by default). The card is created directly via AnkiConnect with the sentence, audio, and image filled in.
|
||||
|
||||
To mine multiple subtitle lines as one sentence card, use `Ctrl/Cmd+Shift+S` followed by a digit (1–9) to select how many recent lines to combine.
|
||||
|
||||
## Field Grouping (Kiku)
|
||||
|
||||
When you mine the same word multiple times, SubMiner can merge the cards instead of creating duplicates. This is designed for note types like [Kiku](https://github.com/youyoumu/kiku) that support grouped sentence/audio/image fields.
|
||||
|
||||
```jsonc
|
||||
"ankiConnect": {
|
||||
"isKiku": {
|
||||
"enabled": true,
|
||||
"fieldGrouping": "manual", // "auto", "manual", or "disabled"
|
||||
"deleteDuplicateInAuto": true // delete new card after auto-merge
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Modes
|
||||
|
||||
**Disabled** (`"disabled"`): No duplicate detection. Each card is independent.
|
||||
|
||||
**Auto** (`"auto"`): When a duplicate expression is found, SubMiner merges the new card into the existing one automatically. Both sentences, audio clips, and images are preserved, and exact duplicate values are collapsed to one entry. If `deleteDuplicateInAuto` is true, the new card is deleted after merging.
|
||||
|
||||
**Manual** (`"manual"`): A modal appears in the overlay showing both cards. You choose which card to keep, preview the merge result, then confirm. The modal has a 90-second timeout, after which it cancels automatically.
|
||||
|
||||
### What Gets Merged
|
||||
|
||||
| Field | Merge behavior |
|
||||
| -------- | --------------------------------------------------------------- |
|
||||
| Sentence | Both sentences preserved (exact duplicate text is deduplicated) |
|
||||
| Audio | Both `[sound:...]` entries kept (exact duplicates deduplicated) |
|
||||
| Image | Both images kept (exact duplicates deduplicated) |
|
||||
|
||||
### Keyboard Shortcuts in the Modal
|
||||
|
||||
| Key | Action |
|
||||
| --------- | ---------------------------------- |
|
||||
| `1` / `2` | Select card 1 or card 2 to keep |
|
||||
| `Enter` | Confirm selection |
|
||||
| `Esc` | Cancel (keep both cards unchanged) |
|
||||
|
||||
## Full Config Example
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"ankiConnect": {
|
||||
"enabled": true,
|
||||
"url": "http://127.0.0.1:8765",
|
||||
"pollingRate": 3000,
|
||||
"proxy": {
|
||||
"enabled": false,
|
||||
"host": "127.0.0.1",
|
||||
"port": 8766,
|
||||
"upstreamUrl": "http://127.0.0.1:8765",
|
||||
},
|
||||
"fields": {
|
||||
"audio": "ExpressionAudio",
|
||||
"image": "Picture",
|
||||
"sentence": "Sentence",
|
||||
"miscInfo": "MiscInfo",
|
||||
"translation": "SelectionText",
|
||||
},
|
||||
"media": {
|
||||
"generateAudio": true,
|
||||
"generateImage": true,
|
||||
"imageType": "static",
|
||||
"imageFormat": "jpg",
|
||||
"imageQuality": 92,
|
||||
"audioPadding": 0.5,
|
||||
"maxMediaDuration": 30,
|
||||
},
|
||||
"behavior": {
|
||||
"overwriteAudio": true,
|
||||
"overwriteImage": true,
|
||||
"mediaInsertMode": "append",
|
||||
"autoUpdateNewCards": true,
|
||||
"notificationType": "osd",
|
||||
},
|
||||
"ai": {
|
||||
"enabled": false,
|
||||
"model": "openai/gpt-4o-mini",
|
||||
"systemPrompt": "Translate mined sentence text only.",
|
||||
},
|
||||
"isKiku": {
|
||||
"enabled": false,
|
||||
"fieldGrouping": "disabled",
|
||||
"deleteDuplicateInAuto": true,
|
||||
},
|
||||
"isLapis": {
|
||||
"enabled": false,
|
||||
"sentenceCardModel": "Japanese sentences",
|
||||
},
|
||||
},
|
||||
"ai": {
|
||||
"enabled": false,
|
||||
"apiKey": "",
|
||||
"apiKeyCommand": "",
|
||||
"baseUrl": "https://openrouter.ai/api",
|
||||
"requestTimeoutMs": 15000,
|
||||
},
|
||||
}
|
||||
```
|
||||
356
docs-site/architecture.md
Normal file
@@ -0,0 +1,356 @@
|
||||
# Architecture
|
||||
|
||||
SubMiner is split into three cooperating runtimes:
|
||||
|
||||
- Electron desktop app (`src/`) for overlay/UI/runtime orchestration.
|
||||
- Launcher CLI (`launcher/`) for mpv/app command workflows.
|
||||
- mpv Lua plugin (`plugin/subminer/init.lua` + module files) for player-side controls and IPC handoff.
|
||||
|
||||
Within the desktop app, `src/main.ts` is a composition root that wires small runtime/domain modules plus core services.
|
||||
|
||||
## Goals
|
||||
|
||||
- Keep behavior stable while reducing coupling.
|
||||
- Prefer small, single-purpose units that can be tested in isolation.
|
||||
- Keep `main.ts` focused on wiring and state ownership, not implementation detail.
|
||||
- Follow Unix-style composability:
|
||||
- each service does one job
|
||||
- services compose through explicit inputs/outputs
|
||||
- orchestration is separate from implementation
|
||||
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
launcher/ # Standalone CLI launcher wrapper and mpv helpers
|
||||
commands/ # Command modules (doctor/config/mpv/jellyfin/playback/app passthrough)
|
||||
config/ # Launcher config parsers + CLI parser builder
|
||||
main.ts # Launcher entrypoint and command dispatch
|
||||
plugin/
|
||||
subminer/ # Modular mpv plugin (init · main · bootstrap · lifecycle · process
|
||||
# state · messages · hover · ui · options · environment · log
|
||||
# binary · aniskip · aniskip_match)
|
||||
src/
|
||||
main-entry.ts # Background-mode bootstrap wrapper before loading main.js
|
||||
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
|
||||
app-lifecycle.ts # App lifecycle + app-ready runtime runner factories
|
||||
cli-runtime.ts # CLI command runtime service adapters
|
||||
config-validation.ts # Startup/hot-reload config error formatting and fail-fast helpers
|
||||
dependencies.ts # Shared dependency builders for IPC/runtime services
|
||||
ipc-runtime.ts # IPC runtime registration wrappers
|
||||
overlay-runtime.ts # Overlay modal routing + active-window selection
|
||||
overlay-shortcuts-runtime.ts # Overlay keyboard shortcut handling
|
||||
overlay-visibility-runtime.ts # Overlay visibility + tracker-driven bounds service
|
||||
frequency-dictionary-runtime.ts # Frequency dictionary runtime adapter
|
||||
jlpt-runtime.ts # JLPT dictionary runtime adapter
|
||||
media-runtime.ts # Media path/title/subtitle-position runtime service
|
||||
startup.ts # Startup bootstrap dependency builder
|
||||
startup-lifecycle.ts # Lifecycle runtime runner adapter
|
||||
state.ts # Application runtime state container + reducer transitions
|
||||
subsync-runtime.ts # Subsync command runtime adapter
|
||||
runtime/
|
||||
composers/ # High-level composition clusters used by main.ts
|
||||
domains/ # Domain barrel exports (startup/overlay/mpv/jellyfin/...)
|
||||
registry.ts # Domain registry consumed by main.ts
|
||||
core/
|
||||
services/ # Focused runtime services (Electron adapters + pure logic)
|
||||
anilist/ # AniList token store/update queue/update helpers
|
||||
immersion-tracker/ # Immersion persistence/session/metadata modules
|
||||
tokenizer/ # Tokenizer stage modules (selection/enrichment/annotation)
|
||||
utils/ # Pure helpers and coercion/config utilities
|
||||
cli/ # CLI parsing and help output
|
||||
config/ # Config defaults/definitions, loading, parse, resolution pipeline
|
||||
definitions/ # Domain-specific defaults + option registries
|
||||
resolve/ # Domain-specific config resolution pipeline stages
|
||||
shared/ipc/ # Cross-process IPC channel constants + payload validators
|
||||
renderer/ # Overlay renderer (modularized UI/runtime)
|
||||
handlers/ # Keyboard/mouse interaction modules
|
||||
modals/ # Jimaku/Kiku/subsync/runtime-options/session-help modals
|
||||
positioning/ # Subtitle position controller (drag-to-reposition)
|
||||
window-trackers/ # Backend-specific tracker implementations (Hyprland, Sway, X11, macOS)
|
||||
jimaku/ # Jimaku API integration helpers
|
||||
subsync/ # Subtitle sync (alass/ffsubsync) helpers
|
||||
subtitle/ # Subtitle processing utilities
|
||||
tokenizers/ # Tokenizer implementations
|
||||
anki-integration/ # AnkiConnect proxy server + note-update enrichment workflow
|
||||
token-mergers/ # Token merge strategies
|
||||
translators/ # AI translation providers
|
||||
```
|
||||
|
||||
### Service Layer (`src/core/services/`)
|
||||
|
||||
- **Overlay/window runtime:** `overlay-manager.ts`, `overlay-window.ts`, `overlay-visibility.ts`, `overlay-bridge.ts`, `overlay-runtime-init.ts`, `overlay-content-measurement.ts`
|
||||
- **Shortcuts/input:** `shortcut.ts`, `overlay-shortcut.ts`, `overlay-shortcut-handler.ts`, `shortcut-fallback.ts`, `numeric-shortcut.ts`
|
||||
- **MPV runtime:** `mpv.ts`, `mpv-transport.ts`, `mpv-protocol.ts`, `mpv-properties.ts`, `mpv-render-metrics.ts`
|
||||
- **Mining + Anki/Jimaku runtime:** `mining.ts`, `field-grouping.ts`, `field-grouping-overlay.ts`, `anki-jimaku.ts`, `anki-jimaku-ipc.ts`
|
||||
- **Subtitle/token pipeline:** `subtitle-processing-controller.ts`, `subtitle-position.ts`, `subtitle-ws.ts`, `tokenizer.ts` + `tokenizer/*` stage modules (including `parser-enrichment-worker-runtime.ts` for async MeCab enrichment and `yomitan-parser-runtime.ts`)
|
||||
- **Integrations:** `jimaku.ts`, `subsync.ts`, `subsync-runner.ts`, `texthooker.ts`, `jellyfin.ts`, `jellyfin-remote.ts`, `discord-presence.ts`, `yomitan-extension-loader.ts`, `yomitan-settings.ts`
|
||||
- **Anki integration:** `anki-integration.ts`, `anki-integration/anki-connect-proxy.ts` (local proxy for push-based auto-enrichment), `anki-integration/note-update-workflow.ts`
|
||||
- **Config/runtime controls:** `config-hot-reload.ts`, `runtime-options-ipc.ts`, `cli-command.ts`, `startup.ts`
|
||||
- **Domain submodules:** `anilist/*` (token/update queue/updater), `immersion-tracker/*` (storage/session/metadata/query/reducer)
|
||||
|
||||
### Renderer Layer (`src/renderer/`)
|
||||
|
||||
The renderer keeps `renderer.ts` focused on orchestration. UI behavior is delegated to per-concern modules.
|
||||
|
||||
```text
|
||||
src/renderer/
|
||||
renderer.ts # Entrypoint/orchestration only
|
||||
context.ts # Shared runtime context contract
|
||||
state.ts # Centralized renderer mutable state (visible overlay only)
|
||||
error-recovery.ts # Global renderer error boundary + recovery actions
|
||||
overlay-content-measurement.ts # Reports rendered bounds to main process
|
||||
subtitle-render.ts # Primary/secondary subtitle rendering + style application
|
||||
positioning.ts # Facade export for positioning controller
|
||||
yomitan-popup.ts # Yomitan popup iframe detection utilities
|
||||
positioning/
|
||||
controller.ts # Subtitle drag-position controller
|
||||
position-state.ts # Position state helpers (yPercent)
|
||||
handlers/
|
||||
keyboard.ts # Keybindings, chord handling, modal key routing
|
||||
mouse.ts # Hover/drag behavior, selection + observer wiring
|
||||
modals/
|
||||
jimaku.ts # Jimaku modal flow
|
||||
kiku.ts # Kiku field-grouping modal flow
|
||||
runtime-options.ts # Runtime options modal flow
|
||||
session-help.ts # Keyboard shortcuts/help modal flow
|
||||
subsync.ts # Manual subsync modal flow
|
||||
utils/
|
||||
dom.ts # Required DOM lookups + typed handles
|
||||
platform.ts # Layer/platform capability detection
|
||||
```
|
||||
|
||||
### Launcher + Plugin Runtimes
|
||||
|
||||
- `launcher/main.ts` dispatches commands through `launcher/commands/*` and shared config readers in `launcher/config/*`. It handles mpv startup, app passthrough, Jellyfin helper commands, and playback handoff.
|
||||
- `plugin/subminer/init.lua` runs inside mpv and loads modular Lua files: `main.lua` (orchestration), `bootstrap.lua` (startup), `lifecycle.lua` (connect/disconnect), `process.lua` (process management), `state.lua` (shared state), `messages.lua` (IPC), `hover.lua` (hover-token highlight rendering), `ui.lua` (OSD rendering), `options.lua` (config), `environment.lua` (detection), `log.lua` (logging), `binary.lua` (path resolution), `aniskip.lua` + `aniskip_match.lua` (intro-skip UX).
|
||||
|
||||
## Flow Diagram
|
||||
|
||||
The main process orchestrates a single primary overlay window plus modal surfaces: `main.ts` delegates to composition modules that wire together domain services. Subtitle layers (primary + secondary bar) are rendered in the same overlay renderer process, connected through `preload.ts`. External runtimes (launcher CLI and mpv plugin) operate independently and communicate via IPC socket or CLI passthrough.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
classDef entry fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:2px,font-weight:bold
|
||||
classDef comp fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef svc fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef bridge fill:#f5a97f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef rend fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef ext fill:#a6da95,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef extrt fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
|
||||
subgraph ExtRt["External Runtimes"]
|
||||
Launcher["Launcher CLI"]:::extrt
|
||||
Plugin["mpv Plugin"]:::extrt
|
||||
end
|
||||
|
||||
subgraph Ext["External Systems"]
|
||||
mpvExt["mpv"]:::ext
|
||||
AnkiExt["AnkiConnect"]:::ext
|
||||
JimakuExt["Jimaku"]:::ext
|
||||
TrackerExt["Window Tracker"]:::ext
|
||||
AnilistExt["AniList"]:::ext
|
||||
JellyfinExt["Jellyfin"]:::ext
|
||||
DiscordExt["Discord"]:::ext
|
||||
end
|
||||
|
||||
Main["main.ts"]:::entry
|
||||
|
||||
subgraph Comp["Composition"]
|
||||
Startup["Startup & Lifecycle"]:::comp
|
||||
Wiring["Runtime Wiring"]:::comp
|
||||
Composers["Domain Composers"]:::comp
|
||||
end
|
||||
|
||||
subgraph Svc["Services"]
|
||||
Mpv["MPV Stack"]:::svc
|
||||
OverlaySvc["Overlay Manager"]:::svc
|
||||
Mining["Mining & Subtitles"]:::svc
|
||||
AnkiProxy["Anki Proxy"]:::svc
|
||||
Integrations["Integrations"]:::svc
|
||||
Tracking["Tracking"]:::svc
|
||||
Config["Config & Options"]:::svc
|
||||
end
|
||||
|
||||
Bridge(["preload.ts"]):::bridge
|
||||
|
||||
subgraph Rend["Renderer"]
|
||||
OverlayWin["Overlay Window"]:::rend
|
||||
UI["Subtitles & Modals"]:::rend
|
||||
end
|
||||
|
||||
Launcher -->|"CLI"| Main
|
||||
Plugin -->|"IPC"| mpvExt
|
||||
|
||||
Main --> Comp
|
||||
Comp --> Svc
|
||||
|
||||
mpvExt <-->|"socket"| Mpv
|
||||
AnkiExt <-->|"HTTP"| AnkiProxy
|
||||
JimakuExt <-->|"HTTP"| Integrations
|
||||
TrackerExt <-->|"platform"| OverlaySvc
|
||||
AnilistExt <-->|"HTTP"| Tracking
|
||||
JellyfinExt <-->|"HTTP"| Tracking
|
||||
DiscordExt <-->|"RPC"| Integrations
|
||||
|
||||
OverlaySvc & Mining --> Bridge
|
||||
Bridge --> OverlayWin
|
||||
OverlayWin --> UI
|
||||
|
||||
style Comp fill:#363a4f,stroke:#494d64,color:#cad3f5
|
||||
style Svc fill:#363a4f,stroke:#494d64,color:#cad3f5
|
||||
style Rend fill:#363a4f,stroke:#494d64,color:#cad3f5
|
||||
style Ext fill:#363a4f,stroke:#494d64,color:#cad3f5
|
||||
style ExtRt fill:#363a4f,stroke:#494d64,color:#cad3f5
|
||||
```
|
||||
|
||||
## Composition Pattern
|
||||
|
||||
Most runtime code follows a dependency-injection pattern:
|
||||
|
||||
1. Define a service interface in `src/core/services/*`.
|
||||
2. Keep core logic in pure or side-effect-bounded functions.
|
||||
3. Build runtime deps in `src/main/` composition modules; extract an adapter/helper only when it adds meaningful behavior or reuse.
|
||||
4. Call the service from lifecycle/command wiring points.
|
||||
|
||||
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
|
||||
|
||||
Composer modules share contract conventions via `src/main/runtime/composers/contracts.ts`:
|
||||
|
||||
- composer input surfaces are declared with `ComposerInputs<T>` so required dependencies cannot be omitted at compile time
|
||||
- composer outputs are declared with `ComposerOutputs<T>` to keep result contracts explicit and stable
|
||||
- builder return payload extraction should use shared type helpers instead of inline ad-hoc inference
|
||||
|
||||
This keeps side effects explicit and makes behavior easy to unit-test with fakes.
|
||||
|
||||
Additional conventions in the current code:
|
||||
|
||||
- `main.ts` uses `createMainRuntimeRegistry()` (`src/main/runtime/registry.ts`) to access domain handlers (`startup`, `overlay`, `mpv`, `ipc`, `shortcuts`, `anilist`, `jellyfin`, `mining`) without importing every runtime module directly.
|
||||
- Domain barrels in `src/main/runtime/domains/*` re-export runtime handlers + main-deps builders, while composers in `src/main/runtime/composers/*` assemble larger runtime clusters.
|
||||
- Many runtime handlers accept `*MainDeps` objects generated by `createBuild*MainDepsHandler` builders to isolate side effects and keep units testable.
|
||||
|
||||
### IPC Contract + Validation Boundary
|
||||
|
||||
- Central channel constants live in `src/shared/ipc/contracts.ts` and are consumed by both main (`ipcMain`) and renderer preload (`ipcRenderer`) wiring.
|
||||
- Runtime payload parsers/type guards live in `src/shared/ipc/validators.ts`.
|
||||
- Rule: renderer-supplied payloads must be validated at IPC entry points (`src/core/services/ipc.ts`, `src/core/services/anki-jimaku-ipc.ts`) before calling domain handlers.
|
||||
- Malformed invoke payloads return explicit structured errors (for example `{ ok: false, error: ... }`) and malformed fire-and-forget payloads are ignored safely.
|
||||
|
||||
### Runtime State Ownership (Migrated Domains)
|
||||
|
||||
For domains migrated to reducer-style transitions (for example AniList token/queue/media-guess runtime state), follow these rules:
|
||||
|
||||
- Composition/runtime modules own mutable state cells and expose narrow `get*`/`set*` accessors.
|
||||
- Domain handlers do not mutate foreign state directly; they call explicit transition helpers that encode invariants.
|
||||
- Transition helpers may sync derived counters/snapshots, but must preserve non-owned metadata unless the transition explicitly owns that metadata.
|
||||
- Reducer boundary: when a domain has transition helpers in `src/main/state.ts`, new callsites should route updates through those helpers instead of ad-hoc object mutation in `main.ts` or composers.
|
||||
- Tests for migrated domains should assert both the intended field changes and non-targeted field invariants.
|
||||
|
||||
## Program Lifecycle
|
||||
|
||||
- **Module-level init:** Before `app.ready`, the composition root registers protocols, sets platform flags, constructs all services, and wires dependency injection. `runAndApplyStartupState()` parses CLI args and detects the compositor backend.
|
||||
- **Startup:** If `--generate-config` is passed, it writes the template and exits. Otherwise `app-lifecycle.ts` acquires the single-instance lock and registers Electron lifecycle hooks.
|
||||
- **Critical-path init:** Once `app.whenReady()` fires, `composeAppReadyRuntime()` runs strict config reload, resolves keybindings, creates the `MpvIpcClient` (which immediately connects and subscribes to 26 properties), and initializes the `RuntimeOptionsManager`, `SubtitleTimingTracker`, and `ImmersionTrackerService`.
|
||||
- **Overlay runtime:** `initializeOverlayRuntime()` creates the primary overlay window (interactive Yomitan lookups and subtitle rendering), registers global shortcuts, and sets up bounds tracking via the active window tracker. mpv subtitle suppression is handled by a dedicated `overlay-mpv-sub-visibility` service.
|
||||
- **Background warmups:** Non-critical services are launched asynchronously: MeCab tokenizer check (with async worker thread), Yomitan extension load, JLPT + frequency dictionary prewarm, optional Jellyfin remote session, Discord presence service, AniList token refresh, and optional AnkiConnect proxy server. Warmup coverage is configurable through `startupWarmups` (including low-power mode that defers all but Yomitan).
|
||||
- **Runtime:** Event-driven. mpv property changes, IPC messages, CLI commands, overlay shortcuts, and hot-reload notifications route through runtime handlers/composers. Subtitle text flows through `SubtitlePipeline` (normalize → tokenize → merge), and results are sent to the main overlay renderer and modal surfaces.
|
||||
- **Shutdown:** `onWillQuitCleanup` destroys tray + config watcher, unregisters shortcuts, stops WebSocket + texthooker servers, closes the mpv socket + flushes OSD log, stops the window tracker, closes the Yomitan parser window, flushes the immersion tracker (SQLite), stops Jellyfin/Discord services, stops the AnkiConnect proxy server, and cleans Anki/AniList state.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
classDef start fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:2px,font-weight:bold
|
||||
classDef phase fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef decision fill:#f5a97f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef init fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef runtime fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef shutdown fill:#ed8796,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef warmup fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
|
||||
CLI["CLI + Environment"]:::start
|
||||
CLI --> Init["Module Init"]:::phase
|
||||
Init --> Parse["Parse argv"]:::phase
|
||||
Parse --> GenCheck{"--generate\nconfig?"}:::decision
|
||||
GenCheck -->|"yes"| GenExit["Write & exit"]:::phase
|
||||
GenCheck -->|"no"| Lock["Acquire lock"]:::phase
|
||||
|
||||
Lock -->|"app.whenReady()"| Ready["App Ready"]:::phase
|
||||
|
||||
Ready --> Config["Config + keybindings"]:::init
|
||||
Ready --> MpvInit["MPV socket connect"]:::init
|
||||
Ready --> Platform["Runtime services"]:::init
|
||||
|
||||
Config & MpvInit & Platform --> OverlayInit["Overlay Init"]:::phase
|
||||
|
||||
OverlayInit --> MainWin["Create window"]:::init
|
||||
OverlayInit --> Shortcuts["Register shortcuts"]:::init
|
||||
|
||||
MainWin & Shortcuts --> Warmups["Background Warmups"]:::phase
|
||||
|
||||
subgraph WarmupGroup[" "]
|
||||
direction TB
|
||||
W1["MeCab"]:::warmup
|
||||
W2["Yomitan"]:::warmup
|
||||
W3["Dictionaries"]:::warmup
|
||||
W4["Jellyfin"]:::warmup
|
||||
W5["Discord"]:::warmup
|
||||
W6["AniList"]:::warmup
|
||||
W7["Anki Proxy"]:::warmup
|
||||
W1 ~~~ W2 ~~~ W3 ~~~ W4 ~~~ W5 ~~~ W6 ~~~ W7
|
||||
end
|
||||
|
||||
Warmups --> WarmupGroup
|
||||
|
||||
subgraph Loop["Event Loop"]
|
||||
direction TB
|
||||
Events["mpv · IPC · shortcuts · config"]:::runtime
|
||||
Events --> Route["Composers"]:::runtime
|
||||
Route --> Pipeline["Subtitle Pipeline"]:::runtime
|
||||
Pipeline --> Broadcast["State + Renderer"]:::runtime
|
||||
end
|
||||
|
||||
WarmupGroup --> Loop
|
||||
|
||||
style WarmupGroup fill:transparent,stroke:none
|
||||
|
||||
Loop -->|"quit"| Quit["Shutdown"]:::shutdown
|
||||
|
||||
Quit --> T1["UI cleanup"]:::shutdown
|
||||
Quit --> T2["Socket + server teardown"]:::shutdown
|
||||
Quit --> T3["Flush tracking + state"]:::shutdown
|
||||
|
||||
style Loop fill:#363a4f,stroke:#494d64,color:#cad3f5
|
||||
```
|
||||
|
||||
## Why This Design
|
||||
|
||||
- **Smaller blast radius:** changing one feature usually touches one service.
|
||||
- **Better testability:** most behavior can be tested without Electron windows/mpv.
|
||||
- **Better reviewability:** PRs can be scoped to one subsystem.
|
||||
- **Backward compatibility:** CLI flags and IPC channels can remain stable while internals evolve.
|
||||
- **Runtime registry + domain barrels:** `src/main/runtime/registry.ts` and `src/main/runtime/domains/*` reduce direct fan-in inside `main.ts` while keeping domain ownership explicit.
|
||||
- **Extracted composition root:** `main.ts` delegates to focused modules under `src/main/` and `src/main/runtime/composers/` for lifecycle, IPC, overlay, mpv, shortcut, and integration wiring.
|
||||
- **Split MPV service layers:** MPV internals are separated into transport (`mpv-transport.ts`), protocol (`mpv-protocol.ts`), and properties/render metrics modules for maintainability.
|
||||
- **Config by domain:** defaults, option registries, and resolution are split by domain under `src/config/definitions/*` and `src/config/resolve/*`, keeping config evolution localized.
|
||||
|
||||
## Extension Rules
|
||||
|
||||
- Add behavior to an existing service in `src/core/services/*` or create a focused runtime module under `src/main/runtime/*`; avoid ad-hoc logic in `main.ts`.
|
||||
- Add new cross-process channels in `src/shared/ipc/contracts.ts` first, validate payloads in `src/shared/ipc/validators.ts`, then wire handlers in IPC runtime modules.
|
||||
- See also the contributor IPC onboarding page: [IPC + Runtime Contracts](/ipc-contracts).
|
||||
- If change spans startup/overlay/mpv/integration wiring, prefer composing through `src/main/runtime/domains/*` + `src/main/runtime/composers/*` rather than direct wiring in `main.ts`.
|
||||
- Keep service APIs explicit and narrowly scoped, and preserve existing CLI flag / IPC channel behavior unless the change is intentionally breaking.
|
||||
- Add or update focused tests (including malformed-payload IPC tests) when runtime boundaries or contracts change.
|
||||
626
docs-site/bun.lock
Normal file
@@ -0,0 +1,626 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "subminer-docs",
|
||||
"dependencies": {
|
||||
"@catppuccin/vitepress": "^0.1.2",
|
||||
"@fontsource/jetbrains-mono": "^5.2.8",
|
||||
"@fontsource/manrope": "^5.2.8",
|
||||
"@plausible-analytics/tracker": "^0.4.4",
|
||||
"mermaid": "^11.12.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitepress": "^1.6.4",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@algolia/abtesting": ["@algolia/abtesting@1.15.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-2yuIC48rUuHGhU1U5qJ9kJHaxYpJ0jpDHJVI5ekOxSMYXlH4+HP+pA31G820lsAznfmu2nzDV7n5RO44zIY1zw=="],
|
||||
|
||||
"@algolia/autocomplete-core": ["@algolia/autocomplete-core@1.17.7", "", { "dependencies": { "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", "@algolia/autocomplete-shared": "1.17.7" } }, "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q=="],
|
||||
|
||||
"@algolia/autocomplete-plugin-algolia-insights": ["@algolia/autocomplete-plugin-algolia-insights@1.17.7", "", { "dependencies": { "@algolia/autocomplete-shared": "1.17.7" }, "peerDependencies": { "search-insights": ">= 1 < 3" } }, "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A=="],
|
||||
|
||||
"@algolia/autocomplete-preset-algolia": ["@algolia/autocomplete-preset-algolia@1.17.7", "", { "dependencies": { "@algolia/autocomplete-shared": "1.17.7" }, "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", "algoliasearch": ">= 4.9.1 < 6" } }, "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA=="],
|
||||
|
||||
"@algolia/autocomplete-shared": ["@algolia/autocomplete-shared@1.17.7", "", { "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", "algoliasearch": ">= 4.9.1 < 6" } }, "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg=="],
|
||||
|
||||
"@algolia/client-abtesting": ["@algolia/client-abtesting@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-h6M7HzPin+45/l09q0r2dYmocSSt2MMGOOk5c4O5K/bBBlEwf1BKfN6z+iX4b8WXcQQhf7rgQwC52kBZJt/ZZw=="],
|
||||
|
||||
"@algolia/client-analytics": ["@algolia/client-analytics@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-048T9/Z8OeLmTk8h76QUqaNFp7Rq2VgS2Zm6Y2tNMYGQ1uNuzePY/udB5l5krlXll7ZGflyCjFvRiOtlPZpE9g=="],
|
||||
|
||||
"@algolia/client-common": ["@algolia/client-common@5.49.1", "", {}, "sha512-vp5/a9ikqvf3mn9QvHN8PRekn8hW34aV9eX+O0J5mKPZXeA6Pd5OQEh2ZWf7gJY6yyfTlLp5LMFzQUAU+Fpqpg=="],
|
||||
|
||||
"@algolia/client-insights": ["@algolia/client-insights@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-B6N7PgkvYrul3bntTz/l6uXnhQ2bvP+M7NqTcayh681tSqPaA5cJCUBp/vrP7vpPRpej4Eeyx2qz5p0tE/2N2g=="],
|
||||
|
||||
"@algolia/client-personalization": ["@algolia/client-personalization@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-v+4DN+lkYfBd01Hbnb9ZrCHe7l+mvihyx218INRX/kaCXROIWUDIT1cs3urQxfE7kXBFnLsqYeOflQALv/gA5w=="],
|
||||
|
||||
"@algolia/client-query-suggestions": ["@algolia/client-query-suggestions@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-Un11cab6ZCv0W+Jiak8UktGIqoa4+gSNgEZNfG8m8eTsXGqwIEr370H3Rqwj87zeNSlFpH2BslMXJ/cLNS1qtg=="],
|
||||
|
||||
"@algolia/client-search": ["@algolia/client-search@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-Nt9hri7nbOo0RipAsGjIssHkpLMHHN/P7QqENywAq5TLsoYDzUyJGny8FEiD/9KJUxtGH8blGpMedilI6kK3rA=="],
|
||||
|
||||
"@algolia/ingestion": ["@algolia/ingestion@1.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-b5hUXwDqje0Y4CpU6VL481DXgPgxpTD5sYMnfQTHKgUispGnaCLCm2/T9WbJo1YNUbX3iHtYDArp804eD6CmRQ=="],
|
||||
|
||||
"@algolia/monitoring": ["@algolia/monitoring@1.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-bvrXwZ0WsL3rN6Q4m4QqxsXFCo6WAew7sAdrpMQMK4Efn4/W920r9ptOuckejOSSvyLr9pAWgC5rsHhR2FYuYw=="],
|
||||
|
||||
"@algolia/recommend": ["@algolia/recommend@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-h2yz3AGeGkQwNgbLmoe3bxYs8fac4An1CprKTypYyTU/k3Q+9FbIvJ8aS1DoBKaTjSRZVoyQS7SZQio6GaHbZw=="],
|
||||
|
||||
"@algolia/requester-browser-xhr": ["@algolia/requester-browser-xhr@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1" } }, "sha512-2UPyRuUR/qpqSqH8mxFV5uBZWEpxhGPHLlx9Xf6OVxr79XO2ctzZQAhsmTZ6X22x+N8MBWpB9UEky7YU2HGFgA=="],
|
||||
|
||||
"@algolia/requester-fetch": ["@algolia/requester-fetch@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1" } }, "sha512-N+xlE4lN+wpuT+4vhNEwPVlrfN+DWAZmSX9SYhbz986Oq8AMsqdntOqUyiOXVxYsQtfLwmiej24vbvJGYv1Qtw=="],
|
||||
|
||||
"@algolia/requester-node-http": ["@algolia/requester-node-http@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1" } }, "sha512-zA5bkUOB5PPtTr182DJmajCiizHp0rCJQ0Chf96zNFvkdESKYlDeYA3tQ7r2oyHbu/8DiohAQ5PZ85edctzbXA=="],
|
||||
|
||||
"@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
|
||||
"@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="],
|
||||
|
||||
"@catppuccin/vitepress": ["@catppuccin/vitepress@0.1.2", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-dqhgo6U6GWbgh3McAgwemUC8Y2Aj48rRcQx/9iuPzBPAgo7NA3yi7ZcR0wolAENMmoOMAHBV+rz/5DfiGxtZLA=="],
|
||||
|
||||
"@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@11.1.2", "", { "dependencies": { "@chevrotain/gast": "11.1.2", "@chevrotain/types": "11.1.2", "lodash-es": "4.17.23" } }, "sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q=="],
|
||||
|
||||
"@chevrotain/gast": ["@chevrotain/gast@11.1.2", "", { "dependencies": { "@chevrotain/types": "11.1.2", "lodash-es": "4.17.23" } }, "sha512-Z9zfXR5jNZb1Hlsd/p+4XWeUFugrHirq36bKzPWDSIacV+GPSVXdk+ahVWZTwjhNwofAWg/sZg58fyucKSQx5g=="],
|
||||
|
||||
"@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@11.1.2", "", {}, "sha512-nMU3Uj8naWer7xpZTYJdxbAs6RIv/dxYzkYU8GSwgUtcAAlzjcPfX1w+RKRcYG8POlzMeayOQ/znfwxEGo5ulw=="],
|
||||
|
||||
"@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="],
|
||||
|
||||
"@chevrotain/utils": ["@chevrotain/utils@11.1.2", "", {}, "sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA=="],
|
||||
|
||||
"@docsearch/css": ["@docsearch/css@3.8.2", "", {}, "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ=="],
|
||||
|
||||
"@docsearch/js": ["@docsearch/js@3.8.2", "", { "dependencies": { "@docsearch/react": "3.8.2", "preact": "^10.0.0" } }, "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ=="],
|
||||
|
||||
"@docsearch/react": ["@docsearch/react@3.8.2", "", { "dependencies": { "@algolia/autocomplete-core": "1.17.7", "@algolia/autocomplete-preset-algolia": "1.17.7", "@docsearch/css": "3.8.2", "algoliasearch": "^5.14.2" }, "peerDependencies": { "@types/react": ">= 16.8.0 < 19.0.0", "react": ">= 16.8.0 < 19.0.0", "react-dom": ">= 16.8.0 < 19.0.0", "search-insights": ">= 1 < 3" }, "optionalPeers": ["@types/react", "react", "react-dom", "search-insights"] }, "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
|
||||
|
||||
"@fontsource/jetbrains-mono": ["@fontsource/jetbrains-mono@5.2.8", "", {}, "sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ=="],
|
||||
|
||||
"@fontsource/manrope": ["@fontsource/manrope@5.2.8", "", {}, "sha512-gJHJmcuUk7qWcNCfcAri/DJQtXtBYqi9yKratr4jXhSo0I3xUtNNKI+igQIcw5c+m95g0vounk8ZnX/kb8o0TA=="],
|
||||
|
||||
"@iconify-json/simple-icons": ["@iconify-json/simple-icons@1.2.72", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-wkcixntHvaCoqPqerGrNFcHQ3Yx1ux4ZkhscCDK0DEHpP62XCH+cxq1HTsRjbUiQl/M9K8bj03HF6Wgn5iE2rQ=="],
|
||||
|
||||
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
||||
|
||||
"@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@mermaid-js/parser": ["@mermaid-js/parser@1.0.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw=="],
|
||||
|
||||
"@plausible-analytics/tracker": ["@plausible-analytics/tracker@0.4.4", "", {}, "sha512-fz0NOYUEYXtg1TBaPEEvtcBq3FfmLFuTe1VZw4M8sTWX129br5dguu3M15+plOQnc181ShYe67RfwhKgK89VnA=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
|
||||
|
||||
"@shikijs/core": ["@shikijs/core@2.5.0", "", { "dependencies": { "@shikijs/engine-javascript": "2.5.0", "@shikijs/engine-oniguruma": "2.5.0", "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg=="],
|
||||
|
||||
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@2.5.0", "", { "dependencies": { "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^3.1.0" } }, "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w=="],
|
||||
|
||||
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@2.5.0", "", { "dependencies": { "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw=="],
|
||||
|
||||
"@shikijs/langs": ["@shikijs/langs@2.5.0", "", { "dependencies": { "@shikijs/types": "2.5.0" } }, "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w=="],
|
||||
|
||||
"@shikijs/themes": ["@shikijs/themes@2.5.0", "", { "dependencies": { "@shikijs/types": "2.5.0" } }, "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw=="],
|
||||
|
||||
"@shikijs/transformers": ["@shikijs/transformers@2.5.0", "", { "dependencies": { "@shikijs/core": "2.5.0", "@shikijs/types": "2.5.0" } }, "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg=="],
|
||||
|
||||
"@shikijs/types": ["@shikijs/types@2.5.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw=="],
|
||||
|
||||
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||
|
||||
"@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="],
|
||||
|
||||
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
||||
|
||||
"@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="],
|
||||
|
||||
"@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="],
|
||||
|
||||
"@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="],
|
||||
|
||||
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
|
||||
|
||||
"@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="],
|
||||
|
||||
"@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="],
|
||||
|
||||
"@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="],
|
||||
|
||||
"@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="],
|
||||
|
||||
"@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="],
|
||||
|
||||
"@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
|
||||
|
||||
"@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="],
|
||||
|
||||
"@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="],
|
||||
|
||||
"@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="],
|
||||
|
||||
"@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="],
|
||||
|
||||
"@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="],
|
||||
|
||||
"@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
|
||||
|
||||
"@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
|
||||
|
||||
"@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="],
|
||||
|
||||
"@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="],
|
||||
|
||||
"@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="],
|
||||
|
||||
"@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
|
||||
|
||||
"@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="],
|
||||
|
||||
"@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="],
|
||||
|
||||
"@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="],
|
||||
|
||||
"@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
|
||||
|
||||
"@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="],
|
||||
|
||||
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
|
||||
|
||||
"@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="],
|
||||
|
||||
"@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
|
||||
|
||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||
|
||||
"@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="],
|
||||
|
||||
"@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="],
|
||||
|
||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||
|
||||
"@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
|
||||
"@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@5.2.4", "", { "peerDependencies": { "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA=="],
|
||||
|
||||
"@vue/compiler-core": ["@vue/compiler-core@3.5.29", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.29", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw=="],
|
||||
|
||||
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.29", "", { "dependencies": { "@vue/compiler-core": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg=="],
|
||||
|
||||
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.29", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.29", "@vue/compiler-dom": "3.5.29", "@vue/compiler-ssr": "3.5.29", "@vue/shared": "3.5.29", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA=="],
|
||||
|
||||
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.29", "", { "dependencies": { "@vue/compiler-dom": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw=="],
|
||||
|
||||
"@vue/devtools-api": ["@vue/devtools-api@7.7.9", "", { "dependencies": { "@vue/devtools-kit": "^7.7.9" } }, "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g=="],
|
||||
|
||||
"@vue/devtools-kit": ["@vue/devtools-kit@7.7.9", "", { "dependencies": { "@vue/devtools-shared": "^7.7.9", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA=="],
|
||||
|
||||
"@vue/devtools-shared": ["@vue/devtools-shared@7.7.9", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA=="],
|
||||
|
||||
"@vue/reactivity": ["@vue/reactivity@3.5.29", "", { "dependencies": { "@vue/shared": "3.5.29" } }, "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA=="],
|
||||
|
||||
"@vue/runtime-core": ["@vue/runtime-core@3.5.29", "", { "dependencies": { "@vue/reactivity": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg=="],
|
||||
|
||||
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.29", "", { "dependencies": { "@vue/reactivity": "3.5.29", "@vue/runtime-core": "3.5.29", "@vue/shared": "3.5.29", "csstype": "^3.2.3" } }, "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg=="],
|
||||
|
||||
"@vue/server-renderer": ["@vue/server-renderer@3.5.29", "", { "dependencies": { "@vue/compiler-ssr": "3.5.29", "@vue/shared": "3.5.29" }, "peerDependencies": { "vue": "3.5.29" } }, "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g=="],
|
||||
|
||||
"@vue/shared": ["@vue/shared@3.5.29", "", {}, "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg=="],
|
||||
|
||||
"@vueuse/core": ["@vueuse/core@12.8.2", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "12.8.2", "@vueuse/shared": "12.8.2", "vue": "^3.5.13" } }, "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ=="],
|
||||
|
||||
"@vueuse/integrations": ["@vueuse/integrations@12.8.2", "", { "dependencies": { "@vueuse/core": "12.8.2", "@vueuse/shared": "12.8.2", "vue": "^3.5.13" }, "peerDependencies": { "async-validator": "^4", "axios": "^1", "change-case": "^5", "drauu": "^0.4", "focus-trap": "^7", "fuse.js": "^7", "idb-keyval": "^6", "jwt-decode": "^4", "nprogress": "^0.2", "qrcode": "^1.5", "sortablejs": "^1", "universal-cookie": "^7" }, "optionalPeers": ["async-validator", "axios", "change-case", "drauu", "focus-trap", "fuse.js", "idb-keyval", "jwt-decode", "nprogress", "qrcode", "sortablejs", "universal-cookie"] }, "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g=="],
|
||||
|
||||
"@vueuse/metadata": ["@vueuse/metadata@12.8.2", "", {}, "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A=="],
|
||||
|
||||
"@vueuse/shared": ["@vueuse/shared@12.8.2", "", { "dependencies": { "vue": "^3.5.13" } }, "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w=="],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
"algoliasearch": ["algoliasearch@5.49.1", "", { "dependencies": { "@algolia/abtesting": "1.15.1", "@algolia/client-abtesting": "5.49.1", "@algolia/client-analytics": "5.49.1", "@algolia/client-common": "5.49.1", "@algolia/client-insights": "5.49.1", "@algolia/client-personalization": "5.49.1", "@algolia/client-query-suggestions": "5.49.1", "@algolia/client-search": "5.49.1", "@algolia/ingestion": "1.49.1", "@algolia/monitoring": "1.49.1", "@algolia/recommend": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-X3Pp2aRQhg4xUC6PQtkubn5NpRKuUPQ9FPDQlx36SmpFwwH2N0/tw4c+NXV3nw3PsgeUs+BuWGP0gjz3TvENLQ=="],
|
||||
|
||||
"birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
|
||||
|
||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||
|
||||
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
||||
|
||||
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
||||
|
||||
"chevrotain": ["chevrotain@11.1.2", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.1.2", "@chevrotain/gast": "11.1.2", "@chevrotain/regexp-to-ast": "11.1.2", "@chevrotain/types": "11.1.2", "@chevrotain/utils": "11.1.2", "lodash-es": "4.17.23" } }, "sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg=="],
|
||||
|
||||
"chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="],
|
||||
|
||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||
|
||||
"commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
|
||||
"confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
||||
|
||||
"copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="],
|
||||
|
||||
"cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"cytoscape": ["cytoscape@3.33.1", "", {}, "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ=="],
|
||||
|
||||
"cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="],
|
||||
|
||||
"cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="],
|
||||
|
||||
"d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="],
|
||||
|
||||
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
|
||||
|
||||
"d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="],
|
||||
|
||||
"d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="],
|
||||
|
||||
"d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="],
|
||||
|
||||
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
|
||||
|
||||
"d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="],
|
||||
|
||||
"d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="],
|
||||
|
||||
"d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="],
|
||||
|
||||
"d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="],
|
||||
|
||||
"d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="],
|
||||
|
||||
"d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
|
||||
|
||||
"d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="],
|
||||
|
||||
"d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="],
|
||||
|
||||
"d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
|
||||
|
||||
"d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
|
||||
|
||||
"d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="],
|
||||
|
||||
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
|
||||
|
||||
"d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
|
||||
|
||||
"d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="],
|
||||
|
||||
"d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="],
|
||||
|
||||
"d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="],
|
||||
|
||||
"d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="],
|
||||
|
||||
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
|
||||
|
||||
"d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="],
|
||||
|
||||
"d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="],
|
||||
|
||||
"d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
|
||||
|
||||
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
|
||||
|
||||
"d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
|
||||
|
||||
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
|
||||
|
||||
"d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="],
|
||||
|
||||
"d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="],
|
||||
|
||||
"dagre-d3-es": ["dagre-d3-es@7.0.13", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q=="],
|
||||
|
||||
"dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="],
|
||||
|
||||
"delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||
|
||||
"dompurify": ["dompurify@3.3.2", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ=="],
|
||||
|
||||
"emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="],
|
||||
|
||||
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
|
||||
|
||||
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"focus-trap": ["focus-trap@7.8.0", "", { "dependencies": { "tabbable": "^6.4.0" } }, "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
|
||||
|
||||
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
|
||||
|
||||
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
||||
|
||||
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||
|
||||
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="],
|
||||
|
||||
"is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="],
|
||||
|
||||
"katex": ["katex@0.16.35", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-S0+riEvy1CK4VKse1ivMff8gmabe/prY7sKB3njjhyoLLsNFDQYtKNgXrbWUggGDCJBz7Fctl5i8fLCESHXzSg=="],
|
||||
|
||||
"khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="],
|
||||
|
||||
"langium": ["langium@4.2.1", "", { "dependencies": { "chevrotain": "~11.1.1", "chevrotain-allstar": "~0.3.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.1.0" } }, "sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ=="],
|
||||
|
||||
"layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="],
|
||||
|
||||
"lodash-es": ["lodash-es@4.17.23", "", {}, "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"mark.js": ["mark.js@8.11.1", "", {}, "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ=="],
|
||||
|
||||
"marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
|
||||
|
||||
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
|
||||
|
||||
"mermaid": ["mermaid@11.12.3", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.1", "@mermaid-js/parser": "^1.0.0", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.13", "dayjs": "^1.11.18", "dompurify": "^3.2.5", "katex": "^0.16.22", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.2.1", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ=="],
|
||||
|
||||
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
||||
|
||||
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
|
||||
|
||||
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
|
||||
|
||||
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
|
||||
|
||||
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||
|
||||
"minisearch": ["minisearch@7.2.0", "", {}, "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg=="],
|
||||
|
||||
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
||||
|
||||
"mlly": ["mlly@1.8.1", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"oniguruma-to-es": ["oniguruma-to-es@3.1.1", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ=="],
|
||||
|
||||
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
|
||||
|
||||
"path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||
|
||||
"points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="],
|
||||
|
||||
"points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="],
|
||||
|
||||
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||
|
||||
"preact": ["preact@10.28.4", "", {}, "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ=="],
|
||||
|
||||
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||
|
||||
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
|
||||
|
||||
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
|
||||
|
||||
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
|
||||
|
||||
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
||||
|
||||
"robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="],
|
||||
|
||||
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
|
||||
|
||||
"roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
|
||||
|
||||
"rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"search-insights": ["search-insights@2.17.3", "", {}, "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ=="],
|
||||
|
||||
"shiki": ["shiki@2.5.0", "", { "dependencies": { "@shikijs/core": "2.5.0", "@shikijs/engine-javascript": "2.5.0", "@shikijs/engine-oniguruma": "2.5.0", "@shikijs/langs": "2.5.0", "@shikijs/themes": "2.5.0", "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
|
||||
"speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="],
|
||||
|
||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||
|
||||
"stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="],
|
||||
|
||||
"superjson": ["superjson@2.2.6", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="],
|
||||
|
||||
"tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
|
||||
|
||||
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||
|
||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||
|
||||
"ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
||||
|
||||
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
||||
|
||||
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
||||
|
||||
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||
|
||||
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
||||
|
||||
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
|
||||
|
||||
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
|
||||
|
||||
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||
|
||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||
|
||||
"vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
|
||||
|
||||
"vitepress": ["vitepress@1.6.4", "", { "dependencies": { "@docsearch/css": "3.8.2", "@docsearch/js": "3.8.2", "@iconify-json/simple-icons": "^1.2.21", "@shikijs/core": "^2.1.0", "@shikijs/transformers": "^2.1.0", "@shikijs/types": "^2.1.0", "@types/markdown-it": "^14.1.2", "@vitejs/plugin-vue": "^5.2.1", "@vue/devtools-api": "^7.7.0", "@vue/shared": "^3.5.13", "@vueuse/core": "^12.4.0", "@vueuse/integrations": "^12.4.0", "focus-trap": "^7.6.4", "mark.js": "8.11.1", "minisearch": "^7.1.1", "shiki": "^2.1.0", "vite": "^5.4.14", "vue": "^3.5.13" }, "peerDependencies": { "markdown-it-mathjax3": "^4", "postcss": "^8" }, "optionalPeers": ["markdown-it-mathjax3", "postcss"], "bin": { "vitepress": "bin/vitepress.js" } }, "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg=="],
|
||||
|
||||
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
|
||||
|
||||
"vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="],
|
||||
|
||||
"vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="],
|
||||
|
||||
"vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="],
|
||||
|
||||
"vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
|
||||
|
||||
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
|
||||
|
||||
"vue": ["vue@3.5.29", "", { "dependencies": { "@vue/compiler-dom": "3.5.29", "@vue/compiler-sfc": "3.5.29", "@vue/runtime-dom": "3.5.29", "@vue/server-renderer": "3.5.29", "@vue/shared": "3.5.29" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="],
|
||||
|
||||
"d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
||||
|
||||
"d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="],
|
||||
|
||||
"d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
|
||||
|
||||
"cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="],
|
||||
|
||||
"d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
|
||||
}
|
||||
}
|
||||
77
docs-site/changelog.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Changelog
|
||||
|
||||
## v0.6.0 (2026-03-12)
|
||||
- Added Chrome Gamepad API controller support for keyboard-only overlay mode.
|
||||
- Added configurable controller bindings for lookup, mining, popup navigation, Yomitan audio, mpv pause, and d-pad fallback navigation.
|
||||
- Added smooth, slower popup scrolling for controller navigation.
|
||||
- Added `Alt+C` controller selection and `Alt+Shift+C` controller debug modals, with preferred controller persistence and live raw input inspection.
|
||||
- Added a transient in-overlay controller-detected indicator when a controller is first found.
|
||||
- Fixed cleanup of stale keyboard-only token highlights when keyboard-only mode is disabled or when the Yomitan popup closes.
|
||||
- Added an enforced `verify:config-example` gate so checked-in example config artifacts cannot drift silently.
|
||||
|
||||
## v0.5.6 (2026-03-10)
|
||||
- Persisted merged character-dictionary MRU state as soon as a new retained set is built so revisits do not get dropped if later Yomitan import work fails.
|
||||
- Fixed early Electron startup writing config and user data under a lowercase `~/.config/subminer` path instead of canonical `~/.config/SubMiner`.
|
||||
- Kept JLPT underline colors stable during Yomitan hover and selection states, even when tokens also use known, N+1, name-match, or frequency styling.
|
||||
|
||||
## v0.5.1 (2026-03-09)
|
||||
- Removed the old YouTube subtitle-generation mode switch; YouTube playback now resolves subtitles before mpv starts.
|
||||
- Hardened YouTube AI subtitle fixing so fenced/text-only responses keep original cue timing.
|
||||
- Skipped AniSkip during URL/YouTube playback where anime metadata cannot be resolved reliably.
|
||||
- Kept the background SubMiner process warm across launcher-managed mpv exits so reconnects do not repeat startup pause/warmup work.
|
||||
- Fixed Windows single-instance reuse so overlay and video launches reuse the running background app instead of booting a second full app.
|
||||
- Hardened the Windows signing/release workflow with SignPath retry handling for signed `.exe` and `.zip` artifacts.
|
||||
|
||||
## v0.5.0 (2026-03-08)
|
||||
- Added the initial packaged Windows release.
|
||||
- Added Windows-native mpv window tracking, launcher/runtime plumbing, and packaged helper assets.
|
||||
- Improved close behavior so ending playback hides the visible overlay while the background app stays running.
|
||||
- Limited the native overlay outline/debug frame to debug mode on Windows.
|
||||
|
||||
## v0.3.0 (2026-03-05)
|
||||
- Added keyboard-driven Yomitan navigation and popup controls, including optional auto-pause.
|
||||
- Added subtitle/jump keyboard handling fixes for smoother subtitle playback control.
|
||||
- Improved Anki/Yomitan reliability with stronger Yomitan proxy syncing and safer extension refresh logic.
|
||||
- Added Subsync `replace` option and deterministic retime naming for subtitle workflows.
|
||||
- Moved aniskip resolution to launcher-script options for better control.
|
||||
- Tuned tokenizer frequency highlighting filters for improved term visibility.
|
||||
- Added release build quality-of-life for CLI publish (`gh`-based clobber upload).
|
||||
- Removed docs Plausible integration and cleaned associated tracker settings.
|
||||
|
||||
## v0.2.3 (2026-03-02)
|
||||
- Added performance and tokenization optimizations (faster warmup, persistent MeCab usage, reduced enrichment lookups).
|
||||
- Added subtitle controls for no-jump delay shifts.
|
||||
- Improved subtitle highlight logic with priority and reliability fixes.
|
||||
- Fixed plugin loading behavior to keep OSD visible during startup.
|
||||
- Fixed Jellyfin remote resume behavior and improved autoplay/tokenization interaction.
|
||||
- Updated startup flow to load dictionaries asynchronously and unblock first tokenization sooner.
|
||||
|
||||
## v0.2.2 (2026-03-01)
|
||||
- Improved subtitle highlighting reliability for frequency modes.
|
||||
- Fixed Jellyfin misc info formatting cleanup.
|
||||
- Version bump maintenance for 0.2.2.
|
||||
|
||||
## v0.2.1 (2026-03-01)
|
||||
- Delivered Jellyfin and Subsync fixes from release patch cycle.
|
||||
- Version bump maintenance for 0.2.1.
|
||||
|
||||
## v0.2.0 (2026-03-01)
|
||||
- Added task-related release work for the overlay 2.0 cycle.
|
||||
- Introduced Overlay 2.0.
|
||||
- Improved release automation reliability.
|
||||
|
||||
## v0.1.2 (2026-02-24)
|
||||
- Added encrypted AniList token handling and default GNOME keyring support.
|
||||
- Added launcher passthrough for password-store flows (Jellyfin path).
|
||||
- Updated docs for auth and integration behavior.
|
||||
- Version bump maintenance for 0.1.2.
|
||||
|
||||
## v0.1.1 (2026-02-23)
|
||||
- Fixed overlay modal focus handling (`grab input`) behavior.
|
||||
- Version bump maintenance for 0.1.1.
|
||||
|
||||
## v0.1.0 (2026-02-23)
|
||||
- Bootstrapped Electron runtime, services, and composition model.
|
||||
- Added runtime asset packaging and dependency vendoring.
|
||||
- Added project docs baseline, setup guides, architecture notes, and submodule/runtime assets.
|
||||
- Added CI release job dependency ordering fixes before launcher build.
|
||||
257
docs-site/character-dictionary.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# Character Dictionary
|
||||
|
||||
SubMiner can build a Yomitan-compatible character dictionary from AniList 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.
|
||||
|
||||
The dictionary is generated per-media, merged across your recently-watched titles, and auto-imported into Yomitan. When a character name appears in a subtitle line, it gets highlighted and becomes clickable for a full profile lookup.
|
||||
|
||||
## How It Works
|
||||
|
||||
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.
|
||||
|
||||
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. Tokens that match a character entry are flagged with `isNameMatch` and highlighted in the overlay with a distinct color.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
classDef api fill:#a6da95,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef store fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef build fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef dict fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef render fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
|
||||
AL["AniList API"]:::api
|
||||
Snap["Snapshot JSON"]:::store
|
||||
Merge["Merge"]:::build
|
||||
ZIP["Yomitan ZIP"]:::dict
|
||||
Yomi["Yomitan Import"]:::dict
|
||||
Sub["Subtitle Scan"]:::render
|
||||
HL["Name Highlight"]:::render
|
||||
|
||||
AL -->|"GraphQL"| Snap
|
||||
Snap --> Merge
|
||||
Merge --> ZIP
|
||||
ZIP --> Yomi
|
||||
Yomi --> Sub
|
||||
Sub --> HL
|
||||
```
|
||||
|
||||
## Enabling the Feature
|
||||
|
||||
Character dictionary sync is disabled by default. To turn it on:
|
||||
|
||||
1. Authenticate with AniList (see [AniList configuration](/configuration#anilist)).
|
||||
2. Set `anilist.characterDictionary.enabled` to `true` in your config.
|
||||
3. Start watching — SubMiner will generate a snapshot for the current media and import the merged dictionary into Yomitan automatically.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"anilist": {
|
||||
"enabled": true,
|
||||
"accessToken": "your-token",
|
||||
"characterDictionary": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: tip
|
||||
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 snapshot.
|
||||
:::
|
||||
|
||||
## Name Generation
|
||||
|
||||
A single character produces many searchable terms so that names are recognized regardless of how they appear in dialogue. SubMiner generates variants for:
|
||||
|
||||
**Spacing and combination:**
|
||||
- Full name with space: 須々木 心一
|
||||
- Combined form: 須々木心一
|
||||
- Family name alone: 須々木
|
||||
- Given name alone: 心一
|
||||
|
||||
**Middle-dot removal** (common in katakana foreign names):
|
||||
- ア・リ・ス → アリス (combined), plus individual segments
|
||||
|
||||
**Honorific suffixes** — each base name is expanded with 15 common suffixes:
|
||||
|
||||
| Honorific | Reading |
|
||||
| --- | --- |
|
||||
| さん | さん |
|
||||
| 様 | さま |
|
||||
| 先生 | せんせい |
|
||||
| 先輩 | せんぱい |
|
||||
| 後輩 | こうはい |
|
||||
| 氏 | し |
|
||||
| 君 | くん |
|
||||
| くん | くん |
|
||||
| ちゃん | ちゃん |
|
||||
| たん | たん |
|
||||
| 坊 | ぼう |
|
||||
| 殿 | どの |
|
||||
| 博士 | はかせ |
|
||||
| 社長 | しゃちょう |
|
||||
| 部長 | ぶちょう |
|
||||
|
||||
**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.
|
||||
|
||||
## 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'`.
|
||||
3. Matched tokens are flagged `isNameMatch: true` and forwarded to the renderer.
|
||||
4. The renderer applies the name-match highlight color (default: `#f5bde6`).
|
||||
|
||||
Name matches are visually distinct from [N+1 targeting, frequency highlighting, and JLPT tags](/subtitle-annotations) so you can tell at a glance whether a highlighted word is a character name or a vocabulary target.
|
||||
|
||||
**Key settings:**
|
||||
|
||||
| Option | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `subtitleStyle.nameMatchEnabled` | `true` | Toggle character-name highlighting |
|
||||
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Highlight color for matched names |
|
||||
|
||||
## Dictionary Entries
|
||||
|
||||
Each character entry in the Yomitan dictionary includes structured content:
|
||||
|
||||
- **Name** — native (Japanese) and romanized forms
|
||||
- **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:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"anilist": {
|
||||
"characterDictionary": {
|
||||
"collapsibleSections": {
|
||||
"description": false,
|
||||
"characterInformation": false,
|
||||
"voicedBy": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Auto-Sync Lifecycle
|
||||
|
||||
When `characterDictionary.enabled` is `true`, SubMiner runs an auto-sync routine whenever the active media changes.
|
||||
|
||||
**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.
|
||||
|
||||
**State tracking** is persisted in `character-dictionaries/auto-sync-state.json`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"activeMediaIds": [170942, 163134, 154587],
|
||||
"mergedRevision": "a1b2c3d4e5f6",
|
||||
"mergedDictionaryTitle": "SubMiner Character Dictionary"
|
||||
}
|
||||
```
|
||||
|
||||
The `maxLoaded` setting (default: 3) controls how many media snapshots stay in the active set. When you start a 4th title, the oldest is evicted and the merged dictionary is rebuilt without it.
|
||||
|
||||
## Manual Generation
|
||||
|
||||
You can generate a character dictionary from the command line without auto-sync:
|
||||
|
||||
```bash
|
||||
# Generate for a file or directory
|
||||
subminer dictionary /path/to/media
|
||||
|
||||
# Generate for current anime (AppImage)
|
||||
SubMiner.AppImage --dictionary
|
||||
```
|
||||
|
||||
This creates a standalone dictionary ZIP for the target media and saves it alongside the snapshots.
|
||||
|
||||
## File Structure
|
||||
|
||||
All character dictionary data lives under `{userData}/character-dictionaries/`:
|
||||
|
||||
```text
|
||||
character-dictionaries/
|
||||
snapshots/
|
||||
anilist-170942.json # Per-media character snapshot
|
||||
anilist-163134.json
|
||||
merged.zip # Active merged dictionary (imported into Yomitan)
|
||||
auto-sync-state.json # Tracks active media IDs and revision
|
||||
img/
|
||||
m170942-c12345.jpg # Character portrait
|
||||
m170942-va67890.jpg # Voice actor portrait
|
||||
```
|
||||
|
||||
**Snapshot format** (v15): each snapshot contains the media ID, title, entry count, timestamp, an array of Yomitan term entries, and base64-encoded images.
|
||||
|
||||
**ZIP structure** follows the Yomitan dictionary format:
|
||||
|
||||
```text
|
||||
merged.zip
|
||||
index.json # { title, revision, format: 3, author: "SubMiner" }
|
||||
tag_bank_1.json # Tag definitions
|
||||
term_bank_1.json # Up to 10,000 terms per bank
|
||||
term_bank_2.json
|
||||
img/ # Embedded character and VA portraits
|
||||
```
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
| Option | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `anilist.characterDictionary.enabled` | `false` | Enable auto-sync of character dictionary from AniList |
|
||||
| `anilist.characterDictionary.maxLoaded` | `3` | Number of recent media snapshots kept in the merged dictionary |
|
||||
| `anilist.characterDictionary.profileScope` | `"all"` | Apply dictionary to `"all"` Yomitan profiles or `"active"` only |
|
||||
| `anilist.characterDictionary.collapsibleSections.description` | `false` | Start Description section expanded |
|
||||
| `anilist.characterDictionary.collapsibleSections.characterInformation` | `false` | Start Character Information section expanded |
|
||||
| `anilist.characterDictionary.collapsibleSections.voicedBy` | `false` | Start Voiced By section expanded |
|
||||
| `subtitleStyle.nameMatchEnabled` | `true` | Toggle character-name highlighting in subtitles |
|
||||
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Highlight color for character-name matches |
|
||||
|
||||
## 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.
|
||||
|
||||
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 |
|
||||
| --- | --- | --- |
|
||||
| **Runtime** | TypeScript, runs inside Electron | Rust, standalone web service |
|
||||
| **Data sources** | AniList only | AniList + VNDB |
|
||||
| **Delivery** | Auto-synced into bundled Yomitan | ZIP download via web UI |
|
||||
| **Honorific strategy** | Eager generation at build time | Lazy generation during ZIP export |
|
||||
| **Caching** | File-based snapshots | Multi-tier (memory + disk + SQLite) |
|
||||
| **Updates** | Revision-hashed; skips reimport if unchanged | URL-encoded settings for auto-refresh |
|
||||
|
||||
If you work with visual novels or want a standalone dictionary generator independent of SubMiner, the reference implementation is worth checking out.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Names not highlighting:** Confirm `anilist.characterDictionary.enabled` is `true` and `subtitleStyle.nameMatchEnabled` is `true`. Check that the current media has an AniList entry — SubMiner needs a media ID to fetch characters.
|
||||
- **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:** The merged dictionary includes your `maxLoaded` most recent titles. If you're seeing names from a previous show, they'll rotate out once you watch enough new titles to push it past the limit.
|
||||
- **Yomitan import fails:** SubMiner waits up to 7 seconds for Yomitan to be ready for mutations. If Yomitan is still loading dictionaries or performing another import, the operation may time out. Restarting the overlay typically resolves this.
|
||||
- **Portraits missing:** Images are downloaded from AniList CDN during snapshot generation. If the network was unavailable during the initial sync, delete the snapshot file from `character-dictionaries/snapshots/` and let it regenerate.
|
||||
|
||||
## Related
|
||||
|
||||
- [Subtitle Annotations](/subtitle-annotations) — how name matches interact with N+1, frequency, and JLPT layers
|
||||
- [AniList Configuration](/configuration#anilist) — authentication and AniList settings
|
||||
- [Configuration Reference](/configuration) — full config options
|
||||
1213
docs-site/configuration.md
Normal file
72
docs-site/demos.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Feature Demos
|
||||
|
||||
Short recordings of SubMiner's key features and integrations from real playback sessions.
|
||||
|
||||
<script setup>
|
||||
const v = '20260301-1';
|
||||
</script>
|
||||
|
||||
## Anki Card Mining & Enrichment
|
||||
|
||||
Mine vocabulary cards from Yomitan or directly from subtitle lines. SubMiner automatically attaches the sentence, a timing-accurate audio clip, a screenshot, and a translation.
|
||||
|
||||
<video controls playsinline preload="metadata" :poster="`/assets/minecard-poster.jpg?v=${demoAssetVersion}`">
|
||||
<source :src="`/assets/minecard.webm?v=${demoAssetVersion}`" type="video/webm" />
|
||||
<source :src="`/assets/minecard.mp4?v=${demoAssetVersion}`" type="video/mp4" />
|
||||
<a :href="`/assets/minecard.webm?v=${demoAssetVersion}`" target="_blank" rel="noreferrer">
|
||||
<img :src="`/assets/minecard.webp?v=${demoAssetVersion}`" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />
|
||||
</a>
|
||||
</video>
|
||||
|
||||
::: info VIDEO COMING SOON
|
||||
:::
|
||||
|
||||
## Subtitle Download & Sync
|
||||
|
||||
Search and download subtitles from Jimaku, then automatically synchronize them with alass or ffsubsync — all from within SubMiner.
|
||||
|
||||
<!-- <video controls playsinline preload="metadata" :poster="`/assets/demos/subtitle-sync-poster.jpg?v=${v}`">
|
||||
<source :src="`/assets/demos/subtitle-sync.webm?v=${v}`" type="video/webm" />
|
||||
<source :src="`/assets/demos/subtitle-sync.mp4?v=${v}`" type="video/mp4" />
|
||||
</video> -->
|
||||
|
||||
::: info VIDEO COMING SOON
|
||||
:::
|
||||
|
||||
## Jellyfin Integration
|
||||
|
||||
Browse your Jellyfin library, cast to devices, and launch playback directly from SubMiner. Watch progress syncs back to your Jellyfin server.
|
||||
|
||||
<!-- <video controls playsinline preload="metadata" :poster="`/assets/demos/jellyfin-poster.jpg?v=${v}`">
|
||||
<source :src="`/assets/demos/jellyfin.webm?v=${v}`" type="video/webm" />
|
||||
<source :src="`/assets/demos/jellyfin.mp4?v=${v}`" type="video/mp4" />
|
||||
</video> -->
|
||||
|
||||
::: info VIDEO COMING SOON
|
||||
:::
|
||||
|
||||
## Texthooker
|
||||
|
||||
Open subtitles in an external texthooker page for use with browser-based tools and extensions alongside the overlay.
|
||||
|
||||
<!-- <video controls playsinline preload="metadata" :poster="`/assets/demos/texthooker-poster.jpg?v=${v}`">
|
||||
<source :src="`/assets/demos/texthooker.webm?v=${v}`" type="video/webm" />
|
||||
<source :src="`/assets/demos/texthooker.mp4?v=${v}`" type="video/mp4" />
|
||||
</video> -->
|
||||
|
||||
::: info VIDEO COMING SOON
|
||||
:::
|
||||
|
||||
<style>
|
||||
video {
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.28);
|
||||
margin: 0.75rem 0 2.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 2.5rem !important;
|
||||
}
|
||||
</style>
|
||||
243
docs-site/development.md
Normal file
@@ -0,0 +1,243 @@
|
||||
# Building & Testing
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Bun](https://bun.sh)
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/ksyasuda/SubMiner.git
|
||||
cd SubMiner
|
||||
# if you cloned without --recurse-submodules:
|
||||
git submodule update --init --recursive
|
||||
|
||||
bun install
|
||||
(cd vendor/texthooker-ui && bun install --frozen-lockfile)
|
||||
```
|
||||
|
||||
`make deps` is still available as a convenience wrapper around the same dependency install flow.
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
# Main app build
|
||||
bun run build
|
||||
|
||||
# Platform packages
|
||||
bun run build:appimage # Linux AppImage
|
||||
bun run build:mac # macOS DMG + ZIP (signed)
|
||||
bun run build:mac:unsigned # macOS DMG + ZIP (unsigned)
|
||||
bun run build:win # Windows NSIS installer + ZIP
|
||||
|
||||
# Optional launcher artifact only
|
||||
make build-launcher
|
||||
# output: dist/launcher/subminer
|
||||
```
|
||||
|
||||
`bun run build` includes the Yomitan build step. It builds the bundled Chrome extension directly from the `vendor/subminer-yomitan` submodule into `build/yomitan` using Bun.
|
||||
|
||||
## Launcher Artifact Workflow
|
||||
|
||||
- Source of truth: `launcher/*.ts`
|
||||
- Generated output: `dist/launcher/subminer`
|
||||
- Do not hand-edit generated launcher output.
|
||||
- Repo-root `./subminer` is a stale artifact path and is rejected by verification checks.
|
||||
- Install targets (`make install-linux`, `make install-macos`) copy from `dist/launcher/subminer`.
|
||||
|
||||
Verify the workflow:
|
||||
|
||||
```bash
|
||||
make build-launcher
|
||||
dist/launcher/subminer --help >/dev/null
|
||||
bash scripts/verify-generated-launcher.sh
|
||||
```
|
||||
|
||||
## Running Locally
|
||||
|
||||
```bash
|
||||
bun run dev # builds + launches with --start --dev
|
||||
electron . --start --dev --log-level debug # equivalent Electron launch with verbose logging
|
||||
electron . --background # tray/background mode, minimal default logging
|
||||
make dev-start # build + launch via Makefile
|
||||
make dev-watch # watch TS + renderer and launch Electron (faster edit loop)
|
||||
make dev-watch-macos # same as dev-watch, forcing --backend macos
|
||||
```
|
||||
|
||||
For mpv-plugin-driven testing without exporting `SUBMINER_BINARY_PATH` each run, set a one-time
|
||||
dev binary path in `~/.config/mpv/script-opts/subminer.conf`:
|
||||
|
||||
```ini
|
||||
binary_path=/absolute/path/to/SubMiner/scripts/subminer-dev.sh
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Default lanes:
|
||||
|
||||
```bash
|
||||
bun run test # alias for test:fast
|
||||
bun run test:fast # default fast lane
|
||||
bun run test:full # maintained source + launcher-unit + runtime compat surface
|
||||
bun run test:runtime:compat # compiled/runtime compatibility slice only
|
||||
bun run test:env # launcher/plugin + env-sensitive verification
|
||||
bun run test:immersion:sqlite # SQLite persistence lane
|
||||
bun run test:subtitle # maintained alass/ffsubsync subtitle surface
|
||||
```
|
||||
|
||||
- `bun run test` and `bun run test:fast` cover config/core suites plus representative entry/runtime, Anki integration, release-workflow coverage, typecheck, and runtime-registry checks.
|
||||
- `bun run test:full` is the maintained full surface: Bun-compatible `src/**` discovery, Bun-compatible launcher unit discovery, and the compiled/runtime compatibility lane for suites routed through `dist/**`.
|
||||
- `bun run test:runtime:compat` covers the compiled/runtime slice directly: `ipc`, `anki-jimaku-ipc`, `overlay-manager`, `config-validation`, `startup-config`, and `registry`.
|
||||
- `bun run test:env` covers environment-sensitive checks: launcher smoke/plugin verification plus the Bun source SQLite lane.
|
||||
- `bun run test:immersion:sqlite` is the reproducible persistence lane when you need real DB-backed SQLite coverage under Bun.
|
||||
|
||||
The Bun-managed discovery lanes intentionally exclude a small compiled/runtime-focused set: `src/core/services/ipc.test.ts`, `src/core/services/anki-jimaku-ipc.test.ts`, `src/core/services/overlay-manager.test.ts`, `src/main/config-validation.test.ts`, `src/main/runtime/startup-config.test.ts`, and `src/main/runtime/registry.test.ts`. `bun run test:runtime:compat` keeps them in the standard workflow via `dist/**`.
|
||||
|
||||
Suggested local gate before handoff:
|
||||
|
||||
```bash
|
||||
bun run typecheck
|
||||
bun run test:fast
|
||||
bun run test:env
|
||||
bun run build
|
||||
bun run test:smoke:dist
|
||||
```
|
||||
|
||||
If you changed docs in `docs-site/`, also run:
|
||||
|
||||
```bash
|
||||
bun run docs:test
|
||||
bun run docs:build
|
||||
```
|
||||
|
||||
Focused commands:
|
||||
|
||||
```bash
|
||||
bun run test:config # Source-level config schema/validation tests
|
||||
bun run test:launcher # Launcher regression tests (config discovery + command routing)
|
||||
bun run test:core # Source-level core regression tests (default lane)
|
||||
bun run test:launcher:smoke:src # Launcher e2e smoke: launcher -> mpv IPC -> overlay start/stop wiring
|
||||
bun run test:launcher:env:src # Launcher smoke + Lua plugin gate
|
||||
bun run test:src # Bun-managed maintained src/** discovery lane
|
||||
bun run test:launcher:unit:src # Bun-managed maintained launcher unit lane
|
||||
bun run test:immersion:sqlite:src # Bun source lane
|
||||
```
|
||||
|
||||
Dist-level tests are now an explicit smoke lane used to validate compiled/runtime assumptions.
|
||||
|
||||
Launcher smoke artifacts are written to `.tmp/launcher-smoke` locally and uploaded by CI/release workflows when the smoke step fails.
|
||||
|
||||
Smoke and optional deep dist commands:
|
||||
|
||||
```bash
|
||||
bun run build # compile dist artifacts
|
||||
bun run test:immersion:sqlite # compile + run SQLite-backed immersion tests under Bun
|
||||
bun run test:smoke:dist # explicit smoke scope for compiled runtime
|
||||
bun run test:config:dist # optional full dist config suite
|
||||
bun run test:core:dist # optional full dist core suite
|
||||
```
|
||||
|
||||
Use `bun run test:immersion:sqlite` when you need real DB-backed coverage for the immersion tracker.
|
||||
|
||||
## Formatting
|
||||
|
||||
Use the scoped formatter for normal app-repo work:
|
||||
|
||||
```bash
|
||||
make pretty
|
||||
bun run format:check:src
|
||||
```
|
||||
|
||||
- `make pretty` runs the maintained Prettier allowlist only (`format:src`).
|
||||
- `bun run format:check:src` checks the same scoped set without writing changes.
|
||||
- `bun run format` remains the broad repo-wide Prettier command; use it intentionally.
|
||||
## Config Generation
|
||||
|
||||
```bash
|
||||
# Generate default config to ~/.config/SubMiner/config.jsonc (or %APPDATA%\SubMiner\config.jsonc on Windows)
|
||||
bun run electron . --generate-config
|
||||
|
||||
# Regenerate the repo's config.example.jsonc from centralized defaults
|
||||
bun run generate:config-example
|
||||
```
|
||||
|
||||
Convenience wrappers still exist:
|
||||
|
||||
- `make generate-config`
|
||||
- `make generate-example-config`
|
||||
|
||||
## Documentation Site
|
||||
|
||||
The docs site now lives in `docs-site/` inside the main repo.
|
||||
|
||||
From the SubMiner app repo:
|
||||
|
||||
```bash
|
||||
bun --cwd docs-site install
|
||||
bun run docs:dev # Dev server at http://localhost:5173
|
||||
bun run docs:build # Production build into docs-site/.vitepress/dist
|
||||
bun run docs:preview # Preview built site at http://localhost:4173
|
||||
bun run docs:test # Docs regression tests
|
||||
```
|
||||
|
||||
Cloudflare Pages deploy settings:
|
||||
|
||||
- Git repo: `ksyasuda/SubMiner`
|
||||
- Root directory: `docs-site`
|
||||
- Build command: `bun run docs:build`
|
||||
- Build output directory: `.vitepress/dist`
|
||||
- Build watch paths: `docs-site/*`
|
||||
|
||||
Use Cloudflare's single `*` wildcard syntax for watch paths. `docs-site/*` covers nested docs-site changes in the repo; `docs-site/**` is not the correct Pages pattern and may skip docs-only pushes.
|
||||
|
||||
## Makefile Reference
|
||||
|
||||
Run `make help` for a full list of targets. Key ones:
|
||||
|
||||
| Target | Description |
|
||||
| ---------------------- | ---------------------------------------------------------------- |
|
||||
| `make build` | Build platform package for detected OS |
|
||||
| `make build-launcher` | Generate Bun launcher wrapper at `dist/launcher/subminer` |
|
||||
| `make install` | Install platform artifacts (wrapper, theme, AppImage/app bundle) |
|
||||
| `make install-plugin` | Install mpv Lua plugin and config |
|
||||
| `make deps` | Install JS dependencies (root + texthooker-ui) |
|
||||
| `make pretty` | Run scoped Prettier formatting for maintained source/config files |
|
||||
| `make generate-config` | Generate default config from centralized registry |
|
||||
| `make build-linux` | Convenience wrapper for Linux packaging |
|
||||
| `make build-macos` | Convenience wrapper for signed macOS packaging |
|
||||
| `make build-macos-unsigned` | Convenience wrapper for unsigned macOS packaging |
|
||||
|
||||
## Contributor Notes
|
||||
|
||||
- To add/change a config default, edit the matching domain file in `src/config/definitions/defaults-*.ts`.
|
||||
- To add/change config option metadata, edit the matching domain file in `src/config/definitions/options-*.ts`.
|
||||
- To add/change generated config template blocks/comments, update `src/config/definitions/template-sections.ts`.
|
||||
- Keep `src/config/definitions.ts` as the composed public API (`DEFAULT_CONFIG`, registries, template export) that wires domain modules together.
|
||||
- Overlay window/visibility state is owned by `src/core/services/overlay-manager.ts`.
|
||||
- Runtime architecture/module-boundary conventions are documented in [Architecture](/architecture); keep contributor changes aligned with that canonical guide.
|
||||
- Linux packaged desktop launches pass `--background` using electron-builder `build.linux.executableArgs` in `package.json`.
|
||||
- Prefer direct inline deps objects in `src/main/` modules for simple pass-through wiring.
|
||||
- Add a helper/adapter service only when it performs meaningful adaptation, validation, or reuse (not identity mapping).
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
| ---------------------------------- | ------------------------------------------------------------------------------ |
|
||||
| `SUBMINER_APPIMAGE_PATH` | Override SubMiner app binary path for launcher playback commands |
|
||||
| `SUBMINER_BINARY_PATH` | Alias for `SUBMINER_APPIMAGE_PATH` |
|
||||
| `SUBMINER_ROFI_THEME` | Override rofi theme path for launcher picker |
|
||||
| `SUBMINER_LOG_LEVEL` | Override app logger level (`debug`, `info`, `warn`, `error`) |
|
||||
| `SUBMINER_MPV_LOG` | Override mpv/app shared log file path |
|
||||
| `SUBMINER_WHISPER_BIN` | Override `youtubeSubgen.whisperBin` for launcher |
|
||||
| `SUBMINER_WHISPER_MODEL` | Override `youtubeSubgen.whisperModel` for launcher |
|
||||
| `SUBMINER_WHISPER_VAD_MODEL` | Override `youtubeSubgen.whisperVadModel` for launcher |
|
||||
| `SUBMINER_WHISPER_THREADS` | Override `youtubeSubgen.whisperThreads` for launcher |
|
||||
| `SUBMINER_YT_SUBGEN_OUT_DIR` | Override generated subtitle output directory |
|
||||
| `SUBMINER_YT_SUBGEN_AUDIO_FORMAT` | Override extraction format used for whisper fallback |
|
||||
| `SUBMINER_YT_SUBGEN_KEEP_TEMP` | Set to `1` to keep temporary subtitle-generation workspace |
|
||||
| `SUBMINER_JIMAKU_API_KEY` | Override Jimaku API key for launcher subtitle downloads |
|
||||
| `SUBMINER_JIMAKU_API_KEY_COMMAND` | Command used to resolve Jimaku API key at runtime |
|
||||
| `SUBMINER_JIMAKU_API_BASE_URL` | Override Jimaku API base URL |
|
||||
| `SUBMINER_JELLYFIN_ACCESS_TOKEN` | Override Jellyfin access token (used before stored encrypted session fallback) |
|
||||
| `SUBMINER_JELLYFIN_USER_ID` | Optional Jellyfin user ID override |
|
||||
| `SUBMINER_SKIP_MACOS_HELPER_BUILD` | Set to `1` to skip building the macOS helper binary during `bun run build` |
|
||||
39
docs-site/docs-sync.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const readmeContents = readFileSync(new URL('./README.md', import.meta.url), 'utf8');
|
||||
const usageContents = readFileSync(new URL('./usage.md', import.meta.url), 'utf8');
|
||||
const installationContents = readFileSync(new URL('./installation.md', import.meta.url), 'utf8');
|
||||
const mpvPluginContents = readFileSync(new URL('./mpv-plugin.md', import.meta.url), 'utf8');
|
||||
const developmentContents = readFileSync(new URL('./development.md', import.meta.url), 'utf8');
|
||||
const changelogContents = readFileSync(new URL('./changelog.md', import.meta.url), 'utf8');
|
||||
const ankiIntegrationContents = readFileSync(new URL('./anki-integration.md', import.meta.url), 'utf8');
|
||||
const configurationContents = readFileSync(new URL('./configuration.md', import.meta.url), 'utf8');
|
||||
|
||||
test('docs reflect current launcher and release surfaces', () => {
|
||||
expect(usageContents).not.toContain('--mode preprocess');
|
||||
expect(usageContents).not.toContain('"automatic" (default)');
|
||||
expect(usageContents).toContain('before mpv starts');
|
||||
|
||||
expect(installationContents).toContain('bun run build:appimage');
|
||||
expect(installationContents).toContain('bun run build:win');
|
||||
|
||||
expect(mpvPluginContents).toContain('\\\\.\\pipe\\subminer-socket');
|
||||
|
||||
expect(readmeContents).toContain('Root directory: `docs-site`');
|
||||
expect(readmeContents).toContain('Build output directory: `.vitepress/dist`');
|
||||
expect(readmeContents).toContain('Build watch paths: `docs-site/*`');
|
||||
expect(developmentContents).not.toContain('../subminer-docs');
|
||||
expect(developmentContents).toContain('bun run docs:build');
|
||||
expect(developmentContents).toContain('bun run docs:test');
|
||||
expect(developmentContents).toContain('Build watch paths: `docs-site/*`');
|
||||
expect(developmentContents).not.toContain('test:subtitle:dist');
|
||||
expect(developmentContents).toContain('bun run build:win');
|
||||
|
||||
expect(ankiIntegrationContents).not.toContain('alwaysUseAiTranslation');
|
||||
expect(ankiIntegrationContents).not.toContain('targetLanguage');
|
||||
expect(configurationContents).not.toContain('youtubeSubgen": {\n "mode"');
|
||||
expect(configurationContents).toContain('### Shared AI Provider');
|
||||
|
||||
expect(changelogContents).toContain('## v0.5.1 (2026-03-09)');
|
||||
});
|
||||
160
docs-site/immersion-tracking.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Immersion Tracking
|
||||
|
||||
SubMiner can log your watching and mining activity to a local SQLite database. This is optional and disabled by default.
|
||||
|
||||
When enabled, SubMiner records per-session statistics (watch time, subtitle lines seen, words encountered, cards mined) and maintains daily and monthly rollups. You can query the database directly with any SQLite tool to track your progress over time.
|
||||
|
||||
## Enabling
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"immersionTracking": {
|
||||
"enabled": true,
|
||||
"dbPath": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Leave `dbPath` empty to use the default location (`immersion.sqlite` in SubMiner's app-data directory).
|
||||
- Set an explicit path to move the database (useful for backups, cloud syncing, or external tools).
|
||||
|
||||
## Retention Defaults
|
||||
|
||||
Data is kept for the following durations before automatic cleanup:
|
||||
|
||||
| Data type | Retention |
|
||||
| -------------- | --------- |
|
||||
| Raw events | 7 days |
|
||||
| Telemetry | 30 days |
|
||||
| Daily rollups | 1 year |
|
||||
| Monthly rollups | 5 years |
|
||||
|
||||
Maintenance runs on startup and every 24 hours. Vacuum runs weekly.
|
||||
|
||||
## Configurable Knobs
|
||||
|
||||
All policy options live under `immersionTracking` in your config:
|
||||
|
||||
| Option | Description |
|
||||
| ------ | ----------- |
|
||||
| `batchSize` | Writes per flush batch |
|
||||
| `flushIntervalMs` | Max delay between flushes (default: 500ms) |
|
||||
| `queueCap` | Max queued writes before oldest are dropped |
|
||||
| `payloadCapBytes` | Max payload size per write |
|
||||
| `maintenanceIntervalMs` | How often maintenance runs |
|
||||
| `retention.eventsDays` | Raw event retention |
|
||||
| `retention.telemetryDays` | Telemetry retention |
|
||||
| `retention.dailyRollupsDays` | Daily rollup retention |
|
||||
| `retention.monthlyRollupsDays` | Monthly rollup retention |
|
||||
| `retention.vacuumIntervalDays` | Minimum spacing between vacuums |
|
||||
|
||||
## Query Templates
|
||||
|
||||
### Session timeline
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
sample_ms,
|
||||
total_watched_ms,
|
||||
active_watched_ms,
|
||||
lines_seen,
|
||||
words_seen,
|
||||
tokens_seen,
|
||||
cards_mined
|
||||
FROM imm_session_telemetry
|
||||
WHERE session_id = ?
|
||||
ORDER BY sample_ms DESC, telemetry_id DESC
|
||||
LIMIT ?;
|
||||
```
|
||||
|
||||
### Session throughput summary
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
s.session_id,
|
||||
s.video_id,
|
||||
s.started_at_ms,
|
||||
s.ended_at_ms,
|
||||
COALESCE(SUM(t.active_watched_ms), 0) AS active_watched_ms,
|
||||
COALESCE(SUM(t.words_seen), 0) AS words_seen,
|
||||
COALESCE(SUM(t.cards_mined), 0) AS cards_mined,
|
||||
CASE
|
||||
WHEN COALESCE(SUM(t.active_watched_ms), 0) > 0
|
||||
THEN COALESCE(SUM(t.words_seen), 0) / (COALESCE(SUM(t.active_watched_ms), 0) / 60000.0)
|
||||
ELSE NULL
|
||||
END AS words_per_min,
|
||||
CASE
|
||||
WHEN COALESCE(SUM(t.active_watched_ms), 0) > 0
|
||||
THEN (COALESCE(SUM(t.cards_mined), 0) * 60.0) / (COALESCE(SUM(t.active_watched_ms), 0) / 60000.0)
|
||||
ELSE NULL
|
||||
END AS cards_per_hour
|
||||
FROM imm_sessions s
|
||||
LEFT JOIN imm_session_telemetry t ON t.session_id = s.session_id
|
||||
GROUP BY s.session_id
|
||||
ORDER BY s.started_at_ms DESC
|
||||
LIMIT ?;
|
||||
```
|
||||
|
||||
### Daily rollups
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
rollup_day,
|
||||
video_id,
|
||||
total_sessions,
|
||||
total_active_min,
|
||||
total_lines_seen,
|
||||
total_words_seen,
|
||||
total_tokens_seen,
|
||||
total_cards,
|
||||
cards_per_hour,
|
||||
words_per_min,
|
||||
lookup_hit_rate
|
||||
FROM imm_daily_rollups
|
||||
ORDER BY rollup_day DESC, video_id DESC
|
||||
LIMIT ?;
|
||||
```
|
||||
|
||||
### Monthly rollups
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
rollup_month,
|
||||
video_id,
|
||||
total_sessions,
|
||||
total_active_min,
|
||||
total_lines_seen,
|
||||
total_words_seen,
|
||||
total_tokens_seen,
|
||||
total_cards
|
||||
FROM imm_monthly_rollups
|
||||
ORDER BY rollup_month DESC, video_id DESC
|
||||
LIMIT ?;
|
||||
```
|
||||
|
||||
## Technical Details
|
||||
|
||||
- Write path is asynchronous and queue-backed. Hot paths (subtitle parsing, render, token flows) enqueue telemetry and never await SQLite writes.
|
||||
- Queue overflow policy: drop oldest queued writes, keep newest.
|
||||
- SQLite pragmas: `journal_mode=WAL`, `synchronous=NORMAL`, `foreign_keys=ON`, `busy_timeout=2500`.
|
||||
- Rollups run incrementally from the last processed telemetry sample; startup performs a one-time bootstrap pass.
|
||||
- If retention pruning removes telemetry/session rows, maintenance triggers a full rollup rebuild to resync historical aggregates.
|
||||
|
||||
### Schema (v3)
|
||||
|
||||
Core tables:
|
||||
|
||||
- `imm_videos` — video key/title/source metadata
|
||||
- `imm_sessions` — session UUID, video reference, timing/status
|
||||
- `imm_session_telemetry` — high-frequency session aggregates over time
|
||||
- `imm_session_events` — event stream with compact numeric event types
|
||||
|
||||
Rollup tables:
|
||||
|
||||
- `imm_daily_rollups`
|
||||
- `imm_monthly_rollups`
|
||||
|
||||
Vocabulary tables:
|
||||
|
||||
- `imm_words(id, headword, word, reading, first_seen, last_seen, frequency)`
|
||||
- `imm_kanji(id, kanji, first_seen, last_seen, frequency)`
|
||||
24
docs-site/index.assets.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const docsIndexPath = new URL('./index.md', import.meta.url);
|
||||
const docsIndexContents = readFileSync(docsIndexPath, 'utf8');
|
||||
|
||||
test('docs demo media uses shared cache-busting asset version token', () => {
|
||||
expect(docsIndexContents).toMatch(/const demoAssetVersion = ['"][^'"]+['"]/);
|
||||
expect(docsIndexContents).toContain(
|
||||
':poster="`/assets/minecard-poster.jpg?v=${demoAssetVersion}`"',
|
||||
);
|
||||
expect(docsIndexContents).toContain(
|
||||
'<source :src="`/assets/minecard.webm?v=${demoAssetVersion}`" type="video/webm" />',
|
||||
);
|
||||
expect(docsIndexContents).toContain(
|
||||
'<source :src="`/assets/minecard.mp4?v=${demoAssetVersion}`" type="video/mp4" />',
|
||||
);
|
||||
expect(docsIndexContents).toContain(
|
||||
'<a :href="`/assets/minecard.webm?v=${demoAssetVersion}`" target="_blank" rel="noreferrer">',
|
||||
);
|
||||
expect(docsIndexContents).toContain(
|
||||
'<img :src="`/assets/minecard.webp?v=${demoAssetVersion}`" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />',
|
||||
);
|
||||
});
|
||||
363
docs-site/index.md
Normal file
@@ -0,0 +1,363 @@
|
||||
---
|
||||
layout: home
|
||||
|
||||
title: SubMiner
|
||||
titleTemplate: Immersion Mining Workflow for MPV
|
||||
|
||||
hero:
|
||||
name: SubMiner
|
||||
text: Immersion Mining for MPV
|
||||
tagline: Watch media, mine vocabulary, and craft anki cards without leaving the scene.
|
||||
image:
|
||||
src: /assets/SubMiner.png
|
||||
alt: SubMiner logo
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Install
|
||||
link: /installation
|
||||
- theme: alt
|
||||
text: Explore workflow
|
||||
link: /mining-workflow
|
||||
|
||||
features:
|
||||
- icon:
|
||||
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.
|
||||
link: /usage
|
||||
linkText: How it works
|
||||
- icon:
|
||||
src: /assets/yomitan-icon.svg
|
||||
alt: Yomitan logo
|
||||
title: Bundled Yomitan
|
||||
details: Ships with a built-in Yomitan instance for instant word lookups and context-aware card creation directly from subtitle text.
|
||||
link: /mining-workflow
|
||||
linkText: Mining workflow
|
||||
- icon:
|
||||
src: /assets/anki-card.svg
|
||||
alt: Anki card icon
|
||||
title: Anki Card Enrichment
|
||||
details: Auto-fills card fields with sentence, audio clip, screenshot, and translation so you can focus on learning.
|
||||
link: /anki-integration
|
||||
linkText: Anki integration
|
||||
- icon:
|
||||
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.
|
||||
link: /subtitle-annotations
|
||||
linkText: Annotation details
|
||||
- icon:
|
||||
src: /assets/video.svg
|
||||
alt: Video playback icon
|
||||
title: YouTube & Whisper
|
||||
details: Play YouTube URLs or searches with native subtitles, or generate them with whisper.cpp and optional AI cleanup.
|
||||
link: /usage#youtube-playback
|
||||
linkText: YouTube playback
|
||||
- icon:
|
||||
src: /assets/jellyfin.svg
|
||||
alt: Jellyfin icon
|
||||
title: Jellyfin Integration
|
||||
details: Browse your Jellyfin library, pick media interactively, and play through mpv with full subtitle and mining support.
|
||||
link: /jellyfin-integration
|
||||
linkText: Jellyfin setup
|
||||
- icon:
|
||||
src: /assets/subtitle-download.svg
|
||||
alt: Subtitle download icon
|
||||
title: Subtitle Download & Sync
|
||||
details: Search and pull subtitles from Jimaku, then auto-sync timing with alass or ffsubsync — all from the overlay.
|
||||
link: /configuration#jimaku
|
||||
linkText: Jimaku integration
|
||||
- icon:
|
||||
src: /assets/tokenization.svg
|
||||
alt: Tracking chart icon
|
||||
title: Immersion Tracking
|
||||
details: Logs watch time, words encountered, and cards mined to SQLite with daily and monthly rollups for long-term progress tracking.
|
||||
link: /immersion-tracking
|
||||
linkText: Tracking details
|
||||
- icon:
|
||||
src: /assets/cross-platform.svg
|
||||
alt: Cross-platform icon
|
||||
title: Cross-Platform
|
||||
details: Runs on Linux (Hyprland, Sway, X11), macOS, and Windows with compositor-aware window positioning and platform-native integration.
|
||||
link: /installation
|
||||
linkText: Platform setup
|
||||
---
|
||||
|
||||
<script setup>
|
||||
const demoAssetVersion = '20260223-2';
|
||||
</script>
|
||||
|
||||
<div class="landing-shell">
|
||||
<section class="workflow-section">
|
||||
<h2>How it fits together</h2>
|
||||
<div class="workflow-steps">
|
||||
<div class="workflow-step" style="animation-delay: 0ms">
|
||||
<div class="step-number">01</div>
|
||||
<div class="step-title">Start</div>
|
||||
<div class="step-desc">Launch with the wrapper or existing mpv setup and keep subtitles in sync.</div>
|
||||
</div>
|
||||
<div class="workflow-connector" aria-hidden="true"></div>
|
||||
<div class="workflow-step" style="animation-delay: 60ms">
|
||||
<div class="step-number">02</div>
|
||||
<div class="step-title">Lookup</div>
|
||||
<div class="step-desc">Hover or click a token in the interactive overlay to open Yomitan context.</div>
|
||||
</div>
|
||||
<div class="workflow-connector" aria-hidden="true"></div>
|
||||
<div class="workflow-step" style="animation-delay: 120ms">
|
||||
<div class="step-number">03</div>
|
||||
<div class="step-title">Mine</div>
|
||||
<div class="step-desc">Create cards from Yomitan or mine sentence cards directly from subtitle lines.</div>
|
||||
</div>
|
||||
<div class="workflow-connector" aria-hidden="true"></div>
|
||||
<div class="workflow-step" style="animation-delay: 180ms">
|
||||
<div class="step-number">04</div>
|
||||
<div class="step-title">Enrich</div>
|
||||
<div class="step-desc">Automatically attach timing-accurate audio, sentence text, and visual evidence.</div>
|
||||
</div>
|
||||
<div class="workflow-connector" aria-hidden="true"></div>
|
||||
<div class="workflow-step" style="animation-delay: 240ms">
|
||||
<div class="step-number">05</div>
|
||||
<div class="step-title">Track</div>
|
||||
<div class="step-desc">Review immersion history and repeat high-value patterns over time.</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="demo-section">
|
||||
<h2>See it in action</h2>
|
||||
<p>Subtitles, lookup flow, and card enrichment from a real playback session.</p>
|
||||
<div class="demo-window">
|
||||
<div class="demo-window__bar">
|
||||
<span class="demo-window__dot"></span>
|
||||
<span class="demo-window__dot"></span>
|
||||
<span class="demo-window__dot"></span>
|
||||
<span class="demo-window__title">subminer -- playback</span>
|
||||
</div>
|
||||
<video controls playsinline preload="metadata" :poster="`/assets/minecard-poster.jpg?v=${demoAssetVersion}`">
|
||||
<source :src="`/assets/minecard.webm?v=${demoAssetVersion}`" type="video/webm" />
|
||||
<source :src="`/assets/minecard.mp4?v=${demoAssetVersion}`" type="video/mp4" />
|
||||
<a :href="`/assets/minecard.webm?v=${demoAssetVersion}`" target="_blank" rel="noreferrer">
|
||||
<img :src="`/assets/minecard.webp?v=${demoAssetVersion}`" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />
|
||||
</a>
|
||||
</video>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.landing-shell {
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
padding: 0.5rem 1rem 4rem;
|
||||
}
|
||||
|
||||
.landing-shell,
|
||||
.landing-shell .step-title,
|
||||
.landing-shell h1,
|
||||
.landing-shell h2 {
|
||||
font-family: var(--tui-font-mono);
|
||||
}
|
||||
|
||||
.VPHome :deep(.VPFeature),
|
||||
.VPHome :deep(.VPButton),
|
||||
.landing-shell .workflow-step,
|
||||
.landing-shell .demo-window,
|
||||
.landing-shell .demo-window__bar {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.step-title,
|
||||
.step-number {
|
||||
font-family: var(--tui-font-mono);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* === Workflow === */
|
||||
.workflow-section {
|
||||
margin: 2.4rem auto 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.workflow-section h2,
|
||||
.demo-section h2 {
|
||||
font-size: 1.45rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.workflow-section h2::after,
|
||||
.demo-section h2::after {
|
||||
content: '';
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
height: 1px;
|
||||
background: repeating-linear-gradient(
|
||||
to right,
|
||||
var(--vp-c-divider) 0,
|
||||
var(--vp-c-divider) 1ch,
|
||||
transparent 1ch,
|
||||
transparent 1.5ch
|
||||
);
|
||||
}
|
||||
|
||||
.workflow-steps {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.workflow-step {
|
||||
flex: 1;
|
||||
padding: 1.2rem 1.25rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
animation: step-enter 400ms ease-out both;
|
||||
position: relative;
|
||||
transition: background 180ms ease;
|
||||
}
|
||||
|
||||
.workflow-step:hover {
|
||||
background: hsla(232, 23%, 18%, 0.6);
|
||||
}
|
||||
|
||||
.workflow-step:hover .step-number {
|
||||
color: var(--vp-c-brand-1);
|
||||
text-shadow: 0 0 12px hsla(267, 83%, 80%, 0.3);
|
||||
}
|
||||
|
||||
.workflow-connector {
|
||||
width: 1px;
|
||||
background: var(--vp-c-divider);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.workflow-step .step-number {
|
||||
display: inline-block;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 0.5rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
transition: color 180ms ease, text-shadow 180ms ease;
|
||||
}
|
||||
|
||||
.workflow-step .step-number::before {
|
||||
content: '$ ';
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.workflow-step .step-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.workflow-step .step-desc {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@keyframes step-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.workflow-steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1px;
|
||||
background: var(--vp-c-divider);
|
||||
}
|
||||
.workflow-step {
|
||||
min-width: 0;
|
||||
}
|
||||
.workflow-step:last-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.workflow-connector {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.workflow-steps {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.workflow-step:last-child {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* === Demo === */
|
||||
.demo-section {
|
||||
max-width: 960px;
|
||||
margin: 3rem auto 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.demo-section p {
|
||||
color: var(--vp-c-text-2);
|
||||
margin: 0 0 1.2rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.demo-window {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
animation: step-enter 400ms ease-out 300ms both;
|
||||
box-shadow:
|
||||
0 4px 16px rgba(0, 0, 0, 0.18),
|
||||
0 20px 48px rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
|
||||
.demo-window__bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-window__dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.demo-window__dot:nth-child(1) { background: #ed8796; }
|
||||
.demo-window__dot:nth-child(2) { background: #eed49f; }
|
||||
.demo-window__dot:nth-child(3) { background: #a6da95; }
|
||||
|
||||
.demo-window__title {
|
||||
font-family: var(--tui-font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.demo-window video {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
264
docs-site/installation.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# Installation
|
||||
|
||||
## Requirements
|
||||
|
||||
### System Dependencies
|
||||
|
||||
| Dependency | Required | Notes |
|
||||
| -------------------- | ---------- | -------------------------------------------------------- |
|
||||
| Bun | Yes | Required for `subminer` wrapper and source workflows |
|
||||
| mpv | Yes | Must support IPC sockets (`--input-ipc-server`) |
|
||||
| ffmpeg | For media | Audio extraction and screenshot generation |
|
||||
| MeCab + mecab-ipadic | No | Optional Japanese metadata enrichment (not the primary tokenizer) |
|
||||
| fuse2 | Linux only | Required for AppImage |
|
||||
| yt-dlp | No | Recommended for YouTube playback and subtitle extraction |
|
||||
|
||||
### Platform-Specific
|
||||
|
||||
**Linux** — one of the following compositors:
|
||||
|
||||
- Hyprland (uses `hyprctl`)
|
||||
- Sway (uses `swaymsg`)
|
||||
- X11 (uses `xdotool` and `xwininfo`)
|
||||
|
||||
**macOS** — macOS 10.13 or later. Accessibility permission required for window tracking.
|
||||
|
||||
**Windows** — Windows 10 or later. Install `mpv` and keep it available on `PATH`; SubMiner's packaged build handles window tracking directly.
|
||||
|
||||
### Optional Tools
|
||||
|
||||
| Tool | Purpose |
|
||||
| ----------------- | ------------------------------------------------------------- |
|
||||
| fzf | Terminal-based video picker (default) |
|
||||
| rofi | GUI-based video picker |
|
||||
| chafa | Thumbnail previews in fzf |
|
||||
| ffmpegthumbnailer | Generate video thumbnails for picker |
|
||||
| guessit | Better AniSkip title/season/episode parsing for file playback |
|
||||
| alass | Subtitle sync engine (preferred) |
|
||||
| ffsubsync | Subtitle sync engine (fallback) |
|
||||
|
||||
## Linux
|
||||
|
||||
### Arch Linux (AUR)
|
||||
|
||||
Install [`subminer-bin`](https://aur.archlinux.org/packages/subminer-bin) from the AUR if you want the packaged Linux release managed by pacman. The package installs the official SubMiner AppImage plus the `subminer` wrapper.
|
||||
|
||||
```bash
|
||||
paru -S subminer-bin
|
||||
```
|
||||
|
||||
Or manually:
|
||||
|
||||
```bash
|
||||
git clone https://aur.archlinux.org/subminer-bin.git
|
||||
cd subminer-bin
|
||||
makepkg -si
|
||||
```
|
||||
|
||||
### AppImage (Recommended)
|
||||
|
||||
Download the latest AppImage from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest):
|
||||
|
||||
```bash
|
||||
# Download and install AppImage
|
||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/SubMiner.AppImage -O ~/.local/bin/SubMiner.AppImage
|
||||
chmod +x ~/.local/bin/SubMiner.AppImage
|
||||
|
||||
# Download subminer wrapper script
|
||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O ~/.local/bin/subminer
|
||||
chmod +x ~/.local/bin/subminer
|
||||
|
||||
```
|
||||
|
||||
The `subminer` wrapper uses a Bun shebang (`#!/usr/bin/env bun`), so [Bun](https://bun.sh) must be installed and available on `PATH`.
|
||||
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/ksyasuda/SubMiner.git
|
||||
cd SubMiner
|
||||
# if you cloned without --recurse-submodules:
|
||||
git submodule update --init --recursive
|
||||
|
||||
bun install
|
||||
bun run build
|
||||
|
||||
# Optional packaged Linux artifact
|
||||
bun run build:appimage
|
||||
```
|
||||
|
||||
Bundled Yomitan is built during `bun run build`.
|
||||
If you prefer Make wrappers for local install flows, `make build-launcher` still generates `dist/launcher/subminer` and `make install` still installs the wrapper/theme/AppImage when those artifacts exist.
|
||||
|
||||
`make build` also builds the bundled Yomitan Chrome extension from the `vendor/subminer-yomitan` submodule into `build/yomitan` using Bun.
|
||||
|
||||
## macOS
|
||||
|
||||
### DMG (Recommended)
|
||||
|
||||
Download the **DMG** artifact from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest). Open it and drag `SubMiner.app` into `/Applications`.
|
||||
|
||||
A **ZIP** artifact is also available as a fallback — unzip and drag `SubMiner.app` into `/Applications`.
|
||||
|
||||
Install dependencies using Homebrew:
|
||||
|
||||
```bash
|
||||
brew install mpv mecab mecab-ipadic
|
||||
```
|
||||
|
||||
### From Source (macOS)
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/ksyasuda/SubMiner.git
|
||||
cd SubMiner
|
||||
git submodule update --init --recursive
|
||||
make build-macos
|
||||
```
|
||||
|
||||
The built app will be available in the `release` directory (`.dmg` and `.zip`).
|
||||
|
||||
For unsigned local builds:
|
||||
|
||||
```bash
|
||||
bun run build:mac:unsigned
|
||||
```
|
||||
|
||||
### Accessibility Permission
|
||||
|
||||
After launching SubMiner for the first time, grant accessibility permission:
|
||||
|
||||
1. Open **System Preferences** → **Security & Privacy** → **Privacy** tab
|
||||
2. Select **Accessibility** from the left sidebar
|
||||
3. Add SubMiner to the list
|
||||
|
||||
Without this permission, window tracking will not work and the overlay won't follow the mpv window.
|
||||
|
||||
### macOS Usage Notes
|
||||
|
||||
**Launching MPV with IPC:**
|
||||
|
||||
```bash
|
||||
mpv --input-ipc-server=/tmp/subminer-socket video.mkv
|
||||
```
|
||||
|
||||
**Config location:** `$XDG_CONFIG_HOME/SubMiner/config.jsonc` (or `~/.config/SubMiner/config.jsonc`).
|
||||
|
||||
**MeCab paths (Homebrew):**
|
||||
|
||||
- Apple Silicon (M1/M2): `/opt/homebrew/bin/mecab`
|
||||
- Intel: `/usr/local/bin/mecab`
|
||||
|
||||
Ensure `mecab` is available on your PATH when launching SubMiner.
|
||||
|
||||
**Fullscreen:** The overlay should appear correctly in fullscreen. If you encounter issues, check that accessibility permissions are granted.
|
||||
|
||||
**mpv plugin binary path:**
|
||||
|
||||
```ini
|
||||
binary_path=/Applications/SubMiner.app/Contents/MacOS/subminer
|
||||
```
|
||||
|
||||
## Windows
|
||||
|
||||
### Installer (Recommended)
|
||||
|
||||
Download the latest Windows installer from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest):
|
||||
|
||||
- `SubMiner-<version>.exe` installs the app, Start menu shortcut, and default files under `Program Files`
|
||||
- `SubMiner-<version>.zip` is available as a portable fallback
|
||||
|
||||
Install `mpv` separately and ensure `mpv.exe` is on `PATH`. `ffmpeg` is still required for media extraction, and MeCab remains optional.
|
||||
|
||||
### Windows Usage Notes
|
||||
|
||||
- Launch `SubMiner.exe` once to let the first-run setup flow seed `%APPDATA%\\SubMiner\\config.jsonc`, offer mpv plugin installation, open bundled Yomitan settings, and optionally create `SubMiner mpv` Start Menu/Desktop shortcuts.
|
||||
- If you use the mpv plugin, leave `binary_path` empty unless SubMiner is installed in a non-standard location.
|
||||
- Windows plugin installs rewrite `socket_path` to `\\.\pipe\subminer-socket`; do not keep `/tmp/subminer-socket` on Windows.
|
||||
- Native window tracking is built in on Windows; no `xdotool`, `xwininfo`, or compositor-specific helper is required.
|
||||
|
||||
### From Source (Windows)
|
||||
|
||||
```powershell
|
||||
git clone https://github.com/ksyasuda/SubMiner.git
|
||||
cd SubMiner
|
||||
bun install
|
||||
Set-Location vendor/texthooker-ui
|
||||
bun install --frozen-lockfile
|
||||
bun run build
|
||||
Set-Location ../..
|
||||
bun run build:win
|
||||
```
|
||||
|
||||
Windows installer builds already get the required NSIS `WinShell` helper through electron-builder's cached `nsis-resources` bundle.
|
||||
No extra repo-local WinShell plugin install step is required.
|
||||
|
||||
## MPV Plugin (Recommended)
|
||||
|
||||
The Lua plugin provides in-player keybindings to control the overlay from mpv. It communicates with SubMiner by invoking the binary with CLI flags.
|
||||
|
||||
::: warning Important
|
||||
mpv must be launched with `--input-ipc-server=/tmp/subminer-socket` for SubMiner to connect.
|
||||
:::
|
||||
|
||||
On Windows, the packaged plugin config is rewritten to `socket_path=\\.\pipe\subminer-socket`.
|
||||
|
||||
```bash
|
||||
# Option 1: install from release assets bundle
|
||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer-assets.tar.gz -O /tmp/subminer-assets.tar.gz
|
||||
tar -xzf /tmp/subminer-assets.tar.gz -C /tmp
|
||||
mkdir -p ~/.config/SubMiner
|
||||
cp /tmp/config.example.jsonc ~/.config/SubMiner/config.jsonc
|
||||
mkdir -p ~/.config/mpv/scripts/subminer
|
||||
mkdir -p ~/.config/mpv/script-opts
|
||||
cp -R /tmp/plugin/subminer/. ~/.config/mpv/scripts/subminer/
|
||||
cp /tmp/plugin/subminer.conf ~/.config/mpv/script-opts/
|
||||
|
||||
# Option 2: from source checkout
|
||||
# make install-plugin
|
||||
```
|
||||
|
||||
## Rofi Theme (Linux Only)
|
||||
|
||||
SubMiner ships a default rofi theme at `assets/themes/subminer.rasi`.
|
||||
|
||||
Install path (default auto-detected by `subminer`):
|
||||
|
||||
- Linux: `~/.local/share/SubMiner/themes/subminer.rasi`
|
||||
- macOS: `~/Library/Application Support/SubMiner/themes/subminer.rasi`
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.local/share/SubMiner/themes
|
||||
cp /tmp/assets/themes/subminer.rasi ~/.local/share/SubMiner/themes/subminer.rasi
|
||||
```
|
||||
|
||||
Override with `SUBMINER_ROFI_THEME=/absolute/path/to/theme.rasi`.
|
||||
|
||||
See [MPV Plugin](/mpv-plugin) for the full configuration reference, keybindings, script messages, and binary auto-detection details.
|
||||
|
||||
## Verify Installation
|
||||
|
||||
After installing, confirm SubMiner is working:
|
||||
|
||||
On Windows, replace `SubMiner.AppImage` with `SubMiner.exe` in the direct app commands below.
|
||||
|
||||
```bash
|
||||
# Play a file (default plugin config auto-starts visible overlay and waits for annotation readiness; first launch may open first-run setup popup)
|
||||
subminer video.mkv
|
||||
|
||||
# Optional explicit overlay start for setups with plugin auto_start=no
|
||||
subminer --start video.mkv
|
||||
|
||||
# Useful launch modes for troubleshooting
|
||||
subminer --log-level debug video.mkv
|
||||
SubMiner.AppImage --start --log-level debug
|
||||
|
||||
# Or with direct AppImage control
|
||||
SubMiner.AppImage --background # Background tray service mode
|
||||
SubMiner.AppImage --start
|
||||
SubMiner.AppImage --start --dev
|
||||
SubMiner.AppImage --help # Show all CLI options
|
||||
```
|
||||
|
||||
You should see the overlay appear over mpv. If subtitles are loaded in the video, they will appear as interactive text in the overlay.
|
||||
|
||||
Next: [Usage](/usage) — learn about the `subminer` wrapper, keybindings, and YouTube playback.
|
||||
93
docs-site/ipc-contracts.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# 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.
|
||||
|
||||
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
|
||||
|
||||
Renderer-initiated calls (`invoke`) pass through four boundaries before reaching a service. Fire-and-forget messages (`send`) follow the same path but skip the response leg. Malformed payloads are caught at the validator and never reach domain code.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
classDef rend fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef bridge fill:#f5a97f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef valid fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef handler fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef svc fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef err fill:#ed8796,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
|
||||
R["Renderer"]:::rend
|
||||
P(["preload.ts"]):::bridge
|
||||
M["ipcMain handler"]:::handler
|
||||
V{"Validator"}:::valid
|
||||
S["Service"]:::svc
|
||||
E["Structured error"]:::err
|
||||
|
||||
R -->|"invoke / send"| P
|
||||
P -->|"ipcRenderer"| M
|
||||
M --> V
|
||||
V -->|"valid"| S
|
||||
V -->|"malformed"| E
|
||||
S -->|"result"| P
|
||||
E -->|"{ ok: false }"| P
|
||||
P -->|"return"| R
|
||||
|
||||
style E fill:#ed8796,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
```
|
||||
|
||||
## Core Surfaces
|
||||
|
||||
| File | Role |
|
||||
| --- | --- |
|
||||
| `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/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. |
|
||||
| `src/main/cli-runtime.ts` | CLI/runtime command boundary. Handles commands that originate from the launcher or mpv plugin rather than the renderer. |
|
||||
|
||||
## 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.
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
**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
|
||||
|
||||
1. Add the channel constant in `src/shared/ipc/contracts.ts`.
|
||||
2. Add or extend the payload validator in `src/shared/ipc/validators.ts`.
|
||||
3. Expose a typed bridge method in `src/preload.ts`.
|
||||
4. Register the handler in `src/main/ipc-runtime.ts` (or the relevant domain runtime module).
|
||||
5. Add tests for both valid and malformed payload cases in `src/core/services/*`.
|
||||
6. Update renderer tests when behavior or state transitions change.
|
||||
|
||||
## Runtime State Notes
|
||||
|
||||
- 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.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Unknown payload in handler:** The validator is not being applied before the handler runs. Check that the channel is routed through `ipc-runtime.ts` with validation, not registered directly.
|
||||
- **Renderer invoke fails:** Verify the preload bridge method exists and matches the channel constant. Check that the handler is registered and returning (not throwing).
|
||||
- **Contract drift:** When invoke calls return unexpected shapes, compare the shared contract, validator, preload bridge, and main handler signatures side by side. One of them was updated without the others.
|
||||
|
||||
## Related Docs
|
||||
|
||||
- [Architecture](/architecture)
|
||||
- [Development](/development)
|
||||
- [Configuration](/configuration)
|
||||
- [Troubleshooting](/troubleshooting)
|
||||
177
docs-site/jellyfin-integration.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Jellyfin Integration
|
||||
|
||||
SubMiner includes an optional Jellyfin CLI integration for:
|
||||
|
||||
- authenticating against a server
|
||||
- listing libraries and media items
|
||||
- launching item playback in the connected mpv instance
|
||||
- receiving Jellyfin remote cast-to-device playback events in-app
|
||||
- opening an in-app setup window for server/user/password input
|
||||
|
||||
## Requirements
|
||||
|
||||
- Jellyfin server URL and user credentials
|
||||
- For `--jellyfin-play`: connected mpv IPC socket (`--start` or existing mpv plugin workflow)
|
||||
- On Linux, token encryption defaults to `gnome-libsecret`; pass `--password-store=<backend>` to override.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Set base config values (`config.jsonc`):
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"jellyfin": {
|
||||
"enabled": true,
|
||||
"serverUrl": "http://127.0.0.1:8096",
|
||||
"username": "your-user",
|
||||
"remoteControlEnabled": true,
|
||||
"remoteControlAutoConnect": true,
|
||||
"autoAnnounce": false,
|
||||
"remoteControlDeviceName": "SubMiner",
|
||||
"defaultLibraryId": "",
|
||||
"pullPictures": false,
|
||||
"iconCacheDir": "/tmp/subminer-jellyfin-icons",
|
||||
"directPlayPreferred": true,
|
||||
"directPlayContainers": ["mkv", "mp4", "webm", "mov", "flac", "mp3", "aac"],
|
||||
"transcodeVideoCodec": "h264",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
2. Authenticate:
|
||||
|
||||
```bash
|
||||
subminer jellyfin
|
||||
subminer jellyfin -l \
|
||||
--server http://127.0.0.1:8096 \
|
||||
--username your-user \
|
||||
--password 'your-password'
|
||||
```
|
||||
|
||||
3. List libraries:
|
||||
|
||||
```bash
|
||||
SubMiner.AppImage --jellyfin-libraries
|
||||
```
|
||||
|
||||
Launcher wrapper equivalent for interactive playback flow:
|
||||
|
||||
```bash
|
||||
subminer jellyfin -p
|
||||
```
|
||||
|
||||
Launcher wrapper for Jellyfin cast discovery mode (background app + tray):
|
||||
|
||||
```bash
|
||||
subminer jellyfin -d
|
||||
```
|
||||
|
||||
Stop discovery session/app:
|
||||
|
||||
```bash
|
||||
subminer app --stop
|
||||
```
|
||||
|
||||
`subminer jf ...` is an alias for `subminer jellyfin ...`.
|
||||
|
||||
To clear saved session credentials:
|
||||
|
||||
```bash
|
||||
subminer jellyfin --logout
|
||||
```
|
||||
|
||||
4. List items in a library:
|
||||
|
||||
```bash
|
||||
SubMiner.AppImage --jellyfin-items --jellyfin-library-id LIBRARY_ID --jellyfin-search term
|
||||
```
|
||||
|
||||
Optional listing controls:
|
||||
|
||||
- `--jellyfin-recursive=true|false` (default: true)
|
||||
- `--jellyfin-include-item-types=Series,Season,Folder,CollectionFolder,Movie,...`
|
||||
|
||||
These are used by the launcher picker flow to:
|
||||
|
||||
- keep root search focused on shows/folders/movies (exclude episode rows)
|
||||
- browse selected anime/show directories as folder-or-file lists
|
||||
- recurse for playable files only after selecting a folder
|
||||
|
||||
5. Start playback:
|
||||
|
||||
```bash
|
||||
SubMiner.AppImage --start
|
||||
SubMiner.AppImage --jellyfin-play --jellyfin-item-id ITEM_ID
|
||||
```
|
||||
|
||||
Optional stream overrides:
|
||||
|
||||
- `--jellyfin-audio-stream-index N`
|
||||
- `--jellyfin-subtitle-stream-index N`
|
||||
|
||||
## Playback Behavior
|
||||
|
||||
- Direct play is attempted first when:
|
||||
- `jellyfin.directPlayPreferred=true`
|
||||
- media source supports direct stream
|
||||
- source container matches `jellyfin.directPlayContainers`
|
||||
- If direct play is not selected/available, SubMiner requests a Jellyfin transcoded stream (`master.m3u8`) using `jellyfin.transcodeVideoCodec`.
|
||||
- Resume position (`PlaybackPositionTicks`) is applied via mpv seek.
|
||||
- Media title is set in mpv as `[Jellyfin/<mode>] <title>`.
|
||||
|
||||
## Cast To Device Mode (jellyfin-mpv-shim style)
|
||||
|
||||
When SubMiner is running with a valid Jellyfin session, it can appear as a
|
||||
remote playback target in Jellyfin's cast-to-device menu.
|
||||
|
||||
### Requirements
|
||||
|
||||
- `jellyfin.enabled=true`
|
||||
- valid `jellyfin.serverUrl` and Jellyfin auth session (env override or stored login session)
|
||||
- `jellyfin.remoteControlEnabled=true` (default)
|
||||
- `jellyfin.remoteControlAutoConnect=true` (default)
|
||||
- `jellyfin.autoAnnounce=false` by default (`true` enables auto announce/visibility check logs on connect)
|
||||
|
||||
### Behavior
|
||||
|
||||
- SubMiner connects to Jellyfin remote websocket and posts playback capabilities.
|
||||
- `Play` events open media in mpv with the same defaults used by `--jellyfin-play`.
|
||||
- If mpv IPC is not connected at cast time, SubMiner auto-launches mpv in idle mode with SubMiner defaults and retries playback.
|
||||
- `Playstate` events map to mpv pause/resume/seek/stop controls.
|
||||
- Stream selection commands (`SetAudioStreamIndex`, `SetSubtitleStreamIndex`) are mapped to mpv track selection.
|
||||
- SubMiner reports start/progress/stop timeline updates back to Jellyfin so now-playing and resume state stay synchronized.
|
||||
- `--jellyfin-remote-announce` forces an immediate capability re-broadcast and logs whether server sessions can see the device.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- Device not visible in Jellyfin cast menu:
|
||||
- ensure SubMiner is running
|
||||
- ensure session token is valid (`--jellyfin-login` again if needed)
|
||||
- ensure `remoteControlEnabled` and `remoteControlAutoConnect` are true
|
||||
- Cast command received but playback does not start:
|
||||
- verify mpv IPC can connect (`--start` flow)
|
||||
- verify item is playable from normal `--jellyfin-play --jellyfin-item-id ...`
|
||||
- Frequent reconnects:
|
||||
- check Jellyfin server/network stability and token expiration
|
||||
|
||||
## Failure Handling
|
||||
|
||||
User-visible errors are shown through CLI logs and mpv OSD for:
|
||||
|
||||
- invalid credentials
|
||||
- expired/invalid token
|
||||
- server/network errors
|
||||
- missing library/item identifiers
|
||||
- no playable source
|
||||
- mpv not connected for playback
|
||||
|
||||
## Security Notes and Limitations
|
||||
|
||||
- Jellyfin auth session (`accessToken` + `userId`) is stored in local encrypted token storage after login/setup.
|
||||
- Launcher wrappers support `--password-store=<backend>` and forward it through to the app process.
|
||||
- Optional environment overrides are supported: `SUBMINER_JELLYFIN_ACCESS_TOKEN` and `SUBMINER_JELLYFIN_USER_ID`.
|
||||
- Treat both token storage and config files as secrets and avoid committing them.
|
||||
- Password is used only for login and is not stored.
|
||||
- Optional setup UI is available via `--jellyfin`; all actions are also available via CLI flags.
|
||||
- `subminer` wrapper uses Jellyfin subcommands (`subminer jellyfin ...`, alias `subminer jf ...`). Use `SubMiner.AppImage` for direct `--jellyfin-libraries` and `--jellyfin-items`.
|
||||
- For direct app CLI usage (`SubMiner.AppImage ...`), `--jellyfin-server` can override server URL for login/play flows without editing config.
|
||||
45
docs-site/jlpt-vocab-bundle.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# JLPT Vocabulary Bundle (Offline)
|
||||
|
||||
## Bundle location
|
||||
|
||||
SubMiner expects the JLPT term-meta bank files to be available locally at:
|
||||
|
||||
- `vendor/yomitan-jlpt-vocab`
|
||||
|
||||
At runtime, SubMiner also searches these derived locations:
|
||||
|
||||
- `vendor/yomitan-jlpt-vocab`
|
||||
- `vendor/yomitan-jlpt-vocab/vendor/yomitan-jlpt-vocab`
|
||||
- `vendor/yomitan-jlpt-vocab/yomitan-jlpt-vocab`
|
||||
|
||||
and user-data/config fallback paths (see `getJlptDictionarySearchPaths` in `src/main.ts`).
|
||||
|
||||
## Required files
|
||||
|
||||
The expected files are:
|
||||
|
||||
- `term_meta_bank_1.json`
|
||||
- `term_meta_bank_2.json`
|
||||
- `term_meta_bank_3.json`
|
||||
- `term_meta_bank_4.json`
|
||||
- `term_meta_bank_5.json`
|
||||
|
||||
Each bank maps terms to frequency metadata; only entries with a `frequency.displayValue` are considered for JLPT tagging.
|
||||
|
||||
SubMiner also reuses the same `term_meta_bank_*.json` format for frequency-based subtitle highlighting, using installed/default `frequency-dictionary` locations or an explicit `subtitleStyle.frequencyDictionary.sourcePath`.
|
||||
|
||||
## Source and update process
|
||||
|
||||
For reproducible updates:
|
||||
|
||||
1. Obtain the JLPT term-meta bank archive from the same upstream source that supplies the bundled Yomitan dictionary data.
|
||||
2. Extract the five `term_meta_bank_*.json` files.
|
||||
3. Place them into `vendor/yomitan-jlpt-vocab/`.
|
||||
4. Commit the update with the source URL/version in the task notes.
|
||||
|
||||
This repository currently ships the folder path in `electron-builder` `extraResources` as:
|
||||
`vendor/yomitan-jlpt-vocab -> yomitan-jlpt-vocab`.
|
||||
|
||||
## Fallback Behavior
|
||||
|
||||
If bank files are missing, malformed, or lack expected metadata, SubMiner skips them gracefully. When no usable entries are found, JLPT underlining is silently disabled and subtitle rendering remains unchanged.
|
||||
104
docs-site/launcher-script.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Launcher Script
|
||||
|
||||
The `subminer` wrapper script is an all-in-one launcher that handles video selection, mpv startup, and overlay management. It's a Bun script distributed alongside the AppImage.
|
||||
|
||||
## Video Picker
|
||||
|
||||
When you run `subminer` without specifying a file, it opens an interactive video picker. By default it uses **fzf** in the terminal; pass `-R` to use **rofi** instead.
|
||||
|
||||
### fzf (default)
|
||||
|
||||
```bash
|
||||
subminer # pick from current directory
|
||||
subminer -d ~/Videos # pick from a specific directory
|
||||
subminer -r -d ~/Anime # recursive search
|
||||
```
|
||||
|
||||
fzf shows video files in a fuzzy-searchable list. If `chafa` is installed, you get thumbnail previews in the right pane. Thumbnails are sourced from the freedesktop thumbnail cache first, then generated on the fly with `ffmpegthumbnailer` or `ffmpeg` as fallback.
|
||||
|
||||
| Optional tool | Purpose |
|
||||
| ------------------- | --------------------------------- |
|
||||
| `chafa` | Render thumbnails in the terminal |
|
||||
| `ffmpegthumbnailer` | Generate thumbnails on the fly |
|
||||
|
||||
### rofi
|
||||
|
||||
```bash
|
||||
subminer -R # rofi picker, current directory
|
||||
subminer -R -d ~/Videos # rofi picker, specific directory
|
||||
subminer -R -r -d ~/Anime # rofi picker, recursive
|
||||
```
|
||||
|
||||
rofi shows a GUI menu with icon thumbnails when available. SubMiner ships a custom rofi theme that can be installed from the release assets:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.local/share/SubMiner/themes
|
||||
cp /tmp/assets/themes/subminer.rasi ~/.local/share/SubMiner/themes/subminer.rasi
|
||||
```
|
||||
|
||||
The theme is auto-detected from these paths (first match wins):
|
||||
|
||||
- `$SUBMINER_ROFI_THEME` environment variable (absolute path)
|
||||
- `$XDG_DATA_HOME/SubMiner/themes/subminer.rasi` (default: `~/.local/share/SubMiner/themes/subminer.rasi`)
|
||||
- `/usr/local/share/SubMiner/themes/subminer.rasi`
|
||||
- `/usr/share/SubMiner/themes/subminer.rasi`
|
||||
- macOS: `~/Library/Application Support/SubMiner/themes/subminer.rasi`
|
||||
|
||||
Override with the `SUBMINER_ROFI_THEME` environment variable:
|
||||
|
||||
```bash
|
||||
SUBMINER_ROFI_THEME=/path/to/custom-theme.rasi subminer -R
|
||||
```
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
subminer video.mkv # play a specific file (default plugin config auto-starts visible overlay)
|
||||
subminer --start video.mkv # optional explicit overlay start when plugin auto_start=no
|
||||
subminer -S video.mkv # same as above via --start-overlay
|
||||
subminer https://youtu.be/... # YouTube playback (requires yt-dlp)
|
||||
subminer ytsearch:"jp news" # YouTube search
|
||||
subminer --setup # Open first-run setup popup
|
||||
```
|
||||
|
||||
## Subcommands
|
||||
|
||||
| Subcommand | Purpose |
|
||||
| -------------------------- | ---------------------------------------------------------- |
|
||||
| `subminer jellyfin` / `jf` | Jellyfin workflows (`-d` discovery, `-p` play, `-l` login) |
|
||||
| `subminer yt` / `youtube` | YouTube shorthand (`-o`, `-m`) |
|
||||
| `subminer doctor` | Dependency + config + socket diagnostics |
|
||||
| `subminer config path` | Print active config file path |
|
||||
| `subminer config show` | Print active config contents |
|
||||
| `subminer mpv status` | Check mpv socket readiness |
|
||||
| `subminer mpv socket` | Print active socket path |
|
||||
| `subminer mpv idle` | Launch detached idle mpv instance |
|
||||
| `subminer dictionary <path>` | Generate character dictionary ZIP from file/dir target |
|
||||
| `subminer texthooker` | Launch texthooker-only mode |
|
||||
| `subminer app` | Pass arguments directly to SubMiner binary |
|
||||
|
||||
Use `subminer <subcommand> -h` for command-specific help.
|
||||
|
||||
## Options
|
||||
|
||||
| Flag | Description |
|
||||
| --------------------- | --------------------------------------------------- |
|
||||
| `-d, --directory` | Video search directory (default: cwd) |
|
||||
| `-r, --recursive` | Search directories recursively |
|
||||
| `-R, --rofi` | Use rofi instead of fzf |
|
||||
| `--setup` | Open first-run setup popup manually |
|
||||
| `--start` | Explicitly start overlay after mpv launches |
|
||||
| `-S, --start-overlay` | Explicitly start overlay after mpv launches |
|
||||
| `-T, --no-texthooker` | Disable texthooker server |
|
||||
| `-p, --profile` | mpv profile name (default: `subminer`) |
|
||||
| `-b, --backend` | Force window backend (`hyprland`, `sway`, `x11`) |
|
||||
| `--log-level` | Logger verbosity (`debug`, `info`, `warn`, `error`) |
|
||||
| `--dev`, `--debug` | Enable app dev-mode (not tied to log level) |
|
||||
|
||||
With default plugin settings (`auto_start=yes`, `auto_start_visible_overlay=yes`, `auto_start_pause_until_ready=yes`), explicit start flags are usually unnecessary.
|
||||
|
||||
## Logging
|
||||
|
||||
- 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
|
||||
210
docs-site/mining-workflow.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# Mining Workflow
|
||||
|
||||
This guide walks through the sentence mining loop — from watching a video to creating Anki cards with audio, screenshots, and context.
|
||||
|
||||
## Overview
|
||||
|
||||
SubMiner runs as a transparent overlay on top of mpv. As subtitles play, the overlay displays them as interactive text. You click a word to look it up with Yomitan, then create an Anki card with a single action. SubMiner automatically attaches the sentence, audio clip, and screenshot.
|
||||
|
||||
```text
|
||||
Watch video → See subtitle → Click word → Yomitan lookup → Add to Anki
|
||||
↓
|
||||
SubMiner auto-fills:
|
||||
sentence, audio, image, translation
|
||||
```
|
||||
|
||||
## Subtitle Delivery Path (Startup + Runtime)
|
||||
|
||||
SubMiner prioritizes subtitle responsiveness over heavy initialization:
|
||||
|
||||
1. The first subtitle render is **plain text first** (no tokenization wait).
|
||||
2. Tokenized enrichment (word spans, known-word flags, JLPT/frequency metadata) is applied right after parsing completes.
|
||||
3. Under rapid subtitle churn, SubMiner uses a **latest-only tokenization queue** so stale lines are dropped instead of building lag.
|
||||
4. MeCab, Yomitan extension load, and dictionary prewarm run as background warmups after overlay initialization (configurable via `startupWarmups`, including low-power mode).
|
||||
|
||||
This keeps early playback snappy and avoids mpv-side sluggishness while startup work completes.
|
||||
|
||||
## Overlay Model
|
||||
|
||||
SubMiner uses one overlay window with modal surfaces.
|
||||
|
||||
### Primary Subtitle Layer
|
||||
|
||||
The visible overlay renders subtitles as tokenized, clickable word spans. Each word is a separate element with reading and headword data attached. This plane is styled independently from mpv subtitles and supports:
|
||||
|
||||
- Word-level click targets for Yomitan lookup
|
||||
- Auto pause/resume on subtitle hover (enabled by default via `subtitleStyle.autoPauseVideoOnHover`)
|
||||
- Optional pause while the Yomitan popup is open (`subtitleStyle.autoPauseVideoOnYomitanPopup`)
|
||||
- Right-click to pause/resume
|
||||
- Right-click + drag to reposition subtitles
|
||||
- Modal dialogs for Jimaku search, field grouping, subsync, and runtime options
|
||||
- **Reading annotations** — known words, N+1 targets, character-name matches, JLPT levels, and frequency hits can all be visually highlighted
|
||||
|
||||
Toggle visibility with `Alt+Shift+O` (global) or `y-t` (mpv plugin).
|
||||
|
||||
### Secondary Subtitle Bar
|
||||
|
||||
The secondary subtitle bar is a compact top-strip region in the same overlay window for translation/context visibility while keeping primary reading flow below. It mirrors your configured secondary subtitle preference and can be independently shown or hidden.
|
||||
|
||||
It is controlled by `secondarySub` configuration and shares lifecycle with the main overlay window.
|
||||
|
||||
### Modal Surfaces
|
||||
|
||||
Jimaku search, field-grouping, runtime options, and manual subsync open as modal surfaces on top of the same overlay window.
|
||||
|
||||
## Looking Up Words
|
||||
|
||||
1. Hover over the subtitle area — the overlay activates pointer events.
|
||||
2. Click any word. SubMiner uses Unicode-aware boundary detection (`Intl.Segmenter`) to select it. On macOS, hovering is enough.
|
||||
3. Yomitan detects the selection and opens its lookup popup.
|
||||
4. From the popup, add the word to Anki.
|
||||
|
||||
### Controller Workflow
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Creating Anki Cards
|
||||
|
||||
There are three ways to create cards, depending on your workflow.
|
||||
|
||||
### 1. Auto-Update from Yomitan
|
||||
|
||||
This is the most common flow. Yomitan creates a card in Anki, and SubMiner enriches it automatically.
|
||||
|
||||
1. Click a word → Yomitan popup appears.
|
||||
2. Click the Anki icon in Yomitan to add the word.
|
||||
3. SubMiner receives or detects the new card:
|
||||
- **Proxy mode** (`ankiConnect.proxy.enabled: true`): immediate enrich after successful `addNote` / `addNotes`.
|
||||
- **Polling mode** (default): detects via AnkiConnect polling (`ankiConnect.pollingRate`, default 3 seconds).
|
||||
4. SubMiner updates the card with:
|
||||
- **Sentence**: The current subtitle line.
|
||||
- **Audio**: Extracted from the video using the subtitle's start/end timing (plus configurable padding).
|
||||
- **Image**: A screenshot or animated clip from the current playback position.
|
||||
- **Translation**: From the secondary subtitle track, or generated via AI if configured.
|
||||
- **MiscInfo**: Metadata like filename and timestamp.
|
||||
|
||||
Configure which fields to fill in `ankiConnect.fields`. See [Anki Integration](/anki-integration) for details.
|
||||
|
||||
### 2. Manual Update from Clipboard
|
||||
|
||||
If you prefer a hands-on approach (animecards-style), you can copy the current subtitle to the clipboard and then paste it onto the last-added Anki card:
|
||||
|
||||
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.
|
||||
|
||||
This is useful when auto-update is disabled or when you want explicit control over which subtitle line gets attached to the card.
|
||||
|
||||
| Shortcut | Action | Config key |
|
||||
| -------------------------- | ------------------------------- | --------------------------------------- |
|
||||
| `Ctrl/Cmd+C` | Copy current subtitle | `shortcuts.copySubtitle` |
|
||||
| `Ctrl/Cmd+Shift+C` + digit | Copy multiple recent lines | `shortcuts.copySubtitleMultiple` |
|
||||
| `Ctrl/Cmd+V` | Update last card from clipboard | `shortcuts.updateLastCardFromClipboard` |
|
||||
|
||||
### 3. Mine Sentence (Hotkey)
|
||||
|
||||
Create a standalone sentence card without going through Yomitan:
|
||||
|
||||
- **Mine current sentence**: `Ctrl/Cmd+S` (configurable via `shortcuts.mineSentence`)
|
||||
- **Mine multiple lines**: `Ctrl/Cmd+Shift+S` followed by a digit 1–9 to select how many recent subtitle lines to combine.
|
||||
|
||||
The sentence card uses the note type configured in `isLapis.sentenceCardModel` and always maps sentence/audio to `Sentence` and `SentenceAudio`.
|
||||
|
||||
### 4. Mark as Audio Card
|
||||
|
||||
After adding a word via Yomitan, press the audio card shortcut to overwrite the audio with a longer clip spanning the full subtitle timing.
|
||||
|
||||
## Secondary Subtitles
|
||||
|
||||
SubMiner can display a secondary subtitle track (typically English) alongside the primary Japanese subtitles. This is useful for:
|
||||
|
||||
- Quick comprehension checks without leaving the mining flow.
|
||||
- Auto-populating the translation field on mined cards.
|
||||
|
||||
### Display Modes
|
||||
|
||||
Cycle through modes with the configured shortcut:
|
||||
|
||||
- **Hidden**: Secondary subtitle not shown.
|
||||
- **Visible**: Always displayed below the primary subtitle.
|
||||
- **Hover**: Only shown when you hover over the primary subtitle.
|
||||
|
||||
When a card is created, SubMiner uses the secondary subtitle text as the translation field value (unless AI translation is configured to override it).
|
||||
|
||||
## Field Grouping (Kiku)
|
||||
|
||||
If you mine the same word from different sentences, SubMiner can merge the cards instead of creating duplicates. This feature is designed for use with [Kiku](https://github.com/youyoumu/kiku) and similar note types that support grouped fields.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. You add a word via Yomitan.
|
||||
2. SubMiner detects the new card and checks if a card with the same expression already exists.
|
||||
3. If a duplicate is found:
|
||||
- **Auto mode** (`fieldGrouping: "auto"`): Merges automatically. Both sentences, audio clips, and images are combined into the existing card. The duplicate is optionally deleted.
|
||||
- **Manual mode** (`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.
|
||||
|
||||
## Jimaku Subtitle Search
|
||||
|
||||
SubMiner integrates with [Jimaku](https://jimaku.cc) to search and download subtitle files for anime directly from the overlay.
|
||||
|
||||
1. Open the Jimaku modal via the configured shortcut (`Ctrl+Shift+J` by default).
|
||||
2. SubMiner auto-fills the search from the current video filename (title, season, episode).
|
||||
3. Browse matching entries and select a subtitle file to download.
|
||||
4. The subtitle is loaded into mpv as a new track.
|
||||
|
||||
Requires an internet connection. An API key (`jimaku.apiKey`) is optional but recommended for higher rate limits.
|
||||
|
||||
## 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.
|
||||
|
||||
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.
|
||||
|
||||
## Subtitle Sync (Subsync)
|
||||
|
||||
If your subtitle file is out of sync with the audio, SubMiner can resynchronize it using [alass](https://github.com/kaegi/alass) or [ffsubsync](https://github.com/smacke/ffsubsync).
|
||||
|
||||
1. Open the subsync modal from the overlay.
|
||||
2. Select the sync engine (alass or ffsubsync).
|
||||
3. For alass, select a reference subtitle track from the video.
|
||||
4. SubMiner runs the sync and reloads the corrected subtitle.
|
||||
|
||||
Install the sync tools separately — see [Troubleshooting](/troubleshooting#subtitle-sync-subsync) if the tools are not found.
|
||||
|
||||
## N+1 Word Highlighting
|
||||
|
||||
When enabled, SubMiner cross-references your Anki decks to highlight known words in the overlay, making true N+1 sentences (exactly one unknown word) easy to spot during immersion.
|
||||
|
||||
See [Subtitle Annotations — N+1](/subtitle-annotations#n1-word-highlighting) for configuration options and color settings.
|
||||
|
||||
## Immersion Tracking
|
||||
|
||||
SubMiner can log your watching and mining activity to a local SQLite database — session times, words seen, cards mined, and daily/monthly rollups.
|
||||
|
||||
Enable it in your config:
|
||||
|
||||
```jsonc
|
||||
"immersionTracking": {
|
||||
"enabled": true,
|
||||
"dbPath": "" // leave empty to use the default location
|
||||
}
|
||||
```
|
||||
|
||||
See [Immersion Tracking](/immersion-tracking) for the full schema and retention settings.
|
||||
|
||||
Next: [Anki Integration](/anki-integration) — field mapping, media generation, and card enrichment configuration.
|
||||
245
docs-site/mpv-plugin.md
Normal file
@@ -0,0 +1,245 @@
|
||||
# MPV Plugin
|
||||
|
||||
The SubMiner mpv plugin (`subminer/main.lua`) provides in-player keybindings to control the overlay without leaving mpv. It communicates with SubMiner by invoking the AppImage (or binary) with CLI flags.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# From release bundle:
|
||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer-assets.tar.gz -O /tmp/subminer-assets.tar.gz
|
||||
tar -xzf /tmp/subminer-assets.tar.gz -C /tmp
|
||||
mkdir -p ~/.config/SubMiner
|
||||
cp /tmp/config.example.jsonc ~/.config/SubMiner/config.jsonc
|
||||
mkdir -p ~/.config/mpv/scripts/subminer
|
||||
mkdir -p ~/.config/mpv/script-opts
|
||||
cp -R /tmp/plugin/subminer/. ~/.config/mpv/scripts/subminer/
|
||||
cp /tmp/plugin/subminer.conf ~/.config/mpv/script-opts/
|
||||
|
||||
# Or from source checkout: make install-plugin
|
||||
```
|
||||
|
||||
mpv must have IPC enabled for SubMiner to connect:
|
||||
|
||||
```ini
|
||||
# ~/.config/mpv/mpv.conf
|
||||
input-ipc-server=/tmp/subminer-socket
|
||||
```
|
||||
|
||||
On Windows, use a named pipe instead:
|
||||
|
||||
```ini
|
||||
input-ipc-server=\\.\pipe\subminer-socket
|
||||
```
|
||||
|
||||
## Keybindings
|
||||
|
||||
All keybindings use a `y` chord prefix — press `y`, then the second key:
|
||||
|
||||
| Chord | Action |
|
||||
| ----- | ---------------------- |
|
||||
| `y-y` | Open menu |
|
||||
| `y-s` | Start overlay |
|
||||
| `y-S` | Stop overlay |
|
||||
| `y-t` | Toggle visible overlay |
|
||||
| `y-o` | Open settings window |
|
||||
| `y-r` | Restart overlay |
|
||||
| `y-c` | Check status |
|
||||
| `y-k` | Skip intro (AniSkip) |
|
||||
|
||||
## Menu
|
||||
|
||||
Press `y-y` to open an interactive menu in mpv's OSD:
|
||||
|
||||
```text
|
||||
SubMiner:
|
||||
1. Start overlay
|
||||
2. Stop overlay
|
||||
3. Toggle overlay
|
||||
4. Open options
|
||||
5. Restart overlay
|
||||
6. Check status
|
||||
```
|
||||
|
||||
Select an item by pressing its number.
|
||||
|
||||
## Configuration
|
||||
|
||||
Create or edit `~/.config/mpv/script-opts/subminer.conf`:
|
||||
|
||||
```ini
|
||||
# Path to SubMiner binary. Leave empty for auto-detection.
|
||||
binary_path=
|
||||
|
||||
# MPV IPC socket path. Must match input-ipc-server in mpv.conf.
|
||||
socket_path=/tmp/subminer-socket
|
||||
|
||||
# Enable the texthooker WebSocket server.
|
||||
texthooker_enabled=yes
|
||||
|
||||
# Port for the texthooker server.
|
||||
texthooker_port=5174
|
||||
|
||||
# Window manager backend: auto, hyprland, sway, x11, macos.
|
||||
backend=auto
|
||||
|
||||
# Start the overlay automatically when a file is loaded.
|
||||
# Runs only when mpv input-ipc-server matches socket_path.
|
||||
auto_start=yes
|
||||
|
||||
# Show the visible overlay on auto-start.
|
||||
# Runs only when mpv input-ipc-server matches socket_path.
|
||||
auto_start_visible_overlay=yes
|
||||
|
||||
# Pause mpv on visible auto-start until SubMiner signals overlay/tokenization readiness.
|
||||
# Requires auto_start=yes and auto_start_visible_overlay=yes.
|
||||
auto_start_pause_until_ready=yes
|
||||
|
||||
# Show OSD messages for overlay status changes.
|
||||
osd_messages=yes
|
||||
|
||||
# Logging level: debug, info, warn, error.
|
||||
log_level=info
|
||||
|
||||
# Enable AniSkip intro detection/markers.
|
||||
aniskip_enabled=yes
|
||||
|
||||
# Optional title override (launcher fills from guessit when available).
|
||||
aniskip_title=
|
||||
|
||||
# Optional season override (launcher fills from guessit when available).
|
||||
aniskip_season=
|
||||
|
||||
# Optional MAL ID override. Leave blank to resolve from media title.
|
||||
aniskip_mal_id=
|
||||
|
||||
# Optional episode override. Leave blank to detect from filename/title.
|
||||
aniskip_episode=
|
||||
|
||||
# Show OSD skip button while inside intro range.
|
||||
aniskip_show_button=yes
|
||||
|
||||
# OSD label + keybinding for intro skip action.
|
||||
aniskip_button_text=You can skip by pressing %s
|
||||
aniskip_button_key=y-k
|
||||
aniskip_button_duration=3
|
||||
```
|
||||
|
||||
### Option Reference
|
||||
|
||||
| Option | Default | Values | Description |
|
||||
| ------------------------------ | ----------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------- |
|
||||
| `binary_path` | `""` (auto-detect) | file path | Path to SubMiner binary |
|
||||
| `socket_path` | `/tmp/subminer-socket` | file path | MPV IPC socket path |
|
||||
| `texthooker_enabled` | `yes` | `yes` / `no` | Enable texthooker server |
|
||||
| `texthooker_port` | `5174` | 1–65535 | Texthooker server port |
|
||||
| `backend` | `auto` | `auto`, `hyprland`, `sway`, `x11`, `macos` | Window manager backend |
|
||||
| `auto_start` | `yes` | `yes` / `no` | Auto-start overlay on file load when mpv socket matches `socket_path` |
|
||||
| `auto_start_visible_overlay` | `yes` | `yes` / `no` | Show visible layer on auto-start when mpv socket matches `socket_path` |
|
||||
| `auto_start_pause_until_ready` | `yes` | `yes` / `no` | Pause mpv on visible auto-start; resume when SubMiner signals tokenization-ready |
|
||||
| `osd_messages` | `yes` | `yes` / `no` | Show OSD status messages |
|
||||
| `log_level` | `info` | `debug`, `info`, `warn`, `error` | Log verbosity |
|
||||
| `aniskip_enabled` | `yes` | `yes` / `no` | Enable AniSkip intro detection |
|
||||
| `aniskip_title` | `""` | string | Override title used for lookup |
|
||||
| `aniskip_season` | `""` | numeric season | Optional season hint |
|
||||
| `aniskip_mal_id` | `""` | numeric MAL id | Skip title lookup; use fixed id |
|
||||
| `aniskip_episode` | `""` | numeric episode | Skip episode parsing; use fixed |
|
||||
| `aniskip_payload` | `""` | JSON / base64-encoded JSON | Optional pre-fetched AniSkip payload for this media. When set, plugin skips network lookup |
|
||||
| `aniskip_show_button` | `yes` | `yes` / `no` | Show in-range intro skip prompt |
|
||||
| `aniskip_button_text` | `You can skip by pressing %s` | string | OSD prompt format (`%s`=key) |
|
||||
| `aniskip_button_key` | `y-k` | mpv key chord | Primary key for intro skip action (`y-k` always works as fallback) |
|
||||
| `aniskip_button_duration` | `3` | float seconds | OSD hint duration |
|
||||
|
||||
## Binary Auto-Detection
|
||||
|
||||
When `binary_path` is empty, the plugin searches platform-specific locations:
|
||||
|
||||
**Linux:**
|
||||
|
||||
1. `~/.local/bin/SubMiner.AppImage`
|
||||
2. `/opt/SubMiner/SubMiner.AppImage`
|
||||
3. `/usr/local/bin/SubMiner`
|
||||
4. `/usr/bin/SubMiner`
|
||||
|
||||
**macOS:**
|
||||
|
||||
1. `/Applications/SubMiner.app/Contents/MacOS/SubMiner`
|
||||
2. `~/Applications/SubMiner.app/Contents/MacOS/SubMiner`
|
||||
|
||||
**Windows:**
|
||||
|
||||
1. `C:\Program Files\SubMiner\SubMiner.exe`
|
||||
2. `C:\Program Files (x86)\SubMiner\SubMiner.exe`
|
||||
3. `C:\SubMiner\SubMiner.exe`
|
||||
|
||||
Packaged Windows plugin installs also rewrite `socket_path` to `\\.\pipe\subminer-socket` automatically.
|
||||
|
||||
## Backend Detection
|
||||
|
||||
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.
|
||||
|
||||
## Script Messages
|
||||
|
||||
The plugin can be controlled from other mpv scripts or the mpv command line using script messages:
|
||||
|
||||
```
|
||||
script-message subminer-start
|
||||
script-message subminer-stop
|
||||
script-message subminer-toggle
|
||||
script-message subminer-menu
|
||||
script-message subminer-options
|
||||
script-message subminer-restart
|
||||
script-message subminer-status
|
||||
script-message subminer-autoplay-ready
|
||||
script-message subminer-aniskip-refresh
|
||||
script-message subminer-skip-intro
|
||||
```
|
||||
|
||||
The `subminer-start` message accepts overrides:
|
||||
|
||||
```
|
||||
script-message subminer-start backend=hyprland socket=/custom/path texthooker=no log-level=debug
|
||||
```
|
||||
|
||||
`log-level` here controls only logging verbosity passed to SubMiner.
|
||||
`--debug` is a separate app/dev-mode flag in the main CLI and should not be used here for logging.
|
||||
|
||||
## AniSkip Intro Skip
|
||||
|
||||
- AniSkip lookups are gated. The plugin only runs lookup when:
|
||||
- SubMiner launcher metadata is present, or
|
||||
- SubMiner app process is already running, or
|
||||
- You explicitly call `script-message subminer-aniskip-refresh`.
|
||||
- Lookups are asynchronous (no blocking `ps`/`curl` on `file-loaded`).
|
||||
- MAL/title resolution is cached for the current mpv session.
|
||||
- When launched via `subminer`, launcher can pass `aniskip_payload` (pre-fetched AniSkip `skip-times` payload) and the plugin applies it directly without making API calls.
|
||||
- If the payload is absent or invalid, lookup falls back to title/MAL-based async fetch.
|
||||
- Install `guessit` for best detection quality (`python3 -m pip install --user guessit`).
|
||||
- If OP interval exists, plugin adds `AniSkip Intro Start` and `AniSkip Intro End` chapters.
|
||||
- At intro start, plugin shows an OSD hint for the first 3 seconds (`You can skip by pressing y-k` by default).
|
||||
- Use `script-message subminer-aniskip-refresh` after changing media metadata/options to retry lookup.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
- **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).
|
||||
- **Duplicate auto-start events**: Repeated `file-loaded` hooks while overlay is already running are ignored for auto-start triggers (prevents duplicate start attempts).
|
||||
- **MPV shutdown**: The plugin sends a stop command to gracefully shut down both the overlay and the texthooker server.
|
||||
- **Texthooker**: Starts as a separate subprocess before the overlay to ensure the app lock is acquired first.
|
||||
|
||||
## Using with the `subminer` Wrapper
|
||||
|
||||
The `subminer` wrapper script handles mpv launch, socket setup, and overlay lifecycle automatically. You do not need the plugin if you always use the wrapper.
|
||||
|
||||
The plugin is useful when you:
|
||||
|
||||
- Launch mpv from other tools (file managers, media centers).
|
||||
- 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.
|
||||
23
docs-site/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "subminer-docs",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"description": "In-repo VitePress documentation site for SubMiner",
|
||||
"packageManager": "bun@1.3.5",
|
||||
"scripts": {
|
||||
"docs:dev": "VITE_EXTRA_EXTENSIONS=jsonc vitepress dev --host 0.0.0.0 --port 5173 --strictPort",
|
||||
"docs:build": "VITE_EXTRA_EXTENSIONS=jsonc vitepress build",
|
||||
"docs:preview": "VITE_EXTRA_EXTENSIONS=jsonc vitepress preview --host 0.0.0.0 --port 4173 --strictPort",
|
||||
"test": "bun test plausible.test.ts index.assets.test.ts docs-sync.test.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@catppuccin/vitepress": "^0.1.2",
|
||||
"@fontsource/jetbrains-mono": "^5.2.8",
|
||||
"@fontsource/manrope": "^5.2.8",
|
||||
"@plausible-analytics/tracker": "^0.4.4",
|
||||
"mermaid": "^11.12.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitepress": "^1.6.4"
|
||||
}
|
||||
}
|
||||
23
docs-site/plausible.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const docsConfigPath = new URL('./.vitepress/config.ts', import.meta.url);
|
||||
const docsThemePath = new URL('./.vitepress/theme/index.ts', import.meta.url);
|
||||
const docsConfigContents = readFileSync(docsConfigPath, 'utf8');
|
||||
const docsThemeContents = readFileSync(docsThemePath, 'utf8');
|
||||
|
||||
test('docs site keeps docs hostname while sending plausible events to subminer.moe via worker.subminer.moe', () => {
|
||||
expect(docsConfigContents).toContain("hostname: 'https://docs.subminer.moe'");
|
||||
expect(docsThemeContents).toContain("const PLAUSIBLE_DOMAIN = 'subminer.moe'");
|
||||
expect(docsThemeContents).toContain(
|
||||
"const PLAUSIBLE_ENDPOINT = 'https://worker.subminer.moe/api/event'",
|
||||
);
|
||||
expect(docsThemeContents).toContain('@plausible-analytics/tracker');
|
||||
expect(docsThemeContents).toContain('const { init } = await import');
|
||||
expect(docsThemeContents).toContain('domain: PLAUSIBLE_DOMAIN');
|
||||
expect(docsThemeContents).toContain('endpoint: PLAUSIBLE_ENDPOINT');
|
||||
expect(docsThemeContents).toContain('outboundLinks: true');
|
||||
expect(docsThemeContents).toContain('fileDownloads: true');
|
||||
expect(docsThemeContents).toContain('formSubmissions: true');
|
||||
expect(docsThemeContents).toContain('captureOnLocalhost: false');
|
||||
});
|
||||
BIN
docs-site/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs-site/public/assets/SubMiner.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
40
docs-site/public/assets/anki-card.svg
Normal file
@@ -0,0 +1,40 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="ac-card" x1="6" y1="8" x2="38" y2="44" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#34d399"/>
|
||||
<stop offset="1" stop-color="#059669"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="ac-glow" x1="8" y1="10" x2="36" y2="42" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6ee7b7" stop-opacity="0.5"/>
|
||||
<stop offset="1" stop-color="#059669" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<filter id="ac-soft" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="1.2"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<!-- Glow aura behind card -->
|
||||
<rect x="6" y="8" width="28" height="36" rx="5" fill="url(#ac-glow)" filter="url(#ac-soft)"/>
|
||||
<!-- Shadow card (back) -->
|
||||
<rect x="14" y="5" width="26" height="34" rx="4" fill="#059669" opacity="0.15"/>
|
||||
<!-- Main card -->
|
||||
<rect x="6" y="9" width="26" height="34" rx="4" fill="url(#ac-card)"/>
|
||||
<!-- Sentence line -->
|
||||
<rect x="10" y="16" width="16" height="2.5" rx="1.25" fill="white" opacity="0.9"/>
|
||||
<!-- Audio waveform mini -->
|
||||
<rect x="10" y="22" width="1.8" height="5" rx="0.9" fill="white" opacity="0.55"/>
|
||||
<rect x="13" y="20.5" width="1.8" height="8" rx="0.9" fill="white" opacity="0.55"/>
|
||||
<rect x="16" y="21.5" width="1.8" height="6" rx="0.9" fill="white" opacity="0.55"/>
|
||||
<rect x="19" y="23" width="1.8" height="3" rx="0.9" fill="white" opacity="0.55"/>
|
||||
<rect x="22" y="21" width="1.8" height="7" rx="0.9" fill="white" opacity="0.55"/>
|
||||
<rect x="25" y="22.5" width="1.8" height="4" rx="0.9" fill="white" opacity="0.55"/>
|
||||
<!-- Image thumbnail placeholder -->
|
||||
<rect x="10" y="30" width="10" height="8" rx="2" fill="white" opacity="0.25"/>
|
||||
<path d="M12.5 35.5l2-2.5 2 1.8 1.5-1 2.5 3h-8z" fill="white" opacity="0.5"/>
|
||||
<!-- Translation line -->
|
||||
<rect x="22" y="32" width="7" height="2" rx="1" fill="white" opacity="0.35"/>
|
||||
<rect x="22" y="35.5" width="5" height="2" rx="1" fill="white" opacity="0.25"/>
|
||||
<!-- Enrichment sparkle burst -->
|
||||
<path d="M40 10l1.6 3.8 3.8 1.6-3.8 1.6L40 20.8l-1.6-3.8L34.6 15.4l3.8-1.6z" fill="#6ee7b7"/>
|
||||
<path d="M37 29l0.9 2.1 2.1 0.9-2.1 0.9L37 35l-0.9-2.1-2.1-0.9 2.1-0.9z" fill="#6ee7b7" opacity="0.5"/>
|
||||
<circle cx="43" cy="25" r="1.2" fill="#34d399" opacity="0.4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
35
docs-site/public/assets/cross-platform.svg
Normal file
@@ -0,0 +1,35 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="cp" x1="4" y1="6" x2="44" y2="42" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#38bdf8"/>
|
||||
<stop offset="1" stop-color="#0284c7"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Linux window (back-left) -->
|
||||
<rect x="2" y="10" width="22" height="16" rx="3" fill="url(#cp)" opacity="0.1"/>
|
||||
<rect x="2" y="10" width="22" height="16" rx="3" stroke="url(#cp)" stroke-width="1.2" fill="none" opacity="0.5"/>
|
||||
<text x="6" y="16" font-size="4" font-weight="700" fill="#38bdf8" opacity="0.6" font-family="monospace">$_</text>
|
||||
<rect x="6" y="19" width="10" height="1.5" rx="0.75" fill="#38bdf8" opacity="0.25"/>
|
||||
<rect x="6" y="22" width="7" height="1.5" rx="0.75" fill="#38bdf8" opacity="0.15"/>
|
||||
<!-- macOS window (center-top) -->
|
||||
<rect x="13" y="4" width="22" height="16" rx="3" fill="url(#cp)" opacity="0.15"/>
|
||||
<rect x="13" y="4" width="22" height="16" rx="3" stroke="url(#cp)" stroke-width="1.2" fill="none" opacity="0.65"/>
|
||||
<circle cx="17" cy="8" r="1.3" fill="#ed8796" opacity="0.7"/>
|
||||
<circle cx="21" cy="8" r="1.3" fill="#eed49f" opacity="0.7"/>
|
||||
<circle cx="25" cy="8" r="1.3" fill="#a6da95" opacity="0.7"/>
|
||||
<rect x="17" y="12" width="12" height="1.5" rx="0.75" fill="#38bdf8" opacity="0.3"/>
|
||||
<rect x="17" y="15" width="8" height="1.5" rx="0.75" fill="#38bdf8" opacity="0.2"/>
|
||||
<!-- Windows window (front-right) -->
|
||||
<rect x="24" y="18" width="22" height="16" rx="3" fill="url(#cp)" opacity="0.12"/>
|
||||
<rect x="24" y="18" width="22" height="16" rx="3" stroke="url(#cp)" stroke-width="1.3" fill="none"/>
|
||||
<!-- Windows title bar buttons -->
|
||||
<rect x="38" y="21" width="2.5" height="1.5" rx="0.5" fill="#38bdf8" opacity="0.4"/>
|
||||
<rect x="41.5" y="21" width="2.5" height="1.5" rx="0.5" fill="#38bdf8" opacity="0.4"/>
|
||||
<rect x="28" y="26" width="13" height="1.5" rx="0.75" fill="#38bdf8" opacity="0.35"/>
|
||||
<rect x="28" y="29" width="9" height="1.5" rx="0.75" fill="#38bdf8" opacity="0.2"/>
|
||||
<!-- Connecting arc (unifying element) -->
|
||||
<path d="M14 38c4-4 16-4 20 0" stroke="url(#cp)" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.4"/>
|
||||
<circle cx="24" cy="40" r="3" fill="url(#cp)" opacity="0.2"/>
|
||||
<circle cx="24" cy="40" r="3" stroke="url(#cp)" stroke-width="1.2" fill="none" opacity="0.5"/>
|
||||
<path d="M22.5 40l1.5 1.5 2.5-3" stroke="#38bdf8" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" fill="none" opacity="0.7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
BIN
docs-site/public/assets/demo-poster.jpg
Normal file
|
After Width: | Height: | Size: 458 KiB |
15
docs-site/public/assets/dual-layer.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="dl" x1="4" y1="24" x2="44" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#818cf8"/>
|
||||
<stop offset="1" stop-color="#6366f1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="4" y="6" width="40" height="14" rx="4" fill="#818cf8" opacity="0.12"/>
|
||||
<rect x="4" y="6" width="40" height="14" rx="4" stroke="#818cf8" stroke-width="1.5" stroke-dasharray="4 3" fill="none" opacity="0.55"/>
|
||||
<rect x="10" y="11" width="20" height="3" rx="1.5" fill="#818cf8" opacity="0.35"/>
|
||||
<line x1="24" y1="22" x2="24" y2="26" stroke="#a5b4fc" stroke-width="1.5" stroke-linecap="round" opacity="0.5"/>
|
||||
<path d="M21.5 24.5L24 27l2.5-2.5" stroke="#a5b4fc" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none" opacity="0.5"/>
|
||||
<rect x="4" y="28" width="40" height="14" rx="4" fill="url(#dl)"/>
|
||||
<rect x="10" y="33" width="20" height="3" rx="1.5" fill="white" opacity="0.85"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
39
docs-site/public/assets/highlight.svg
Normal file
@@ -0,0 +1,39 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="hl-freq" x1="0" y1="0" x2="14" y2="8" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#fbbf24"/>
|
||||
<stop offset="1" stop-color="#f59e0b"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="hl-n1" x1="0" y1="0" x2="10" y2="10" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#60a5fa"/>
|
||||
<stop offset="1" stop-color="#3b82f6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="hl-jlpt" x1="0" y1="0" x2="12" y2="8" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#a78bfa"/>
|
||||
<stop offset="1" stop-color="#7c3aed"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Viewport / video frame background -->
|
||||
<rect x="1" y="5" width="46" height="38" rx="4" fill="#1e293b" opacity="0.55"/>
|
||||
<rect x="1" y="5" width="46" height="38" rx="4" stroke="#334155" stroke-width="0.8" fill="none" opacity="0.5"/>
|
||||
<!-- Subtitle line 1 — tokens with frequency highlight -->
|
||||
<rect x="6" y="18" width="9" height="5" rx="1.5" fill="#cbd5e1" opacity="0.2"/>
|
||||
<!-- Frequency-highlighted token -->
|
||||
<rect x="17" y="17" width="14" height="7" rx="2" fill="url(#hl-freq)" opacity="0.2"/>
|
||||
<rect x="17.5" y="17.5" width="13" height="6" rx="1.8" fill="url(#hl-freq)"/>
|
||||
<rect x="20" y="19.5" width="8" height="2" rx="1" fill="white" opacity="0.85"/>
|
||||
<rect x="33" y="18" width="8" height="5" rx="1.5" fill="#cbd5e1" opacity="0.2"/>
|
||||
<!-- Subtitle line 2 — tokens with N+1 dot and JLPT badge -->
|
||||
<rect x="8" y="28" width="8" height="5" rx="1.5" fill="#cbd5e1" opacity="0.2"/>
|
||||
<!-- N+1 targeted token with dot -->
|
||||
<rect x="18" y="28" width="10" height="5" rx="1.5" fill="#60a5fa" opacity="0.15"/>
|
||||
<rect x="18.5" y="28.5" width="9" height="4" rx="1.2" fill="#cbd5e1" opacity="0.3"/>
|
||||
<circle cx="16.5" cy="30.5" r="2.2" fill="url(#hl-n1)"/>
|
||||
<text x="16.5" y="31.9" text-anchor="middle" font-size="2.6" font-weight="800" fill="white" font-family="sans-serif">+1</text>
|
||||
<!-- JLPT badge token -->
|
||||
<rect x="30" y="28" width="7" height="5" rx="1.5" fill="#cbd5e1" opacity="0.3"/>
|
||||
<rect x="37.5" y="27" width="9" height="7" rx="2" fill="url(#hl-jlpt)"/>
|
||||
<text x="42" y="31.8" text-anchor="middle" font-size="3.5" font-weight="700" fill="white" font-family="sans-serif">N2</text>
|
||||
<!-- Subtle sparkle -->
|
||||
<path d="M43 10l0.7 1.6 1.6 0.7-1.6 0.7L43 14.6l-0.7-1.6-1.6-0.7 1.6-0.7z" fill="#fbbf24" opacity="0.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
23
docs-site/public/assets/jellyfin.svg
Normal file
@@ -0,0 +1,23 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="jf" x1="4" y1="6" x2="44" y2="42" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#a78bfa"/>
|
||||
<stop offset="1" stop-color="#7c3aed"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Server body -->
|
||||
<rect x="6" y="6" width="36" height="14" rx="3.5" fill="url(#jf)" opacity="0.12"/>
|
||||
<rect x="6" y="6" width="36" height="14" rx="3.5" stroke="url(#jf)" stroke-width="1.4" fill="none"/>
|
||||
<!-- Server dots -->
|
||||
<circle cx="12" cy="13" r="2" fill="#a78bfa" opacity="0.6"/>
|
||||
<circle cx="18" cy="13" r="2" fill="#a78bfa" opacity="0.4"/>
|
||||
<!-- Server drive bar -->
|
||||
<rect x="28" y="11.5" width="10" height="3" rx="1.5" fill="#a78bfa" opacity="0.25"/>
|
||||
<!-- Connection line -->
|
||||
<line x1="24" y1="20" x2="24" y2="26" stroke="#a78bfa" stroke-width="1.5" stroke-linecap="round" opacity="0.4"/>
|
||||
<!-- Player screen -->
|
||||
<rect x="6" y="28" width="36" height="14" rx="3.5" fill="url(#jf)" opacity="0.12"/>
|
||||
<rect x="6" y="28" width="36" height="14" rx="3.5" stroke="url(#jf)" stroke-width="1.4" fill="none"/>
|
||||
<!-- Play triangle -->
|
||||
<path d="M21 32l10 3.5-10 3.5z" fill="url(#jf)" opacity="0.8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
31
docs-site/public/assets/keyboard.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="kb-main" x1="2" y1="10" x2="46" y2="42" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#c084fc"/>
|
||||
<stop offset="1" stop-color="#7c3aed"/>
|
||||
</linearGradient>
|
||||
<filter id="kb-glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="2"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<!-- Keyboard body -->
|
||||
<rect x="2" y="14" width="44" height="28" rx="4.5" fill="url(#kb-main)" opacity="0.1"/>
|
||||
<rect x="2" y="14" width="44" height="28" rx="4.5" stroke="url(#kb-main)" stroke-width="1.4" fill="none"/>
|
||||
<!-- Row 1 -->
|
||||
<rect x="6" y="18" width="7" height="5.5" rx="1.8" fill="url(#kb-main)" opacity="0.3"/>
|
||||
<rect x="15" y="18" width="7" height="5.5" rx="1.8" fill="url(#kb-main)" opacity="0.3"/>
|
||||
<rect x="24" y="18" width="7" height="5.5" rx="1.8" fill="url(#kb-main)" opacity="0.3"/>
|
||||
<rect x="33" y="18" width="11" height="5.5" rx="1.8" fill="url(#kb-main)" opacity="0.3"/>
|
||||
<!-- Row 2 — active key with glow -->
|
||||
<rect x="6" y="25.5" width="7" height="5.5" rx="1.8" fill="url(#kb-main)" opacity="0.3"/>
|
||||
<!-- Active/pressed key glow -->
|
||||
<rect x="15" y="25.5" width="7" height="5.5" rx="1.8" fill="#c084fc" opacity="0.25" filter="url(#kb-glow)"/>
|
||||
<rect x="15" y="25.5" width="7" height="5.5" rx="1.8" fill="url(#kb-main)"/>
|
||||
<text x="18.5" y="30" text-anchor="middle" font-size="3.5" font-weight="700" fill="white" font-family="sans-serif">M</text>
|
||||
<rect x="24" y="25.5" width="7" height="5.5" rx="1.8" fill="url(#kb-main)" opacity="0.3"/>
|
||||
<rect x="33" y="25.5" width="11" height="5.5" rx="1.8" fill="url(#kb-main)" opacity="0.3"/>
|
||||
<!-- Row 3 — spacebar -->
|
||||
<rect x="6" y="33" width="7" height="5.5" rx="1.8" fill="url(#kb-main)" opacity="0.3"/>
|
||||
<rect x="15" y="33" width="16" height="5.5" rx="1.8" fill="url(#kb-main)" opacity="0.25"/>
|
||||
<rect x="33" y="33" width="11" height="5.5" rx="1.8" fill="url(#kb-main)" opacity="0.3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
BIN
docs-site/public/assets/kiku-integration-poster.jpg
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
docs-site/public/assets/kiku-integration.mkv
Normal file
BIN
docs-site/public/assets/kiku-integration.mp4
Normal file
BIN
docs-site/public/assets/kiku-integration.webm
Normal file
29
docs-site/public/assets/launcher.svg
Normal file
@@ -0,0 +1,29 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="ln" x1="4" y1="6" x2="44" y2="42" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#34d399"/>
|
||||
<stop offset="1" stop-color="#059669"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Terminal window -->
|
||||
<rect x="4" y="6" width="40" height="36" rx="4" fill="url(#ln)" opacity="0.1"/>
|
||||
<rect x="4" y="6" width="40" height="36" rx="4" stroke="url(#ln)" stroke-width="1.4" fill="none"/>
|
||||
<!-- Title bar dots -->
|
||||
<circle cx="10" cy="11" r="1.5" fill="#34d399" opacity="0.5"/>
|
||||
<circle cx="15" cy="11" r="1.5" fill="#34d399" opacity="0.35"/>
|
||||
<circle cx="20" cy="11" r="1.5" fill="#34d399" opacity="0.2"/>
|
||||
<!-- Divider -->
|
||||
<line x1="4" y1="16" x2="44" y2="16" stroke="#34d399" stroke-width="0.8" opacity="0.2"/>
|
||||
<!-- Prompt + search text -->
|
||||
<text x="9" y="24" font-size="5" font-weight="700" fill="#34d399" opacity="0.7" font-family="monospace">$</text>
|
||||
<rect x="16" y="20.5" width="18" height="4.5" rx="1.5" fill="#34d399" opacity="0.15"/>
|
||||
<!-- File list lines -->
|
||||
<rect x="9" y="28" width="8" height="2" rx="1" fill="#34d399" opacity="0.5"/>
|
||||
<rect x="19" y="28" width="16" height="2" rx="1" fill="#34d399" opacity="0.25"/>
|
||||
<!-- Active/selected line -->
|
||||
<rect x="8" y="32" width="30" height="3" rx="1.5" fill="url(#ln)" opacity="0.35"/>
|
||||
<text x="10" y="34.8" font-size="3.5" font-weight="700" fill="white" opacity="0.85" font-family="monospace">▸</text>
|
||||
<!-- More lines -->
|
||||
<rect x="9" y="37" width="10" height="2" rx="1" fill="#34d399" opacity="0.2"/>
|
||||
<rect x="21" y="37" width="14" height="2" rx="1" fill="#34d399" opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
docs-site/public/assets/minecard-poster.jpg
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
docs-site/public/assets/minecard.gif
Normal file
|
After Width: | Height: | Size: 23 MiB |
BIN
docs-site/public/assets/minecard.jpg
Normal file
|
After Width: | Height: | Size: 303 KiB |
BIN
docs-site/public/assets/minecard.mkv
Normal file
BIN
docs-site/public/assets/minecard.mp4
Normal file
BIN
docs-site/public/assets/minecard.png
Normal file
|
After Width: | Height: | Size: 523 KiB |
BIN
docs-site/public/assets/minecard.webm
Normal file
BIN
docs-site/public/assets/minecard.webp
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
86
docs-site/public/assets/mpv.svg
Normal file
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 63.999999 63.999999"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="mpv.svg">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="5.3710484"
|
||||
inkscape:cx="10.112865"
|
||||
inkscape:cy="18.643164"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1016"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-988.3622)">
|
||||
<circle
|
||||
style="opacity:1;fill:#e5e5e5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.10161044;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:1;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.99215686"
|
||||
id="path4380"
|
||||
cx="32"
|
||||
cy="1020.3622"
|
||||
r="27.949194" />
|
||||
<circle
|
||||
style="opacity:1;fill:#672168;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0988237;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:1;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.99215686"
|
||||
id="path4390"
|
||||
cx="32.727058"
|
||||
cy="1019.5079"
|
||||
r="25.950588" />
|
||||
<circle
|
||||
style="opacity:1;fill:#420143;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:1;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.99215686"
|
||||
id="path4400"
|
||||
cx="34.224396"
|
||||
cy="1017.7957"
|
||||
r="20" />
|
||||
<path
|
||||
style="fill:#dddbdd;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 44.481446,1020.4807 a 12.848894,12.848894 0 0 1 -12.84889,12.8489 12.848894,12.848894 0 0 1 -12.8489,-12.8489 12.848894,12.848894 0 0 1 12.8489,-12.8489 12.848894,12.848894 0 0 1 12.84889,12.8489 z"
|
||||
id="path4412"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#691f69;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 28.374316,1014.709 0,11.4502 9.21608,-5.8647 z"
|
||||
id="path4426"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
35
docs-site/public/assets/subtitle-download.svg
Normal file
@@ -0,0 +1,35 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="sd-main" x1="4" y1="4" x2="44" y2="44" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#22d3ee"/>
|
||||
<stop offset="1" stop-color="#0891b2"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="sd-sync" x1="30" y1="28" x2="46" y2="44" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#34d399"/>
|
||||
<stop offset="1" stop-color="#059669"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Subtitle file -->
|
||||
<rect x="4" y="3" width="26" height="34" rx="3.5" fill="url(#sd-main)" opacity="0.12"/>
|
||||
<rect x="4" y="3" width="26" height="34" rx="3.5" stroke="url(#sd-main)" stroke-width="1.4" fill="none"/>
|
||||
<!-- SRT-style timing line -->
|
||||
<rect x="8.5" y="10" width="10" height="2" rx="1" fill="#22d3ee" opacity="0.35"/>
|
||||
<rect x="20" y="10" width="3" height="2" rx="1" fill="#22d3ee" opacity="0.25"/>
|
||||
<!-- Subtitle text lines -->
|
||||
<rect x="8.5" y="15" width="17" height="2.5" rx="1.25" fill="#22d3ee" opacity="0.6"/>
|
||||
<rect x="8.5" y="20" width="12" height="2.5" rx="1.25" fill="#22d3ee" opacity="0.4"/>
|
||||
<!-- Divider -->
|
||||
<line x1="8.5" y1="25.5" x2="26" y2="25.5" stroke="#22d3ee" stroke-width="0.6" opacity="0.2"/>
|
||||
<!-- Second block timing -->
|
||||
<rect x="8.5" y="28" width="10" height="2" rx="1" fill="#22d3ee" opacity="0.35"/>
|
||||
<!-- Second block text -->
|
||||
<rect x="8.5" y="32.5" width="14" height="2.5" rx="1.25" fill="#22d3ee" opacity="0.4"/>
|
||||
<!-- Download arrow -->
|
||||
<line x1="38" y1="6" x2="38" y2="20" stroke="url(#sd-main)" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path d="M33 16.5l5 5.5 5-5.5" stroke="url(#sd-main)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
<!-- Sync arrows (circular) -->
|
||||
<path d="M35 35a6 6 0 0 1 8.5-1.5" stroke="url(#sd-sync)" stroke-width="1.8" stroke-linecap="round" fill="none"/>
|
||||
<path d="M44.5 35.5l-1-2.8-2.8 1" stroke="url(#sd-sync)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
<path d="M43.5 41a6 6 0 0 1-8.5 1.5" stroke="url(#sd-sync)" stroke-width="1.8" stroke-linecap="round" fill="none"/>
|
||||
<path d="M34 40.5l1 2.8 2.8-1" stroke="url(#sd-sync)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
46
docs-site/public/assets/texthooker.svg
Normal file
@@ -0,0 +1,46 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="th-main" x1="2" y1="6" x2="22" y2="42" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#f97316"/>
|
||||
<stop offset="1" stop-color="#c2410c"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="th-browser" x1="28" y1="6" x2="46" y2="42" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#fb923c"/>
|
||||
<stop offset="1" stop-color="#ea580c"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Source panel (subtitle/text source) -->
|
||||
<rect x="2" y="8" width="18" height="32" rx="3" fill="url(#th-main)" opacity="0.12"/>
|
||||
<rect x="2" y="8" width="18" height="32" rx="3" stroke="url(#th-main)" stroke-width="1.3" fill="none"/>
|
||||
<!-- Subtitle text lines streaming out -->
|
||||
<rect x="5" y="14" width="12" height="2" rx="1" fill="#f97316" opacity="0.6"/>
|
||||
<rect x="5" y="19" width="10" height="2" rx="1" fill="#f97316" opacity="0.5"/>
|
||||
<rect x="5" y="24" width="11" height="2" rx="1" fill="#f97316" opacity="0.4"/>
|
||||
<rect x="5" y="29" width="9" height="2" rx="1" fill="#f97316" opacity="0.35"/>
|
||||
<rect x="5" y="34" width="12" height="2" rx="1" fill="#f97316" opacity="0.3"/>
|
||||
<!-- WebSocket stream particles -->
|
||||
<circle cx="23" cy="18" r="1.2" fill="#fb923c" opacity="0.7"/>
|
||||
<circle cx="25" cy="24" r="1" fill="#fb923c" opacity="0.5"/>
|
||||
<circle cx="23.5" cy="30" r="1.1" fill="#fb923c" opacity="0.4"/>
|
||||
<!-- Connection line (wavy/flowing) -->
|
||||
<path d="M20 15c2-1 4 2 6 1s3-3 5-2" stroke="#fb923c" stroke-width="1" stroke-linecap="round" fill="none" opacity="0.3"/>
|
||||
<path d="M20 24c2.5 0 3 2 5 1.5s3-2.5 5.5-1.5" stroke="#fb923c" stroke-width="1" stroke-linecap="round" fill="none" opacity="0.25"/>
|
||||
<path d="M20 33c2-1 3.5 1.5 5.5 0.5s3-2 5-1" stroke="#fb923c" stroke-width="1" stroke-linecap="round" fill="none" opacity="0.2"/>
|
||||
<!-- Browser window (destination) -->
|
||||
<rect x="28" y="8" width="18" height="32" rx="3" fill="url(#th-browser)" opacity="0.12"/>
|
||||
<rect x="28" y="8" width="18" height="32" rx="3" stroke="url(#th-browser)" stroke-width="1.3" fill="none"/>
|
||||
<!-- Browser chrome dots -->
|
||||
<circle cx="32" cy="12" r="1.2" fill="#f97316" opacity="0.45"/>
|
||||
<circle cx="35.5" cy="12" r="1.2" fill="#f97316" opacity="0.35"/>
|
||||
<circle cx="39" cy="12" r="1.2" fill="#f97316" opacity="0.25"/>
|
||||
<!-- Browser address bar -->
|
||||
<rect x="31" y="15.5" width="12" height="2.5" rx="1.25" fill="#f97316" opacity="0.15"/>
|
||||
<!-- Received text lines in browser -->
|
||||
<rect x="31" y="21" width="11" height="2" rx="1" fill="#fb923c" opacity="0.55"/>
|
||||
<rect x="31" y="25.5" width="9" height="2" rx="1" fill="#fb923c" opacity="0.45"/>
|
||||
<rect x="31" y="30" width="10" height="2" rx="1" fill="#fb923c" opacity="0.35"/>
|
||||
<rect x="31" y="34.5" width="8" height="2" rx="1" fill="#fb923c" opacity="0.25"/>
|
||||
<!-- WS label -->
|
||||
<rect x="21" y="5" width="8" height="5.5" rx="2.5" fill="#c2410c"/>
|
||||
<text x="25" y="9.2" text-anchor="middle" font-size="3.2" font-weight="800" fill="white" font-family="sans-serif">WS</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
34
docs-site/public/assets/tokenization.svg
Normal file
@@ -0,0 +1,34 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="tk-bar" x1="0" y1="40" x2="0" y2="10" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0891b2"/>
|
||||
<stop offset="1" stop-color="#22d3ee"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="tk-glow" x1="4" y1="40" x2="44" y2="10" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#22d3ee" stop-opacity="0.25"/>
|
||||
<stop offset="1" stop-color="#06b6d4" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Subtle grid lines -->
|
||||
<line x1="4" y1="14" x2="44" y2="14" stroke="#22d3ee" stroke-width="0.5" opacity="0.12"/>
|
||||
<line x1="4" y1="22" x2="44" y2="22" stroke="#22d3ee" stroke-width="0.5" opacity="0.12"/>
|
||||
<line x1="4" y1="30" x2="44" y2="30" stroke="#22d3ee" stroke-width="0.5" opacity="0.12"/>
|
||||
<!-- Base line -->
|
||||
<line x1="4" y1="40" x2="44" y2="40" stroke="#0891b2" stroke-width="1" opacity="0.3"/>
|
||||
<!-- Activity bars (daily rollups) -->
|
||||
<rect x="5" y="30" width="4" height="10" rx="1.5" fill="url(#tk-bar)" opacity="0.4"/>
|
||||
<rect x="11" y="24" width="4" height="16" rx="1.5" fill="url(#tk-bar)" opacity="0.55"/>
|
||||
<rect x="17" y="28" width="4" height="12" rx="1.5" fill="url(#tk-bar)" opacity="0.5"/>
|
||||
<rect x="23" y="18" width="4" height="22" rx="1.5" fill="url(#tk-bar)" opacity="0.7"/>
|
||||
<rect x="29" y="22" width="4" height="18" rx="1.5" fill="url(#tk-bar)" opacity="0.6"/>
|
||||
<rect x="35" y="14" width="4" height="26" rx="1.5" fill="url(#tk-bar)"/>
|
||||
<rect x="41" y="20" width="4" height="20" rx="1.5" fill="url(#tk-bar)" opacity="0.65"/>
|
||||
<!-- Trend line -->
|
||||
<polyline points="7,28 13,22 19,25.5 25,16 31,20 37,12 43,18" stroke="#67e8f9" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none" opacity="0.7"/>
|
||||
<!-- Trend dot on peak -->
|
||||
<circle cx="37" cy="12" r="2.2" fill="#22d3ee" opacity="0.6"/>
|
||||
<circle cx="37" cy="12" r="1" fill="white" opacity="0.9"/>
|
||||
<!-- Mini counter badge -->
|
||||
<rect x="33" y="4" width="12" height="7" rx="3.5" fill="#0891b2"/>
|
||||
<text x="39" y="9" text-anchor="middle" font-size="4" font-weight="700" fill="white" font-family="sans-serif">42d</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
12
docs-site/public/assets/video.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="vd" x1="4" y1="10" x2="44" y2="38" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#fb7185"/>
|
||||
<stop offset="1" stop-color="#e11d48"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="4" y="10" width="40" height="28" rx="4" fill="url(#vd)" opacity="0.15"/>
|
||||
<rect x="4" y="10" width="40" height="28" rx="4" stroke="url(#vd)" stroke-width="1.5" fill="none"/>
|
||||
<path d="M20 18l12 6-12 6z" fill="url(#vd)"/>
|
||||
<rect x="10" y="32" width="22" height="2.5" rx="1.25" fill="white" opacity="0.4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 635 B |
1
docs-site/public/assets/yomitan-icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><defs><linearGradient id="a" x1="11.876" x2="4.014" y1="4.073" y2="11.935" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#bc00ff" stop-opacity=".941" style="stop-color:#bc00ff;stop-opacity:1"/><stop offset="1" stop-color="#00b9fe"/></linearGradient></defs><rect width="16" height="16" fill="url(#a)" rx="1.625" ry="1.625"/><path d="M2 2v2h3v3H2v2h3v3H2v2h5V2Zm7 0v2h5V2Zm0 5v2h5V7Zm0 5v2h5v-2z" shape-rendering="crispEdges" style="fill:#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 527 B |
454
docs-site/public/config.example.jsonc
Normal file
@@ -0,0 +1,454 @@
|
||||
/**
|
||||
* SubMiner Example Configuration File
|
||||
*
|
||||
* This file is auto-generated from src/config/definitions.ts.
|
||||
* Copy to %APPDATA%/SubMiner/config.jsonc on Windows, or $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) on Linux/macOS.
|
||||
*/
|
||||
{
|
||||
|
||||
// ==========================================
|
||||
// Overlay Auto-Start
|
||||
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles.
|
||||
// ==========================================
|
||||
"auto_start_overlay": false, // When overlay connects to mpv, automatically show overlay and hide mpv subtitles. Values: true | false
|
||||
|
||||
// ==========================================
|
||||
// Texthooker Server
|
||||
// Configure texthooker startup launch and browser opening behavior.
|
||||
// ==========================================
|
||||
"texthooker": {
|
||||
"launchAtStartup": true, // Launch texthooker server automatically when SubMiner starts. Values: true | false
|
||||
"openBrowser": true // Open browser setting. Values: true | false
|
||||
}, // Configure texthooker startup launch and browser opening behavior.
|
||||
|
||||
// ==========================================
|
||||
// WebSocket Server
|
||||
// Built-in WebSocket server broadcasts subtitle text to connected clients.
|
||||
// Auto mode disables built-in server if mpv_websocket is detected.
|
||||
// ==========================================
|
||||
"websocket": {
|
||||
"enabled": "auto", // Built-in subtitle websocket server mode. Values: auto | true | false
|
||||
"port": 6677 // Built-in subtitle websocket server port.
|
||||
}, // Built-in WebSocket server broadcasts subtitle text to connected clients.
|
||||
|
||||
// ==========================================
|
||||
// Annotation WebSocket
|
||||
// Dedicated annotated subtitle websocket for bundled texthooker and token-aware clients.
|
||||
// Independent from websocket.auto and defaults to port 6678.
|
||||
// ==========================================
|
||||
"annotationWebsocket": {
|
||||
"enabled": true, // Annotated subtitle websocket server enabled state. Values: true | false
|
||||
"port": 6678 // Annotated subtitle websocket server port.
|
||||
}, // Dedicated annotated subtitle websocket for bundled texthooker and token-aware clients.
|
||||
|
||||
// ==========================================
|
||||
// Logging
|
||||
// Controls logging verbosity.
|
||||
// Set to debug for full runtime diagnostics.
|
||||
// ==========================================
|
||||
"logging": {
|
||||
"level": "info" // Minimum log level for runtime logging. Values: debug | info | warn | error
|
||||
}, // Controls logging verbosity.
|
||||
|
||||
// ==========================================
|
||||
// Controller Support
|
||||
// Gamepad support for the visible overlay while keyboard-only mode is active.
|
||||
// Use the selection modal to save a preferred controller by id for future launches.
|
||||
// Trigger input mode can be auto, digital-only, or analog-thresholded depending on the controller.
|
||||
// Override controller.buttonIndices when your pad reports non-standard raw button numbers.
|
||||
// ==========================================
|
||||
"controller": {
|
||||
"enabled": true, // Enable overlay controller support through the Chrome Gamepad API. Values: true | false
|
||||
"preferredGamepadId": "", // Preferred controller id saved from the controller selection modal.
|
||||
"preferredGamepadLabel": "", // Preferred controller display label saved for diagnostics.
|
||||
"smoothScroll": true, // Use smooth scrolling for controller-driven popup scroll input. Values: true | false
|
||||
"scrollPixelsPerSecond": 900, // Base popup scroll speed for controller stick input.
|
||||
"horizontalJumpPixels": 160, // Popup page-jump distance for controller jump input.
|
||||
"stickDeadzone": 0.2, // Deadzone applied to controller stick axes.
|
||||
"triggerInputMode": "auto", // How controller triggers are interpreted: auto, pressed-only, or thresholded analog. Values: auto | digital | analog
|
||||
"triggerDeadzone": 0.5, // Minimum analog trigger value required when trigger input uses auto or analog mode.
|
||||
"repeatDelayMs": 320, // Delay before repeating held controller actions.
|
||||
"repeatIntervalMs": 120, // Repeat interval for held controller actions.
|
||||
"buttonIndices": {
|
||||
"select": 6, // Raw button index used for the controller select/minus/back button.
|
||||
"buttonSouth": 0, // Raw button index used for controller south/A button input.
|
||||
"buttonEast": 1, // Raw button index used for controller east/B button input.
|
||||
"buttonWest": 2, // Raw button index used for controller west/X button input.
|
||||
"buttonNorth": 3, // Raw button index used for controller north/Y button input.
|
||||
"leftShoulder": 4, // Raw button index used for controller left shoulder input.
|
||||
"rightShoulder": 5, // Raw button index used for controller right shoulder input.
|
||||
"leftStickPress": 9, // Raw button index used for controller L3 input.
|
||||
"rightStickPress": 10, // Raw button index used for controller R3 input.
|
||||
"leftTrigger": 6, // Raw button index used for controller L2 input.
|
||||
"rightTrigger": 7 // Raw button index used for controller R2 input.
|
||||
}, // Button indices setting.
|
||||
"bindings": {
|
||||
"toggleLookup": "buttonSouth", // Controller binding for toggling lookup. Values: none | select | buttonSouth | buttonEast | buttonNorth | buttonWest | leftShoulder | rightShoulder | leftStickPress | rightStickPress | leftTrigger | rightTrigger
|
||||
"closeLookup": "buttonEast", // Controller binding for closing lookup. Values: none | select | buttonSouth | buttonEast | buttonNorth | buttonWest | leftShoulder | rightShoulder | leftStickPress | rightStickPress | leftTrigger | rightTrigger
|
||||
"toggleKeyboardOnlyMode": "buttonNorth", // Controller binding for toggling keyboard-only mode. Values: none | select | buttonSouth | buttonEast | buttonNorth | buttonWest | leftShoulder | rightShoulder | leftStickPress | rightStickPress | leftTrigger | rightTrigger
|
||||
"mineCard": "buttonWest", // Controller binding for mining the active card. Values: none | select | buttonSouth | buttonEast | buttonNorth | buttonWest | leftShoulder | rightShoulder | leftStickPress | rightStickPress | leftTrigger | rightTrigger
|
||||
"quitMpv": "select", // Controller binding for quitting mpv. Values: none | select | buttonSouth | buttonEast | buttonNorth | buttonWest | leftShoulder | rightShoulder | leftStickPress | rightStickPress | leftTrigger | rightTrigger
|
||||
"previousAudio": "none", // Controller binding for previous Yomitan audio. Values: none | select | buttonSouth | buttonEast | buttonNorth | buttonWest | leftShoulder | rightShoulder | leftStickPress | rightStickPress | leftTrigger | rightTrigger
|
||||
"nextAudio": "rightShoulder", // Controller binding for next Yomitan audio. Values: none | select | buttonSouth | buttonEast | buttonNorth | buttonWest | leftShoulder | rightShoulder | leftStickPress | rightStickPress | leftTrigger | rightTrigger
|
||||
"playCurrentAudio": "leftShoulder", // Controller binding for playing the current Yomitan audio. Values: none | select | buttonSouth | buttonEast | buttonNorth | buttonWest | leftShoulder | rightShoulder | leftStickPress | rightStickPress | leftTrigger | rightTrigger
|
||||
"toggleMpvPause": "leftStickPress", // Controller binding for toggling mpv play/pause. Values: none | select | buttonSouth | buttonEast | buttonNorth | buttonWest | leftShoulder | rightShoulder | leftStickPress | rightStickPress | leftTrigger | rightTrigger
|
||||
"leftStickHorizontal": "leftStickX", // Axis binding used for left/right token selection. Values: leftStickX | leftStickY | rightStickX | rightStickY
|
||||
"leftStickVertical": "leftStickY", // Axis binding used for primary popup scrolling. Values: leftStickX | leftStickY | rightStickX | rightStickY
|
||||
"rightStickHorizontal": "rightStickX", // Axis binding reserved for alternate right-stick mappings. Values: leftStickX | leftStickY | rightStickX | rightStickY
|
||||
"rightStickVertical": "rightStickY" // Axis binding used for popup page jumps. Values: leftStickX | leftStickY | rightStickX | rightStickY
|
||||
} // Bindings setting.
|
||||
}, // Gamepad support for the visible overlay while keyboard-only mode is active.
|
||||
|
||||
// ==========================================
|
||||
// Startup Warmups
|
||||
// Background warmup controls for MeCab, Yomitan, dictionaries, and Jellyfin session.
|
||||
// Disable individual warmups to defer load until first real usage.
|
||||
// lowPowerMode defers all warmups except Yomitan extension.
|
||||
// ==========================================
|
||||
"startupWarmups": {
|
||||
"lowPowerMode": false, // Defer startup warmups except Yomitan extension. Values: true | false
|
||||
"mecab": true, // Warm up MeCab tokenizer at startup. Values: true | false
|
||||
"yomitanExtension": true, // Warm up Yomitan extension at startup. Values: true | false
|
||||
"subtitleDictionaries": true, // Warm up subtitle dictionaries at startup. Values: true | false
|
||||
"jellyfinRemoteSession": true // Warm up Jellyfin remote session at startup. Values: true | false
|
||||
}, // Background warmup controls for MeCab, Yomitan, dictionaries, and Jellyfin session.
|
||||
|
||||
// ==========================================
|
||||
// Keyboard Shortcuts
|
||||
// Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||
// Hot-reload: shortcut changes apply live and update the session help modal on reopen.
|
||||
// ==========================================
|
||||
"shortcuts": {
|
||||
"toggleVisibleOverlayGlobal": "Alt+Shift+O", // Toggle visible overlay global setting.
|
||||
"copySubtitle": "CommandOrControl+C", // Copy subtitle setting.
|
||||
"copySubtitleMultiple": "CommandOrControl+Shift+C", // Copy subtitle multiple setting.
|
||||
"updateLastCardFromClipboard": "CommandOrControl+V", // Update last card from clipboard setting.
|
||||
"triggerFieldGrouping": "CommandOrControl+G", // Trigger field grouping setting.
|
||||
"triggerSubsync": "Ctrl+Alt+S", // Trigger subsync setting.
|
||||
"mineSentence": "CommandOrControl+S", // Mine sentence setting.
|
||||
"mineSentenceMultiple": "CommandOrControl+Shift+S", // Mine sentence multiple setting.
|
||||
"multiCopyTimeoutMs": 3000, // Timeout for multi-copy/mine modes.
|
||||
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
|
||||
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting.
|
||||
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting.
|
||||
"openJimaku": "Ctrl+Shift+J" // Open jimaku setting.
|
||||
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||
|
||||
// ==========================================
|
||||
// Keybindings (MPV Commands)
|
||||
// Extra keybindings that are merged with built-in defaults.
|
||||
// Set command to null to disable a default keybinding.
|
||||
// Hot-reload: keybinding changes apply live and update the session help modal on reopen.
|
||||
// ==========================================
|
||||
"keybindings": [], // Extra keybindings that are merged with built-in defaults.
|
||||
|
||||
// ==========================================
|
||||
// Secondary Subtitles
|
||||
// Dual subtitle track options.
|
||||
// Used by subminer YouTube subtitle generation as secondary language preferences.
|
||||
// Hot-reload: defaultMode updates live while SubMiner is running.
|
||||
// ==========================================
|
||||
"secondarySub": {
|
||||
"secondarySubLanguages": [], // Secondary sub languages setting.
|
||||
"autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false
|
||||
"defaultMode": "hover" // Default mode setting.
|
||||
}, // Dual subtitle track options.
|
||||
|
||||
// ==========================================
|
||||
// Auto Subtitle Sync
|
||||
// Subsync engine and executable paths.
|
||||
// ==========================================
|
||||
"subsync": {
|
||||
"defaultMode": "auto", // Subsync default mode. Values: auto | manual
|
||||
"alass_path": "", // Alass path setting.
|
||||
"ffsubsync_path": "", // Ffsubsync path setting.
|
||||
"ffmpeg_path": "", // Ffmpeg path setting.
|
||||
"replace": true // Replace the active subtitle file when sync completes. Values: true | false
|
||||
}, // Subsync engine and executable paths.
|
||||
|
||||
// ==========================================
|
||||
// Subtitle Position
|
||||
// Initial vertical subtitle position from the bottom.
|
||||
// ==========================================
|
||||
"subtitlePosition": {
|
||||
"yPercent": 10 // Y percent setting.
|
||||
}, // Initial vertical subtitle position from the bottom.
|
||||
|
||||
// ==========================================
|
||||
// Subtitle Appearance
|
||||
// Primary and secondary subtitle styling.
|
||||
// Hot-reload: subtitle style changes apply live without restarting SubMiner.
|
||||
// ==========================================
|
||||
"subtitleStyle": {
|
||||
"enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false
|
||||
"preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false
|
||||
"autoPauseVideoOnHover": true, // Automatically pause mpv playback while hovering subtitle text, then resume on leave. Values: true | false
|
||||
"autoPauseVideoOnYomitanPopup": false, // Automatically pause mpv playback while Yomitan popup is open, then resume when popup closes. Values: true | false
|
||||
"hoverTokenColor": "#f4dbd6", // Hex color used for hovered subtitle token highlight in mpv.
|
||||
"hoverTokenBackgroundColor": "rgba(54, 58, 79, 0.84)", // CSS color used for hovered subtitle token background highlight in mpv.
|
||||
"nameMatchEnabled": true, // Enable subtitle token coloring for matches from the SubMiner character dictionary. Values: true | false
|
||||
"nameMatchColor": "#f5bde6", // Hex color used when a subtitle token matches an entry from the SubMiner character dictionary.
|
||||
"fontFamily": "M PLUS 1 Medium, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
|
||||
"fontSize": 35, // Font size setting.
|
||||
"fontColor": "#cad3f5", // Font color setting.
|
||||
"fontWeight": "600", // Font weight setting.
|
||||
"lineHeight": 1.35, // Line height setting.
|
||||
"letterSpacing": "-0.01em", // Letter spacing setting.
|
||||
"wordSpacing": 0, // Word spacing setting.
|
||||
"fontKerning": "normal", // Font kerning setting.
|
||||
"textRendering": "geometricPrecision", // Text rendering setting.
|
||||
"textShadow": "0 3px 10px rgba(0,0,0,0.69)", // Text shadow setting.
|
||||
"fontStyle": "normal", // Font style setting.
|
||||
"backgroundColor": "rgb(30, 32, 48, 0.88)", // Background color setting.
|
||||
"backdropFilter": "blur(6px)", // Backdrop filter setting.
|
||||
"nPlusOneColor": "#c6a0f6", // N plus one color setting.
|
||||
"knownWordColor": "#a6da95", // Known word color setting.
|
||||
"jlptColors": {
|
||||
"N1": "#ed8796", // N1 setting.
|
||||
"N2": "#f5a97f", // N2 setting.
|
||||
"N3": "#f9e2af", // N3 setting.
|
||||
"N4": "#a6e3a1", // N4 setting.
|
||||
"N5": "#8aadf4" // N5 setting.
|
||||
}, // Jlpt colors setting.
|
||||
"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).
|
||||
"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`.
|
||||
"bandedColors": [
|
||||
"#ed8796",
|
||||
"#f5a97f",
|
||||
"#f9e2af",
|
||||
"#8bd5ca",
|
||||
"#8aadf4"
|
||||
] // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
|
||||
}, // Frequency dictionary setting.
|
||||
"secondary": {
|
||||
"fontFamily": "Inter, Noto Sans, Helvetica Neue, sans-serif", // Font family setting.
|
||||
"fontSize": 24, // Font size setting.
|
||||
"fontColor": "#cad3f5", // Font color setting.
|
||||
"lineHeight": 1.35, // Line height setting.
|
||||
"letterSpacing": "-0.01em", // Letter spacing setting.
|
||||
"wordSpacing": 0, // Word spacing setting.
|
||||
"fontKerning": "normal", // Font kerning setting.
|
||||
"textRendering": "geometricPrecision", // Text rendering setting.
|
||||
"textShadow": "0 2px 4px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.8), 0 0 16px rgba(0,0,0,0.55)", // Text shadow setting.
|
||||
"backgroundColor": "rgba(20, 22, 34, 0.78)", // Background color setting.
|
||||
"backdropFilter": "blur(6px)", // Backdrop filter setting.
|
||||
"fontWeight": "600", // Font weight setting.
|
||||
"fontStyle": "normal" // Font style setting.
|
||||
} // Secondary setting.
|
||||
}, // Primary and secondary subtitle styling.
|
||||
|
||||
// ==========================================
|
||||
// Shared AI Provider
|
||||
// Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
|
||||
// ==========================================
|
||||
"ai": {
|
||||
"enabled": false, // Enable shared OpenAI-compatible AI provider features. Values: true | false
|
||||
"apiKey": "", // Static API key for the shared OpenAI-compatible AI provider.
|
||||
"apiKeyCommand": "", // Shell command used to resolve the shared AI provider API key.
|
||||
"model": "openai/gpt-4o-mini", // Model setting.
|
||||
"baseUrl": "https://openrouter.ai/api", // Base URL for the shared OpenAI-compatible AI provider.
|
||||
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // System prompt setting.
|
||||
"requestTimeoutMs": 15000 // Timeout in milliseconds for shared AI provider requests.
|
||||
}, // Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
|
||||
|
||||
// ==========================================
|
||||
// AnkiConnect Integration
|
||||
// Automatic Anki updates and media generation options.
|
||||
// Hot-reload: ankiConnect.ai.enabled updates live while SubMiner is running.
|
||||
// Shared AI provider transport settings are read from top-level ai and typically require restart.
|
||||
// Most other AnkiConnect settings still require restart.
|
||||
// ==========================================
|
||||
"ankiConnect": {
|
||||
"enabled": false, // Enable AnkiConnect integration. Values: true | false
|
||||
"url": "http://127.0.0.1:8765", // Url setting.
|
||||
"pollingRate": 3000, // Polling interval in milliseconds.
|
||||
"proxy": {
|
||||
"enabled": true, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false
|
||||
"host": "127.0.0.1", // Bind host for local AnkiConnect proxy.
|
||||
"port": 8766, // Bind port for local AnkiConnect proxy.
|
||||
"upstreamUrl": "http://127.0.0.1:8765" // Upstream AnkiConnect URL proxied by local AnkiConnect proxy.
|
||||
}, // Proxy setting.
|
||||
"tags": [
|
||||
"SubMiner"
|
||||
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
||||
"fields": {
|
||||
"audio": "ExpressionAudio", // Audio setting.
|
||||
"image": "Picture", // Image setting.
|
||||
"sentence": "Sentence", // Sentence setting.
|
||||
"miscInfo": "MiscInfo", // Misc info setting.
|
||||
"translation": "SelectionText" // Translation setting.
|
||||
}, // Fields setting.
|
||||
"ai": {
|
||||
"enabled": false, // Enable AI provider usage for Anki translation/enrichment flows. Values: true | false
|
||||
"model": "", // Optional model override for Anki AI translation/enrichment flows.
|
||||
"systemPrompt": "" // Optional system prompt override for Anki AI translation/enrichment flows.
|
||||
}, // Ai setting.
|
||||
"media": {
|
||||
"generateAudio": true, // Generate audio setting. Values: true | false
|
||||
"generateImage": true, // Generate image setting. Values: true | false
|
||||
"imageType": "static", // Image type setting.
|
||||
"imageFormat": "jpg", // Image format setting.
|
||||
"imageQuality": 92, // Image quality setting.
|
||||
"animatedFps": 10, // Animated fps setting.
|
||||
"animatedMaxWidth": 640, // Animated max width setting.
|
||||
"animatedCrf": 35, // Animated crf setting.
|
||||
"audioPadding": 0.5, // Audio padding setting.
|
||||
"fallbackDuration": 3, // Fallback duration setting.
|
||||
"maxMediaDuration": 30 // Max media duration setting.
|
||||
}, // Media setting.
|
||||
"behavior": {
|
||||
"overwriteAudio": true, // Overwrite audio setting. Values: true | false
|
||||
"overwriteImage": true, // Overwrite image setting. Values: true | false
|
||||
"mediaInsertMode": "append", // Media insert mode setting.
|
||||
"highlightWord": true, // Highlight word setting. Values: true | false
|
||||
"notificationType": "osd", // Notification type setting.
|
||||
"autoUpdateNewCards": true // Automatically update newly added cards. Values: true | false
|
||||
}, // Behavior setting.
|
||||
"nPlusOne": {
|
||||
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
|
||||
"refreshMinutes": 1440, // Minutes between known-word cache refreshes.
|
||||
"matchMode": "headword", // Known-word matching strategy for N+1 highlighting. Values: headword | surface
|
||||
"decks": [], // Decks used for N+1 known-word cache scope. Supports one or more deck names.
|
||||
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3).
|
||||
"nPlusOne": "#c6a0f6", // Color used for the single N+1 target token highlight.
|
||||
"knownWord": "#a6da95" // Color used for legacy known-word highlights.
|
||||
}, // N plus one setting.
|
||||
"metadata": {
|
||||
"pattern": "[SubMiner] %f (%t)" // Pattern setting.
|
||||
}, // Metadata setting.
|
||||
"isLapis": {
|
||||
"enabled": false, // Enabled setting. Values: true | false
|
||||
"sentenceCardModel": "Japanese sentences" // Sentence card model setting.
|
||||
}, // Is lapis setting.
|
||||
"isKiku": {
|
||||
"enabled": false, // Enabled setting. Values: true | false
|
||||
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
|
||||
"deleteDuplicateInAuto": true // Delete duplicate in auto setting. Values: true | false
|
||||
} // Is kiku setting.
|
||||
}, // Automatic Anki updates and media generation options.
|
||||
|
||||
// ==========================================
|
||||
// Jimaku
|
||||
// Jimaku API configuration and defaults.
|
||||
// ==========================================
|
||||
"jimaku": {
|
||||
"apiBaseUrl": "https://jimaku.cc", // Api base url setting.
|
||||
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
|
||||
"maxEntryResults": 10 // Maximum Jimaku search results returned.
|
||||
}, // Jimaku API configuration and defaults.
|
||||
|
||||
// ==========================================
|
||||
// YouTube Subtitle Generation
|
||||
// Defaults for SubMiner YouTube subtitle generation.
|
||||
// ==========================================
|
||||
"youtubeSubgen": {
|
||||
"whisperBin": "", // Path to whisper.cpp CLI used as fallback transcription engine.
|
||||
"whisperModel": "", // Path to whisper model used for fallback transcription.
|
||||
"whisperVadModel": "", // Path to optional whisper VAD model used for subtitle generation.
|
||||
"whisperThreads": 4, // Thread count passed to whisper.cpp subtitle generation runs.
|
||||
"fixWithAi": false, // Use shared AI provider to post-process whisper-generated YouTube subtitles. Values: true | false
|
||||
"ai": {
|
||||
"model": "", // Optional model override for YouTube subtitle AI post-processing.
|
||||
"systemPrompt": "" // Optional system prompt override for YouTube subtitle AI post-processing.
|
||||
}, // Ai setting.
|
||||
"primarySubLanguages": [
|
||||
"ja",
|
||||
"jpn"
|
||||
] // Comma-separated primary subtitle language priority used by the launcher.
|
||||
}, // Defaults for SubMiner YouTube subtitle generation.
|
||||
|
||||
// ==========================================
|
||||
// Anilist
|
||||
// Anilist API credentials and update behavior.
|
||||
// Includes optional auto-sync for a merged MRU-based character dictionary in bundled Yomitan.
|
||||
// Character dictionaries are keyed by AniList media ID (no season/franchise merge).
|
||||
// ==========================================
|
||||
"anilist": {
|
||||
"enabled": false, // Enable AniList post-watch progress updates. Values: true | false
|
||||
"accessToken": "", // Optional explicit AniList access token override; leave empty to use locally stored token from setup.
|
||||
"characterDictionary": {
|
||||
"enabled": false, // Enable automatic Yomitan character dictionary sync for currently watched AniList media. Values: true | false
|
||||
"refreshTtlHours": 168, // Legacy setting; merged character dictionary retention is now usage-based and this value is ignored.
|
||||
"maxLoaded": 3, // Maximum number of most-recently-used anime snapshots included in the merged Yomitan character dictionary.
|
||||
"evictionPolicy": "delete", // Legacy setting; merged character dictionary eviction is usage-based and this value is ignored. Values: disable | delete
|
||||
"profileScope": "all", // Yomitan profile scope for dictionary enable/disable updates. Values: all | active
|
||||
"collapsibleSections": {
|
||||
"description": false, // Open the Description section by default in character dictionary glossary entries. Values: true | false
|
||||
"characterInformation": false, // Open the Character Information section by default in character dictionary glossary entries. Values: true | false
|
||||
"voicedBy": false // Open the Voiced by section by default in character dictionary glossary entries. Values: true | false
|
||||
} // Collapsible sections setting.
|
||||
} // Character dictionary setting.
|
||||
}, // Anilist API credentials and update behavior.
|
||||
|
||||
// ==========================================
|
||||
// Jellyfin
|
||||
// Optional Jellyfin integration for auth, browsing, and playback launch.
|
||||
// Access token is stored in local encrypted token storage after login/setup.
|
||||
// jellyfin.accessToken remains an optional explicit override in config.
|
||||
// ==========================================
|
||||
"jellyfin": {
|
||||
"enabled": false, // Enable optional Jellyfin integration and CLI control commands. Values: true | false
|
||||
"serverUrl": "", // Base Jellyfin server URL (for example: http://localhost:8096).
|
||||
"username": "", // Default Jellyfin username used during CLI login.
|
||||
"deviceId": "subminer", // Device id setting.
|
||||
"clientName": "SubMiner", // Client name setting.
|
||||
"clientVersion": "0.1.0", // Client version setting.
|
||||
"defaultLibraryId": "", // Optional default Jellyfin library ID for item listing.
|
||||
"remoteControlEnabled": true, // Enable Jellyfin remote cast control mode. Values: true | false
|
||||
"remoteControlAutoConnect": true, // Auto-connect to the configured remote control target. Values: true | false
|
||||
"autoAnnounce": false, // When enabled, automatically trigger remote announce/visibility check on websocket connect. Values: true | false
|
||||
"remoteControlDeviceName": "SubMiner", // Device name reported for Jellyfin remote control sessions.
|
||||
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
|
||||
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
|
||||
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
|
||||
"directPlayContainers": [
|
||||
"mkv",
|
||||
"mp4",
|
||||
"webm",
|
||||
"mov",
|
||||
"flac",
|
||||
"mp3",
|
||||
"aac"
|
||||
], // Container allowlist for direct play decisions.
|
||||
"transcodeVideoCodec": "h264" // Preferred transcode video codec when direct play is unavailable.
|
||||
}, // Optional Jellyfin integration for auth, browsing, and playback launch.
|
||||
|
||||
// ==========================================
|
||||
// Discord Rich Presence
|
||||
// Optional Discord Rich Presence activity card updates for current playback/study session.
|
||||
// Uses official SubMiner Discord app assets for polished card visuals.
|
||||
// ==========================================
|
||||
"discordPresence": {
|
||||
"enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false
|
||||
"updateIntervalMs": 3000, // Minimum interval between presence payload updates.
|
||||
"debounceMs": 750 // Debounce delay used to collapse bursty presence updates.
|
||||
}, // Optional Discord Rich Presence activity card updates for current playback/study session.
|
||||
|
||||
// ==========================================
|
||||
// Immersion Tracking
|
||||
// Enable/disable immersion tracking.
|
||||
// Set dbPath to override the default sqlite database location.
|
||||
// Policy tuning is available for queue, flush, and retention values.
|
||||
// ==========================================
|
||||
"immersionTracking": {
|
||||
"enabled": true, // Enable immersion tracking for mined subtitle metadata. Values: true | false
|
||||
"dbPath": "", // Optional SQLite database path for immersion tracking. Empty value uses the default app data path.
|
||||
"batchSize": 25, // Buffered telemetry/event writes per SQLite transaction.
|
||||
"flushIntervalMs": 500, // Max delay before queue flush in milliseconds.
|
||||
"queueCap": 1000, // In-memory write queue cap before overflow policy applies.
|
||||
"payloadCapBytes": 256, // Max JSON payload size per event before truncation.
|
||||
"maintenanceIntervalMs": 86400000, // Maintenance cadence (prune + rollup + vacuum checks).
|
||||
"retention": {
|
||||
"eventsDays": 7, // Raw event retention window in days.
|
||||
"telemetryDays": 30, // Telemetry retention window in days.
|
||||
"dailyRollupsDays": 365, // Daily rollup retention window in days.
|
||||
"monthlyRollupsDays": 1825, // Monthly rollup retention window in days.
|
||||
"vacuumIntervalDays": 7 // Minimum days between VACUUM runs.
|
||||
} // Retention setting.
|
||||
} // Enable/disable immersion tracking.
|
||||
}
|
||||
BIN
docs-site/public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
docs-site/public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
docs-site/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
133
docs-site/shortcuts.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Keyboard Shortcuts
|
||||
|
||||
All shortcuts are configurable in `config.jsonc` under `shortcuts` and `keybindings`. Set any shortcut to `null` to disable it.
|
||||
|
||||
## Global Shortcuts
|
||||
|
||||
These work system-wide regardless of which window has focus.
|
||||
|
||||
| Shortcut | Action | Configurable |
|
||||
| ------------- | ---------------------- | -------------------------------------- |
|
||||
| `Alt+Shift+O` | Toggle visible overlay | `shortcuts.toggleVisibleOverlayGlobal` |
|
||||
| `Alt+Shift+Y` | Open Yomitan settings | Fixed (not configurable) |
|
||||
|
||||
::: tip
|
||||
Global shortcuts are registered with the OS. If they conflict with another application, update them in `shortcuts` config and restart SubMiner.
|
||||
:::
|
||||
|
||||
## Mining Shortcuts
|
||||
|
||||
These work when the overlay window has focus.
|
||||
|
||||
| Shortcut | Action | Config key |
|
||||
| ------------------ | ----------------------------------------------- | --------------------------------------- |
|
||||
| `Ctrl/Cmd+S` | Mine current subtitle as sentence card | `shortcuts.mineSentence` |
|
||||
| `Ctrl/Cmd+Shift+S` | Mine multiple lines (press 1–9 to select count) | `shortcuts.mineSentenceMultiple` |
|
||||
| `Ctrl/Cmd+C` | Copy current subtitle text | `shortcuts.copySubtitle` |
|
||||
| `Ctrl/Cmd+Shift+C` | Copy multiple lines (press 1–9 to select count) | `shortcuts.copySubtitleMultiple` |
|
||||
| `Ctrl/Cmd+V` | Update last Anki card from clipboard text | `shortcuts.updateLastCardFromClipboard` |
|
||||
| `Ctrl/Cmd+G` | Trigger field grouping (Kiku merge check) | `shortcuts.triggerFieldGrouping` |
|
||||
| `Ctrl/Cmd+Shift+A` | Mark last card as audio card | `shortcuts.markAudioCard` |
|
||||
|
||||
The multi-line shortcuts open a digit selector with a 3-second timeout (`shortcuts.multiCopyTimeoutMs`). Press `1`–`9` to select how many recent subtitle lines to combine.
|
||||
|
||||
## Overlay Controls
|
||||
|
||||
These control playback and subtitle display. They require overlay window focus.
|
||||
|
||||
| Shortcut | Action |
|
||||
| -------------------- | -------------------------------------------------- |
|
||||
| `Space` | Toggle mpv pause |
|
||||
| `J` | Cycle primary subtitle track |
|
||||
| `Shift+J` | Cycle secondary subtitle track |
|
||||
| `ArrowRight` | Seek forward 5 seconds |
|
||||
| `ArrowLeft` | Seek backward 5 seconds |
|
||||
| `ArrowUp` | Seek forward 60 seconds |
|
||||
| `ArrowDown` | Seek backward 60 seconds |
|
||||
| `Shift+H` | Jump to previous subtitle |
|
||||
| `Shift+L` | Jump to next subtitle |
|
||||
| `Shift+[` | Shift subtitle delay to previous subtitle cue |
|
||||
| `Shift+]` | Shift subtitle delay to next subtitle cue |
|
||||
| `Ctrl+Shift+H` | Replay current subtitle (play to end, then pause) |
|
||||
| `Ctrl+Shift+L` | Play next subtitle (jump, play to end, then pause) |
|
||||
| `Q` | Quit mpv |
|
||||
| `Ctrl+W` | Quit mpv |
|
||||
| `Right-click` | Toggle pause (outside subtitle area) |
|
||||
| `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.
|
||||
|
||||
Mouse-hover playback behavior is configured separately from shortcuts: `subtitleStyle.autoPauseVideoOnHover` defaults to `true` (pause on subtitle hover, resume on leave).
|
||||
|
||||
## Subtitle & Feature Shortcuts
|
||||
|
||||
| Shortcut | Action | Config key |
|
||||
| ------------------ | -------------------------------------------------------- | ------------------------------ |
|
||||
| `Ctrl/Cmd+Shift+V` | Cycle secondary subtitle mode (hidden → visible → hover) | `shortcuts.toggleSecondarySub` |
|
||||
| `Ctrl/Cmd+Shift+O` | Open runtime options palette | `shortcuts.openRuntimeOptions` |
|
||||
| `Ctrl+Shift+J` | Open Jimaku subtitle search modal | `shortcuts.openJimaku` |
|
||||
| `Ctrl+Alt+S` | Open subtitle sync (subsync) modal | `shortcuts.triggerSubsync` |
|
||||
|
||||
## Controller Shortcuts
|
||||
|
||||
These overlay-local shortcuts are fixed and open controller utilities for the Chrome Gamepad API integration.
|
||||
|
||||
| Shortcut | Action | Configurable |
|
||||
| ------------- | ------------------------------ | ------------ |
|
||||
| `Alt+C` | Open controller selection modal | Fixed |
|
||||
| `Alt+Shift+C` | Open controller debug modal | Fixed |
|
||||
|
||||
Controller input only drives the overlay while keyboard-only mode is enabled. The controller mapping and tuning live under the top-level `controller` config block; keyboard-only mode still works normally without a controller.
|
||||
|
||||
## 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.
|
||||
|
||||
| Chord | Action |
|
||||
| ----- | ------------------------ |
|
||||
| `y-y` | Open SubMiner menu (OSD) |
|
||||
| `y-s` | Start overlay |
|
||||
| `y-S` | Stop overlay |
|
||||
| `y-t` | Toggle visible overlay |
|
||||
| `y-o` | Open Yomitan settings |
|
||||
| `y-r` | Restart overlay |
|
||||
| `y-c` | Check overlay status |
|
||||
|
||||
When the overlay has focus, press `y` then `d` to toggle DevTools (debugging helper).
|
||||
|
||||
## Drag-and-Drop
|
||||
|
||||
| Gesture | Action |
|
||||
| ------------------------- | ------------------------------------------------ |
|
||||
| Drop file(s) onto overlay | Replace current mpv playlist with dropped files |
|
||||
| `Shift` + drop file(s) | Append all dropped files to current mpv playlist |
|
||||
|
||||
## Customizing Shortcuts
|
||||
|
||||
All `shortcuts.*` keys accept [Electron accelerator strings](https://www.electronjs.org/docs/latest/tutorial/keyboard-shortcuts), for example `"CommandOrControl+Shift+M"`. Use `null` to disable a shortcut.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"shortcuts": {
|
||||
"mineSentence": "CommandOrControl+S",
|
||||
"copySubtitle": "CommandOrControl+C",
|
||||
"toggleVisibleOverlayGlobal": "Alt+Shift+O",
|
||||
"openJimaku": null, // disabled
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The `keybindings` array overrides or extends the overlay's built-in key handling for mpv commands:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"keybindings": [
|
||||
{ "key": "f", "command": ["cycle", "fullscreen"] },
|
||||
{ "key": "m", "command": ["cycle", "mute"] },
|
||||
{ "key": "Space", "command": null }, // disable default Space → pause
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Both `shortcuts` and `keybindings` are [hot-reloadable](/configuration#hot-reload-behavior) — changes take effect without restarting SubMiner.
|
||||
133
docs-site/subtitle-annotations.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Subtitle Annotations
|
||||
|
||||
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` and `ankiConnect.nPlusOne` in your config. They apply independently — you can enable any combination.
|
||||
|
||||
## N+1 Word Highlighting
|
||||
|
||||
N+1 highlighting identifies sentences where you know every word except one, making them ideal mining targets. When enabled, SubMiner builds a local cache of your known vocabulary from Anki and highlights tokens accordingly.
|
||||
|
||||
**How it works:**
|
||||
|
||||
1. SubMiner queries your Anki decks for existing `Expression` / `Word` field values.
|
||||
2. The results are cached locally (`known-words-cache.json`) and refreshed on a configurable interval.
|
||||
3. When a subtitle line appears, each token is checked against the cache.
|
||||
4. If exactly one unknown word remains in the sentence, it is highlighted with `nPlusOneColor` (default: `#c6a0f6`).
|
||||
5. Already-known tokens can optionally display in `knownWordColor` (default: `#a6da95`).
|
||||
|
||||
**Key settings:**
|
||||
|
||||
| Option | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `ankiConnect.nPlusOne.highlightEnabled` | `false` | Enable N+1 highlighting |
|
||||
| `ankiConnect.nPlusOne.refreshMinutes` | `60` | Minutes between Anki cache refreshes |
|
||||
| `ankiConnect.nPlusOne.decks` | `[]` | Decks to query (falls back to `ankiConnect.deck`) |
|
||||
| `ankiConnect.nPlusOne.matchMode` | `"headword"` | `"headword"` (dictionary form) or `"surface"` (raw text) |
|
||||
| `ankiConnect.nPlusOne.minSentenceWords` | `3` | Minimum tokens in a sentence for N+1 to trigger |
|
||||
| `subtitleStyle.nPlusOneColor` | `#c6a0f6` | Color for the single unknown target word |
|
||||
| `subtitleStyle.knownWordColor` | `#a6da95` | Color for already-known tokens |
|
||||
|
||||
::: tip
|
||||
Set `refreshMinutes` to `1440` (24 hours) for daily sync if your Anki collection is large.
|
||||
:::
|
||||
|
||||
## 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. Matching names are highlighted in subtitles and become clickable for full character profiles — portraits, roles, voice actors, and biographical detail.
|
||||
|
||||
**How it works:**
|
||||
|
||||
1. Subtitles are tokenized, then candidate name tokens are matched against the character dictionary via Yomitan's scanning pipeline.
|
||||
2. Matching tokens receive a dedicated style distinct from N+1 and frequency layers.
|
||||
3. This layer can be independently toggled with `subtitleStyle.nameMatchEnabled`.
|
||||
|
||||
**Key settings:**
|
||||
|
||||
| Option | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `subtitleStyle.nameMatchEnabled` | `true` | Enable character-name token highlighting |
|
||||
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Color used for character-name matches |
|
||||
|
||||
For full details on dictionary generation, name variant expansion, auto-sync lifecycle, and configuration, see the dedicated [Character Dictionary](/character-dictionary) page.
|
||||
|
||||
## 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.
|
||||
|
||||
**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.
|
||||
|
||||
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.
|
||||
|
||||
**Key settings:**
|
||||
|
||||
| Option | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `subtitleStyle.frequencyDictionary.enabled` | `false` | Enable frequency highlighting |
|
||||
| `subtitleStyle.frequencyDictionary.topX` | `1000` | Max frequency rank to highlight |
|
||||
| `subtitleStyle.frequencyDictionary.mode` | `"single"` | `"single"` or `"banded"` |
|
||||
| `subtitleStyle.frequencyDictionary.matchMode` | `"headword"` | `"headword"` or `"surface"` |
|
||||
| `subtitleStyle.frequencyDictionary.singleColor` | — | Color for single mode |
|
||||
| `subtitleStyle.frequencyDictionary.bandedColors` | — | Array of five hex colors for banded mode |
|
||||
| `subtitleStyle.frequencyDictionary.sourcePath` | — | Custom path to frequency dictionary root |
|
||||
|
||||
When `sourcePath` is omitted, SubMiner searches default install/runtime locations for `frequency-dictionary` directories automatically.
|
||||
|
||||
::: info
|
||||
Frequency highlighting skips tokens that look like non-lexical noise (kana reduplication, short kana endings like `っ`), even when dictionary ranks exist.
|
||||
:::
|
||||
|
||||
## JLPT Tagging
|
||||
|
||||
JLPT tagging adds colored underlines to tokens based on their JLPT level (N1–N5), giving you an at-a-glance sense of difficulty distribution in each subtitle line.
|
||||
|
||||
**How it works:**
|
||||
|
||||
SubMiner loads offline `term_meta_bank_*.json` files from `vendor/yomitan-jlpt-vocab` and matches each token's headword against the bank entries. Tokens with a recognized JLPT level receive a colored underline.
|
||||
|
||||
**Default colors:**
|
||||
|
||||
| Level | Color | Preview |
|
||||
| --- | --- | --- |
|
||||
| N1 | `#ed8796` | Red |
|
||||
| N2 | `#f5a97f` | Peach |
|
||||
| N3 | `#f9e2af` | Yellow |
|
||||
| N4 | `#a6e3a1` | Green |
|
||||
| N5 | `#8aadf4` | Blue |
|
||||
|
||||
All colors are customizable via the `subtitleStyle.jlptColors` object.
|
||||
|
||||
**Key settings:**
|
||||
|
||||
| Option | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `subtitleStyle.enableJlpt` | `false` | Enable JLPT underline styling |
|
||||
| `subtitleStyle.jlptColors.N1`–`N5` | see above | Per-level underline colors |
|
||||
|
||||
::: tip
|
||||
JLPT tagging requires the offline vocabulary bundle. See [JLPT Vocabulary Bundle](jlpt-vocab-bundle) for setup instructions and file locations.
|
||||
:::
|
||||
|
||||
## Runtime Toggles
|
||||
|
||||
All annotation layers can be toggled at runtime via the mpv command menu without restarting:
|
||||
|
||||
- `ankiConnect.nPlusOne.highlightEnabled` (`On` / `Off`)
|
||||
- `subtitleStyle.nameMatchEnabled` (`On` / `Off`)
|
||||
- `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.
|
||||
|
||||
## 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)
|
||||
320
docs-site/troubleshooting.md
Normal file
@@ -0,0 +1,320 @@
|
||||
# Troubleshooting
|
||||
|
||||
Common issues and how to resolve them.
|
||||
|
||||
## MPV Connection
|
||||
|
||||
**Overlay starts but shows no subtitles**
|
||||
|
||||
SubMiner connects to mpv via a Unix socket (or named pipe on Windows). If the socket does not exist or the path does not match, the overlay will appear but subtitles will never arrive.
|
||||
|
||||
- Ensure mpv is running with `--input-ipc-server=/tmp/subminer-socket`.
|
||||
- If you use a custom socket path, set it in both your mpv config and SubMiner config (`mpvSocketPath`).
|
||||
- The `subminer` wrapper script sets the socket automatically when it launches mpv. If you launch mpv yourself, the `--input-ipc-server` flag is required.
|
||||
|
||||
SubMiner retries the connection automatically with increasing delays (200 ms, 500 ms, 1 s, 2 s on first connect; 1 s, 2 s, 5 s, 10 s on reconnect). If mpv exits and restarts, the overlay reconnects without needing a restart.
|
||||
|
||||
## Logging and App Mode
|
||||
|
||||
- Default log output is `info`.
|
||||
- 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.
|
||||
|
||||
## Performance and Resource Impact
|
||||
|
||||
### At a glance
|
||||
|
||||
- Baseline: `SubMiner --start` is usually lightweight for normal playback.
|
||||
- Common spikes come from:
|
||||
- first subtitle parse/tokenization bursts
|
||||
- media generation (`ffmpeg` audio/image and AVIF paths)
|
||||
- media sync and subtitle tooling (`alass`, `ffsubsync`, `whisper` fallback path)
|
||||
- `ankiConnect` enrichment (plus polling overhead when proxy mode is disabled)
|
||||
|
||||
### If playback feels sluggish
|
||||
|
||||
1. Reduce overlay workload:
|
||||
|
||||
- set secondary subtitles hidden:
|
||||
- `secondarySub.defaultMode: "hidden"`
|
||||
- disable optional enrichment:
|
||||
- `subtitleStyle.enableJlpt: false`
|
||||
- `subtitleStyle.frequencyDictionary.enabled: false`
|
||||
|
||||
2. Reduce rendering pressure:
|
||||
|
||||
- lower `subtitleStyle.fontSize`
|
||||
- keep overlay complexity minimal during heavy CPU periods
|
||||
|
||||
3. Reduce media overhead:
|
||||
|
||||
- keep `ankiConnect.media.imageType` set to `static` (avoid animated AVIF unless needed)
|
||||
- lower `ankiConnect.media.imageQuality`
|
||||
- reduce `ankiConnect.media.maxMediaDuration`
|
||||
|
||||
4. Lower integration cost:
|
||||
|
||||
- disable AI translation when not needed (`ankiConnect.ai.enabled: false`)
|
||||
- if needed, run immersion telemetry with lower duration expectations (`immersionTracking.enabled: false` for constrained sessions)
|
||||
- prefer YouTube `--mode automatic` over `preprocess` on low-resource systems
|
||||
|
||||
### Practical low-impact profile
|
||||
|
||||
```json
|
||||
{
|
||||
"subtitleStyle": {
|
||||
"fontSize": 30,
|
||||
"enableJlpt": false,
|
||||
"frequencyDictionary": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"secondarySub": {
|
||||
"defaultMode": "hidden"
|
||||
},
|
||||
"ankiConnect": {
|
||||
"media": {
|
||||
"imageType": "static",
|
||||
"imageQuality": 80,
|
||||
"maxMediaDuration": 12
|
||||
},
|
||||
"ai": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"immersionTracking": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### If usage is still high
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## AnkiConnect
|
||||
|
||||
**"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.
|
||||
|
||||
SubMiner connects to the active Anki endpoint:
|
||||
|
||||
- `ankiConnect.url` (direct mode, default `http://127.0.0.1:8765`)
|
||||
- `http://<ankiConnect.proxy.host>:<ankiConnect.proxy.port>` (proxy mode)
|
||||
|
||||
This error means the active endpoint is unavailable, or (in proxy mode) the proxy cannot reach `ankiConnect.proxy.upstreamUrl`.
|
||||
|
||||
- If you changed the AnkiConnect port, update `ankiConnect.url` (or `ankiConnect.proxy.upstreamUrl` if using proxy mode).
|
||||
- If using external Yomitan/browser clients, confirm they point to your SubMiner proxy URL.
|
||||
|
||||
SubMiner retries with exponential backoff (up to 5 s) and suppresses repeated error logs after 5 consecutive failures. When Anki comes back, you will see "AnkiConnect connection restored".
|
||||
|
||||
**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.
|
||||
|
||||
See [Anki Integration](/anki-integration) for the full field mapping reference.
|
||||
|
||||
**"Update failed" OSD message**
|
||||
|
||||
Shown when SubMiner tries to update a card that no longer exists, or when AnkiConnect rejects the update. Common causes:
|
||||
|
||||
- The card was deleted in Anki between creation and enrichment update.
|
||||
- The note type changed and a mapped field no longer exists.
|
||||
|
||||
## Overlay
|
||||
|
||||
**Overlay does not appear**
|
||||
|
||||
- Confirm SubMiner is running: `SubMiner.AppImage --start` or check for the process.
|
||||
- On Linux, the overlay requires a compositor. Hyprland and Sway are supported natively; X11 requires `xdotool` and `xwininfo`.
|
||||
- On macOS, grant Accessibility permission to SubMiner in System Settings > Privacy & Security > Accessibility.
|
||||
|
||||
**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.
|
||||
- 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.
|
||||
|
||||
**Overlay briefly freezes after a modal/runtime error**
|
||||
|
||||
- Renderer errors now trigger an automatic recovery path. You should see a short toast ("Renderer error recovered. Overlay is still running.").
|
||||
- Recovery closes any open modal and restores click-through/shortcuts automatically without interrupting mpv playback.
|
||||
- If errors keep recurring, toggle the overlay's DevTools using overlay chord `y` then `d` (or global `F12`) and inspect the `renderer overlay recovery` error payload for stack trace + modal/subtitle context.
|
||||
|
||||
**Overlay is on the wrong monitor or position**
|
||||
|
||||
SubMiner positions the overlay by tracking the mpv window. If tracking fails:
|
||||
|
||||
- Hyprland: Ensure `hyprctl` is available.
|
||||
- Sway: Ensure `swaymsg` is available.
|
||||
- X11: Ensure `xdotool` and `xwininfo` are installed.
|
||||
|
||||
If the overlay position is slightly off, right-click and drag on subtitle text to fine-tune the overlay subtitle offset.
|
||||
|
||||
## Yomitan
|
||||
|
||||
If you haven't set up dictionaries yet, see [Yomitan setup](/usage#yomitan-setup) first.
|
||||
|
||||
**"Yomitan extension not found in any search path"**
|
||||
|
||||
SubMiner bundles Yomitan and searches for it in these locations (in order):
|
||||
|
||||
1. `build/yomitan` (local/source build output)
|
||||
2. `<resources>/yomitan` (Electron resources path)
|
||||
3. `/usr/share/SubMiner/yomitan`
|
||||
4. `~/.config/SubMiner/yomitan` (user-data fallback on Linux)
|
||||
|
||||
SubMiner does not load the source tree directly from `vendor/subminer-yomitan`; source builds must produce `build/yomitan` first.
|
||||
|
||||
If you installed from the AppImage and see this error, the package may be incomplete. Re-download the AppImage or place the unpacked Yomitan extension manually in `~/.config/SubMiner/yomitan`.
|
||||
|
||||
**Yomitan popup does not appear when clicking words**
|
||||
|
||||
- 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 --settings`) and confirm at least one dictionary is imported.
|
||||
- If the overlay shows subtitles but words are not clickable, the tokenizer may have failed. See the MeCab section below.
|
||||
|
||||
## MeCab / Tokenization
|
||||
|
||||
**"MeCab not found on system"**
|
||||
|
||||
This is informational, not an error. SubMiner tokenization is driven by Yomitan's internal parser. MeCab availability checks may still run for auxiliary token metadata, but MeCab is not used as a tokenization fallback path.
|
||||
|
||||
To install MeCab:
|
||||
|
||||
- **Arch Linux**: `sudo pacman -S mecab mecab-ipadic`
|
||||
- **Ubuntu/Debian**: `sudo apt install mecab libmecab-dev mecab-ipadic-utf8`
|
||||
- **macOS**: `brew install mecab mecab-ipadic`
|
||||
|
||||
**Words are not segmented correctly**
|
||||
|
||||
Japanese word boundaries depend on Yomitan parser output. If segmentation seems wrong:
|
||||
|
||||
- Verify Yomitan dictionaries are installed and active.
|
||||
- Note that CJK characters without spaces are segmented using parser heuristics, which is not always perfect.
|
||||
|
||||
## Media Generation
|
||||
|
||||
**"FFmpeg not found"**
|
||||
|
||||
SubMiner uses FFmpeg to extract audio clips and generate screenshots. Install it:
|
||||
|
||||
- **Arch Linux**: `sudo pacman -S ffmpeg`
|
||||
- **Ubuntu/Debian**: `sudo apt install ffmpeg`
|
||||
- **macOS**: `brew install ffmpeg`
|
||||
|
||||
Without FFmpeg, card creation still works but audio and image fields will be empty.
|
||||
|
||||
**Audio or screenshot generation hangs**
|
||||
|
||||
Media generation has a 30-second timeout (60 seconds for animated AVIF). If your video file is on a slow network mount or the codec requires software decoding, generation may time out. Try:
|
||||
|
||||
- Using a local copy of the video file.
|
||||
- Reducing `media.imageQuality` or switching from `avif` to `static` image type.
|
||||
- Checking that `media.maxMediaDuration` is not set too high.
|
||||
|
||||
## Shortcuts
|
||||
|
||||
**"Failed to register global shortcut"**
|
||||
|
||||
Global shortcuts (`Alt+Shift+O`, `Alt+Shift+Y`) may conflict with other applications or desktop environment keybindings.
|
||||
|
||||
- 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.
|
||||
|
||||
**Overlay keybindings not working**
|
||||
|
||||
Overlay-local shortcuts (Space, arrow keys, etc.) only work when the overlay window has focus. Click on the overlay or use the global shortcut to toggle it to give it focus.
|
||||
|
||||
## Subtitle Timing
|
||||
|
||||
**"Subtitle timing not found; copy again while playing"**
|
||||
|
||||
This OSD message appears when you try to mine a sentence but SubMiner has no timing data for the current subtitle. Causes:
|
||||
|
||||
- The video is paused and no subtitle has been received yet.
|
||||
- The subtitle track changed and timing data was cleared.
|
||||
- You are using an external subtitle file that mpv has not fully loaded.
|
||||
|
||||
Resume playback and wait for the next subtitle to appear, then try mining again.
|
||||
|
||||
## Subtitle Sync (Subsync)
|
||||
|
||||
**"Configured alass executable not found"**
|
||||
|
||||
Install alass or configure the path:
|
||||
|
||||
- **Arch Linux (AUR)**: `yay -S alass-git`
|
||||
- Set the path: `subsync.alass_path` in your config.
|
||||
|
||||
**"Subtitle synchronization failed"**
|
||||
|
||||
SubMiner tries alass first, then falls back to ffsubsync. If both fail:
|
||||
|
||||
- Ensure the reference subtitle track exists in the video (alass requires a source track).
|
||||
- Check that `ffmpeg` is available (used to extract the internal subtitle track).
|
||||
- Try running the sync tool manually to see detailed error output.
|
||||
|
||||
## Jimaku
|
||||
|
||||
**"Jimaku request failed" or HTTP 429**
|
||||
|
||||
The Jimaku API has rate limits. If you see 429 errors, wait for the retry duration shown in the OSD message and try again. If you have a Jimaku API key, set it in `jimaku.apiKey` or `jimaku.apiKeyCommand` to get higher rate limits.
|
||||
|
||||
## Platform-Specific
|
||||
|
||||
### Linux
|
||||
|
||||
- **Wayland (Hyprland/Sway)**: Window tracking uses compositor-specific commands. If `hyprctl` or `swaymsg` are not on `PATH`, tracking will fail silently.
|
||||
- **X11**: Requires `xdotool` and `xwininfo`. If missing, the overlay cannot track the mpv window position.
|
||||
- **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.
|
||||
|
||||
### 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.
|
||||
|
||||
**Overlay is not transparent or has a visible border**
|
||||
|
||||
Add these window rules to your `hyprland.conf`:
|
||||
|
||||
```ini
|
||||
windowrule = float on, match:class SubMiner
|
||||
windowrule = border_size 0, match:class SubMiner
|
||||
windowrule = xray off override, match:class SubMiner
|
||||
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.
|
||||
|
||||
**Global shortcuts not working**
|
||||
|
||||
On Hyprland, Electron cannot register global shortcuts on its own. You must explicitly pass keybindings to SubMiner using `pass` rules:
|
||||
|
||||
```ini
|
||||
bind = ALT SHIFT, O, pass, class:^(SubMiner)$
|
||||
bind = ALT SHIFT, Y, pass, class:^(SubMiner)$
|
||||
```
|
||||
|
||||
Add a `pass` rule for each global shortcut you configure. The defaults are `Alt+Shift+O` (toggle overlay) and `Alt+Shift+Y` (Yomitan settings). If you remap `shortcuts.toggleVisibleOverlayGlobal` to a different key, update the `pass` rule to match.
|
||||
|
||||
Without these rules, Hyprland intercepts the keypresses before they reach SubMiner, and the shortcuts silently do nothing.
|
||||
|
||||
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/).
|
||||
|
||||
### macOS
|
||||
|
||||
- **Accessibility permission**: Required for window tracking. Grant it in System Settings > Privacy & Security > Accessibility.
|
||||
- **Font rendering**: macOS uses a 0.87x font compensation factor for subtitle alignment between mpv and the overlay. If text alignment looks off, adjust subtitle offset by right-click dragging subtitle text.
|
||||
- **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`
|
||||
310
docs-site/usage.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# Usage
|
||||
|
||||
> [!IMPORTANT]
|
||||
> SubMiner requires the bundled Yomitan instance to have at least one dictionary imported for lookups to work.
|
||||
> See [Yomitan setup](#yomitan-setup) for details.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. SubMiner starts the overlay app in the background
|
||||
2. MPV runs with an IPC socket at `/tmp/subminer-socket`
|
||||
3. The overlay connects and subscribes to subtitle changes
|
||||
4. Subtitles are tokenized with Yomitan's internal parser
|
||||
5. Words are displayed as interactive spans in the overlay
|
||||
6. Hovering or clicking a word triggers Yomitan popup for dictionary lookup
|
||||
7. Optional [subtitle annotations](/subtitle-annotations) (N+1, character-name, frequency, JLPT) highlight useful cues in real time
|
||||
|
||||
There are two ways to use SubMiner:
|
||||
|
||||
| Approach | Use when | How |
|
||||
| -------- | -------- | --- |
|
||||
| **`subminer` script** | You want SubMiner to handle everything — launch mpv, set up the socket, start the overlay. The simplest path. | `subminer video.mkv` |
|
||||
| **MPV plugin** | You launch mpv yourself or from another tool (file manager, Jellyfin, etc.). Requires `--input-ipc-server=/tmp/subminer-socket` in your mpv config. | Use `y` chord keybindings inside mpv |
|
||||
|
||||
You can use both — the plugin provides in-player controls, while the `subminer` script is convenient for direct playback. The `subminer` script runs directly via shebang (no `bun run` needed).
|
||||
|
||||
## Live Config Reload
|
||||
|
||||
While SubMiner is running, it watches your active config file and applies safe updates automatically.
|
||||
|
||||
Live-updated settings:
|
||||
|
||||
- `subtitleStyle`
|
||||
- `keybindings`
|
||||
- `shortcuts`
|
||||
- `secondarySub.defaultMode`
|
||||
- `ankiConnect.ai`
|
||||
|
||||
Invalid config edits are rejected; SubMiner keeps the previous valid runtime config and shows an error notification.
|
||||
For restart-required sections, SubMiner shows a restart-needed notification.
|
||||
|
||||
## Commands
|
||||
|
||||
On Windows, replace `SubMiner.AppImage` with `SubMiner.exe` in the direct packaged-app examples below.
|
||||
|
||||
```bash
|
||||
# Browse and play videos
|
||||
subminer # Current directory (uses fzf)
|
||||
subminer -R # Use rofi instead of fzf
|
||||
subminer -d ~/Videos # Specific directory
|
||||
subminer -r -d ~/Anime # Recursive search
|
||||
subminer video.mkv # Play specific file (default plugin config auto-starts visible overlay)
|
||||
subminer --start video.mkv # Optional explicit overlay start (use when plugin auto_start=no)
|
||||
subminer -S video.mkv # Same as above via --start-overlay
|
||||
subminer https://youtu.be/... # Play a YouTube URL
|
||||
subminer ytsearch:"jp news" # Play first YouTube search result
|
||||
subminer --setup # Open first-run setup popup
|
||||
subminer --log-level debug video.mkv # Enable verbose logs for launch/debugging
|
||||
subminer --log-level warn video.mkv # Set logging level explicitly
|
||||
|
||||
# Options
|
||||
subminer -T video.mkv # Disable texthooker server
|
||||
subminer -b x11 video.mkv # Force X11 backend
|
||||
subminer video.mkv # Uses mpv profile "subminer" by default
|
||||
subminer -p gpu-hq video.mkv # Override mpv profile
|
||||
subminer jellyfin # Open Jellyfin setup window (subcommand form)
|
||||
subminer jellyfin -l --server http://127.0.0.1:8096 --username me --password 'secret'
|
||||
subminer jellyfin --logout # Clear stored Jellyfin token/session data
|
||||
subminer jellyfin -p # Interactive Jellyfin library/item picker + playback
|
||||
subminer jellyfin -d # Jellyfin cast-discovery mode (background tray app)
|
||||
subminer app --stop # Stop background app (including Jellyfin cast broadcast)
|
||||
subminer doctor # Dependency + config + socket diagnostics
|
||||
subminer config path # Print active config path
|
||||
subminer config show # Print active config contents
|
||||
subminer mpv socket # Print active mpv socket path
|
||||
subminer mpv status # Exit 0 if socket is ready, else exit 1
|
||||
subminer mpv idle # Launch detached idle mpv with SubMiner defaults
|
||||
subminer dictionary /path/to/file-or-directory # Generate character dictionary ZIP from target (manual Yomitan import)
|
||||
subminer texthooker # Launch texthooker-only mode
|
||||
subminer app --anilist # Pass args directly to SubMiner binary (example: AniList login flow)
|
||||
subminer yt -o ~/subs https://youtu.be/... # YouTube subcommand: output directory shortcut
|
||||
subminer yt --keep-temp --whisper-bin /path/to/whisper-cli --whisper-model /path/to/model.bin --whisper-vad-model /path/to/ggml-vad.bin https://youtu.be/... # Keep generated subtitle workspace for debugging
|
||||
|
||||
# Direct packaged app control
|
||||
SubMiner.AppImage --background # Start in background (tray + IPC wait, minimal logs)
|
||||
SubMiner.AppImage --start --texthooker # Start overlay with texthooker
|
||||
SubMiner.AppImage --texthooker # Launch texthooker only (no overlay window)
|
||||
SubMiner.AppImage --setup # Open first-run setup popup
|
||||
SubMiner.AppImage --stop # Stop overlay
|
||||
SubMiner.AppImage --start --toggle # Start MPV IPC + toggle visibility
|
||||
SubMiner.AppImage --show-visible-overlay # Force show visible overlay
|
||||
SubMiner.AppImage --hide-visible-overlay # Force hide visible overlay
|
||||
SubMiner.AppImage --start --dev # Enable app/dev mode only
|
||||
SubMiner.AppImage --start --debug # Alias for --dev
|
||||
SubMiner.AppImage --start --log-level debug # Force verbose logging without app/dev mode
|
||||
SubMiner.AppImage --settings # Open Yomitan settings
|
||||
SubMiner.AppImage --jellyfin # Open Jellyfin setup window
|
||||
SubMiner.AppImage --jellyfin-login --jellyfin-server http://127.0.0.1:8096 --jellyfin-username me --jellyfin-password 'secret'
|
||||
SubMiner.AppImage --jellyfin-logout # Clear stored Jellyfin token/session data
|
||||
SubMiner.AppImage --jellyfin-libraries
|
||||
SubMiner.AppImage --jellyfin-items --jellyfin-library-id LIBRARY_ID --jellyfin-search anime --jellyfin-limit 20
|
||||
SubMiner.AppImage --jellyfin-play --jellyfin-item-id ITEM_ID --jellyfin-audio-stream-index 1 --jellyfin-subtitle-stream-index 2 # Requires connected mpv IPC (--start or plugin workflow)
|
||||
SubMiner.AppImage --jellyfin-remote-announce # Force cast-target capability announce + visibility check
|
||||
SubMiner.AppImage --dictionary # Generate character dictionary ZIP for current anime
|
||||
SubMiner.AppImage --help # Show all options
|
||||
```
|
||||
|
||||
### Logging and App Mode
|
||||
|
||||
- `--log-level` controls logger verbosity.
|
||||
- `--dev` and `--debug` are app/dev-mode switches; they are not log-level aliases.
|
||||
- `--background` defaults to quieter logging (`warn`) unless `--log-level` is set.
|
||||
- `--background` launched from a terminal detaches and returns the prompt; stop it with tray Quit or `SubMiner.AppImage --stop` (`SubMiner.exe --stop` on Windows).
|
||||
- Linux desktop launcher starts SubMiner with `--background` by default (via electron-builder `linux.executableArgs`).
|
||||
- On Linux, the app now defaults `safeStorage` to `gnome-libsecret` for encrypted token persistence.
|
||||
Launcher pass-through commands also support `--password-store=<backend>` and forward it to the app when present.
|
||||
Override with e.g. `--password-store=basic_text`.
|
||||
- Use both when needed, for example `SubMiner.AppImage --start --dev --log-level debug` (or `SubMiner.exe --start --dev --log-level debug` on Windows).
|
||||
|
||||
### Windows mpv Shortcut
|
||||
|
||||
If you enabled the optional Windows shortcut during install, SubMiner creates a `SubMiner mpv` shortcut in the Start menu and/or on the desktop. It runs `SubMiner.exe --launch-mpv`, which starts `mpv.exe` with SubMiner's `subminer` profile.
|
||||
|
||||
You can use it three ways:
|
||||
|
||||
- Double-click `SubMiner mpv` to open `mpv` with the SubMiner profile.
|
||||
- Drag a video file onto `SubMiner mpv` to launch that file with the same profile.
|
||||
- Run it directly from Command Prompt or PowerShell with `--launch-mpv`.
|
||||
|
||||
```powershell
|
||||
& "C:\Program Files\SubMiner\SubMiner.exe" --launch-mpv
|
||||
& "C:\Program Files\SubMiner\SubMiner.exe" --launch-mpv "C:\Videos\episode 01.mkv"
|
||||
```
|
||||
|
||||
This flow requires `mpv.exe` to be on `PATH`. If it is installed elsewhere, set `SUBMINER_MPV_PATH` to the full `mpv.exe` path before launching.
|
||||
|
||||
### Launcher Subcommands
|
||||
|
||||
- `subminer jellyfin` / `subminer jf`: Jellyfin-focused workflow aliases.
|
||||
- `subminer yt` / `subminer youtube`: YouTube-focused shorthand flags (`-o`, `--keep-temp`, `--whisper-*`).
|
||||
- `subminer doctor`: health checks for core dependencies and runtime paths.
|
||||
- `subminer config`: config helpers (`path`, `show`).
|
||||
- `subminer mpv`: mpv helpers (`status`, `socket`, `idle`).
|
||||
- `subminer dictionary <path>`: generates a Yomitan-importable character dictionary ZIP from a file/directory target.
|
||||
- `subminer texthooker`: texthooker-only shortcut (same behavior as `--texthooker`).
|
||||
- `subminer app` / `subminer bin`: direct passthrough to the SubMiner binary/AppImage.
|
||||
- Subcommand help pages are available (for example `subminer jellyfin -h`, `subminer yt -h`).
|
||||
|
||||
### First-Run Setup
|
||||
|
||||
SubMiner auto-opens the setup popup on fresh installs when launched with `--start` or `--background` and setup is incomplete.
|
||||
|
||||
You can also open it manually:
|
||||
|
||||
```bash
|
||||
subminer --setup
|
||||
SubMiner.AppImage --setup
|
||||
```
|
||||
|
||||
Setup flow:
|
||||
|
||||
- config file: create the default config directory and prefer `config.jsonc`
|
||||
- plugin status: install or skip the bundled mpv plugin
|
||||
- Yomitan shortcut: open bundled Yomitan settings directly from the setup window
|
||||
- dictionary check: ensure at least one bundled Yomitan dictionary is available
|
||||
- Windows: optionally create or remove `SubMiner mpv` Start Menu/Desktop shortcuts (`SubMiner.exe --launch-mpv`)
|
||||
- refresh: re-check plugin + dictionary state without restarting
|
||||
- `Finish setup` stays disabled until dictionary availability is detected
|
||||
- finish action writes setup completion state and suppresses future auto-open prompts
|
||||
|
||||
AniList character dictionary auto-sync (optional):
|
||||
|
||||
- Enable with `anilist.characterDictionary.enabled=true` in config.
|
||||
- SubMiner syncs the currently watched AniList media into a per-media snapshot, then rebuilds one merged `SubMiner Character Dictionary` from the most recently used snapshots.
|
||||
- Rotation limit defaults to 3 recent media snapshots in that merged dictionary (`maxLoaded`).
|
||||
|
||||
Use subcommands for Jellyfin/YouTube command families (`subminer jellyfin ...`, `subminer yt ...`).
|
||||
Top-level launcher flags like `--jellyfin-*` and `--yt-subgen-*` are intentionally rejected.
|
||||
|
||||
### MPV Profile Example (mpv.conf)
|
||||
|
||||
`subminer` passes the following MPV options directly on launch by default:
|
||||
|
||||
- `--input-ipc-server=/tmp/subminer-socket` (or your configured socket path)
|
||||
- `--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us`
|
||||
- `--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us`
|
||||
- `--sub-auto=fuzzy`
|
||||
- `--sub-file-paths=.;subs;subtitles`
|
||||
- `--sid=auto`
|
||||
- `--secondary-sid=auto`
|
||||
- `--secondary-sub-visibility=no`
|
||||
|
||||
You can define a matching profile in `~/.config/mpv/mpv.conf` for consistency when launching `mpv` manually or from other tools. `subminer` launches with `--profile=subminer` by default (or override with `subminer -p <profile> ...`):
|
||||
|
||||
```ini
|
||||
[subminer]
|
||||
# IPC socket (must match SubMiner config)
|
||||
input-ipc-server=/tmp/subminer-socket
|
||||
|
||||
# Prefer JP/EN audio + subtitle language variants
|
||||
alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us
|
||||
slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us
|
||||
|
||||
# Auto-load external subtitles
|
||||
sub-auto=fuzzy
|
||||
sub-file-paths=.;subs;subtitles
|
||||
|
||||
# Select primary + secondary subtitle tracks automatically
|
||||
sid=auto
|
||||
secondary-sid=auto
|
||||
secondary-sub-visibility=no
|
||||
```
|
||||
|
||||
::: warning
|
||||
`secondary-slang` is not a valid mpv option. Use `slang` with `sid=auto` / `secondary-sid=auto` to set subtitle language preferences.
|
||||
:::
|
||||
|
||||
### Yomitan setup
|
||||
|
||||
SubMiner includes a bundled Yomitan extension for overlay word lookup. This bundled extension is separate from any Yomitan browser extension you may have installed.
|
||||
|
||||
For SubMiner overlay lookups to work, open Yomitan settings (`subminer app --settings` or `SubMiner.AppImage --settings`) and import at least one dictionary in the bundled Yomitan instance.
|
||||
|
||||
If you also use Yomitan in a browser, configure that browser profile separately; it does not inherit dictionaries or settings from the bundled instance.
|
||||
|
||||
### YouTube Playback
|
||||
|
||||
`subminer` accepts direct URLs (for example, YouTube links) and `ytsearch:` targets.
|
||||
For YouTube playback, SubMiner now generates or downloads subtitle tracks before mpv starts, then launches mpv with the resolved subtitle files attached.
|
||||
|
||||
Notes:
|
||||
|
||||
- Install `yt-dlp` so mpv can resolve YouTube streams and subtitle tracks reliably.
|
||||
- For YouTube URLs, `subminer` now generates any missing subtitles before mpv launch.
|
||||
- It probes manual/native YouTube subtitle tracks first, then falls back to local `whisper.cpp` only for missing tracks.
|
||||
- Primary subtitle target languages come from `youtubeSubgen.primarySubLanguages` (defaults to `["ja","jpn"]`).
|
||||
- Secondary target languages come from `secondarySub.secondarySubLanguages` (defaults to English if unset).
|
||||
- Whisper translation fallback currently only supports English secondary targets; non-English secondary targets rely on native/manual subtitle availability.
|
||||
- Optional AI cleanup for whisper-generated subtitles is controlled by `youtubeSubgen.fixWithAi` plus the shared top-level `ai` config (with optional `youtubeSubgen.ai` overrides).
|
||||
- Configure defaults in `$XDG_CONFIG_HOME/SubMiner/config.jsonc` (or `~/.config/SubMiner/config.jsonc`) under `youtubeSubgen`, `secondarySub`, and `ai`.
|
||||
- CLI overrides are available through `subminer yt` / `subminer youtube`:
|
||||
- `-o, --out-dir`
|
||||
- `--keep-temp`
|
||||
- `--whisper-bin`
|
||||
- `--whisper-model`
|
||||
- `--whisper-vad-model`
|
||||
- `--whisper-threads`
|
||||
- `--yt-subgen-audio-format`
|
||||
|
||||
## Controller Support
|
||||
|
||||
SubMiner supports gamepad/controller input for couch-friendly usage via the Chrome Gamepad API. Controller input drives the overlay while keyboard-only mode is enabled.
|
||||
|
||||
### Getting Started
|
||||
|
||||
1. Connect a controller before or after launching SubMiner.
|
||||
2. Enable keyboard-only mode — press `Y` on the controller (default binding) or use the overlay keybinding.
|
||||
3. Use the left stick to navigate subtitle tokens and the right stick to scroll the Yomitan popup.
|
||||
4. Press `A` to look up the selected word, `X` to mine a card, `B` to close the popup.
|
||||
|
||||
By default SubMiner uses the first connected controller. Press `Alt+C` in the overlay to open the controller selection modal and persist your preferred controller across sessions. Press `Alt+Shift+C` to open a live debug modal showing raw axes and button values.
|
||||
|
||||
### Default Button Mapping
|
||||
|
||||
| Button | Action |
|
||||
| ------ | ------ |
|
||||
| `A` (South) | Toggle lookup |
|
||||
| `B` (East) | Close lookup |
|
||||
| `Y` (North) | Toggle keyboard-only mode |
|
||||
| `X` (West) | Mine card |
|
||||
| `L1` | Play current Yomitan audio |
|
||||
| `R1` | Next Yomitan audio track |
|
||||
| `L3` (left stick press) | Toggle mpv pause |
|
||||
| `Select` / `Minus` | Quit mpv |
|
||||
| `L2` / `R2` | Unbound (available for custom bindings) |
|
||||
|
||||
### Analog Controls
|
||||
|
||||
| Input | Action |
|
||||
| ----- | ------ |
|
||||
| Left stick horizontal | Move token selection left/right |
|
||||
| Left stick vertical | Smooth scroll Yomitan popup |
|
||||
| Right stick horizontal | Jump inside popup (horizontal) |
|
||||
| Right stick vertical | Smooth scroll popup (vertical) |
|
||||
| D-pad | Fallback for stick navigation |
|
||||
|
||||
All button and axis mappings are configurable under the `controller` config block. See [Configuration — Controller Support](/configuration#controller-support) for the full options.
|
||||
|
||||
## Keybindings
|
||||
|
||||
See [Keyboard Shortcuts](/shortcuts) for the full reference, including mining shortcuts, overlay controls, and customization.
|
||||
|
||||
**Global shortcuts** (work system-wide):
|
||||
|
||||
| Keybind | Action |
|
||||
| ------------- | ---------------------- |
|
||||
| `Alt+Shift+O` | Toggle visible overlay |
|
||||
| `Alt+Shift+Y` | Open Yomitan settings |
|
||||
|
||||
::: tip
|
||||
`Alt+Shift+Y` is fixed and not configurable. All other shortcuts can be changed under `shortcuts` in your config.
|
||||
:::
|
||||
|
||||
Hovering over subtitle text pauses mpv by default; leaving resumes it. Disable with `subtitleStyle.autoPauseVideoOnHover: false`. To also pause while the Yomitan popup is open, set `subtitleStyle.autoPauseVideoOnYomitanPopup: true`.
|
||||
|
||||
### Drag-and-Drop
|
||||
|
||||
- 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.
|
||||
@@ -9,6 +9,7 @@
|
||||
4. Review `CHANGELOG.md`.
|
||||
5. Run release gate locally:
|
||||
`bun run changelog:check --version <version>`
|
||||
`bun run verify:config-example`
|
||||
`bun run test:fast`
|
||||
`bun run typecheck`
|
||||
6. Commit release prep.
|
||||
|
||||
105
docs/plans/2026-03-10-subminer-change-verification-design.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# SubMiner Change Verification Skill Design
|
||||
|
||||
**Date:** 2026-03-10
|
||||
**Status:** Approved
|
||||
|
||||
## Goal
|
||||
|
||||
Create a SubMiner-specific skill that agents can use to verify code changes with automated checks. The skill must support both targeted regression testing during debugging and pre-handoff verification before final response.
|
||||
|
||||
## Skill Contract
|
||||
|
||||
- **Name:** `subminer-change-verification`
|
||||
- **Trigger:** Use when working in the SubMiner repo and you need to verify code changes actually work, especially for launcher, mpv, plugin, overlay, runtime, Electron, or env-sensitive behavior.
|
||||
- **Default posture:** cheap-first; prefer repo-native tests and narrow lanes before broader or GUI-dependent verification.
|
||||
- **Outputs:**
|
||||
- verification summary
|
||||
- exact commands run
|
||||
- artifact paths for logs, captured summaries, and preserved temp state on failures
|
||||
- skipped lanes and blockers
|
||||
- **Non-goals:**
|
||||
- replacing the repo's native tests
|
||||
- launching real GUI apps for every change
|
||||
- default visual regression or pixel-diff workflows
|
||||
|
||||
## Lane Selection
|
||||
|
||||
The skill chooses lanes from the diff or explicit file list.
|
||||
|
||||
- **`docs`**
|
||||
- For `docs-site/`, `docs/`, and similar documentation-only changes.
|
||||
- Prefer `bun run docs:test` and `bun run docs:build`.
|
||||
- **`config`**
|
||||
- For `src/config/`, config example generation/verification paths, and config-template-sensitive changes.
|
||||
- Prefer `bun run test:config`.
|
||||
- **`core`**
|
||||
- For general source-level changes where type safety and the fast maintained lane are the best cheap signal.
|
||||
- Prefer `bun run typecheck` and `bun run test:fast`.
|
||||
- **`launcher-plugin`**
|
||||
- For `launcher/`, `plugin/subminer/`, plugin gating scripts, and wrapper/mpv routing work.
|
||||
- Prefer `bun run test:launcher:smoke:src` and `bun run test:plugin:src`.
|
||||
- **`runtime-compat`**
|
||||
- For runtime/composition/bundled behavior where dist-sensitive validation matters.
|
||||
- Prefer `bun run build`, `bun run test:runtime:compat`, and `bun run test:smoke:dist`.
|
||||
- **`real-gui`**
|
||||
- Reserved for cases where actual Electron/mpv/window behavior must be validated.
|
||||
- Not part of the default lane set; the classifier marks these changes as candidates so the agent can escalate deliberately.
|
||||
|
||||
## Escalation Rules
|
||||
|
||||
1. Start with the narrowest lane that credibly exercises the changed behavior.
|
||||
2. If a narrow lane fails in a way that suggests broader fallout, expand once.
|
||||
3. If a change touches launcher/mpv/plugin/runtime/overlay/window tracking paths, include the relevant specialized lanes before falling back to broad suites.
|
||||
4. Treat real GUI/mpv verification as opt-in escalation:
|
||||
- use only when cheaper evidence is insufficient
|
||||
- allow for platform/display/permission blockers
|
||||
- report skipped/blocker states explicitly
|
||||
|
||||
## Helper Script Design
|
||||
|
||||
The skill uses two small shell helpers:
|
||||
|
||||
- **`scripts/classify_subminer_diff.sh`**
|
||||
- Accepts explicit paths or discovers local changes from git.
|
||||
- Emits lane suggestions and flags in a simple line-oriented format.
|
||||
- Marks real GUI-sensitive paths as `flag:real-gui-candidate` instead of forcing GUI execution.
|
||||
- **`scripts/verify_subminer_change.sh`**
|
||||
- Creates an artifact directory under `.tmp/skill-verification/<timestamp>/`.
|
||||
- Selects lanes from the classifier unless lanes are supplied explicitly.
|
||||
- Runs repo-native commands in a stable order and captures stdout/stderr per step.
|
||||
- Writes a compact `summary.json` and a human-readable `summary.txt`.
|
||||
- Skips real GUI verification unless explicitly enabled.
|
||||
|
||||
## Artifact Contract
|
||||
|
||||
Each invocation should create:
|
||||
|
||||
- `summary.json`
|
||||
- `summary.txt`
|
||||
- `classification.txt`
|
||||
- `env.txt`
|
||||
- `lanes.txt`
|
||||
- `steps.tsv`
|
||||
- `steps/<step>.stdout.log`
|
||||
- `steps/<step>.stderr.log`
|
||||
|
||||
Failures should preserve the artifact directory and identify the exact failing command and log paths.
|
||||
|
||||
## Agent Workflow
|
||||
|
||||
1. Inspect changed files or requested area.
|
||||
2. Classify the change into verification lanes.
|
||||
3. Run the cheapest sufficient lane set.
|
||||
4. Escalate only if evidence is insufficient.
|
||||
5. Escalate to real GUI/mpv only for actual Electron/mpv/window behavior claims.
|
||||
6. Return a short report with:
|
||||
- pass/fail/skipped per lane
|
||||
- exact commands run
|
||||
- artifact paths
|
||||
- blockers/gaps
|
||||
|
||||
## Initial Implementation Scope
|
||||
|
||||
- Ship the skill entrypoint plus the classifier/verifier helpers.
|
||||
- Make real GUI verification an explicit future hook rather than a default workflow.
|
||||
- Verify the new skill locally with representative classifier output and artifact generation.
|
||||
111
docs/plans/2026-03-10-subminer-scrum-master-design.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# SubMiner Scrum Master Skill Design
|
||||
|
||||
**Date:** 2026-03-10
|
||||
**Status:** Approved
|
||||
|
||||
## Goal
|
||||
|
||||
Create a repo-local skill that can take incoming requests, bugs, or issues in the SubMiner repo, decide whether backlog tracking is warranted, create or update backlog work when appropriate, plan the implementation, dispatch one or more subagents, and ensure verification happens before handoff.
|
||||
|
||||
## Skill Contract
|
||||
|
||||
- **Name:** `subminer-scrum-master`
|
||||
- **Location:** `.agents/skills/subminer-scrum-master/`
|
||||
- **Use when:** the user gives a feature request, bug report, issue, refactor, or implementation ask in the SubMiner repo and the agent should own intake, planning, backlog hygiene, dispatch, and completion flow.
|
||||
- **Responsibilities:**
|
||||
- assess whether backlog tracking is warranted
|
||||
- if needed, search/update/create proper backlog structure
|
||||
- write a plan before dispatching coding work
|
||||
- choose sequential vs parallel execution
|
||||
- assign explicit ownership to workers
|
||||
- require verification before final handoff
|
||||
- **Limits:**
|
||||
- not the default code implementer unless delegation would be wasteful
|
||||
- no overlapping parallel write scopes
|
||||
- no skipping planning before dispatch
|
||||
- no skipping verification
|
||||
- must pause for ambiguous, risky, or external side-effect work
|
||||
|
||||
## Backlog Decision Rules
|
||||
|
||||
Backlog use is conditional, not mandatory.
|
||||
|
||||
- **Skip backlog when:**
|
||||
- question only
|
||||
- obvious mechanical edit
|
||||
- tiny isolated change with no real planning
|
||||
- **Use backlog when:**
|
||||
- implementation requires planning
|
||||
- scope/approach needs decisions
|
||||
- multiple phases or subsystems
|
||||
- likely subagent dispatch
|
||||
- work should remain traceable
|
||||
|
||||
When backlog is used:
|
||||
- search first
|
||||
- update existing matching work when appropriate
|
||||
- otherwise create standalone task or parent task
|
||||
- use parent + subtasks for multi-part work
|
||||
- record the implementation plan before coding starts
|
||||
|
||||
## Orchestration Policy
|
||||
|
||||
The skill orchestrates; workers implement.
|
||||
|
||||
- **No dispatch** for trivial/mechanical work
|
||||
- **Single worker** for focused single-scope work
|
||||
- **Parallel workers** only for clearly disjoint scopes
|
||||
- **Sequential flow** for shared files, runtime coupling, or unclear boundaries
|
||||
|
||||
Every worker should receive:
|
||||
- owned files/modules
|
||||
- explicit reminder not to revert unrelated edits
|
||||
- requirement to report changed files, tests run, and blockers
|
||||
|
||||
## Verification Policy
|
||||
|
||||
Every nontrivial code task gets verification.
|
||||
|
||||
- prefer `subminer-change-verification`
|
||||
- use cheap-first lanes
|
||||
- escalate only when needed
|
||||
- accept worker-run verification only if it is clearly relevant and sufficient
|
||||
- run a consolidating final verification pass when the scrum master needs stronger evidence
|
||||
|
||||
## Representative Flows
|
||||
|
||||
### Trivial fix, no backlog
|
||||
|
||||
1. assess request as mechanical or narrowly reversible
|
||||
2. skip backlog
|
||||
3. keep a short internal plan
|
||||
4. implement directly or use one worker if helpful
|
||||
5. run targeted verification
|
||||
6. report concise summary
|
||||
|
||||
### Single-task implementation
|
||||
|
||||
1. search backlog
|
||||
2. create/update one task
|
||||
3. record plan
|
||||
4. dispatch one worker
|
||||
5. integrate result
|
||||
6. run verification
|
||||
7. update task and report outcome
|
||||
|
||||
### Multi-part feature
|
||||
|
||||
1. search backlog
|
||||
2. create/update parent task
|
||||
3. create subtasks for distinct deliverables/phases
|
||||
4. record sequencing in the plan
|
||||
5. dispatch workers only where write scopes do not overlap
|
||||
6. integrate
|
||||
7. run consolidated verification
|
||||
8. update task state and report outcome
|
||||
|
||||
## V1 Scope
|
||||
|
||||
- instruction-heavy `SKILL.md`
|
||||
- no helper scripts unless orchestration becomes too repetitive
|
||||
- strong coordination with existing Backlog workflow and `subminer-change-verification`
|
||||
110
docs/plans/2026-03-11-overlay-controller-support-design.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Overlay Controller Support Design
|
||||
|
||||
**Date:** 2026-03-11
|
||||
**Backlog:** `TASK-159`
|
||||
|
||||
## Goal
|
||||
|
||||
Add controller support to the visible overlay through the Chrome Gamepad API without replacing the existing keyboard-only workflow. Controller input should only supplement keyboard-only mode, preserve existing behavior, and expose controller selection plus raw-input debugging in overlay-local modals.
|
||||
|
||||
## Scope
|
||||
|
||||
- Poll connected gamepads from the visible overlay renderer.
|
||||
- Default to the first connected controller unless config specifies a preferred controller.
|
||||
- Add logical controller bindings and tuning knobs to config.
|
||||
- Add `Alt+C` controller selection modal.
|
||||
- Add `Alt+Shift+C` controller debug modal.
|
||||
- Map controller actions onto existing keyboard-only/Yomitan behaviors.
|
||||
- Fix stale selected-token highlight cleanup when keyboard-only mode turns off or popup closes.
|
||||
|
||||
Out of scope for this pass:
|
||||
|
||||
- Raw arbitrary axis/button index remapping in config.
|
||||
- Controller support outside the visible overlay renderer.
|
||||
- Haptics or vibration.
|
||||
|
||||
## Architecture
|
||||
|
||||
Use a renderer-local controller runtime. The overlay already owns keyboard-only token selection, Yomitan popup integration, and modal UX, and the Gamepad API is browser-native. A renderer module can poll `navigator.getGamepads()` on animation frames, normalize sticks/buttons into logical actions, and call the same helpers used by keyboard-only mode.
|
||||
|
||||
Avoid synthetic keyboard events as the primary implementation. Analog sticks need deadzones, continuous smooth scrolling, and per-action repeat behavior that do not fit cleanly into key event emulation. Direct logical actions keep tests clear and make the debug modal show the exact values the runtime uses.
|
||||
|
||||
## Behavior
|
||||
|
||||
Controller actions are active only while keyboard-only mode is enabled, except the controller action that toggles keyboard-only mode can always fire so the user can enter the mode from the controller.
|
||||
|
||||
Default logical mappings:
|
||||
|
||||
- left stick vertical: smooth Yomitan popup/window scroll when popup is open
|
||||
- left stick horizontal: move token selection left/right
|
||||
- right stick vertical: smooth Yomitan popup/window scroll
|
||||
- right stick horizontal: jump horizontally inside Yomitan popup/window
|
||||
- `A`: toggle lookup
|
||||
- `B`: close lookup
|
||||
- `Y`: toggle keyboard-only mode
|
||||
- `X`: mine card
|
||||
- `L1` / `R1`: previous / next Yomitan audio
|
||||
- `R2`: activate current Yomitan audio button
|
||||
- `L2`: toggle mpv play/pause
|
||||
|
||||
Selection-highlight cleanup:
|
||||
|
||||
- disabling keyboard-only mode clears the selected token class immediately
|
||||
- closing the Yomitan popup also clears the selected token class if keyboard-only mode is no longer active
|
||||
- helper ownership should live in the shared keyboard-only selection sync path so keyboard and controller exits stay consistent
|
||||
|
||||
## Config
|
||||
|
||||
Add a top-level `controller` block in resolved config with:
|
||||
|
||||
- `enabled`
|
||||
- `preferredGamepadId`
|
||||
- `preferredGamepadLabel`
|
||||
- `smoothScroll`
|
||||
- `scrollPixelsPerSecond`
|
||||
- `horizontalJumpPixels`
|
||||
- `stickDeadzone`
|
||||
- `triggerDeadzone`
|
||||
- `repeatDelayMs`
|
||||
- `repeatIntervalMs`
|
||||
- `bindings` logical fields for the named actions/sticks
|
||||
|
||||
Persist the preferred controller by stable browser-exposed `id` when possible, with label stored as a diagnostic/display fallback.
|
||||
|
||||
## UI
|
||||
|
||||
Controller selection modal:
|
||||
|
||||
- overlay-hosted modal in the visible renderer
|
||||
- lists currently connected controllers
|
||||
- highlights current active choice
|
||||
- selecting one persists config and makes it the active controller immediately if connected
|
||||
|
||||
Controller debug modal:
|
||||
|
||||
- overlay-hosted modal
|
||||
- shows selected controller and all connected controllers
|
||||
- live raw axis array values
|
||||
- live raw button values, pressed flags, and touched flags if available
|
||||
|
||||
## Testing
|
||||
|
||||
Test first:
|
||||
|
||||
- controller gating outside keyboard-only mode
|
||||
- logical mapping to existing helpers
|
||||
- continuous stick scroll and repeat behavior
|
||||
- modal open shortcuts
|
||||
- preferred-controller selection persistence
|
||||
- highlight cleanup on keyboard-only disable and popup close
|
||||
- config defaults/parse/template generation coverage
|
||||
|
||||
## Risks
|
||||
|
||||
- Browser gamepad identity strings can differ across OS/browser/runtime versions.
|
||||
Mitigation: match by exact preferred id first; fall back to first connected controller.
|
||||
- Continuous stick input can spam actions.
|
||||
Mitigation: deadzones plus repeat throttling and frame-time-based smooth scroll.
|
||||
- Popup DOM/audio controls may vary.
|
||||
Mitigation: target stable Yomitan popup/document selectors and cover with focused renderer tests.
|
||||
|
||||
245
docs/plans/2026-03-11-overlay-controller-support.md
Normal file
@@ -0,0 +1,245 @@
|
||||
# Overlay Controller Support Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Add Chrome Gamepad API controller support to the visible overlay as a supplement to keyboard-only mode, including controller selection/debug modals, config-backed logical bindings, and selected-token highlight cleanup.
|
||||
|
||||
**Architecture:** Keep controller support in the visible overlay renderer. Poll and normalize gamepad state in a dedicated runtime, route logical actions into the existing keyboard-only/Yomitan helpers, and persist preferred-controller config through the existing config pipeline and preload bridge.
|
||||
|
||||
**Tech Stack:** TypeScript, Bun tests, Electron preload IPC, renderer DOM modals, Chrome Gamepad API
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Track work and lock the design
|
||||
|
||||
**Files:**
|
||||
- Create: `backlog/tasks/task-159 - Add-overlay-controller-support-for-keyboard-only-mode.md`
|
||||
- Create: `docs/plans/2026-03-11-overlay-controller-support-design.md`
|
||||
- Create: `docs/plans/2026-03-11-overlay-controller-support.md`
|
||||
|
||||
**Step 1: Record the approved scope**
|
||||
|
||||
Capture controller-only-in-keyboard-mode behavior, the modal shortcuts, config scope, and the stale selection-highlight cleanup requirement.
|
||||
|
||||
**Step 2: Verify the written scope matches the approved design**
|
||||
|
||||
Run: `sed -n '1,220p' backlog/tasks/task-159\\ -\\ Add-overlay-controller-support-for-keyboard-only-mode.md && sed -n '1,240p' docs/plans/2026-03-11-overlay-controller-support-design.md`
|
||||
|
||||
Expected: task and design doc both mention controller selection/debug modals and highlight cleanup.
|
||||
|
||||
### Task 2: Add failing config tests and defaults
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/config/config.test.ts`
|
||||
- Modify: `src/config/definitions/defaults-core.ts`
|
||||
- Modify: `src/config/definitions/options-core.ts`
|
||||
- Modify: `src/config/definitions/template-sections.ts`
|
||||
- Modify: `src/types.ts`
|
||||
- Modify: `config.example.jsonc`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Add coverage asserting a new `controller` config block resolves with the expected defaults and accepts logical-field overrides.
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `bun test src/config/config.test.ts`
|
||||
|
||||
Expected: FAIL because `controller` config is not defined yet.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Add the controller config types/defaults/registry/template wiring and regenerate the example config if needed.
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `bun test src/config/config.test.ts`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
### Task 3: Add failing keyboard-selection cleanup tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/renderer/handlers/keyboard.test.ts`
|
||||
- Modify: `src/renderer/handlers/keyboard.ts`
|
||||
- Modify: `src/renderer/state.ts`
|
||||
|
||||
**Step 1: Write the failing tests**
|
||||
|
||||
Add tests for:
|
||||
|
||||
- turning keyboard-only mode off clears `.keyboard-selected`
|
||||
- closing the popup clears stale selection highlight when keyboard-only mode is off
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `bun test src/renderer/handlers/keyboard.test.ts`
|
||||
|
||||
Expected: FAIL because selection cleanup is incomplete today.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Centralize selection clearing in the keyboard-only sync helpers and popup-close flow.
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `bun test src/renderer/handlers/keyboard.test.ts`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
### Task 4: Add failing controller runtime tests
|
||||
|
||||
**Files:**
|
||||
- Create: `src/renderer/handlers/gamepad-controller.test.ts`
|
||||
- Create: `src/renderer/handlers/gamepad-controller.ts`
|
||||
- Modify: `src/renderer/context.ts`
|
||||
- Modify: `src/renderer/state.ts`
|
||||
- Modify: `src/renderer/renderer.ts`
|
||||
|
||||
**Step 1: Write the failing tests**
|
||||
|
||||
Cover:
|
||||
|
||||
- first connected controller is selected by default
|
||||
- preferred controller wins when connected
|
||||
- controller actions are ignored unless keyboard-only mode is enabled, except keyboard-only toggle
|
||||
- stick/button mappings invoke the expected logical helpers
|
||||
- smooth scroll and repeat throttling behavior
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `bun test src/renderer/handlers/gamepad-controller.test.ts`
|
||||
|
||||
Expected: FAIL because controller runtime does not exist.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Add a renderer-local polling runtime with deadzone handling, action edge detection, repeat timing, and helper callbacks into the keyboard/Yomitan flow.
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `bun test src/renderer/handlers/gamepad-controller.test.ts`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
### Task 5: Add failing controller modal tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/renderer/index.html`
|
||||
- Modify: `src/renderer/style.css`
|
||||
- Create: `src/renderer/modals/controller-select.ts`
|
||||
- Create: `src/renderer/modals/controller-select.test.ts`
|
||||
- Create: `src/renderer/modals/controller-debug.ts`
|
||||
- Create: `src/renderer/modals/controller-debug.test.ts`
|
||||
- Modify: `src/renderer/renderer.ts`
|
||||
- Modify: `src/renderer/context.ts`
|
||||
- Modify: `src/renderer/state.ts`
|
||||
|
||||
**Step 1: Write the failing tests**
|
||||
|
||||
Add tests for:
|
||||
|
||||
- `Alt+C` opens controller selection modal
|
||||
- `Alt+Shift+C` opens controller debug modal
|
||||
- selection modal renders connected controllers and persists the chosen device
|
||||
- debug modal shows live axes/buttons state
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `bun test src/renderer/modals/controller-select.test.ts src/renderer/modals/controller-debug.test.ts`
|
||||
|
||||
Expected: FAIL because modals and shortcuts do not exist.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Add modal DOM, renderer modules, modal state wiring, and controller runtime integration.
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `bun test src/renderer/modals/controller-select.test.ts src/renderer/modals/controller-debug.test.ts`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
### Task 6: Persist controller preference through preload/main wiring
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/preload.ts`
|
||||
- Modify: `src/types.ts`
|
||||
- Modify: `src/shared/ipc/contracts.ts`
|
||||
- Modify: `src/core/services/ipc.ts`
|
||||
- Modify: `src/main.ts`
|
||||
- Modify: related main/runtime tests as needed
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Add coverage for reading current controller config and saving preferred-controller changes from the renderer.
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `bun test src/core/services/ipc.test.ts`
|
||||
|
||||
Expected: FAIL because no controller preference IPC exists yet.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Expose renderer-safe getters/setters for the controller config fields needed by the selection modal/runtime.
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `bun test src/core/services/ipc.test.ts`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
### Task 7: Update docs and config example
|
||||
|
||||
**Files:**
|
||||
- Modify: `config.example.jsonc`
|
||||
- Modify: `README.md`
|
||||
- Modify: relevant docs under `docs-site/` for shortcuts/usage/troubleshooting if touched by current docs structure
|
||||
|
||||
**Step 1: Write the failing doc/config check if needed**
|
||||
|
||||
If config example generation is covered by tests, add/refresh the failing assertion first.
|
||||
|
||||
**Step 2: Implement the docs**
|
||||
|
||||
Document controller behavior, modal shortcuts, config block, and the keyboard-only-only activation rule.
|
||||
|
||||
**Step 3: Run doc/config verification**
|
||||
|
||||
Run: `bun run test:config`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
### Task 8: Run the handoff gate and update the backlog task
|
||||
|
||||
**Files:**
|
||||
- Modify: `backlog/tasks/task-159 - Add-overlay-controller-support-for-keyboard-only-mode.md`
|
||||
|
||||
**Step 1: Run targeted verification**
|
||||
|
||||
Run:
|
||||
|
||||
- `bun test src/config/config.test.ts`
|
||||
- `bun test src/renderer/handlers/keyboard.test.ts`
|
||||
- `bun test src/renderer/handlers/gamepad-controller.test.ts`
|
||||
- `bun test src/renderer/modals/controller-select.test.ts`
|
||||
- `bun test src/renderer/modals/controller-debug.test.ts`
|
||||
- `bun test src/core/services/ipc.test.ts`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
**Step 2: Run broader gate**
|
||||
|
||||
Run:
|
||||
|
||||
- `bun run typecheck`
|
||||
- `bun run test:fast`
|
||||
- `bun run test:env`
|
||||
- `bun run build`
|
||||
|
||||
Expected: PASS, or document exact blockers/failures.
|
||||
|
||||
**Step 3: Update backlog notes**
|
||||
|
||||
Fill in implementation notes, verification commands, and final summary in `TASK-159`.
|
||||
13
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "subminer",
|
||||
"version": "0.5.6",
|
||||
"version": "0.6.0",
|
||||
"description": "All-in-one sentence mining overlay with AnkiConnect and dictionary integration",
|
||||
"packageManager": "bun@1.3.5",
|
||||
"main": "dist/main-entry.js",
|
||||
@@ -24,8 +24,12 @@
|
||||
"format:check": "prettier --check .",
|
||||
"format:src": "bash scripts/prettier-scope.sh --write",
|
||||
"format:check:src": "bash scripts/prettier-scope.sh --check",
|
||||
"test:config:src": "bun test src/config/config.test.ts src/config/path-resolution.test.ts src/config/resolve/anki-connect.test.ts src/config/resolve/subtitle-style.test.ts src/config/resolve/jellyfin.test.ts src/config/definitions/domain-registry.test.ts src/generate-config-example.test.ts",
|
||||
"test:config:dist": "bun test dist/config/config.test.js dist/config/path-resolution.test.js dist/config/resolve/anki-connect.test.js dist/config/resolve/subtitle-style.test.js dist/config/resolve/jellyfin.test.js dist/config/definitions/domain-registry.test.js dist/generate-config-example.test.js",
|
||||
"docs:dev": "bun run --cwd docs-site docs:dev",
|
||||
"docs:build": "bun run --cwd docs-site docs:build",
|
||||
"docs:preview": "bun run --cwd docs-site docs:preview",
|
||||
"docs:test": "bun run --cwd docs-site test",
|
||||
"test:config:src": "bun test src/config/config.test.ts src/config/path-resolution.test.ts src/config/resolve/anki-connect.test.ts src/config/resolve/subtitle-style.test.ts src/config/resolve/jellyfin.test.ts src/config/definitions/domain-registry.test.ts src/generate-config-example.test.ts src/verify-config-example.test.ts",
|
||||
"test:config:dist": "bun test dist/config/config.test.js dist/config/path-resolution.test.js dist/config/resolve/anki-connect.test.js dist/config/resolve/subtitle-style.test.js dist/config/resolve/jellyfin.test.js dist/config/definitions/domain-registry.test.js dist/generate-config-example.test.js dist/verify-config-example.test.js",
|
||||
"test:config:smoke:dist": "bun test dist/config/path-resolution.test.js",
|
||||
"test:plugin:src": "lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-binary-windows.lua",
|
||||
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
||||
@@ -51,7 +55,8 @@
|
||||
"test:core": "bun run test:core:src",
|
||||
"test:subtitle": "bun run test:subtitle:src",
|
||||
"test:fast": "bun run test:config:src && bun run test:core:src && bun test src/main-entry-runtime.test.ts src/anki-integration/anki-connect-proxy.test.ts src/release-workflow.test.ts src/ci-workflow.test.ts scripts/build-changelog.test.ts scripts/mkv-to-readme-video.test.ts && bun run tsc && bun test dist/main/runtime/registry.test.js",
|
||||
"generate:config-example": "bun run build && bun dist/generate-config-example.js",
|
||||
"generate:config-example": "bun run src/generate-config-example.ts",
|
||||
"verify:config-example": "bun run src/verify-config-example.ts",
|
||||
"start": "bun run build && electron . --start",
|
||||
"dev": "bun run build && electron . --start --dev",
|
||||
"stop": "electron . --stop",
|
||||
|
||||
@@ -14,3 +14,7 @@ test('ci workflow checks pull requests for required changelog fragments', () =>
|
||||
assert.match(ciWorkflow, /bun run changelog:pr-check/);
|
||||
assert.match(ciWorkflow, /skip-changelog/);
|
||||
});
|
||||
|
||||
test('ci workflow verifies generated config examples stay in sync', () => {
|
||||
assert.match(ciWorkflow, /bun run verify:config-example/);
|
||||
});
|
||||
|
||||
@@ -1106,6 +1106,135 @@ test('parses global shortcuts and startup settings', () => {
|
||||
assert.equal(config.youtubeSubgen.fixWithAi, true);
|
||||
});
|
||||
|
||||
test('parses controller settings with logical bindings and tuning knobs', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"controller": {
|
||||
"enabled": true,
|
||||
"preferredGamepadId": "Xbox Wireless Controller (STANDARD GAMEPAD Vendor: 045e Product: 0b13)",
|
||||
"preferredGamepadLabel": "Xbox Wireless Controller",
|
||||
"smoothScroll": false,
|
||||
"scrollPixelsPerSecond": 1440,
|
||||
"horizontalJumpPixels": 180,
|
||||
"stickDeadzone": 0.3,
|
||||
"triggerInputMode": "analog",
|
||||
"triggerDeadzone": 0.4,
|
||||
"repeatDelayMs": 220,
|
||||
"repeatIntervalMs": 70,
|
||||
"buttonIndices": {
|
||||
"select": 6,
|
||||
"leftStickPress": 9,
|
||||
"rightStickPress": 10
|
||||
},
|
||||
"bindings": {
|
||||
"toggleLookup": "buttonWest",
|
||||
"closeLookup": "buttonEast",
|
||||
"toggleKeyboardOnlyMode": "buttonNorth",
|
||||
"mineCard": "buttonSouth",
|
||||
"quitMpv": "select",
|
||||
"previousAudio": "leftShoulder",
|
||||
"nextAudio": "rightShoulder",
|
||||
"playCurrentAudio": "none",
|
||||
"toggleMpvPause": "leftStickPress",
|
||||
"leftStickHorizontal": "rightStickX",
|
||||
"leftStickVertical": "rightStickY",
|
||||
"rightStickHorizontal": "leftStickX",
|
||||
"rightStickVertical": "leftStickY"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
|
||||
assert.equal(config.controller.enabled, true);
|
||||
assert.equal(
|
||||
config.controller.preferredGamepadId,
|
||||
'Xbox Wireless Controller (STANDARD GAMEPAD Vendor: 045e Product: 0b13)',
|
||||
);
|
||||
assert.equal(config.controller.preferredGamepadLabel, 'Xbox Wireless Controller');
|
||||
assert.equal(config.controller.smoothScroll, false);
|
||||
assert.equal(config.controller.scrollPixelsPerSecond, 1440);
|
||||
assert.equal(config.controller.horizontalJumpPixels, 180);
|
||||
assert.equal(config.controller.stickDeadzone, 0.3);
|
||||
assert.equal(config.controller.triggerInputMode, 'analog');
|
||||
assert.equal(config.controller.triggerDeadzone, 0.4);
|
||||
assert.equal(config.controller.repeatDelayMs, 220);
|
||||
assert.equal(config.controller.repeatIntervalMs, 70);
|
||||
assert.equal(config.controller.buttonIndices.select, 6);
|
||||
assert.equal(config.controller.buttonIndices.leftStickPress, 9);
|
||||
assert.equal(config.controller.bindings.toggleLookup, 'buttonWest');
|
||||
assert.equal(config.controller.bindings.quitMpv, 'select');
|
||||
assert.equal(config.controller.bindings.playCurrentAudio, 'none');
|
||||
assert.equal(config.controller.bindings.toggleMpvPause, 'leftStickPress');
|
||||
assert.equal(config.controller.bindings.leftStickHorizontal, 'rightStickX');
|
||||
assert.equal(config.controller.bindings.rightStickVertical, 'leftStickY');
|
||||
});
|
||||
|
||||
test('controller positive-number tuning rejects sub-unit values that floor to zero', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"controller": {
|
||||
"scrollPixelsPerSecond": 0.5,
|
||||
"horizontalJumpPixels": 0.2,
|
||||
"repeatDelayMs": 0.9,
|
||||
"repeatIntervalMs": 0.1
|
||||
}
|
||||
}`,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
const warnings = service.getWarnings();
|
||||
|
||||
assert.equal(config.controller.scrollPixelsPerSecond, DEFAULT_CONFIG.controller.scrollPixelsPerSecond);
|
||||
assert.equal(config.controller.horizontalJumpPixels, DEFAULT_CONFIG.controller.horizontalJumpPixels);
|
||||
assert.equal(config.controller.repeatDelayMs, DEFAULT_CONFIG.controller.repeatDelayMs);
|
||||
assert.equal(config.controller.repeatIntervalMs, DEFAULT_CONFIG.controller.repeatIntervalMs);
|
||||
assert.equal(warnings.some((warning) => warning.path === 'controller.scrollPixelsPerSecond'), true);
|
||||
assert.equal(warnings.some((warning) => warning.path === 'controller.horizontalJumpPixels'), true);
|
||||
assert.equal(warnings.some((warning) => warning.path === 'controller.repeatDelayMs'), true);
|
||||
assert.equal(warnings.some((warning) => warning.path === 'controller.repeatIntervalMs'), true);
|
||||
});
|
||||
|
||||
test('controller button index config rejects fractional values', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"controller": {
|
||||
"buttonIndices": {
|
||||
"select": 6.5,
|
||||
"leftStickPress": 9.1
|
||||
}
|
||||
}
|
||||
}`,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
const warnings = service.getWarnings();
|
||||
|
||||
assert.equal(config.controller.buttonIndices.select, DEFAULT_CONFIG.controller.buttonIndices.select);
|
||||
assert.equal(
|
||||
config.controller.buttonIndices.leftStickPress,
|
||||
DEFAULT_CONFIG.controller.buttonIndices.leftStickPress,
|
||||
);
|
||||
assert.equal(warnings.some((warning) => warning.path === 'controller.buttonIndices.select'), true);
|
||||
assert.equal(
|
||||
warnings.some((warning) => warning.path === 'controller.buttonIndices.leftStickPress'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('runtime options registry is centralized', () => {
|
||||
const ids = RUNTIME_OPTION_REGISTRY.map((entry) => entry.id);
|
||||
assert.deepEqual(ids, [
|
||||
@@ -1638,6 +1767,7 @@ test('template generator includes known keys', () => {
|
||||
const output = generateConfigTemplate(DEFAULT_CONFIG);
|
||||
assert.match(output, /"ai":/);
|
||||
assert.match(output, /"ankiConnect":/);
|
||||
assert.match(output, /"controller":/);
|
||||
assert.match(output, /"logging":/);
|
||||
assert.match(output, /"websocket":/);
|
||||
assert.match(output, /"discordPresence":/);
|
||||
@@ -1662,6 +1792,14 @@ test('template generator includes known keys', () => {
|
||||
output,
|
||||
/"enabled": true,? \/\/ Annotated subtitle websocket server enabled state\. Values: true \| false/,
|
||||
);
|
||||
assert.match(
|
||||
output,
|
||||
/"scrollPixelsPerSecond": 900,? \/\/ Base popup scroll speed for controller stick input\./,
|
||||
);
|
||||
assert.match(
|
||||
output,
|
||||
/"triggerInputMode": "auto",? \/\/ How controller triggers are interpreted: auto, pressed-only, or thresholded analog\. Values: auto \| digital \| analog/,
|
||||
);
|
||||
assert.match(output, /"port": 6678,? \/\/ Annotated subtitle websocket server port\./);
|
||||
assert.match(
|
||||
output,
|
||||
|
||||
@@ -25,6 +25,7 @@ const {
|
||||
annotationWebsocket,
|
||||
logging,
|
||||
texthooker,
|
||||
controller,
|
||||
shortcuts,
|
||||
secondarySub,
|
||||
subsync,
|
||||
@@ -43,6 +44,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
|
||||
annotationWebsocket,
|
||||
logging,
|
||||
texthooker,
|
||||
controller,
|
||||
ankiConnect,
|
||||
shortcuts,
|
||||
secondarySub,
|
||||
|
||||
@@ -8,6 +8,7 @@ export const CORE_DEFAULT_CONFIG: Pick<
|
||||
| 'annotationWebsocket'
|
||||
| 'logging'
|
||||
| 'texthooker'
|
||||
| 'controller'
|
||||
| 'shortcuts'
|
||||
| 'secondarySub'
|
||||
| 'subsync'
|
||||
@@ -31,6 +32,47 @@ export const CORE_DEFAULT_CONFIG: Pick<
|
||||
launchAtStartup: true,
|
||||
openBrowser: true,
|
||||
},
|
||||
controller: {
|
||||
enabled: true,
|
||||
preferredGamepadId: '',
|
||||
preferredGamepadLabel: '',
|
||||
smoothScroll: true,
|
||||
scrollPixelsPerSecond: 900,
|
||||
horizontalJumpPixels: 160,
|
||||
stickDeadzone: 0.2,
|
||||
triggerInputMode: 'auto',
|
||||
triggerDeadzone: 0.5,
|
||||
repeatDelayMs: 320,
|
||||
repeatIntervalMs: 120,
|
||||
buttonIndices: {
|
||||
select: 6,
|
||||
buttonSouth: 0,
|
||||
buttonEast: 1,
|
||||
buttonWest: 2,
|
||||
buttonNorth: 3,
|
||||
leftShoulder: 4,
|
||||
rightShoulder: 5,
|
||||
leftStickPress: 9,
|
||||
rightStickPress: 10,
|
||||
leftTrigger: 6,
|
||||
rightTrigger: 7,
|
||||
},
|
||||
bindings: {
|
||||
toggleLookup: 'buttonSouth',
|
||||
closeLookup: 'buttonEast',
|
||||
toggleKeyboardOnlyMode: 'buttonNorth',
|
||||
mineCard: 'buttonWest',
|
||||
quitMpv: 'select',
|
||||
previousAudio: 'none',
|
||||
nextAudio: 'rightShoulder',
|
||||
playCurrentAudio: 'leftShoulder',
|
||||
toggleMpvPause: 'leftStickPress',
|
||||
leftStickHorizontal: 'leftStickX',
|
||||
leftStickVertical: 'leftStickY',
|
||||
rightStickHorizontal: 'rightStickX',
|
||||
rightStickVertical: 'rightStickY',
|
||||
},
|
||||
},
|
||||
shortcuts: {
|
||||
toggleVisibleOverlayGlobal: 'Alt+Shift+O',
|
||||
copySubtitle: 'CommandOrControl+C',
|
||||
|
||||
@@ -19,6 +19,8 @@ test('config option registry includes critical paths and has unique entries', ()
|
||||
for (const requiredPath of [
|
||||
'logging.level',
|
||||
'annotationWebsocket.enabled',
|
||||
'controller.enabled',
|
||||
'controller.scrollPixelsPerSecond',
|
||||
'startupWarmups.lowPowerMode',
|
||||
'subtitleStyle.enableJlpt',
|
||||
'subtitleStyle.autoPauseVideoOnYomitanPopup',
|
||||
@@ -38,6 +40,7 @@ test('config template sections include expected domains and unique keys', () =>
|
||||
const requiredKeys: (typeof keys)[number][] = [
|
||||
'websocket',
|
||||
'annotationWebsocket',
|
||||
'controller',
|
||||
'startupWarmups',
|
||||
'subtitleStyle',
|
||||
'ankiConnect',
|
||||
|
||||
@@ -4,6 +4,21 @@ import { ConfigOptionRegistryEntry } from './shared';
|
||||
export function buildCoreConfigOptionRegistry(
|
||||
defaultConfig: ResolvedConfig,
|
||||
): ConfigOptionRegistryEntry[] {
|
||||
const controllerButtonEnumValues = [
|
||||
'none',
|
||||
'select',
|
||||
'buttonSouth',
|
||||
'buttonEast',
|
||||
'buttonNorth',
|
||||
'buttonWest',
|
||||
'leftShoulder',
|
||||
'rightShoulder',
|
||||
'leftStickPress',
|
||||
'rightStickPress',
|
||||
'leftTrigger',
|
||||
'rightTrigger',
|
||||
];
|
||||
|
||||
return [
|
||||
{
|
||||
path: 'logging.level',
|
||||
@@ -12,6 +27,230 @@ export function buildCoreConfigOptionRegistry(
|
||||
defaultValue: defaultConfig.logging.level,
|
||||
description: 'Minimum log level for runtime logging.',
|
||||
},
|
||||
{
|
||||
path: 'controller.enabled',
|
||||
kind: 'boolean',
|
||||
defaultValue: defaultConfig.controller.enabled,
|
||||
description: 'Enable overlay controller support through the Chrome Gamepad API.',
|
||||
},
|
||||
{
|
||||
path: 'controller.preferredGamepadId',
|
||||
kind: 'string',
|
||||
defaultValue: defaultConfig.controller.preferredGamepadId,
|
||||
description: 'Preferred controller id saved from the controller selection modal.',
|
||||
},
|
||||
{
|
||||
path: 'controller.preferredGamepadLabel',
|
||||
kind: 'string',
|
||||
defaultValue: defaultConfig.controller.preferredGamepadLabel,
|
||||
description: 'Preferred controller display label saved for diagnostics.',
|
||||
},
|
||||
{
|
||||
path: 'controller.smoothScroll',
|
||||
kind: 'boolean',
|
||||
defaultValue: defaultConfig.controller.smoothScroll,
|
||||
description: 'Use smooth scrolling for controller-driven popup scroll input.',
|
||||
},
|
||||
{
|
||||
path: 'controller.scrollPixelsPerSecond',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.controller.scrollPixelsPerSecond,
|
||||
description: 'Base popup scroll speed for controller stick input.',
|
||||
},
|
||||
{
|
||||
path: 'controller.horizontalJumpPixels',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.controller.horizontalJumpPixels,
|
||||
description: 'Popup page-jump distance for controller jump input.',
|
||||
},
|
||||
{
|
||||
path: 'controller.stickDeadzone',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.controller.stickDeadzone,
|
||||
description: 'Deadzone applied to controller stick axes.',
|
||||
},
|
||||
{
|
||||
path: 'controller.triggerInputMode',
|
||||
kind: 'enum',
|
||||
enumValues: ['auto', 'digital', 'analog'],
|
||||
defaultValue: defaultConfig.controller.triggerInputMode,
|
||||
description: 'How controller triggers are interpreted: auto, pressed-only, or thresholded analog.',
|
||||
},
|
||||
{
|
||||
path: 'controller.triggerDeadzone',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.controller.triggerDeadzone,
|
||||
description: 'Minimum analog trigger value required when trigger input uses auto or analog mode.',
|
||||
},
|
||||
{
|
||||
path: 'controller.repeatDelayMs',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.controller.repeatDelayMs,
|
||||
description: 'Delay before repeating held controller actions.',
|
||||
},
|
||||
{
|
||||
path: 'controller.repeatIntervalMs',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.controller.repeatIntervalMs,
|
||||
description: 'Repeat interval for held controller actions.',
|
||||
},
|
||||
{
|
||||
path: 'controller.buttonIndices.select',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.controller.buttonIndices.select,
|
||||
description: 'Raw button index used for the controller select/minus/back button.',
|
||||
},
|
||||
{
|
||||
path: 'controller.buttonIndices.buttonSouth',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.controller.buttonIndices.buttonSouth,
|
||||
description: 'Raw button index used for controller south/A button input.',
|
||||
},
|
||||
{
|
||||
path: 'controller.buttonIndices.buttonEast',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.controller.buttonIndices.buttonEast,
|
||||
description: 'Raw button index used for controller east/B button input.',
|
||||
},
|
||||
{
|
||||
path: 'controller.buttonIndices.buttonNorth',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.controller.buttonIndices.buttonNorth,
|
||||
description: 'Raw button index used for controller north/Y button input.',
|
||||
},
|
||||
{
|
||||
path: 'controller.buttonIndices.buttonWest',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.controller.buttonIndices.buttonWest,
|
||||
description: 'Raw button index used for controller west/X button input.',
|
||||
},
|
||||
{
|
||||
path: 'controller.buttonIndices.leftShoulder',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.controller.buttonIndices.leftShoulder,
|
||||
description: 'Raw button index used for controller left shoulder input.',
|
||||
},
|
||||
{
|
||||
path: 'controller.buttonIndices.rightShoulder',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.controller.buttonIndices.rightShoulder,
|
||||
description: 'Raw button index used for controller right shoulder input.',
|
||||
},
|
||||
{
|
||||
path: 'controller.buttonIndices.leftStickPress',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.controller.buttonIndices.leftStickPress,
|
||||
description: 'Raw button index used for controller L3 input.',
|
||||
},
|
||||
{
|
||||
path: 'controller.buttonIndices.rightStickPress',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.controller.buttonIndices.rightStickPress,
|
||||
description: 'Raw button index used for controller R3 input.',
|
||||
},
|
||||
{
|
||||
path: 'controller.buttonIndices.leftTrigger',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.controller.buttonIndices.leftTrigger,
|
||||
description: 'Raw button index used for controller L2 input.',
|
||||
},
|
||||
{
|
||||
path: 'controller.buttonIndices.rightTrigger',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.controller.buttonIndices.rightTrigger,
|
||||
description: 'Raw button index used for controller R2 input.',
|
||||
},
|
||||
{
|
||||
path: 'controller.bindings.toggleLookup',
|
||||
kind: 'enum',
|
||||
enumValues: controllerButtonEnumValues,
|
||||
defaultValue: defaultConfig.controller.bindings.toggleLookup,
|
||||
description: 'Controller binding for toggling lookup.',
|
||||
},
|
||||
{
|
||||
path: 'controller.bindings.closeLookup',
|
||||
kind: 'enum',
|
||||
enumValues: controllerButtonEnumValues,
|
||||
defaultValue: defaultConfig.controller.bindings.closeLookup,
|
||||
description: 'Controller binding for closing lookup.',
|
||||
},
|
||||
{
|
||||
path: 'controller.bindings.toggleKeyboardOnlyMode',
|
||||
kind: 'enum',
|
||||
enumValues: controllerButtonEnumValues,
|
||||
defaultValue: defaultConfig.controller.bindings.toggleKeyboardOnlyMode,
|
||||
description: 'Controller binding for toggling keyboard-only mode.',
|
||||
},
|
||||
{
|
||||
path: 'controller.bindings.mineCard',
|
||||
kind: 'enum',
|
||||
enumValues: controllerButtonEnumValues,
|
||||
defaultValue: defaultConfig.controller.bindings.mineCard,
|
||||
description: 'Controller binding for mining the active card.',
|
||||
},
|
||||
{
|
||||
path: 'controller.bindings.quitMpv',
|
||||
kind: 'enum',
|
||||
enumValues: controllerButtonEnumValues,
|
||||
defaultValue: defaultConfig.controller.bindings.quitMpv,
|
||||
description: 'Controller binding for quitting mpv.',
|
||||
},
|
||||
{
|
||||
path: 'controller.bindings.previousAudio',
|
||||
kind: 'enum',
|
||||
enumValues: controllerButtonEnumValues,
|
||||
defaultValue: defaultConfig.controller.bindings.previousAudio,
|
||||
description: 'Controller binding for previous Yomitan audio.',
|
||||
},
|
||||
{
|
||||
path: 'controller.bindings.nextAudio',
|
||||
kind: 'enum',
|
||||
enumValues: controllerButtonEnumValues,
|
||||
defaultValue: defaultConfig.controller.bindings.nextAudio,
|
||||
description: 'Controller binding for next Yomitan audio.',
|
||||
},
|
||||
{
|
||||
path: 'controller.bindings.playCurrentAudio',
|
||||
kind: 'enum',
|
||||
enumValues: controllerButtonEnumValues,
|
||||
defaultValue: defaultConfig.controller.bindings.playCurrentAudio,
|
||||
description: 'Controller binding for playing the current Yomitan audio.',
|
||||
},
|
||||
{
|
||||
path: 'controller.bindings.toggleMpvPause',
|
||||
kind: 'enum',
|
||||
enumValues: controllerButtonEnumValues,
|
||||
defaultValue: defaultConfig.controller.bindings.toggleMpvPause,
|
||||
description: 'Controller binding for toggling mpv play/pause.',
|
||||
},
|
||||
{
|
||||
path: 'controller.bindings.leftStickHorizontal',
|
||||
kind: 'enum',
|
||||
enumValues: ['leftStickX', 'leftStickY', 'rightStickX', 'rightStickY'],
|
||||
defaultValue: defaultConfig.controller.bindings.leftStickHorizontal,
|
||||
description: 'Axis binding used for left/right token selection.',
|
||||
},
|
||||
{
|
||||
path: 'controller.bindings.leftStickVertical',
|
||||
kind: 'enum',
|
||||
enumValues: ['leftStickX', 'leftStickY', 'rightStickX', 'rightStickY'],
|
||||
defaultValue: defaultConfig.controller.bindings.leftStickVertical,
|
||||
description: 'Axis binding used for primary popup scrolling.',
|
||||
},
|
||||
{
|
||||
path: 'controller.bindings.rightStickHorizontal',
|
||||
kind: 'enum',
|
||||
enumValues: ['leftStickX', 'leftStickY', 'rightStickX', 'rightStickY'],
|
||||
defaultValue: defaultConfig.controller.bindings.rightStickHorizontal,
|
||||
description: 'Axis binding reserved for alternate right-stick mappings.',
|
||||
},
|
||||
{
|
||||
path: 'controller.bindings.rightStickVertical',
|
||||
kind: 'enum',
|
||||
enumValues: ['leftStickX', 'leftStickY', 'rightStickX', 'rightStickY'],
|
||||
defaultValue: defaultConfig.controller.bindings.rightStickVertical,
|
||||
description: 'Axis binding used for popup page jumps.',
|
||||
},
|
||||
{
|
||||
path: 'texthooker.launchAtStartup',
|
||||
kind: 'boolean',
|
||||
|
||||
@@ -34,6 +34,16 @@ const CORE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
||||
description: ['Controls logging verbosity.', 'Set to debug for full runtime diagnostics.'],
|
||||
key: 'logging',
|
||||
},
|
||||
{
|
||||
title: 'Controller Support',
|
||||
description: [
|
||||
'Gamepad support for the visible overlay while keyboard-only mode is active.',
|
||||
'Use the selection modal to save a preferred controller by id for future launches.',
|
||||
'Trigger input mode can be auto, digital-only, or analog-thresholded depending on the controller.',
|
||||
'Override controller.buttonIndices when your pad reports non-standard raw button numbers.',
|
||||
],
|
||||
key: 'controller',
|
||||
},
|
||||
{
|
||||
title: 'Startup Warmups',
|
||||
description: [
|
||||
|
||||