mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-11-10 04:46:07 -08:00
Compare commits
58
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79e5535823
|
||
|
|
5140b5e9be
|
||
|
|
c2dac537da
|
||
|
|
bc7e82fee6
|
||
|
|
c3e672efdb
|
||
|
|
269c0708cb | ||
|
|
f89f78b861 | ||
|
|
14ef7e823c | ||
|
|
a8bc585f75
|
||
|
|
5f635f3042
|
||
|
|
7c637149dd | ||
|
|
33f5f890d5 | ||
|
|
d8c2315405 | ||
|
|
8976d78390
|
||
|
|
1c50d249e0 | ||
|
|
8e29cb3c55 | ||
|
|
fa3051f9cb | ||
|
|
848a6c4021 | ||
|
|
b99409bbcd | ||
|
|
6d0f37ac21 | ||
|
|
696462a82e | ||
|
|
655510cf2e | ||
|
|
af44735bb8 | ||
|
|
804fb8933e
|
||
|
|
2924f04316 | ||
|
|
a9a4c4f083 | ||
|
|
90fc7cf940 | ||
|
|
2a0e931325 | ||
|
|
0129e03c0f
|
||
|
|
586d7950cf
|
||
|
|
33dceabc3e
|
||
|
|
ad1abc3268
|
||
|
|
e59b78dca0 | ||
|
|
203a61a20f
|
||
|
|
a681bbaf25
|
||
|
|
6219a81144 | ||
|
|
cc9af79d6e | ||
|
|
d3a94536a8
|
||
|
|
3068d02f89
|
||
|
|
9150a60630 | ||
|
|
3549e0ed93 | ||
|
|
b0a8b6b3fc
|
||
|
|
153bc5d4bf
|
||
|
|
cbaa4626e1 | ||
|
|
e690ffba07
|
||
|
|
d047e3e0a4 | ||
|
|
01613f1be5 | ||
|
|
bf4e5bb609 | ||
|
|
7909fcd2cf | ||
|
|
8fc20b2499 | ||
|
|
ea2b3a9507 | ||
|
|
a6d96e9966 | ||
|
|
7108e086ba | ||
|
|
78a9b6346f | ||
|
|
78eca52b7e | ||
|
|
b48083ddd8 | ||
|
|
4b6842d034 | ||
|
|
9ffa2bce94
|
No files matched your search
@@ -27,6 +27,7 @@ Do not use hidden wrapper commands. Verification commands are owned by `package.
|
||||
- Launcher or mpv plugin: `bun run test:launcher` or `bun run test:env`, based on the behavior changed
|
||||
- Runtime compatibility or dist-sensitive wiring: `bun run test:runtime:compat`
|
||||
- Stats dashboard: `bun run test:stats`
|
||||
- Overlay, renderer, mpv, or mining behavior in the running app: `bun run build`, then `bun run test:e2e`
|
||||
- Build/release scripts: `bun run test:scripts`
|
||||
|
||||
For substantial changes, use the full gate documented in `AGENTS.md` and `docs/workflow/verification.md`.
|
||||
|
||||
@@ -9,3 +9,88 @@ on:
|
||||
jobs:
|
||||
build-test-audit:
|
||||
uses: ./.github/workflows/quality-gate.yml
|
||||
|
||||
# Boots the built app with a real mpv on each platform. Kept outside the
|
||||
# reusable quality gate so releases do not depend on it while the lane is new.
|
||||
e2e:
|
||||
name: E2E (${{ matrix.os }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
env:
|
||||
SUBMINER_E2E_KEEP: '1'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.5
|
||||
|
||||
- name: Point temp files at an uploadable directory
|
||||
run: |
|
||||
# os.tmpdir() follows these, so the kept session logs land under runner.temp.
|
||||
dir="$RUNNER_TEMP/e2e"
|
||||
mkdir -p "$dir"
|
||||
{ echo "TMPDIR=$dir"; echo "TEMP=$dir"; echo "TMP=$dir"; } >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install mpv and ffmpeg (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
# Only Ubuntu sources are needed; unrelated runner repositories can be unavailable.
|
||||
apt_sources=(-o Dir::Etc::sourcelist=sources.list.d/ubuntu.sources -o Dir::Etc::sourceparts=-)
|
||||
sudo apt-get "${apt_sources[@]}" update
|
||||
sudo apt-get "${apt_sources[@]}" install -y mpv ffmpeg xvfb
|
||||
|
||||
- name: Install mpv and ffmpeg (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
# Runner images bake in different Homebrew metadata, so without an update the
|
||||
# installed mpv build depends on which image the job lands on.
|
||||
brew update
|
||||
brew install mpv ffmpeg
|
||||
mpv --version
|
||||
|
||||
- name: Install mpv and ffmpeg (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
choco install mpv ffmpeg -y --no-progress
|
||||
# The mpv package adds no shim, so put its directory on PATH for later steps.
|
||||
mpv_exe="$(find /c/ProgramData/chocolatey/lib /c/ProgramData/chocolatey/bin "/c/Program Files" /c/tools -maxdepth 6 -iname mpv.exe 2>/dev/null | head -n 1)"
|
||||
test -n "$mpv_exe"
|
||||
cygpath -w "$(dirname "$mpv_exe")" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
bun install --frozen-lockfile
|
||||
cd stats && bun install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: bun run build
|
||||
|
||||
- name: End-to-end suite
|
||||
run: bun run test:e2e
|
||||
|
||||
- name: Dictionary backend windows (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: xvfb-run -a bun run test:dictionary:electron
|
||||
|
||||
- name: Upload session logs (on failure)
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-logs-${{ matrix.os }}
|
||||
path: ${{ runner.temp }}/e2e/subminer-e2e-*/*.log
|
||||
if-no-files-found: ignore
|
||||
@@ -1,49 +0,0 @@
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
||||
# prompt: 'Update the pull request description to include a summary of changes.'
|
||||
|
||||
# Optional: Add claude_args to customize behavior and configuration
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
||||
@@ -67,26 +67,26 @@ jobs:
|
||||
- name: Build unversioned AppImage
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
appimages=(release/SubMiner-*.AppImage)
|
||||
appimages=(dist/release/SubMiner-*.AppImage)
|
||||
if [ "${#appimages[@]}" -eq 0 ]; then
|
||||
echo "No versioned AppImage found to create unversioned artifact."
|
||||
ls -la release
|
||||
ls -la dist/release
|
||||
exit 1
|
||||
fi
|
||||
cp "${appimages[0]}" release/SubMiner.AppImage
|
||||
cp "${appimages[0]}" dist/release/SubMiner.AppImage
|
||||
|
||||
- name: Smoke packaged runtime assets
|
||||
shell: bash
|
||||
run: xvfb-run -a bun run test:package "release/linux-unpacked/resources"
|
||||
run: xvfb-run -a bun run test:package "dist/release/linux-unpacked/resources"
|
||||
|
||||
- name: Upload AppImage artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: appimage
|
||||
path: |
|
||||
release/*.AppImage
|
||||
release/latest*.yml
|
||||
release/*.blockmap
|
||||
dist/release/*.AppImage
|
||||
dist/release/latest*.yml
|
||||
dist/release/*.blockmap
|
||||
if-no-files-found: error
|
||||
|
||||
build-macos:
|
||||
@@ -159,17 +159,17 @@ jobs:
|
||||
|
||||
- name: Smoke packaged runtime assets
|
||||
shell: bash
|
||||
run: bun run test:package "release/mac-arm64/SubMiner.app/Contents/Resources"
|
||||
run: bun run test:package "dist/release/mac-arm64/SubMiner.app/Contents/Resources"
|
||||
|
||||
- name: Upload macOS artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macos
|
||||
path: |
|
||||
release/*.dmg
|
||||
release/*.zip
|
||||
release/latest*.yml
|
||||
release/*.blockmap
|
||||
dist/release/*.dmg
|
||||
dist/release/*.zip
|
||||
dist/release/latest*.yml
|
||||
dist/release/*.blockmap
|
||||
if-no-files-found: error
|
||||
|
||||
build-windows:
|
||||
@@ -224,15 +224,15 @@ jobs:
|
||||
|
||||
- name: Smoke packaged runtime assets
|
||||
shell: bash
|
||||
run: bun run test:package "release/win-unpacked/resources"
|
||||
run: bun run test:package "dist/release/win-unpacked/resources"
|
||||
|
||||
- name: Upload Windows artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows
|
||||
path: |
|
||||
release/*.exe
|
||||
release/*.zip
|
||||
release/latest*.yml
|
||||
release/*.blockmap
|
||||
dist/release/*.exe
|
||||
dist/release/*.zip
|
||||
dist/release/latest*.yml
|
||||
dist/release/*.blockmap
|
||||
if-no-files-found: error
|
||||
@@ -44,19 +44,19 @@ jobs:
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: appimage
|
||||
path: release
|
||||
path: dist/release
|
||||
|
||||
- name: Download macOS artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: macos
|
||||
path: release
|
||||
path: dist/release
|
||||
|
||||
- name: Download Windows artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: windows
|
||||
path: release
|
||||
path: dist/release
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
@@ -90,13 +90,15 @@ jobs:
|
||||
|
||||
- name: Package optional assets bundle
|
||||
run: |
|
||||
tar -czf "release/subminer-assets.tar.gz" \
|
||||
# Keep the archive layout stable for the AUR PKGBUILD.
|
||||
tar -czf "dist/release/subminer-assets.tar.gz" \
|
||||
--transform 's,^packaging/bun/licenses,resources/bun/licenses,' \
|
||||
config.example.jsonc \
|
||||
plugin/subminer \
|
||||
plugin/subminer.conf \
|
||||
assets/themes/subminer.rasi \
|
||||
assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer \
|
||||
resources/bun/licenses
|
||||
packaging/bun/licenses
|
||||
|
||||
- name: Package Bun corresponding source
|
||||
run: bun scripts/package-bun-source.mjs
|
||||
@@ -104,16 +106,16 @@ jobs:
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd)
|
||||
files=(dist/release/*.AppImage dist/release/*.dmg dist/release/*.exe dist/release/*.zip dist/release/*.tar.gz dist/release/latest*.yml dist/release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd)
|
||||
if [ "${#files[@]}" -eq 0 ]; then
|
||||
echo "No release artifacts found for checksum generation."
|
||||
exit 1
|
||||
fi
|
||||
: > release/SHA256SUMS.txt
|
||||
: > dist/release/SHA256SUMS.txt
|
||||
for file in "${files[@]}"; do
|
||||
printf '%s %s\n' \
|
||||
"$(sha256sum "$file" | awk '{print $1}')" \
|
||||
"${file##*/}" >> release/SHA256SUMS.txt
|
||||
"${file##*/}" >> dist/release/SHA256SUMS.txt
|
||||
done
|
||||
|
||||
- name: Get version from tag
|
||||
@@ -124,12 +126,12 @@ jobs:
|
||||
env:
|
||||
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
|
||||
run: |
|
||||
if [ ! -s release/prerelease-notes.md ]; then
|
||||
echo "::error::release/prerelease-notes.md is missing or empty. Run 'bun run changelog:prerelease-notes --version <version>' locally and commit the file before tagging."
|
||||
if [ ! -s packaging/release-notes/prerelease-notes.md ]; then
|
||||
echo "::error::packaging/release-notes/prerelease-notes.md is missing or empty. Run 'bun run changelog:prerelease-notes --version <version>' locally and commit the file before tagging."
|
||||
exit 1
|
||||
fi
|
||||
if ! bun run changelog:check-prerelease-notes --version "$RELEASE_VERSION"; then
|
||||
echo "::error::release/prerelease-notes.md was not generated for $RELEASE_VERSION. Rerun 'bun run changelog:prerelease-notes --version $RELEASE_VERSION' locally, commit, and retag."
|
||||
echo "::error::packaging/release-notes/prerelease-notes.md was not generated for $RELEASE_VERSION. Rerun 'bun run changelog:prerelease-notes --version $RELEASE_VERSION' locally, commit, and retag."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -142,15 +144,15 @@ jobs:
|
||||
|
||||
shopt -s nullglob
|
||||
artifacts=(
|
||||
release/*.AppImage
|
||||
release/*.dmg
|
||||
release/*.exe
|
||||
release/*.zip
|
||||
release/*.tar.gz
|
||||
release/*.tar.gz.sha256
|
||||
release/latest*.yml
|
||||
release/*.blockmap
|
||||
release/SHA256SUMS.txt
|
||||
dist/release/*.AppImage
|
||||
dist/release/*.dmg
|
||||
dist/release/*.exe
|
||||
dist/release/*.zip
|
||||
dist/release/*.tar.gz
|
||||
dist/release/*.tar.gz.sha256
|
||||
dist/release/latest*.yml
|
||||
dist/release/*.blockmap
|
||||
dist/release/SHA256SUMS.txt
|
||||
dist/launcher/subminer
|
||||
dist/launcher/subminer.cmd
|
||||
)
|
||||
@@ -165,14 +167,14 @@ jobs:
|
||||
--draft \
|
||||
--prerelease \
|
||||
--title "$RELEASE_VERSION" \
|
||||
--notes-file release/prerelease-notes.md
|
||||
--notes-file packaging/release-notes/prerelease-notes.md
|
||||
else
|
||||
gh release create "$RELEASE_VERSION" \
|
||||
--draft \
|
||||
--latest=false \
|
||||
--prerelease \
|
||||
--title "$RELEASE_VERSION" \
|
||||
--notes-file release/prerelease-notes.md
|
||||
--notes-file packaging/release-notes/prerelease-notes.md
|
||||
fi
|
||||
|
||||
for asset in "${artifacts[@]}"; do
|
||||
@@ -183,4 +185,4 @@ jobs:
|
||||
--draft=false \
|
||||
--prerelease \
|
||||
--title "$RELEASE_VERSION" \
|
||||
--notes-file release/prerelease-notes.md
|
||||
--notes-file packaging/release-notes/prerelease-notes.md
|
||||
@@ -45,19 +45,19 @@ jobs:
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: appimage
|
||||
path: release
|
||||
path: dist/release
|
||||
|
||||
- name: Download macOS artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: macos
|
||||
path: release
|
||||
path: dist/release
|
||||
|
||||
- name: Download Windows artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: windows
|
||||
path: release
|
||||
path: dist/release
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
@@ -91,13 +91,15 @@ jobs:
|
||||
|
||||
- name: Package optional assets bundle
|
||||
run: |
|
||||
tar -czf "release/subminer-assets.tar.gz" \
|
||||
# Keep the archive layout stable for the AUR PKGBUILD.
|
||||
tar -czf "dist/release/subminer-assets.tar.gz" \
|
||||
--transform 's,^packaging/bun/licenses,resources/bun/licenses,' \
|
||||
config.example.jsonc \
|
||||
plugin/subminer \
|
||||
plugin/subminer.conf \
|
||||
assets/themes/subminer.rasi \
|
||||
assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer \
|
||||
resources/bun/licenses
|
||||
packaging/bun/licenses
|
||||
|
||||
- name: Package Bun corresponding source
|
||||
run: bun scripts/package-bun-source.mjs
|
||||
@@ -105,16 +107,16 @@ jobs:
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd)
|
||||
files=(dist/release/*.AppImage dist/release/*.dmg dist/release/*.exe dist/release/*.zip dist/release/*.tar.gz dist/release/latest*.yml dist/release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd)
|
||||
if [ "${#files[@]}" -eq 0 ]; then
|
||||
echo "No release artifacts found for checksum generation."
|
||||
exit 1
|
||||
fi
|
||||
: > release/SHA256SUMS.txt
|
||||
: > dist/release/SHA256SUMS.txt
|
||||
for file in "${files[@]}"; do
|
||||
printf '%s %s\n' \
|
||||
"$(sha256sum "$file" | awk '{print $1}')" \
|
||||
"${file##*/}" >> release/SHA256SUMS.txt
|
||||
"${file##*/}" >> dist/release/SHA256SUMS.txt
|
||||
done
|
||||
|
||||
- name: Get version from tag
|
||||
@@ -152,24 +154,24 @@ jobs:
|
||||
gh release edit "$RELEASE_VERSION" \
|
||||
--draft=false \
|
||||
--title "$RELEASE_VERSION" \
|
||||
--notes-file release/release-notes.md
|
||||
--notes-file packaging/release-notes/release-notes.md
|
||||
else
|
||||
gh release create "$RELEASE_VERSION" \
|
||||
--title "$RELEASE_VERSION" \
|
||||
--notes-file release/release-notes.md
|
||||
--notes-file packaging/release-notes/release-notes.md
|
||||
fi
|
||||
|
||||
shopt -s nullglob
|
||||
artifacts=(
|
||||
release/*.AppImage
|
||||
release/*.dmg
|
||||
release/*.exe
|
||||
release/*.zip
|
||||
release/*.tar.gz
|
||||
release/*.tar.gz.sha256
|
||||
release/latest*.yml
|
||||
release/*.blockmap
|
||||
release/SHA256SUMS.txt
|
||||
dist/release/*.AppImage
|
||||
dist/release/*.dmg
|
||||
dist/release/*.exe
|
||||
dist/release/*.zip
|
||||
dist/release/*.tar.gz
|
||||
dist/release/*.tar.gz.sha256
|
||||
dist/release/latest*.yml
|
||||
dist/release/*.blockmap
|
||||
dist/release/SHA256SUMS.txt
|
||||
dist/launcher/subminer
|
||||
dist/launcher/subminer.cmd
|
||||
)
|
||||
|
||||
+1
-5
@@ -8,11 +8,6 @@ node_modules/
|
||||
# Electron build output
|
||||
out/
|
||||
dist/
|
||||
release/*
|
||||
!release/
|
||||
!release/release-notes.md
|
||||
!release/prerelease-notes.md
|
||||
build/yomitan/
|
||||
coverage/
|
||||
|
||||
# Launcher build artifact (produced by make build-launcher)
|
||||
@@ -41,6 +36,7 @@ environment.toml
|
||||
.vitepress/dist/
|
||||
docs/.vitepress/cache/
|
||||
docs/.vitepress/dist/
|
||||
# Local test media and scripts
|
||||
tests/*
|
||||
.worktrees/
|
||||
.tmp/
|
||||
|
||||
@@ -8,3 +8,7 @@
|
||||
[submodule "vendor/subminer-yomitan"]
|
||||
path = vendor/subminer-yomitan
|
||||
url = https://github.com/ksyasuda/subminer-yomitan
|
||||
[submodule "vendor/hachidori"]
|
||||
path = vendor/hachidori
|
||||
url = https://github.com/ksyasuda/hachidori.git
|
||||
branch = subminer
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
node_modules
|
||||
dist
|
||||
release
|
||||
coverage
|
||||
vendor
|
||||
*.log
|
||||
src/core/services/tokenizer/__fixtures__/golden/*.json
|
||||
# Reviewed release text and vendored license texts; never reformat
|
||||
packaging/release-notes
|
||||
packaging/bun/licenses
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
@@ -8,6 +8,7 @@ Start here, then leave this file.
|
||||
- Architecture map: [`docs/architecture/README.md`](./docs/architecture/README.md)
|
||||
- Workflow map: [`docs/workflow/README.md`](./docs/workflow/README.md)
|
||||
- Verification lanes: [`docs/workflow/verification.md`](./docs/workflow/verification.md)
|
||||
- Testing guidelines: [`docs/workflow/testing.md`](./docs/workflow/testing.md)
|
||||
- Knowledge-base rules: [`docs/knowledge-base/README.md`](./docs/knowledge-base/README.md)
|
||||
- Release guide: [`docs/RELEASING.md`](./docs/RELEASING.md)
|
||||
|
||||
@@ -44,10 +45,11 @@ Start here, then leave this file.
|
||||
- Launcher/plugin: `bun run test:launcher` or `bun run test:env`
|
||||
- Runtime-compat / dist-sensitive: `bun run test:runtime:compat`
|
||||
- Stats dashboard UI (`stats/`): `bun run test:stats`
|
||||
- Overlay / renderer / mpv / mining behavior in the running app: `bun run build`, then `bun run test:e2e` (headless, sandboxed; `bun run e2e start` for exploratory checks)
|
||||
- Build/release scripts (`scripts/**`): `bun run test:scripts`
|
||||
- Internal docs, `AGENTS.md`, or repo skills: `bun run test:docs:kb`
|
||||
- User-facing `docs-site/`: `bun run docs:test`, then `bun run docs:build`
|
||||
- macOS mpv window helper: `bun test scripts/get-mpv-window-macos.test.ts`
|
||||
- macOS Swift helpers (`scripts/get-mpv-window-macos.swift`, `scripts/macos-controller-watch.swift`): compile them on macOS with `bun run build`
|
||||
- Test lanes are directory-discovered via `scripts/test-lanes.ts`; never hand-list test files in `package.json`
|
||||
|
||||
## Docs Upkeep
|
||||
|
||||
+118
@@ -1,5 +1,123 @@
|
||||
# Changelog
|
||||
|
||||
## v0.21.1 (2026-10-26)
|
||||
|
||||
### Changed
|
||||
- **Stats Session View**: Redesigned the expanded session view in the stats dashboard.
|
||||
- New stat tiles show pace, lines, known-word rate, lookups, and paused time.
|
||||
- A reading density chart of known and unknown words replaces the cumulative chart.
|
||||
- Yomitan lookups and mined cards now appear in their own lane below the chart.
|
||||
|
||||
### Fixed
|
||||
- **Subtitle Retiming**: Mining, the subtitle sidebar, subtitle navigation, and replay now use the timing you set with mpv's subtitle delay keys (z/x and sub-step), so mined audio and images match the retimed line.
|
||||
- **Subtitle Sidebar**: mpv keys and mouse actions, like volume keys and double-click fullscreen, work again while the sidebar is open.
|
||||
|
||||
### Docs
|
||||
- **README**: Refreshed the anime screenshots using the current build.
|
||||
|
||||
<details>
|
||||
<summary>Internal changes</summary>
|
||||
|
||||
### Internal
|
||||
- **Packaging Layout**: Packaging inputs now live under `packaging/`, and all build output lands in `dist/`, with packaged apps in `dist/release/`.
|
||||
|
||||
</details>
|
||||
|
||||
## v0.21.0 (2026-10-24)
|
||||
|
||||
### Added
|
||||
- **Hachidori Dictionary Backend**:
|
||||
- Hachidori is now bundled as an alternative to Yomitan. Select it with `dictionaryBackend`, then restart SubMiner.
|
||||
- It works with subtitle scanning, popups, frequency highlighting, character dictionaries, Anki mining, and stats dashboard mining.
|
||||
- First-run setup builds Hachidori's Anki template from your SubMiner settings. It can also link an external Hachidori app, browser, or Docker host.
|
||||
- The tray and dictionary-settings shortcut open settings for the active backend. Use `--hachidori` or `--yomitan` to open a specific one.
|
||||
- **Jellyfin Subtitle Generation**: You can now generate Japanese subtitles while playing a Jellyfin episode, with both direct play and transcoded playback.
|
||||
|
||||
### Fixed
|
||||
- **Jellyfin**:
|
||||
- `subminer jellyfin -p` now lists libraries and shows image previews when SubMiner is already running. Real errors are shown instead of an empty list.
|
||||
- Watch progress from terminal playback is reported to Jellyfin even with cast discovery off.
|
||||
- When `subminer jellyfin -p` starts SubMiner, the app shows its tray icon and quits when mpv closes.
|
||||
- Casting on macOS now finds mpv when SubMiner is opened from Finder, the Dock, or Spotlight.
|
||||
- **Halfwidth Katakana**: Halfwidth katakana in subtitles now displays as fullwidth, including in mined cards and older stats lines. Stats sentence search matches katakana in either width.
|
||||
- **Media Timing Review**: The review opens on the first attempt instead of after a delay. Playback stays paused while it is open.
|
||||
- **Subtitle Annotations**: Lines shown while two cues overlap now get annotations right away. Prefetching no longer restarts during startup.
|
||||
- **Keyboard-Only Mode**: `h`, `l`, and the arrow keys now work for typing in modal text fields instead of seeking mpv.
|
||||
- **Stats Dashboard Mining**: Cards mined from the stats dashboard keep the selected history line and media.
|
||||
- **Hyprland and Sway**: The app no longer freezes when `hyprctl` or `swaymsg` is slow or hangs.
|
||||
- **Subtitle Sidebar**: The open sidebar does much less background work during playback.
|
||||
- **macOS Launcher Status**: The command-line launcher no longer shows "Not on PATH" when SubMiner is opened from Finder or the Dock.
|
||||
- **Security Updates**: Updated Electron, undici, and follow-redirects to address security advisories.
|
||||
|
||||
### Docs
|
||||
- **Hachidori Setup**: Documented backend selection, Hachidori dictionary and Anki setup, settings shortcuts, and external host linking.
|
||||
- **Jellyfin Casting**: Explained when SubMiner joins the cast menu at startup, and how casting finds mpv on each platform. Added a troubleshooting entry for casts that never start mpv.
|
||||
- **Jimaku Key**: Removed environment variables that never had any effect. Set the key with `jimaku.apiKey` or `jimaku.apiKeyCommand`.
|
||||
|
||||
<details>
|
||||
<summary>Internal changes</summary>
|
||||
|
||||
### Internal
|
||||
- **End-to-End Harness**: Added a headless e2e harness (`bun run test:e2e`, `bun run e2e`) with real mpv, a fixture dictionary, and a fake AnkiConnect.
|
||||
- **Hachidori Fork**: The bundled Hachidori integration lives in a pinned fork submodule.
|
||||
- **Dead Code Removal**:
|
||||
- Removed the unused launcher Jimaku client, the mpv plugin hover-highlight module, and the unused character dictionary config keys.
|
||||
- Removed 16 unused overlay IPC methods. SubMiner no longer forwards every ASS line to the overlay.
|
||||
- Typecheck now runs with `noUnusedLocals`.
|
||||
- **Setup Launcher Label**: First-run setup now labels a missing launcher "Not set up" instead of "Not installed".
|
||||
|
||||
</details>
|
||||
|
||||
## v0.20.1 (2026-10-03)
|
||||
|
||||
### Added
|
||||
- **YouTube Browser Window**:
|
||||
- Browse YouTube with your login kept across restarts. Open it with `subminer youtube`, `subminer yt`, or tray **Browse YouTube**.
|
||||
- Clicking a video plays it in mpv. Middle-click, Shift/Ctrl-click, or the right-click menu adds it to mpv's playlist.
|
||||
- Queued videos get the same Japanese subtitle setup, and the rest of the queue is kept on Windows.
|
||||
- Closing the window quits a SubMiner started by `subminer youtube`.
|
||||
- **Whisper Subtitles for YouTube**:
|
||||
- Set `youtube.subtitleSource` to `whisper` to transcribe YouTube videos with Whisper instead of using YouTube's captions.
|
||||
- The video stays paused while Whisper transcribes, and the subtitle generation modal shows progress.
|
||||
- The subtitle generation modal (`Ctrl+Shift+G`) now works on YouTube videos, including Windows stream playback.
|
||||
- **Scroll Wheel and Mouse Bindings**: `keybindings` now accepts scroll wheel keys, and the settings key editor can capture them. Mouse button and wheel bindings from mpv now work while the cursor is over the overlay, including on Hyprland.
|
||||
- **Run Commands from Session Help**: During playback, press `Enter` or double-click a row in session help to run that command.
|
||||
|
||||
### Changed
|
||||
- **Release Assets**: Releases no longer include package-size reports.
|
||||
|
||||
### Fixed
|
||||
- **Hyprland Overlay**: The overlay stays clickable after mpv goes fullscreen with `mpv.backend: x11`, and it no longer interrupts mpv while mpv enters fullscreen.
|
||||
- **mpv Key Bindings**: Bindings from mpv such as `9`/`0` for volume now work while the overlay has focus, even if the overlay loaded before mpv connected.
|
||||
- **Jellyfin**:
|
||||
- The Japanese subtitle track is selected as soon as it downloads, without waiting for the other tracks.
|
||||
- Japanese and English tracks are auto-selected on episodes whose embedded subtitles are images.
|
||||
- Timing review previews and mined card audio use the audio track you are playing.
|
||||
- **Media Timing Review**: Trim handles are now thin lines on the clip edges, and original subtitle boundaries no longer cover the waveform. The preview cursor no longer stutters on macOS with Bluetooth headphones.
|
||||
- **YouTube Subtitles**: Japanese subtitles no longer fail with "HTTP 429" on videos where YouTube also lists a machine-translated Japanese track.
|
||||
- **Stats**:
|
||||
- Mined cards are counted once in session and lifetime stats.
|
||||
- Word and kanji dates and the new-words charts stay accurate after deleting sessions. Existing data is repaired in the background.
|
||||
- Overview totals, charts, and the activity calendar refresh after deleting sessions.
|
||||
- **Yomitan Data Safety**: Electron is updated to 43.7.7. SubMiner now refuses to load your Yomitan data with an unsupported or downgraded runtime. Automatic character dictionary changes stop if Yomitan suddenly reports no dictionaries.
|
||||
- **AppImage in Sandboxes**: The AppImage now opens its window when launched in a sandbox such as `firejail --appimage`.
|
||||
- **Ctrl+C Crash**: SubMiner no longer crashes with "write EPIPE" when the terminal command that started it is stopped with Ctrl+C.
|
||||
|
||||
### Docs
|
||||
- **Docs Site**: The docs site is shorter and easier to scan, with a key and default table for each config block. Outdated pages were corrected, and the MeCab install command for Arch Linux is fixed.
|
||||
- **Stats Docs**: The docs now cover the Overview refresh after deleting sessions and how discovery dates are kept after old sessions are pruned.
|
||||
|
||||
<details>
|
||||
<summary>Internal changes</summary>
|
||||
|
||||
### Internal
|
||||
- **Release Notes**: Release notes and `CHANGELOG.md` are now generated as short items per feature or area.
|
||||
- **Dev Profile**: Development launches use a separate `SubMiner-dev` profile unless production-profile access is requested.
|
||||
- **Dependencies**: undici is updated to 7.29.1 to clear the security advisories that were blocking CI.
|
||||
- **Package Checks**: CI no longer compares package sizes. Package content checks still run.
|
||||
|
||||
</details>
|
||||
|
||||
## v0.20.0 (2026-09-23)
|
||||
|
||||
### Added
|
||||
|
||||
@@ -21,11 +21,10 @@ MACOS_DATA_DIR ?= $(HOME)/Library/Application Support/SubMiner
|
||||
MACOS_APP_DIR ?= $(HOME)/Applications
|
||||
MACOS_APP_DEST ?= $(MACOS_APP_DIR)/SubMiner.app
|
||||
|
||||
# If building from source, the AppImage will typically land in release/.
|
||||
APPIMAGE_SRC = $(firstword $(wildcard release/SubMiner-*.AppImage))
|
||||
MACOS_APP_SRC = $(firstword $(wildcard release/*.app release/*/*.app))
|
||||
MACOS_ZIP_SRC = $(firstword $(wildcard release/SubMiner-*.zip))
|
||||
PRERELEASE_NOTES := release/prerelease-notes.md
|
||||
# If building from source, the AppImage will typically land in dist/release/.
|
||||
APPIMAGE_SRC = $(firstword $(wildcard dist/release/SubMiner-*.AppImage))
|
||||
MACOS_APP_SRC = $(firstword $(wildcard dist/release/*.app dist/release/*/*.app))
|
||||
MACOS_ZIP_SRC = $(firstword $(wildcard dist/release/SubMiner-*.zip))
|
||||
|
||||
UNAME_S := $(shell uname -s 2>/dev/null || echo Unknown)
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@@ -57,7 +56,7 @@ help:
|
||||
" build-linux Build Linux AppImage" \
|
||||
" build-macos Build macOS DMG/ZIP (signed if configured)" \
|
||||
" build-macos-unsigned Build macOS DMG/ZIP without signing/notarization" \
|
||||
" clean Remove build artifacts (dist/, release/, AppImage, binary)" \
|
||||
" clean Remove build artifacts (dist/, AppImage, binary)" \
|
||||
" dev-start Build and launch local Electron app" \
|
||||
" dev-start-macos Build and launch local Electron app with macOS tracker backend" \
|
||||
" dev-watch Start fast watch loop (tsc + renderer + Electron dev app)" \
|
||||
@@ -165,15 +164,7 @@ build-launcher:
|
||||
|
||||
clean:
|
||||
@printf '%s\n' "[INFO] Removing build artifacts"
|
||||
@if [ -f "$(PRERELEASE_NOTES)" ]; then \
|
||||
PRERELEASE_NOTES_BACKUP="$$(mktemp -t subminer-prerelease-notes.XXXXXX)" && \
|
||||
cp "$(PRERELEASE_NOTES)" "$$PRERELEASE_NOTES_BACKUP" && \
|
||||
rm -rf dist release && \
|
||||
install -d release && \
|
||||
mv "$$PRERELEASE_NOTES_BACKUP" "$(PRERELEASE_NOTES)"; \
|
||||
else \
|
||||
rm -rf dist release; \
|
||||
fi
|
||||
@rm -rf dist
|
||||
@rm -f "$(BINDIR)/subminer" "$(BINDIR)/SubMiner.AppImage"
|
||||
|
||||
generate-config: ensure-bun
|
||||
@@ -229,7 +220,7 @@ install-linux: build-launcher
|
||||
@if [ -n "$(APPIMAGE_SRC)" ]; then \
|
||||
install -m 0755 "$(APPIMAGE_SRC)" "$(BINDIR)/SubMiner.AppImage"; \
|
||||
else \
|
||||
printf '%s\n' "[WARN] No release/SubMiner-*.AppImage found; skipping AppImage install"; \
|
||||
printf '%s\n' "[WARN] No dist/release/SubMiner-*.AppImage found; skipping AppImage install"; \
|
||||
printf '%s\n' " Build one with: make build"; \
|
||||
fi
|
||||
@printf '%s\n' "Installed to:" " $(BINDIR)/subminer" " $(LINUX_DATA_DIR)/themes/$(THEME_FILE)" " $(LINUX_DATA_DIR)/thumbnailers/$(THUMBNAILER_FILE)"
|
||||
@@ -252,7 +243,7 @@ install-macos: build-launcher
|
||||
ditto -x -k "$(MACOS_ZIP_SRC)" "$(MACOS_APP_DIR)"; \
|
||||
printf '%s\n' "[INFO] Installed app bundle from $(MACOS_ZIP_SRC)"; \
|
||||
else \
|
||||
printf '%s\n' "[WARN] No macOS app bundle or zip found in release/; skipping app install"; \
|
||||
printf '%s\n' "[WARN] No macOS app bundle or zip found in dist/release/; skipping app install"; \
|
||||
printf '%s\n' " Build one with: make build"; \
|
||||
fi
|
||||
@printf '%s\n' "Installed to:" " $(BINDIR)/subminer" " $(MACOS_DATA_DIR)/themes/$(THEME_FILE)" " $(MACOS_APP_DEST)"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
# SubMiner
|
||||
|
||||
Look up words with Yomitan, mine them to Anki, and track your immersion without leaving mpv
|
||||
Look up words with Yomitan or Hachidori, mine them to Anki, and track your immersion without leaving mpv
|
||||
|
||||
[Installation](#quick-start) · [Requirements](#requirements) · [Usage](https://docs.subminer.moe/usage) · [Documentation](https://docs.subminer.moe)
|
||||
|
||||
@@ -25,6 +25,8 @@ Look up words with Yomitan, mine them to Anki, and track your immersion without
|
||||
|
||||
Hover over any word in the subtitles to open the full Yomitan popup with definitions, pitch accent, and frequency data. SubMiner bundles its own Yomitan, separate from any browser install.
|
||||
|
||||
Yomitan remains the default. Select the bundled [Hachidori](https://github.com/bee-san/hachidori) backend with `dictionaryBackend: "hachidori"` and restart SubMiner. The tray opens the selected backend's settings. See [dictionary setup](https://docs.subminer.moe/usage#hachidori-setup) for importing dictionaries, linking an external Hachidori host, and configuring Anki.
|
||||
|
||||
<div align="center">
|
||||
<img src="docs-site/public/screenshots/yomitan-lookup.png" width="800" alt="Yomitan dictionary popup over annotated subtitles in mpv">
|
||||
</div>
|
||||
@@ -36,7 +38,7 @@ Hover over any word in the subtitles to open the full Yomitan popup with definit
|
||||
Create an Anki card from the exact playback moment with one key press, click, or controller input. SubMiner fills in the sentence, an audio clip, and a screenshot or animated image.
|
||||
|
||||
<div align="center">
|
||||
<img src="docs-site/public/screenshots/one-key-mining.png" width="800" alt="Anki card created from SubMiner with sentence, audio, and screenshot">
|
||||
<img src="docs-site/public/screenshots/one-key-mining.png" width="800" alt="Yomitan lookup over an anime scene with Anki mining controls">
|
||||
</div>
|
||||
|
||||
<br>
|
||||
@@ -46,7 +48,7 @@ Create an Anki card from the exact playback moment with one key press, click, or
|
||||
Subtitles are annotated as they play with frequency highlighting, JLPT tags, N+1 targeting, and character names from a generated dictionary. Particles and grammar-only tokens stay plain so the words worth learning stand out.
|
||||
|
||||
<div align="center">
|
||||
<img src="docs-site/public/screenshots/annotations.png" width="800" alt="Annotated subtitles with frequency coloring, JLPT underlines, and N+1 targets">
|
||||
<img src="docs-site/public/screenshots/annotations.png" width="800" alt="Annotated subtitles with frequency colors, character portraits, and JLPT underlines">
|
||||
</div>
|
||||
|
||||
<br>
|
||||
@@ -100,6 +102,10 @@ A stats dashboard tracks watch time, vocabulary growth, mining throughput, sessi
|
||||
<td><b>WebSocket</b></td>
|
||||
<td>Plain subtitle feed plus a dedicated annotated feed for texthooker pages and custom tools</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Plugins</b></td>
|
||||
<td>Load your own JavaScript at startup to react to playback, run timers, add key-bound actions, and show results in an overlay panel. The bundled example explains the current line with Codex. <a href="https://docs.subminer.moe/plugins">Plugin guide</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br>
|
||||
@@ -199,7 +205,7 @@ The AppImage is all you need. First-run setup can install the optional `subminer
|
||||
|
||||
Download the latest DMG from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) and drag `SubMiner.app` into `/Applications`.
|
||||
|
||||
Then enable SubMiner under **System Settings > Privacy & Security > Accessibility**, or the overlay cannot follow the mpv window. If macOS blocks the first launch, right-click the app and choose **Open**.
|
||||
Then enable SubMiner under **System Settings > Privacy & Security > Accessibility** (named **Device Control and Data Access** on macOS 27) so the overlay can track the mpv window. Without it, SubMiner falls back to a basic window lookup that cannot tell when mpv is minimized. If macOS blocks the first launch, right-click the app and choose **Open**.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -265,7 +271,8 @@ SubMiner builds on the work of these open-source projects:
|
||||
| [jellyfin-mpv-shim](https://github.com/jellyfin/jellyfin-mpv-shim) | Jellyfin integration |
|
||||
| [Jimaku.cc](https://jimaku.cc) | Japanese subtitle search and downloads |
|
||||
| [Renji's Texthooker Page](https://github.com/Renji-XD/texthooker-ui) | Base for the WebSocket texthooker integration |
|
||||
| [Yomitan](https://github.com/yomidevs/yomitan) | Dictionary engine powering all lookups and the morphological parser |
|
||||
| [Yomitan](https://github.com/yomidevs/yomitan) | Default dictionary engine and morphological parser |
|
||||
| [Hachidori](https://github.com/bee-san/hachidori) | Alternative dictionary backend, powered by HoshiDicts |
|
||||
| [yomitan-jlpt-vocab](https://github.com/stephenmk/yomitan-jlpt-vocab) | JLPT level tags for vocabulary |
|
||||
|
||||
## License
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// Sends the current subtitle line, with a few lines of context, to `codex exec` and shows the
|
||||
// answer in the SubMiner plugin panel. Registers two actions: `explain` and `translate`.
|
||||
const { mkdtemp, readFile, rm } = require('node:fs/promises');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
codexPath: 'codex',
|
||||
model: '',
|
||||
// Passed as -c model_reasoning_effort; low keeps answers to a few seconds. Empty uses your Codex config.
|
||||
reasoningEffort: 'low',
|
||||
before: 2,
|
||||
after: 1,
|
||||
timeoutMs: 120_000,
|
||||
pause: true,
|
||||
prompts: {},
|
||||
};
|
||||
|
||||
const ACTIONS = {
|
||||
explain: {
|
||||
title: 'Grammar',
|
||||
prompt:
|
||||
'You are a Japanese tutor. Explain the line marked >>> for an intermediate learner. ' +
|
||||
'Use the other lines only as context. Reply in English markdown: a natural translation, ' +
|
||||
'then a bullet list of the grammar points and notable vocabulary. Be concise.',
|
||||
},
|
||||
translate: {
|
||||
title: 'Translation',
|
||||
prompt:
|
||||
'Translate the line marked >>> into natural English. Use the other lines only as context. ' +
|
||||
'Reply with the translation, then one short note only if the line is ambiguous.',
|
||||
},
|
||||
};
|
||||
|
||||
function activate(ctx) {
|
||||
const settings = { ...DEFAULT_SETTINGS, ...ctx.settings };
|
||||
for (const [name, action] of Object.entries(ACTIONS)) {
|
||||
const prompt = settings.prompts?.[name] ?? action.prompt;
|
||||
ctx.actions.register(name, () => ask(ctx, settings, action.title, prompt));
|
||||
}
|
||||
}
|
||||
|
||||
function quote(text) {
|
||||
return text
|
||||
.split('\n')
|
||||
.map((line) => `> ${line}`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
// Subtitles wrap across lines; flatten each cue so the >>> marker covers all of it.
|
||||
function oneLine(text) {
|
||||
return text
|
||||
.split('\n')
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function tail(text, lineCount) {
|
||||
return text.trim().split('\n').slice(-lineCount).join('\n');
|
||||
}
|
||||
|
||||
async function ask(ctx, settings, title, instructions) {
|
||||
const { before, current, after } = ctx.subtitles.context({
|
||||
before: settings.before,
|
||||
after: settings.after,
|
||||
});
|
||||
if (!current) {
|
||||
ctx.notify('No subtitle line to send.', { variant: 'warning' });
|
||||
return;
|
||||
}
|
||||
if (settings.pause) ctx.mpv.command(['set', 'pause', 'yes']);
|
||||
|
||||
const header = quote(current.text);
|
||||
const panel = ctx.ui.panel({ title, markdown: `${header}\n\n_Asking Codex..._` });
|
||||
const prompt = [
|
||||
instructions,
|
||||
'',
|
||||
'Subtitles:',
|
||||
...before.map((line) => oneLine(line.text)),
|
||||
`>>> ${oneLine(current.text)}`,
|
||||
...after.map((line) => oneLine(line.text)),
|
||||
].join('\n');
|
||||
|
||||
// Codex runs in an empty read-only directory so any command it tries cannot touch real files.
|
||||
const workDir = await mkdtemp(path.join(os.tmpdir(), 'subminer-ai-explain-'));
|
||||
const answerPath = path.join(workDir, 'answer.md');
|
||||
const args = [
|
||||
'exec',
|
||||
'--ephemeral',
|
||||
'--skip-git-repo-check',
|
||||
'--sandbox',
|
||||
'read-only',
|
||||
'--color',
|
||||
'never',
|
||||
'-C',
|
||||
workDir,
|
||||
'-o',
|
||||
answerPath,
|
||||
];
|
||||
if (settings.model) args.push('-m', settings.model);
|
||||
if (settings.reasoningEffort) {
|
||||
args.push('-c', `model_reasoning_effort=${JSON.stringify(settings.reasoningEffort)}`);
|
||||
}
|
||||
args.push('-');
|
||||
|
||||
try {
|
||||
const result = await ctx.exec(settings.codexPath, args, {
|
||||
stdin: prompt,
|
||||
timeoutMs: settings.timeoutMs,
|
||||
signal: panel.signal,
|
||||
});
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(tail(result.stderr, 6) || `codex exited with code ${result.exitCode}`);
|
||||
}
|
||||
const answer = (await readFile(answerPath, 'utf8')).trim();
|
||||
panel.update({ markdown: `${header}\n\n${answer || '_Codex returned an empty answer._'}` });
|
||||
} catch (error) {
|
||||
// Closing the panel aborts the request; there is nobody left to show the error to.
|
||||
if (panel.signal.aborted) return;
|
||||
const message =
|
||||
error?.code === 'ENOENT'
|
||||
? `Cannot find \`${settings.codexPath}\`. Set \`plugins.ai-explain.settings.codexPath\` to its full path.`
|
||||
: String(error?.message ?? error);
|
||||
ctx.log.warn(`Codex request failed: ${message}`);
|
||||
panel.update({
|
||||
markdown: `${header}\n\n**Codex request failed.**\n\n\`\`\`\n${message}\n\`\`\``,
|
||||
});
|
||||
} finally {
|
||||
await rm(workDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { activate };
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "AI Explain",
|
||||
"version": "1.0.0",
|
||||
"apiVersion": 1,
|
||||
"main": "main.js"
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Runs AnkiConnect's `sync` (AnkiWeb) after you mine cards. A burst of mining becomes one sync:
|
||||
// it waits until no card has been mined for `delayMs`, and syncs right away when playback ends.
|
||||
// Also registers a `sync` action for a manual sync.
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
delayMs: 30_000,
|
||||
syncOnEnd: true,
|
||||
timeoutMs: 60_000,
|
||||
notify: true,
|
||||
};
|
||||
|
||||
const END_REASONS = new Set(['eof', 'stop', 'quit']);
|
||||
|
||||
function activate(ctx) {
|
||||
const settings = { ...DEFAULT_SETTINGS, ...ctx.settings };
|
||||
// Cards mined since the last sync started.
|
||||
let dirty = false;
|
||||
let syncing = false;
|
||||
let cancelTimer = null;
|
||||
|
||||
function schedule() {
|
||||
cancelTimer?.();
|
||||
cancelTimer = ctx.after(settings.delayMs, flush);
|
||||
}
|
||||
|
||||
async function flush() {
|
||||
cancelTimer?.();
|
||||
cancelTimer = null;
|
||||
// A sync already running picks up new cards when it finishes.
|
||||
if (!dirty || syncing) return;
|
||||
dirty = false;
|
||||
syncing = true;
|
||||
try {
|
||||
await ctx.anki.invoke('sync', {}, { timeoutMs: settings.timeoutMs });
|
||||
ctx.log.info('Synced Anki.');
|
||||
if (settings.notify) ctx.notify('Anki synced');
|
||||
} catch (error) {
|
||||
ctx.log.warn(`Anki sync failed: ${error.message}`);
|
||||
ctx.notify(`Anki sync failed: ${error.message}`, { variant: 'error' });
|
||||
} finally {
|
||||
syncing = false;
|
||||
if (dirty) schedule();
|
||||
}
|
||||
}
|
||||
|
||||
ctx.anki.on('card-mined', () => {
|
||||
dirty = true;
|
||||
schedule();
|
||||
});
|
||||
|
||||
ctx.mpv.on('end-file', ({ reason }) => {
|
||||
if (settings.syncOnEnd && END_REASONS.has(reason)) return flush();
|
||||
});
|
||||
|
||||
ctx.actions.register('sync', () => {
|
||||
dirty = true;
|
||||
return flush();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { activate };
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Anki Sync",
|
||||
"version": "1.0.0",
|
||||
"apiVersion": 1,
|
||||
"main": "main.js"
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Tags each mined card with the show and episode it came from, e.g. `show::Frieren::S01E07`.
|
||||
// The show and episode are parsed from the file name, or from mpv's media title for streams.
|
||||
const path = require('node:path');
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
// Placeholders: {show}, {season}, {episode}, and {ep} (S01E07, or E07 without a season).
|
||||
// A tag stops before the first `::` level whose placeholder has no value.
|
||||
tags: ['show::{show}::{ep}'],
|
||||
notify: false,
|
||||
};
|
||||
|
||||
// Tried in order; the first match wins. Runs on names with brackets and dots already removed.
|
||||
const EPISODE_PATTERNS = [
|
||||
/^(?<show>.*?)\s*S(?<season>\d{1,2})\s?E(?<episode>\d{1,4})\b/i,
|
||||
/^(?<show>.*?)\s*(?:Season\s?(?<season>\d{1,2})\s*)?(?:Episode|Ep)\s?(?<episode>\d{1,4})\b/i,
|
||||
/^(?<show>.*?)\s+-\s+(?<episode>\d{1,4})(?:v\d)?\b/,
|
||||
/^(?<show>.*?)\s+(?<episode>\d{1,4})(?:v\d)?$/,
|
||||
/^(?<show>.*?)\s*第(?<episode>\d{1,4})話/,
|
||||
];
|
||||
|
||||
// `[Group] Show Name - 07 (1080p).mkv` -> { show: 'Show Name', season: null, episode: '07' }
|
||||
function parseMediaName(name) {
|
||||
const cleaned = name
|
||||
.replace(/\.[a-z0-9]{2,4}$/i, '')
|
||||
.replace(/\[[^\]]*\]|\([^)]*\)|【[^】]*】/g, ' ')
|
||||
.replace(/[._]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
for (const pattern of EPISODE_PATTERNS) {
|
||||
const groups = cleaned.match(pattern)?.groups;
|
||||
const show = groups?.show.replace(/[\s-]+$/, '');
|
||||
if (!show) continue;
|
||||
return { show, season: pad(groups.season), episode: pad(groups.episode) };
|
||||
}
|
||||
return { show: cleaned || null, season: null, episode: null };
|
||||
}
|
||||
|
||||
function pad(number) {
|
||||
return number ? String(Number(number)).padStart(2, '0') : null;
|
||||
}
|
||||
|
||||
// Anki tags can't hold spaces, and a `::` in a value would add a hierarchy level.
|
||||
function tagValue(value) {
|
||||
return value.replace(/::?/g, ' ').trim().replace(/\s+/g, '_').replace(/"/g, '');
|
||||
}
|
||||
|
||||
function renderTag(template, values) {
|
||||
const levels = [];
|
||||
for (const level of template.split('::')) {
|
||||
let missing = false;
|
||||
const rendered = level.replace(/\{(\w+)\}/g, (_, key) => {
|
||||
const value = values[key];
|
||||
if (!value) missing = true;
|
||||
return value ? tagValue(value) : '';
|
||||
});
|
||||
if (missing) break;
|
||||
levels.push(rendered);
|
||||
}
|
||||
return levels.join('::');
|
||||
}
|
||||
|
||||
function mediaName(mediaPath, title) {
|
||||
// A stream URL's last segment is usually an id, so streams use the title mpv shows.
|
||||
if (typeof mediaPath === 'string' && mediaPath && !/^[a-z]+:\/\//i.test(mediaPath)) {
|
||||
return path.basename(mediaPath);
|
||||
}
|
||||
return typeof title === 'string' && title ? title : null;
|
||||
}
|
||||
|
||||
function renderTags(templates, name) {
|
||||
const { show, season, episode } = parseMediaName(name);
|
||||
const ep = episode ? `${season ? `S${season}` : ''}E${episode}` : null;
|
||||
const values = { show, season, episode, ep };
|
||||
return [...new Set(templates.map((template) => renderTag(template, values)))].filter(Boolean);
|
||||
}
|
||||
|
||||
function activate(ctx) {
|
||||
const settings = { ...DEFAULT_SETTINGS, ...ctx.settings };
|
||||
ctx.anki.on('card-mined', async ({ notes }) => {
|
||||
// Notes from the same file share one addTags call.
|
||||
const notesByName = new Map();
|
||||
for (const { noteId, provenance } of notes) {
|
||||
const name = mediaName(provenance?.media?.path, provenance?.media?.title);
|
||||
if (!name) {
|
||||
ctx.log.debug(`Note ${noteId} has no known media source; not tagging it.`);
|
||||
continue;
|
||||
}
|
||||
if (!notesByName.has(name)) notesByName.set(name, new Set());
|
||||
notesByName.get(name).add(noteId);
|
||||
}
|
||||
|
||||
for (const [name, notes] of notesByName) {
|
||||
const tags = renderTags(settings.tags, name);
|
||||
if (tags.length === 0) {
|
||||
ctx.log.debug(`No tags for "${name}".`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await ctx.anki.invoke('addTags', { notes: [...notes], tags: tags.join(' ') });
|
||||
ctx.log.info(`Tagged ${notes.size} note(s): ${tags.join(' ')}`);
|
||||
if (settings.notify) ctx.notify(`Tagged ${tags.join(' ')}`);
|
||||
} catch (error) {
|
||||
ctx.log.warn(`Tagging failed: ${error.message}`);
|
||||
ctx.notify(`Episode tagging failed: ${error.message}`, { variant: 'error' });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { activate };
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Episode Tags",
|
||||
"version": "1.0.0",
|
||||
"apiVersion": 1,
|
||||
"main": "main.js"
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
"": {
|
||||
"name": "subminer",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/geist": "^5.2.8",
|
||||
"@fontsource-variable/geist-mono": "^5.2.7",
|
||||
"@xhayper/discord-rpc": "^1.3.4",
|
||||
"axios": "^1.20.0",
|
||||
"commander": "^14.0.3",
|
||||
@@ -21,25 +19,22 @@
|
||||
"@electron/asar": "3.4.1",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"electron": "43.7.7",
|
||||
"electron": "43.8.3",
|
||||
"electron-builder": "26.16.1",
|
||||
"esbuild": "^0.25.12",
|
||||
"eslint": "^10.8.0",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "^5.9.3",
|
||||
"undici": "7.29.1",
|
||||
"undici": "7.30.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@discordjs/rest@2.6.1": "patches/@discordjs%2Frest@2.6.1.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@xmldom/xmldom": "0.8.15",
|
||||
"app-builder-lib": "26.16.1",
|
||||
"brace-expansion": "5.0.11",
|
||||
"electron-builder-squirrel-windows": "26.16.1",
|
||||
"fast-uri": "3.1.7",
|
||||
"follow-redirects": "1.16.2",
|
||||
"form-data": "4.0.6",
|
||||
"ip-address": "10.2.0",
|
||||
"js-yaml": "4.3.2",
|
||||
@@ -48,7 +43,7 @@
|
||||
"picomatch": "4.0.4",
|
||||
"tar": "7.5.21",
|
||||
"tmp": "0.2.7",
|
||||
"undici": "7.29.1",
|
||||
"undici": "7.30.1",
|
||||
},
|
||||
"packages": {
|
||||
"@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
|
||||
@@ -127,34 +122,6 @@
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
|
||||
"@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
|
||||
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.7.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw=="],
|
||||
|
||||
"@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="],
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.2", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A=="],
|
||||
|
||||
"@fontsource-variable/geist": ["@fontsource-variable/geist@5.2.8", "", {}, "sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw=="],
|
||||
|
||||
"@fontsource-variable/geist-mono": ["@fontsource-variable/geist-mono@5.2.7", "", {}, "sha512-ZKlZ5sjtalb2TwXKs400mAGDlt/+2ENLNySPx0wTz3bP3mWARCsUW+rpxzZc7e05d2qGch70pItt3K4qttbIYA=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
|
||||
|
||||
"@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="],
|
||||
|
||||
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||
|
||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||
|
||||
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
||||
|
||||
"@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.5.28", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Lc/b8JXO2W2+H+5UXfw7PCHZCim1jlrB0CmLPsjfVmihMluBpdYafFImhjAHxHlWGfuZ32WzjVPUap5fGmkthw=="],
|
||||
@@ -203,16 +170,10 @@
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
||||
|
||||
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="],
|
||||
|
||||
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="],
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
@@ -231,13 +192,9 @@
|
||||
|
||||
"abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||
|
||||
"ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||
"ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
@@ -317,8 +274,6 @@
|
||||
|
||||
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
||||
"defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],
|
||||
|
||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||
@@ -347,7 +302,7 @@
|
||||
|
||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||
|
||||
"electron": ["electron@43.7.7", "", { "dependencies": { "@electron-internal/extract-zip": "^1.0.1", "@electron/get": "^5.0.0", "@types/node": "^24.9.0" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-y0pg+KhWLAkW2Ts2njkFFNFl3KtB1FmbUp9Te9i+R2Og2v1fcI/I6MADJ7jvnzB5UKdt+U+0EUoCnVL79xMPMA=="],
|
||||
"electron": ["electron@43.8.3", "", { "dependencies": { "@electron-internal/extract-zip": "^1.0.1", "@electron/get": "^5.0.0", "@types/node": "^24.9.0" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-KgvSm3bHXz96hSXzqxyRjLeli80rIdszbDQFKlRSfckmGazTPCH/CxUN31Dp9c5rFKTQSNf9gPJRWl1yWNB3pA=="],
|
||||
|
||||
"electron-builder": ["electron-builder@26.16.1", "", { "dependencies": { "app-builder-lib": "26.16.1", "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.16.1", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "./cli.js", "install-app-deps": "./install-app-deps.js" } }, "sha512-LrLK65QX5PUYYODXqp23FKrV7CILTtVY7mrJckNknO9jLNSMiqFkKbSMiDRw4CjOADMPVDdWLxY4mezOZWswxg=="],
|
||||
|
||||
@@ -383,45 +338,17 @@
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@10.8.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||
|
||||
"espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
|
||||
|
||||
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||
|
||||
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.7", "", {}, "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||
|
||||
"filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="],
|
||||
|
||||
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||
|
||||
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||
|
||||
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="],
|
||||
"follow-redirects": ["follow-redirects@1.16.2", "", {}, "sha512-/kVP2U8z596Ia0gwFrwyIeo74ta5aVmSeU1FfAUMAmcEP5EP7zydmag3SLiN64NaRKef3lxsq7i/g7ltJQDGow=="],
|
||||
|
||||
"form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="],
|
||||
|
||||
@@ -441,8 +368,6 @@
|
||||
|
||||
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
|
||||
|
||||
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
||||
@@ -475,20 +400,12 @@
|
||||
|
||||
"https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
|
||||
|
||||
"isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="],
|
||||
@@ -503,9 +420,7 @@
|
||||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
|
||||
|
||||
@@ -521,12 +436,8 @@
|
||||
|
||||
"lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"libsql": ["libsql@0.5.28", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.28", "@libsql/darwin-x64": "0.5.28", "@libsql/linux-arm-gnueabihf": "0.5.28", "@libsql/linux-arm-musleabihf": "0.5.28", "@libsql/linux-arm64-gnu": "0.5.28", "@libsql/linux-arm64-musl": "0.5.28", "@libsql/linux-x64-gnu": "0.5.28", "@libsql/linux-x64-musl": "0.5.28", "@libsql/win32-x64-msvc": "0.5.28" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-wKqx9FgtPcKHdPfR/Kfm0gejsnbuf8zV+ESPmltFvsq5uXwdeN9fsWn611DmqrdXj1e94NkARcMA2f1syiAqOg=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
"lodash": ["lodash@4.18.0", "", {}, "sha512-l1mfj2atMqndAHI3ls7XqPxEjV2J9ZkcNyHpoZA3r2T1LLwDB69jgkMWh71YKwhBbK0G2f4WSn05ahmQXVxupA=="],
|
||||
|
||||
"lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="],
|
||||
@@ -563,8 +474,6 @@
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"node-abi": ["node-abi@4.28.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g=="],
|
||||
|
||||
"node-api-version": ["node-api-version@0.2.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q=="],
|
||||
@@ -581,16 +490,10 @@
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
|
||||
"p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
@@ -607,8 +510,6 @@
|
||||
|
||||
"postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" } }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
||||
|
||||
"proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="],
|
||||
@@ -625,8 +526,6 @@
|
||||
|
||||
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
|
||||
|
||||
"pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="],
|
||||
@@ -711,13 +610,11 @@
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici": ["undici@7.29.1", "", {}, "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q=="],
|
||||
"undici": ["undici@7.30.1", "", {}, "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
@@ -725,8 +622,6 @@
|
||||
|
||||
"unzipper": ["unzipper@0.12.5", "", { "dependencies": { "bluebird": "~3.7.2", "duplexer2": "~0.1.4", "fs-extra": "11.3.1", "graceful-fs": "^4.2.2", "node-int64": "^0.4.0" } }, "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
@@ -735,8 +630,6 @@
|
||||
|
||||
"which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
@@ -771,8 +664,6 @@
|
||||
|
||||
"@electron/windows-sign/fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
|
||||
|
||||
"@types/cacheable-request/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
||||
@@ -787,8 +678,6 @@
|
||||
|
||||
"app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="],
|
||||
|
||||
"app-builder-lib/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||
|
||||
"app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
|
||||
|
||||
"builder-util/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
@@ -835,8 +724,6 @@
|
||||
|
||||
"app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"app-builder-lib/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"builder-util/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
+2
-2
@@ -44,11 +44,11 @@ How fragments turn into a release:
|
||||
- The polish step treats pending fragments as the final release outcome, not prerelease history. If a feature is added and then renamed or fixed before the stable cut, ship the final feature bullet instead of separate prerelease-only breaking/fix entries.
|
||||
- `CHANGELOG.md`, GitHub release notes, and prerelease notes all use one item per feature or area, inline by default: `- **Name**: one or two short sentences.` Items with several separate changes may use a few short nested bullets instead of a run-on sentence. Items say what changed and how to reach it; implementation details, edge cases, and step-by-step usage belong in the docs, not the release body.
|
||||
- `internal` fragments stay in `CHANGELOG.md` (inside a collapsed `<details>` block) but are dropped from the GitHub release notes entirely.
|
||||
- The polished `CHANGELOG.md` and `release/release-notes.md` are committed and reviewed before tagging — edit the Markdown by hand if Claude misses something.
|
||||
- The polished `CHANGELOG.md` and `packaging/release-notes/release-notes.md` are committed and reviewed before tagging — edit the Markdown by hand if Claude misses something.
|
||||
|
||||
Prerelease notes:
|
||||
|
||||
- prerelease tags like `v0.11.3-beta.1` and `v0.11.3-rc.1` reuse the current pending fragments to generate `release/prerelease-notes.md`
|
||||
- prerelease tags like `v0.11.3-beta.1` and `v0.11.3-rc.1` reuse the current pending fragments to generate `packaging/release-notes/prerelease-notes.md`
|
||||
- from the second prerelease of a base version onward, the notes also open with a `## Changes since <previous tag>` section generated from the fragment diff against the previous beta/RC tag; keep fragment edits meaningful. Editorial-only rewording is filtered out of that section, while genuinely changed behavior and deleted fragments (reverted changes) are reported
|
||||
- existing prerelease notes are a reviewed baseline; later prerelease runs should replace stale beta/RC wording with the current outcome instead of appending fix churn
|
||||
- prerelease note generation does not consume fragments and does not update `CHANGELOG.md` or `docs-site/changelog.md`
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- The controller settings window (`Alt+C`) no longer edits a controller it cannot see. A newly opened window cannot see any controller until you press one of its buttons (browsers hide a pad from a new window until then); it used to show and edit the global defaults meanwhile, and its status could stay stuck on "No controllers detected". Now it shows the bindings of the controller the overlay is using (or the saved one), labelled as a preview, and Learn, Clear, Reset and the button style are locked until the press.
|
||||
- With several controllers connected, `Alt+C` now selects the one you just pressed, and Learn listens to that controller.
|
||||
@@ -0,0 +1,5 @@
|
||||
type: fixed
|
||||
area: anilist
|
||||
|
||||
- AniList progress sync now adds a show to your Watching list (or Completed on the final episode) when it is not on your list yet, instead of failing with "AniList update not possible". This only happens when the file title exactly matches an AniList title or the entry is pinned with the character dictionary override; loose matches still require adding the show yourself.
|
||||
- Entries marked Completed, Paused, Dropped, or Rewatching are still never changed.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: fixed
|
||||
area: anki
|
||||
|
||||
- `ankiConnect.fields.audio` now defaults to `SentenceAudio` instead of `ExpressionAudio`, so generated sentence audio no longer overwrites Yomitan's word audio on default configs. `fields.wordAudio` still defaults to `ExpressionAudio`. If you relied on the old default, set `fields.audio` to `ExpressionAudio` explicitly.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: fixed
|
||||
area: linux
|
||||
|
||||
- Fixed AppImage builds on Ubuntu 26.04+ (uutils coreutils) releasing their mount before Chromium child processes finished shutting down, which brought back "Service Crash" notifications on quit.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: fixed
|
||||
area: linux
|
||||
|
||||
- Fixed the AppImage exiting without opening a window when launched inside a sandbox that mounts the image itself, such as `firejail --appimage` (used by the AppImage catalog test). The detached background app now runs straight from the sandbox's squashfs mount instead of trying to re-mount the AppImage over FUSE.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: fixed
|
||||
area: app
|
||||
|
||||
- SubMiner no longer crashes with "write EPIPE" when the terminal command that started it is stopped with Ctrl+C.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: internal
|
||||
area: release
|
||||
|
||||
- Release notes, prerelease notes, and `CHANGELOG.md` are now generated as short items per feature or area, nesting only when an item has several separate changes.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- Controller remaps made in `Alt+C` now apply right away; before, the overlay kept the old bindings until SubMiner restarted.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: added
|
||||
area: overlay
|
||||
|
||||
- `Alt+C` now warns when two controller actions share an input, as the dialog layouts already did. Both rows show "⚠ Same as …", including a button on the D-pad while a **(D-pad)** row under **Navigation** is still on, and a stick direction used by a stick action.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: added
|
||||
area: overlay
|
||||
|
||||
- `Alt+C` has a **Button Style** choice (Xbox, Nintendo or PlayStation), saved per controller. PlayStation is new: buttons and the review and duplicate card shortcuts show ╳ ○ □ △. `controller.faceButtonLabels` accepts `playstation` and stays the default for controllers without their own style.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: changed
|
||||
area: overlay
|
||||
|
||||
- Controller connected and controller-disabled messages are now regular notifications that follow `ankiConnect.behavior.notificationType` (overlay, system, both or none) instead of a separate pill in the corner of the overlay. Connected notices only appear when `controller.enabled` is on.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- Clicking a row in `Alt+C` shows its **Learn**, **Clear** and **Reset** buttons again. The panel opened but was squeezed to nothing once the list was taller than the window, so **Clear** (set a binding to None) could not be reached.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: added
|
||||
area: overlay
|
||||
|
||||
- The duplicate card (field grouping) dialog has a controller layout: the D-pad picks the original or new card, `A` continues and confirms the merge, `B` cancels or goes back from the preview, `X` turns "Delete duplicate" on or off, `Y` switches Compact / Full, and the D-pad scrolls the preview. Each control shows its shortcut while a controller is connected, and the layout can be changed per controller in the new **Duplicate card (field grouping)** group of `Alt+C`.
|
||||
@@ -0,0 +1,5 @@
|
||||
type: added
|
||||
area: overlay
|
||||
|
||||
- Added a controller layout for the media timing review dialog covering every button, with L1 / R1 + D-pad combos for the clip start and end, and D-pad actions that repeat while held. Each button shows its controller shortcut while a controller is connected, and the first press in a newly opened dialog already counts. The controller's usual actions pause while the dialog is open, including in the hidden subtitle overlay behind it.
|
||||
- Added `controller.faceButtonLabels` (`xbox` or `nintendo`) so fixed controller layouts and the `Alt+C` button names follow the letters printed on the pad. Use `nintendo` for controllers with A on the right and B at the bottom.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: changed
|
||||
area: overlay
|
||||
|
||||
- The controller's **Toggle MPV Pause** button now works without keyboard-only mode, so you can pause while just watching.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: added
|
||||
area: overlay
|
||||
|
||||
- The media timing review controller layout can now be changed in the new **Review media timing** group of `Alt+C`. Learn records a single button or a combo (hold one button, press another), any button can be a hold button, changes are saved per controller, rows warn about clashing inputs, and **Reset all** restores the default layout. The review's button hints follow your bindings.
|
||||
@@ -0,0 +1,5 @@
|
||||
type: added
|
||||
area: overlay
|
||||
|
||||
- Added a controller binding that shows the secondary subtitle only while a button is held (`controller.bindings.holdSecondarySub`). It is unbound by default; assign it with Learn in the `Alt+C` controller modal. It works with or without keyboard-only mode and does not change the saved secondary subtitle mode.
|
||||
- Added controller bindings to select the previous or next word entry in the open Yomitan popup (`controller.bindings.previousEntry` / `nextEntry`). Mine Card mines the selected entry. Both are unbound by default; assign them with Learn in the `Alt+C` controller modal.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- The `Alt+C` controller screen now shows the right button names for controllers that report the W3C standard layout (most Xbox, PlayStation, Switch and 8BitDo pads). Previously Select and Start showed as `L3` and `R3`, and the triggers as `Back` and `Start`. Bindings themselves were unaffected.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: added
|
||||
area: overlay
|
||||
|
||||
- New controller actions for subtitle lines, in the **Subtitles** group of `Alt+C`: **Previous Subtitle**, **Next Subtitle**, **Replay Subtitle** and **Play Next Subtitle**, matching `Shift+H`, `Shift+L`, `Ctrl+Shift+H` and `Ctrl+Shift+L`. They are unbound by default and work with or without keyboard-only mode.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: changed
|
||||
area: overlay
|
||||
|
||||
- With the Xbox **Button Style**, `Alt+C` names the face buttons `A`, `B`, `X` and `Y` instead of `A / Cross`, `B / Circle`, `X / Square` and `Y / Triangle`; PlayStation and Nintendo already showed their own names.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: docs
|
||||
area: overlay
|
||||
|
||||
- Listed the keyboard-only `C` key in Keyboard shortcuts, and pointed the configuration reference to Controller support for default button actions.
|
||||
@@ -1,7 +0,0 @@
|
||||
type: docs
|
||||
area: docs
|
||||
|
||||
- Rewrote the docs site to be shorter and easier to scan: pages lead with setup and use, reference material lives in compact tables, and internal detail was cut from user pages.
|
||||
- The configuration reference now has a short explanation and a key/default table for each config block.
|
||||
- Fixed docs that no longer matched current behavior.
|
||||
- Fixed the Arch Linux install command for MeCab, which is only in the AUR (`mecab-git`), and brought the README requirements and quick start in line with the installation guide.
|
||||
@@ -1,6 +0,0 @@
|
||||
type: fixed
|
||||
area: dictionary
|
||||
|
||||
- Upgraded the desktop runtime to Electron 43.7.7 and added profile guards that block unsupported runtimes and Electron major or minor downgrades before Yomitan storage is loaded. Patch downgrades within the same major and minor version are allowed.
|
||||
- Development launches now use a separate `SubMiner-dev` profile unless production-profile access is explicitly requested.
|
||||
- Automatic character-dictionary changes now stop when a previously non-empty Yomitan profile suddenly reports zero dictionaries.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: changed
|
||||
area: dictionary
|
||||
|
||||
- Updated the bundled Hachidori to upstream 0.4.0 (d959a09), including every pitch accent in every popup entry, pitch drawn on the headword's reading, headword furigana split per kanji, and Yomitan-style definition tags.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- Keep the subtitle overlay mapped during fullscreen transitions on Hyprland, avoiding focus changes from repeated hide/show calls that can cancel mpv fullscreen entry. Preserve the hide/show refresh on Sway.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- Fixed the overlay (subtitles and sidebar) becoming unclickable after mpv goes fullscreen on Hyprland with `mpv.backend: x11`. The launcher no longer strips `HYPRLAND_INSTANCE_SIGNATURE` from the X11 environment, so the overlay can lift Hyprland's fullscreen input block again.
|
||||
@@ -1,5 +0,0 @@
|
||||
type: fixed
|
||||
area: jellyfin
|
||||
|
||||
- Jellyfin playback now selects the Japanese subtitle track, and starts subtitle annotations, as soon as that track downloads instead of waiting for every other subtitle track. Tracks now download in parallel, so a slow embedded track Jellyfin has to extract no longer delays the primary subtitles.
|
||||
- Jellyfin episodes with image-based embedded subtitles (PGS, DVD, DVB) now auto-select the Japanese and English tracks. SubMiner no longer requests those tracks as text, and a subtitle track that fails to download no longer cancels selection of the others.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: fixed
|
||||
area: jellyfin
|
||||
|
||||
- Timing review previews and mined card audio now use the audio track you are playing on Jellyfin direct-play streams, instead of the track Jellyfin marks as default.
|
||||
@@ -0,0 +1,5 @@
|
||||
type: added
|
||||
area: jimaku
|
||||
|
||||
- Added `jimaku.autoOpenWhenPrimarySubtitleMissing` (off by default). When a local video loads without a Japanese (primary-language) subtitle track, SubMiner pauses playback and opens the Jimaku modal; playback resumes when the modal closes.
|
||||
- Opening Jimaku while it is already open now reloads the title and episode if the video changed, instead of keeping the previous episode's search.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: added
|
||||
area: overlay
|
||||
|
||||
- Keyboard-only mode can move by character instead of by word: press `C`, or the new **Toggle Character Navigation** controller binding (unbound by default). The cursor box turns orange and covers one character, and a lookup searches from it, which reaches words the subtitle split cuts in two.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: added
|
||||
area: overlay
|
||||
|
||||
- New **Pause Video In Keyboard-Only Mode** setting (`subtitleStyle.autoPauseVideoOnKeyboardMode`, off by default): turning keyboard-only mode on, from the keyboard or a controller, pauses the video, and turning it off resumes it. A video that was already paused, or that you played or paused yourself in the meantime, is left as it is.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: changed
|
||||
area: overlay
|
||||
|
||||
- Turning keyboard-only mode off, from the keyboard or a controller, now also closes the dictionary popup.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- On macOS, controller buttons now work while mpv is in front: a press brings the overlay forward and runs that button's action, instead of doing nothing until you click the overlay.
|
||||
@@ -0,0 +1,5 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- On macOS and Windows, one controller press no longer runs its action twice, which cancelled out toggles like mpv pause.
|
||||
- On macOS, the press that brings the overlay forward from mpv no longer runs its action a second time.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- Fixed mpv key bindings (input.conf and mpv defaults, e.g. `9`/`0` volume) doing nothing while the overlay had focus when the overlay loaded before SubMiner connected to mpv, common in `mpv.backend: x11` mode. The overlay now reloads mpv's bindings once mpv connects.
|
||||
@@ -1,5 +0,0 @@
|
||||
type: added
|
||||
area: overlay
|
||||
|
||||
- Added scroll wheel keys (`WHEEL_UP`, `WHEEL_DOWN`, `WHEEL_LEFT`, `WHEEL_RIGHT`, with modifiers) to `keybindings`, including capture from the settings key editor.
|
||||
- Mouse button and scroll wheel bindings from mpv (`input.conf` and mpv defaults, e.g. double-click fullscreen, wheel volume, back/forward for playlist) now work while the cursor is over the overlay. On Hyprland, where the overlay always receives input, they previously did nothing. SubMiner bindings and the right-click pause still take priority.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: fixed
|
||||
area: dependencies
|
||||
|
||||
- Updated Electron to 42.10.0 and undici to 7.29.1 to resolve the security advisories blocking CI.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: fixed
|
||||
area: mpv-plugin
|
||||
|
||||
- Fixed the "Overlay loading" message staying on screen when mpv moves to the next file in a playlist, then turning into "Overlay did not become ready; check SubMiner logs" after 30 seconds even though the overlay was working. The overlay window is reused between files, so the plugin no longer restarts the loading message once the overlay has reported ready.
|
||||
@@ -0,0 +1,9 @@
|
||||
type: added
|
||||
area: plugins
|
||||
|
||||
- Added JavaScript plugins loaded at startup from `<config dir>/plugins/<id>/` and enabled under `plugins` in the config. Plugins can listen to mpv events, run timers, register actions bound with `__plugin-action:<id>/<action>` keybindings or run with `--plugin-action`, run built-in actions, react to mined Anki cards and call AnkiConnect, start mpv, run programs, keep state in a per-plugin data folder, and show results in an overlay panel or their own window.
|
||||
- Added an example `ai-explain` plugin that sends the current line and its neighbors to `codex exec` and shows a translation or grammar explanation.
|
||||
- Added an example `anki-sync` plugin that syncs Anki to AnkiWeb after you mine cards, once mining goes quiet or playback ends.
|
||||
- Added an example `episode-tags` plugin that tags mined cards with the show and episode, such as `show::Frieren::S01E07`.
|
||||
- Added a versioned plugin API with standalone authoring types, cached playback snapshots, lifecycle cancellation and cleanup, and awaitable mpv commands. Async activation is bounded and actions wait only for their own plugin. Shutdown skips pending loads, runs each deactivation once, and blocks calls from disposed plugins.
|
||||
- Mining events preserve original and kept note ids with the media and subtitle captured when mining starts, so delayed enrichment and merges into older notes keep the source episode. Stats-dashboard cards do not trigger plugin events. Plugin subtitle lookups and mining provenance account for subtitle delay and return timing on the subtitle file's timeline.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- Fixed controller and keyboard lookups sometimes pausing playback without a popup, leaving playback paused after the popup closed until the mouse moved, or not pausing when the popup opened.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- Moving between words with the keyboard or a controller right after closing the dictionary popup no longer reopens it. A popup that closed under the mouse pointer was still treated as open.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: changed
|
||||
area: release
|
||||
|
||||
- Removed package-size JSON reports from future releases and their CI size comparisons. Package-content validation remains enabled.
|
||||
@@ -0,0 +1,4 @@
|
||||
type: fixed
|
||||
area: playback
|
||||
|
||||
- Reloading the same file in mpv (`loadfile <current path> replace`) could intermittently leave playback paused forever. SubMiner now treats that reload like any other newly loaded file, so it resets the subtitle state and resumes once the file is ready.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: added
|
||||
area: overlay
|
||||
|
||||
- Session help can run the highlighted command during playback: press `Enter` or double-click a row. Hovering a row with the mouse makes it the `Enter` target. Help opened from the tray with no video loaded stays read-only.
|
||||
@@ -0,0 +1,8 @@
|
||||
type: fixed
|
||||
area: stats
|
||||
|
||||
- The stats dashboard loads much faster on large databases. Overview, media and library detail, and trends no longer re-count every recorded word in the whole database on each request, so these views only cost as much as the sessions they show. On a ~1 GB database the overview went from ~1.4 s to ~0.14 s, media detail from ~1.5 s to ~0.08 s, the 30-day trends from ~1.5 s to ~0.06 s, and all-time trends from ~1.6 s to ~0.2 s.
|
||||
- The Vocabulary tab's summary and word list load several times faster (summary ~2 s to ~0.2 s, word list ~0.6 s to ~0.2 s).
|
||||
- The Trends tab downloads far less data: the per-title library charts now arrive as daily values and are totalled in the dashboard, so the all-time response shrank from ~3.9 MB to ~0.2 MB.
|
||||
- Sentence search with headword matching returns several times faster (~0.3 s to ~0.08 s per query).
|
||||
- Vocabulary totals and new-word charts stay correct after running vocabulary cleanup. Words it repaired could be counted or hidden by mistake; the first launch after updating corrects them in the background.
|
||||
@@ -1,7 +0,0 @@
|
||||
type: fixed
|
||||
area: stats
|
||||
|
||||
- Fixed sentence mining (single and multi-line) counting each created card twice in session and lifetime stats.
|
||||
- Fixed word and kanji dates after session deletion. Historical "first seen" dates and counts survive retention pruning; new occurrences record observation time so deletion can update dates exactly. Legacy earlier dates are preserved when the remaining history cannot prove their origin. Stale "last seen" dates are repaired once in the background on next launch.
|
||||
- Fixed re-seen words that the vocabulary filter hides still being counted in the new-words charts, so charts and the vocabulary summary now agree. Existing mismatches are repaired by the same background pass.
|
||||
- Fixed Overview totals, charts, activity calendar, and vocabulary summary staying stale after deleting sessions. Failed refreshes keep the dashboard visible and offer a retry.
|
||||
@@ -1,5 +0,0 @@
|
||||
type: docs
|
||||
area: stats
|
||||
|
||||
- Documented the automatic Overview refresh after deleting sessions or session groups and retrying failed refreshes.
|
||||
- Documented discovery-date preservation after retention pruning and the handling of older write-time timestamps.
|
||||
@@ -0,0 +1,15 @@
|
||||
type: changed
|
||||
area: stats
|
||||
|
||||
- Rebuilt the Trends tab: summary totals with period comparison, a Learning section (new words per 1,000 seen, new words by frequency rank), a day and hour heatmap, a title timeline, and a single running-total chart with a metric switcher that climbs from zero. These replace the seven cumulative charts, the Sessions chart, the day-of-week and hour charts, the four overlapping per-title charts, and the top titles chart.
|
||||
- Added week grouping, custom date ranges, a title filter for the whole page, and click-through from charts to sessions and from titles to their library page. Range and grouping are remembered, and changing them no longer blanks the page or loses your scroll position.
|
||||
- Fixed the Library section including days outside the selected range, so its totals now match Activity.
|
||||
- Fixed episode counts: Trends now counts finished episodes, each once, instead of counting a video again for every day it was opened.
|
||||
- New word counts across Trends and Vocabulary now count dictionary words, so a word's conjugations count once. Totals are lower than before.
|
||||
- Rate charts no longer plot idle days as zero or spike on very short days: they show gaps when there is too little data, and daily views use a rolling 7 days.
|
||||
- Fixed clipped chart axis labels and month labels that read like dates, and the All range ignoring Show empty days.
|
||||
- Rebuilt the Vocabulary tab: one word table with Worth mining, All words, Across titles, and Newest views, search across every word, form, and reading, sortable columns, and inline Hide. It replaces three lists that could only see your 500 most seen word forms. Also added known words by frequency band, a vocabulary growth chart, and a kanji view covering every kanji, with a filter for kanji outside your known words.
|
||||
- Fixed word counts and readings: lists and the word detail panel now total every form of a word and show the dictionary form's reading, so 見る no longer shows みてる. The detail panel loads example lines straight away and lists a word's forms.
|
||||
- Fixed names slipping past Hide Names and the Trends new-word counts when tagged with more than one part of speech.
|
||||
- Stats now says when a word or line can't be mined because it came from a stream, in Search and the word panel, instead of showing Unavailable buttons. Lines you can mine are listed first. The word panel counts affected lines, and the word table marks words with only streamed lines as Stream only.
|
||||
- Fixed stats treating the day as one earlier in time zones ahead of UTC, which affected today's totals, the custom date picker, and first-seen dates.
|
||||
@@ -0,0 +1,5 @@
|
||||
type: added
|
||||
area: subtitles
|
||||
|
||||
- Added subtitle filters (`subtitleFilter`) that remove unwanted text from primary subtitles before it reaches the overlay, sidebar, mined sentences and stats. Built-in presets cover text in parentheses (speaker names, readings, notes; whole-line inner thoughts are kept), square-bracket sound descriptions, speaker names before a colon, and music notes. Custom regex rules with a replacement can be added after the presets. Lines left with no words are hidden.
|
||||
- Configure them in the settings window under Behavior → Subtitle Filters; changes apply live to the current subtitle file.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- On macOS with Bluetooth headphones such as AirPods, the timing review preview cursor now moves smoothly across the selection instead of stuttering and jumping to the end partway through. This affected Opus audio, including Jellyfin streams.
|
||||
@@ -1,5 +0,0 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- Media timing review trim handles are now thin lines centered on the clip edge, so it's clear where the clip starts and ends and less of the waveform is covered.
|
||||
- Original subtitle boundaries use orange bars without text tags, leaving the waveform unobstructed.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: fixed
|
||||
area: youtube
|
||||
|
||||
- The subtitle generation modal now works during Windows YouTube playback, where mpv plays a resolved stream URL instead of the YouTube page; it no longer asks you to open a local file or YouTube video, and generated subtitles load into the playing video.
|
||||
@@ -1,7 +0,0 @@
|
||||
type: added
|
||||
area: youtube
|
||||
|
||||
- Added a YouTube browser window (`subminer youtube`/`subminer yt`, or tray **Browse YouTube**) that keeps your YouTube login across restarts.
|
||||
- Clicking a video plays it in mpv instead of the page; middle-click, Shift/Ctrl-click, or the right-click menu queues it in mpv's playlist.
|
||||
- Queued YouTube videos get the same Japanese subtitle setup when mpv reaches them.
|
||||
- Closing the YouTube window quits a SubMiner started by `subminer youtube` (after mpv closes, if a video is still playing).
|
||||
@@ -1,4 +0,0 @@
|
||||
type: fixed
|
||||
area: youtube
|
||||
|
||||
- YouTube Japanese subtitles no longer fail with "HTTP 429" on videos where YouTube also lists a machine-translated Japanese track; SubMiner now picks the real track and falls back to yt-dlp if a direct subtitle download is refused.
|
||||
@@ -1,7 +0,0 @@
|
||||
type: added
|
||||
area: youtube
|
||||
|
||||
- Added `youtube.subtitleSource`: set it to `whisper` to transcribe YouTube videos with Whisper instead of downloading YouTube's captions. The default stays `youtube`.
|
||||
- Whisper mode keeps the video paused while it transcribes, with progress in the subtitle generation modal; close the modal to keep watching, or cancel to continue without subtitles.
|
||||
- The subtitle generation modal (`Ctrl+Shift+G`) now also works on YouTube videos, whatever `youtube.subtitleSource` is set to.
|
||||
- Whisper mode downloads a small audio-only stream and deletes it once generation ends, or when you switch videos, close mpv, or quit.
|
||||
@@ -1,4 +0,0 @@
|
||||
type: fixed
|
||||
area: youtube
|
||||
|
||||
- Fixed Windows YouTube playback dropping the rest of mpv's queue when it advanced to a video queued from the YouTube browser window. The direct stream now replaces only that playlist entry.
|
||||
+345
-5
@@ -6,6 +6,13 @@
|
||||
*/
|
||||
{
|
||||
|
||||
// ==========================================
|
||||
// Dictionary Backend
|
||||
// Select the dictionary lookup backend: yomitan or hachidori.
|
||||
// Restart SubMiner after changing the backend. Each backend keeps separate settings and dictionaries.
|
||||
// ==========================================
|
||||
"dictionaryBackend": "yomitan", // Dictionary lookup backend. Restart SubMiner after changing this setting. Values: yomitan | hachidori
|
||||
|
||||
// ==========================================
|
||||
// Subtitle Selection
|
||||
// Select primary and secondary mpv subtitle tracks from the overlay.
|
||||
@@ -101,6 +108,7 @@
|
||||
"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.
|
||||
"faceButtonLabels": "xbox", // Face button style of your controller: xbox (A at the bottom), nintendo (A on the right, B at the bottom; Switch-style and many 8BitDo pads) or playstation (╳ ○ □ △, laid out like xbox). Places the A/B/X/Y of the dialog layouts and names buttons in Alt+C and the dialog hints. Alt+C sets it per controller (controller.profiles.<id>.faceButtonLabels); this is the default for the others. Values: xbox | nintendo | playstation
|
||||
"repeatDelayMs": 320, // Delay before repeating held controller actions.
|
||||
"repeatIntervalMs": 120, // Repeat interval for held controller actions.
|
||||
"buttonIndices": {
|
||||
@@ -151,7 +159,31 @@
|
||||
"toggleMpvPause": {
|
||||
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
"buttonIndex": 9 // Raw button index captured for this discrete controller action.
|
||||
}, // Controller binding descriptor for toggling mpv play/pause. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
}, // Controller binding descriptor for toggling mpv play/pause. Works with or without keyboard-only mode. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"holdSecondarySub": {
|
||||
"kind": "none" // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
}, // Controller binding descriptor that shows the secondary subtitle only while held. Works with or without keyboard-only mode. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"previousEntry": {
|
||||
"kind": "none" // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
}, // Controller binding descriptor that selects the previous word entry in the open Yomitan popup. Mine Card mines the selected entry. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"nextEntry": {
|
||||
"kind": "none" // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
}, // Controller binding descriptor that selects the next word entry in the open Yomitan popup. Mine Card mines the selected entry. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"toggleCharacterNavigation": {
|
||||
"kind": "none" // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
}, // Controller binding descriptor that switches keyboard-only mode between moving by word and moving by character, like C. A lookup on a character searches from that character, which reaches words the subtitle split cuts in two. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"previousSubtitle": {
|
||||
"kind": "none" // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
}, // Controller binding descriptor that jumps to the previous subtitle line, like Shift+H. Works with or without keyboard-only mode. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"nextSubtitle": {
|
||||
"kind": "none" // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
}, // Controller binding descriptor that jumps to the next subtitle line, like Shift+L. Works with or without keyboard-only mode. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"replaySubtitle": {
|
||||
"kind": "none" // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
}, // Controller binding descriptor that replays the current subtitle line, then pauses, like Ctrl+Shift+H. Works with or without keyboard-only mode. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"playNextSubtitle": {
|
||||
"kind": "none" // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
}, // Controller binding descriptor that plays the next subtitle line, then pauses, like Ctrl+Shift+L. Works with or without keyboard-only mode. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"leftStickHorizontal": {
|
||||
"kind": "axis", // Analog binding input source kind. Values: none | axis
|
||||
"axisIndex": 0, // Raw axis index captured for this analog controller action.
|
||||
@@ -173,6 +205,288 @@
|
||||
"dpadFallback": "none" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
|
||||
} // Axis binding descriptor used for popup page jumps. Use Alt+C learn mode or set a raw axis descriptor manually.
|
||||
}, // Raw controller binding descriptors saved by Alt+C learn mode. For discrete axis bindings, kind "axis" requires axisIndex and direction.
|
||||
"reviewBindings": {
|
||||
"confirm": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "a" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Use this timing.
|
||||
"cancel": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "b" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Cancel.
|
||||
"play": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "x" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Play selection.
|
||||
"resetTiming": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "y" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Reset timing.
|
||||
"showEarlier": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "left" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Earlier −2s. Repeats while held.
|
||||
"showLater": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "right" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Later +2s. Repeats while held.
|
||||
"framePrevious": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "up" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Previous screenshot frame. Repeats while held.
|
||||
"frameNext": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "down" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Next screenshot frame. Repeats while held.
|
||||
"frameReset": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "button", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"buttonIndex": 5 // Raw button index.
|
||||
}, // Input that triggers the action.
|
||||
"hold": {
|
||||
"kind": "button", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"buttonIndex": 4 // Raw button index.
|
||||
} // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Reset screenshot frame.
|
||||
"startBack": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "left" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": {
|
||||
"kind": "button", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"buttonIndex": 4 // Raw button index.
|
||||
} // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Start −0.1s. Repeats while held.
|
||||
"startForward": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "right" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": {
|
||||
"kind": "button", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"buttonIndex": 4 // Raw button index.
|
||||
} // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Start +0.1s. Repeats while held.
|
||||
"previousLineAdd": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "up" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": {
|
||||
"kind": "button", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"buttonIndex": 4 // Raw button index.
|
||||
} // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Add previous line. Repeats while held.
|
||||
"previousLineRemove": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "down" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": {
|
||||
"kind": "button", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"buttonIndex": 4 // Raw button index.
|
||||
} // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Remove previous line. Repeats while held.
|
||||
"endBack": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "left" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": {
|
||||
"kind": "button", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"buttonIndex": 5 // Raw button index.
|
||||
} // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: End −0.1s. Repeats while held.
|
||||
"endForward": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "right" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": {
|
||||
"kind": "button", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"buttonIndex": 5 // Raw button index.
|
||||
} // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: End +0.1s. Repeats while held.
|
||||
"nextLineAdd": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "up" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": {
|
||||
"kind": "button", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"buttonIndex": 5 // Raw button index.
|
||||
} // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Add next line. Repeats while held.
|
||||
"nextLineRemove": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "down" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": {
|
||||
"kind": "button", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"buttonIndex": 5 // Raw button index.
|
||||
} // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Remove next line. Repeats while held.
|
||||
"useOriginal": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "a" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Use original timing (after cancel).
|
||||
"keepEditing": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "b" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Keep editing (after cancel).
|
||||
"skipMedia": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "x" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Without media (after cancel).
|
||||
"discard": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "y" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
} // Review dialog: Discard card (after cancel).
|
||||
}, // Controller layout for the media timing review dialog, one entry per action. Set it with the Review media timing group in Alt+C. Each entry is {"kind": "none"} or {"press": input, "hold": input}, where an input is {"kind": "button", "buttonIndex"}, {"kind": "axis", "axisIndex", "direction"}, {"kind": "face", "letter": "a"|"b"|"x"|"y"} (printed letter) or {"kind": "dpad", "direction": "left"|"right"|"up"|"down"}.
|
||||
"fieldGroupingBindings": {
|
||||
"selectOriginal": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "left" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Duplicate card dialog: Original card.
|
||||
"selectNew": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "right" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Duplicate card dialog: New card.
|
||||
"toggleDeleteDuplicate": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "x" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Duplicate card dialog: Delete duplicate on/off.
|
||||
"continue": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "a" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Duplicate card dialog: Continue.
|
||||
"cancel": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "b" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Duplicate card dialog: Cancel.
|
||||
"confirmMerge": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "a" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Duplicate card dialog: Confirm merge (merge preview).
|
||||
"back": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "b" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Duplicate card dialog: Back (merge preview).
|
||||
"cancelMerge": {
|
||||
"kind": "none" // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
}, // Duplicate card dialog: Cancel (merge preview).
|
||||
"toggleView": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "y" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Duplicate card dialog: Compact / Full (merge preview).
|
||||
"scrollUp": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "up" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Duplicate card dialog: Scroll up (merge preview). Repeats while held.
|
||||
"scrollDown": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "down" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
} // Duplicate card dialog: Scroll down (merge preview). Repeats while held.
|
||||
}, // Controller layout for the duplicate card (field grouping) dialog, one entry per action. Set it with the Duplicate card (field grouping) group in Alt+C. Each entry is {"kind": "none"} or {"press": input, "hold": input}, where an input is {"kind": "button", "buttonIndex"}, {"kind": "axis", "axisIndex", "direction"}, {"kind": "face", "letter": "a"|"b"|"x"|"y"} (printed letter) or {"kind": "dpad", "direction": "left"|"right"|"up"|"down"}.
|
||||
"profiles": {} // Per-controller binding and button-index overrides keyed by the controller id reported by the Gamepad API.
|
||||
}, // Gamepad support for the visible overlay while keyboard-only mode is active.
|
||||
|
||||
@@ -211,6 +525,14 @@
|
||||
"overlayPosition": "top-right" // Position for in-overlay notification cards. Values: top-left | top | top-right
|
||||
}, // Overlay notification display behavior.
|
||||
|
||||
// ==========================================
|
||||
// Plugins
|
||||
// JavaScript plugins loaded from <config dir>/plugins/<id>/ at startup.
|
||||
// Plugins run inside SubMiner with full access to your system. Only enable code you trust.
|
||||
// Restart SubMiner after changing plugins.
|
||||
// ==========================================
|
||||
"plugins": {}, // Plugins to load at startup, keyed by directory name under <config dir>/plugins/. Each entry is { "enabled": true, "settings": { ... } }; settings are passed to the plugin as ctx.settings.
|
||||
|
||||
// ==========================================
|
||||
// Keyboard Shortcuts
|
||||
// Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||
@@ -455,6 +777,7 @@
|
||||
"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": true, // Automatically pause mpv playback while Yomitan popup is open, then resume when popup closes. Values: true | false
|
||||
"autoPauseVideoOnKeyboardMode": false, // Pause mpv when keyboard-only mode turns on (from the keyboard or a controller) and resume when it turns off. Leaving the mode resumes only a pause it made: a video that was already paused, or that you played or paused yourself meanwhile, is left as it is. Values: true | false
|
||||
"primaryVisibleOnYomitanPopup": true, // Keep the primary subtitle bar visible while a Yomitan popup is open when primary subtitles are in hover mode. Values: true | false
|
||||
"nameMatchEnabled": false, // Enable character dictionary sync and subtitle token coloring for character-name matches. Values: true | false
|
||||
"nameMatchImagesEnabled": false, // Show small character portraits beside subtitle tokens matched from the SubMiner character dictionary. Values: true | false
|
||||
@@ -534,6 +857,22 @@
|
||||
} // CSS declaration object applied to the subtitle sidebar. Includes color, background-color, and all font properties.
|
||||
}, // Parsed-subtitle sidebar cue list styling, behavior, and toggle key.
|
||||
|
||||
// ==========================================
|
||||
// Subtitle Filters
|
||||
// Remove unwanted text from primary subtitles, such as speaker names, readings and sound descriptions.
|
||||
// Applies to the overlay, sidebar, mined sentences and stats.
|
||||
// Hot-reload: filter changes apply to the current subtitle file immediately.
|
||||
// ==========================================
|
||||
"subtitleFilter": {
|
||||
"presets": {
|
||||
"parentheses": false, // Remove text in () and (), such as speaker names, readings and notes. A line wrapped entirely in parentheses, usually inner thoughts, is kept. Values: true | false
|
||||
"brackets": false, // Remove text in [], [], 【】 and 〔〕, such as sound effects and music cues. Values: true | false
|
||||
"speakerPrefix": false, // Remove a short name followed by a colon at the start of a line, as in 山田:元気? Values: true | false
|
||||
"musicSymbols": false // Remove ♪, ♫ and ♬. A line left with only symbols such as ~ is removed. Values: true | false
|
||||
}, // Built-in filters. Each removes its matches and is off by default.
|
||||
"customRules": [] // Your own regex rules, applied in order after the presets. Each rule is { name, pattern, replacement, enabled }; patterns use the gmu flags, so ^ and $ match per line. Leave replacement empty to remove the match.
|
||||
}, // Remove unwanted text from primary subtitles, such as speaker names, readings and sound descriptions.
|
||||
|
||||
// ==========================================
|
||||
// Shared AI Provider
|
||||
// Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
|
||||
@@ -571,7 +910,7 @@
|
||||
"deck": "", // Restrict duplicate detection and card enrichment to this Anki deck. Leave empty to use the Yomitan mining deck when available.
|
||||
"fields": {
|
||||
"word": "Expression", // Card field for the mined word or expression text.
|
||||
"audio": "ExpressionAudio", // Card field that receives generated sentence audio.
|
||||
"audio": "SentenceAudio", // Card field that receives generated sentence audio.
|
||||
"wordAudio": "ExpressionAudio", // Existing word-audio field read to time the frozen first frame of animated images. This mapping is only used for synchronization.
|
||||
"image": "Picture", // Card field that receives the captured screenshot or animated image.
|
||||
"sentence": "Sentence", // Card field that receives the source sentence text.
|
||||
@@ -656,7 +995,8 @@
|
||||
"apiKey": "", // Jimaku API key. Optional but recommended for higher rate limits. Get one for free at https://jimaku.cc.
|
||||
"apiKeyCommand": "", // Shell command that prints the Jimaku API key to stdout. Used instead of apiKey to avoid storing the key in plain text.
|
||||
"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.
|
||||
"autoOpenWhenPrimarySubtitleMissing": false // Pause playback and open the Jimaku modal when a local video has no subtitle track in youtube.primarySubLanguages (Japanese by default). Values: true | false
|
||||
}, // Jimaku API configuration and defaults.
|
||||
|
||||
// ==========================================
|
||||
@@ -734,10 +1074,10 @@
|
||||
// autoStartSubMiner starts SubMiner in the background; auto_start_overlay only controls visible overlay display.
|
||||
// Set mpv.launchMode to choose normal, maximized, or fullscreen SubMiner-managed mpv playback.
|
||||
// Set mpv.profile to pass an mpv profile to managed mpv launches; leave it blank to pass none.
|
||||
// Leave mpv.executablePath blank to auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.
|
||||
// Leave mpv.executablePath blank to auto-discover mpv from SUBMINER_MPV_PATH, PATH, or (macOS) Homebrew and /Applications/mpv.app.
|
||||
// ==========================================
|
||||
"mpv": {
|
||||
"executablePath": "", // Optional absolute path to mpv.exe for Windows launch flows. Leave empty to auto-discover from SUBMINER_MPV_PATH or PATH.
|
||||
"executablePath": "", // Optional absolute path to the mpv binary SubMiner starts itself (Windows launches, Jellyfin casting, timing preview). Leave empty to auto-discover from SUBMINER_MPV_PATH, PATH, and on macOS the Homebrew and /Applications/mpv.app locations.
|
||||
"launchMode": "normal", // Default window state for SubMiner-managed mpv launches. Values: normal | maximized | fullscreen
|
||||
"profile": "", // Optional mpv profile name passed to SubMiner-managed mpv launches. Leave empty to pass no profile.
|
||||
"socketPath": "/tmp/subminer-socket", // mpv IPC socket path used by SubMiner-managed playback and the bundled mpv plugin.
|
||||
|
||||
@@ -221,6 +221,7 @@ const sidebar: DefaultTheme.SidebarItem[] = [
|
||||
{ text: 'AniList', link: '/anilist-integration' },
|
||||
{ text: 'AniSkip', link: '/aniskip-integration' },
|
||||
{ text: 'Character Dictionary', link: '/character-dictionary' },
|
||||
{ text: 'Plugins', link: '/plugins' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@ An episode counts as watched after 85% of its length and at least 10 minutes of
|
||||
2. Finds the matching AniList entry. For season 2 and later, it follows the show's sequels.
|
||||
3. Sets your progress to that episode and marks the entry Watching, or Completed on the final episode.
|
||||
|
||||
The show must already be on your Planning or Watching list. SubMiner does not add new entries, and it never lowers your progress.
|
||||
If the show is not on your list yet, SubMiner adds it as Watching, but only when the file title exactly matches an AniList title or you pinned the entry with the character dictionary's AniList override. For a looser match, add the show to Planning or Watching yourself. SubMiner never changes entries marked Completed, Paused, Dropped, or Rewatching, and it never lowers your progress.
|
||||
|
||||
Failed updates are saved and retried in the background, up to 8 times with growing delays. The queue survives restarts.
|
||||
|
||||
@@ -61,7 +61,7 @@ Character dictionary settings live under `anilist.characterDictionary` and are c
|
||||
|
||||
**No update after an episode.** Check that `anilist.enabled` is `true` and that you watched at least 85% of the episode.
|
||||
|
||||
**"AniList update not possible."** Add the show to your Planning or Watching list, then mark the episode watched again.
|
||||
**"AniList update not possible."** The show is marked Completed, Paused, Dropped, or Rewatching, or it is not on your list and the title match was not exact. Add the show to your Planning or Watching list, or pin it with the character dictionary's AniList override, then mark the episode watched again.
|
||||
|
||||
**Wrong show or episode.** Install guessit and make sure it is on your `PATH`. Unusual file names parse poorly without it.
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ If you changed AnkiConnect's port, set `ankiConnect.url` to match.
|
||||
|
||||
## How cards get filled
|
||||
|
||||
When Yomitan adds a note, SubMiner fills the sentence, audio, image, and MiscInfo fields. It finds new notes in one of two ways:
|
||||
When Yomitan or Hachidori adds a note, SubMiner fills the sentence, audio, image, and MiscInfo fields. It finds new notes in one of two ways:
|
||||
|
||||
- **Proxy (default).** SubMiner runs a local AnkiConnect-compatible server. Yomitan sends notes through it, and SubMiner fills each one right after Anki accepts it.
|
||||
- **Proxy (default).** SubMiner runs a local AnkiConnect-compatible server. The dictionary sends notes through it, and SubMiner fills each one right after Anki accepts it.
|
||||
- **Polling.** With `ankiConnect.proxy.enabled` set to `false`, SubMiner asks AnkiConnect for recently added notes every `ankiConnect.pollingRate` milliseconds.
|
||||
|
||||
Set `ankiConnect.behavior.autoUpdateNewCards` to `false` to stop automatic filling and update cards by hand with `Ctrl/Cmd+V` instead.
|
||||
@@ -42,6 +42,20 @@ Clients must send notes to the proxy (`http://127.0.0.1:8766` here), not to Anki
|
||||
|
||||
- **Bundled Yomitan.** SubMiner sets the active Yomitan profile's Anki server for you. With the proxy on, it always points the profile at the proxy. With the proxy off, it sets `ankiConnect.url`, but only if the profile's server is blank or the stock `http://127.0.0.1:8765`.
|
||||
- **Browser Yomitan or other clients.** Set the Anki server to the proxy URL yourself. To leave your main profile alone, create a separate Yomitan profile for SubMiner, set its Anki server (Settings, Anki) to the proxy URL, and make it active while you mine.
|
||||
- **Hachidori.** SubMiner routes Hachidori to the proxy while it is active. Keep the proxy on for screenshots and sentence audio.
|
||||
|
||||
### Hachidori settings from SubMiner
|
||||
|
||||
With the [Hachidori backend](/usage#hachidori-setup), SubMiner fills Hachidori's first Anki template from your `ankiConnect` settings on startup and whenever you open Hachidori Settings:
|
||||
|
||||
- The deck always follows `ankiConnect.deck`, because polling only looks for new cards in that deck.
|
||||
- While the template still has Hachidori's default `hachidori` tag, the configured tags are added ahead of it so cards carry both. Tags you edit in Hachidori settings are kept as written.
|
||||
- Missing word, sentence, pronunciation-audio, and picture mappings are filled with fields that exist in Anki. Pronunciation uses `fields.wordAudio`, or `fields.audio` when no word-audio field is set.
|
||||
- If the note type is unset, SubMiner picks the one note type that has your word and sentence fields. Enabled Lapis, Kiku, or Senren narrows the search. A fresh mapping also gets Hachidori's matching preset for readings, definitions, and other known fields.
|
||||
|
||||
Apart from the deck, SubMiner only fills missing settings. Custom tags, field mappings, advanced templates, and extra templates stay as you set them. If several note types match, pick one in Hachidori Settings. If Anki was closed, start it and open Hachidori Settings again to retry.
|
||||
|
||||
Sentence audio, image timing, translation, metadata, and field grouping stay under SubMiner's control. Pronunciation sources are set in Hachidori. Linking an external dictionary host does not change any of this.
|
||||
|
||||
### Proxy troubleshooting
|
||||
|
||||
@@ -87,7 +101,9 @@ If cards are not getting filled:
|
||||
|
||||
Field names are matched case-insensitively. A mapped field that is missing from the note type is skipped.
|
||||
|
||||
`fields.audio` gets sentence audio, not word audio. Yomitan writes its own dictionary audio into your note, so point `fields.audio` at a separate field such as `SentenceAudio`. The default, `ExpressionAudio`, is the field many note types use for Yomitan's word audio, so leaving it would overwrite that audio.
|
||||
Hachidori prepares downloadable word audio before it saves a note through the proxy, so the animated image delay works on the first mine. Set a downloadable pronunciation source in Hachidori's Audio settings. Browser speech cannot be saved to Anki. Without word audio, Hachidori shows a warning and the card gets no word-audio hold.
|
||||
|
||||
`fields.audio` gets sentence audio, not word audio. Yomitan writes its own dictionary audio into your note (often `ExpressionAudio`), so keep `fields.audio` pointed at a separate field such as the default `SentenceAudio`. Pointing it at Yomitan's word-audio field would overwrite that audio.
|
||||
|
||||
`ankiConnect.tags` adds tags to every mined or updated card. Set it to `[]` to add none.
|
||||
|
||||
@@ -122,6 +138,8 @@ Media settings apply to the next card without a restart.
|
||||
|
||||
With `media.reviewTiming` on, SubMiner pauses before making media for word, sentence, and audio cards and opens a review dialog. You can also toggle it for the current session with **Review Media Timing** in the runtime options palette (`Ctrl/Cmd+Shift+O`). Clipboard updates and stats-dashboard mining skip the review.
|
||||
|
||||
Playback stays paused while the dialog is open, even if the popup or hover that paused it goes away. When the dialog closes, playback resumes if it was playing before, or if the popup closed in the meantime. A popup that is still open keeps it paused.
|
||||
|
||||
The dialog shows the clip over a speech waveform. When the waveform loads, an untouched clip end moves back to just after the last speech in the line. The orange bars still mark the subtitle's original start and end.
|
||||
|
||||
During preview, the yellow cursor follows the audio player's position through the selected range and stays at the end when playback finishes. Buffering pauses the cursor too. If the player stops reporting progress for 15 seconds, the preview stops so you can retry.
|
||||
@@ -142,6 +160,34 @@ The confirmed range is used as is, with no extra padding. Added lines go into th
|
||||
|
||||
When you cancel, you can go back to editing, keep the original timing, create the card without media, or discard it. Discard deletes the Yomitan note or audio card, and skips creation for a sentence card.
|
||||
|
||||
#### With a controller
|
||||
|
||||
With [controller support](/usage#controller-support) on, the dialog has its own controller layout, and each button shows its controller shortcut once SubMiner has seen a controller, which takes one button press after connecting it. It works with or without keyboard-only mode, and the controller's usual actions pause until the dialog closes. Buttons that were already held when the dialog opened, such as the mine button, wait for a fresh press.
|
||||
|
||||
`A`, `B`, `X` and `Y` mean the letters printed on your controller. Pick your controller's **Button Style** in `Alt+C`: choose Nintendo if A is on the right and B at the bottom (Switch-style and many 8BitDo pads). On PlayStation, `A` is ╳, `B` ○, `X` □ and `Y` △, and the shortcuts on screen show those symbols.
|
||||
|
||||
Hold `L1` or `R1` and press the D-pad for the combos. `L1` covers the clip start and the lines before it, and `R1` the clip end and the lines after it. D-pad actions repeat while held.
|
||||
|
||||
This is the default layout:
|
||||
|
||||
| Controller | Editing | After Cancel |
|
||||
| ----------------------- | -------------------------------- | ------------------- |
|
||||
| `A` | Use this timing | Use original timing |
|
||||
| `B` | Cancel | Keep editing |
|
||||
| `X` | Play selection | Without media |
|
||||
| `Y` | Reset timing | Discard |
|
||||
| D-pad left / right | Earlier −2s / Later +2s | |
|
||||
| D-pad up / down | Previous / next screenshot frame | |
|
||||
| `L1` + D-pad left/right | Start −0.1s / +0.1s | |
|
||||
| `L1` + D-pad up/down | Add / remove a previous line | |
|
||||
| `R1` + D-pad left/right | End −0.1s / +0.1s | |
|
||||
| `R1` + D-pad up/down | Add / remove a next line | |
|
||||
| `L1` + `R1` | Reset the screenshot frame | |
|
||||
|
||||
To change it, open `Alt+C` and expand **Review media timing**. Click an action's pencil, then press a button, or hold one button and press another to make a combo; the first button becomes the one you hold. Any button can be a hold button. Changes are saved per controller, and **Reset all** brings back the default layout.
|
||||
|
||||
Nudges, Earlier/Later, lines and frames repeat while held whichever buttons they are on. A row warns you when two actions on the same step share an input, or when an action uses a button alone that other actions hold.
|
||||
|
||||
### Update behavior
|
||||
|
||||
| Key | What it does |
|
||||
@@ -208,6 +254,8 @@ For Senren, use the same keys under `isSenren`.
|
||||
| `auto` | Merge into the existing card. With `deleteDuplicateInAuto`, delete the new card |
|
||||
| `manual` | Show both cards, let you choose which to keep and preview the merge |
|
||||
|
||||
Grouping runs when a new note is added while duplicates exist. With Hachidori, that is the popup's **Add anyway** choice. **Overwrite** updates the existing note in place and only gets media.
|
||||
|
||||
The manual dialog cancels itself after 90 seconds. Identical entries are not deduplicated. Press `Ctrl/Cmd+G` to run the duplicate check on the last card yourself.
|
||||
|
||||
| Key | Action |
|
||||
@@ -217,6 +265,21 @@ The manual dialog cancels itself after 90 seconds. Identical entries are not ded
|
||||
| `Backspace` | Back from the merge preview |
|
||||
| `Esc` | Cancel and leave both cards unchanged |
|
||||
|
||||
### With a controller {#field-grouping-with-a-controller}
|
||||
|
||||
With [controller support](/usage#controller-support) on, the dialog has its own controller layout, and each control shows its controller shortcut once SubMiner has seen a controller. Like the [media timing review](#with-a-controller), it works with or without keyboard-only mode, `A`/`B`/`X`/`Y` are the printed letters (set by **Button Style** in `Alt+C`), and a button already held when the dialog opens waits for a fresh press.
|
||||
|
||||
| Controller | Choosing the card | Merge preview |
|
||||
| ------------------ | --------------------------------- | ----------------------- |
|
||||
| D-pad left / right | Original card / New card | |
|
||||
| D-pad up / down | | Scroll (repeats) |
|
||||
| `A` | Continue | Confirm merge |
|
||||
| `B` | Cancel | Back to the card choice |
|
||||
| `X` | Turn "Delete duplicate" on or off | |
|
||||
| `Y` | | Compact / Full |
|
||||
|
||||
To cancel from the preview, press `B` twice, or bind the preview's **Cancel** row (unbound by default). Change any of it in the **Duplicate card (field grouping)** group of `Alt+C`, the same way as the review layout.
|
||||
|
||||
## Config validation
|
||||
|
||||
Invalid `ankiConnect` values produce a warning and fall back to the default. Use JSON booleans (`true`, not `"true"`) and a positive number for `pollingRate`.
|
||||
@@ -24,6 +24,7 @@ src/
|
||||
preload*.ts # preload bridges (overlay, settings, stats, sync, Jellyfin setup)
|
||||
main/ # main-process runtime modules and IPC/CLI wiring
|
||||
boot/ # pre-ready boot helpers
|
||||
assemblies/ # per-domain assemblies main.ts builds once (startup, overlay, playback, mining, stats, Yomitan, shortcuts, session actions, plugins, subtitle generation, tray, first-run setup, YouTube, Jellyfin, AniList, settings, sync, dictionaries, overlay modals)
|
||||
runtime/composers/ # larger runtime clusters assembled for main.ts
|
||||
runtime/domains/ # domain barrels (startup, overlay, mpv, ipc, shortcuts, anilist, jellyfin, mining)
|
||||
core/services/ # focused services: mpv client, overlay, tokenizer, mining, integrations, stats
|
||||
@@ -138,7 +139,7 @@ Runtime code uses dependency injection:
|
||||
2. Build its runtime dependencies in a `src/main/` module. Pass simple dependencies inline; extract an adapter only when it adds behavior or gets reused.
|
||||
3. Call the service from lifecycle or command wiring.
|
||||
|
||||
`main.ts` gets domain handlers through `createMainRuntimeRegistry()` (`src/main/runtime/registry.ts`), which exposes the barrels in `src/main/runtime/domains/`. Larger clusters, such as app-ready startup, mpv, Jellyfin, AniList tracking, shortcuts, and IPC, are assembled by composers in `src/main/runtime/composers/`. Many handlers take a `*MainDeps` object built by a `createBuild*MainDepsHandler` builder, which keeps side effects out of the unit under test.
|
||||
Domain handlers live in the barrels under `src/main/runtime/domains/`. Larger clusters, such as app-ready startup, mpv, Jellyfin, AniList tracking, shortcuts, and IPC, are assembled by composers in `src/main/runtime/composers/`. A domain whose runtimes share state, such as app startup, the overlay windows, mpv playback and subtitle state, card mining, stats, plugins, YouTube, Jellyfin, AniList, config settings, sync, or the character dictionary, is grouped into one assembly in `src/main/assemblies/` that owns that state and returns its runtimes. An assembly that answers renderer IPC or CLI commands also returns typed `ipc`/`cli` slices, and `main.ts` registers the handlers by combining those slices. Handlers take their dependencies as a plain object passed straight to the handler factory, which keeps side effects out of the unit under test.
|
||||
|
||||
Composers declare their inputs with `ComposerInputs<T>` and results with `ComposerOutputs<T>` from `src/main/runtime/composers/contracts.ts`. A missing dependency then fails at compile time.
|
||||
|
||||
|
||||
+125
-2
@@ -1,6 +1,129 @@
|
||||
# Changelog
|
||||
|
||||
## v0.20.0 (2026-09-23)
|
||||
## v0.21.1 (2026-10-26)
|
||||
|
||||
**Changed**
|
||||
- **Stats Session View**: Redesigned the expanded session view in the stats dashboard.
|
||||
- New stat tiles show pace, lines, known-word rate, lookups, and paused time.
|
||||
- A reading density chart of known and unknown words replaces the cumulative chart.
|
||||
- Yomitan lookups and mined cards now appear in their own lane below the chart.
|
||||
|
||||
**Fixed**
|
||||
- **Subtitle Retiming**: Mining, the subtitle sidebar, subtitle navigation, and replay now use the timing you set with mpv's subtitle delay keys (z/x and sub-step), so mined audio and images match the retimed line.
|
||||
- **Subtitle Sidebar**: mpv keys and mouse actions, like volume keys and double-click fullscreen, work again while the sidebar is open.
|
||||
|
||||
**Docs**
|
||||
- **README**: Refreshed the anime screenshots using the current build.
|
||||
|
||||
<details>
|
||||
<summary>Internal changes</summary>
|
||||
|
||||
**Internal**
|
||||
- **Packaging Layout**: Packaging inputs now live under `packaging/`, and all build output lands in `dist/`, with packaged apps in `dist/release/`.
|
||||
|
||||
</details>
|
||||
|
||||
## v0.21.0 (2026-10-24)
|
||||
|
||||
**Added**
|
||||
- **Hachidori Dictionary Backend**:
|
||||
- Hachidori is now bundled as an alternative to Yomitan. Select it with `dictionaryBackend`, then restart SubMiner.
|
||||
- It works with subtitle scanning, popups, frequency highlighting, character dictionaries, Anki mining, and stats dashboard mining.
|
||||
- First-run setup builds Hachidori's Anki template from your SubMiner settings. It can also link an external Hachidori app, browser, or Docker host.
|
||||
- The tray and dictionary-settings shortcut open settings for the active backend. Use `--hachidori` or `--yomitan` to open a specific one.
|
||||
- **Jellyfin Subtitle Generation**: You can now generate Japanese subtitles while playing a Jellyfin episode, with both direct play and transcoded playback.
|
||||
|
||||
**Fixed**
|
||||
- **Jellyfin**:
|
||||
- `subminer jellyfin -p` now lists libraries and shows image previews when SubMiner is already running. Real errors are shown instead of an empty list.
|
||||
- Watch progress from terminal playback is reported to Jellyfin even with cast discovery off.
|
||||
- When `subminer jellyfin -p` starts SubMiner, the app shows its tray icon and quits when mpv closes.
|
||||
- Casting on macOS now finds mpv when SubMiner is opened from Finder, the Dock, or Spotlight.
|
||||
- **Halfwidth Katakana**: Halfwidth katakana in subtitles now displays as fullwidth, including in mined cards and older stats lines. Stats sentence search matches katakana in either width.
|
||||
- **Media Timing Review**: The review opens on the first attempt instead of after a delay. Playback stays paused while it is open.
|
||||
- **Subtitle Annotations**: Lines shown while two cues overlap now get annotations right away. Prefetching no longer restarts during startup.
|
||||
- **Keyboard-Only Mode**: `h`, `l`, and the arrow keys now work for typing in modal text fields instead of seeking mpv.
|
||||
- **Stats Dashboard Mining**: Cards mined from the stats dashboard keep the selected history line and media.
|
||||
- **Hyprland and Sway**: The app no longer freezes when `hyprctl` or `swaymsg` is slow or hangs.
|
||||
- **Subtitle Sidebar**: The open sidebar does much less background work during playback.
|
||||
- **macOS Launcher Status**: The command-line launcher no longer shows "Not on PATH" when SubMiner is opened from Finder or the Dock.
|
||||
- **Security Updates**: Updated Electron, undici, and follow-redirects to address security advisories.
|
||||
|
||||
**Docs**
|
||||
- **Hachidori Setup**: Documented backend selection, Hachidori dictionary and Anki setup, settings shortcuts, and external host linking.
|
||||
- **Jellyfin Casting**: Explained when SubMiner joins the cast menu at startup, and how casting finds mpv on each platform. Added a troubleshooting entry for casts that never start mpv.
|
||||
- **Jimaku Key**: Removed environment variables that never had any effect. Set the key with `jimaku.apiKey` or `jimaku.apiKeyCommand`.
|
||||
|
||||
<details>
|
||||
<summary>Internal changes</summary>
|
||||
|
||||
**Internal**
|
||||
- **End-to-End Harness**: Added a headless e2e harness (`bun run test:e2e`, `bun run e2e`) with real mpv, a fixture dictionary, and a fake AnkiConnect.
|
||||
- **Hachidori Fork**: The bundled Hachidori integration lives in a pinned fork submodule.
|
||||
- **Dead Code Removal**:
|
||||
- Removed the unused launcher Jimaku client, the mpv plugin hover-highlight module, and the unused character dictionary config keys.
|
||||
- Removed 16 unused overlay IPC methods. SubMiner no longer forwards every ASS line to the overlay.
|
||||
- Typecheck now runs with `noUnusedLocals`.
|
||||
- **Setup Launcher Label**: First-run setup now labels a missing launcher "Not set up" instead of "Not installed".
|
||||
|
||||
</details>
|
||||
|
||||
## Previous Versions
|
||||
|
||||
<details>
|
||||
<summary>v0.20.x</summary>
|
||||
|
||||
<h2>v0.20.1 (2026-10-03)</h2>
|
||||
|
||||
**Added**
|
||||
- **YouTube Browser Window**:
|
||||
- Browse YouTube with your login kept across restarts. Open it with `subminer youtube`, `subminer yt`, or tray **Browse YouTube**.
|
||||
- Clicking a video plays it in mpv. Middle-click, Shift/Ctrl-click, or the right-click menu adds it to mpv's playlist.
|
||||
- Queued videos get the same Japanese subtitle setup, and the rest of the queue is kept on Windows.
|
||||
- Closing the window quits a SubMiner started by `subminer youtube`.
|
||||
- **Whisper Subtitles for YouTube**:
|
||||
- Set `youtube.subtitleSource` to `whisper` to transcribe YouTube videos with Whisper instead of using YouTube's captions.
|
||||
- The video stays paused while Whisper transcribes, and the subtitle generation modal shows progress.
|
||||
- The subtitle generation modal (`Ctrl+Shift+G`) now works on YouTube videos, including Windows stream playback.
|
||||
- **Scroll Wheel and Mouse Bindings**: `keybindings` now accepts scroll wheel keys, and the settings key editor can capture them. Mouse button and wheel bindings from mpv now work while the cursor is over the overlay, including on Hyprland.
|
||||
- **Run Commands from Session Help**: During playback, press `Enter` or double-click a row in session help to run that command.
|
||||
|
||||
**Changed**
|
||||
- **Release Assets**: Releases no longer include package-size reports.
|
||||
|
||||
**Fixed**
|
||||
- **Hyprland Overlay**: The overlay stays clickable after mpv goes fullscreen with `mpv.backend: x11`, and it no longer interrupts mpv while mpv enters fullscreen.
|
||||
- **mpv Key Bindings**: Bindings from mpv such as `9`/`0` for volume now work while the overlay has focus, even if the overlay loaded before mpv connected.
|
||||
- **Jellyfin**:
|
||||
- The Japanese subtitle track is selected as soon as it downloads, without waiting for the other tracks.
|
||||
- Japanese and English tracks are auto-selected on episodes whose embedded subtitles are images.
|
||||
- Timing review previews and mined card audio use the audio track you are playing.
|
||||
- **Media Timing Review**: Trim handles are now thin lines on the clip edges, and original subtitle boundaries no longer cover the waveform. The preview cursor no longer stutters on macOS with Bluetooth headphones.
|
||||
- **YouTube Subtitles**: Japanese subtitles no longer fail with "HTTP 429" on videos where YouTube also lists a machine-translated Japanese track.
|
||||
- **Stats**:
|
||||
- Mined cards are counted once in session and lifetime stats.
|
||||
- Word and kanji dates and the new-words charts stay accurate after deleting sessions. Existing data is repaired in the background.
|
||||
- Overview totals, charts, and the activity calendar refresh after deleting sessions.
|
||||
- **Yomitan Data Safety**: Electron is updated to 43.7.7. SubMiner now refuses to load your Yomitan data with an unsupported or downgraded runtime. Automatic character dictionary changes stop if Yomitan suddenly reports no dictionaries.
|
||||
- **AppImage in Sandboxes**: The AppImage now opens its window when launched in a sandbox such as `firejail --appimage`.
|
||||
- **Ctrl+C Crash**: SubMiner no longer crashes with "write EPIPE" when the terminal command that started it is stopped with Ctrl+C.
|
||||
|
||||
**Docs**
|
||||
- **Docs Site**: The docs site is shorter and easier to scan, with a key and default table for each config block. Outdated pages were corrected, and the MeCab install command for Arch Linux is fixed.
|
||||
- **Stats Docs**: The docs now cover the Overview refresh after deleting sessions and how discovery dates are kept after old sessions are pruned.
|
||||
|
||||
<details>
|
||||
<summary>Internal changes</summary>
|
||||
|
||||
**Internal**
|
||||
- **Release Notes**: Release notes and `CHANGELOG.md` are now generated as short items per feature or area.
|
||||
- **Dev Profile**: Development launches use a separate `SubMiner-dev` profile unless production-profile access is requested.
|
||||
- **Dependencies**: undici is updated to 7.29.1 to clear the security advisories that were blocking CI.
|
||||
- **Package Checks**: CI no longer compares package sizes. Package content checks still run.
|
||||
|
||||
</details>
|
||||
|
||||
<h2>v0.20.0 (2026-09-23)</h2>
|
||||
|
||||
**Added**
|
||||
- **Japanese Subtitle Generation**:
|
||||
@@ -121,7 +244,7 @@
|
||||
|
||||
</details>
|
||||
|
||||
## Previous Versions
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v0.19.x</summary>
|
||||
|
||||
+120
-79
@@ -52,7 +52,7 @@ SubMiner watches the config file while running. When it changes, live settings a
|
||||
|
||||
These apply live:
|
||||
|
||||
- `subtitleStyle`, `subtitleSidebar`, `subtitleSelection`, `keybindings`, `shortcuts`
|
||||
- `subtitleStyle`, `subtitleSidebar`, `subtitleSelection`, `subtitleFilter`, `keybindings`, `shortcuts`
|
||||
- `logging.level`, `logging.rotation`, `logging.files`
|
||||
- `secondarySub.defaultMode`, `youtube.primarySubLanguages`, `youtube.subtitleSource`
|
||||
- `mpv.aniskipEnabled`, `mpv.aniskipButtonKey`, `stats.toggleKey`, `stats.markWatchedKey`
|
||||
@@ -67,6 +67,12 @@ Everything else needs a restart.
|
||||
|
||||
## Core settings
|
||||
|
||||
### Dictionary backend
|
||||
|
||||
`dictionaryBackend` accepts `"yomitan"` or `"hachidori"`. The default is defined in the [generated configuration example](/config.example.jsonc). Restart SubMiner after changing it. The tray shows the selected backend's settings, and the global dictionary-settings shortcut follows the same selection.
|
||||
|
||||
Each backend stores its own dictionaries and mining settings. `yomitan.externalProfilePath` applies only to Yomitan. See [Hachidori setup](./usage.md#hachidori-setup) before switching an existing installation.
|
||||
|
||||
### Logging
|
||||
|
||||
Log files are named by date (`app-YYYY-MM-DD.log`, `launcher-...`, `mpv-...`). Log export writes a sanitized copy and leaves the originals alone.
|
||||
@@ -100,6 +106,15 @@ Overlay notifications are also kept in a session-only history panel. Toggle it w
|
||||
|
||||
Mining and startup status notifications use `ankiConnect.behavior.notificationType` (see [AnkiConnect](#ankiconnect)).
|
||||
|
||||
### Plugins
|
||||
|
||||
`plugins` enables [plugins](/plugins) from the `plugins/` folder next to this config file. Each key is a plugin folder name. Changes apply after a restart.
|
||||
|
||||
| Key | Default | What it does |
|
||||
| ----------------------- | ------- | -------------------------------------- |
|
||||
| `plugins.<id>.enabled` | `false` | Load the plugin at startup |
|
||||
| `plugins.<id>.settings` | `{}` | Passed to the plugin as `ctx.settings` |
|
||||
|
||||
### Auto-start overlay
|
||||
|
||||
When mpv is started by SubMiner or the `subminer` launcher, the launcher passes these settings to the bundled mpv plugin. There is no separate plugin config file. `mpv.autoStartSubMiner` and `mpv.pauseUntilOverlayReady` (see [MPV launcher](#mpv-launcher)) control the background start and the initial pause.
|
||||
@@ -160,30 +175,31 @@ Controls how primary and secondary subtitles look and which annotations they sho
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Default | What it does |
|
||||
| ------------------------------------------------ | ------------ | ----------------------------------------------------------------------------------------------------- |
|
||||
| `subtitleStyle.primaryDefaultMode` | `"visible"` | Primary bar at startup: `hidden`, `visible`, or `hover` |
|
||||
| `subtitleStyle.css` | see example | CSS for primary subtitles (font, size `35px`, color, shadow, and so on) |
|
||||
| `subtitleStyle.secondary.css` | see example | CSS for secondary subtitles (size `24px`) |
|
||||
| `subtitleStyle.preserveLineBreaks` | `false` | Keep line breaks as mpv shows them instead of one line |
|
||||
| `subtitleStyle.autoPauseVideoOnHover` | `true` | Pause while the mouse is over subtitle text |
|
||||
| `subtitleStyle.autoPauseVideoOnYomitanPopup` | `true` | Pause while a Yomitan popup is open |
|
||||
| `subtitleStyle.primaryVisibleOnYomitanPopup` | `true` | In hover mode, keep the primary bar visible while a popup is open |
|
||||
| `subtitleStyle.knownWordColor` | `#a6da95` | Known-word highlight color |
|
||||
| `subtitleStyle.knownWordMaturityColors` | see example | `new`, `learning`, `young`, `mature` colors, used when `ankiConnect.knownWords.maturityEnabled` is on |
|
||||
| `subtitleStyle.nPlusOneColor` | `#c6a0f6` | N+1 target word color |
|
||||
| `subtitleStyle.enableJlpt` | `false` | Underline words by JLPT level |
|
||||
| `subtitleStyle.jlptColors` | see example | Underline colors for `N1` to `N5` |
|
||||
| `subtitleStyle.nameMatchEnabled` | `false` | Sync the character dictionary and color character names |
|
||||
| `subtitleStyle.nameMatchImagesEnabled` | `false` | Show small character portraits next to matched names |
|
||||
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Character-name color |
|
||||
| `subtitleStyle.frequencyDictionary.enabled` | `false` | Color words by frequency rank |
|
||||
| `subtitleStyle.frequencyDictionary.sourcePath` | `""` | Folder with `term_meta_bank_*.json` files. Empty searches the default locations |
|
||||
| `subtitleStyle.frequencyDictionary.topX` | `10000` | Only color words ranked at or below this |
|
||||
| `subtitleStyle.frequencyDictionary.mode` | `"single"` | `single` (one color) or `banded` (five colors, common to rare) |
|
||||
| `subtitleStyle.frequencyDictionary.matchMode` | `"headword"` | Look up by `headword` (dictionary form) or `surface` (text as shown) |
|
||||
| `subtitleStyle.frequencyDictionary.singleColor` | `#f5a97f` | Color for `single` mode |
|
||||
| `subtitleStyle.frequencyDictionary.bandedColors` | see example | Five colors for `banded` mode |
|
||||
| Key | Default | What it does |
|
||||
| ------------------------------------------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `subtitleStyle.primaryDefaultMode` | `"visible"` | Primary bar at startup: `hidden`, `visible`, or `hover` |
|
||||
| `subtitleStyle.css` | see example | CSS for primary subtitles (font, size `35px`, color, shadow, and so on) |
|
||||
| `subtitleStyle.secondary.css` | see example | CSS for secondary subtitles (size `24px`) |
|
||||
| `subtitleStyle.preserveLineBreaks` | `false` | Keep line breaks as mpv shows them instead of one line |
|
||||
| `subtitleStyle.autoPauseVideoOnHover` | `true` | Pause while the mouse is over subtitle text |
|
||||
| `subtitleStyle.autoPauseVideoOnYomitanPopup` | `true` | Pause while a Yomitan popup is open |
|
||||
| `subtitleStyle.autoPauseVideoOnKeyboardMode` | `false` | Pause while keyboard-only mode is on (keyboard or controller toggle). Leaving resumes only a pause it made: a video that was already paused, or that you played or paused yourself meanwhile, stays as it is |
|
||||
| `subtitleStyle.primaryVisibleOnYomitanPopup` | `true` | In hover mode, keep the primary bar visible while a popup is open |
|
||||
| `subtitleStyle.knownWordColor` | `#a6da95` | Known-word highlight color |
|
||||
| `subtitleStyle.knownWordMaturityColors` | see example | `new`, `learning`, `young`, `mature` colors, used when `ankiConnect.knownWords.maturityEnabled` is on |
|
||||
| `subtitleStyle.nPlusOneColor` | `#c6a0f6` | N+1 target word color |
|
||||
| `subtitleStyle.enableJlpt` | `false` | Underline words by JLPT level |
|
||||
| `subtitleStyle.jlptColors` | see example | Underline colors for `N1` to `N5` |
|
||||
| `subtitleStyle.nameMatchEnabled` | `false` | Sync the character dictionary and color character names |
|
||||
| `subtitleStyle.nameMatchImagesEnabled` | `false` | Show small character portraits next to matched names |
|
||||
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Character-name color |
|
||||
| `subtitleStyle.frequencyDictionary.enabled` | `false` | Color words by frequency rank |
|
||||
| `subtitleStyle.frequencyDictionary.sourcePath` | `""` | Folder with `term_meta_bank_*.json` files. Empty searches the default locations |
|
||||
| `subtitleStyle.frequencyDictionary.topX` | `10000` | Only color words ranked at or below this |
|
||||
| `subtitleStyle.frequencyDictionary.mode` | `"single"` | `single` (one color) or `banded` (five colors, common to rare) |
|
||||
| `subtitleStyle.frequencyDictionary.matchMode` | `"headword"` | Look up by `headword` (dictionary form) or `surface` (text as shown) |
|
||||
| `subtitleStyle.frequencyDictionary.singleColor` | `#f5a97f` | Color for `single` mode |
|
||||
| `subtitleStyle.frequencyDictionary.bandedColors` | see example | Five colors for `banded` mode |
|
||||
|
||||
Two CSS custom properties style the hovered word: `--subtitle-hover-token-color` (`#f4dbd6`) and `--subtitle-hover-token-background-color` (`transparent`). Set them inside `subtitleStyle.css`.
|
||||
|
||||
@@ -205,6 +221,29 @@ Sidebar custom properties: `--subtitle-sidebar-max-width` (`420px`), `--subtitle
|
||||
|
||||
If `embedded` layout places the video oddly on your system, switch back to `overlay`.
|
||||
|
||||
### Subtitle filters {#subtitle-filters}
|
||||
|
||||
Removes unwanted text from primary subtitles, such as speaker names, readings and sound descriptions. Filtering happens as the line arrives, so the overlay, sidebar, mined sentences and stats all see the filtered text. A line left with no words is hidden. In the settings window, find it under **Behavior → Subtitle Filters**.
|
||||
|
||||
| Key | Default | What it does |
|
||||
| -------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| `subtitleFilter.presets.parentheses` | `false` | Remove `(…)` and `(…)` anywhere in a line. A line wrapped entirely in parentheses (inner thoughts) is kept |
|
||||
| `subtitleFilter.presets.brackets` | `false` | Remove `[…]`, `[…]`, `【…】` and `〔…〕`, such as sound effects |
|
||||
| `subtitleFilter.presets.speakerPrefix` | `false` | Remove a short name before a colon at the start of a line, as in `山田:` |
|
||||
| `subtitleFilter.presets.musicSymbols` | `false` | Remove `♪`, `♫` and `♬` |
|
||||
| `subtitleFilter.customRules` | `[]` | Your own regex rules, applied in order after the presets |
|
||||
|
||||
Each custom rule is `{ "name", "pattern", "replacement", "enabled" }`. Patterns are JavaScript regular expressions compiled with the `gmu` flags, so `^` and `$` match each line. An empty `replacement` removes the match.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"subtitleFilter": {
|
||||
"presets": { "parentheses": true },
|
||||
"customRules": [{ "name": "Join lines", "pattern": "\\n", "replacement": "", "enabled": true }],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Subtitle position
|
||||
|
||||
You can also drag subtitles with `Right-click + drag` while watching.
|
||||
@@ -262,7 +301,7 @@ Adds a modal for choosing mpv's primary and secondary subtitle tracks. Open it w
|
||||
|
||||
- `key` uses `KeyboardEvent.code` names (`Space`, `KeyR`, `ArrowRight`) with optional `Ctrl+`, `Alt+`, `Shift+`, `Meta+`. Mouse buttons are `MBTN_LEFT`, `MBTN_MID`, `MBTN_RIGHT`, `MBTN_BACK`, `MBTN_FORWARD`. Scroll wheel keys are `WHEEL_UP`, `WHEEL_DOWN`, `WHEEL_LEFT`, `WHEEL_RIGHT`.
|
||||
- `command` is any mpv JSON IPC command array. Set it to `null` to disable a default.
|
||||
- Commands starting with `__` run inside SubMiner: `__playlist-browser-open`, `__youtube-picker-open`, `__replay-subtitle`, `__play-next-subtitle`, `__runtime-options-open`, and `__runtime-option-cycle:<id>[:next|prev]`.
|
||||
- Commands starting with `__` run inside SubMiner: `__playlist-browser-open`, `__youtube-picker-open`, `__replay-subtitle`, `__play-next-subtitle`, `__runtime-options-open`, `__runtime-option-cycle:<id>[:next|prev]`, and `__plugin-action:<plugin id>/<action>` (see [Plugins](/plugins)).
|
||||
- Unused single-key, mouse button, and scroll wheel bindings from your mpv config also work in the overlay. Your SubMiner bindings win on conflicts.
|
||||
|
||||
### Shortcuts configuration
|
||||
@@ -275,21 +314,22 @@ Adds a modal for choosing mpv's primary and secondary subtitle tracks. Open it w
|
||||
|
||||
### Controller support
|
||||
|
||||
Gamepad input for the overlay, through the browser Gamepad API. It only works while keyboard-only mode is on. Use the `Alt+C` modal to pick a controller and learn bindings, and `Alt+Shift+C` to see raw button and axis values. Default button actions are on [Keyboard shortcuts](/shortcuts).
|
||||
Gamepad input for the overlay, through the browser Gamepad API. Most actions only work while keyboard-only mode is on; the toggle for that mode, the secondary subtitle hold (`controller.bindings.holdSecondarySub`), pause (`toggleMpvPause`) and the subtitle line actions (`previousSubtitle`, `nextSubtitle`, `replaySubtitle`, `playNextSubtitle`) work either way. Use the `Alt+C` modal to pick a controller and learn bindings, and `Alt+Shift+C` to see raw button and axis values. Default button actions are in [Controller support](/usage#controller-support).
|
||||
|
||||
| Key | Default | What it does |
|
||||
| ---------------------------------- | -------- | ------------------------------------------------------------------------------------- |
|
||||
| `controller.enabled` | `false` | Enable controller support. The `Alt+C` and `Alt+Shift+C` modals stay closed while off |
|
||||
| `controller.smoothScroll` | `true` | Smooth popup scrolling |
|
||||
| `controller.scrollPixelsPerSecond` | `900` | Popup scroll speed |
|
||||
| `controller.horizontalJumpPixels` | `160` | Popup page-jump distance |
|
||||
| `controller.stickDeadzone` | `0.2` | Stick deadzone |
|
||||
| `controller.triggerInputMode` | `"auto"` | `auto`, `digital`, or `analog`. Use `analog` if your L2/R2 report analog values |
|
||||
| `controller.triggerDeadzone` | `0.5` | Trigger threshold for `auto` and `analog` |
|
||||
| `controller.repeatDelayMs` | `320` | Delay before a held button repeats |
|
||||
| `controller.repeatIntervalMs` | `120` | Repeat interval for held buttons |
|
||||
| Key | Default | What it does |
|
||||
| ---------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `controller.enabled` | `false` | Enable controller support. The `Alt+C` and `Alt+Shift+C` modals stay closed while off |
|
||||
| `controller.smoothScroll` | `true` | Smooth popup scrolling |
|
||||
| `controller.scrollPixelsPerSecond` | `900` | Popup scroll speed |
|
||||
| `controller.horizontalJumpPixels` | `160` | Popup page-jump distance |
|
||||
| `controller.stickDeadzone` | `0.2` | Stick deadzone |
|
||||
| `controller.triggerInputMode` | `"auto"` | `auto`, `digital`, or `analog`. Use `analog` if your L2/R2 report analog values |
|
||||
| `controller.triggerDeadzone` | `0.5` | Trigger threshold for `auto` and `analog` |
|
||||
| `controller.faceButtonLabels` | `"xbox"` | Face button style: `xbox`, `nintendo` (A on the right, B at the bottom; Switch-style and many 8BitDo pads) or `playstation` (╳ ○ □ △). **Button Style** in `Alt+C` sets it per controller; this is the default for the others |
|
||||
| `controller.repeatDelayMs` | `320` | Delay before a held button repeats |
|
||||
| `controller.repeatIntervalMs` | `120` | Repeat interval for held buttons |
|
||||
|
||||
Bindings are set with `Alt+C` learn mode, which saves them per controller.
|
||||
Bindings, including the media timing review and duplicate card dialog layouts, are set with `Alt+C` learn mode, which saves them per controller.
|
||||
|
||||
## Anki integration
|
||||
|
||||
@@ -323,15 +363,15 @@ Creates and updates Anki cards with sentence, audio, and screenshot. Needs the [
|
||||
|
||||
**Fields**
|
||||
|
||||
| Key | Default | What it does |
|
||||
| ------------------------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `ankiConnect.fields.word` | `"Expression"` | Word field |
|
||||
| `ankiConnect.fields.audio` | `"ExpressionAudio"` | Field that receives sentence audio. Set a separate field such as `SentenceAudio` so it does not overwrite Yomitan's word audio |
|
||||
| `ankiConnect.fields.wordAudio` | `"ExpressionAudio"` | Existing word-audio field, read only to time animated images |
|
||||
| `ankiConnect.fields.image` | `"Picture"` | Screenshot field |
|
||||
| `ankiConnect.fields.sentence` | `"Sentence"` | Sentence field |
|
||||
| `ankiConnect.fields.miscInfo` | `"MiscInfo"` | Metadata field. `null` disables |
|
||||
| `ankiConnect.metadata.pattern` | `"[SubMiner] %f (%t)"` | MiscInfo template: `%f` filename, `%F` filename with extension, `%t` time, `%T` time with ms, `<br>` newline |
|
||||
| Key | Default | What it does |
|
||||
| ------------------------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `ankiConnect.fields.word` | `"Expression"` | Word field |
|
||||
| `ankiConnect.fields.audio` | `"SentenceAudio"` | Field that receives sentence audio. Keep it separate from Yomitan's word-audio field so it does not overwrite that audio |
|
||||
| `ankiConnect.fields.wordAudio` | `"ExpressionAudio"` | Existing word-audio field, read only to time animated images |
|
||||
| `ankiConnect.fields.image` | `"Picture"` | Screenshot field |
|
||||
| `ankiConnect.fields.sentence` | `"Sentence"` | Sentence field |
|
||||
| `ankiConnect.fields.miscInfo` | `"MiscInfo"` | Metadata field. `null` disables |
|
||||
| `ankiConnect.metadata.pattern` | `"[SubMiner] %f (%t)"` | MiscInfo template: `%f` filename, `%F` filename with extension, `%t` time, `%T` time with ms, `<br>` newline |
|
||||
|
||||
**Media**
|
||||
|
||||
@@ -431,13 +471,14 @@ When SubMiner fills the sentence on a word card, it sets one card-type flag and
|
||||
|
||||
Search and download Japanese subtitles from [Jimaku](https://jimaku.cc). See [Jimaku integration](/jimaku-integration).
|
||||
|
||||
| Key | Default | What it does |
|
||||
| --------------------------- | --------------------- | ---------------------------------------------------------- |
|
||||
| `jimaku.apiKey` | `""` | API key. Optional, but raises your rate limit |
|
||||
| `jimaku.apiKeyCommand` | `""` | Shell command that prints the key. Use instead of `apiKey` |
|
||||
| `jimaku.apiBaseUrl` | `"https://jimaku.cc"` | API base URL |
|
||||
| `jimaku.languagePreference` | `"ja"` | Preferred language: `ja`, `en`, or `none` |
|
||||
| `jimaku.maxEntryResults` | `10` | Maximum search results |
|
||||
| Key | Default | What it does |
|
||||
| ------------------------------------------- | --------------------- | --------------------------------------------------------------------------------- |
|
||||
| `jimaku.apiKey` | `""` | API key. Optional, but raises your rate limit |
|
||||
| `jimaku.apiKeyCommand` | `""` | Shell command that prints the key. Use instead of `apiKey` |
|
||||
| `jimaku.apiBaseUrl` | `"https://jimaku.cc"` | API base URL |
|
||||
| `jimaku.languagePreference` | `"ja"` | Preferred language: `ja`, `en`, or `none` |
|
||||
| `jimaku.maxEntryResults` | `10` | Maximum search results |
|
||||
| `jimaku.autoOpenWhenPrimarySubtitleMissing` | `false` | Pause and open the Jimaku modal when a local video has no Japanese subtitle track |
|
||||
|
||||
### TsukiHime
|
||||
|
||||
@@ -514,18 +555,18 @@ In external-profile mode, SubMiner only reads the profile. It does not open its
|
||||
|
||||
Log in to a Jellyfin server, browse libraries, and play or cast to SubMiner. Login tokens are stored encrypted, not in this file. See [Jellyfin integration](/jellyfin-integration).
|
||||
|
||||
| Key | Default | What it does |
|
||||
| ----------------------------------- | -------------------------------- | ----------------------------------------------- |
|
||||
| `jellyfin.enabled` | `false` | Enable Jellyfin |
|
||||
| `jellyfin.serverUrl` | `""` | Server URL, for example `http://localhost:8096` |
|
||||
| `jellyfin.username` | `""` | Default username for `subminer jellyfin -l` |
|
||||
| `jellyfin.remoteControlEnabled` | `true` | Let Jellyfin apps cast to SubMiner |
|
||||
| `jellyfin.remoteControlAutoConnect` | `true` | Connect the cast session on startup |
|
||||
| `jellyfin.autoAnnounce` | `false` | Announce SubMiner as a cast target on connect |
|
||||
| `jellyfin.pullPictures` | `false` | Fetch posters for launcher pickers |
|
||||
| `jellyfin.iconCacheDir` | `"/tmp/subminer-jellyfin-icons"` | Poster cache folder |
|
||||
| `jellyfin.directPlayPreferred` | `true` | Try direct play before transcoding |
|
||||
| `jellyfin.transcodeVideoCodec` | `"h264"` | Codec requested when transcoding |
|
||||
| Key | Default | What it does |
|
||||
| ----------------------------------- | -------------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| `jellyfin.enabled` | `false` | Enable Jellyfin |
|
||||
| `jellyfin.serverUrl` | `""` | Server URL, for example `http://localhost:8096` |
|
||||
| `jellyfin.username` | `""` | Default username for `subminer jellyfin -l` |
|
||||
| `jellyfin.remoteControlEnabled` | `true` | Let Jellyfin apps cast to SubMiner |
|
||||
| `jellyfin.remoteControlAutoConnect` | `true` | Connect the cast session on startup (also needs `startupWarmups.jellyfinRemoteSession`) |
|
||||
| `jellyfin.autoAnnounce` | `false` | Announce SubMiner as a cast target on connect |
|
||||
| `jellyfin.pullPictures` | `false` | Fetch posters for launcher pickers |
|
||||
| `jellyfin.iconCacheDir` | `"/tmp/subminer-jellyfin-icons"` | Poster cache folder |
|
||||
| `jellyfin.directPlayPreferred` | `true` | Try direct play before transcoding |
|
||||
| `jellyfin.transcodeVideoCodec` | `"h264"` | Codec requested when transcoding |
|
||||
|
||||
### Discord rich presence
|
||||
|
||||
@@ -579,18 +620,18 @@ A local web dashboard at `http://127.0.0.1:<serverPort>`, also available as an o
|
||||
|
||||
Settings for mpv instances that SubMiner starts, and for the bundled mpv plugin. See [mpv plugin](/mpv-plugin).
|
||||
|
||||
| Key | Default | What it does |
|
||||
| ---------------------------- | ----------------- | --------------------------------------------------------------------------- |
|
||||
| `mpv.executablePath` | `""` | Path to `mpv.exe` on Windows. Empty checks `SUBMINER_MPV_PATH`, then `PATH` |
|
||||
| `mpv.launchMode` | `"normal"` | Window state: `normal`, `maximized`, or `fullscreen` |
|
||||
| `mpv.profile` | `""` | mpv profile to pass. Combined with a launcher `--profile` if both are set |
|
||||
| `mpv.socketPath` | platform-specific | mpv IPC socket. See the warning under [Config file](#configuration-file) |
|
||||
| `mpv.backend` | `"auto"` | Window tracking: `auto`, `hyprland`, `sway`, `x11`, `macos`, `windows` |
|
||||
| `mpv.autoStartSubMiner` | `true` | Start SubMiner in the background when mpv loads a file |
|
||||
| `mpv.pauseUntilOverlayReady` | `true` | Keep mpv paused until subtitles are ready, up to 30 seconds |
|
||||
| `mpv.subminerBinaryPath` | `""` | SubMiner app path for the plugin. Empty uses the detected path |
|
||||
| `mpv.aniskipEnabled` | `true` | Detect intros with AniSkip and show a skip prompt |
|
||||
| `mpv.aniskipButtonKey` | `"TAB"` | mpv key that skips the intro while the prompt is shown |
|
||||
| Key | Default | What it does |
|
||||
| ---------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| `mpv.executablePath` | `""` | Full path to mpv. Empty auto-discovers it ([lookup order](/jellyfin-integration#casting-from-jellyfin)) |
|
||||
| `mpv.launchMode` | `"normal"` | Window state: `normal`, `maximized`, or `fullscreen` |
|
||||
| `mpv.profile` | `""` | mpv profile to pass. Combined with a launcher `--profile` if both are set |
|
||||
| `mpv.socketPath` | platform-specific | mpv IPC socket. See the warning under [Config file](#configuration-file) |
|
||||
| `mpv.backend` | `"auto"` | Window tracking: `auto`, `hyprland`, `sway`, `x11`, `macos`, `windows` |
|
||||
| `mpv.autoStartSubMiner` | `true` | Start SubMiner in the background when mpv loads a file |
|
||||
| `mpv.pauseUntilOverlayReady` | `true` | Keep mpv paused until subtitles are ready, up to 30 seconds |
|
||||
| `mpv.subminerBinaryPath` | `""` | SubMiner app path for the plugin. Empty uses the detected path |
|
||||
| `mpv.aniskipEnabled` | `true` | Detect intros with AniSkip and show a skip prompt |
|
||||
| `mpv.aniskipButtonKey` | `"TAB"` | mpv key that skips the intro while the prompt is shown |
|
||||
|
||||
### YouTube playback settings
|
||||
|
||||
|
||||
@@ -175,9 +175,6 @@ Run `make help` for the full list.
|
||||
| `SUBMINER_MPV_PLUGIN_PATH` | mpv plugin directory the launcher injects |
|
||||
| `SUBMINER_LOG_LEVEL` | App log level (`debug`, `info`, `warn`, `error`) |
|
||||
| `SUBMINER_MPV_LOG` | Shared mpv/app log file path |
|
||||
| `SUBMINER_JIMAKU_API_KEY` | Jimaku API key for launcher subtitle downloads |
|
||||
| `SUBMINER_JIMAKU_API_KEY_COMMAND` | Command that prints the Jimaku API key |
|
||||
| `SUBMINER_JIMAKU_API_BASE_URL` | Jimaku API base URL |
|
||||
| `SUBMINER_JELLYFIN_ACCESS_TOKEN` | Jellyfin access token, used before the stored encrypted session |
|
||||
| `SUBMINER_JELLYFIN_USER_ID` | Jellyfin user ID |
|
||||
| `SUBMINER_SKIP_MACOS_HELPER_BUILD` | Set to `1` to skip the macOS helper build during `bun run build` |
|
||||
@@ -64,22 +64,37 @@ The same show can end up on several cards when release names disagree. To fix th
|
||||
|
||||
### Trends
|
||||
|
||||
Charts for watch time, cards, words, and sessions per day or month, running totals, efficiency (words per minute, cards per hour), and viewing patterns by weekday and hour. Each chart has its own date range and grouping.
|
||||
Pick a date range (or set **Custom** dates), group by day, week, or month, and the whole page follows. Your choices are remembered.
|
||||
|
||||
- **Summary**: totals for the range, compared with the period just before it.
|
||||
- **Activity**: watch time, cards, words, and finished episodes. Click a bar to open that period's sessions.
|
||||
- **Learning**: new words per 1,000 words seen, and how common those new words are. Conjugations of a word count once, and names are left out.
|
||||
- **Efficiency**: words per minute, cards per hour, and lookups per 100 words. Daily views use a rolling 7 days, and gaps mean too little data to rate.
|
||||
- **Running total**: one chart with a switcher for each metric, climbing from zero at the start of the range.
|
||||
- **Patterns**: a day and hour heatmap of when you watch.
|
||||
- **Titles**: a timeline of what you watched when, and a sortable summary table. Click a title, or use the filter box, to narrow the page to it. **↗** opens the title.
|
||||
|
||||
An episode counts as finished once, on the last day you watched it.
|
||||
|
||||

|
||||
|
||||
### Sessions
|
||||
|
||||
Session history with new-word activity and pause, seek, and card markers. The **↗** button on a row opens that show's detail view.
|
||||
Session history. Expand a session for its pace, lines, known-word rate, lookups, and paused time, plus a reading density chart of known and unknown words over the session with Yomitan lookup ticks and clickable card-mined markers underneath. The **↗** button on a row opens that show's detail view.
|
||||
|
||||

|
||||
|
||||
### Vocabulary
|
||||
|
||||
Unique words and kanji you have seen, new words per day, frequency rank tables with Hide Known and Hide Kana filters, and a kanji breakdown. Click a word to see every line it appeared in.
|
||||
Words are counted as dictionary words, so a word's conjugations count once. Names are left out unless you turn off **Hide names**.
|
||||
|
||||
- **Exclusions** hides words from every vocabulary view. You can restore them from the same dialog.
|
||||
- **Duplicates** cleans up lines repeated by karaoke openings and animated signs (see [Repeated lines](#repeated-lines)).
|
||||
- **Summary**: words seen, how many are in Anki, new words this week, and kanji seen.
|
||||
- **Known Words by Frequency**: how much of each frequency band you already know.
|
||||
- **Vocabulary Growth**: your running total of words seen.
|
||||
- **Words**: one table for every word you have seen. **Worth mining** lists common words you haven't mined, most seen first. **All words**, **Across titles**, and **Newest** are the other views. Sort by any column, or filter by known words, names, kana, and part of speech. Searching looks through everything, including conjugated forms and readings. Click a word for its forms, where it appeared, and example lines you can mine from. **Hide** excludes a word.
|
||||
- **Kanji**: every kanji you have seen, shaded by how often. Dashed kanji are not in any word you know.
|
||||
|
||||
**Exclusions** at the bottom of the page lists hidden words so you can restore them. **Clean up duplicates** removes lines repeated by karaoke openings and animated signs (see [Repeated lines](#repeated-lines)).
|
||||
|
||||

|
||||
|
||||
@@ -89,19 +104,21 @@ Searches the primary subtitle lines and titles in your history. **Search by head
|
||||
|
||||
## Mining from the dashboard
|
||||
|
||||
Search results and the Vocabulary word panel can create cards from past lines, as long as the source video file is still available:
|
||||
Search results and the Vocabulary word panel can create cards from past lines, as long as the source video file is still available. Lines from a stream, such as Jellyfin or YouTube, have no file to cut clips from, so they say so instead of offering mining. Lines you can mine are listed first. The word panel counts how many of a word's lines this affects, and the word table marks words with only streamed lines as **Stream only**.
|
||||
|
||||
- **Mine Word**: full Yomitan lookup for the word, plus sentence, audio, and image.
|
||||
- **Mine Word**: looks up the word with the selected dictionary backend (Yomitan or Hachidori), plus sentence, audio, and image. The history line you picked is the card's sentence, even while mpv plays another line. Hachidori uses its own Anki template, dictionary aliases, and frequency data.
|
||||
- **Mine Sentence**: a sentence card with `IsSentenceCard` set, for Lapis and Kiku note types.
|
||||
- **Mine Audio**: an audio card with `IsAudioCard` set.
|
||||
|
||||
Word and audio mining appear only when the word occurs in the sentence. All three use your `ankiConnect` deck, note type, fields, and media settings. Anki must be running, and Mine Word needs Yomitan dictionaries.
|
||||
Word and audio mining appear only when the word occurs in the sentence. All three use your `ankiConnect` deck, note type, fields, and media settings. Anki must be running, and Mine Word needs dictionaries in the selected backend.
|
||||
|
||||
Dashboard cards also get the `SubMiner::Stats` tag. With the Anki proxy off, SubMiner uses it to keep the chosen history line when polling picks up the card.
|
||||
|
||||
## Repeated lines
|
||||
|
||||
Karaoke openings and animated signs repeat the same text once per frame. SubMiner collapses these as it records, so one lyric is stored once. Stats recorded before that can hold hundreds of copies and skew Top Repeated Words.
|
||||
|
||||
To clean them, use **Duplicates** in the Vocabulary tab: pick a time window, **Scan** to preview, then **Clean Up**. Or from the terminal:
|
||||
To clean them, use **Clean up duplicates** in the Vocabulary tab: pick a time window, **Scan** to preview, then **Clean Up**. Or from the terminal:
|
||||
|
||||
```bash
|
||||
subminer stats cleanup --duplicate-lines --dry-run --lookback-days 30
|
||||
|
||||
@@ -187,7 +187,7 @@ First-run setup can install the `subminer` command for you.
|
||||
xattr -d com.apple.quarantine /Applications/SubMiner.app
|
||||
```
|
||||
|
||||
3. Open **System Settings > Privacy & Security > Accessibility** and enable SubMiner (add it if it is missing). The overlay cannot follow the mpv window without this.
|
||||
3. Open **System Settings > Privacy & Security > Accessibility** and enable SubMiner (add it if it is missing). On macOS 27 this list is named **Device Control and Data Access**. SubMiner uses it to track the mpv window. Without it, SubMiner falls back to a basic window lookup that cannot tell when mpv is minimized.
|
||||
|
||||
First-run setup can install the `subminer` command for you.
|
||||
|
||||
@@ -226,7 +226,7 @@ make deps
|
||||
make build-macos
|
||||
```
|
||||
|
||||
The `.dmg` and `.zip` land in `release/`. For an unsigned local build, run `bun run build:mac:unsigned`.
|
||||
The `.dmg` and `.zip` land in `dist/release/`. For an unsigned local build, run `bun run build:mac:unsigned`.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ SubMiner stores an encrypted session token, not your password, and turns the int
|
||||
|
||||
## Casting from Jellyfin
|
||||
|
||||
After you sign in, SubMiner connects to Jellyfin at startup and shows up in the cast ("Play on") menu under your computer's hostname. To connect for the current session only, tick **Jellyfin Discovery** in the tray menu.
|
||||
After you sign in, tick **Jellyfin Discovery** in the tray menu. SubMiner then shows up in the cast ("Play on") menu under your computer's hostname until it quits. To connect every time SubMiner starts, turn on `startupWarmups.jellyfinRemoteSession` and leave `remoteControlAutoConnect` on.
|
||||
|
||||
1. In the Jellyfin web or mobile app, start playing an episode.
|
||||
2. Open the cast menu and pick your computer.
|
||||
@@ -23,7 +23,9 @@ SubMiner starts mpv if it is not already running. Pause, seek, stop, and track c
|
||||
|
||||
SubMiner selects a Japanese subtitle track automatically and resets mpv's subtitle delay to zero. It direct-plays files when it can and asks Jellyfin to transcode the rest.
|
||||
|
||||
On Windows, casting finds mpv through `mpv.executablePath`, then `SUBMINER_MPV_PATH`, then `PATH`. An invalid `mpv.executablePath` stops mpv from starting.
|
||||
If an episode has no Japanese subtitles, you can [generate them with Whisper](/subtitle-generation#generating-from-the-overlay) while it plays.
|
||||
|
||||
Casting finds mpv through `mpv.executablePath`, then `SUBMINER_MPV_PATH`, then `PATH`. On macOS it then checks `/opt/homebrew/bin/mpv`, `/usr/local/bin/mpv`, and `/Applications/mpv.app`, because an app opened from Finder, the Dock, or Spotlight does not get your shell's `PATH`. If mpv lives somewhere else, set `mpv.executablePath` to its full path (`command -v mpv` prints it). An invalid `mpv.executablePath` stops mpv from starting.
|
||||
|
||||
## Playing from the terminal
|
||||
|
||||
@@ -34,20 +36,20 @@ subminer jellyfin -p # fzf picker; `jf` is an alias for `jellyfin`
|
||||
subminer -R jellyfin -p # rofi picker
|
||||
```
|
||||
|
||||
Sign in first. See [Launcher script](/launcher-script) for the other `jellyfin` subcommands.
|
||||
Sign in first. Playback resumes from Jellyfin's saved position, and watch progress syncs back the same way it does for casting, whether or not discovery is on. If SubMiner is already running, the picker uses it and leaves it running. Otherwise the launcher starts SubMiner and quits it when you close mpv, the same as playing a local file. See [Launcher script](/launcher-script) for the other `jellyfin` subcommands.
|
||||
|
||||
## Options
|
||||
|
||||
All options are under **Settings > Integrations > Jellyfin**, or `jellyfin` in `config.jsonc`. See [Configuration](/configuration#jellyfin) for the full list and defaults.
|
||||
|
||||
| Key | What it does |
|
||||
| -------------------------- | -------------------------------------------------------------------- |
|
||||
| `enabled` | Turns the integration on. Set for you when you sign in. |
|
||||
| `serverUrl` | Your Jellyfin server. Filled in when you sign in. |
|
||||
| `remoteControlEnabled` | Lets SubMiner act as a cast target. |
|
||||
| `remoteControlAutoConnect` | Connects at startup. Turn off to start discovery from the tray. |
|
||||
| `autoAnnounce` | Re-announces the device on connect. Try it if SubMiner appears late. |
|
||||
| `transcodeVideoCodec` | Video codec requested when Jellyfin transcodes. |
|
||||
| Key | What it does |
|
||||
| -------------------------- | ---------------------------------------------------------------------- |
|
||||
| `enabled` | Turns the integration on. Set for you when you sign in. |
|
||||
| `serverUrl` | Your Jellyfin server. Filled in when you sign in. |
|
||||
| `remoteControlEnabled` | Lets SubMiner act as a cast target. |
|
||||
| `remoteControlAutoConnect` | Connects at startup when `startupWarmups.jellyfinRemoteSession` is on. |
|
||||
| `autoAnnounce` | Re-announces the device on connect. Try it if SubMiner appears late. |
|
||||
| `transcodeVideoCodec` | Video codec requested when Jellyfin transcodes. |
|
||||
|
||||
For headless setups, `SUBMINER_JELLYFIN_ACCESS_TOKEN` and `SUBMINER_JELLYFIN_USER_ID` supply a session without the sign-in window. Treat the token store and `config.jsonc` as secrets.
|
||||
|
||||
|
||||
@@ -37,17 +37,32 @@ The file is saved next to the video (or to a temp directory for streams) and loa
|
||||
|
||||
You can also open the modal with `subminer app --open-jimaku`, or change the shortcut with `shortcuts.openJimaku`.
|
||||
|
||||
### Open automatically when a video has no Japanese subtitles
|
||||
|
||||
Set `jimaku.autoOpenWhenPrimarySubtitleMissing` to `true` to skip the shortcut. When a local video loads without a subtitle track in `youtube.primarySubLanguages` (Japanese by default), SubMiner pauses playback about a second after the video loads and opens the Jimaku modal, so you can pick a subtitle before watching. Playback stays paused while the modal is open and resumes when it closes, whether or not you picked a subtitle. On a cold start it resumes once subtitle processing is ready, as usual, and a pause you made yourself before the modal opened is kept.
|
||||
|
||||
A single unlabeled sidecar file (such as `episode.srt` next to the video) counts as a Japanese track, matching how SubMiner auto-selects local subtitles. An embedded track with no language tag does not, so the modal still opens for it. Audio files, streams, YouTube, and Jellyfin playback are not checked.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"jimaku": {
|
||||
"autoOpenWhenPrimarySubtitleMissing": true,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The file name parser understands `S01E03`, `1x03`, `E03`, `EP03`, and `Title - 03 -` patterns, and reads the season from a parent folder such as `Season 2`. It ignores bracket tags like `[SubGroup]` and year tags like `(2024)`.
|
||||
|
||||
## Options
|
||||
|
||||
| Key | What it does |
|
||||
| --------------------------- | ------------------------------------------------------------------- |
|
||||
| `jimaku.apiKey` | API key in plain text. |
|
||||
| `jimaku.apiKeyCommand` | Shell command that prints the API key. |
|
||||
| `jimaku.languagePreference` | Sorts files tagged with this language first: `ja`, `en`, or `none`. |
|
||||
| `jimaku.maxEntryResults` | Maximum entries per search. |
|
||||
| `jimaku.apiBaseUrl` | API address. Change only for a mirror. |
|
||||
| Key | What it does |
|
||||
| ------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| `jimaku.apiKey` | API key in plain text. |
|
||||
| `jimaku.apiKeyCommand` | Shell command that prints the API key. |
|
||||
| `jimaku.languagePreference` | Sorts files tagged with this language first: `ja`, `en`, or `none`. |
|
||||
| `jimaku.maxEntryResults` | Maximum entries per search. |
|
||||
| `jimaku.apiBaseUrl` | API address. Change only for a mirror. |
|
||||
| `jimaku.autoOpenWhenPrimarySubtitleMissing` | Pauses and opens the modal when a local video has no Japanese track. |
|
||||
|
||||
See [Configuration](/configuration#jimaku) for defaults.
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ script-message subminer-start backend=hyprland socket=/custom/path texthooker=no
|
||||
|
||||
`log-level` sets SubMiner's log verbosity. Do not use `--debug` for this; it turns on the app's dev mode.
|
||||
|
||||
The plugin also handles messages the SubMiner app sends it (`subminer-autoplay-ready`, `subminer-autoplay-hold`, `subminer-visible-overlay-shown`, `subminer-visible-overlay-hidden`, `subminer-managed-subtitles-loading`, `subminer-overlay-loading-ready`, `subminer-reload-session-bindings`). You do not need to send these yourself. The AniSkip messages are listed on the [AniSkip page](/aniskip-integration#triggering-from-mpv).
|
||||
The plugin also handles messages the SubMiner app sends it (`subminer-autoplay-ready`, `subminer-autoplay-hold`, `subminer-visible-overlay-shown`, `subminer-visible-overlay-hidden`, `subminer-managed-subtitles-loading`, `subminer-overlay-loading-ready`, `subminer-overlay-loading-dismissed`, `subminer-reload-session-bindings`). You do not need to send these yourself. The AniSkip messages are listed on the [AniSkip page](/aniskip-integration#triggering-from-mpv).
|
||||
|
||||
## Auto-start behavior
|
||||
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
# Plugins
|
||||
|
||||
Plugins are JavaScript files that SubMiner loads at startup. A plugin can react to playback, run code on a timer, add actions you bind to keys, run programs, and show results in an overlay panel, all without rebuilding SubMiner.
|
||||
|
||||
::: warning Only run plugins you trust
|
||||
Plugins run inside SubMiner with the same access to your files and system as SubMiner itself. There is no sandbox.
|
||||
:::
|
||||
|
||||
These are SubMiner app plugins. The Lua script that runs inside mpv is the separate [mpv plugin](/mpv-plugin).
|
||||
|
||||
## Install a plugin
|
||||
|
||||
1. Put the plugin in its own folder in `plugins/` next to your [config file](/configuration#configuration-file), for example `~/.config/SubMiner/plugins/ai-explain/`. The folder name is the plugin id.
|
||||
2. Enable it in `config.jsonc`. `settings` is passed to the plugin as-is:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"plugins": {
|
||||
"ai-explain": { "enabled": true, "settings": { "reasoningEffort": "medium" } },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
3. Bind its actions to keys with the `__plugin-action:<plugin id>/<action>` command:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"keybindings": [
|
||||
{ "key": "Ctrl+KeyE", "command": ["__plugin-action:ai-explain/explain"] },
|
||||
{ "key": "Ctrl+KeyT", "command": ["__plugin-action:ai-explain/translate"] },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
To run an action from a script or a window-manager shortcut, use `subminer app --plugin-action ai-explain/explain`. It goes to the running SubMiner, or starts one if none is running.
|
||||
|
||||
4. Restart SubMiner. Plugin changes apply on the next start; keybinding changes apply immediately.
|
||||
|
||||
Load errors and plugin log output go to the app log with a `plugin:<id>` scope.
|
||||
|
||||
## Example: explain the current line with Codex
|
||||
|
||||
The repository ships [`assets/plugins/ai-explain`](https://github.com/ksyasuda/SubMiner/tree/main/assets/plugins/ai-explain). It sends the current line, plus the lines around it, to [`codex exec`](https://github.com/openai/codex) and shows the answer in a panel. It needs a logged-in Codex CLI.
|
||||
|
||||
| Action | What it does |
|
||||
| ----------- | --------------------------------------------------------------- |
|
||||
| `explain` | Translation plus a breakdown of the grammar and vocabulary |
|
||||
| `translate` | Natural English translation, with a note only when it's unclear |
|
||||
|
||||
| Setting | Default | What it does |
|
||||
| ----------------- | --------- | ----------------------------------------------------------------- |
|
||||
| `codexPath` | `"codex"` | Codex binary. Use a full path if SubMiner can't find it on `PATH` |
|
||||
| `model` | `""` | Model passed to `codex exec -m`; empty uses your Codex config |
|
||||
| `reasoningEffort` | `"low"` | Codex reasoning effort; empty uses your Codex config |
|
||||
| `before`, `after` | `2`, `1` | Lines of context sent before and after the current line |
|
||||
| `pause` | `true` | Pause playback when the panel opens |
|
||||
| `timeoutMs` | `120000` | Give up after this long |
|
||||
| `prompts` | `{}` | Replace an action's instructions, e.g. `{ "translate": "..." }` |
|
||||
|
||||
Press `Esc` to close the panel. Closing it, or starting another request, stops the running Codex process.
|
||||
|
||||
## Example: sync Anki after mining
|
||||
|
||||
[`assets/plugins/anki-sync`](https://github.com/ksyasuda/SubMiner/tree/main/assets/plugins/anki-sync) runs AnkiConnect's `sync` after you mine cards, so they reach AnkiWeb without opening Anki. It waits until you stop mining for a while, so a burst of cards becomes one sync. It also adds a `sync` action you can bind to a key.
|
||||
|
||||
| Setting | Default | What it does |
|
||||
| ----------- | ------- | ------------------------------------------------------- |
|
||||
| `delayMs` | `30000` | Sync after no card has been mined for this long |
|
||||
| `syncOnEnd` | `true` | Sync right away when playback ends or mpv quits |
|
||||
| `timeoutMs` | `60000` | Give up on a sync after this long |
|
||||
| `notify` | `true` | Show a notification after each sync. Failures always do |
|
||||
|
||||
Anki must be running and logged in to AnkiWeb. Cards mined from the stats dashboard don't trigger a sync.
|
||||
|
||||
## Example: tag cards with the episode
|
||||
|
||||
[`assets/plugins/episode-tags`](https://github.com/ksyasuda/SubMiner/tree/main/assets/plugins/episode-tags) tags each mined card with the show and episode it came from, such as `show::Sousou_no_Frieren::E07`. It reads them from the file name (`[Group] Show - 07 (1080p).mkv`, `Show.S01E07.mkv`, `Show 第7話.mkv`), or from the title mpv shows for streams. It uses the source captured when mining started, so delayed media and field grouping into an older note keep the correct episode. Notes with unknown provenance, including cards discovered by polling, are left alone. Spaces become `_`.
|
||||
|
||||
| Setting | Default | What it does |
|
||||
| -------- | ------------------------ | ----------------------------------------------- |
|
||||
| `tags` | `["show::{show}::{ep}"]` | Tags to add, one template each |
|
||||
| `notify` | `false` | Show a notification with the tags for each card |
|
||||
|
||||
Templates can use `{show}`, `{season}`, `{episode}`, and `{ep}` (`S01E07`, or `E07` when there is no season). When a placeholder has no value, the tag stops before that `::` level, so a movie gets `show::<title>`. The parsing is a guess from the name: a title ending in a number, like `Mob Psycho 100`, reads that number as the episode.
|
||||
|
||||
## Write a plugin
|
||||
|
||||
A plugin folder needs a `plugin.json`:
|
||||
|
||||
```json
|
||||
{ "name": "My Plugin", "version": "1.0.0", "apiVersion": 1, "main": "main.js" }
|
||||
```
|
||||
|
||||
`apiVersion` declares the required SubMiner plugin API version. The current version is `1`; omitted values also mean v1 for existing plugins. An unsupported version is rejected before the plugin's entry module runs. `version` is the plugin's own version and does not control compatibility.
|
||||
|
||||
`main` points to a CommonJS module (`.js`) or an ES module (`.mjs`) that exports `activate(ctx)`. Activation may be asynchronous and has a 10-second timeout. A plugin's actions wait for its own activation, so a slow plugin does not hold up other plugins. If activation fails or times out, its registrations are removed and its signal aborts. Synchronous code still shares the app's main thread.
|
||||
|
||||
Activation may return a cleanup function, or the module can export `deactivate()`. On quit, SubMiner aborts `ctx.signal` first and waits up to 2 seconds in total for loading and deactivation. A plugin still reading its files is skipped. Everything a plugin registers through `ctx` is cleaned up automatically. A late activation cannot leave new listeners, timers, or actions behind, and its returned cleanup still runs once. Deactivation can call Anki, mpv, built-in actions, and notifications until disposal. After disposal, those calls throw or reject, including work that resumes after an activation or deactivation timeout.
|
||||
|
||||
```js
|
||||
function activate(ctx) {
|
||||
ctx.media.on('change', (media) => {
|
||||
if (media) ctx.log.info(`Now playing ${media.path}`);
|
||||
});
|
||||
ctx.every(60_000, () => ctx.log.info('Still watching'));
|
||||
ctx.actions.register('hello', () => {
|
||||
const line = ctx.subtitles.current();
|
||||
ctx.ui.panel({ title: 'Hello', markdown: line ? `> ${line.text}` : '_No subtitle_' });
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { activate };
|
||||
```
|
||||
|
||||
An error thrown by a plugin is logged and does not crash SubMiner. A plugin that blocks with a long synchronous loop still freezes the app, so keep slow work asynchronous.
|
||||
|
||||
### Types and cleanup
|
||||
|
||||
The public types live in [`src/plugin-api.ts`](https://github.com/ksyasuda/SubMiner/blob/main/src/plugin-api.ts). A build emits `dist/plugin-api.d.ts`; copy that file into your plugin as `subminer.d.ts`, or copy the source file with that name. It has no imports from SubMiner internals. Electron's types must be available to your editor because `ui.openWindow` returns a `BrowserWindow`.
|
||||
|
||||
JavaScript plugins can use the types through JSDoc:
|
||||
|
||||
```js
|
||||
/** @param {import('./subminer').PluginContext} ctx */
|
||||
function activate(ctx) {
|
||||
const { ipcMain } = require('electron');
|
||||
ipcMain.handle(`${ctx.id}:info`, () => ({ media: ctx.media.current() }));
|
||||
ctx.onDispose(() => ipcMain.removeHandler(`${ctx.id}:info`));
|
||||
}
|
||||
|
||||
module.exports = { activate };
|
||||
```
|
||||
|
||||
Use `ctx.signal` with `fetch` or other cancellable work. It aborts when unloading starts, before `deactivate` runs. `ctx.onDispose(fn)` registers synchronous cleanup for resources outside `ctx`, such as file watchers and Electron IPC handlers. The returned function runs the cleanup early, at most once. Registering cleanup after unloading starts runs it immediately. Timers and listeners registered after that point are removed immediately too. Use the returned activation cleanup or `deactivate` for asynchronous teardown, within the shared quit timeout.
|
||||
|
||||
### `ctx` reference
|
||||
|
||||
| Member | What it does |
|
||||
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `id`, `dir`, `settings` | Plugin id, plugin folder path, and `plugins.<id>.settings` |
|
||||
| `apiVersion` | The host's plugin API version, currently `1` |
|
||||
| `signal`, `onDispose(fn)` | Lifetime cancellation and cleanup for resources created outside `ctx` |
|
||||
| `dataDir` | `plugin-data/<id>/` next to your config file, for caches and state. Exists before `activate` runs |
|
||||
| `log.debug/info/warn/error(message)` | Write to the app log |
|
||||
| `every(ms, fn)`, `after(ms, fn)` | Repeating and one-shot timers. Return a function that cancels them |
|
||||
| `mpv.on(event, fn)` | Listen to mpv events (below). Returns an unsubscribe function |
|
||||
| `mpv.command(args)` | Send an mpv command, e.g. `['set', 'pause', 'yes']` |
|
||||
| `mpv.request(args)` | Await an mpv command result; rejects on command error, disconnect, or timeout |
|
||||
| `mpv.getProperty(name)` | Read an mpv property (Promise) |
|
||||
| `mpv.connected` | Whether mpv is connected |
|
||||
| `mpv.ensureRunning()` | Connect to mpv, starting an idle mpv if none is running. Resolves `false` if that fails |
|
||||
| `media.current()` | Cached `{ playbackId, path, title }`, or `null` when there is no connected media |
|
||||
| `media.on('change', fn, options)` | Subscribe to playback snapshots; immediately delivers the current snapshot unless `emitCurrent: false` |
|
||||
| `anki.on(event, fn)` | Listen to Anki events (below). Returns an unsubscribe function |
|
||||
| `anki.invoke(action, params, options)` | Send an AnkiConnect action to `ankiConnect.url`, e.g. `('sync')`. Resolves its result (below) |
|
||||
| `subtitles.current()` | The displayed line as `{ text, startTime, endTime }`, or `null` |
|
||||
| `subtitles.context({ before, after })` | `{ current, before, after }`. Neighbors come from the loaded subtitle file, or recent lines for streams |
|
||||
| `actions.register(name, fn)` | Add an action, bindable as `__plugin-action:<id>/<name>` |
|
||||
| `actions.run(actionId, payload)` | Run a built-in action, the same ids keybindings use, e.g. `openJimaku` or `mineSentence` |
|
||||
| `notify(message, { variant })` | Overlay notification. `variant` is `info`, `warning`, or `error` |
|
||||
| `ui.panel({ title, markdown })` | Open the plugin panel. Returns `{ update(content), close(), closed, signal }` |
|
||||
| `ui.openWindow({ html, preload })` | Open a window from the plugin folder (below). Returns the Electron `BrowserWindow` |
|
||||
| `exec(command, args, options)` | Run a program without a shell (Windows `.cmd`/`.bat` shims go through `cmd.exe`). Resolves `{ exitCode, stdout, stderr }` |
|
||||
| `unstable` | SubMiner internals. These change between releases without notice |
|
||||
|
||||
A panel's `signal` aborts when the user closes it or another panel replaces it. Pass it to `exec` to stop work nobody will see. Closing or replacing a panel also cancels pending open requests, including while its window loads. Only one panel is open at a time. Panel markdown supports headings, lists, quotes, code, bold, and italics; HTML is shown as plain text.
|
||||
|
||||
`exec` options are `stdin`, `cwd`, `env`, `timeoutMs`, and `signal`. It resolves for any exit code and rejects when the program can't start, times out, or is aborted.
|
||||
|
||||
`ui.openWindow` takes `html` and an optional `preload`, both relative to the plugin folder, plus `title`, `width`, and `height`. Opening the same `html` again focuses the open window. The page runs with context isolation and no Node access. The preload is sandboxed too: it can `require('electron')` but nothing else, so bundle anything more into it. Expose what the page needs from the preload with `contextBridge`, and handle its messages with `ipcMain` from `require('electron')` in the plugin. Prefix channel names with the plugin id. Plugin windows close when SubMiner quits.
|
||||
|
||||
`media` snapshots are immutable. `playbackId` changes on a new load, including reopening the same path. A title may arrive later with the same id. Disconnecting or ending playback delivers `null`. Use the id to discard stale asynchronous results. Snapshots describe cached playback state; they do not promise that external subtitle discovery has finished.
|
||||
|
||||
`mpv.request` accepts string, number, and boolean arguments, e.g. `await ctx.mpv.request(['set_property', 'pause', true])`. It returns mpv's result data and rejects after 4 seconds. A timeout does not cancel the command in mpv, so a slow command may still complete. `mpv.command` remains available for commands whose result you do not need.
|
||||
|
||||
mpv events: `connection-change`, `subtitle-change`, `subtitle-timing`, `time-pos-change`, `duration-change`, `pause-change`, `fullscreen-change`, `media-path-change`, `media-title-change`, `subtitle-track-change`, `subtitle-track-list-change`, `secondary-subtitle-change`, `secondary-subtitle-track-change`, `secondary-subtitle-delay-change`, `secondary-subtitle-visibility`, `client-message`, `file-loaded`, and `end-file`. `file-loaded` has no payload. `end-file` gives `{ reason, playlistEntryId, error }`, where `reason` is `eof`, `stop`, `quit`, `error`, `redirect`, or `unknown`.
|
||||
|
||||
Anki events: `card-mined` gives `{ noteIds, notes }` once SubMiner has finished filling in a mined card (sentence, audio, image, and field grouping). For YouTube cards whose media waits on the download, it fires once that media is added or the download fails. With `ankiConnect.behavior.autoUpdateNewCards` off, it fires as soon as SubMiner sees the card. If field grouping merged the card into an older one, `noteIds` contains the older card's id.
|
||||
|
||||
Each `notes` entry has `{ originalNoteId, noteId, provenance }`. `originalNoteId` identifies the note before merges; `noteId` is the note that kept its content. `provenance` is `{ minedAt, media, subtitle }`, captured before asynchronous creation or the AnkiConnect proxy's upstream request. `minedAt` is Unix time in milliseconds. `media` uses the snapshot format above, and `subtitle` uses `{ text, startTime, endTime }`. Either may be `null`. Polling cannot establish the original source, so its provenance is `null` rather than the currently playing file.
|
||||
|
||||
`subtitles.current()` resolves parsed cues using the current subtitle delay and returns their times on the subtitle file's timeline.
|
||||
|
||||
Sentence-card provenance also uses the subtitle file's timeline. Multi-line mining converts the history range using the subtitle delay when mining starts; changing the delay between those lines can affect the reported range.
|
||||
|
||||
Several original notes can merge into one kept note. `notes` preserves each source, while `noteIds` contains unique kept ids. Existing listeners using only `noteIds` continue to work. Cards mined from the stats dashboard do not emit this event.
|
||||
|
||||
`anki.invoke` takes `{ timeoutMs }` (default 10 seconds), rejects on an Anki error, and never retries, so an action like `sync` or `addNote` runs at most once.
|
||||
@@ -6,6 +6,13 @@
|
||||
*/
|
||||
{
|
||||
|
||||
// ==========================================
|
||||
// Dictionary Backend
|
||||
// Select the dictionary lookup backend: yomitan or hachidori.
|
||||
// Restart SubMiner after changing the backend. Each backend keeps separate settings and dictionaries.
|
||||
// ==========================================
|
||||
"dictionaryBackend": "yomitan", // Dictionary lookup backend. Restart SubMiner after changing this setting. Values: yomitan | hachidori
|
||||
|
||||
// ==========================================
|
||||
// Subtitle Selection
|
||||
// Select primary and secondary mpv subtitle tracks from the overlay.
|
||||
@@ -101,6 +108,7 @@
|
||||
"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.
|
||||
"faceButtonLabels": "xbox", // Face button style of your controller: xbox (A at the bottom), nintendo (A on the right, B at the bottom; Switch-style and many 8BitDo pads) or playstation (╳ ○ □ △, laid out like xbox). Places the A/B/X/Y of the dialog layouts and names buttons in Alt+C and the dialog hints. Alt+C sets it per controller (controller.profiles.<id>.faceButtonLabels); this is the default for the others. Values: xbox | nintendo | playstation
|
||||
"repeatDelayMs": 320, // Delay before repeating held controller actions.
|
||||
"repeatIntervalMs": 120, // Repeat interval for held controller actions.
|
||||
"buttonIndices": {
|
||||
@@ -151,7 +159,31 @@
|
||||
"toggleMpvPause": {
|
||||
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
"buttonIndex": 9 // Raw button index captured for this discrete controller action.
|
||||
}, // Controller binding descriptor for toggling mpv play/pause. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
}, // Controller binding descriptor for toggling mpv play/pause. Works with or without keyboard-only mode. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"holdSecondarySub": {
|
||||
"kind": "none" // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
}, // Controller binding descriptor that shows the secondary subtitle only while held. Works with or without keyboard-only mode. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"previousEntry": {
|
||||
"kind": "none" // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
}, // Controller binding descriptor that selects the previous word entry in the open Yomitan popup. Mine Card mines the selected entry. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"nextEntry": {
|
||||
"kind": "none" // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
}, // Controller binding descriptor that selects the next word entry in the open Yomitan popup. Mine Card mines the selected entry. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"toggleCharacterNavigation": {
|
||||
"kind": "none" // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
}, // Controller binding descriptor that switches keyboard-only mode between moving by word and moving by character, like C. A lookup on a character searches from that character, which reaches words the subtitle split cuts in two. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"previousSubtitle": {
|
||||
"kind": "none" // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
}, // Controller binding descriptor that jumps to the previous subtitle line, like Shift+H. Works with or without keyboard-only mode. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"nextSubtitle": {
|
||||
"kind": "none" // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
}, // Controller binding descriptor that jumps to the next subtitle line, like Shift+L. Works with or without keyboard-only mode. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"replaySubtitle": {
|
||||
"kind": "none" // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
}, // Controller binding descriptor that replays the current subtitle line, then pauses, like Ctrl+Shift+H. Works with or without keyboard-only mode. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"playNextSubtitle": {
|
||||
"kind": "none" // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
}, // Controller binding descriptor that plays the next subtitle line, then pauses, like Ctrl+Shift+L. Works with or without keyboard-only mode. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"leftStickHorizontal": {
|
||||
"kind": "axis", // Analog binding input source kind. Values: none | axis
|
||||
"axisIndex": 0, // Raw axis index captured for this analog controller action.
|
||||
@@ -173,6 +205,288 @@
|
||||
"dpadFallback": "none" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
|
||||
} // Axis binding descriptor used for popup page jumps. Use Alt+C learn mode or set a raw axis descriptor manually.
|
||||
}, // Raw controller binding descriptors saved by Alt+C learn mode. For discrete axis bindings, kind "axis" requires axisIndex and direction.
|
||||
"reviewBindings": {
|
||||
"confirm": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "a" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Use this timing.
|
||||
"cancel": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "b" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Cancel.
|
||||
"play": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "x" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Play selection.
|
||||
"resetTiming": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "y" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Reset timing.
|
||||
"showEarlier": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "left" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Earlier −2s. Repeats while held.
|
||||
"showLater": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "right" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Later +2s. Repeats while held.
|
||||
"framePrevious": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "up" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Previous screenshot frame. Repeats while held.
|
||||
"frameNext": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "down" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Next screenshot frame. Repeats while held.
|
||||
"frameReset": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "button", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"buttonIndex": 5 // Raw button index.
|
||||
}, // Input that triggers the action.
|
||||
"hold": {
|
||||
"kind": "button", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"buttonIndex": 4 // Raw button index.
|
||||
} // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Reset screenshot frame.
|
||||
"startBack": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "left" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": {
|
||||
"kind": "button", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"buttonIndex": 4 // Raw button index.
|
||||
} // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Start −0.1s. Repeats while held.
|
||||
"startForward": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "right" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": {
|
||||
"kind": "button", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"buttonIndex": 4 // Raw button index.
|
||||
} // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Start +0.1s. Repeats while held.
|
||||
"previousLineAdd": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "up" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": {
|
||||
"kind": "button", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"buttonIndex": 4 // Raw button index.
|
||||
} // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Add previous line. Repeats while held.
|
||||
"previousLineRemove": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "down" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": {
|
||||
"kind": "button", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"buttonIndex": 4 // Raw button index.
|
||||
} // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Remove previous line. Repeats while held.
|
||||
"endBack": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "left" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": {
|
||||
"kind": "button", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"buttonIndex": 5 // Raw button index.
|
||||
} // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: End −0.1s. Repeats while held.
|
||||
"endForward": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "right" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": {
|
||||
"kind": "button", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"buttonIndex": 5 // Raw button index.
|
||||
} // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: End +0.1s. Repeats while held.
|
||||
"nextLineAdd": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "up" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": {
|
||||
"kind": "button", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"buttonIndex": 5 // Raw button index.
|
||||
} // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Add next line. Repeats while held.
|
||||
"nextLineRemove": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "down" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": {
|
||||
"kind": "button", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"buttonIndex": 5 // Raw button index.
|
||||
} // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Remove next line. Repeats while held.
|
||||
"useOriginal": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "a" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Use original timing (after cancel).
|
||||
"keepEditing": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "b" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Keep editing (after cancel).
|
||||
"skipMedia": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "x" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Review dialog: Without media (after cancel).
|
||||
"discard": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "y" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
} // Review dialog: Discard card (after cancel).
|
||||
}, // Controller layout for the media timing review dialog, one entry per action. Set it with the Review media timing group in Alt+C. Each entry is {"kind": "none"} or {"press": input, "hold": input}, where an input is {"kind": "button", "buttonIndex"}, {"kind": "axis", "axisIndex", "direction"}, {"kind": "face", "letter": "a"|"b"|"x"|"y"} (printed letter) or {"kind": "dpad", "direction": "left"|"right"|"up"|"down"}.
|
||||
"fieldGroupingBindings": {
|
||||
"selectOriginal": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "left" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Duplicate card dialog: Original card.
|
||||
"selectNew": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "right" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Duplicate card dialog: New card.
|
||||
"toggleDeleteDuplicate": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "x" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Duplicate card dialog: Delete duplicate on/off.
|
||||
"continue": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "a" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Duplicate card dialog: Continue.
|
||||
"cancel": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "b" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Duplicate card dialog: Cancel.
|
||||
"confirmMerge": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "a" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Duplicate card dialog: Confirm merge (merge preview).
|
||||
"back": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "b" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Duplicate card dialog: Back (merge preview).
|
||||
"cancelMerge": {
|
||||
"kind": "none" // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
}, // Duplicate card dialog: Cancel (merge preview).
|
||||
"toggleView": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "face", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"letter": "y" // Printed face letter, placed by controller.faceButtonLabels. Values: a | b | x | y
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Duplicate card dialog: Compact / Full (merge preview).
|
||||
"scrollUp": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "up" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
}, // Duplicate card dialog: Scroll up (merge preview). Repeats while held.
|
||||
"scrollDown": {
|
||||
"kind": "combo", // none disables the action; combo presses "press" (while holding "hold", if set). Values: none | combo
|
||||
"press": {
|
||||
"kind": "dpad", // Input kind: a raw button or axis recorded by Alt+C, a printed face letter, or a D-pad direction. Values: button | axis | face | dpad
|
||||
"direction": "down" // D-pad direction, or axis direction for raw axis inputs. Values: left | right | up | down | negative | positive
|
||||
}, // Input that triggers the action.
|
||||
"hold": null // Optional input held while pressing; null for none.
|
||||
} // Duplicate card dialog: Scroll down (merge preview). Repeats while held.
|
||||
}, // Controller layout for the duplicate card (field grouping) dialog, one entry per action. Set it with the Duplicate card (field grouping) group in Alt+C. Each entry is {"kind": "none"} or {"press": input, "hold": input}, where an input is {"kind": "button", "buttonIndex"}, {"kind": "axis", "axisIndex", "direction"}, {"kind": "face", "letter": "a"|"b"|"x"|"y"} (printed letter) or {"kind": "dpad", "direction": "left"|"right"|"up"|"down"}.
|
||||
"profiles": {} // Per-controller binding and button-index overrides keyed by the controller id reported by the Gamepad API.
|
||||
}, // Gamepad support for the visible overlay while keyboard-only mode is active.
|
||||
|
||||
@@ -211,6 +525,14 @@
|
||||
"overlayPosition": "top-right" // Position for in-overlay notification cards. Values: top-left | top | top-right
|
||||
}, // Overlay notification display behavior.
|
||||
|
||||
// ==========================================
|
||||
// Plugins
|
||||
// JavaScript plugins loaded from <config dir>/plugins/<id>/ at startup.
|
||||
// Plugins run inside SubMiner with full access to your system. Only enable code you trust.
|
||||
// Restart SubMiner after changing plugins.
|
||||
// ==========================================
|
||||
"plugins": {}, // Plugins to load at startup, keyed by directory name under <config dir>/plugins/. Each entry is { "enabled": true, "settings": { ... } }; settings are passed to the plugin as ctx.settings.
|
||||
|
||||
// ==========================================
|
||||
// Keyboard Shortcuts
|
||||
// Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||
@@ -455,6 +777,7 @@
|
||||
"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": true, // Automatically pause mpv playback while Yomitan popup is open, then resume when popup closes. Values: true | false
|
||||
"autoPauseVideoOnKeyboardMode": false, // Pause mpv when keyboard-only mode turns on (from the keyboard or a controller) and resume when it turns off. Leaving the mode resumes only a pause it made: a video that was already paused, or that you played or paused yourself meanwhile, is left as it is. Values: true | false
|
||||
"primaryVisibleOnYomitanPopup": true, // Keep the primary subtitle bar visible while a Yomitan popup is open when primary subtitles are in hover mode. Values: true | false
|
||||
"nameMatchEnabled": false, // Enable character dictionary sync and subtitle token coloring for character-name matches. Values: true | false
|
||||
"nameMatchImagesEnabled": false, // Show small character portraits beside subtitle tokens matched from the SubMiner character dictionary. Values: true | false
|
||||
@@ -534,6 +857,22 @@
|
||||
} // CSS declaration object applied to the subtitle sidebar. Includes color, background-color, and all font properties.
|
||||
}, // Parsed-subtitle sidebar cue list styling, behavior, and toggle key.
|
||||
|
||||
// ==========================================
|
||||
// Subtitle Filters
|
||||
// Remove unwanted text from primary subtitles, such as speaker names, readings and sound descriptions.
|
||||
// Applies to the overlay, sidebar, mined sentences and stats.
|
||||
// Hot-reload: filter changes apply to the current subtitle file immediately.
|
||||
// ==========================================
|
||||
"subtitleFilter": {
|
||||
"presets": {
|
||||
"parentheses": false, // Remove text in () and (), such as speaker names, readings and notes. A line wrapped entirely in parentheses, usually inner thoughts, is kept. Values: true | false
|
||||
"brackets": false, // Remove text in [], [], 【】 and 〔〕, such as sound effects and music cues. Values: true | false
|
||||
"speakerPrefix": false, // Remove a short name followed by a colon at the start of a line, as in 山田:元気? Values: true | false
|
||||
"musicSymbols": false // Remove ♪, ♫ and ♬. A line left with only symbols such as ~ is removed. Values: true | false
|
||||
}, // Built-in filters. Each removes its matches and is off by default.
|
||||
"customRules": [] // Your own regex rules, applied in order after the presets. Each rule is { name, pattern, replacement, enabled }; patterns use the gmu flags, so ^ and $ match per line. Leave replacement empty to remove the match.
|
||||
}, // Remove unwanted text from primary subtitles, such as speaker names, readings and sound descriptions.
|
||||
|
||||
// ==========================================
|
||||
// Shared AI Provider
|
||||
// Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
|
||||
@@ -571,7 +910,7 @@
|
||||
"deck": "", // Restrict duplicate detection and card enrichment to this Anki deck. Leave empty to use the Yomitan mining deck when available.
|
||||
"fields": {
|
||||
"word": "Expression", // Card field for the mined word or expression text.
|
||||
"audio": "ExpressionAudio", // Card field that receives generated sentence audio.
|
||||
"audio": "SentenceAudio", // Card field that receives generated sentence audio.
|
||||
"wordAudio": "ExpressionAudio", // Existing word-audio field read to time the frozen first frame of animated images. This mapping is only used for synchronization.
|
||||
"image": "Picture", // Card field that receives the captured screenshot or animated image.
|
||||
"sentence": "Sentence", // Card field that receives the source sentence text.
|
||||
@@ -656,7 +995,8 @@
|
||||
"apiKey": "", // Jimaku API key. Optional but recommended for higher rate limits. Get one for free at https://jimaku.cc.
|
||||
"apiKeyCommand": "", // Shell command that prints the Jimaku API key to stdout. Used instead of apiKey to avoid storing the key in plain text.
|
||||
"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.
|
||||
"autoOpenWhenPrimarySubtitleMissing": false // Pause playback and open the Jimaku modal when a local video has no subtitle track in youtube.primarySubLanguages (Japanese by default). Values: true | false
|
||||
}, // Jimaku API configuration and defaults.
|
||||
|
||||
// ==========================================
|
||||
@@ -734,10 +1074,10 @@
|
||||
// autoStartSubMiner starts SubMiner in the background; auto_start_overlay only controls visible overlay display.
|
||||
// Set mpv.launchMode to choose normal, maximized, or fullscreen SubMiner-managed mpv playback.
|
||||
// Set mpv.profile to pass an mpv profile to managed mpv launches; leave it blank to pass none.
|
||||
// Leave mpv.executablePath blank to auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.
|
||||
// Leave mpv.executablePath blank to auto-discover mpv from SUBMINER_MPV_PATH, PATH, or (macOS) Homebrew and /Applications/mpv.app.
|
||||
// ==========================================
|
||||
"mpv": {
|
||||
"executablePath": "", // Optional absolute path to mpv.exe for Windows launch flows. Leave empty to auto-discover from SUBMINER_MPV_PATH or PATH.
|
||||
"executablePath": "", // Optional absolute path to the mpv binary SubMiner starts itself (Windows launches, Jellyfin casting, timing preview). Leave empty to auto-discover from SUBMINER_MPV_PATH, PATH, and on macOS the Homebrew and /Applications/mpv.app locations.
|
||||
"launchMode": "normal", // Default window state for SubMiner-managed mpv launches. Values: normal | maximized | fullscreen
|
||||
"profile": "", // Optional mpv profile name passed to SubMiner-managed mpv launches. Leave empty to pass no profile.
|
||||
"socketPath": "/tmp/subminer-socket", // mpv IPC socket path used by SubMiner-managed playback and the bundled mpv plugin.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 110 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 614 KiB After Width: | Height: | Size: 1.9 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 2.2 MiB |
+19
-16
@@ -6,12 +6,12 @@ Shortcuts work when the overlay has focus. With the [mpv plugin](/mpv-plugin), `
|
||||
|
||||
## Global
|
||||
|
||||
| Shortcut | Action | Config key |
|
||||
| ------------- | ---------------------- | -------------------------------------- |
|
||||
| `Alt+Shift+O` | Toggle visible overlay | `shortcuts.toggleVisibleOverlayGlobal` |
|
||||
| `Alt+Shift+Y` | Open Yomitan settings | Fixed |
|
||||
| Shortcut | Action | Config key |
|
||||
| ------------- | ------------------------------- | -------------------------------------- |
|
||||
| `Alt+Shift+O` | Toggle visible overlay | `shortcuts.toggleVisibleOverlayGlobal` |
|
||||
| `Alt+Shift+Y` | Open active dictionary settings | Fixed |
|
||||
|
||||
`Alt+Shift+Y` is registered with the OS and works from any app. If another app already uses it, SubMiner cannot take it and you cannot rebind it.
|
||||
`Alt+Shift+Y` opens Yomitan or Hachidori settings, whichever backend is running. It is registered with the OS and works from any app. If another app already uses it, SubMiner cannot take it and you cannot rebind it.
|
||||
|
||||
## Mining
|
||||
|
||||
@@ -62,6 +62,7 @@ Built into the overlay, not configurable:
|
||||
| Input | Action |
|
||||
| ------------------------- | -------------------------------------------------- |
|
||||
| `V` | Cycle primary subtitle bar: hidden, visible, hover |
|
||||
| `C` (keyboard-only mode) | Move by word or by character |
|
||||
| Right-click | Pause or resume (outside the subtitle area) |
|
||||
| Right-click + drag | Move the subtitles |
|
||||
| Drop files on the overlay | Replace the mpv playlist |
|
||||
@@ -98,17 +99,17 @@ The subtitle picker (`g` then `s`) is off until you turn it on in **Settings, Be
|
||||
|
||||
Press `y`, then the second key.
|
||||
|
||||
| Keys | Action |
|
||||
| ----- | -------------------------- |
|
||||
| `y-y` | Open the SubMiner menu |
|
||||
| `y-s` | Start the overlay |
|
||||
| `y-S` | Stop the overlay |
|
||||
| `y-t` | Toggle the visible overlay |
|
||||
| `y-o` | Open Yomitan settings |
|
||||
| `y-r` | Restart the overlay |
|
||||
| `y-c` | Show overlay status |
|
||||
| `y-h` | Open session help |
|
||||
| `v` | Cycle primary subtitle bar |
|
||||
| Keys | Action |
|
||||
| ----- | ------------------------------- |
|
||||
| `y-y` | Open the SubMiner menu |
|
||||
| `y-s` | Start the overlay |
|
||||
| `y-S` | Stop the overlay |
|
||||
| `y-t` | Toggle the visible overlay |
|
||||
| `y-o` | Open active dictionary settings |
|
||||
| `y-r` | Restart the overlay |
|
||||
| `y-c` | Show overlay status |
|
||||
| `y-h` | Open session help |
|
||||
| `v` | Cycle primary subtitle bar |
|
||||
|
||||
The plugin's `v` replaces mpv's own subtitle visibility toggle. When the overlay has focus, `y` then `d` toggles DevTools.
|
||||
|
||||
@@ -138,6 +139,8 @@ The plugin's `v` replaces mpv's own subtitle visibility toggle. When the overlay
|
||||
}
|
||||
```
|
||||
|
||||
To run a [plugin](/plugins) action, use `["__plugin-action:<plugin id>/<action>"]` as the command.
|
||||
|
||||
Mouse button names are `MBTN_LEFT`, `MBTN_MID`, `MBTN_RIGHT`, `MBTN_BACK`, and `MBTN_FORWARD`. Scroll wheel names are `WHEEL_UP`, `WHEEL_DOWN`, `WHEEL_LEFT`, and `WHEEL_RIGHT`. See [keybindings](/configuration#keybindings) and [shortcuts configuration](/configuration#shortcuts-configuration) in the config reference.
|
||||
|
||||
## Automatic mpv bindings
|
||||
|
||||
@@ -16,7 +16,7 @@ The modal's **Local tools** section lists anything missing. After you install a
|
||||
|
||||
## Generating from the overlay
|
||||
|
||||
1. Open a local video in mpv and select its Japanese audio track, or play a YouTube video.
|
||||
1. Open a local video in mpv and select its Japanese audio track, or play a Jellyfin or YouTube video.
|
||||
2. Press `Ctrl+Shift+G`. If the subtitle sidebar is empty, its **Generate Japanese subtitles** button opens the same modal.
|
||||
3. Pick a model and download it if needed.
|
||||
4. Optionally check **Focus on spoken dialogue** (see below).
|
||||
@@ -26,6 +26,8 @@ The modal shows progress. **Cancel** stops the job. Closing the modal lets the j
|
||||
|
||||
SubMiner saves `<video>.ja.generated.srt` next to the video and adds a number if that name is taken. If the same file is still playing, it loads the subtitles and resets the subtitle delay.
|
||||
|
||||
For a Jellyfin video, SubMiner reads the original file from your server and saves the subtitles to `~/.cache/subminer/generated-subs/`, since a stream has no folder of its own. Each Jellyfin generation replaces the previous file there. Playback keeps going while it works. If Jellyfin is transcoding, SubMiner transcribes the audio track that playback started with.
|
||||
|
||||
For a YouTube video, SubMiner downloads its audio first and pauses the video until the subtitles load. The subtitles are temporary, and the audio is deleted when generation ends or you switch videos. See [YouTube integration](/youtube-integration#generate-subtitles-with-whisper).
|
||||
|
||||
Change the shortcut with `shortcuts.openSubtitleGeneration`.
|
||||
@@ -46,7 +48,7 @@ subminer generate-subs episode.mkv --model-path /path/to/ggml-small.bin
|
||||
| `--audio-stream <index>` | Pick an audio stream by its absolute FFmpeg index |
|
||||
| `--output <path>` | Write to this SRT path. Existing files are never overwritten. |
|
||||
|
||||
With a file argument, SubMiner uses the audio stream tagged Japanese, or the first stream. `Ctrl+C` cancels.
|
||||
With a file argument, SubMiner uses the audio stream tagged Japanese, or the first stream. `Ctrl+C` cancels. The launcher only works with local files. Use the overlay for Jellyfin and YouTube.
|
||||
|
||||
## Choosing a model
|
||||
|
||||
|
||||
@@ -39,11 +39,18 @@ SubMiner reads subtitles from mpv over an IPC socket (a named pipe on Windows).
|
||||
|
||||
SubMiner reconnects on its own if mpv restarts.
|
||||
|
||||
## Casting from Jellyfin does nothing
|
||||
|
||||
When `connect ENOENT /tmp/subminer-socket` follows a cast in the app log, SubMiner could not start mpv, so the socket never appeared. Look for `Failed handling Jellyfin remote Play event` or `Failed to launch mpv for Jellyfin remote playback` just before it.
|
||||
|
||||
- Check that `mpv --version` works in a terminal.
|
||||
- If it does, SubMiner is not finding mpv where it is installed. Set `mpv.executablePath` to the output of `command -v mpv`. See [Casting from Jellyfin](/jellyfin-integration#casting-from-jellyfin) for where SubMiner looks.
|
||||
|
||||
## Overlay does not appear
|
||||
|
||||
- Confirm SubMiner is running (`SubMiner.AppImage --start`, or check for the process).
|
||||
- Linux: Hyprland and Sway work natively. Any other compositor needs mpv and SubMiner under X11 or Xwayland, with `xdotool`, `xprop`, and `xwininfo` installed. See [KDE Plasma and other Wayland compositors](#kde-plasma-and-other-wayland-compositors).
|
||||
- macOS: grant Accessibility permission in System Settings > Privacy & Security > Accessibility.
|
||||
- macOS: grant Accessibility permission. See [macOS](#macos).
|
||||
|
||||
## Overlay is on the wrong monitor or position
|
||||
|
||||
@@ -79,7 +86,7 @@ SubMiner detected that a previously non-empty Yomitan profile suddenly appears e
|
||||
|
||||
## "Yomitan extension not found in any search path"
|
||||
|
||||
The bundled Yomitan is missing. Re-download the AppImage, or place an unpacked Yomitan extension in `~/.config/SubMiner/yomitan`. Source builds must run `bun run build` first to produce `build/yomitan`.
|
||||
The bundled Yomitan is missing. Re-download the AppImage, or place an unpacked Yomitan extension in `~/.config/SubMiner/yomitan`. Source builds must run `bun run build` first to produce `dist/yomitan`.
|
||||
|
||||
## "MeCab not found on system"
|
||||
|
||||
@@ -255,5 +262,5 @@ mpv --gpu-context=x11vk,x11egl,x11 video.mkv
|
||||
|
||||
## macOS
|
||||
|
||||
- Accessibility permission is required for window tracking: System Settings > Privacy & Security > Accessibility.
|
||||
- Grant Accessibility permission under System Settings > Privacy & Security > Accessibility (named **Device Control and Data Access** on macOS 27). Without it, SubMiner falls back to a basic window lookup that cannot tell when mpv is minimized.
|
||||
- If Gatekeeper blocks the app, right-click it and choose Open, or run `xattr -d com.apple.quarantine /path/to/SubMiner.app`.
|
||||
Loaded 100 of 1035 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user