mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-12 04:19:25 -07:00
Compare commits
32 Commits
v0.4.0
...
5d96f9d535
| Author | SHA1 | Date | |
|---|---|---|---|
|
5d96f9d535
|
|||
|
1d76e05cd3
|
|||
|
3dff6c2515
|
|||
|
755c1175b0
|
|||
|
78cd99a2d0
|
|||
|
1fd3f0575b
|
|||
|
ca0eec568c
|
|||
|
94abd0f372
|
|||
|
4d60f64bea
|
|||
|
dbd6803623
|
|||
|
5ff4cc21bd
|
|||
|
82bec02a36
|
|||
|
c548044c61
|
|||
|
39976c03f9
|
|||
|
e659b5d8f4
|
|||
|
85bd6c6ec2
|
|||
|
6fe6976dc9
|
|||
|
e6150e9513
|
|||
|
40521e769d
|
|||
|
2f31227471
|
|||
|
69fd69c0b2
|
|||
|
746696b1a4
|
|||
|
ebe9515486
|
|||
|
8c2c950564
|
|||
|
e2b51c6306
|
|||
|
f160ca6af8
|
|||
|
289486a5b1
|
|||
|
ac4fd60098
|
|||
|
72b18110b5
|
|||
|
c791887d5c
|
|||
|
8570b262e4
|
|||
|
33ded3c1bf
|
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -31,8 +31,7 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
~/.bun/install/cache
|
~/.bun/install/cache
|
||||||
node_modules
|
node_modules
|
||||||
vendor/subminer-yomitan/node_modules
|
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
|
||||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
|
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-bun-
|
${{ runner.os }}-bun-
|
||||||
|
|
||||||
|
|||||||
25
.github/workflows/release.yml
vendored
25
.github/workflows/release.yml
vendored
@@ -31,23 +31,22 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 22.12.0
|
node-version: 22.12.0
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
- name: Cache dependencies
|
- name: Cache dependencies
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.bun/install/cache
|
~/.bun/install/cache
|
||||||
node_modules
|
node_modules
|
||||||
vendor/subminer-yomitan/node_modules
|
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
|
||||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
|
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-bun-
|
${{ runner.os }}-bun-
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build (TypeScript check)
|
|
||||||
run: bun run typecheck
|
|
||||||
|
|
||||||
- name: Test suite (source)
|
- name: Test suite (source)
|
||||||
run: bun run test:fast
|
run: bun run test:fast
|
||||||
|
|
||||||
@@ -85,11 +84,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
bun-version: 1.3.5
|
bun-version: 1.3.5
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22.12.0
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
- name: Cache dependencies
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -97,8 +91,7 @@ jobs:
|
|||||||
~/.bun/install/cache
|
~/.bun/install/cache
|
||||||
node_modules
|
node_modules
|
||||||
vendor/texthooker-ui/node_modules
|
vendor/texthooker-ui/node_modules
|
||||||
vendor/subminer-yomitan/node_modules
|
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'vendor/texthooker-ui/package.json') }}
|
||||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
|
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-bun-
|
${{ runner.os }}-bun-
|
||||||
|
|
||||||
@@ -147,11 +140,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
bun-version: 1.3.5
|
bun-version: 1.3.5
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22.12.0
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
- name: Cache dependencies
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -159,8 +147,7 @@ jobs:
|
|||||||
~/.bun/install/cache
|
~/.bun/install/cache
|
||||||
node_modules
|
node_modules
|
||||||
vendor/texthooker-ui/node_modules
|
vendor/texthooker-ui/node_modules
|
||||||
vendor/subminer-yomitan/node_modules
|
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'vendor/texthooker-ui/package.json') }}
|
||||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
|
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-bun-
|
${{ runner.os }}-bun-
|
||||||
|
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,7 +5,6 @@ node_modules/
|
|||||||
out/
|
out/
|
||||||
dist/
|
dist/
|
||||||
release/
|
release/
|
||||||
build/yomitan/
|
|
||||||
|
|
||||||
# Launcher build artifact (produced by make build-launcher)
|
# Launcher build artifact (produced by make build-launcher)
|
||||||
/subminer
|
/subminer
|
||||||
@@ -37,4 +36,3 @@ tests/*
|
|||||||
.worktrees/
|
.worktrees/
|
||||||
.codex/*
|
.codex/*
|
||||||
.agents/*
|
.agents/*
|
||||||
docs/*
|
|
||||||
|
|||||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -5,6 +5,6 @@
|
|||||||
[submodule "vendor/yomitan-jlpt-vocab"]
|
[submodule "vendor/yomitan-jlpt-vocab"]
|
||||||
path = vendor/yomitan-jlpt-vocab
|
path = vendor/yomitan-jlpt-vocab
|
||||||
url = https://github.com/stephenmk/yomitan-jlpt-vocab
|
url = https://github.com/stephenmk/yomitan-jlpt-vocab
|
||||||
[submodule "vendor/subminer-yomitan"]
|
[submodule "yomitan-jlpt-vocab"]
|
||||||
path = vendor/subminer-yomitan
|
path = vendor/yomitan-jlpt-vocab
|
||||||
url = https://github.com/ksyasuda/subminer-yomitan
|
url = https://github.com/stephenmk/yomitan-jlpt-vocab
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -98,7 +98,7 @@ ensure-bun:
|
|||||||
@command -v bun >/dev/null 2>&1 || { printf '%s\n' "[ERROR] bun not found"; exit 1; }
|
@command -v bun >/dev/null 2>&1 || { printf '%s\n' "[ERROR] bun not found"; exit 1; }
|
||||||
|
|
||||||
pretty: ensure-bun
|
pretty: ensure-bun
|
||||||
@bun run format:src
|
@bun run format
|
||||||
|
|
||||||
build:
|
build:
|
||||||
@printf '%s\n' "[INFO] Detected platform: $(PLATFORM)"
|
@printf '%s\n' "[INFO] Detected platform: $(PLATFORM)"
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ chmod +x ~/.local/bin/subminer
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The `subminer` wrapper uses a [Bun](https://bun.sh) shebang. Make sure `bun` is on your `PATH`.
|
> The `subminer` wrapper uses a [Bun](https://bun.sh) shebang. Make sure `bun` is on your `PATH`.
|
||||||
|
|
||||||
**From source** or **macOS** — initialize submodules first (`git submodule update --init --recursive`). Source builds now also require Node.js 22 + npm because bundled Yomitan is built from the `vendor/subminer-yomitan` submodule into `build/yomitan` during `bun run build`. Full install guide: [docs.subminer.moe/installation#from-source](https://docs.subminer.moe/installation#from-source).
|
**From source** or **macOS** — see the [installation guide](https://docs.subminer.moe/installation#from-source).
|
||||||
|
|
||||||
### 2. Launch the app once
|
### 2. Launch the app once
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ subminer --start video.mkv # optional explicit overlay start when plugin auto_st
|
|||||||
|
|
||||||
| Required | Optional |
|
| Required | Optional |
|
||||||
| ------------------------------------------ | -------------------------------------------------- |
|
| ------------------------------------------ | -------------------------------------------------- |
|
||||||
| `bun`, `node` 22, `npm` | |
|
| `bun` | |
|
||||||
| `mpv` with IPC socket | `yt-dlp` |
|
| `mpv` with IPC socket | `yt-dlp` |
|
||||||
| `ffmpeg` | `guessit` (better AniSkip title/episode detection) |
|
| `ffmpeg` | `guessit` (better AniSkip title/episode detection) |
|
||||||
| `mecab` + `mecab-ipadic` | `fzf` / `rofi` |
|
| `mecab` + `mecab-ipadic` | `fzf` / `rofi` |
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ title: Index AniList character alternative names in the character dictionary
|
|||||||
status: Done
|
status: Done
|
||||||
assignee: []
|
assignee: []
|
||||||
created_date: '2026-03-07 00:00'
|
created_date: '2026-03-07 00:00'
|
||||||
updated_date: '2026-03-08 00:11'
|
updated_date: '2026-03-07 00:00'
|
||||||
labels:
|
labels:
|
||||||
- dictionary
|
- dictionary
|
||||||
- anilist
|
- anilist
|
||||||
|
priority: high
|
||||||
dependencies: []
|
dependencies: []
|
||||||
references:
|
references:
|
||||||
- src/main/character-dictionary-runtime.ts
|
- /home/sudacode/projects/japanese/SubMiner/src/main/character-dictionary-runtime.ts
|
||||||
- src/main/character-dictionary-runtime.test.ts
|
- /home/sudacode/projects/japanese/SubMiner/src/main/character-dictionary-runtime.test.ts
|
||||||
priority: high
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-110
|
|
||||||
title: Replace vendored Yomitan with submodule-built Chrome artifact workflow
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-03-07 11:05'
|
|
||||||
updated_date: '2026-03-07 11:22'
|
|
||||||
labels:
|
|
||||||
- yomitan
|
|
||||||
- build
|
|
||||||
- release
|
|
||||||
dependencies: []
|
|
||||||
priority: high
|
|
||||||
ordinal: 9010
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
Replace the checked-in `vendor/yomitan` release tree with a `subminer-yomitan` git submodule. Build Yomitan from source, extract the Chromium zip artifact into a stable local build directory, and make SubMiner dev/runtime/tests/release packaging load that extracted extension instead of the source tree or vendored files.
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
<!-- AC:BEGIN -->
|
|
||||||
|
|
||||||
- [x] #1 Repo tracks Yomitan as a git submodule instead of committed extension files under `vendor/yomitan`.
|
|
||||||
- [x] #2 SubMiner has a reproducible build/extract step that produces a local Chromium extension directory from `subminer-yomitan`.
|
|
||||||
- [x] #3 Dev/runtime/tests resolve the extracted build output as the default Yomitan extension path.
|
|
||||||
- [x] #4 Release packaging includes the extracted Chromium extension files instead of the old vendored tree.
|
|
||||||
- [x] #5 Docs and verification commands reflect the new workflow.
|
|
||||||
|
|
||||||
<!-- AC:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
Replaced the checked-in `vendor/yomitan` extension tree with a `vendor/subminer-yomitan` git submodule and added a reproducible `bun run build:yomitan` workflow that builds `yomitan-chrome.zip`, extracts it into `build/yomitan`, and reuses a source-state stamp to skip redundant rebuilds. Runtime path resolution, helper CLIs, Yomitan integration tests, packaging, CI cache keys, and README source-build notes now all target that generated artifact instead of the old vendored files.
|
|
||||||
|
|
||||||
Verification:
|
|
||||||
|
|
||||||
- `bun run build:yomitan`
|
|
||||||
- `bun test src/core/services/yomitan-extension-paths.test.ts src/core/services/yomitan-structured-content-generator.test.ts src/yomitan-translator-sort.test.ts`
|
|
||||||
- `bun run typecheck`
|
|
||||||
- `bun run build`
|
|
||||||
- `bun run test:core:src`
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-111
|
|
||||||
title: Fix subtitle-cycle OSD labels for J keybindings
|
|
||||||
status: Done
|
|
||||||
assignee:
|
|
||||||
- Codex
|
|
||||||
created_date: '2026-03-07 23:45'
|
|
||||||
updated_date: '2026-03-08 00:06'
|
|
||||||
labels: []
|
|
||||||
dependencies: []
|
|
||||||
references:
|
|
||||||
- /Users/sudacode/projects/japanese/SubMiner/src/core/services/ipc-command.ts
|
|
||||||
- /Users/sudacode/projects/japanese/SubMiner/src/core/services/mpv.ts
|
|
||||||
- >-
|
|
||||||
/Users/sudacode/projects/japanese/SubMiner/src/core/services/ipc-command.test.ts
|
|
||||||
- >-
|
|
||||||
/Users/sudacode/projects/japanese/SubMiner/src/core/services/mpv-control.test.ts
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
When cycling subtitle tracks with the default J/Shift+J keybindings, the mpv OSD currently shows raw template text like `${sid}` instead of a resolved subtitle label. Update the keybinding OSD behavior so users see the active subtitle selection clearly when cycling tracks, and ensure placeholder-based OSD messages sent through the mpv client API render correctly.
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
<!-- AC:BEGIN -->
|
|
||||||
- [x] #1 Pressing the primary subtitle cycle keybinding shows a resolved subtitle label on the OSD instead of a raw `${sid}` placeholder.
|
|
||||||
- [x] #2 Pressing the secondary subtitle cycle keybinding shows a resolved subtitle label on the OSD instead of a raw `${secondary-sid}` placeholder.
|
|
||||||
- [x] #3 Proxy OSD messages that rely on mpv property expansion render resolved values when sent through the mpv client API.
|
|
||||||
- [x] #4 Regression tests cover the subtitle-cycle OSD behavior and the placeholder-expansion OSD path.
|
|
||||||
<!-- AC:END -->
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
<!-- SECTION:PLAN:BEGIN -->
|
|
||||||
1. Add focused failing tests for subtitle-cycle OSD labels and mpv placeholder-expansion behavior.
|
|
||||||
2. Update the IPC mpv command handler to resolve primary and secondary subtitle track labels from mpv `track-list` data after cycling subtitle tracks.
|
|
||||||
3. Update the mpv OSD runtime path so placeholder-based `show-text` messages sent through the client API opt into property expansion.
|
|
||||||
4. Run focused tests, then the relevant core test lane, and record results in the task notes.
|
|
||||||
<!-- SECTION:PLAN:END -->
|
|
||||||
|
|
||||||
## Implementation Notes
|
|
||||||
|
|
||||||
<!-- SECTION:NOTES:BEGIN -->
|
|
||||||
Initial triage: `ipc-command.ts` emits raw `${sid}`/`${secondary-sid}` placeholder strings, and `showMpvOsdRuntime` sends `show-text` via mpv client API without enabling property expansion.
|
|
||||||
|
|
||||||
User approved implementation plan on 2026-03-07.
|
|
||||||
|
|
||||||
Implementation: proxy mpv command OSD now supports an async resolver so subtitle track cycling can show human-readable labels instead of raw `${sid}` placeholders.
|
|
||||||
|
|
||||||
Implementation: `showMpvOsdRuntime` now prefixes placeholder-based messages with mpv client-api `expand-properties`, which fixes raw `${...}` OSD output for subtitle delay/position messages.
|
|
||||||
|
|
||||||
Testing: `bun test src/core/services/ipc-command.test.ts src/core/services/mpv-control.test.ts src/main/runtime/mpv-proxy-osd.test.ts src/main/runtime/ipc-mpv-command-main-deps.test.ts src/main/runtime/ipc-bridge-actions.test.ts src/main/runtime/ipc-bridge-actions-main-deps.test.ts src/main/runtime/composers/ipc-runtime-composer.test.ts` passed.
|
|
||||||
|
|
||||||
Testing: `bun x tsc --noEmit` passed.
|
|
||||||
|
|
||||||
Testing: `bun run test:core:src` passed (423 pass, 6 skip, 0 fail).
|
|
||||||
|
|
||||||
Docs: no update required because no checked-in docs or help text describe the J/Shift+J OSD output behavior.
|
|
||||||
<!-- SECTION:NOTES:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
Fixed subtitle-cycle OSD handling for the default J/Shift+J keybindings. The IPC mpv command path now supports resolving proxy OSD text asynchronously, and the main-runtime resolver reads mpv `track-list` state so primary and secondary subtitle cycling show human-readable track labels instead of raw `${sid}` / `${secondary-sid}` placeholders.
|
|
||||||
|
|
||||||
Also fixed the lower-level mpv OSD transport so placeholder-based `show-text` messages sent through the client API opt into `expand-properties`. That preserves existing template-based OSD messages like subtitle delay and subtitle position without leaking the raw `${...}` syntax.
|
|
||||||
|
|
||||||
Added regression coverage for the async proxy OSD path, the placeholder-expansion `showMpvOsdRuntime` path, and the runtime subtitle-track label resolver. Verification run: `bun x tsc --noEmit`; focused mpv/IPC tests; and the maintained `bun run test:core:src` lane (423 pass, 6 skip, 0 fail).
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-112
|
|
||||||
title: Address Claude review items on PR 15
|
|
||||||
status: Done
|
|
||||||
assignee:
|
|
||||||
- codex
|
|
||||||
created_date: '2026-03-08 00:11'
|
|
||||||
updated_date: '2026-03-08 00:12'
|
|
||||||
labels:
|
|
||||||
- pr-review
|
|
||||||
- ci
|
|
||||||
dependencies: []
|
|
||||||
references:
|
|
||||||
- .github/workflows/release.yml
|
|
||||||
- .github/workflows/ci.yml
|
|
||||||
- .gitmodules
|
|
||||||
- >-
|
|
||||||
backlog/tasks/task-101 -
|
|
||||||
Index-AniList-character-alternative-names-in-the-character-dictionary.md
|
|
||||||
priority: medium
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
Review Claude's PR feedback on PR #15, implement only the technically valid fixes on the current branch, and document which comments are non-actionable or already acceptable.
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
<!-- AC:BEGIN -->
|
|
||||||
- [x] #1 Validated Claude's concrete PR review items against current branch state and repo conventions
|
|
||||||
- [x] #2 Implemented the accepted fixes with regression coverage or verification where applicable
|
|
||||||
- [x] #3 Documented which review items are non-blocking or intentionally left unchanged
|
|
||||||
<!-- AC:END -->
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
<!-- SECTION:PLAN:BEGIN -->
|
|
||||||
1. Validate each Claude review item against current branch files and repo workflow.
|
|
||||||
2. Patch release quality-gate to match CI ordering and add explicit typecheck.
|
|
||||||
3. Remove duplicate .gitmodules stanza and normalize the TASK-101 reference path through Backlog MCP.
|
|
||||||
4. Run relevant verification for workflow/config metadata changes and record which review items remain non-actionable.
|
|
||||||
<!-- SECTION:PLAN:END -->
|
|
||||||
|
|
||||||
## Implementation Notes
|
|
||||||
|
|
||||||
<!-- SECTION:NOTES:BEGIN -->
|
|
||||||
User asked to address Claude PR comments on PR #15 and assess whether any action items remain. Treat review suggestions skeptically; only fix validated defects.
|
|
||||||
|
|
||||||
Validated Claude's five review items. Fixed release workflow ordering/typecheck, removed the duplicate .gitmodules entry, and normalized TASK-101 references to repo-relative paths via Backlog MCP.
|
|
||||||
|
|
||||||
Left the vendor/subminer-yomitan branch-pin suggestion unchanged. The committed submodule SHA already controls reproducibility; adding a branch would only affect update ergonomics and was not required to address a concrete defect.
|
|
||||||
<!-- SECTION:NOTES:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
Validated Claude's PR #15 review summary against the current branch and applied the actionable fixes. In `.github/workflows/release.yml`, the release `quality-gate` job now restores the dependency cache before installation, no longer installs twice, and runs `bun run typecheck` before the fast test suite to match CI expectations. In `.gitmodules`, removed the duplicate `vendor/yomitan-jlpt-vocab` stanza with the conflicting duplicate path. Through Backlog MCP, updated `TASK-101` references from an absolute local path to repo-relative paths so the task metadata is portable across contributors.
|
|
||||||
|
|
||||||
Verification: `git diff --check`, `git config -f .gitmodules --get-regexp '^submodule\..*\.path$'`, `bun run typecheck`, and `bun run test:fast` all passed. `bun run format:check` still fails on many pre-existing unrelated files already present on the branch, including multiple backlog task files and existing source/docs files; this review patch did not attempt a repo-wide formatting sweep.
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-113
|
|
||||||
title: Scope make pretty to maintained source files
|
|
||||||
status: Done
|
|
||||||
assignee:
|
|
||||||
- codex
|
|
||||||
created_date: '2026-03-08 00:20'
|
|
||||||
updated_date: '2026-03-08 00:22'
|
|
||||||
labels:
|
|
||||||
- tooling
|
|
||||||
- formatting
|
|
||||||
dependencies: []
|
|
||||||
references:
|
|
||||||
- Makefile
|
|
||||||
- package.json
|
|
||||||
priority: medium
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
Change the `make pretty` workflow so it formats only the maintained source/config files we intentionally keep under Prettier, instead of sweeping backlog/docs/generated content across the whole repository.
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
<!-- AC:BEGIN -->
|
|
||||||
- [x] #1 `make pretty` formats only the approved maintained source/config paths
|
|
||||||
- [x] #2 The allowlist is reusable for check/write flows instead of duplicating path logic
|
|
||||||
- [x] #3 Verification shows the scoped formatting command targets the intended files without touching backlog or vendored content
|
|
||||||
<!-- AC:END -->
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
<!-- SECTION:PLAN:BEGIN -->
|
|
||||||
1. Inspect current Prettier config/ignore behavior and keep the broad repo-wide format command unchanged.
|
|
||||||
2. Add a reusable scoped Prettier script that targets maintained source/config paths only.
|
|
||||||
3. Update `make pretty` to call the scoped script.
|
|
||||||
4. Verify the scoped command resolves only intended files and does not traverse backlog or vendor paths.
|
|
||||||
<!-- SECTION:PLAN:END -->
|
|
||||||
|
|
||||||
## Implementation Notes
|
|
||||||
|
|
||||||
<!-- SECTION:NOTES:BEGIN -->
|
|
||||||
User approved the allowlist approach: keep repo-wide `format` intact, make `make pretty` use a maintained-path formatter scope.
|
|
||||||
|
|
||||||
Added `scripts/prettier-scope.sh` as the single allowlist for scoped Prettier paths and wired `format:src` / `format:check:src` to it.
|
|
||||||
|
|
||||||
Updated `make pretty` to call `bun run format:src`. Verified with `make -n pretty` and shell tracing that the helper only targets the maintained allowlist and does not traverse `backlog/` or `vendor/`.
|
|
||||||
|
|
||||||
Excluded `Makefile` and `.prettierignore` from the allowlist after verification showed Prettier cannot infer parsers for them.
|
|
||||||
<!-- SECTION:NOTES:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
Scoped the repo's day-to-day formatting entrypoint without changing the existing broad repo-wide Prettier scripts. Added `scripts/prettier-scope.sh` as the shared allowlist for maintained source/config paths (`.github`, `build`, `launcher`, `scripts`, `src`, plus selected root JSON config files), added `format:src` and `format:check:src` in `package.json`, and updated `make pretty` to run the scoped formatter.
|
|
||||||
|
|
||||||
Verification: `make -n pretty` now resolves to `bun run format:src`. `bash -n scripts/prettier-scope.sh` passed, and shell-traced `bash -x scripts/prettier-scope.sh --check` confirmed the exact allowlist passed to Prettier. `bun run format:check:src` fails only because existing files inside the allowed source scope are not currently formatted; it no longer touches `backlog/` or `vendor/`.
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-114
|
|
||||||
title: Fix failing CI checks on PR 15
|
|
||||||
status: Done
|
|
||||||
assignee:
|
|
||||||
- codex
|
|
||||||
created_date: '2026-03-08 00:34'
|
|
||||||
updated_date: '2026-03-08 00:37'
|
|
||||||
labels:
|
|
||||||
- ci
|
|
||||||
- test
|
|
||||||
dependencies: []
|
|
||||||
references:
|
|
||||||
- src/renderer/subtitle-render.test.ts
|
|
||||||
- src/renderer/style.css
|
|
||||||
- .github/workflows/ci.yml
|
|
||||||
priority: high
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
Investigate the failing GitHub Actions CI run for PR #15 on branch `yomitan-fork`, fix the underlying test or code regression, and verify the affected local test/CI lane passes.
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
<!-- AC:BEGIN -->
|
|
||||||
- [x] #1 Identified the concrete failing CI job and captured the relevant failure context
|
|
||||||
- [x] #2 Implemented the minimal code or test change needed to resolve the CI failure
|
|
||||||
- [x] #3 Verified the affected local test target and the broader fast CI test lane pass
|
|
||||||
<!-- AC:END -->
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
<!-- SECTION:PLAN:BEGIN -->
|
|
||||||
1. Inspect the failing GitHub Actions run and confirm the exact failing test/assertion.
|
|
||||||
2. Reproduce the failing renderer stylesheet test locally and compare the assertion against current CSS.
|
|
||||||
3. Apply the minimal test or stylesheet fix needed to restore the intended hover/selection behavior.
|
|
||||||
4. Re-run the targeted renderer test, then re-run `bun run test` to verify the fast CI lane is green.
|
|
||||||
<!-- SECTION:PLAN:END -->
|
|
||||||
|
|
||||||
## Implementation Notes
|
|
||||||
|
|
||||||
<!-- SECTION:NOTES:BEGIN -->
|
|
||||||
GitHub Actions run 22810400921 failed in job build-test-audit, step `Test suite (source)`, with a single failing test: `JLPT CSS rules use underline-only styling in renderer stylesheet` in src/renderer/subtitle-render.test.ts.
|
|
||||||
|
|
||||||
Reproduced the failing test locally with `bun test src/renderer/subtitle-render.test.ts`. The failure was a brittle stylesheet assertion, not a renderer behavior regression.
|
|
||||||
|
|
||||||
Updated the renderer stylesheet test helper to split selectors safely across `:is(...)` commas and normalize multiline selector whitespace, then switched the failing hover/JLPT assertions to inspect extracted rule blocks instead of matching the entire CSS file text.
|
|
||||||
|
|
||||||
Verification passed with `bun test src/renderer/subtitle-render.test.ts` and `bun run test`.
|
|
||||||
<!-- SECTION:NOTES:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
Investigated GitHub Actions CI run `22810400921` for PR #15 and confirmed the only failing job was `build-test-audit`, step `Test suite (source)`, with a single failure in `src/renderer/subtitle-render.test.ts` (`JLPT CSS rules use underline-only styling in renderer stylesheet`).
|
|
||||||
|
|
||||||
The renderer CSS itself was still correct; the regression was in the test helper. `extractClassBlock` was splitting selector lists on every comma, which breaks selectors containing `:is(...)`, and the affected assertions fell back to brittle whole-file regex matching against a multiline selector. Fixed the test by teaching the helper to split selectors only at top-level commas, normalizing selector whitespace around multiline `:not(...)` / `:is(...)` clauses, and asserting on extracted rule blocks for the plain-word hover and JLPT-only hover/selection rules.
|
|
||||||
|
|
||||||
Verification: `bun test src/renderer/subtitle-render.test.ts` passed, and `bun run test` passed end to end (the same fast lane that failed in CI).
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-115
|
|
||||||
title: Refresh subminer-docs contributor docs for current repo workflow
|
|
||||||
status: Done
|
|
||||||
assignee:
|
|
||||||
- codex
|
|
||||||
created_date: '2026-03-08 00:40'
|
|
||||||
updated_date: '2026-03-08 00:42'
|
|
||||||
labels:
|
|
||||||
- docs
|
|
||||||
dependencies: []
|
|
||||||
references:
|
|
||||||
- ../subminer-docs/development.md
|
|
||||||
- ../subminer-docs/README.md
|
|
||||||
- Makefile
|
|
||||||
- package.json
|
|
||||||
priority: medium
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
Update the sibling `subminer-docs` repo so contributor/development docs match the current SubMiner repo workflow after the docs split and recent tooling changes, including removing stale in-repo docs build steps and documenting the scoped formatting command.
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
<!-- AC:BEGIN -->
|
|
||||||
- [x] #1 Contributor docs in `subminer-docs` no longer reference stale in-repo docs build commands for the app repo
|
|
||||||
- [x] #2 Contributor docs mention the current scoped formatting workflow (`make pretty` / `format:src`) where relevant
|
|
||||||
- [x] #3 Removed stale or no-longer-needed instructions that no longer match the current repo layout
|
|
||||||
<!-- AC:END -->
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
<!-- SECTION:PLAN:BEGIN -->
|
|
||||||
1. Inspect `subminer-docs` for contributor/development instructions that drifted after the docs repo split and recent tooling changes.
|
|
||||||
2. Update contributor docs to remove stale app-repo docs commands and document the current scoped formatting workflow.
|
|
||||||
3. Verify the modified docs page and build the docs site from the sibling docs repo when local dependencies are available.
|
|
||||||
<!-- SECTION:PLAN:END -->
|
|
||||||
|
|
||||||
## Implementation Notes
|
|
||||||
|
|
||||||
<!-- SECTION:NOTES:BEGIN -->
|
|
||||||
Detected concrete doc drift in `subminer-docs/development.md`: stale in-repo docs build commands and no mention of the scoped `make pretty` formatter.
|
|
||||||
|
|
||||||
Updated `../subminer-docs/development.md` to remove stale app-repo docs build steps from the local gate, document `make pretty` / `format:check:src`, and point docs-site work to the sibling docs repo explicitly.
|
|
||||||
|
|
||||||
Installed docs repo dependencies locally with `bun install` and verified the docs site with `bun run docs:build` in `../subminer-docs`.
|
|
||||||
|
|
||||||
Did not change `../subminer-docs/README.md`; it was already accurate for the docs repo itself.
|
|
||||||
<!-- SECTION:NOTES:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
Refreshed the contributor/development docs in the sibling `subminer-docs` repo to match the current SubMiner workflow. In `development.md`, removed the stale app-repo `bun run docs:build` step from the local CI-equivalent gate, added an explicit note to run docs builds from `../subminer-docs` when docs change, documented the scoped formatting workflow (`make pretty` and `bun run format:check:src`), and replaced the old in-repo `make docs*` instructions with the correct sibling-repo `bun run docs:*` commands. Also updated the Makefile reference to include `make pretty` and removed the obsolete `make docs-dev` entry.
|
|
||||||
|
|
||||||
Verification: installed docs repo dependencies with `bun install` in `../subminer-docs` and ran `bun run docs:build` successfully. Left `README.md` unchanged because it was already accurate for the standalone docs repo.
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-116
|
|
||||||
title: Audit branch commits for remaining subminer-docs updates
|
|
||||||
status: Done
|
|
||||||
assignee:
|
|
||||||
- codex
|
|
||||||
created_date: '2026-03-08 00:46'
|
|
||||||
updated_date: '2026-03-08 00:48'
|
|
||||||
labels:
|
|
||||||
- docs
|
|
||||||
dependencies: []
|
|
||||||
references:
|
|
||||||
- ../subminer-docs/installation.md
|
|
||||||
- ../subminer-docs/troubleshooting.md
|
|
||||||
- src/core/services/yomitan-extension-paths.ts
|
|
||||||
- scripts/build-yomitan.mjs
|
|
||||||
priority: medium
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
Review recent `yomitan-fork` commits against the sibling `subminer-docs` repo, identify any concrete documentation drift that remains after the earlier contributor-doc updates, and patch the docs for behavior/tooling changes that are now outdated or misleading.
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
<!-- AC:BEGIN -->
|
|
||||||
- [x] #1 Reviewed recent branch commits for user-facing or contributor-facing changes that may require docs updates
|
|
||||||
- [x] #2 Updated `subminer-docs` pages where branch changes introduced concrete doc drift
|
|
||||||
- [x] #3 Verified the docs site still builds after the updates
|
|
||||||
<!-- AC:END -->
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
<!-- SECTION:PLAN:BEGIN -->
|
|
||||||
1. Review branch commit themes against `subminer-docs` and identify only concrete drift introduced by recent workflow/runtime changes.
|
|
||||||
2. Patch docs for the Yomitan submodule build workflow, updated source-build prerequisites, and current runtime Yomitan search paths/manual fallback path.
|
|
||||||
3. Rebuild the docs site to verify the updated pages render cleanly.
|
|
||||||
<!-- SECTION:PLAN:END -->
|
|
||||||
|
|
||||||
## Implementation Notes
|
|
||||||
|
|
||||||
<!-- SECTION:NOTES:BEGIN -->
|
|
||||||
Concrete remaining drift after commit audit: installation/development docs still understate the Node/npm + submodule requirements for the Yomitan build flow, and troubleshooting still points at obsolete `vendor/yomitan` / `extensions/yomitan` paths.
|
|
||||||
|
|
||||||
Audited branch commits against subminer-docs coverage. Existing docs already cover first-run setup, texthooker startup/annotated websocket config, AniList merged character dictionaries, configurable collapsible sections, and subtitle name highlighting. Patched remaining drift around source-build prerequisites and Yomitan build/install paths in installation.md, development.md, and troubleshooting.md. Verified with `bun run docs:build` in ../subminer-docs.
|
|
||||||
<!-- SECTION:NOTES:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
Audited branch commits for missing documentation updates in ../subminer-docs. Updated installation, development, and troubleshooting docs to match the current Yomitan submodule build flow, source-build prerequisites, and runtime extension search/manual fallback paths. Confirmed other recent branch features were already documented and rebuilt the docs site successfully.
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
28
docs/anki-integration.md
Normal file
28
docs/anki-integration.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Anki Integration
|
||||||
|
|
||||||
|
read_when:
|
||||||
|
- changing `src/anki-integration.ts`
|
||||||
|
- changing Anki transport/config hot-reload behavior
|
||||||
|
- tracing note update, field grouping, or proxy ownership
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- `src/anki-integration.ts`: thin facade; wires dependencies; exposes public Anki API used by runtime/services.
|
||||||
|
- `src/anki-integration/runtime.ts`: normalized config state, polling-vs-proxy transport lifecycle, runtime config patch handling.
|
||||||
|
- `src/anki-integration/card-creation.ts`: sentence/audio card creation and clipboard update flow.
|
||||||
|
- `src/anki-integration/note-update-workflow.ts`: enrich newly added notes.
|
||||||
|
- `src/anki-integration/field-grouping.ts`: preview/build helpers for Kiku field grouping.
|
||||||
|
- `src/anki-integration/field-grouping-workflow.ts`: auto/manual merge execution.
|
||||||
|
- `src/anki-integration/anki-connect-proxy.ts`: local proxy transport for post-add enrichment.
|
||||||
|
- `src/anki-integration/known-word-cache.ts`: known-word cache lifecycle and persistence.
|
||||||
|
|
||||||
|
## Refactor seam
|
||||||
|
|
||||||
|
`AnkiIntegrationRuntime` owns the cluster that previously mixed:
|
||||||
|
|
||||||
|
- config normalization/defaulting
|
||||||
|
- polling vs proxy startup/shutdown
|
||||||
|
- transport restart decisions during runtime patches
|
||||||
|
- known-word cache lifecycle toggles tied to config changes
|
||||||
|
|
||||||
|
Keep new orchestration work in `runtime.ts` when it changes process-level Anki state. Keep note/card behavior in the workflow/service modules.
|
||||||
50
docs/plans/2026-03-06-character-name-gating.md
Normal file
50
docs/plans/2026-03-06-character-name-gating.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Character Name Gating Implementation Plan
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Disable subtitle character-name lookup/highlighting when the AniList character dictionary feature is disabled, while keeping tokenization and all other annotations working.
|
||||||
|
|
||||||
|
**Architecture:** Gate `getNameMatchEnabled` at the runtime-deps boundary used by subtitle tokenization. Keep the tokenizer pipeline intact and only suppress character-name metadata requests when `anilist.characterDictionary.enabled` is false, regardless of `subtitleStyle.nameMatchEnabled`.
|
||||||
|
|
||||||
|
**Tech Stack:** TypeScript, Bun test runner, Electron main/runtime wiring.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add runtime gating coverage
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/main/runtime/subtitle-tokenization-main-deps.test.ts`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Add a test proving `getNameMatchEnabled()` resolves to `false` when `getCharacterDictionaryEnabled()` is `false` even if `getNameMatchEnabled()` is `true`.
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test src/main/runtime/subtitle-tokenization-main-deps.test.ts`
|
||||||
|
Expected: FAIL because the deps builder does not yet combine the two flags.
|
||||||
|
|
||||||
|
### Task 2: Implement minimal runtime gate
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/main/runtime/subtitle-tokenization-main-deps.ts`
|
||||||
|
- Modify: `src/main.ts`
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Add `getCharacterDictionaryEnabled` to the main handler deps and make the built `getNameMatchEnabled` return true only when both the subtitle setting and the character dictionary setting are enabled.
|
||||||
|
|
||||||
|
**Step 4: Run tests to verify green**
|
||||||
|
|
||||||
|
Run: `bun test src/main/runtime/subtitle-tokenization-main-deps.test.ts`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
### Task 3: Verify no regressions in related tokenization seams
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: none unless failures reveal drift
|
||||||
|
|
||||||
|
**Step 5: Run focused verification**
|
||||||
|
|
||||||
|
Run: `bun test src/core/services/subtitle-processing-controller.test.ts src/main/runtime/subtitle-tokenization-main-deps.test.ts`
|
||||||
|
Expected: PASS.
|
||||||
155
docs/plans/2026-03-06-immersion-sqlite-verification.md
Normal file
155
docs/plans/2026-03-06-immersion-sqlite-verification.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# Immersion SQLite Verification Implementation Plan
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Make the SQLite-backed immersion tracking persistence tests visible in the repo's verification surface and reproducible through at least one documented automated command.
|
||||||
|
|
||||||
|
**Architecture:** Keep the existing Bun fast lane intact for routine local verification, but add an explicit SQLite verification lane that runs the database-backed immersion tests under a runtime with `node:sqlite` support. Surface unsupported-runtime behavior clearly in the source tests and contributor docs so skipped or omitted coverage is no longer mistaken for a fully green persistence lane.
|
||||||
|
|
||||||
|
**Tech Stack:** TypeScript, Bun scripts in `package.json`, Node's built-in `node:test` and `node:sqlite`, GitHub Actions workflows, Markdown docs in `README.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Audit and expose the SQLite-backed immersion test surface
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `src/core/services/immersion-tracker-service.test.ts`
|
||||||
|
- Modify: `src/core/services/immersion-tracker/storage-session.test.ts`
|
||||||
|
- Reference: `src/main/runtime/registry.test.ts`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Refactor the SQLite-gated immersion tests so missing `node:sqlite` support is reported with an explicit skip reason instead of a silent top-level `test.skip` alias.
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test src/core/services/immersion-tracker-service.test.ts src/core/services/immersion-tracker/storage-session.test.ts`
|
||||||
|
Expected: the current output shows generic skips or hides the storage-session suite from normal scripted verification, which is too opaque for contributors.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Mirror the `src/main/runtime/registry.test.ts` pattern: add a helper that either loads `DatabaseSync` or skips with a message like `requires node:sqlite support in this runtime`, then wrap each SQLite-backed test through that helper.
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `bun test src/core/services/immersion-tracker-service.test.ts src/core/services/immersion-tracker/storage-session.test.ts`
|
||||||
|
Expected: PASS, with explicit skip messages in unsupported runtimes.
|
||||||
|
|
||||||
|
### Task 2: Add a reproducible SQLite verification command
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `package.json`
|
||||||
|
- Reference: `src/core/services/immersion-tracker-service.test.ts`
|
||||||
|
- Reference: `src/core/services/immersion-tracker/storage-session.test.ts`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Add a dedicated script contract for the SQLite-backed immersion verification lane so both persistence-heavy suites are intentionally grouped and runnable together.
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun run test:immersion:sqlite`
|
||||||
|
Expected: FAIL because no such reproducible lane exists yet.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Update `package.json` with explicit scripts for the SQLite lane. Prefer a command shape that actually executes the built JS tests under Node with `node:sqlite` support, for example:
|
||||||
|
|
||||||
|
- `test:immersion:sqlite:dist`: `node --test dist/core/services/immersion-tracker-service.test.js dist/core/services/immersion-tracker/storage-session.test.js`
|
||||||
|
- `test:immersion:sqlite`: `bun run build && bun run test:immersion:sqlite:dist`
|
||||||
|
|
||||||
|
If build cost or runtime behavior requires a small adjustment, keep the core contract the same: one documented command must run both SQLite-backed immersion suites end-to-end.
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `bun run test:immersion:sqlite`
|
||||||
|
Expected: PASS in a Node runtime with `node:sqlite`, executing both persistence suites without Bun-only skips.
|
||||||
|
|
||||||
|
### Task 3: Wire the SQLite lane into automated verification
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `.github/workflows/ci.yml`
|
||||||
|
- Modify: `.github/workflows/release.yml`
|
||||||
|
- Reference: `package.json`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Add the new SQLite immersion lane to the repo's automated verification so contributors and CI can rely on a real persistence check rather than the Bun fast lane alone.
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun run test:immersion:sqlite`
|
||||||
|
Expected: local command may pass, but CI/release workflows still omit the lane entirely.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Update both workflows to provision a Node version with `node:sqlite` support before the SQLite lane runs, then execute `bun run test:immersion:sqlite` in the quality gate after the bundle build produces `dist/**` test files.
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `bun run test:immersion:sqlite`
|
||||||
|
Expected: PASS locally, and workflow definitions clearly show the SQLite lane as part of automated verification.
|
||||||
|
|
||||||
|
### Task 4: Document contributor-facing prerequisites and commands
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `README.md`
|
||||||
|
- Reference: `package.json`
|
||||||
|
- Reference: `.github/workflows/ci.yml`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Extend the verification docs so contributors can discover the SQLite lane, know why the Bun source lane may skip those cases, and understand which command reproduces the persistence coverage.
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `grep -n "test:immersion:sqlite" README.md`
|
||||||
|
Expected: FAIL because the dedicated immersion SQLite lane is undocumented.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Update `README.md` to document:
|
||||||
|
|
||||||
|
- the Bun fast/default lane versus the SQLite persistence lane
|
||||||
|
- the `node:sqlite` prerequisite for the reproducible command
|
||||||
|
- that the dedicated lane covers session persistence/finalization behavior beyond seam tests
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `grep -n "test:immersion:sqlite" README.md && grep -n "node:sqlite" README.md`
|
||||||
|
Expected: PASS, with clear contributor guidance.
|
||||||
|
|
||||||
|
### Task 5: Verify persistence coverage end-to-end
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Test: `src/core/services/immersion-tracker-service.test.ts`
|
||||||
|
- Test: `src/core/services/immersion-tracker/storage-session.test.ts`
|
||||||
|
- Reference: `README.md`
|
||||||
|
- Reference: `package.json`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Prove the final lane exercises real DB-backed persistence/finalization paths, not just the seam tests.
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun run test:immersion:sqlite`
|
||||||
|
Expected: before implementation, the command does not exist or does not cover both SQLite-backed suites.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Keep the dedicated lane pointed at both existing SQLite-backed test files so it covers representative finalization and persistence behavior such as:
|
||||||
|
|
||||||
|
- `destroy finalizes active session and persists final telemetry`
|
||||||
|
- `start/finalize session updates ended_at and status`
|
||||||
|
- `executeQueuedWrite inserts event and telemetry rows`
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `bun run test:immersion:sqlite`
|
||||||
|
Expected: PASS, with those DB-backed persistence/finalization cases executing successfully under Node.
|
||||||
92
docs/plans/2026-03-06-merged-character-dictionary.md
Normal file
92
docs/plans/2026-03-06-merged-character-dictionary.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Merged Character Dictionary Implementation Plan
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Replace per-anime character dictionary imports with one merged Yomitan dictionary driven by MRU usage retention.
|
||||||
|
|
||||||
|
**Architecture:** Persist normalized per-media character dictionary snapshots locally, maintain MRU retained media ids in auto-sync state, and rebuild a single merged Yomitan zip only when the retained set changes. Keep external AniList fetches only for media without a local snapshot; normal revisits stay local.
|
||||||
|
|
||||||
|
**Tech Stack:** TypeScript, Bun test, Node fs/path, existing Yomitan zip generation helpers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Lock in merged auto-sync behavior
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/main/runtime/character-dictionary-auto-sync.test.ts`
|
||||||
|
- Test: `src/main/runtime/character-dictionary-auto-sync.test.ts`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Add tests for:
|
||||||
|
- single merged dictionary title/import replacing per-media imports
|
||||||
|
- MRU reorder causing rebuild only when order changes
|
||||||
|
- unchanged revisit skipping rebuild/import
|
||||||
|
- capped retained set evicting least-recently-used media
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test src/main/runtime/character-dictionary-auto-sync.test.ts`
|
||||||
|
Expected: FAIL on old per-media import assumptions / missing merged behavior
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Update auto-sync runtime to track retained media ids and merged revision/hash, call merged zip builder, and replace one imported Yomitan dictionary.
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `bun test src/main/runtime/character-dictionary-auto-sync.test.ts`
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
### Task 2: Add snapshot + merged-zip runtime support
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/main/character-dictionary-runtime.ts`
|
||||||
|
- Modify: `src/main/character-dictionary-runtime.test.ts`
|
||||||
|
- Test: `src/main/character-dictionary-runtime.test.ts`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Add tests for:
|
||||||
|
- saving/loading normalized per-media snapshots without per-media zip cache
|
||||||
|
- building merged zip from retained media snapshots with stable dictionary title
|
||||||
|
- preserving images/terms from multiple media in merged output
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test src/main/character-dictionary-runtime.test.ts`
|
||||||
|
Expected: FAIL because snapshot/merged APIs do not exist yet
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Refactor dictionary runtime to expose snapshot generation/loading and merged zip building from stored metadata/images.
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `bun test src/main/character-dictionary-runtime.test.ts`
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
### Task 3: Wire app/runtime config and docs
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/main.ts`
|
||||||
|
- Modify: `src/config/definitions/options-integrations.ts`
|
||||||
|
- Modify: `README.md`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Add or update tests if needed for new dependency wiring / docs-adjacent config description expectations.
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test src/main/runtime/character-dictionary-auto-sync.test.ts src/main/character-dictionary-runtime.test.ts`
|
||||||
|
Expected: FAIL until wiring matches merged flow
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Swap app wiring to new snapshot + merged build API, update config/docs text from TTL semantics to usage-based merged retention.
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `bun test src/main/runtime/character-dictionary-auto-sync.test.ts src/main/character-dictionary-runtime.test.ts && bun run tsc --noEmit`
|
||||||
|
Expected: PASS
|
||||||
121
docs/plans/2026-03-06-subtitle-sync-verification.md
Normal file
121
docs/plans/2026-03-06-subtitle-sync-verification.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# Subtitle Sync Verification Implementation Plan
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Replace the no-op `test:subtitle` lane with real automated subtitle-sync verification that reuses the maintained subsync tests and documents the real contributor workflow.
|
||||||
|
|
||||||
|
**Architecture:** Repoint the subtitle verification command at the existing source-level subsync tests instead of inventing a second hidden suite. Add one focused ffsubsync failure-path test so the subtitle lane explicitly covers both engines plus a non-happy path, then update contributor docs to describe the dedicated subtitle lane and how it relates to `test:core`.
|
||||||
|
|
||||||
|
**Tech Stack:** TypeScript, Bun test, Node test/assert, npm package scripts, Markdown docs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Lock subtitle lane to real subsync tests
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `package.json`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Define the intended command shape first: `test:subtitle:src` should run `src/core/services/subsync.test.ts` and `src/subsync/utils.test.ts`, `test:subtitle` should invoke that real source lane, and no placeholder echo should remain.
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun run test:subtitle`
|
||||||
|
Expected: It performs a build and prints `Subtitle tests are currently not configured`, proving the lane is still a no-op.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Update `package.json` so:
|
||||||
|
|
||||||
|
- `test:subtitle:src` runs `bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts`
|
||||||
|
- `test:subtitle` runs the new source lane directly
|
||||||
|
- `test:subtitle:dist` is removed if it is no longer the real verification path
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `bun run test:subtitle`
|
||||||
|
Expected: PASS with Bun executing the real subtitle-sync test files.
|
||||||
|
|
||||||
|
### Task 2: Add explicit ffsubsync non-happy-path coverage
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `src/core/services/subsync.test.ts`
|
||||||
|
- Test: `src/core/services/subsync.test.ts`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Add a test that runs `runSubsyncManual({ engine: 'ffsubsync' })` with a stub ffsubsync executable that exits non-zero and writes stderr, then assert:
|
||||||
|
|
||||||
|
- `result.ok === false`
|
||||||
|
- `result.message` starts with `ffsubsync synchronization failed`
|
||||||
|
- the failure message includes command details surfaced to the user
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test src/core/services/subsync.test.ts`
|
||||||
|
Expected: FAIL because ffsubsync failure propagation is not asserted yet.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Keep production code unchanged unless the new test exposes a real bug. If needed, tighten failure assertions or message propagation in `src/core/services/subsync.ts` without changing successful behavior.
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `bun test src/core/services/subsync.test.ts`
|
||||||
|
Expected: PASS with both alass and ffsubsync paths covered, including a non-happy path.
|
||||||
|
|
||||||
|
### Task 3: Make contributor docs match the real verification path
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `README.md`
|
||||||
|
- Modify: `package.json`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Use the repository state as the failure signal: README currently advertises subtitle sync as a feature but does not tell contributors that `bun run test:subtitle` is the real verification lane.
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun run test:subtitle && bun test src/subsync/utils.test.ts`
|
||||||
|
Expected: Tests pass, but docs still do not explain the lane; this is the remaining acceptance-criteria gap.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Update `README.md` with a short contributor-facing verification note that:
|
||||||
|
|
||||||
|
- points to `bun run test:subtitle` for subtitle-sync coverage
|
||||||
|
- states that the lane reuses the maintained subsync tests already included in broader core coverage
|
||||||
|
- avoids implying there is a separate hidden subtitle test harness beyond those tests
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `bun run test:subtitle`
|
||||||
|
Expected: PASS, with docs and scripts now aligned around the same subtitle verification strategy.
|
||||||
|
|
||||||
|
### Task 4: Verify matrix integration stays clean
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `package.json` (only if Task 1/3 exposed cleanup needs)
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Treat duplication as the failure condition: confirm the dedicated subtitle lane reuses the same maintained files already present in `test:core:src` rather than creating a second divergent suite.
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun run test:subtitle && bun run test:core:src`
|
||||||
|
Expected: If file lists diverge unexpectedly, this review step exposes it before handoff.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
If needed, do the smallest script cleanup necessary so subtitle coverage remains explicit without hiding or duplicating existing core coverage.
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `bun run test:subtitle && bun run test:core:src`
|
||||||
|
Expected: PASS, confirming the dedicated lane and the broader core suite agree on subtitle coverage.
|
||||||
169
docs/plans/2026-03-06-testing-workflow-test-matrix.md
Normal file
169
docs/plans/2026-03-06-testing-workflow-test-matrix.md
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
# Testing Workflow Test Matrix Implementation Plan
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Make the standard test commands reflect the maintained test surface so newly added tests are discovered automatically or intentionally documented outside the default lane.
|
||||||
|
|
||||||
|
**Architecture:** Replace the current hand-maintained file allowlists in `package.json` with directory-based Bun test lanes that map to maintained test surfaces. Keep the default developer lane fast, move slower or environment-specific checks into explicit commands, and document the resulting matrix in `README.md` so contributors know exactly which command to run.
|
||||||
|
|
||||||
|
**Tech Stack:** TypeScript, Bun test, npm-style package scripts in `package.json`, Markdown docs in `README.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Lock in the desired script matrix with failing tests/audit checks
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `package.json`
|
||||||
|
- Test: `package.json`
|
||||||
|
- Reference: `src/main-entry-runtime.test.ts`
|
||||||
|
- Reference: `src/anki-integration/anki-connect-proxy.test.ts`
|
||||||
|
- Reference: `src/main/runtime/registry.test.ts`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Add a new script structure in `package.json` expectations by editing the script map so these lanes exist conceptually:
|
||||||
|
|
||||||
|
- `test:fast` for default fast verification
|
||||||
|
- `test:full` for the maintained source test surface
|
||||||
|
- `test:env` for environment-specific checks
|
||||||
|
|
||||||
|
The fast lane should stay selective and intentional. The full lane should use directory-based discovery rather than file-by-file allowlists, with representative coverage from:
|
||||||
|
|
||||||
|
- `src/main-entry-runtime.test.ts`
|
||||||
|
- `src/anki-integration/**/*.test.ts`
|
||||||
|
- `src/main/**/*.test.ts`
|
||||||
|
- `launcher/**/*.test.ts`
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun run test:full`
|
||||||
|
Expected: FAIL because `test:full` does not exist yet, and previously omitted maintained tests are still outside the standard matrix.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Update `package.json` scripts so:
|
||||||
|
|
||||||
|
- `test` points at `test:fast`
|
||||||
|
- `test:fast` runs the fast default lane only
|
||||||
|
- `test:full` runs directory-based maintained suites instead of file allowlists
|
||||||
|
- `test:env` runs environment-specific verification (for example launcher/plugin and sqlite-gated suites)
|
||||||
|
- subsystem scripts use stable path globs or directory arguments so new tests are discovered automatically
|
||||||
|
|
||||||
|
Prefer commands like these, adjusted only as needed for Bun behavior in this repo:
|
||||||
|
|
||||||
|
- `bun test src/config/**/*.test.ts`
|
||||||
|
- `bun test src/{cli,core,renderer,subtitle,subsync,main,anki-integration}/*.test.ts ...` only if Bun cannot take the broader directory directly
|
||||||
|
- `bun test launcher/**/*.test.ts`
|
||||||
|
|
||||||
|
Do not keep large hand-maintained file enumerations for maintained unit/integration lanes.
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `bun run test:full`
|
||||||
|
Expected: PASS, including automated execution of representative tests that were previously omitted from the standard matrix.
|
||||||
|
|
||||||
|
### Task 2: Separate environment-specific verification from the maintained default/full lanes
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `package.json`
|
||||||
|
- Test: `src/main/runtime/registry.test.ts`
|
||||||
|
- Test: `launcher/smoke.e2e.test.ts`
|
||||||
|
- Test: `src/core/services/immersion-tracker-service.test.ts`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Refine the package scripts so environment-specific checks are explicitly grouped outside the default fast lane. Treat these as the primary environment-specific examples unless repo behavior proves a better split during execution:
|
||||||
|
|
||||||
|
- launcher smoke/plugin checks that rely on local process or Lua execution
|
||||||
|
- sqlite-dependent checks that may skip when `node:sqlite` is unavailable
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun run test:env`
|
||||||
|
Expected: FAIL because the environment-specific lane is not defined yet.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Add explicit environment-specific scripts in `package.json`, such as:
|
||||||
|
|
||||||
|
- a launcher/plugin lane that runs `launcher/smoke.e2e.test.ts` plus `lua scripts/test-plugin-start-gate.lua`
|
||||||
|
- a sqlite lane for tests that require `node:sqlite` support or otherwise need environment notes
|
||||||
|
- an aggregate `test:env` command that runs all environment-specific lanes
|
||||||
|
|
||||||
|
Keep these lanes documented and reproducible rather than silently excluded.
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `bun run test:env`
|
||||||
|
Expected: PASS in supported environments, or clear documented skip behavior where the tests themselves intentionally gate on missing runtime support.
|
||||||
|
|
||||||
|
### Task 3: Document contributor-facing test commands and matrix
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `README.md`
|
||||||
|
- Reference: `package.json`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Add a contributor-focused testing section requirement in `README.md` expectations:
|
||||||
|
|
||||||
|
- fast verification command
|
||||||
|
- full verification command
|
||||||
|
- environment-specific verification command
|
||||||
|
- plain-language explanation of which suites each lane covers and why
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `grep -n "Testing" README.md`
|
||||||
|
Expected: no contributor testing matrix section exists yet.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Update `README.md` with a concise `Testing` section that documents:
|
||||||
|
|
||||||
|
- `bun run test` / `bun run test:fast` for fast local verification
|
||||||
|
- `bun run test:full` for the maintained source test surface
|
||||||
|
- `bun run test:env` for environment-specific verification
|
||||||
|
- any important notes about sqlite-gated tests and launcher/plugin checks
|
||||||
|
|
||||||
|
Keep the matrix concrete and reproducible.
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `grep -n "Testing" README.md && grep -n "test:full" README.md && grep -n "test:env" README.md`
|
||||||
|
Expected: PASS with the new contributor-facing matrix present.
|
||||||
|
|
||||||
|
### Task 4: Verify representative omitted suites now belong to automated lanes
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Test: `src/main-entry-runtime.test.ts`
|
||||||
|
- Test: `src/anki-integration/anki-connect-proxy.test.ts`
|
||||||
|
- Test: `src/main/runtime/registry.test.ts`
|
||||||
|
- Reference: `package.json`
|
||||||
|
- Reference: `README.md`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Use targeted command checks to prove these previously omitted surfaces are now in the matrix:
|
||||||
|
|
||||||
|
- entry/runtime: `src/main-entry-runtime.test.ts`
|
||||||
|
- Anki integration: `src/anki-integration/anki-connect-proxy.test.ts`
|
||||||
|
- main runtime: `src/main/runtime/registry.test.ts`
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun run test:full src/main-entry-runtime.test.ts`
|
||||||
|
Expected: either unsupported invocation or evidence that the current matrix still does not include these surfaces automatically.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Adjust the final script paths/globs until the full matrix includes those representative surfaces without file-by-file script maintenance.
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `bun test src/main-entry-runtime.test.ts src/anki-integration/anki-connect-proxy.test.ts src/main/runtime/registry.test.ts && bun run test:fast && bun run test:full`
|
||||||
|
Expected: PASS, with at least one representative test from each required surface executing through the documented automated lanes.
|
||||||
@@ -16,11 +16,7 @@ import { generateYoutubeSubtitles } from '../youtube.js';
|
|||||||
import type { Args } from '../types.js';
|
import type { Args } from '../types.js';
|
||||||
import type { LauncherCommandContext } from './context.js';
|
import type { LauncherCommandContext } from './context.js';
|
||||||
import { ensureLauncherSetupReady } from '../setup-gate.js';
|
import { ensureLauncherSetupReady } from '../setup-gate.js';
|
||||||
import {
|
import { getDefaultConfigDir, getSetupStatePath, readSetupState } from '../../src/shared/setup-state.js';
|
||||||
getDefaultConfigDir,
|
|
||||||
getSetupStatePath,
|
|
||||||
readSetupState,
|
|
||||||
} from '../../src/shared/setup-state.js';
|
|
||||||
|
|
||||||
const SETUP_WAIT_TIMEOUT_MS = 10 * 60 * 1000;
|
const SETUP_WAIT_TIMEOUT_MS = 10 * 60 * 1000;
|
||||||
const SETUP_POLL_INTERVAL_MS = 500;
|
const SETUP_POLL_INTERVAL_MS = 500;
|
||||||
|
|||||||
@@ -4,13 +4,6 @@ import fs from 'node:fs';
|
|||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { spawn, spawnSync } from 'node:child_process';
|
import { spawn, spawnSync } from 'node:child_process';
|
||||||
import {
|
|
||||||
createDefaultSetupState,
|
|
||||||
getDefaultConfigDir,
|
|
||||||
getSetupStatePath,
|
|
||||||
readSetupState,
|
|
||||||
writeSetupState,
|
|
||||||
} from '../src/shared/setup-state.js';
|
|
||||||
|
|
||||||
type RunResult = {
|
type RunResult = {
|
||||||
status: number | null;
|
status: number | null;
|
||||||
@@ -65,13 +58,6 @@ function createSmokeCase(name: string): SmokeCase {
|
|||||||
`socket_path=${socketPath}\n`,
|
`socket_path=${socketPath}\n`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const configDir = getDefaultConfigDir({ xdgConfigHome, homeDir });
|
|
||||||
const setupState = createDefaultSetupState();
|
|
||||||
setupState.status = 'completed';
|
|
||||||
setupState.completedAt = '2026-03-07T00:00:00.000Z';
|
|
||||||
setupState.completionSource = 'user';
|
|
||||||
writeSetupState(getSetupStatePath(configDir), setupState);
|
|
||||||
|
|
||||||
const fakeMpvLogPath = path.join(artifactsDir, 'fake-mpv.log');
|
const fakeMpvLogPath = path.join(artifactsDir, 'fake-mpv.log');
|
||||||
const fakeAppLogPath = path.join(artifactsDir, 'fake-app.log');
|
const fakeAppLogPath = path.join(artifactsDir, 'fake-app.log');
|
||||||
const fakeAppStartLogPath = path.join(artifactsDir, 'fake-app-start.log');
|
const fakeAppStartLogPath = path.join(artifactsDir, 'fake-app-start.log');
|
||||||
@@ -238,22 +224,6 @@ async function waitForJsonLines(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test('launcher smoke fixture seeds completed setup state', () => {
|
|
||||||
const smokeCase = createSmokeCase('setup-state');
|
|
||||||
try {
|
|
||||||
const configDir = getDefaultConfigDir({
|
|
||||||
xdgConfigHome: smokeCase.xdgConfigHome,
|
|
||||||
homeDir: smokeCase.homeDir,
|
|
||||||
});
|
|
||||||
const statePath = getSetupStatePath(configDir);
|
|
||||||
|
|
||||||
assert.equal(readSetupState(statePath)?.status, 'completed');
|
|
||||||
} finally {
|
|
||||||
fs.rmSync(smokeCase.root, { recursive: true, force: true });
|
|
||||||
fs.rmSync(smokeCase.socketDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('launcher mpv status returns ready when socket is connectable', async () => {
|
test('launcher mpv status returns ready when socket is connectable', async () => {
|
||||||
await withSmokeCase('mpv-status', async (smokeCase) => {
|
await withSmokeCase('mpv-status', async (smokeCase) => {
|
||||||
const env = makeTestEnv(smokeCase);
|
const env = makeTestEnv(smokeCase);
|
||||||
|
|||||||
17
package.json
17
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "subminer",
|
"name": "subminer",
|
||||||
"version": "0.4.0",
|
"version": "0.3.0",
|
||||||
"description": "All-in-one sentence mining overlay with AnkiConnect and dictionary integration",
|
"description": "All-in-one sentence mining overlay with AnkiConnect and dictionary integration",
|
||||||
"packageManager": "bun@1.3.5",
|
"packageManager": "bun@1.3.5",
|
||||||
"main": "dist/main-entry.js",
|
"main": "dist/main-entry.js",
|
||||||
@@ -8,24 +8,21 @@
|
|||||||
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
|
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
|
||||||
"typecheck:watch": "tsc --watch --preserveWatchOutput -p tsconfig.typecheck.json",
|
"typecheck:watch": "tsc --watch --preserveWatchOutput -p tsconfig.typecheck.json",
|
||||||
"get-frequency": "bun run scripts/get_frequency.ts --pretty --color-top-x 10000 --yomitan-user-data ~/.config/SubMiner --colorized-line",
|
"get-frequency": "bun run scripts/get_frequency.ts --pretty --color-top-x 10000 --yomitan-user-data ~/.config/SubMiner --colorized-line",
|
||||||
"get-frequency:electron": "bun run build:yomitan && bun build scripts/get_frequency.ts --format=cjs --target=node --outfile dist/scripts/get_frequency.js --external electron && electron dist/scripts/get_frequency.js --pretty --color-top-x 10000 --yomitan-user-data ~/.config/SubMiner --colorized-line",
|
"get-frequency:electron": "bun build scripts/get_frequency.ts --format=cjs --target=node --outfile dist/scripts/get_frequency.js --external electron && electron dist/scripts/get_frequency.js --pretty --color-top-x 10000 --yomitan-user-data ~/.config/SubMiner --colorized-line",
|
||||||
"test-yomitan-parser": "bun run scripts/test-yomitan-parser.ts",
|
"test-yomitan-parser": "bun run scripts/test-yomitan-parser.ts",
|
||||||
"test-yomitan-parser:electron": "bun run build:yomitan && bun build scripts/test-yomitan-parser.ts --format=cjs --target=node --outfile dist/scripts/test-yomitan-parser.js --external electron && electron dist/scripts/test-yomitan-parser.js",
|
"test-yomitan-parser:electron": "bun build scripts/test-yomitan-parser.ts --format=cjs --target=node --outfile dist/scripts/test-yomitan-parser.js --external electron && electron dist/scripts/test-yomitan-parser.js",
|
||||||
"build:yomitan": "node scripts/build-yomitan.mjs",
|
"build": "tsc -p tsconfig.json && bun run build:renderer && cp src/renderer/index.html src/renderer/style.css dist/renderer/ && cp -r src/renderer/fonts dist/renderer/ && bash scripts/build-macos-helper.sh",
|
||||||
"build": "bun run build:yomitan && tsc -p tsconfig.json && bun run build:renderer && cp src/renderer/index.html src/renderer/style.css dist/renderer/ && cp -r src/renderer/fonts dist/renderer/ && bash scripts/build-macos-helper.sh",
|
|
||||||
"build:renderer": "esbuild src/renderer/renderer.ts --bundle --platform=browser --format=esm --target=es2022 --outfile=dist/renderer/renderer.js --sourcemap",
|
"build:renderer": "esbuild src/renderer/renderer.ts --bundle --platform=browser --format=esm --target=es2022 --outfile=dist/renderer/renderer.js --sourcemap",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check .",
|
||||||
"format:src": "bash scripts/prettier-scope.sh --write",
|
|
||||||
"format:check:src": "bash scripts/prettier-scope.sh --check",
|
|
||||||
"test:config:src": "bun test src/config/config.test.ts src/config/path-resolution.test.ts src/config/resolve/anki-connect.test.ts src/config/resolve/subtitle-style.test.ts src/config/resolve/jellyfin.test.ts src/config/definitions/domain-registry.test.ts src/generate-config-example.test.ts",
|
"test:config:src": "bun test src/config/config.test.ts src/config/path-resolution.test.ts src/config/resolve/anki-connect.test.ts src/config/resolve/subtitle-style.test.ts src/config/resolve/jellyfin.test.ts src/config/definitions/domain-registry.test.ts src/generate-config-example.test.ts",
|
||||||
"test:config:dist": "bun test dist/config/config.test.js dist/config/path-resolution.test.js dist/config/resolve/anki-connect.test.js dist/config/resolve/subtitle-style.test.js dist/config/resolve/jellyfin.test.js dist/config/definitions/domain-registry.test.js dist/generate-config-example.test.js",
|
"test:config:dist": "bun test dist/config/config.test.js dist/config/path-resolution.test.js dist/config/resolve/anki-connect.test.js dist/config/resolve/subtitle-style.test.js dist/config/resolve/jellyfin.test.js dist/config/definitions/domain-registry.test.js dist/generate-config-example.test.js",
|
||||||
"test:config:smoke:dist": "bun test dist/config/path-resolution.test.js",
|
"test:config:smoke:dist": "bun test dist/config/path-resolution.test.js",
|
||||||
"test:plugin:src": "lua scripts/test-plugin-start-gate.lua",
|
"test:plugin:src": "lua scripts/test-plugin-start-gate.lua",
|
||||||
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
||||||
"test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/smoke.e2e.test.ts && bun run test:plugin:src",
|
"test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/smoke.e2e.test.ts && bun run test:plugin:src",
|
||||||
"test:core:src": "bun test src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/core/services/cli-command.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/utils/shortcut-config.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/x11-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/setup-gate.test.ts",
|
"test:core:src": "bun test src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/core/services/cli-command.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/utils/shortcut-config.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/x11-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/setup-gate.test.ts",
|
||||||
"test:core:dist": "bun test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/yomitan-extension-paths.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
"test:core:dist": "bun test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
||||||
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
||||||
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
|
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
|
||||||
"test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts",
|
"test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts",
|
||||||
@@ -120,7 +117,7 @@
|
|||||||
],
|
],
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
{
|
{
|
||||||
"from": "build/yomitan",
|
"from": "vendor/yomitan",
|
||||||
"to": "yomitan"
|
"to": "yomitan"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
import fs from 'node:fs';
|
|
||||||
import os from 'node:os';
|
|
||||||
import path from 'node:path';
|
|
||||||
import { createHash } from 'node:crypto';
|
|
||||||
import { execFileSync } from 'node:child_process';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
|
|
||||||
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
||||||
const repoRoot = path.resolve(dirname, '..');
|
|
||||||
const submoduleDir = path.join(repoRoot, 'vendor', 'subminer-yomitan');
|
|
||||||
const submodulePackagePath = path.join(submoduleDir, 'package.json');
|
|
||||||
const submodulePackageLockPath = path.join(submoduleDir, 'package-lock.json');
|
|
||||||
const buildOutputDir = path.join(repoRoot, 'build', 'yomitan');
|
|
||||||
const stampPath = path.join(buildOutputDir, '.subminer-build.json');
|
|
||||||
const zipPath = path.join(submoduleDir, 'builds', 'yomitan-chrome.zip');
|
|
||||||
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
||||||
const dependencyStampPath = path.join(submoduleDir, 'node_modules', '.subminer-package-lock-hash');
|
|
||||||
|
|
||||||
function run(command, args, cwd) {
|
|
||||||
execFileSync(command, args, { cwd, stdio: 'inherit' });
|
|
||||||
}
|
|
||||||
|
|
||||||
function readCommand(command, args, cwd) {
|
|
||||||
return execFileSync(command, args, { cwd, encoding: 'utf8' }).trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function readStamp() {
|
|
||||||
try {
|
|
||||||
return JSON.parse(fs.readFileSync(stampPath, 'utf8'));
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hashFile(filePath) {
|
|
||||||
const hash = createHash('sha256');
|
|
||||||
hash.update(fs.readFileSync(filePath));
|
|
||||||
return hash.digest('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureSubmodulePresent() {
|
|
||||||
if (!fs.existsSync(submodulePackagePath)) {
|
|
||||||
throw new Error(
|
|
||||||
'Missing vendor/subminer-yomitan submodule. Run `git submodule update --init --recursive`.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSourceState() {
|
|
||||||
const revision = readCommand('git', ['rev-parse', 'HEAD'], submoduleDir);
|
|
||||||
const dirty = readCommand('git', ['status', '--short', '--untracked-files=no'], submoduleDir);
|
|
||||||
return { revision, dirty };
|
|
||||||
}
|
|
||||||
|
|
||||||
function isBuildCurrent(force) {
|
|
||||||
if (force) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!fs.existsSync(path.join(buildOutputDir, 'manifest.json'))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stamp = readStamp();
|
|
||||||
if (!stamp) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentState = getSourceState();
|
|
||||||
return stamp.revision === currentState.revision && stamp.dirty === currentState.dirty;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureDependenciesInstalled() {
|
|
||||||
const nodeModulesDir = path.join(submoduleDir, 'node_modules');
|
|
||||||
const currentLockHash = hashFile(submodulePackageLockPath);
|
|
||||||
let installedLockHash = '';
|
|
||||||
try {
|
|
||||||
installedLockHash = fs.readFileSync(dependencyStampPath, 'utf8').trim();
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
if (!fs.existsSync(nodeModulesDir) || installedLockHash !== currentLockHash) {
|
|
||||||
run(npmCommand, ['ci'], submoduleDir);
|
|
||||||
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
||||||
fs.writeFileSync(dependencyStampPath, `${currentLockHash}\n`, 'utf8');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function installAndBuild() {
|
|
||||||
ensureDependenciesInstalled();
|
|
||||||
run(npmCommand, ['run', 'build', '--', '--target', 'chrome'], submoduleDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractBuild() {
|
|
||||||
if (!fs.existsSync(zipPath)) {
|
|
||||||
throw new Error(`Expected Yomitan build artifact at ${zipPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-yomitan-'));
|
|
||||||
try {
|
|
||||||
run('unzip', ['-qo', zipPath, '-d', tempDir], repoRoot);
|
|
||||||
fs.rmSync(buildOutputDir, { recursive: true, force: true });
|
|
||||||
fs.mkdirSync(path.dirname(buildOutputDir), { recursive: true });
|
|
||||||
fs.cpSync(tempDir, buildOutputDir, { recursive: true });
|
|
||||||
if (!fs.existsSync(path.join(buildOutputDir, 'manifest.json'))) {
|
|
||||||
throw new Error(`Extracted Yomitan build missing manifest.json in ${buildOutputDir}`);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeStamp() {
|
|
||||||
const state = getSourceState();
|
|
||||||
fs.writeFileSync(
|
|
||||||
stampPath,
|
|
||||||
`${JSON.stringify(
|
|
||||||
{
|
|
||||||
revision: state.revision,
|
|
||||||
dirty: state.dirty,
|
|
||||||
builtAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)}\n`,
|
|
||||||
'utf8',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
const force = process.argv.includes('--force');
|
|
||||||
ensureSubmodulePresent();
|
|
||||||
|
|
||||||
if (isBuildCurrent(force)) {
|
|
||||||
process.stdout.write(`Yomitan build current: ${buildOutputDir}\n`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
process.stdout.write('Building Yomitan Chrome artifact...\n');
|
|
||||||
installAndBuild();
|
|
||||||
extractBuild();
|
|
||||||
writeStamp();
|
|
||||||
process.stdout.write(`Yomitan extracted to ${buildOutputDir}\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -4,7 +4,6 @@ import process from 'node:process';
|
|||||||
|
|
||||||
import { createTokenizerDepsRuntime, tokenizeSubtitle } from '../src/core/services/tokenizer.js';
|
import { createTokenizerDepsRuntime, tokenizeSubtitle } from '../src/core/services/tokenizer.js';
|
||||||
import { createFrequencyDictionaryLookup } from '../src/core/services/frequency-dictionary.js';
|
import { createFrequencyDictionaryLookup } from '../src/core/services/frequency-dictionary.js';
|
||||||
import { resolveYomitanExtensionPath as resolveBuiltYomitanExtensionPath } from '../src/core/services/yomitan-extension-paths.js';
|
|
||||||
import { MecabTokenizer } from '../src/mecab-tokenizer.js';
|
import { MecabTokenizer } from '../src/mecab-tokenizer.js';
|
||||||
import type { MergedToken, FrequencyDictionaryLookup } from '../src/types.js';
|
import type { MergedToken, FrequencyDictionaryLookup } from '../src/types.js';
|
||||||
|
|
||||||
@@ -95,7 +94,7 @@ function parseCliArgs(argv: string[]): CliOptions {
|
|||||||
if (!next) {
|
if (!next) {
|
||||||
throw new Error('Missing value for --yomitan-extension');
|
throw new Error('Missing value for --yomitan-extension');
|
||||||
}
|
}
|
||||||
yomitanExtensionPath = next;
|
yomitanExtensionPath = path.resolve(next);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +103,7 @@ function parseCliArgs(argv: string[]): CliOptions {
|
|||||||
if (!next) {
|
if (!next) {
|
||||||
throw new Error('Missing value for --yomitan-user-data');
|
throw new Error('Missing value for --yomitan-user-data');
|
||||||
}
|
}
|
||||||
yomitanUserDataPath = next;
|
yomitanUserDataPath = path.resolve(next);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,12 +225,12 @@ function parseCliArgs(argv: string[]): CliOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (arg.startsWith('--yomitan-extension=')) {
|
if (arg.startsWith('--yomitan-extension=')) {
|
||||||
yomitanExtensionPath = arg.slice('--yomitan-extension='.length);
|
yomitanExtensionPath = path.resolve(arg.slice('--yomitan-extension='.length));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (arg.startsWith('--yomitan-user-data=')) {
|
if (arg.startsWith('--yomitan-user-data=')) {
|
||||||
yomitanUserDataPath = arg.slice('--yomitan-user-data='.length);
|
yomitanUserDataPath = path.resolve(arg.slice('--yomitan-user-data='.length));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,10 +524,7 @@ function destroyUnknownParserWindow(window: unknown): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createYomitanRuntimeState(
|
async function createYomitanRuntimeState(userDataPath: string): Promise<YomitanRuntimeState> {
|
||||||
userDataPath: string,
|
|
||||||
extensionPath?: string,
|
|
||||||
): Promise<YomitanRuntimeState> {
|
|
||||||
const state: YomitanRuntimeState = {
|
const state: YomitanRuntimeState = {
|
||||||
yomitanExt: null,
|
yomitanExt: null,
|
||||||
parserWindow: null,
|
parserWindow: null,
|
||||||
@@ -551,7 +547,6 @@ async function createYomitanRuntimeState(
|
|||||||
const loadYomitanExtension = (await import('../src/core/services/yomitan-extension-loader.js'))
|
const loadYomitanExtension = (await import('../src/core/services/yomitan-extension-loader.js'))
|
||||||
.loadYomitanExtension as (options: {
|
.loadYomitanExtension as (options: {
|
||||||
userDataPath: string;
|
userDataPath: string;
|
||||||
extensionPath?: string;
|
|
||||||
getYomitanParserWindow: () => unknown;
|
getYomitanParserWindow: () => unknown;
|
||||||
setYomitanParserWindow: (window: unknown) => void;
|
setYomitanParserWindow: (window: unknown) => void;
|
||||||
setYomitanParserReadyPromise: (promise: Promise<void> | null) => void;
|
setYomitanParserReadyPromise: (promise: Promise<void> | null) => void;
|
||||||
@@ -561,7 +556,6 @@ async function createYomitanRuntimeState(
|
|||||||
|
|
||||||
const extension = await loadYomitanExtension({
|
const extension = await loadYomitanExtension({
|
||||||
userDataPath,
|
userDataPath,
|
||||||
extensionPath,
|
|
||||||
getYomitanParserWindow: () => state.parserWindow,
|
getYomitanParserWindow: () => state.parserWindow,
|
||||||
setYomitanParserWindow: (window) => {
|
setYomitanParserWindow: (window) => {
|
||||||
state.parserWindow = window;
|
state.parserWindow = window;
|
||||||
@@ -595,16 +589,17 @@ async function createYomitanRuntimeStateWithSearch(
|
|||||||
userDataPath: string,
|
userDataPath: string,
|
||||||
extensionPath?: string,
|
extensionPath?: string,
|
||||||
): Promise<YomitanRuntimeState> {
|
): Promise<YomitanRuntimeState> {
|
||||||
const resolvedExtensionPath = resolveBuiltYomitanExtensionPath({
|
const preferredPath = extensionPath ? path.resolve(extensionPath) : undefined;
|
||||||
explicitPath: extensionPath,
|
const defaultVendorPath = path.resolve(process.cwd(), 'vendor', 'yomitan');
|
||||||
cwd: process.cwd(),
|
const candidates = [...(preferredPath ? [preferredPath] : []), defaultVendorPath];
|
||||||
});
|
|
||||||
const candidates = resolvedExtensionPath ? [resolvedExtensionPath] : [];
|
|
||||||
|
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
|
if (!candidate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(path.join(candidate, 'manifest.json'))) {
|
if (fs.existsSync(path.join(candidate, 'manifest.json'))) {
|
||||||
const state = await createYomitanRuntimeState(userDataPath, candidate);
|
const state = await createYomitanRuntimeState(userDataPath);
|
||||||
if (state.available) {
|
if (state.available) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -618,7 +613,7 @@ async function createYomitanRuntimeStateWithSearch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return createYomitanRuntimeState(userDataPath, resolvedExtensionPath ?? undefined);
|
return createYomitanRuntimeState(userDataPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFrequencyLookup(dictionaryPath: string): Promise<FrequencyDictionaryLookup> {
|
async function getFrequencyLookup(dictionaryPath: string): Promise<FrequencyDictionaryLookup> {
|
||||||
|
|||||||
@@ -1,16 +1,287 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# SubMiner - All-in-one sentence mining overlay
|
||||||
|
# Copyright (C) 2024 sudacode
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# patch-yomitan.sh - Apply Electron compatibility patches to Yomitan
|
||||||
|
#
|
||||||
|
# This script applies the necessary patches to make Yomitan work in Electron
|
||||||
|
# after upgrading to a new version. Run this after extracting a fresh Yomitan release.
|
||||||
|
#
|
||||||
|
# Usage: ./patch-yomitan.sh [yomitan_dir]
|
||||||
|
# yomitan_dir: Path to the Yomitan directory (default: vendor/yomitan)
|
||||||
|
#
|
||||||
|
|
||||||
set -euo pipefail
|
set -e
|
||||||
|
|
||||||
cat <<'EOF'
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
patch-yomitan.sh is retired.
|
YOMITAN_DIR="${1:-$SCRIPT_DIR/../vendor/yomitan}"
|
||||||
|
YOMITAN_MANIFEST_PATH="$YOMITAN_DIR/manifest.json"
|
||||||
|
|
||||||
SubMiner now uses the forked source submodule at vendor/subminer-yomitan and builds the
|
if [ ! -d "$YOMITAN_DIR" ]; then
|
||||||
Chromium extension artifact into build/yomitan.
|
echo "Error: Yomitan directory not found: $YOMITAN_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
Use:
|
if [ ! -f "$YOMITAN_MANIFEST_PATH" ]; then
|
||||||
git submodule update --init --recursive
|
echo "Error: manifest.json not found at $YOMITAN_MANIFEST_PATH"
|
||||||
bun run build:yomitan
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
If you need to change Electron compatibility behavior, patch the forked source repo and rebuild.
|
echo "Patching manifest.json..."
|
||||||
EOF
|
if node - "$YOMITAN_MANIFEST_PATH" <<'PATCH_EOF'
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = process.argv[2];
|
||||||
|
const manifest = JSON.parse(fs.readFileSync(path, 'utf8'));
|
||||||
|
const stableKey = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxclvOy2sunfRa2UeSV/L9xyuMR9V65z85mbKCy0XvSLUkTBXM8BnvnrDu1DHhLjpidA3cBtetVt7rzwsJSA6/CzlMmtG6L6//3MOAH5Mhng8tXXWXbuNuJobLv/7MORPqoqYKZuoL1bnUvjdrf4Pb3BBDZtHN8LcDz13gOO4dnEFQbSE4F5RQ4mIQAGMkmbmlJkwFk5I022XyX+cWm/+9VvwPuEDA1Qf7X1G+4use3hGYWVPcRb6xTp7swXsO/fP7auE51gYQD0Ht36wr32UR6lfRmsahbHOX4RLe36S8B4ee74kk5C8iCsZf2fidWmevzLk7kK0GW15pv3dpGFpPQIDAQAB';
|
||||||
|
if (manifest.key === stableKey) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
manifest.key = stableKey;
|
||||||
|
fs.writeFileSync(path, `${JSON.stringify(manifest, null, 4)}\n`, 'utf8');
|
||||||
|
process.exit(0);
|
||||||
|
PATCH_EOF
|
||||||
|
then
|
||||||
|
echo " - Set stable manifest key in manifest.json"
|
||||||
|
else
|
||||||
|
echo " - Failed to patch manifest.json"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Patching Yomitan in: $YOMITAN_DIR"
|
||||||
|
|
||||||
|
PERMISSIONS_UTIL="$YOMITAN_DIR/js/data/permissions-util.js"
|
||||||
|
|
||||||
|
if [ ! -f "$PERMISSIONS_UTIL" ]; then
|
||||||
|
echo "Error: permissions-util.js not found at $PERMISSIONS_UTIL"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Patching permissions-util.js..."
|
||||||
|
|
||||||
|
if grep -q "Electron workaround" "$PERMISSIONS_UTIL"; then
|
||||||
|
echo " - Already patched, skipping"
|
||||||
|
else
|
||||||
|
cat > "$PERMISSIONS_UTIL.tmp" << 'PATCH_EOF'
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2023-2025 Yomitan Authors
|
||||||
|
* Copyright (C) 2021-2022 Yomichan Authors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {getFieldMarkers} from './anki-util.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function returns whether an Anki field marker might require clipboard permissions.
|
||||||
|
* This is speculative and may not guarantee that the field marker actually does require the permission,
|
||||||
|
* as the custom handlebars template is not deeply inspected.
|
||||||
|
* @param {string} marker
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function ankiFieldMarkerMayUseClipboard(marker) {
|
||||||
|
switch (marker) {
|
||||||
|
case 'clipboard-image':
|
||||||
|
case 'clipboard-text':
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {chrome.permissions.Permissions} permissions
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
export function hasPermissions(permissions) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.permissions.contains(permissions, (result) => {
|
||||||
|
const e = chrome.runtime.lastError;
|
||||||
|
if (e) {
|
||||||
|
reject(new Error(e.message));
|
||||||
|
} else {
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {chrome.permissions.Permissions} permissions
|
||||||
|
* @param {boolean} shouldHave
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
export function setPermissionsGranted(permissions, shouldHave) {
|
||||||
|
return (
|
||||||
|
shouldHave ?
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
chrome.permissions.request(permissions, (result) => {
|
||||||
|
const e = chrome.runtime.lastError;
|
||||||
|
if (e) {
|
||||||
|
reject(new Error(e.message));
|
||||||
|
} else {
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}) :
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
chrome.permissions.remove(permissions, (result) => {
|
||||||
|
const e = chrome.runtime.lastError;
|
||||||
|
if (e) {
|
||||||
|
reject(new Error(e.message));
|
||||||
|
} else {
|
||||||
|
resolve(!result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<chrome.permissions.Permissions>}
|
||||||
|
*/
|
||||||
|
export function getAllPermissions() {
|
||||||
|
// Electron workaround - chrome.permissions.getAll() not available
|
||||||
|
return Promise.resolve({
|
||||||
|
origins: ["<all_urls>"],
|
||||||
|
permissions: ["clipboardWrite", "storage", "unlimitedStorage", "scripting", "contextMenus"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} fieldValue
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
export function getRequiredPermissionsForAnkiFieldValue(fieldValue) {
|
||||||
|
const markers = getFieldMarkers(fieldValue);
|
||||||
|
for (const marker of markers) {
|
||||||
|
if (ankiFieldMarkerMayUseClipboard(marker)) {
|
||||||
|
return ['clipboardRead'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {chrome.permissions.Permissions} permissions
|
||||||
|
* @param {import('settings').ProfileOptions} options
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function hasRequiredPermissionsForOptions(permissions, options) {
|
||||||
|
const permissionsSet = new Set(permissions.permissions);
|
||||||
|
|
||||||
|
if (!permissionsSet.has('nativeMessaging') && (options.parsing.enableMecabParser || options.general.enableYomitanApi)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!permissionsSet.has('clipboardRead')) {
|
||||||
|
if (options.clipboard.enableBackgroundMonitor || options.clipboard.enableSearchPageMonitor) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const fieldsList = options.anki.cardFormats.map((cardFormat) => cardFormat.fields);
|
||||||
|
|
||||||
|
for (const fields of fieldsList) {
|
||||||
|
for (const {value: fieldValue} of Object.values(fields)) {
|
||||||
|
const markers = getFieldMarkers(fieldValue);
|
||||||
|
for (const marker of markers) {
|
||||||
|
if (ankiFieldMarkerMayUseClipboard(marker)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
PATCH_EOF
|
||||||
|
|
||||||
|
mv "$PERMISSIONS_UTIL.tmp" "$PERMISSIONS_UTIL"
|
||||||
|
echo " - Patched successfully"
|
||||||
|
fi
|
||||||
|
|
||||||
|
OPTIONS_SCHEMA="$YOMITAN_DIR/data/schemas/options-schema.json"
|
||||||
|
|
||||||
|
if [ ! -f "$OPTIONS_SCHEMA" ]; then
|
||||||
|
echo "Error: options-schema.json not found at $OPTIONS_SCHEMA"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Patching options-schema.json..."
|
||||||
|
|
||||||
|
if grep -q '"selectText".*"default": true' "$OPTIONS_SCHEMA"; then
|
||||||
|
sed -i '/"selectText": {/,/"default":/{s/"default": true/"default": false/}' "$OPTIONS_SCHEMA"
|
||||||
|
echo " - Changed selectText default to false"
|
||||||
|
elif grep -q '"selectText".*"default": false' "$OPTIONS_SCHEMA"; then
|
||||||
|
echo " - selectText already set to false, skipping"
|
||||||
|
else
|
||||||
|
echo " - Warning: Could not find selectText setting"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q '"layoutAwareScan".*"default": true' "$OPTIONS_SCHEMA"; then
|
||||||
|
sed -i '/"layoutAwareScan": {/,/"default":/{s/"default": true/"default": false/}' "$OPTIONS_SCHEMA"
|
||||||
|
echo " - Changed layoutAwareScan default to false"
|
||||||
|
elif grep -q '"layoutAwareScan".*"default": false' "$OPTIONS_SCHEMA"; then
|
||||||
|
echo " - layoutAwareScan already set to false, skipping"
|
||||||
|
else
|
||||||
|
echo " - Warning: Could not find layoutAwareScan setting"
|
||||||
|
fi
|
||||||
|
|
||||||
|
POPUP_JS="$YOMITAN_DIR/js/app/popup.js"
|
||||||
|
|
||||||
|
if [ ! -f "$POPUP_JS" ]; then
|
||||||
|
echo "Error: popup.js not found at $POPUP_JS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Patching popup.js..."
|
||||||
|
|
||||||
|
if grep -q "yomitan-popup-shown" "$POPUP_JS"; then
|
||||||
|
echo " - Already patched, skipping"
|
||||||
|
else
|
||||||
|
# Add the visibility event dispatch after the existing _onVisibleChange code
|
||||||
|
# We need to add it after: void this._invokeSafe('displayVisibilityChanged', {value});
|
||||||
|
sed -i "/void this._invokeSafe('displayVisibilityChanged', {value});/a\\
|
||||||
|
\\
|
||||||
|
// Dispatch custom events for popup visibility (Electron integration)\\
|
||||||
|
if (value) {\\
|
||||||
|
window.dispatchEvent(new CustomEvent('yomitan-popup-shown'));\\
|
||||||
|
} else {\\
|
||||||
|
window.dispatchEvent(new CustomEvent('yomitan-popup-hidden'));\\
|
||||||
|
}" "$POPUP_JS"
|
||||||
|
echo " - Added visibility events"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Yomitan patching complete!"
|
||||||
|
echo ""
|
||||||
|
echo "Changes applied:"
|
||||||
|
echo " 1. permissions-util.js: Hardcoded permissions (Electron workaround)"
|
||||||
|
echo " 2. options-schema.json: selectText=false, layoutAwareScan=false"
|
||||||
|
echo " 3. popup.js: Added yomitan-popup-shown/hidden events"
|
||||||
|
echo ""
|
||||||
|
echo "To verify: Run 'bun run dev' and check for 'Yomitan extension loaded successfully'"
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
cd "$ROOT_DIR"
|
|
||||||
|
|
||||||
paths=(
|
|
||||||
"package.json"
|
|
||||||
"tsconfig.json"
|
|
||||||
"tsconfig.renderer.json"
|
|
||||||
"tsconfig.typecheck.json"
|
|
||||||
".prettierrc.json"
|
|
||||||
".github"
|
|
||||||
"build"
|
|
||||||
"launcher"
|
|
||||||
"scripts"
|
|
||||||
"src"
|
|
||||||
)
|
|
||||||
|
|
||||||
exec bunx prettier "$@" "${paths[@]}"
|
|
||||||
@@ -4,7 +4,6 @@ import path from 'node:path';
|
|||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
|
|
||||||
import { createTokenizerDepsRuntime, tokenizeSubtitle } from '../src/core/services/tokenizer.js';
|
import { createTokenizerDepsRuntime, tokenizeSubtitle } from '../src/core/services/tokenizer.js';
|
||||||
import { resolveYomitanExtensionPath as resolveBuiltYomitanExtensionPath } from '../src/core/services/yomitan-extension-paths.js';
|
|
||||||
import { MecabTokenizer } from '../src/mecab-tokenizer.js';
|
import { MecabTokenizer } from '../src/mecab-tokenizer.js';
|
||||||
import type { MergedToken } from '../src/types.js';
|
import type { MergedToken } from '../src/types.js';
|
||||||
|
|
||||||
@@ -113,12 +112,12 @@ function parseCliArgs(argv: string[]): CliOptions {
|
|||||||
if (!next) {
|
if (!next) {
|
||||||
throw new Error('Missing value for --yomitan-extension');
|
throw new Error('Missing value for --yomitan-extension');
|
||||||
}
|
}
|
||||||
yomitanExtensionPath = next;
|
yomitanExtensionPath = path.resolve(next);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (arg.startsWith('--yomitan-extension=')) {
|
if (arg.startsWith('--yomitan-extension=')) {
|
||||||
yomitanExtensionPath = arg.slice('--yomitan-extension='.length);
|
yomitanExtensionPath = path.resolve(arg.slice('--yomitan-extension='.length));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,12 +126,12 @@ function parseCliArgs(argv: string[]): CliOptions {
|
|||||||
if (!next) {
|
if (!next) {
|
||||||
throw new Error('Missing value for --yomitan-user-data');
|
throw new Error('Missing value for --yomitan-user-data');
|
||||||
}
|
}
|
||||||
yomitanUserDataPath = next;
|
yomitanUserDataPath = path.resolve(next);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (arg.startsWith('--yomitan-user-data=')) {
|
if (arg.startsWith('--yomitan-user-data=')) {
|
||||||
yomitanUserDataPath = arg.slice('--yomitan-user-data='.length);
|
yomitanUserDataPath = path.resolve(arg.slice('--yomitan-user-data='.length));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,10 +372,21 @@ function findSelectedCandidateIndexes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveYomitanExtensionPath(explicitPath?: string): string | null {
|
function resolveYomitanExtensionPath(explicitPath?: string): string | null {
|
||||||
return resolveBuiltYomitanExtensionPath({
|
const candidates = [
|
||||||
explicitPath,
|
explicitPath ? path.resolve(explicitPath) : null,
|
||||||
cwd: process.cwd(),
|
path.resolve(process.cwd(), 'vendor', 'yomitan'),
|
||||||
});
|
];
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (!candidate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (fs.existsSync(path.join(candidate, 'manifest.json'))) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupYomitanRuntime(options: CliOptions): Promise<YomitanRuntimeState> {
|
async function setupYomitanRuntime(options: CliOptions): Promise<YomitanRuntimeState> {
|
||||||
@@ -410,7 +420,7 @@ async function setupYomitanRuntime(options: CliOptions): Promise<YomitanRuntimeS
|
|||||||
|
|
||||||
const extensionPath = resolveYomitanExtensionPath(options.yomitanExtensionPath);
|
const extensionPath = resolveYomitanExtensionPath(options.yomitanExtensionPath);
|
||||||
if (!extensionPath) {
|
if (!extensionPath) {
|
||||||
state.note = 'no built Yomitan extension directory found; run `bun run build:yomitan`';
|
state.note = 'no Yomitan extension directory found';
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,10 +55,7 @@ test('AnkiIntegrationRuntime normalizes url and proxy defaults', () => {
|
|||||||
assert.equal(normalized.proxy?.host, '0.0.0.0');
|
assert.equal(normalized.proxy?.host, '0.0.0.0');
|
||||||
assert.equal(normalized.proxy?.port, 7001);
|
assert.equal(normalized.proxy?.port, 7001);
|
||||||
assert.equal(normalized.proxy?.upstreamUrl, 'http://anki.local:8765');
|
assert.equal(normalized.proxy?.upstreamUrl, 'http://anki.local:8765');
|
||||||
assert.equal(
|
assert.equal(normalized.media?.fallbackDuration, DEFAULT_ANKI_CONNECT_CONFIG.media.fallbackDuration);
|
||||||
normalized.media?.fallbackDuration,
|
|
||||||
DEFAULT_ANKI_CONNECT_CONFIG.media.fallbackDuration,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('AnkiIntegrationRuntime starts proxy transport when proxy mode is enabled', () => {
|
test('AnkiIntegrationRuntime starts proxy transport when proxy mode is enabled', () => {
|
||||||
@@ -73,7 +70,10 @@ test('AnkiIntegrationRuntime starts proxy transport when proxy mode is enabled',
|
|||||||
|
|
||||||
runtime.start();
|
runtime.start();
|
||||||
|
|
||||||
assert.deepEqual(calls, ['known:start', 'proxy:start:127.0.0.1:9999:http://upstream:8765']);
|
assert.deepEqual(calls, [
|
||||||
|
'known:start',
|
||||||
|
'proxy:start:127.0.0.1:9999:http://upstream:8765',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('AnkiIntegrationRuntime switches transports and clears known words when runtime patch disables highlighting', () => {
|
test('AnkiIntegrationRuntime switches transports and clears known words when runtime patch disables highlighting', () => {
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ function trimToNonEmptyString(value: unknown): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeAnkiIntegrationConfig(config: AnkiConnectConfig): AnkiConnectConfig {
|
export function normalizeAnkiIntegrationConfig(config: AnkiConnectConfig): AnkiConnectConfig {
|
||||||
const resolvedUrl = trimToNonEmptyString(config.url) ?? DEFAULT_ANKI_CONNECT_CONFIG.url;
|
const resolvedUrl =
|
||||||
|
trimToNonEmptyString(config.url) ?? DEFAULT_ANKI_CONNECT_CONFIG.url;
|
||||||
const proxySource =
|
const proxySource =
|
||||||
config.proxy && typeof config.proxy === 'object'
|
config.proxy && typeof config.proxy === 'object'
|
||||||
? (config.proxy as NonNullable<AnkiConnectConfig['proxy']>)
|
? (config.proxy as NonNullable<AnkiConnectConfig['proxy']>)
|
||||||
|
|||||||
@@ -166,7 +166,9 @@ test('parses texthooker.launchAtStartup and warns on invalid values', () => {
|
|||||||
DEFAULT_CONFIG.texthooker.launchAtStartup,
|
DEFAULT_CONFIG.texthooker.launchAtStartup,
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
invalidService.getWarnings().some((warning) => warning.path === 'texthooker.launchAtStartup'),
|
invalidService
|
||||||
|
.getWarnings()
|
||||||
|
.some((warning) => warning.path === 'texthooker.launchAtStartup'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -209,10 +211,14 @@ test('parses annotationWebsocket settings and warns on invalid values', () => {
|
|||||||
DEFAULT_CONFIG.annotationWebsocket.port,
|
DEFAULT_CONFIG.annotationWebsocket.port,
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
invalidService.getWarnings().some((warning) => warning.path === 'annotationWebsocket.enabled'),
|
invalidService
|
||||||
|
.getWarnings()
|
||||||
|
.some((warning) => warning.path === 'annotationWebsocket.enabled'),
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
invalidService.getWarnings().some((warning) => warning.path === 'annotationWebsocket.port'),
|
invalidService
|
||||||
|
.getWarnings()
|
||||||
|
.some((warning) => warning.path === 'annotationWebsocket.port'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -344,8 +350,8 @@ test('parses subtitleStyle.nameMatchColor and warns on invalid values', () => {
|
|||||||
|
|
||||||
const validService = new ConfigService(validDir);
|
const validService = new ConfigService(validDir);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
((validService.getConfig().subtitleStyle as unknown as Record<string, unknown>)
|
((validService.getConfig().subtitleStyle as unknown as Record<string, unknown>).nameMatchColor ??
|
||||||
.nameMatchColor ?? null) as string | null,
|
null) as string | null,
|
||||||
'#eed49f',
|
'#eed49f',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -367,7 +373,9 @@ test('parses subtitleStyle.nameMatchColor and warns on invalid values', () => {
|
|||||||
'#f5bde6',
|
'#f5bde6',
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
invalidService.getWarnings().some((warning) => warning.path === 'subtitleStyle.nameMatchColor'),
|
invalidService
|
||||||
|
.getWarnings()
|
||||||
|
.some((warning) => warning.path === 'subtitleStyle.nameMatchColor'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -497,16 +505,10 @@ test('parses anilist.characterDictionary config with clamping and enum validatio
|
|||||||
assert.equal(config.anilist.characterDictionary.maxLoaded, 20);
|
assert.equal(config.anilist.characterDictionary.maxLoaded, 20);
|
||||||
assert.equal(config.anilist.characterDictionary.evictionPolicy, 'delete');
|
assert.equal(config.anilist.characterDictionary.evictionPolicy, 'delete');
|
||||||
assert.equal(config.anilist.characterDictionary.profileScope, 'all');
|
assert.equal(config.anilist.characterDictionary.profileScope, 'all');
|
||||||
assert.ok(
|
assert.ok(warnings.some((warning) => warning.path === 'anilist.characterDictionary.refreshTtlHours'));
|
||||||
warnings.some((warning) => warning.path === 'anilist.characterDictionary.refreshTtlHours'),
|
|
||||||
);
|
|
||||||
assert.ok(warnings.some((warning) => warning.path === 'anilist.characterDictionary.maxLoaded'));
|
assert.ok(warnings.some((warning) => warning.path === 'anilist.characterDictionary.maxLoaded'));
|
||||||
assert.ok(
|
assert.ok(warnings.some((warning) => warning.path === 'anilist.characterDictionary.evictionPolicy'));
|
||||||
warnings.some((warning) => warning.path === 'anilist.characterDictionary.evictionPolicy'),
|
assert.ok(warnings.some((warning) => warning.path === 'anilist.characterDictionary.profileScope'));
|
||||||
);
|
|
||||||
assert.ok(
|
|
||||||
warnings.some((warning) => warning.path === 'anilist.characterDictionary.profileScope'),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('parses anilist.characterDictionary.collapsibleSections booleans and warns on invalid values', () => {
|
test('parses anilist.characterDictionary.collapsibleSections booleans and warns on invalid values', () => {
|
||||||
|
|||||||
@@ -175,8 +175,7 @@ export function buildIntegrationConfigOptionRegistry(
|
|||||||
path: 'anilist.characterDictionary.collapsibleSections.description',
|
path: 'anilist.characterDictionary.collapsibleSections.description',
|
||||||
kind: 'boolean',
|
kind: 'boolean',
|
||||||
defaultValue: defaultConfig.anilist.characterDictionary.collapsibleSections.description,
|
defaultValue: defaultConfig.anilist.characterDictionary.collapsibleSections.description,
|
||||||
description:
|
description: 'Open the Description section by default in character dictionary glossary entries.',
|
||||||
'Open the Description section by default in character dictionary glossary entries.',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'anilist.characterDictionary.collapsibleSections.characterInformation',
|
path: 'anilist.characterDictionary.collapsibleSections.characterInformation',
|
||||||
@@ -190,8 +189,7 @@ export function buildIntegrationConfigOptionRegistry(
|
|||||||
path: 'anilist.characterDictionary.collapsibleSections.voicedBy',
|
path: 'anilist.characterDictionary.collapsibleSections.voicedBy',
|
||||||
kind: 'boolean',
|
kind: 'boolean',
|
||||||
defaultValue: defaultConfig.anilist.characterDictionary.collapsibleSections.voicedBy,
|
defaultValue: defaultConfig.anilist.characterDictionary.collapsibleSections.voicedBy,
|
||||||
description:
|
description: 'Open the Voiced by section by default in character dictionary glossary entries.',
|
||||||
'Open the Voiced by section by default in character dictionary glossary entries.',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'jellyfin.enabled',
|
path: 'jellyfin.enabled',
|
||||||
|
|||||||
@@ -238,9 +238,7 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
|||||||
);
|
);
|
||||||
if (nameMatchEnabled !== undefined) {
|
if (nameMatchEnabled !== undefined) {
|
||||||
resolved.subtitleStyle.nameMatchEnabled = nameMatchEnabled;
|
resolved.subtitleStyle.nameMatchEnabled = nameMatchEnabled;
|
||||||
} else if (
|
} else if ((src.subtitleStyle as { nameMatchEnabled?: unknown }).nameMatchEnabled !== undefined) {
|
||||||
(src.subtitleStyle as { nameMatchEnabled?: unknown }).nameMatchEnabled !== undefined
|
|
||||||
) {
|
|
||||||
resolved.subtitleStyle.nameMatchEnabled = fallbackSubtitleStyleNameMatchEnabled;
|
resolved.subtitleStyle.nameMatchEnabled = fallbackSubtitleStyleNameMatchEnabled;
|
||||||
warn(
|
warn(
|
||||||
'subtitleStyle.nameMatchEnabled',
|
'subtitleStyle.nameMatchEnabled',
|
||||||
|
|||||||
@@ -99,7 +99,8 @@ test('runAppReadyRuntime starts texthooker on startup when enabled in config', a
|
|||||||
calls.indexOf('createMpvClient') < calls.indexOf('startTexthooker:5174:ws://127.0.0.1:6678'),
|
calls.indexOf('createMpvClient') < calls.indexOf('startTexthooker:5174:ws://127.0.0.1:6678'),
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
calls.indexOf('startTexthooker:5174:ws://127.0.0.1:6678') < calls.indexOf('handleInitialArgs'),
|
calls.indexOf('startTexthooker:5174:ws://127.0.0.1:6678') <
|
||||||
|
calls.indexOf('handleInitialArgs'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -261,8 +261,7 @@ export function handleCliCommand(
|
|||||||
|
|
||||||
const ignoreSecondInstanceStart =
|
const ignoreSecondInstanceStart =
|
||||||
source === 'second-instance' && args.start && deps.isOverlayRuntimeInitialized();
|
source === 'second-instance' && args.start && deps.isOverlayRuntimeInitialized();
|
||||||
const shouldStart =
|
const shouldStart = (!ignoreSecondInstanceStart && args.start) || args.toggle || args.toggleVisibleOverlay;
|
||||||
(!ignoreSecondInstanceStart && args.start) || args.toggle || args.toggleVisibleOverlay;
|
|
||||||
const needsOverlayRuntime = commandNeedsOverlayRuntime(args);
|
const needsOverlayRuntime = commandNeedsOverlayRuntime(args);
|
||||||
const shouldInitializeOverlayRuntime = needsOverlayRuntime || args.start;
|
const shouldInitializeOverlayRuntime = needsOverlayRuntime || args.start;
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ function createOptions(overrides: Partial<Parameters<typeof handleMpvCommandFrom
|
|||||||
mpvSendCommand: (command) => {
|
mpvSendCommand: (command) => {
|
||||||
sentCommands.push(command);
|
sentCommands.push(command);
|
||||||
},
|
},
|
||||||
resolveProxyCommandOsd: async () => null,
|
|
||||||
isMpvConnected: () => true,
|
isMpvConnected: () => true,
|
||||||
hasRuntimeOptionsManager: () => true,
|
hasRuntimeOptionsManager: () => true,
|
||||||
...overrides,
|
...overrides,
|
||||||
@@ -53,39 +52,30 @@ test('handleMpvCommandFromIpc forwards regular mpv commands', () => {
|
|||||||
assert.deepEqual(osd, []);
|
assert.deepEqual(osd, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleMpvCommandFromIpc emits osd for subtitle position keybinding proxies', async () => {
|
test('handleMpvCommandFromIpc emits osd for subtitle position keybinding proxies', () => {
|
||||||
const { options, sentCommands, osd } = createOptions();
|
const { options, sentCommands, osd } = createOptions();
|
||||||
handleMpvCommandFromIpc(['add', 'sub-pos', 1], options);
|
handleMpvCommandFromIpc(['add', 'sub-pos', 1], options);
|
||||||
await new Promise((resolve) => setImmediate(resolve));
|
|
||||||
assert.deepEqual(sentCommands, [['add', 'sub-pos', 1]]);
|
assert.deepEqual(sentCommands, [['add', 'sub-pos', 1]]);
|
||||||
assert.deepEqual(osd, ['Subtitle position: ${sub-pos}']);
|
assert.deepEqual(osd, ['Subtitle position: ${sub-pos}']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleMpvCommandFromIpc emits resolved osd for primary subtitle track keybinding proxies', async () => {
|
test('handleMpvCommandFromIpc emits osd for primary subtitle track keybinding proxies', () => {
|
||||||
const { options, sentCommands, osd } = createOptions({
|
const { options, sentCommands, osd } = createOptions();
|
||||||
resolveProxyCommandOsd: async () => 'Subtitle track: Internal #3 - Japanese (active)',
|
|
||||||
});
|
|
||||||
handleMpvCommandFromIpc(['cycle', 'sid'], options);
|
handleMpvCommandFromIpc(['cycle', 'sid'], options);
|
||||||
await new Promise((resolve) => setImmediate(resolve));
|
|
||||||
assert.deepEqual(sentCommands, [['cycle', 'sid']]);
|
assert.deepEqual(sentCommands, [['cycle', 'sid']]);
|
||||||
assert.deepEqual(osd, ['Subtitle track: Internal #3 - Japanese (active)']);
|
assert.deepEqual(osd, ['Subtitle track: ${sid}']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleMpvCommandFromIpc emits resolved osd for secondary subtitle track keybinding proxies', async () => {
|
test('handleMpvCommandFromIpc emits osd for secondary subtitle track keybinding proxies', () => {
|
||||||
const { options, sentCommands, osd } = createOptions({
|
const { options, sentCommands, osd } = createOptions();
|
||||||
resolveProxyCommandOsd: async () =>
|
|
||||||
'Secondary subtitle track: External #8 - English Commentary',
|
|
||||||
});
|
|
||||||
handleMpvCommandFromIpc(['set_property', 'secondary-sid', 'auto'], options);
|
handleMpvCommandFromIpc(['set_property', 'secondary-sid', 'auto'], options);
|
||||||
await new Promise((resolve) => setImmediate(resolve));
|
|
||||||
assert.deepEqual(sentCommands, [['set_property', 'secondary-sid', 'auto']]);
|
assert.deepEqual(sentCommands, [['set_property', 'secondary-sid', 'auto']]);
|
||||||
assert.deepEqual(osd, ['Secondary subtitle track: External #8 - English Commentary']);
|
assert.deepEqual(osd, ['Secondary subtitle track: ${secondary-sid}']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleMpvCommandFromIpc emits osd for subtitle delay keybinding proxies', async () => {
|
test('handleMpvCommandFromIpc emits osd for subtitle delay keybinding proxies', () => {
|
||||||
const { options, sentCommands, osd } = createOptions();
|
const { options, sentCommands, osd } = createOptions();
|
||||||
handleMpvCommandFromIpc(['add', 'sub-delay', 0.1], options);
|
handleMpvCommandFromIpc(['add', 'sub-delay', 0.1], options);
|
||||||
await new Promise((resolve) => setImmediate(resolve));
|
|
||||||
assert.deepEqual(sentCommands, [['add', 'sub-delay', 0.1]]);
|
assert.deepEqual(sentCommands, [['add', 'sub-delay', 0.1]]);
|
||||||
assert.deepEqual(osd, ['Subtitle delay: ${sub-delay}']);
|
assert.deepEqual(osd, ['Subtitle delay: ${sub-delay}']);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export interface HandleMpvCommandFromIpcOptions {
|
|||||||
mpvPlayNextSubtitle: () => void;
|
mpvPlayNextSubtitle: () => void;
|
||||||
shiftSubDelayToAdjacentSubtitle: (direction: 'next' | 'previous') => Promise<void>;
|
shiftSubDelayToAdjacentSubtitle: (direction: 'next' | 'previous') => Promise<void>;
|
||||||
mpvSendCommand: (command: (string | number)[]) => void;
|
mpvSendCommand: (command: (string | number)[]) => void;
|
||||||
resolveProxyCommandOsd?: (command: (string | number)[]) => Promise<string | null>;
|
|
||||||
isMpvConnected: () => boolean;
|
isMpvConnected: () => boolean;
|
||||||
hasRuntimeOptionsManager: () => boolean;
|
hasRuntimeOptionsManager: () => boolean;
|
||||||
}
|
}
|
||||||
@@ -37,7 +36,7 @@ const MPV_PROPERTY_COMMANDS = new Set([
|
|||||||
'multiply',
|
'multiply',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function resolveProxyCommandOsdTemplate(command: (string | number)[]): string | null {
|
function resolveProxyCommandOsd(command: (string | number)[]): string | null {
|
||||||
const operation = typeof command[0] === 'string' ? command[0] : '';
|
const operation = typeof command[0] === 'string' ? command[0] : '';
|
||||||
const property = typeof command[1] === 'string' ? command[1] : '';
|
const property = typeof command[1] === 'string' ? command[1] : '';
|
||||||
if (!MPV_PROPERTY_COMMANDS.has(operation)) return null;
|
if (!MPV_PROPERTY_COMMANDS.has(operation)) return null;
|
||||||
@@ -56,25 +55,6 @@ function resolveProxyCommandOsdTemplate(command: (string | number)[]): string |
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showResolvedProxyCommandOsd(
|
|
||||||
command: (string | number)[],
|
|
||||||
options: HandleMpvCommandFromIpcOptions,
|
|
||||||
): void {
|
|
||||||
const template = resolveProxyCommandOsdTemplate(command);
|
|
||||||
if (!template) return;
|
|
||||||
|
|
||||||
const emit = async () => {
|
|
||||||
try {
|
|
||||||
const resolved = await options.resolveProxyCommandOsd?.(command);
|
|
||||||
options.showMpvOsd(resolved || template);
|
|
||||||
} catch {
|
|
||||||
options.showMpvOsd(template);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleMpvCommandFromIpc(
|
export function handleMpvCommandFromIpc(
|
||||||
command: (string | number)[],
|
command: (string | number)[],
|
||||||
options: HandleMpvCommandFromIpcOptions,
|
options: HandleMpvCommandFromIpcOptions,
|
||||||
@@ -123,7 +103,10 @@ export function handleMpvCommandFromIpc(
|
|||||||
options.mpvPlayNextSubtitle();
|
options.mpvPlayNextSubtitle();
|
||||||
} else {
|
} else {
|
||||||
options.mpvSendCommand(command);
|
options.mpvSendCommand(command);
|
||||||
showResolvedProxyCommandOsd(command, options);
|
const osd = resolveProxyCommandOsd(command);
|
||||||
|
if (osd) {
|
||||||
|
options.showMpvOsd(osd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,22 +22,6 @@ test('showMpvOsdRuntime sends show-text when connected', () => {
|
|||||||
assert.deepEqual(commands, [['show-text', 'hello', '3000']]);
|
assert.deepEqual(commands, [['show-text', 'hello', '3000']]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('showMpvOsdRuntime enables property expansion for placeholder-based messages', () => {
|
|
||||||
const commands: (string | number)[][] = [];
|
|
||||||
showMpvOsdRuntime(
|
|
||||||
{
|
|
||||||
connected: true,
|
|
||||||
send: ({ command }) => {
|
|
||||||
commands.push(command);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'Subtitle delay: ${sub-delay}',
|
|
||||||
);
|
|
||||||
assert.deepEqual(commands, [
|
|
||||||
['expand-properties', 'show-text', 'Subtitle delay: ${sub-delay}', '3000'],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('showMpvOsdRuntime logs fallback when disconnected', () => {
|
test('showMpvOsdRuntime logs fallback when disconnected', () => {
|
||||||
const logs: string[] = [];
|
const logs: string[] = [];
|
||||||
showMpvOsdRuntime(
|
showMpvOsdRuntime(
|
||||||
|
|||||||
@@ -53,10 +53,7 @@ export function showMpvOsdRuntime(
|
|||||||
fallbackLog: (text: string) => void = (line) => logger.info(line),
|
fallbackLog: (text: string) => void = (line) => logger.info(line),
|
||||||
): void {
|
): void {
|
||||||
if (mpvClient && mpvClient.connected) {
|
if (mpvClient && mpvClient.connected) {
|
||||||
const command = text.includes('${')
|
mpvClient.send({ command: ['show-text', text, '3000'] });
|
||||||
? ['expand-properties', 'show-text', text, '3000']
|
|
||||||
: ['show-text', text, '3000'];
|
|
||||||
mpvClient.send({ command });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fallbackLog(`OSD (MPV not connected): ${text}`);
|
fallbackLog(`OSD (MPV not connected): ${text}`);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ test('injectTexthookerBootstrapHtml injects websocket bootstrap before head clos
|
|||||||
/window\.localStorage\.setItem\('bannou-texthooker-websocketUrl', "ws:\/\/127\.0\.0\.1:6678"\)/,
|
/window\.localStorage\.setItem\('bannou-texthooker-websocketUrl', "ws:\/\/127\.0\.0\.1:6678"\)/,
|
||||||
);
|
);
|
||||||
assert.ok(actual.indexOf('</script></head>') !== -1);
|
assert.ok(actual.indexOf('</script></head>') !== -1);
|
||||||
assert.ok(actual.includes('bannou-texthooker-websocketUrl'));
|
assert.ok(actual.includes("bannou-texthooker-websocketUrl"));
|
||||||
assert.ok(!actual.includes('bannou-texthooker-enableKnownWordColoring'));
|
assert.ok(!actual.includes('bannou-texthooker-enableKnownWordColoring'));
|
||||||
assert.ok(!actual.includes('bannou-texthooker-enableNPlusOneColoring'));
|
assert.ok(!actual.includes('bannou-texthooker-enableNPlusOneColoring'));
|
||||||
assert.ok(!actual.includes('bannou-texthooker-enableNameMatchColoring'));
|
assert.ok(!actual.includes('bannou-texthooker-enableNameMatchColoring'));
|
||||||
|
|||||||
@@ -764,9 +764,11 @@ test('requestYomitanScanTokens skips fallback fragments without exact primary so
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await requestYomitanScanTokens('だが それでも届かぬ高みがあった', deps, {
|
const result = await requestYomitanScanTokens(
|
||||||
error: () => undefined,
|
'だが それでも届かぬ高みがあった',
|
||||||
});
|
deps,
|
||||||
|
{ error: () => undefined },
|
||||||
|
);
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
result?.map((token) => ({
|
result?.map((token) => ({
|
||||||
@@ -873,8 +875,7 @@ test('dictionary settings helpers upsert and remove dictionary entries without r
|
|||||||
|
|
||||||
const upsertScript = scripts.find(
|
const upsertScript = scripts.find(
|
||||||
(script) =>
|
(script) =>
|
||||||
script.includes('setAllSettings') &&
|
script.includes('setAllSettings') && script.includes('"SubMiner Character Dictionary (AniList 1)"'),
|
||||||
script.includes('"SubMiner Character Dictionary (AniList 1)"'),
|
|
||||||
);
|
);
|
||||||
assert.ok(upsertScript);
|
assert.ok(upsertScript);
|
||||||
const jitendexOffset = upsertScript?.indexOf('"Jitendex"') ?? -1;
|
const jitendexOffset = upsertScript?.indexOf('"Jitendex"') ?? -1;
|
||||||
@@ -914,18 +915,9 @@ test('importYomitanDictionaryFromZip uses settings automation bridge instead of
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(imported, true);
|
assert.equal(imported, true);
|
||||||
assert.equal(
|
assert.equal(scripts.some((script) => script.includes('__subminerYomitanSettingsAutomation')), true);
|
||||||
scripts.some((script) => script.includes('__subminerYomitanSettingsAutomation')),
|
assert.equal(scripts.some((script) => script.includes('importDictionaryArchiveBase64')), true);
|
||||||
true,
|
assert.equal(scripts.some((script) => script.includes('subminerImportDictionary')), false);
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
scripts.some((script) => script.includes('importDictionaryArchiveBase64')),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
scripts.some((script) => script.includes('subminerImportDictionary')),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('deleteYomitanDictionaryByTitle uses settings automation bridge instead of custom backend action', async () => {
|
test('deleteYomitanDictionaryByTitle uses settings automation bridge instead of custom backend action', async () => {
|
||||||
@@ -955,16 +947,7 @@ test('deleteYomitanDictionaryByTitle uses settings automation bridge instead of
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(deleted, true);
|
assert.equal(deleted, true);
|
||||||
assert.equal(
|
assert.equal(scripts.some((script) => script.includes('__subminerYomitanSettingsAutomation')), true);
|
||||||
scripts.some((script) => script.includes('__subminerYomitanSettingsAutomation')),
|
assert.equal(scripts.some((script) => script.includes('deleteDictionary')), true);
|
||||||
true,
|
assert.equal(scripts.some((script) => script.includes('subminerDeleteDictionary')), false);
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
scripts.some((script) => script.includes('deleteDictionary')),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
scripts.some((script) => script.includes('subminerDeleteDictionary')),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -562,7 +562,9 @@ async function createYomitanExtensionWindow(
|
|||||||
});
|
});
|
||||||
return window;
|
return window;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Failed to create hidden Yomitan ${pageName} window: ${(err as Error).message}`);
|
logger.error(
|
||||||
|
`Failed to create hidden Yomitan ${pageName} window: ${(err as Error).message}`,
|
||||||
|
);
|
||||||
if (!window.isDestroyed()) {
|
if (!window.isDestroyed()) {
|
||||||
window.destroy();
|
window.destroy();
|
||||||
}
|
}
|
||||||
@@ -1041,15 +1043,13 @@ export async function requestYomitanScanTokens(
|
|||||||
}
|
}
|
||||||
if (Array.isArray(rawResult)) {
|
if (Array.isArray(rawResult)) {
|
||||||
const selectedTokens = selectYomitanParseTokens(rawResult, () => false, 'headword');
|
const selectedTokens = selectYomitanParseTokens(rawResult, () => false, 'headword');
|
||||||
return (
|
return selectedTokens?.map((token) => ({
|
||||||
selectedTokens?.map((token) => ({
|
surface: token.surface,
|
||||||
surface: token.surface,
|
reading: token.reading,
|
||||||
reading: token.reading,
|
headword: token.headword,
|
||||||
headword: token.headword,
|
startPos: token.startPos,
|
||||||
startPos: token.startPos,
|
endPos: token.endPos,
|
||||||
endPos: token.endPos,
|
})) ?? null;
|
||||||
})) ?? null
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1523,12 +1523,7 @@ export async function getYomitanDictionaryInfo(
|
|||||||
deps: YomitanParserRuntimeDeps,
|
deps: YomitanParserRuntimeDeps,
|
||||||
logger: LoggerLike,
|
logger: LoggerLike,
|
||||||
): Promise<YomitanDictionaryInfo[]> {
|
): Promise<YomitanDictionaryInfo[]> {
|
||||||
const result = await invokeYomitanBackendAction<unknown>(
|
const result = await invokeYomitanBackendAction<unknown>('getDictionaryInfo', undefined, deps, logger);
|
||||||
'getDictionaryInfo',
|
|
||||||
undefined,
|
|
||||||
deps,
|
|
||||||
logger,
|
|
||||||
);
|
|
||||||
if (!Array.isArray(result)) {
|
if (!Array.isArray(result)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -1551,12 +1546,7 @@ export async function getYomitanSettingsFull(
|
|||||||
deps: YomitanParserRuntimeDeps,
|
deps: YomitanParserRuntimeDeps,
|
||||||
logger: LoggerLike,
|
logger: LoggerLike,
|
||||||
): Promise<Record<string, unknown> | null> {
|
): Promise<Record<string, unknown> | null> {
|
||||||
const result = await invokeYomitanBackendAction<unknown>(
|
const result = await invokeYomitanBackendAction<unknown>('optionsGetFull', undefined, deps, logger);
|
||||||
'optionsGetFull',
|
|
||||||
undefined,
|
|
||||||
deps,
|
|
||||||
logger,
|
|
||||||
);
|
|
||||||
return isObject(result) ? result : null;
|
return isObject(result) ? result : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1663,7 +1653,7 @@ export async function upsertYomitanDictionarySettings(
|
|||||||
(entry) =>
|
(entry) =>
|
||||||
isObject(entry) &&
|
isObject(entry) &&
|
||||||
typeof (entry as { name?: unknown }).name === 'string' &&
|
typeof (entry as { name?: unknown }).name === 'string' &&
|
||||||
(entry as { name: string }).name.trim() === normalizedTitle,
|
((entry as { name: string }).name.trim() === normalizedTitle),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingIndex >= 0) {
|
if (existingIndex >= 0) {
|
||||||
|
|||||||
@@ -90,10 +90,7 @@ export function shouldCopyYomitanExtension(sourceDir: string, targetDir: string)
|
|||||||
return sourceHash === null || targetHash === null || sourceHash !== targetHash;
|
return sourceHash === null || targetHash === null || sourceHash !== targetHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensureExtensionCopy(
|
export function ensureExtensionCopy(sourceDir: string, userDataPath: string): {
|
||||||
sourceDir: string,
|
|
||||||
userDataPath: string,
|
|
||||||
): {
|
|
||||||
targetDir: string;
|
targetDir: string;
|
||||||
copied: boolean;
|
copied: boolean;
|
||||||
} {
|
} {
|
||||||
|
|||||||
@@ -75,10 +75,7 @@ test('ensureExtensionCopy refreshes copied extension when display files change',
|
|||||||
assert.equal(result.targetDir, targetDir);
|
assert.equal(result.targetDir, targetDir);
|
||||||
assert.equal(result.copied, true);
|
assert.equal(result.copied, true);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
fs.readFileSync(
|
fs.readFileSync(path.join(targetDir, 'js', 'display', 'structured-content-generator.js'), 'utf8'),
|
||||||
path.join(targetDir, 'js', 'display', 'structured-content-generator.js'),
|
|
||||||
'utf8',
|
|
||||||
),
|
|
||||||
'new display code',
|
'new display code',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import { BrowserWindow, Extension, session } from 'electron';
|
import { BrowserWindow, Extension, session } from 'electron';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
import { createLogger } from '../../logger';
|
import { createLogger } from '../../logger';
|
||||||
import { ensureExtensionCopy } from './yomitan-extension-copy';
|
import { ensureExtensionCopy } from './yomitan-extension-copy';
|
||||||
import {
|
|
||||||
getYomitanExtensionSearchPaths,
|
|
||||||
resolveExistingYomitanExtensionPath,
|
|
||||||
} from './yomitan-extension-paths';
|
|
||||||
|
|
||||||
const logger = createLogger('main:yomitan-extension-loader');
|
const logger = createLogger('main:yomitan-extension-loader');
|
||||||
|
|
||||||
export interface YomitanExtensionLoaderDeps {
|
export interface YomitanExtensionLoaderDeps {
|
||||||
userDataPath: string;
|
userDataPath: string;
|
||||||
extensionPath?: string;
|
|
||||||
getYomitanParserWindow: () => BrowserWindow | null;
|
getYomitanParserWindow: () => BrowserWindow | null;
|
||||||
setYomitanParserWindow: (window: BrowserWindow | null) => void;
|
setYomitanParserWindow: (window: BrowserWindow | null) => void;
|
||||||
setYomitanParserReadyPromise: (promise: Promise<void> | null) => void;
|
setYomitanParserReadyPromise: (promise: Promise<void> | null) => void;
|
||||||
@@ -22,17 +18,25 @@ export interface YomitanExtensionLoaderDeps {
|
|||||||
export async function loadYomitanExtension(
|
export async function loadYomitanExtension(
|
||||||
deps: YomitanExtensionLoaderDeps,
|
deps: YomitanExtensionLoaderDeps,
|
||||||
): Promise<Extension | null> {
|
): Promise<Extension | null> {
|
||||||
const searchPaths = getYomitanExtensionSearchPaths({
|
const searchPaths = [
|
||||||
explicitPath: deps.extensionPath,
|
path.join(__dirname, '..', '..', 'vendor', 'yomitan'),
|
||||||
moduleDir: __dirname,
|
path.join(__dirname, '..', '..', '..', 'vendor', 'yomitan'),
|
||||||
resourcesPath: process.resourcesPath,
|
path.join(process.resourcesPath, 'yomitan'),
|
||||||
userDataPath: deps.userDataPath,
|
'/usr/share/SubMiner/yomitan',
|
||||||
});
|
path.join(deps.userDataPath, 'yomitan'),
|
||||||
let extPath = resolveExistingYomitanExtensionPath(searchPaths, fs.existsSync);
|
];
|
||||||
|
|
||||||
|
let extPath: string | null = null;
|
||||||
|
for (const p of searchPaths) {
|
||||||
|
if (fs.existsSync(p)) {
|
||||||
|
extPath = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!extPath) {
|
if (!extPath) {
|
||||||
logger.error('Yomitan extension not found in any search path');
|
logger.error('Yomitan extension not found in any search path');
|
||||||
logger.error('Run `bun run build:yomitan` or install Yomitan to one of:', searchPaths);
|
logger.error('Install Yomitan to one of:', searchPaths);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import assert from 'node:assert/strict';
|
|
||||||
import path from 'node:path';
|
|
||||||
import test from 'node:test';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getYomitanExtensionSearchPaths,
|
|
||||||
resolveExistingYomitanExtensionPath,
|
|
||||||
} from './yomitan-extension-paths';
|
|
||||||
|
|
||||||
test('getYomitanExtensionSearchPaths prioritizes generated build output before packaged fallbacks', () => {
|
|
||||||
const searchPaths = getYomitanExtensionSearchPaths({
|
|
||||||
cwd: '/repo',
|
|
||||||
moduleDir: '/repo/dist/core/services',
|
|
||||||
resourcesPath: '/opt/SubMiner/resources',
|
|
||||||
userDataPath: '/Users/kyle/.config/SubMiner',
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.deepEqual(searchPaths, [
|
|
||||||
path.join('/repo', 'build', 'yomitan'),
|
|
||||||
path.join('/opt/SubMiner/resources', 'yomitan'),
|
|
||||||
'/usr/share/SubMiner/yomitan',
|
|
||||||
path.join('/Users/kyle/.config/SubMiner', 'yomitan'),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('resolveExistingYomitanExtensionPath returns first manifest-backed candidate', () => {
|
|
||||||
const existing = new Set<string>([
|
|
||||||
path.join('/repo', 'build', 'yomitan', 'manifest.json'),
|
|
||||||
path.join('/repo', 'vendor', 'subminer-yomitan', 'ext', 'manifest.json'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const resolved = resolveExistingYomitanExtensionPath(
|
|
||||||
[
|
|
||||||
path.join('/repo', 'build', 'yomitan'),
|
|
||||||
path.join('/repo', 'vendor', 'subminer-yomitan', 'ext'),
|
|
||||||
],
|
|
||||||
(candidate) => existing.has(candidate),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.equal(resolved, path.join('/repo', 'build', 'yomitan'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('resolveExistingYomitanExtensionPath ignores source tree without built manifest', () => {
|
|
||||||
const resolved = resolveExistingYomitanExtensionPath(
|
|
||||||
[path.join('/repo', 'vendor', 'subminer-yomitan', 'ext')],
|
|
||||||
() => false,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.equal(resolved, null);
|
|
||||||
});
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import * as fs from 'node:fs';
|
|
||||||
import * as path from 'node:path';
|
|
||||||
|
|
||||||
export interface YomitanExtensionPathOptions {
|
|
||||||
explicitPath?: string;
|
|
||||||
cwd?: string;
|
|
||||||
moduleDir?: string;
|
|
||||||
resourcesPath?: string;
|
|
||||||
userDataPath?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pushUnique(values: string[], candidate: string | null | undefined): void {
|
|
||||||
if (!candidate || values.includes(candidate)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
values.push(candidate);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getYomitanExtensionSearchPaths(
|
|
||||||
options: YomitanExtensionPathOptions = {},
|
|
||||||
): string[] {
|
|
||||||
const searchPaths: string[] = [];
|
|
||||||
|
|
||||||
pushUnique(searchPaths, options.explicitPath ? path.resolve(options.explicitPath) : null);
|
|
||||||
pushUnique(searchPaths, options.cwd ? path.resolve(options.cwd, 'build', 'yomitan') : null);
|
|
||||||
pushUnique(
|
|
||||||
searchPaths,
|
|
||||||
options.moduleDir
|
|
||||||
? path.resolve(options.moduleDir, '..', '..', '..', 'build', 'yomitan')
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
pushUnique(
|
|
||||||
searchPaths,
|
|
||||||
options.resourcesPath ? path.join(options.resourcesPath, 'yomitan') : null,
|
|
||||||
);
|
|
||||||
pushUnique(searchPaths, '/usr/share/SubMiner/yomitan');
|
|
||||||
pushUnique(searchPaths, options.userDataPath ? path.join(options.userDataPath, 'yomitan') : null);
|
|
||||||
|
|
||||||
return searchPaths;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveExistingYomitanExtensionPath(
|
|
||||||
searchPaths: string[],
|
|
||||||
existsSync: (path: string) => boolean = fs.existsSync,
|
|
||||||
): string | null {
|
|
||||||
for (const candidate of searchPaths) {
|
|
||||||
if (existsSync(path.join(candidate, 'manifest.json'))) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveYomitanExtensionPath(
|
|
||||||
options: YomitanExtensionPathOptions = {},
|
|
||||||
existsSync: (path: string) => boolean = fs.existsSync,
|
|
||||||
): string | null {
|
|
||||||
return resolveExistingYomitanExtensionPath(getYomitanExtensionSearchPaths(options), existsSync);
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ import assert from 'node:assert/strict';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { pathToFileURL } from 'node:url';
|
import { pathToFileURL } from 'node:url';
|
||||||
import { resolveYomitanExtensionPath } from './yomitan-extension-paths';
|
|
||||||
|
|
||||||
class FakeStyle {
|
class FakeStyle {
|
||||||
private values = new Map<string, string>();
|
private values = new Map<string, string>();
|
||||||
@@ -156,14 +155,15 @@ function findFirstByClass(node: FakeNode, className: string): FakeNode | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test('StructuredContentGenerator uses direct img loading for popup glossary images', async () => {
|
test('StructuredContentGenerator uses direct img loading for popup glossary images', async () => {
|
||||||
const yomitanRoot = resolveYomitanExtensionPath({ cwd: process.cwd() });
|
|
||||||
assert.ok(yomitanRoot, 'Run `bun run build:yomitan` before Yomitan integration tests.');
|
|
||||||
|
|
||||||
const { DisplayContentManager } = await import(
|
const { DisplayContentManager } = await import(
|
||||||
pathToFileURL(path.join(yomitanRoot, 'js', 'display', 'display-content-manager.js')).href
|
pathToFileURL(
|
||||||
|
path.join(process.cwd(), 'vendor/yomitan/js/display/display-content-manager.js'),
|
||||||
|
).href
|
||||||
);
|
);
|
||||||
const { StructuredContentGenerator } = await import(
|
const { StructuredContentGenerator } = await import(
|
||||||
pathToFileURL(path.join(yomitanRoot, 'js', 'display', 'structured-content-generator.js')).href
|
pathToFileURL(
|
||||||
|
path.join(process.cwd(), 'vendor/yomitan/js/display/structured-content-generator.js'),
|
||||||
|
).href
|
||||||
);
|
);
|
||||||
|
|
||||||
const createObjectURLCalls: string[] = [];
|
const createObjectURLCalls: string[] = [];
|
||||||
@@ -197,10 +197,14 @@ test('StructuredContentGenerator uses direct img loading for popup glossary imag
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const generator = new StructuredContentGenerator(manager, new FakeDocument(), {
|
const generator = new StructuredContentGenerator(
|
||||||
devicePixelRatio: 1,
|
manager,
|
||||||
navigator: { userAgent: 'Mozilla/5.0' },
|
new FakeDocument(),
|
||||||
});
|
{
|
||||||
|
devicePixelRatio: 1,
|
||||||
|
navigator: { userAgent: 'Mozilla/5.0' },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const node = generator.createDefinitionImage(
|
const node = generator.createDefinitionImage(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ test('normalizeStartupArgv defaults no-arg startup to --start --background', ()
|
|||||||
'--background',
|
'--background',
|
||||||
]);
|
]);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
normalizeStartupArgv(['SubMiner.AppImage', '--password-store', 'gnome-libsecret'], {}),
|
normalizeStartupArgv(
|
||||||
|
['SubMiner.AppImage', '--password-store', 'gnome-libsecret'],
|
||||||
|
{},
|
||||||
|
),
|
||||||
['SubMiner.AppImage', '--password-store', 'gnome-libsecret', '--start', '--background'],
|
['SubMiner.AppImage', '--password-store', 'gnome-libsecret', '--start', '--background'],
|
||||||
);
|
);
|
||||||
assert.deepEqual(normalizeStartupArgv(['SubMiner.AppImage', '--background'], {}), [
|
assert.deepEqual(normalizeStartupArgv(['SubMiner.AppImage', '--background'], {}), [
|
||||||
|
|||||||
254
src/main.ts
254
src/main.ts
@@ -1657,9 +1657,10 @@ const {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const maybeFocusExistingFirstRunSetupWindow = createMaybeFocusExistingFirstRunSetupWindowHandler({
|
const maybeFocusExistingFirstRunSetupWindow =
|
||||||
getSetupWindow: () => appState.firstRunSetupWindow,
|
createMaybeFocusExistingFirstRunSetupWindowHandler({
|
||||||
});
|
getSetupWindow: () => appState.firstRunSetupWindow,
|
||||||
|
});
|
||||||
const openFirstRunSetupWindowHandler = createOpenFirstRunSetupWindowHandler({
|
const openFirstRunSetupWindowHandler = createOpenFirstRunSetupWindowHandler({
|
||||||
maybeFocusExistingSetupWindow: maybeFocusExistingFirstRunSetupWindow,
|
maybeFocusExistingSetupWindow: maybeFocusExistingFirstRunSetupWindow,
|
||||||
createSetupWindow: () =>
|
createSetupWindow: () =>
|
||||||
@@ -2403,9 +2404,9 @@ const { appReadyRuntimeRunner } = composeAppReadyRuntime({
|
|||||||
shouldSkipHeavyStartup: () =>
|
shouldSkipHeavyStartup: () =>
|
||||||
Boolean(
|
Boolean(
|
||||||
appState.initialArgs &&
|
appState.initialArgs &&
|
||||||
(shouldRunSettingsOnlyStartup(appState.initialArgs) ||
|
(shouldRunSettingsOnlyStartup(appState.initialArgs) ||
|
||||||
appState.initialArgs.dictionary ||
|
appState.initialArgs.dictionary ||
|
||||||
appState.initialArgs.setup),
|
appState.initialArgs.setup),
|
||||||
),
|
),
|
||||||
createImmersionTracker: () => {
|
createImmersionTracker: () => {
|
||||||
ensureImmersionTrackerStarted();
|
ensureImmersionTrackerStarted();
|
||||||
@@ -2418,64 +2419,65 @@ const { appReadyRuntimeRunner } = composeAppReadyRuntime({
|
|||||||
immersionTrackerStartupMainDeps,
|
immersionTrackerStartupMainDeps,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { runAndApplyStartupState } = runtimeRegistry.startup.createStartupRuntimeHandlers<
|
const { runAndApplyStartupState } =
|
||||||
CliArgs,
|
runtimeRegistry.startup.createStartupRuntimeHandlers<
|
||||||
StartupState,
|
CliArgs,
|
||||||
ReturnType<typeof createStartupBootstrapRuntimeDeps>
|
StartupState,
|
||||||
>({
|
ReturnType<typeof createStartupBootstrapRuntimeDeps>
|
||||||
appLifecycleRuntimeRunnerMainDeps: {
|
>({
|
||||||
app,
|
appLifecycleRuntimeRunnerMainDeps: {
|
||||||
platform: process.platform,
|
app,
|
||||||
shouldStartApp: (nextArgs: CliArgs) => shouldStartApp(nextArgs),
|
platform: process.platform,
|
||||||
parseArgs: (argv: string[]) => parseArgs(argv),
|
shouldStartApp: (nextArgs: CliArgs) => shouldStartApp(nextArgs),
|
||||||
handleCliCommand: (nextArgs: CliArgs, source: CliCommandSource) =>
|
parseArgs: (argv: string[]) => parseArgs(argv),
|
||||||
handleCliCommand(nextArgs, source),
|
handleCliCommand: (nextArgs: CliArgs, source: CliCommandSource) =>
|
||||||
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
handleCliCommand(nextArgs, source),
|
||||||
logNoRunningInstance: () => appLogger.logNoRunningInstance(),
|
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
||||||
onReady: appReadyRuntimeRunner,
|
logNoRunningInstance: () => appLogger.logNoRunningInstance(),
|
||||||
onWillQuitCleanup: () => onWillQuitCleanupHandler(),
|
onReady: appReadyRuntimeRunner,
|
||||||
shouldRestoreWindowsOnActivate: () => shouldRestoreWindowsOnActivateHandler(),
|
onWillQuitCleanup: () => onWillQuitCleanupHandler(),
|
||||||
restoreWindowsOnActivate: () => restoreWindowsOnActivateHandler(),
|
shouldRestoreWindowsOnActivate: () => shouldRestoreWindowsOnActivateHandler(),
|
||||||
shouldQuitOnWindowAllClosed: () => !appState.backgroundMode,
|
restoreWindowsOnActivate: () => restoreWindowsOnActivateHandler(),
|
||||||
},
|
shouldQuitOnWindowAllClosed: () => !appState.backgroundMode,
|
||||||
createAppLifecycleRuntimeRunner: (params) => createAppLifecycleRuntimeRunner(params),
|
|
||||||
buildStartupBootstrapMainDeps: (startAppLifecycle) => ({
|
|
||||||
argv: process.argv,
|
|
||||||
parseArgs: (argv: string[]) => parseArgs(argv),
|
|
||||||
setLogLevel: (level: string, source: LogLevelSource) => {
|
|
||||||
setLogLevel(level, source);
|
|
||||||
},
|
},
|
||||||
forceX11Backend: (args: CliArgs) => {
|
createAppLifecycleRuntimeRunner: (params) => createAppLifecycleRuntimeRunner(params),
|
||||||
forceX11Backend(args);
|
buildStartupBootstrapMainDeps: (startAppLifecycle) => ({
|
||||||
},
|
argv: process.argv,
|
||||||
enforceUnsupportedWaylandMode: (args: CliArgs) => {
|
parseArgs: (argv: string[]) => parseArgs(argv),
|
||||||
enforceUnsupportedWaylandMode(args);
|
setLogLevel: (level: string, source: LogLevelSource) => {
|
||||||
},
|
setLogLevel(level, source);
|
||||||
shouldStartApp: (args: CliArgs) => shouldStartApp(args),
|
|
||||||
getDefaultSocketPath: () => getDefaultSocketPath(),
|
|
||||||
defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT,
|
|
||||||
configDir: CONFIG_DIR,
|
|
||||||
defaultConfig: DEFAULT_CONFIG,
|
|
||||||
generateConfigTemplate: (config: ResolvedConfig) => generateConfigTemplate(config),
|
|
||||||
generateDefaultConfigFile: (
|
|
||||||
args: CliArgs,
|
|
||||||
options: {
|
|
||||||
configDir: string;
|
|
||||||
defaultConfig: unknown;
|
|
||||||
generateTemplate: (config: unknown) => string;
|
|
||||||
},
|
},
|
||||||
) => generateDefaultConfigFile(args, options),
|
forceX11Backend: (args: CliArgs) => {
|
||||||
setExitCode: (code) => {
|
forceX11Backend(args);
|
||||||
process.exitCode = code;
|
},
|
||||||
},
|
enforceUnsupportedWaylandMode: (args: CliArgs) => {
|
||||||
quitApp: () => app.quit(),
|
enforceUnsupportedWaylandMode(args);
|
||||||
logGenerateConfigError: (message) => logger.error(message),
|
},
|
||||||
startAppLifecycle,
|
shouldStartApp: (args: CliArgs) => shouldStartApp(args),
|
||||||
}),
|
getDefaultSocketPath: () => getDefaultSocketPath(),
|
||||||
createStartupBootstrapRuntimeDeps: (deps) => createStartupBootstrapRuntimeDeps(deps),
|
defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT,
|
||||||
runStartupBootstrapRuntime,
|
configDir: CONFIG_DIR,
|
||||||
applyStartupState: (startupState) => applyStartupState(appState, startupState),
|
defaultConfig: DEFAULT_CONFIG,
|
||||||
});
|
generateConfigTemplate: (config: ResolvedConfig) => generateConfigTemplate(config),
|
||||||
|
generateDefaultConfigFile: (
|
||||||
|
args: CliArgs,
|
||||||
|
options: {
|
||||||
|
configDir: string;
|
||||||
|
defaultConfig: unknown;
|
||||||
|
generateTemplate: (config: unknown) => string;
|
||||||
|
},
|
||||||
|
) => generateDefaultConfigFile(args, options),
|
||||||
|
setExitCode: (code) => {
|
||||||
|
process.exitCode = code;
|
||||||
|
},
|
||||||
|
quitApp: () => app.quit(),
|
||||||
|
logGenerateConfigError: (message) => logger.error(message),
|
||||||
|
startAppLifecycle,
|
||||||
|
}),
|
||||||
|
createStartupBootstrapRuntimeDeps: (deps) => createStartupBootstrapRuntimeDeps(deps),
|
||||||
|
runStartupBootstrapRuntime,
|
||||||
|
applyStartupState: (startupState) => applyStartupState(appState, startupState),
|
||||||
|
});
|
||||||
|
|
||||||
runAndApplyStartupState();
|
runAndApplyStartupState();
|
||||||
if (isAnilistTrackingEnabled(getResolvedConfig())) {
|
if (isAnilistTrackingEnabled(getResolvedConfig())) {
|
||||||
@@ -3201,7 +3203,6 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
|
|||||||
shiftSubtitleDelayToAdjacentCueHandler(direction),
|
shiftSubtitleDelayToAdjacentCueHandler(direction),
|
||||||
sendMpvCommand: (rawCommand: (string | number)[]) =>
|
sendMpvCommand: (rawCommand: (string | number)[]) =>
|
||||||
sendMpvCommandRuntime(appState.mpvClient, rawCommand),
|
sendMpvCommandRuntime(appState.mpvClient, rawCommand),
|
||||||
getMpvClient: () => appState.mpvClient,
|
|
||||||
isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected),
|
isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected),
|
||||||
hasRuntimeOptionsManager: () => appState.runtimeOptionsManager !== null,
|
hasRuntimeOptionsManager: () => appState.runtimeOptionsManager !== null,
|
||||||
},
|
},
|
||||||
@@ -3340,75 +3341,74 @@ const createCliCommandContextHandler = createCliCommandContextFactory({
|
|||||||
});
|
});
|
||||||
const { createMainWindow: createMainWindowHandler, createModalWindow: createModalWindowHandler } =
|
const { createMainWindow: createMainWindowHandler, createModalWindow: createModalWindowHandler } =
|
||||||
createOverlayWindowRuntimeHandlers<BrowserWindow>({
|
createOverlayWindowRuntimeHandlers<BrowserWindow>({
|
||||||
createOverlayWindowDeps: {
|
createOverlayWindowDeps: {
|
||||||
createOverlayWindowCore: (kind, options) => createOverlayWindowCore(kind, options),
|
createOverlayWindowCore: (kind, options) => createOverlayWindowCore(kind, options),
|
||||||
isDev,
|
isDev,
|
||||||
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
||||||
onRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
onRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
||||||
setOverlayDebugVisualizationEnabled: (enabled) =>
|
setOverlayDebugVisualizationEnabled: (enabled) => setOverlayDebugVisualizationEnabled(enabled),
|
||||||
setOverlayDebugVisualizationEnabled(enabled),
|
isOverlayVisible: (windowKind) =>
|
||||||
isOverlayVisible: (windowKind) =>
|
windowKind === 'visible' ? overlayManager.getVisibleOverlayVisible() : false,
|
||||||
windowKind === 'visible' ? overlayManager.getVisibleOverlayVisible() : false,
|
tryHandleOverlayShortcutLocalFallback: (input) =>
|
||||||
tryHandleOverlayShortcutLocalFallback: (input) =>
|
overlayShortcutsRuntime.tryHandleOverlayShortcutLocalFallback(input),
|
||||||
overlayShortcutsRuntime.tryHandleOverlayShortcutLocalFallback(input),
|
onWindowClosed: (windowKind) => {
|
||||||
onWindowClosed: (windowKind) => {
|
if (windowKind === 'visible') {
|
||||||
if (windowKind === 'visible') {
|
overlayManager.setMainWindow(null);
|
||||||
overlayManager.setMainWindow(null);
|
} else {
|
||||||
} else {
|
overlayManager.setModalWindow(null);
|
||||||
overlayManager.setModalWindow(null);
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
setMainWindow: (window) => overlayManager.setMainWindow(window),
|
},
|
||||||
setModalWindow: (window) => overlayManager.setModalWindow(window),
|
setMainWindow: (window) => overlayManager.setMainWindow(window),
|
||||||
});
|
setModalWindow: (window) => overlayManager.setModalWindow(window),
|
||||||
|
});
|
||||||
const { ensureTray: ensureTrayHandler, destroyTray: destroyTrayHandler } =
|
const { ensureTray: ensureTrayHandler, destroyTray: destroyTrayHandler } =
|
||||||
createTrayRuntimeHandlers({
|
createTrayRuntimeHandlers({
|
||||||
resolveTrayIconPathDeps: {
|
resolveTrayIconPathDeps: {
|
||||||
resolveTrayIconPathRuntime,
|
resolveTrayIconPathRuntime,
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
resourcesPath: process.resourcesPath,
|
resourcesPath: process.resourcesPath,
|
||||||
appPath: app.getAppPath(),
|
appPath: app.getAppPath(),
|
||||||
dirname: __dirname,
|
dirname: __dirname,
|
||||||
joinPath: (...parts) => path.join(...parts),
|
joinPath: (...parts) => path.join(...parts),
|
||||||
fileExists: (candidate) => fs.existsSync(candidate),
|
fileExists: (candidate) => fs.existsSync(candidate),
|
||||||
|
},
|
||||||
|
buildTrayMenuTemplateDeps: {
|
||||||
|
buildTrayMenuTemplateRuntime,
|
||||||
|
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
||||||
|
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
||||||
|
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
||||||
|
showFirstRunSetup: () => !firstRunSetupService.isSetupCompleted(),
|
||||||
|
openFirstRunSetupWindow: () => openFirstRunSetupWindow(),
|
||||||
|
openYomitanSettings: () => openYomitanSettings(),
|
||||||
|
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||||
|
openJellyfinSetupWindow: () => openJellyfinSetupWindow(),
|
||||||
|
openAnilistSetupWindow: () => openAnilistSetupWindow(),
|
||||||
|
quitApp: () => app.quit(),
|
||||||
|
},
|
||||||
|
ensureTrayDeps: {
|
||||||
|
getTray: () => appTray,
|
||||||
|
setTray: (tray) => {
|
||||||
|
appTray = tray as Tray | null;
|
||||||
},
|
},
|
||||||
buildTrayMenuTemplateDeps: {
|
createImageFromPath: (iconPath) => nativeImage.createFromPath(iconPath),
|
||||||
buildTrayMenuTemplateRuntime,
|
createEmptyImage: () => nativeImage.createEmpty(),
|
||||||
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
createTray: (icon) => new Tray(icon as ConstructorParameters<typeof Tray>[0]),
|
||||||
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
trayTooltip: TRAY_TOOLTIP,
|
||||||
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
platform: process.platform,
|
||||||
showFirstRunSetup: () => !firstRunSetupService.isSetupCompleted(),
|
logWarn: (message) => logger.warn(message),
|
||||||
openFirstRunSetupWindow: () => openFirstRunSetupWindow(),
|
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
||||||
openYomitanSettings: () => openYomitanSettings(),
|
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
||||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
||||||
openJellyfinSetupWindow: () => openJellyfinSetupWindow(),
|
},
|
||||||
openAnilistSetupWindow: () => openAnilistSetupWindow(),
|
destroyTrayDeps: {
|
||||||
quitApp: () => app.quit(),
|
getTray: () => appTray,
|
||||||
|
setTray: (tray) => {
|
||||||
|
appTray = tray as Tray | null;
|
||||||
},
|
},
|
||||||
ensureTrayDeps: {
|
},
|
||||||
getTray: () => appTray,
|
buildMenuFromTemplate: (template) => Menu.buildFromTemplate(template),
|
||||||
setTray: (tray) => {
|
});
|
||||||
appTray = tray as Tray | null;
|
|
||||||
},
|
|
||||||
createImageFromPath: (iconPath) => nativeImage.createFromPath(iconPath),
|
|
||||||
createEmptyImage: () => nativeImage.createEmpty(),
|
|
||||||
createTray: (icon) => new Tray(icon as ConstructorParameters<typeof Tray>[0]),
|
|
||||||
trayTooltip: TRAY_TOOLTIP,
|
|
||||||
platform: process.platform,
|
|
||||||
logWarn: (message) => logger.warn(message),
|
|
||||||
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
|
||||||
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
|
||||||
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
|
||||||
},
|
|
||||||
destroyTrayDeps: {
|
|
||||||
getTray: () => appTray,
|
|
||||||
setTray: (tray) => {
|
|
||||||
appTray = tray as Tray | null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
buildMenuFromTemplate: (template) => Menu.buildFromTemplate(template),
|
|
||||||
});
|
|
||||||
const yomitanExtensionRuntime = createYomitanExtensionRuntime({
|
const yomitanExtensionRuntime = createYomitanExtensionRuntime({
|
||||||
loadYomitanExtensionCore,
|
loadYomitanExtensionCore,
|
||||||
userDataPath: USER_DATA_PATH,
|
userDataPath: USER_DATA_PATH,
|
||||||
|
|||||||
@@ -563,9 +563,7 @@ test('generateForCurrentMedia reapplies collapsible open states when using cache
|
|||||||
content: { content: Array<Record<string, unknown>> };
|
content: { content: Array<Record<string, unknown>> };
|
||||||
}
|
}
|
||||||
).content.content;
|
).content.content;
|
||||||
const sections = children.filter(
|
const sections = children.filter((item) => (item as { tag?: string }).tag === 'details') as Array<{
|
||||||
(item) => (item as { tag?: string }).tag === 'details',
|
|
||||||
) as Array<{
|
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
}>;
|
}>;
|
||||||
assert.ok(sections.length >= 2);
|
assert.ok(sections.length >= 2);
|
||||||
@@ -1891,9 +1889,7 @@ test('buildMergedDictionary reapplies collapsible open states from current confi
|
|||||||
content: { content: Array<Record<string, unknown>> };
|
content: { content: Array<Record<string, unknown>> };
|
||||||
}
|
}
|
||||||
).content.content;
|
).content.content;
|
||||||
const sections = children.filter(
|
const sections = children.filter((item) => (item as { tag?: string }).tag === 'details') as Array<{
|
||||||
(item) => (item as { tag?: string }).tag === 'details',
|
|
||||||
) as Array<{
|
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
}>;
|
}>;
|
||||||
assert.ok(sections.length >= 1);
|
assert.ok(sections.length >= 1);
|
||||||
|
|||||||
@@ -502,10 +502,7 @@ function expandRawNameVariants(rawName: string): string[] {
|
|||||||
if (!trimmed) return [];
|
if (!trimmed) return [];
|
||||||
|
|
||||||
const variants = new Set<string>([trimmed]);
|
const variants = new Set<string>([trimmed]);
|
||||||
const outer = trimmed
|
const outer = trimmed.replace(/[((][^()()]+[))]/g, ' ').replace(/\s+/g, ' ').trim();
|
||||||
.replace(/[((][^()()]+[))]/g, ' ')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim();
|
|
||||||
if (outer && outer !== trimmed) {
|
if (outer && outer !== trimmed) {
|
||||||
variants.add(outer);
|
variants.add(outer);
|
||||||
}
|
}
|
||||||
@@ -1289,14 +1286,12 @@ async function fetchCharactersForMedia(
|
|||||||
if (!node || typeof node.id !== 'number') continue;
|
if (!node || typeof node.id !== 'number') continue;
|
||||||
const fullName = node.name?.full?.trim() || '';
|
const fullName = node.name?.full?.trim() || '';
|
||||||
const nativeName = node.name?.native?.trim() || '';
|
const nativeName = node.name?.native?.trim() || '';
|
||||||
const alternativeNames = [
|
const alternativeNames = [...new Set(
|
||||||
...new Set(
|
(node.name?.alternative ?? [])
|
||||||
(node.name?.alternative ?? [])
|
.filter((value): value is string => typeof value === 'string')
|
||||||
.filter((value): value is string => typeof value === 'string')
|
.map((value) => value.trim())
|
||||||
.map((value) => value.trim())
|
.filter((value) => value.length > 0),
|
||||||
.filter((value) => value.length > 0),
|
)];
|
||||||
),
|
|
||||||
];
|
|
||||||
if (!fullName && !nativeName && alternativeNames.length === 0) continue;
|
if (!fullName && !nativeName && alternativeNames.length === 0) continue;
|
||||||
const voiceActors: VoiceActorRecord[] = [];
|
const voiceActors: VoiceActorRecord[] = [];
|
||||||
for (const va of edge?.voiceActors ?? []) {
|
for (const va of edge?.voiceActors ?? []) {
|
||||||
|
|||||||
@@ -186,7 +186,6 @@ export interface MpvCommandRuntimeServiceDepsParams {
|
|||||||
mpvPlayNextSubtitle: HandleMpvCommandFromIpcOptions['mpvPlayNextSubtitle'];
|
mpvPlayNextSubtitle: HandleMpvCommandFromIpcOptions['mpvPlayNextSubtitle'];
|
||||||
shiftSubDelayToAdjacentSubtitle: HandleMpvCommandFromIpcOptions['shiftSubDelayToAdjacentSubtitle'];
|
shiftSubDelayToAdjacentSubtitle: HandleMpvCommandFromIpcOptions['shiftSubDelayToAdjacentSubtitle'];
|
||||||
mpvSendCommand: HandleMpvCommandFromIpcOptions['mpvSendCommand'];
|
mpvSendCommand: HandleMpvCommandFromIpcOptions['mpvSendCommand'];
|
||||||
resolveProxyCommandOsd?: HandleMpvCommandFromIpcOptions['resolveProxyCommandOsd'];
|
|
||||||
isMpvConnected: HandleMpvCommandFromIpcOptions['isMpvConnected'];
|
isMpvConnected: HandleMpvCommandFromIpcOptions['isMpvConnected'];
|
||||||
hasRuntimeOptionsManager: HandleMpvCommandFromIpcOptions['hasRuntimeOptionsManager'];
|
hasRuntimeOptionsManager: HandleMpvCommandFromIpcOptions['hasRuntimeOptionsManager'];
|
||||||
}
|
}
|
||||||
@@ -340,7 +339,6 @@ export function createMpvCommandRuntimeServiceDeps(
|
|||||||
mpvPlayNextSubtitle: params.mpvPlayNextSubtitle,
|
mpvPlayNextSubtitle: params.mpvPlayNextSubtitle,
|
||||||
shiftSubDelayToAdjacentSubtitle: params.shiftSubDelayToAdjacentSubtitle,
|
shiftSubDelayToAdjacentSubtitle: params.shiftSubDelayToAdjacentSubtitle,
|
||||||
mpvSendCommand: params.mpvSendCommand,
|
mpvSendCommand: params.mpvSendCommand,
|
||||||
resolveProxyCommandOsd: params.resolveProxyCommandOsd,
|
|
||||||
isMpvConnected: params.isMpvConnected,
|
isMpvConnected: params.isMpvConnected,
|
||||||
hasRuntimeOptionsManager: params.hasRuntimeOptionsManager,
|
hasRuntimeOptionsManager: params.hasRuntimeOptionsManager,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,12 +2,6 @@ import type { RuntimeOptionApplyResult, RuntimeOptionId } from '../types';
|
|||||||
import { handleMpvCommandFromIpc } from '../core/services';
|
import { handleMpvCommandFromIpc } from '../core/services';
|
||||||
import { createMpvCommandRuntimeServiceDeps } from './dependencies';
|
import { createMpvCommandRuntimeServiceDeps } from './dependencies';
|
||||||
import { SPECIAL_COMMANDS } from '../config';
|
import { SPECIAL_COMMANDS } from '../config';
|
||||||
import { resolveProxyCommandOsdRuntime } from './runtime/mpv-proxy-osd';
|
|
||||||
|
|
||||||
type MpvPropertyClientLike = {
|
|
||||||
connected: boolean;
|
|
||||||
requestProperty: (name: string) => Promise<unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface MpvCommandFromIpcRuntimeDeps {
|
export interface MpvCommandFromIpcRuntimeDeps {
|
||||||
triggerSubsyncFromConfig: () => void;
|
triggerSubsyncFromConfig: () => void;
|
||||||
@@ -18,7 +12,6 @@ export interface MpvCommandFromIpcRuntimeDeps {
|
|||||||
playNextSubtitle: () => void;
|
playNextSubtitle: () => void;
|
||||||
shiftSubDelayToAdjacentSubtitle: (direction: 'next' | 'previous') => Promise<void>;
|
shiftSubDelayToAdjacentSubtitle: (direction: 'next' | 'previous') => Promise<void>;
|
||||||
sendMpvCommand: (command: (string | number)[]) => void;
|
sendMpvCommand: (command: (string | number)[]) => void;
|
||||||
getMpvClient: () => MpvPropertyClientLike | null;
|
|
||||||
isMpvConnected: () => boolean;
|
isMpvConnected: () => boolean;
|
||||||
hasRuntimeOptionsManager: () => boolean;
|
hasRuntimeOptionsManager: () => boolean;
|
||||||
}
|
}
|
||||||
@@ -40,8 +33,6 @@ export function handleMpvCommandFromIpcRuntime(
|
|||||||
shiftSubDelayToAdjacentSubtitle: (direction) =>
|
shiftSubDelayToAdjacentSubtitle: (direction) =>
|
||||||
deps.shiftSubDelayToAdjacentSubtitle(direction),
|
deps.shiftSubDelayToAdjacentSubtitle(direction),
|
||||||
mpvSendCommand: deps.sendMpvCommand,
|
mpvSendCommand: deps.sendMpvCommand,
|
||||||
resolveProxyCommandOsd: (nextCommand) =>
|
|
||||||
resolveProxyCommandOsdRuntime(nextCommand, deps.getMpvClient),
|
|
||||||
isMpvConnected: deps.isMpvConnected,
|
isMpvConnected: deps.isMpvConnected,
|
||||||
hasRuntimeOptionsManager: deps.hasRuntimeOptionsManager,
|
hasRuntimeOptionsManager: deps.hasRuntimeOptionsManager,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -75,7 +75,5 @@ test('createRegisterSubminerProtocolClientHandler keeps unsupported registration
|
|||||||
});
|
});
|
||||||
|
|
||||||
register();
|
register();
|
||||||
assert.deepEqual(calls, [
|
assert.deepEqual(calls, ['debug:Failed to register default protocol handler for subminer:// URLs']);
|
||||||
'debug:Failed to register default protocol handler for subminer:// URLs',
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -172,10 +172,7 @@ export function createCharacterDictionaryAutoSyncRuntimeService(
|
|||||||
? String(existing.revision)
|
? String(existing.revision)
|
||||||
: null;
|
: null;
|
||||||
const shouldImport =
|
const shouldImport =
|
||||||
merged !== null ||
|
merged !== null || existing === null || existingRevision === null || existingRevision !== revision;
|
||||||
existing === null ||
|
|
||||||
existingRevision === null ||
|
|
||||||
existingRevision !== revision;
|
|
||||||
|
|
||||||
if (shouldImport) {
|
if (shouldImport) {
|
||||||
if (existing !== null) {
|
if (existing !== null) {
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ test('composeIpcRuntimeHandlers returns callable IPC handlers and registration b
|
|||||||
playNextSubtitle: () => {},
|
playNextSubtitle: () => {},
|
||||||
shiftSubDelayToAdjacentSubtitle: async () => {},
|
shiftSubDelayToAdjacentSubtitle: async () => {},
|
||||||
sendMpvCommand: () => {},
|
sendMpvCommand: () => {},
|
||||||
getMpvClient: () => null,
|
|
||||||
isMpvConnected: () => false,
|
isMpvConnected: () => false,
|
||||||
hasRuntimeOptionsManager: () => true,
|
hasRuntimeOptionsManager: () => true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -79,10 +79,7 @@ test('installFirstRunPluginToDefaultLocation installs plugin and backs up existi
|
|||||||
|
|
||||||
const scriptsDirEntries = fs.readdirSync(installPaths.scriptsDir);
|
const scriptsDirEntries = fs.readdirSync(installPaths.scriptsDir);
|
||||||
const scriptOptsEntries = fs.readdirSync(installPaths.scriptOptsDir);
|
const scriptOptsEntries = fs.readdirSync(installPaths.scriptOptsDir);
|
||||||
assert.equal(
|
assert.equal(scriptsDirEntries.some((entry) => entry.startsWith('subminer.bak.')), true);
|
||||||
scriptsDirEntries.some((entry) => entry.startsWith('subminer.bak.')),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
scriptOptsEntries.some((entry) => entry.startsWith('subminer.conf.bak.')),
|
scriptOptsEntries.some((entry) => entry.startsWith('subminer.conf.bak.')),
|
||||||
true,
|
true,
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import assert from 'node:assert/strict';
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { createFirstRunSetupService, shouldAutoOpenFirstRunSetup } from './first-run-setup-service';
|
import {
|
||||||
|
createFirstRunSetupService,
|
||||||
|
shouldAutoOpenFirstRunSetup,
|
||||||
|
} from './first-run-setup-service';
|
||||||
import type { CliArgs } from '../../cli/args';
|
import type { CliArgs } from '../../cli/args';
|
||||||
|
|
||||||
function withTempDir(fn: (dir: string) => Promise<void> | void): Promise<void> | void {
|
function withTempDir(fn: (dir: string) => Promise<void> | void): Promise<void> | void {
|
||||||
|
|||||||
@@ -43,39 +43,39 @@ export interface FirstRunSetupService {
|
|||||||
function hasAnyStartupCommandBeyondSetup(args: CliArgs): boolean {
|
function hasAnyStartupCommandBeyondSetup(args: CliArgs): boolean {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
args.toggle ||
|
args.toggle ||
|
||||||
args.toggleVisibleOverlay ||
|
args.toggleVisibleOverlay ||
|
||||||
args.settings ||
|
args.settings ||
|
||||||
args.show ||
|
args.show ||
|
||||||
args.hide ||
|
args.hide ||
|
||||||
args.showVisibleOverlay ||
|
args.showVisibleOverlay ||
|
||||||
args.hideVisibleOverlay ||
|
args.hideVisibleOverlay ||
|
||||||
args.copySubtitle ||
|
args.copySubtitle ||
|
||||||
args.copySubtitleMultiple ||
|
args.copySubtitleMultiple ||
|
||||||
args.mineSentence ||
|
args.mineSentence ||
|
||||||
args.mineSentenceMultiple ||
|
args.mineSentenceMultiple ||
|
||||||
args.updateLastCardFromClipboard ||
|
args.updateLastCardFromClipboard ||
|
||||||
args.refreshKnownWords ||
|
args.refreshKnownWords ||
|
||||||
args.toggleSecondarySub ||
|
args.toggleSecondarySub ||
|
||||||
args.triggerFieldGrouping ||
|
args.triggerFieldGrouping ||
|
||||||
args.triggerSubsync ||
|
args.triggerSubsync ||
|
||||||
args.markAudioCard ||
|
args.markAudioCard ||
|
||||||
args.openRuntimeOptions ||
|
args.openRuntimeOptions ||
|
||||||
args.anilistStatus ||
|
args.anilistStatus ||
|
||||||
args.anilistLogout ||
|
args.anilistLogout ||
|
||||||
args.anilistSetup ||
|
args.anilistSetup ||
|
||||||
args.anilistRetryQueue ||
|
args.anilistRetryQueue ||
|
||||||
args.dictionary ||
|
args.dictionary ||
|
||||||
args.jellyfin ||
|
args.jellyfin ||
|
||||||
args.jellyfinLogin ||
|
args.jellyfinLogin ||
|
||||||
args.jellyfinLogout ||
|
args.jellyfinLogout ||
|
||||||
args.jellyfinLibraries ||
|
args.jellyfinLibraries ||
|
||||||
args.jellyfinItems ||
|
args.jellyfinItems ||
|
||||||
args.jellyfinSubtitles ||
|
args.jellyfinSubtitles ||
|
||||||
args.jellyfinPlay ||
|
args.jellyfinPlay ||
|
||||||
args.jellyfinRemoteAnnounce ||
|
args.jellyfinRemoteAnnounce ||
|
||||||
args.jellyfinPreviewAuth ||
|
args.jellyfinPreviewAuth ||
|
||||||
args.texthooker ||
|
args.texthooker ||
|
||||||
args.help,
|
args.help
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,10 +85,7 @@ export function shouldAutoOpenFirstRunSetup(args: CliArgs): boolean {
|
|||||||
return !hasAnyStartupCommandBeyondSetup(args);
|
return !hasAnyStartupCommandBeyondSetup(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPluginStatus(
|
function getPluginStatus(state: SetupState, pluginInstalled: boolean): SetupStatusSnapshot['pluginStatus'] {
|
||||||
state: SetupState,
|
|
||||||
pluginInstalled: boolean,
|
|
||||||
): SetupStatusSnapshot['pluginStatus'] {
|
|
||||||
if (pluginInstalled) return 'installed';
|
if (pluginInstalled) return 'installed';
|
||||||
if (state.pluginInstallStatus === 'skipped') return 'skipped';
|
if (state.pluginInstallStatus === 'skipped') return 'skipped';
|
||||||
if (state.pluginInstallStatus === 'failed') return 'failed';
|
if (state.pluginInstallStatus === 'failed') return 'failed';
|
||||||
|
|||||||
@@ -253,9 +253,7 @@ export function createHandleFirstRunSetupNavigationHandler(deps: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createOpenFirstRunSetupWindowHandler<
|
export function createOpenFirstRunSetupWindowHandler<TWindow extends FirstRunSetupWindowLike>(deps: {
|
||||||
TWindow extends FirstRunSetupWindowLike,
|
|
||||||
>(deps: {
|
|
||||||
maybeFocusExistingSetupWindow: () => boolean;
|
maybeFocusExistingSetupWindow: () => boolean;
|
||||||
createSetupWindow: () => TWindow;
|
createSetupWindow: () => TWindow;
|
||||||
getSetupSnapshot: () => Promise<FirstRunSetupHtmlModel>;
|
getSetupSnapshot: () => Promise<FirstRunSetupHtmlModel>;
|
||||||
@@ -281,7 +279,9 @@ export function createOpenFirstRunSetupWindowHandler<
|
|||||||
const render = async (): Promise<void> => {
|
const render = async (): Promise<void> => {
|
||||||
const model = await deps.getSetupSnapshot();
|
const model = await deps.getSetupSnapshot();
|
||||||
const html = deps.buildSetupHtml(model);
|
const html = deps.buildSetupHtml(model);
|
||||||
await setupWindow.loadURL(`data:text/html;charset=utf-8,${deps.encodeURIComponent(html)}`);
|
await setupWindow.loadURL(
|
||||||
|
`data:text/html;charset=utf-8,${deps.encodeURIComponent(html)}`,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNavigation = createHandleFirstRunSetupNavigationHandler({
|
const handleNavigation = createHandleFirstRunSetupNavigationHandler({
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ test('ipc bridge action main deps builders map callbacks', async () => {
|
|||||||
playNextSubtitle: () => {},
|
playNextSubtitle: () => {},
|
||||||
shiftSubDelayToAdjacentSubtitle: async () => {},
|
shiftSubDelayToAdjacentSubtitle: async () => {},
|
||||||
sendMpvCommand: () => {},
|
sendMpvCommand: () => {},
|
||||||
getMpvClient: () => null,
|
|
||||||
isMpvConnected: () => true,
|
isMpvConnected: () => true,
|
||||||
hasRuntimeOptionsManager: () => true,
|
hasRuntimeOptionsManager: () => true,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ test('handle mpv command handler forwards command and built deps', () => {
|
|||||||
playNextSubtitle: () => {},
|
playNextSubtitle: () => {},
|
||||||
shiftSubDelayToAdjacentSubtitle: async () => {},
|
shiftSubDelayToAdjacentSubtitle: async () => {},
|
||||||
sendMpvCommand: () => {},
|
sendMpvCommand: () => {},
|
||||||
getMpvClient: () => null,
|
|
||||||
isMpvConnected: () => true,
|
isMpvConnected: () => true,
|
||||||
hasRuntimeOptionsManager: () => true,
|
hasRuntimeOptionsManager: () => true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ test('ipc mpv command main deps builder maps callbacks', () => {
|
|||||||
calls.push(`shift:${direction}`);
|
calls.push(`shift:${direction}`);
|
||||||
},
|
},
|
||||||
sendMpvCommand: (command) => calls.push(`cmd:${command.join(':')}`),
|
sendMpvCommand: (command) => calls.push(`cmd:${command.join(':')}`),
|
||||||
getMpvClient: () => ({ connected: true, requestProperty: async () => null }),
|
|
||||||
isMpvConnected: () => true,
|
isMpvConnected: () => true,
|
||||||
hasRuntimeOptionsManager: () => false,
|
hasRuntimeOptionsManager: () => false,
|
||||||
})();
|
})();
|
||||||
@@ -28,7 +27,6 @@ test('ipc mpv command main deps builder maps callbacks', () => {
|
|||||||
deps.playNextSubtitle();
|
deps.playNextSubtitle();
|
||||||
void deps.shiftSubDelayToAdjacentSubtitle('next');
|
void deps.shiftSubDelayToAdjacentSubtitle('next');
|
||||||
deps.sendMpvCommand(['show-text', 'ok']);
|
deps.sendMpvCommand(['show-text', 'ok']);
|
||||||
assert.equal(typeof deps.getMpvClient()?.requestProperty, 'function');
|
|
||||||
assert.equal(deps.isMpvConnected(), true);
|
assert.equal(deps.isMpvConnected(), true);
|
||||||
assert.equal(deps.hasRuntimeOptionsManager(), false);
|
assert.equal(deps.hasRuntimeOptionsManager(), false);
|
||||||
assert.deepEqual(calls, [
|
assert.deepEqual(calls, [
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export function createBuildMpvCommandFromIpcRuntimeMainDepsHandler(
|
|||||||
playNextSubtitle: () => deps.playNextSubtitle(),
|
playNextSubtitle: () => deps.playNextSubtitle(),
|
||||||
shiftSubDelayToAdjacentSubtitle: (direction) => deps.shiftSubDelayToAdjacentSubtitle(direction),
|
shiftSubDelayToAdjacentSubtitle: (direction) => deps.shiftSubDelayToAdjacentSubtitle(direction),
|
||||||
sendMpvCommand: (command: (string | number)[]) => deps.sendMpvCommand(command),
|
sendMpvCommand: (command: (string | number)[]) => deps.sendMpvCommand(command),
|
||||||
getMpvClient: () => deps.getMpvClient(),
|
|
||||||
isMpvConnected: () => deps.isMpvConnected(),
|
isMpvConnected: () => deps.isMpvConnected(),
|
||||||
hasRuntimeOptionsManager: () => deps.hasRuntimeOptionsManager(),
|
hasRuntimeOptionsManager: () => deps.hasRuntimeOptionsManager(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import assert from 'node:assert/strict';
|
|
||||||
import test from 'node:test';
|
|
||||||
import { resolveProxyCommandOsdRuntime } from './mpv-proxy-osd';
|
|
||||||
|
|
||||||
function createClient() {
|
|
||||||
return {
|
|
||||||
connected: true,
|
|
||||||
requestProperty: async (name: string) => {
|
|
||||||
if (name === 'sid') return 3;
|
|
||||||
if (name === 'secondary-sid') return 8;
|
|
||||||
if (name === 'track-list') {
|
|
||||||
return [
|
|
||||||
{ id: 3, type: 'sub', title: 'Japanese', selected: true, external: false },
|
|
||||||
{ id: 8, type: 'sub', title: 'English Commentary', external: true },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test('resolveProxyCommandOsdRuntime formats the active primary subtitle track label', async () => {
|
|
||||||
const result = await resolveProxyCommandOsdRuntime(['cycle', 'sid'], () => createClient());
|
|
||||||
assert.equal(result, 'Subtitle track: Internal #3 - Japanese (active)');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('resolveProxyCommandOsdRuntime formats the active secondary subtitle track label', async () => {
|
|
||||||
const result = await resolveProxyCommandOsdRuntime(
|
|
||||||
['set_property', 'secondary-sid', 'auto'],
|
|
||||||
() => createClient(),
|
|
||||||
);
|
|
||||||
assert.equal(result, 'Secondary subtitle track: External #8 - English Commentary');
|
|
||||||
});
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
type MpvPropertyClientLike = {
|
|
||||||
connected: boolean;
|
|
||||||
requestProperty: (name: string) => Promise<unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type MpvSubtitleTrack = {
|
|
||||||
id?: number;
|
|
||||||
type?: string;
|
|
||||||
selected?: boolean;
|
|
||||||
external?: boolean;
|
|
||||||
lang?: string;
|
|
||||||
title?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getTrackOsdPrefix(command: (string | number)[]): string | null {
|
|
||||||
const operation = typeof command[0] === 'string' ? command[0] : '';
|
|
||||||
const property = typeof command[1] === 'string' ? command[1] : '';
|
|
||||||
const modifiesProperty =
|
|
||||||
operation === 'add' ||
|
|
||||||
operation === 'set' ||
|
|
||||||
operation === 'set_property' ||
|
|
||||||
operation === 'cycle' ||
|
|
||||||
operation === 'cycle-values' ||
|
|
||||||
operation === 'multiply';
|
|
||||||
if (!modifiesProperty) return null;
|
|
||||||
if (property === 'sid') return 'Subtitle track';
|
|
||||||
if (property === 'secondary-sid') return 'Secondary subtitle track';
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseTrackId(value: unknown): number | null {
|
|
||||||
if (typeof value === 'number' && Number.isInteger(value)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
const trimmed = value.trim();
|
|
||||||
if (!trimmed.length || trimmed === 'no' || trimmed === 'auto') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const parsed = Number(trimmed);
|
|
||||||
if (Number.isInteger(parsed)) {
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeTrackList(trackListRaw: unknown): MpvSubtitleTrack[] {
|
|
||||||
if (!Array.isArray(trackListRaw)) return [];
|
|
||||||
return trackListRaw
|
|
||||||
.filter(
|
|
||||||
(track): track is Record<string, unknown> => Boolean(track) && typeof track === 'object',
|
|
||||||
)
|
|
||||||
.map((track) => {
|
|
||||||
const id = parseTrackId(track.id);
|
|
||||||
return {
|
|
||||||
...track,
|
|
||||||
id: id === null ? undefined : id,
|
|
||||||
} as MpvSubtitleTrack;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatSubtitleTrackLabel(track: MpvSubtitleTrack): string {
|
|
||||||
const trackId = typeof track.id === 'number' ? track.id : -1;
|
|
||||||
const source = track.external ? 'External' : 'Internal';
|
|
||||||
const label = track.lang || track.title || 'unknown';
|
|
||||||
const active = track.selected ? ' (active)' : '';
|
|
||||||
return `${source} #${trackId} - ${label}${active}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function resolveProxyCommandOsdRuntime(
|
|
||||||
command: (string | number)[],
|
|
||||||
getMpvClient: () => MpvPropertyClientLike | null,
|
|
||||||
): Promise<string | null> {
|
|
||||||
const prefix = getTrackOsdPrefix(command);
|
|
||||||
if (!prefix) return null;
|
|
||||||
|
|
||||||
const client = getMpvClient();
|
|
||||||
if (!client?.connected) return null;
|
|
||||||
|
|
||||||
const property = prefix === 'Subtitle track' ? 'sid' : 'secondary-sid';
|
|
||||||
const [trackListRaw, trackIdRaw] = await Promise.all([
|
|
||||||
client.requestProperty('track-list'),
|
|
||||||
client.requestProperty(property),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const trackId = parseTrackId(trackIdRaw);
|
|
||||||
if (trackId === null) {
|
|
||||||
return `${prefix}: none`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const track = normalizeTrackList(trackListRaw).find(
|
|
||||||
(entry) => entry.type === 'sub' && entry.id === trackId,
|
|
||||||
);
|
|
||||||
if (!track) {
|
|
||||||
return `${prefix}: #${trackId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${prefix}: ${formatSubtitleTrackLabel(track)}`;
|
|
||||||
}
|
|
||||||
@@ -516,11 +516,11 @@ body.settings-modal-open #subtitleContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#subtitleRoot
|
#subtitleRoot
|
||||||
.word:not(.word-known):not(.word-n-plus-one):not(.word-name-match):not(
|
.word:not(.word-known):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(
|
||||||
.word-frequency-single
|
.word-frequency-band-1
|
||||||
):not(.word-frequency-band-1):not(.word-frequency-band-2):not(.word-frequency-band-3):not(
|
):not(.word-frequency-band-2):not(.word-frequency-band-3):not(.word-frequency-band-4):not(
|
||||||
.word-frequency-band-4
|
.word-frequency-band-5
|
||||||
):not(.word-frequency-band-5):hover {
|
):hover {
|
||||||
background: var(--subtitle-hover-token-background-color, rgba(54, 58, 79, 0.84));
|
background: var(--subtitle-hover-token-background-color, rgba(54, 58, 79, 0.84));
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||||
@@ -558,11 +558,9 @@ body.settings-modal-open #subtitleContainer {
|
|||||||
#subtitleRoot
|
#subtitleRoot
|
||||||
.word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not(
|
.word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not(
|
||||||
.word-known
|
.word-known
|
||||||
):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(
|
):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(.word-frequency-band-1):not(
|
||||||
.word-frequency-band-1
|
.word-frequency-band-2
|
||||||
):not(.word-frequency-band-2):not(.word-frequency-band-3):not(.word-frequency-band-4):not(
|
):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5):hover {
|
||||||
.word-frequency-band-5
|
|
||||||
):hover {
|
|
||||||
color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||||
-webkit-text-fill-color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
-webkit-text-fill-color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||||
}
|
}
|
||||||
@@ -638,19 +636,15 @@ body.settings-modal-open #subtitleContainer {
|
|||||||
#subtitleRoot
|
#subtitleRoot
|
||||||
.word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not(
|
.word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not(
|
||||||
.word-known
|
.word-known
|
||||||
):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(
|
):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(.word-frequency-band-1):not(
|
||||||
.word-frequency-band-1
|
.word-frequency-band-2
|
||||||
):not(.word-frequency-band-2):not(.word-frequency-band-3):not(.word-frequency-band-4):not(
|
):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5)::selection,
|
||||||
.word-frequency-band-5
|
|
||||||
)::selection,
|
|
||||||
#subtitleRoot
|
#subtitleRoot
|
||||||
.word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not(
|
.word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not(
|
||||||
.word-known
|
.word-known
|
||||||
):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(
|
):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(.word-frequency-band-1):not(
|
||||||
.word-frequency-band-1
|
.word-frequency-band-2
|
||||||
):not(.word-frequency-band-2):not(.word-frequency-band-3):not(.word-frequency-band-4):not(
|
):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5)
|
||||||
.word-frequency-band-5
|
|
||||||
)
|
|
||||||
.c::selection {
|
.c::selection {
|
||||||
color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||||
-webkit-text-fill-color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
-webkit-text-fill-color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||||
|
|||||||
@@ -114,8 +114,7 @@ function installFakeDocument() {
|
|||||||
|
|
||||||
function collectWordNodes(root: FakeElement): FakeElement[] {
|
function collectWordNodes(root: FakeElement): FakeElement[] {
|
||||||
return root.childNodes.filter(
|
return root.childNodes.filter(
|
||||||
(child): child is FakeElement =>
|
(child): child is FakeElement => child instanceof FakeElement && child.className.includes('word'),
|
||||||
child instanceof FakeElement && child.className.includes('word'),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,15 +137,17 @@ function extractClassBlock(cssText: string, selector: string): string {
|
|||||||
const ruleRegex = /([^{}]+)\{([^}]*)\}/g;
|
const ruleRegex = /([^{}]+)\{([^}]*)\}/g;
|
||||||
let match: RegExpExecArray | null = null;
|
let match: RegExpExecArray | null = null;
|
||||||
let fallbackBlock = '';
|
let fallbackBlock = '';
|
||||||
const normalizedSelector = normalizeCssSelector(selector);
|
|
||||||
|
|
||||||
while ((match = ruleRegex.exec(cssText)) !== null) {
|
while ((match = ruleRegex.exec(cssText)) !== null) {
|
||||||
const selectorsBlock = match[1]?.trim() ?? '';
|
const selectorsBlock = match[1]?.trim() ?? '';
|
||||||
const selectorBlock = match[2] ?? '';
|
const selectorBlock = match[2] ?? '';
|
||||||
|
|
||||||
const selectors = splitCssSelectors(selectorsBlock);
|
const selectors = selectorsBlock
|
||||||
|
.split(',')
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter((entry) => entry.length > 0);
|
||||||
|
|
||||||
if (selectors.some((entry) => normalizeCssSelector(entry) === normalizedSelector)) {
|
if (selectors.includes(selector)) {
|
||||||
if (selectors.length === 1) {
|
if (selectors.length === 1) {
|
||||||
return selectorBlock;
|
return selectorBlock;
|
||||||
}
|
}
|
||||||
@@ -164,53 +165,6 @@ function extractClassBlock(cssText: string, selector: string): string {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitCssSelectors(selectorsBlock: string): string[] {
|
|
||||||
const selectors: string[] = [];
|
|
||||||
let current = '';
|
|
||||||
let parenDepth = 0;
|
|
||||||
|
|
||||||
for (const char of selectorsBlock) {
|
|
||||||
if (char === '(') {
|
|
||||||
parenDepth += 1;
|
|
||||||
current += char;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (char === ')') {
|
|
||||||
parenDepth = Math.max(0, parenDepth - 1);
|
|
||||||
current += char;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (char === ',' && parenDepth === 0) {
|
|
||||||
const trimmed = current.trim();
|
|
||||||
if (trimmed.length > 0) {
|
|
||||||
selectors.push(trimmed);
|
|
||||||
}
|
|
||||||
current = '';
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
current += char;
|
|
||||||
}
|
|
||||||
|
|
||||||
const trimmed = current.trim();
|
|
||||||
if (trimmed.length > 0) {
|
|
||||||
selectors.push(trimmed);
|
|
||||||
}
|
|
||||||
|
|
||||||
return selectors;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeCssSelector(selector: string): string {
|
|
||||||
return selector
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.replace(/\(\s+/g, '(')
|
|
||||||
.replace(/\s+\)/g, ')')
|
|
||||||
.replace(/\s*,\s*/g, ', ')
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
test('computeWordClass preserves known and n+1 classes while adding JLPT classes', () => {
|
test('computeWordClass preserves known and n+1 classes while adding JLPT classes', () => {
|
||||||
const knownJlpt = createToken({
|
const knownJlpt = createToken({
|
||||||
isKnown: true,
|
isKnown: true,
|
||||||
@@ -714,21 +668,9 @@ test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => {
|
|||||||
);
|
);
|
||||||
assert.match(jlptTooltipKeyboardSelectedBlock, /opacity:\s*1;/);
|
assert.match(jlptTooltipKeyboardSelectedBlock, /opacity:\s*1;/);
|
||||||
|
|
||||||
const plainWordHoverBlock = extractClassBlock(
|
assert.match(
|
||||||
cssText,
|
cssText,
|
||||||
'#subtitleRoot .word:not(.word-known):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(.word-frequency-band-1):not(.word-frequency-band-2):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5):hover',
|
/#subtitleRoot\s+\.word:not\(\.word-known\):not\(\.word-n-plus-one\):not\(\.word-name-match\):not\(\.word-frequency-single\):not\(\s*\.word-frequency-band-1\s*\):not\(\.word-frequency-band-2\):not\(\.word-frequency-band-3\):not\(\.word-frequency-band-4\):not\(\s*\.word-frequency-band-5\s*\):hover\s*\{[\s\S]*?background:\s*var\(--subtitle-hover-token-background-color,\s*rgba\(54,\s*58,\s*79,\s*0\.84\)\);[\s\S]*?color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;[\s\S]*?-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
||||||
);
|
|
||||||
assert.match(
|
|
||||||
plainWordHoverBlock,
|
|
||||||
/background:\s*var\(--subtitle-hover-token-background-color,\s*rgba\(54,\s*58,\s*79,\s*0\.84\)\);/,
|
|
||||||
);
|
|
||||||
assert.match(
|
|
||||||
plainWordHoverBlock,
|
|
||||||
/color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
|
||||||
);
|
|
||||||
assert.match(
|
|
||||||
plainWordHoverBlock,
|
|
||||||
/-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const coloredWordHoverBlock = extractClassBlock(cssText, '#subtitleRoot .word.word-known:hover');
|
const coloredWordHoverBlock = extractClassBlock(cssText, '#subtitleRoot .word.word-known:hover');
|
||||||
@@ -764,31 +706,13 @@ test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => {
|
|||||||
assert.match(coloredCharHoverBlock, /background:\s*transparent;/);
|
assert.match(coloredCharHoverBlock, /background:\s*transparent;/);
|
||||||
assert.match(coloredCharHoverBlock, /color:\s*inherit\s*!important;/);
|
assert.match(coloredCharHoverBlock, /color:\s*inherit\s*!important;/);
|
||||||
|
|
||||||
const jlptOnlyHoverBlock = extractClassBlock(
|
assert.match(
|
||||||
cssText,
|
cssText,
|
||||||
'#subtitleRoot .word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not(.word-known):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(.word-frequency-band-1):not(.word-frequency-band-2):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5):hover',
|
/\.word:is\(\.word-jlpt-n1,\s*\.word-jlpt-n2,\s*\.word-jlpt-n3,\s*\.word-jlpt-n4,\s*\.word-jlpt-n5\):not\(\s*\.word-known\s*\):not\(\.word-n-plus-one\):not\(\.word-name-match\):not\(\.word-frequency-single\):not\(\.word-frequency-band-1\):not\(\s*\.word-frequency-band-2\s*\):not\(\.word-frequency-band-3\):not\(\.word-frequency-band-4\):not\(\.word-frequency-band-5\):hover\s*\{[\s\S]*?color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;[\s\S]*?-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
jlptOnlyHoverBlock,
|
cssText,
|
||||||
/color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
/\.word:is\(\.word-jlpt-n1,\s*\.word-jlpt-n2,\s*\.word-jlpt-n3,\s*\.word-jlpt-n4,\s*\.word-jlpt-n5\):not\(\s*\.word-known\s*\):not\(\.word-n-plus-one\):not\(\.word-name-match\):not\(\.word-frequency-single\):not\(\.word-frequency-band-1\):not\(\s*\.word-frequency-band-2\s*\):not\(\.word-frequency-band-3\):not\(\.word-frequency-band-4\):not\(\.word-frequency-band-5\)::selection[\s\S]*?color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;[\s\S]*?-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
||||||
);
|
|
||||||
assert.match(
|
|
||||||
jlptOnlyHoverBlock,
|
|
||||||
/-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
|
||||||
);
|
|
||||||
assert.match(
|
|
||||||
extractClassBlock(
|
|
||||||
cssText,
|
|
||||||
'#subtitleRoot .word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not(.word-known):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(.word-frequency-band-1):not(.word-frequency-band-2):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5)::selection',
|
|
||||||
),
|
|
||||||
/color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
|
||||||
);
|
|
||||||
assert.match(
|
|
||||||
extractClassBlock(
|
|
||||||
cssText,
|
|
||||||
'#subtitleRoot .word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not(.word-known):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(.word-frequency-band-1):not(.word-frequency-band-2):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5)::selection',
|
|
||||||
),
|
|
||||||
/-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectionBlock = extractClassBlock(cssText, '#subtitleRoot::selection');
|
const selectionBlock = extractClassBlock(cssText, '#subtitleRoot::selection');
|
||||||
|
|||||||
@@ -265,7 +265,10 @@ function renderWithTokens(
|
|||||||
span.dataset.tokenIndex = String(segment.tokenIndex);
|
span.dataset.tokenIndex = String(segment.tokenIndex);
|
||||||
if (token.reading) span.dataset.reading = token.reading;
|
if (token.reading) span.dataset.reading = token.reading;
|
||||||
if (token.headword) span.dataset.headword = token.headword;
|
if (token.headword) span.dataset.headword = token.headword;
|
||||||
const frequencyRankLabel = getFrequencyRankLabelForToken(token, resolvedTokenRenderSettings);
|
const frequencyRankLabel = getFrequencyRankLabelForToken(
|
||||||
|
token,
|
||||||
|
resolvedTokenRenderSettings,
|
||||||
|
);
|
||||||
if (frequencyRankLabel) {
|
if (frequencyRankLabel) {
|
||||||
span.dataset.frequencyRank = frequencyRankLabel;
|
span.dataset.frequencyRank = frequencyRankLabel;
|
||||||
}
|
}
|
||||||
@@ -301,7 +304,10 @@ function renderWithTokens(
|
|||||||
span.dataset.tokenIndex = String(index);
|
span.dataset.tokenIndex = String(index);
|
||||||
if (token.reading) span.dataset.reading = token.reading;
|
if (token.reading) span.dataset.reading = token.reading;
|
||||||
if (token.headword) span.dataset.headword = token.headword;
|
if (token.headword) span.dataset.headword = token.headword;
|
||||||
const frequencyRankLabel = getFrequencyRankLabelForToken(token, resolvedTokenRenderSettings);
|
const frequencyRankLabel = getFrequencyRankLabelForToken(
|
||||||
|
token,
|
||||||
|
resolvedTokenRenderSettings,
|
||||||
|
);
|
||||||
if (frequencyRankLabel) {
|
if (frequencyRankLabel) {
|
||||||
span.dataset.frequencyRank = frequencyRankLabel;
|
span.dataset.frequencyRank = frequencyRankLabel;
|
||||||
}
|
}
|
||||||
@@ -407,7 +413,10 @@ export function computeWordClass(
|
|||||||
tokenRenderSettings?.bandedColors,
|
tokenRenderSettings?.bandedColors,
|
||||||
DEFAULT_FREQUENCY_RENDER_SETTINGS.bandedColors,
|
DEFAULT_FREQUENCY_RENDER_SETTINGS.bandedColors,
|
||||||
),
|
),
|
||||||
topX: sanitizeFrequencyTopX(tokenRenderSettings?.topX, DEFAULT_FREQUENCY_RENDER_SETTINGS.topX),
|
topX: sanitizeFrequencyTopX(
|
||||||
|
tokenRenderSettings?.topX,
|
||||||
|
DEFAULT_FREQUENCY_RENDER_SETTINGS.topX,
|
||||||
|
),
|
||||||
singleColor: sanitizeHexColor(
|
singleColor: sanitizeHexColor(
|
||||||
tokenRenderSettings?.singleColor,
|
tokenRenderSettings?.singleColor,
|
||||||
DEFAULT_FREQUENCY_RENDER_SETTINGS.singleColor,
|
DEFAULT_FREQUENCY_RENDER_SETTINGS.singleColor,
|
||||||
|
|||||||
@@ -43,10 +43,7 @@ test('ensureDefaultConfigBootstrap creates config dir and default jsonc only whe
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(fs.existsSync(configDir), true);
|
assert.equal(fs.existsSync(configDir), true);
|
||||||
assert.equal(
|
assert.equal(fs.readFileSync(path.join(configDir, 'config.jsonc'), 'utf8'), '{\n "logging": {}\n}\n');
|
||||||
fs.readFileSync(path.join(configDir, 'config.jsonc'), 'utf8'),
|
|
||||||
'{\n "logging": {}\n}\n',
|
|
||||||
);
|
|
||||||
|
|
||||||
fs.writeFileSync(path.join(configDir, 'config.json'), '{"keep":true}\n');
|
fs.writeFileSync(path.join(configDir, 'config.json'), '{"keep":true}\n');
|
||||||
fs.rmSync(path.join(configDir, 'config.jsonc'));
|
fs.rmSync(path.join(configDir, 'config.jsonc'));
|
||||||
|
|||||||
@@ -162,10 +162,7 @@ export function ensureDefaultConfigBootstrap(options: {
|
|||||||
const writeFileSync = options.writeFileSync ?? fs.writeFileSync;
|
const writeFileSync = options.writeFileSync ?? fs.writeFileSync;
|
||||||
|
|
||||||
mkdirSync(options.configDir, { recursive: true });
|
mkdirSync(options.configDir, { recursive: true });
|
||||||
if (
|
if (existsSync(options.configFilePaths.jsoncPath) || existsSync(options.configFilePaths.jsonPath)) {
|
||||||
existsSync(options.configFilePaths.jsoncPath) ||
|
|
||||||
existsSync(options.configFilePaths.jsonPath)
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +178,7 @@ export function resolveDefaultMpvInstallPaths(
|
|||||||
platform === 'darwin'
|
platform === 'darwin'
|
||||||
? path.join(homeDir, 'Library', 'Application Support', 'mpv')
|
? path.join(homeDir, 'Library', 'Application Support', 'mpv')
|
||||||
: platform === 'linux'
|
: platform === 'linux'
|
||||||
? path.join(xdgConfigHome?.trim() || path.join(homeDir, '.config'), 'mpv')
|
? path.join((xdgConfigHome?.trim() || path.join(homeDir, '.config')), 'mpv')
|
||||||
: path.join(homeDir, 'AppData', 'Roaming', 'mpv');
|
: path.join(homeDir, 'AppData', 'Roaming', 'mpv');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import path from 'node:path';
|
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { pathToFileURL } from 'node:url';
|
|
||||||
|
|
||||||
import { resolveYomitanExtensionPath } from './core/services/yomitan-extension-paths';
|
// @ts-expect-error Vendored Yomitan translator has no local TypeScript declarations.
|
||||||
|
import { Translator } from '../vendor/yomitan/js/language/translator.js';
|
||||||
|
|
||||||
type SortableTermEntry = {
|
type SortableTermEntry = {
|
||||||
matchPrimaryReading: boolean;
|
matchPrimaryReading: boolean;
|
||||||
@@ -29,20 +28,8 @@ type SortableDefinition = {
|
|||||||
index: number;
|
index: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function loadTranslator(): Promise<{ new (...args: unknown[]): { [key: string]: unknown } }> {
|
test('Translator prioritizes SubMiner term entries without changing dictionary index order', () => {
|
||||||
const yomitanRoot = resolveYomitanExtensionPath({ cwd: process.cwd() });
|
const translator = new Translator({});
|
||||||
assert.ok(yomitanRoot, 'Run `bun run build:yomitan` before Yomitan integration tests.');
|
|
||||||
const module = await import(
|
|
||||||
pathToFileURL(path.join(yomitanRoot, 'js', 'language', 'translator.js')).href
|
|
||||||
);
|
|
||||||
return module.Translator as { new (...args: unknown[]): { [key: string]: unknown } };
|
|
||||||
}
|
|
||||||
|
|
||||||
test('Translator prioritizes SubMiner term entries without changing dictionary index order', async () => {
|
|
||||||
const Translator = await loadTranslator();
|
|
||||||
const translator = new Translator({}) as {
|
|
||||||
_sortTermDictionaryEntries: (entries: unknown[]) => void;
|
|
||||||
};
|
|
||||||
const entries: SortableTermEntry[] = [
|
const entries: SortableTermEntry[] = [
|
||||||
{
|
{
|
||||||
matchPrimaryReading: true,
|
matchPrimaryReading: true,
|
||||||
@@ -77,11 +64,8 @@ test('Translator prioritizes SubMiner term entries without changing dictionary i
|
|||||||
assert.equal(entries[0]?.dictionaryAlias, 'SubMiner Character Dictionary');
|
assert.equal(entries[0]?.dictionaryAlias, 'SubMiner Character Dictionary');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Translator prioritizes SubMiner definitions without changing dictionary index order', async () => {
|
test('Translator prioritizes SubMiner definitions without changing dictionary index order', () => {
|
||||||
const Translator = await loadTranslator();
|
const translator = new Translator({});
|
||||||
const translator = new Translator({}) as {
|
|
||||||
_sortTermDictionaryEntryDefinitions: (definitions: unknown[]) => void;
|
|
||||||
};
|
|
||||||
const definitions: SortableDefinition[] = [
|
const definitions: SortableDefinition[] = [
|
||||||
{
|
{
|
||||||
dictionary: 'JMdict',
|
dictionary: 'JMdict',
|
||||||
|
|||||||
1
vendor/subminer-yomitan
vendored
1
vendor/subminer-yomitan
vendored
Submodule vendor/subminer-yomitan deleted from 9863d865e1
64
vendor/yomitan/action-popup.html
vendored
Normal file
64
vendor/yomitan/action-popup.html
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Yomitan Action Popup</title>
|
||||||
|
<link rel="icon" type="image/png" href="/images/icon16.png" sizes="16x16">
|
||||||
|
<link rel="icon" type="image/png" href="/images/icon19.png" sizes="19x19">
|
||||||
|
<link rel="icon" type="image/png" href="/images/icon32.png" sizes="32x32">
|
||||||
|
<link rel="icon" type="image/png" href="/images/icon38.png" sizes="38x38">
|
||||||
|
<link rel="icon" type="image/png" href="/images/icon48.png" sizes="48x48">
|
||||||
|
<link rel="icon" type="image/png" href="/images/icon64.png" sizes="64x64">
|
||||||
|
<link rel="icon" type="image/png" href="/images/icon128.png" sizes="128x128">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/material.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/action-popup.css">
|
||||||
|
<script src="/js/pages/action-popup-main.js" type="module"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="loading">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
<div id="action-popup">
|
||||||
|
<div class="action-container action-select-profile" hidden>
|
||||||
|
<div class="action-item-left">
|
||||||
|
<h2 class="action-title">Profile</h2>
|
||||||
|
</div>
|
||||||
|
<div class="action-item-right">
|
||||||
|
<select tabindex="0" class="profile-select" id="profile-select">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="action-container">
|
||||||
|
<div class="action-item-center">
|
||||||
|
<label class="toggle">
|
||||||
|
<input tabindex="0" type="checkbox" class="enable-search">
|
||||||
|
<div class="toggle-group">
|
||||||
|
<span class="toggle-on">On</span>
|
||||||
|
<span class="toggle-off">Off</span>
|
||||||
|
<span class="toggle-handle"></span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<p class="tooltip">Hover over text to scan</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="action-container action-buttons">
|
||||||
|
<button tabindex="0" type="button" class="low-emphasis action-open-settings" title="Settings" data-hotkey='["global:openSettingsPage","title","Settings ({0})"]'>
|
||||||
|
<div class="action-icon">
|
||||||
|
<span class="icon" data-icon="cog"></span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button tabindex="0" type="button" class="low-emphasis action-open-search" title="Search
Shift+click to open here" data-hotkey='["global:openSearchPage","title","Search ({0})\nShift+click to open here"]'>
|
||||||
|
<div class="action-icon">
|
||||||
|
<span class="icon" data-icon="magnifying-glass"></span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button tabindex="0" type="button" class="low-emphasis action-open-info" title="Information" data-hotkey='["global:openInfoPage","title","Information ({0})"]'>
|
||||||
|
<div class="action-icon">
|
||||||
|
<span class="icon" data-icon="question-mark-circle"></span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
10
vendor/yomitan/background.html
vendored
Normal file
10
vendor/yomitan/background.html
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Yomitan Background</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="module" src="js/background/background-main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
376
vendor/yomitan/css/action-popup.css
vendored
Normal file
376
vendor/yomitan/css/action-popup.css
vendored
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023-2025 Yomitan Authors
|
||||||
|
* Copyright (C) 2020-2022 Yomichan Authors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Variables */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--font-size-no-units: 14;
|
||||||
|
--font-size: calc(1px * var(--font-size-no-units));
|
||||||
|
--line-height-no-units: 20;
|
||||||
|
--line-height: calc(var(--line-height-no-units) / var(--font-size-no-units));
|
||||||
|
--background-color: #f8f9fa;
|
||||||
|
--text-color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme=dark] {
|
||||||
|
--background-color: #1e1e1e;
|
||||||
|
--text-color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Initilization */
|
||||||
|
|
||||||
|
body[data-loaded=true] #loading {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not([data-loaded=true]) #action-popup {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-mode=full] #action-popup {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
#action-popup {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding: 5px;
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Segoe UI', Tahoma, sans-serif;
|
||||||
|
font-size: var(--font-size);
|
||||||
|
width: max-content;
|
||||||
|
height: max-content;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle */
|
||||||
|
|
||||||
|
body[data-loaded=true] .toggle-group {
|
||||||
|
transition: transform 0.35s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle>input[type=checkbox] {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
display: block;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle>input[type=checkbox]:not(:checked)~.toggle-group {
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 85px;
|
||||||
|
height: 43px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #245580;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-group {
|
||||||
|
position: absolute;
|
||||||
|
width: 200%;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-on,
|
||||||
|
.toggle-off,
|
||||||
|
.toggle-handle {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: var(--font-size);
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: var(--line-height);
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-on,
|
||||||
|
.toggle-off {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-on {
|
||||||
|
padding-right: 24px;
|
||||||
|
left: 0;
|
||||||
|
right: 50%;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #2e6da4;
|
||||||
|
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), inset 0 3px 5px rgba(0, 0, 0, 0);
|
||||||
|
background-image: linear-gradient(225deg, #bc00ff, #00eeff);
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
}
|
||||||
|
input[type=checkbox]:focus~.toggle-group>.toggle-on,
|
||||||
|
input[type=checkbox]~.toggle-group>.toggle-on:hover {
|
||||||
|
filter: grayscale(30%);
|
||||||
|
}
|
||||||
|
input[type=checkbox]:focus:not(:focus-visible)~.toggle-group>.toggle-on:not(:hover) {
|
||||||
|
background-image: linear-gradient(225deg, #bc00ff, #00eeff);
|
||||||
|
}
|
||||||
|
input[type=checkbox]:focus-visible~.toggle-group>.toggle-on {
|
||||||
|
filter: grayscale(30%);
|
||||||
|
}
|
||||||
|
input[type=checkbox]~.toggle-group>.toggle-on:active,
|
||||||
|
input[type=checkbox]~.toggle-group>.toggle-on:active:focus {
|
||||||
|
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-off {
|
||||||
|
padding-left: 24px;
|
||||||
|
left: 50%;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-handle {
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0 1px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-off,
|
||||||
|
.toggle-handle {
|
||||||
|
color: #333333;
|
||||||
|
text-shadow: 0 1px 0 #ffffff;
|
||||||
|
background-color: #ffffff;
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
}
|
||||||
|
input[type=checkbox]:focus~.toggle-group>.toggle-off,
|
||||||
|
input[type=checkbox]~.toggle-group>.toggle-off:hover,
|
||||||
|
input[type=checkbox]~.toggle-group>.toggle-handle:hover {
|
||||||
|
background-color: #e6e6e6;
|
||||||
|
}
|
||||||
|
input[type=checkbox]:focus:not(:focus-visible)~.toggle-group>.toggle-off:not(:hover) {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
input[type=checkbox]:focus-visible~.toggle-group>.toggle-off,
|
||||||
|
input[type=checkbox]~.toggle-group>.toggle-off:hover {
|
||||||
|
background-color: #e6e6e6;
|
||||||
|
}
|
||||||
|
input[type=checkbox]~.toggle-group>.toggle-off:active,
|
||||||
|
input[type=checkbox]~.toggle-group>.toggle-handle:active,
|
||||||
|
input[type=checkbox]~.toggle-group>.toggle-off:active:focus,
|
||||||
|
input[type=checkbox]~.toggle-group>.toggle-handle:active:focus {
|
||||||
|
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action Containers */
|
||||||
|
|
||||||
|
h2.action-title {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.125em;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
.action-icon>.icon {
|
||||||
|
display: block;
|
||||||
|
background-color: var(--button-default-icon-color);
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.low-emphasis {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-item-left {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
align-self: center;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
.action-item-center {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
align-self: center;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.action-item-right {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
align-self: stretch;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-container:not([hidden]) {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-container button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-container.action-buttons {
|
||||||
|
gap: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Actions */
|
||||||
|
|
||||||
|
.action-container.action-select-profile {
|
||||||
|
position: relative;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
select.profile-select {
|
||||||
|
width: 7em;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--font-size);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip */
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
color: #808080;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
margin-top: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enable-dictionary-tooltip {
|
||||||
|
color: #f0ad4e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip>a:link, a:visited {
|
||||||
|
color: #f0ad4e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile overrides */
|
||||||
|
|
||||||
|
:root[data-mode=full] #action-popup {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
:root[data-mode=full] body {
|
||||||
|
min-width: 95%;
|
||||||
|
width: max-content;
|
||||||
|
font-size: calc(var(--font-size) * 2);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
:root[data-mode=full] .toggle-on, :root[data-mode=full] .toggle-off {
|
||||||
|
font-size: calc(var(--font-size) * 4);
|
||||||
|
}
|
||||||
|
:root[data-mode=full] .toggle-handle {
|
||||||
|
padding-left: 65px;
|
||||||
|
padding-right: 65px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
:root[data-mode=full] .toggle {
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 37.7%;
|
||||||
|
}
|
||||||
|
:root[data-mode=full] #extension-info {
|
||||||
|
max-width: 95vw;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
:root[data-mode=full] select.profile-select {
|
||||||
|
font-size: calc(var(--font-size) * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback Mobile overrides */
|
||||||
|
|
||||||
|
/* Treat devices that can't hover as mobile devices */
|
||||||
|
@media (hover: none) {
|
||||||
|
#action-popup {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
min-width: 95%;
|
||||||
|
width: max-content;
|
||||||
|
font-size: calc(var(--font-size) * 2);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.toggle-on, .toggle-off {
|
||||||
|
font-size: calc(var(--font-size) * 4);
|
||||||
|
}
|
||||||
|
.toggle-handle {
|
||||||
|
padding-left: 65px;
|
||||||
|
padding-right: 65px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.toggle {
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 37.7%;
|
||||||
|
}
|
||||||
|
#extension-info {
|
||||||
|
max-width: 95vw;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
select.profile-select {
|
||||||
|
font-size: calc(var(--font-size) * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode before themes are applied
|
||||||
|
DO NOT use this for normal theming */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body:not([data-loaded=true]) {
|
||||||
|
color: #cccccc;
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
vendor/yomitan/css/background.css
vendored
Normal file
30
vendor/yomitan/css/background.css
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023-2025 Yomitan Authors
|
||||||
|
* Copyright (C) 2022 Yomichan Authors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* stylelint-disable declaration-no-important */
|
||||||
|
#clipboard-rich-content-paste-target * {
|
||||||
|
background-image: none !important;
|
||||||
|
list-style-image: none !important;
|
||||||
|
content: none !important;
|
||||||
|
cursor: auto !important;
|
||||||
|
border-image-source: none !important;
|
||||||
|
offset-path: none !important;
|
||||||
|
-webkit-mask-image: none !important;
|
||||||
|
mask-image: none !important;
|
||||||
|
}
|
||||||
|
/* stylelint-enable declaration-no-important */
|
||||||
132
vendor/yomitan/css/display-pronunciation.css
vendored
Normal file
132
vendor/yomitan/css/display-pronunciation.css
vendored
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023-2025 Yomitan Authors
|
||||||
|
* Copyright (C) 2021-2022 Yomichan Authors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the entrys of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--pronunciation-annotation-color: #000000;
|
||||||
|
}
|
||||||
|
:root[data-theme=dark] {
|
||||||
|
--pronunciation-annotation-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pronunciation-downstep-notation {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pronunciation-text {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.pronunciation-mora {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.pronunciation-mora-line {
|
||||||
|
border-color: var(--pronunciation-annotation-color);
|
||||||
|
}
|
||||||
|
.pronunciation-mora[data-pitch=high]>.pronunciation-mora-line {
|
||||||
|
display: block;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0.1em;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top-width: 0.1em;
|
||||||
|
border-top-style: solid;
|
||||||
|
}
|
||||||
|
.pronunciation-mora[data-pitch=high][data-pitch-next=low]>.pronunciation-mora-line {
|
||||||
|
right: -0.1em;
|
||||||
|
height: 0.4em;
|
||||||
|
border-right-width: 0.1em;
|
||||||
|
border-right-style: solid;
|
||||||
|
}
|
||||||
|
.pronunciation-mora[data-pitch=high][data-pitch-next=low] {
|
||||||
|
padding-right: 0.1em;
|
||||||
|
margin-right: 0.1em;
|
||||||
|
}
|
||||||
|
.pronunciation-devoice-indicator {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 1.125em;
|
||||||
|
height: 1.125em;
|
||||||
|
border: calc(1.5em / var(--font-size-no-units)) dotted var(--danger-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 1;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
.pronunciation-nasal-indicator {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
right: -0.125em;
|
||||||
|
top: 0.125em;
|
||||||
|
width: 0.375em;
|
||||||
|
height: 0.375em;
|
||||||
|
border: calc(1.5em / var(--font-size-no-units)) solid var(--danger-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.pronunciation-nasal-diacritic {
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.pronunciation-character {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.pronunciation-character-group {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pronunciation-graph {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 1.5em;
|
||||||
|
}
|
||||||
|
.pronunciation-graph-line,
|
||||||
|
.pronunciation-graph-line-tail {
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--pronunciation-annotation-color);
|
||||||
|
stroke-width: 5;
|
||||||
|
}
|
||||||
|
.pronunciation-graph-line-tail {
|
||||||
|
stroke-dasharray: 5 5;
|
||||||
|
}
|
||||||
|
.pronunciation-graph-dot {
|
||||||
|
fill: var(--pronunciation-annotation-color);
|
||||||
|
stroke: var(--pronunciation-annotation-color);
|
||||||
|
stroke-width: 5;
|
||||||
|
}
|
||||||
|
.pronunciation-graph-dot-downstep1 {
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--pronunciation-annotation-color);
|
||||||
|
stroke-width: 5;
|
||||||
|
}
|
||||||
|
.pronunciation-graph-dot-downstep2 {
|
||||||
|
fill: var(--pronunciation-annotation-color);
|
||||||
|
}
|
||||||
|
.pronunciation-graph-triangle {
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--pronunciation-annotation-color);
|
||||||
|
stroke-width: 5;
|
||||||
|
}
|
||||||
2100
vendor/yomitan/css/display.css
vendored
Normal file
2100
vendor/yomitan/css/display.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1344
vendor/yomitan/css/material.css
vendored
Normal file
1344
vendor/yomitan/css/material.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
36
vendor/yomitan/css/permissions.css
vendored
Normal file
36
vendor/yomitan/css/permissions.css
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023-2025 Yomitan Authors
|
||||||
|
* Copyright (C) 2021-2022 Yomichan Authors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#permissions-origin-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
grid-template-rows: auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.permissions-origin-index {
|
||||||
|
margin: 0 0.5em;
|
||||||
|
}
|
||||||
|
input[type=text].permissions-origin-input {
|
||||||
|
width: auto;
|
||||||
|
justify-self: stretch;
|
||||||
|
}
|
||||||
|
.permissions-origin-button {
|
||||||
|
justify-self: center;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
50
vendor/yomitan/css/popup-outer.css
vendored
Normal file
50
vendor/yomitan/css/popup-outer.css
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023-2025 Yomitan Authors
|
||||||
|
* Copyright (C) 2016-2022 Yomichan Authors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
iframe.yomitan-popup {
|
||||||
|
all: initial;
|
||||||
|
font-size: 1px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1em solid #999999;
|
||||||
|
box-shadow: 0 0 10em rgba(0, 0, 0, 0.5);
|
||||||
|
position: fixed;
|
||||||
|
resize: none;
|
||||||
|
visibility: hidden;
|
||||||
|
z-index: 2147483647;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
iframe.yomitan-popup[data-theme=dark] {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
border-color: #666666;
|
||||||
|
}
|
||||||
|
iframe.yomitan-popup[data-outer-theme=dark] {
|
||||||
|
box-shadow: 0 0 10em rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
iframe.yomitan-popup[data-outer-theme=none] {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
iframe.yomitan-popup[data-popup-display-mode=full-width] {
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
iframe.yomitan-popup[data-popup-display-mode=full-width][data-below=true] {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
iframe.yomitan-popup[data-popup-display-mode=full-width]:not([data-below=true]) {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
164
vendor/yomitan/css/popup-preview.css
vendored
Normal file
164
vendor/yomitan/css/popup-preview.css
vendored
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023-2025 Yomitan Authors
|
||||||
|
* Copyright (C) 2020-2022 Yomichan Authors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--font-size-no-units: 14;
|
||||||
|
--font-size: calc(1px * var(--font-size-no-units));
|
||||||
|
|
||||||
|
--line-height-no-units: 20;
|
||||||
|
--line-height: calc(var(--line-height-no-units) / var(--font-size-no-units));
|
||||||
|
|
||||||
|
--animation-duration: 0s;
|
||||||
|
|
||||||
|
--example-text-color: #333333;
|
||||||
|
--background-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
:root[data-loaded=true] {
|
||||||
|
--animation-duration: 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme=dark] {
|
||||||
|
--example-text-color: #d4d4d4;
|
||||||
|
--background-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
html {
|
||||||
|
background-color: var(--background-color);
|
||||||
|
}
|
||||||
|
html.dark {
|
||||||
|
background-color: var(--background-color);
|
||||||
|
}
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
|
font-size: var(--font-size);
|
||||||
|
line-height: var(--line-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
min-width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 1em;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.content-body {
|
||||||
|
max-width: 100%;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
.top-options {
|
||||||
|
max-width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.top-options-left {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
.top-options-right {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-text-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.example-text {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.25em;
|
||||||
|
height: 1.25em;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid rgba(221, 221, 221, 0);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
outline: none;
|
||||||
|
color: var(--example-text-color);
|
||||||
|
background-color: transparent;
|
||||||
|
white-space: pre;
|
||||||
|
transition: background-color var(--animation-duration) linear 0s, border-color var(--animation-duration) linear 0s;
|
||||||
|
}
|
||||||
|
.example-text:hover,
|
||||||
|
.example-text-input {
|
||||||
|
border-color: #dddddd;
|
||||||
|
}
|
||||||
|
.example-text[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.example-text-input {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.example-text-input:not([hidden])+.example-text {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-placeholder {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 250px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0);
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.placeholder-info {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity var(--animation-duration) linear 0s, visibility 0s linear var(--animation-duration);
|
||||||
|
}
|
||||||
|
.placeholder-info.placeholder-info-visible {
|
||||||
|
color: var(--example-text-color);
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity var(--animation-duration) linear 0s, visibility 0s linear 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-button {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
.theme-button>input {
|
||||||
|
vertical-align: middle;
|
||||||
|
margin: 0 0.25em 0 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.theme-button>span {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.theme-button:hover>span {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
489
vendor/yomitan/css/search-settings.css
vendored
Normal file
489
vendor/yomitan/css/search-settings.css
vendored
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
:root {
|
||||||
|
--padding: 10px;
|
||||||
|
--padding-negative: calc(var(--padding) * -1);
|
||||||
|
--content-width: 700px;
|
||||||
|
--shadow-vertical: 0 1px 4px 0 var(--shadow-color), 0 2px 2px 0 var(--shadow-color);
|
||||||
|
--shadow-left: -1px 0 4px 0 var(--shadow-color), -2px 0 2px 0 var(--shadow-color);
|
||||||
|
--settings-group-horizontal-margin: 0;
|
||||||
|
--settings-group-inner-vertical-padding: 0.85em;
|
||||||
|
--settings-group-inner-horizontal-padding: 1.5em;
|
||||||
|
--settings-group-inner-horizontal-padding-half: calc(var(--settings-group-inner-horizontal-padding) * 0.5);
|
||||||
|
--settings-group-inner-horizontal-padding-half-wrappable: var(--settings-group-inner-horizontal-padding-half);
|
||||||
|
--settings-group-inner-horizontal-padding-fourth: calc(var(--settings-group-inner-horizontal-padding) * 0.25);
|
||||||
|
--settings-group-border-radius: 0.3em;
|
||||||
|
--settings-group-right-max-height: 40px;
|
||||||
|
--settings-group-wrap: nowrap;
|
||||||
|
--show-preview-label-height: 40px;
|
||||||
|
|
||||||
|
--font-size-no-units: 14;
|
||||||
|
--font-size: calc(1px * var(--font-size-no-units));
|
||||||
|
--font-size-small: 12px;
|
||||||
|
--outline-item-height: 40px;
|
||||||
|
--outline-item-icon-size: 32px;
|
||||||
|
--input-short-width: calc(var(--input-width-large) / 2 - var(--padding) / 2);
|
||||||
|
--input-short-height: 24px;
|
||||||
|
--input-medium-width: calc(var(--input-width-large) * 0.75);
|
||||||
|
--fab-button-size: 56px;
|
||||||
|
--fab-button-padding: 16px;
|
||||||
|
--modal-width: 600px;
|
||||||
|
--modal-height: 400px;
|
||||||
|
--modal-width-small: 400px;
|
||||||
|
--modal-height-small: 200px;
|
||||||
|
--modal-width-medium: 600px;
|
||||||
|
--modal-height-medium: 400px;
|
||||||
|
--modal-transition-offset: -64px;
|
||||||
|
--badge-size: 16px;
|
||||||
|
|
||||||
|
--link-color: var(--accent-color);
|
||||||
|
--link-color-hover: var(--accent-color-dark);
|
||||||
|
--separator-color1: #cccccc;
|
||||||
|
--separator-color2: #eeeeee;
|
||||||
|
--outline-item-background-color: rgba(13, 13, 13, 0);
|
||||||
|
--outline-item-background-color-hover: rgba(13, 13, 13, 0.15);
|
||||||
|
--warning-color: #96751c;
|
||||||
|
--warning-color-light: #edc75e;
|
||||||
|
--dim-background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
--content-dimmer-color: rgba(0, 0, 0, 0.1);
|
||||||
|
--advanced-color: #6640be;
|
||||||
|
--advanced-color-lighter: hsl(258, 50%, 75%);
|
||||||
|
--advanced-color-transparent25: rgba(102, 64, 190, 0.5);
|
||||||
|
|
||||||
|
--modal-padding-horizontal: 1em;
|
||||||
|
--modal-padding-vertical: 0.625em;
|
||||||
|
--modal-padding-vertical-half: calc(var(--modal-padding-vertical) * 0.5);
|
||||||
|
--modal-button-spacing: 0.625em;
|
||||||
|
}
|
||||||
|
:root:not([data-loaded=true]) {
|
||||||
|
--animation-duration: 0s;
|
||||||
|
}
|
||||||
|
:root[data-theme=dark] {
|
||||||
|
--separator-color1: #333333;
|
||||||
|
--separator-color2: #222222;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
background-color: var(--dim-background-color);
|
||||||
|
outline: none;
|
||||||
|
z-index: 10000;
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transition:
|
||||||
|
opacity var(--animation-duration2) ease-out,
|
||||||
|
visibility 0s linear;
|
||||||
|
}
|
||||||
|
.modal[hidden] {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition:
|
||||||
|
opacity var(--animation-duration2) ease-in,
|
||||||
|
visibility 0s linear var(--animation-duration2);
|
||||||
|
}
|
||||||
|
.modal[hidden]:not(.hidden-animating) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
width: var(--modal-width);
|
||||||
|
height: var(--modal-height);
|
||||||
|
background-color: var(--background-color-light);
|
||||||
|
flex: 0 1 auto;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
transform: translate(0, 0);
|
||||||
|
transition:
|
||||||
|
transform var(--animation-duration2) ease-out,
|
||||||
|
width var(--animation-duration2) ease-in-out,
|
||||||
|
height var(--animation-duration2) ease-in-out,
|
||||||
|
border-radius var(--animation-duration2) ease-in-out;
|
||||||
|
box-shadow: var(--shadow-vertical);
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.modal[hidden] .modal-content {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.modal-content.modal-content-small {
|
||||||
|
width: var(--modal-width-small);
|
||||||
|
min-height: var(--modal-height-small);
|
||||||
|
height: auto;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
.modal-content.modal-content-medium {
|
||||||
|
width: var(--modal-width-medium);
|
||||||
|
min-height: var(--modal-height-medium);
|
||||||
|
height: auto;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
.modal-content.modal-content-full {
|
||||||
|
width: var(--content-width);
|
||||||
|
height: 100%;
|
||||||
|
transform: translate(0, 0);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.modal[hidden] .modal-content {
|
||||||
|
transform: translate(0, var(--modal-transition-offset));
|
||||||
|
transition:
|
||||||
|
transform 0s linear var(--animation-duration2),
|
||||||
|
width var(--animation-duration2) ease-in-out,
|
||||||
|
height var(--animation-duration2) ease-in-out,
|
||||||
|
border-radius var(--animation-duration2) ease-in-out;
|
||||||
|
}
|
||||||
|
.modal-header {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: var(--modal-padding-vertical) var(--modal-padding-horizontal) var(--modal-padding-vertical-half);
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.modal-title {
|
||||||
|
font-size: 1.125em;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
.modal-footer {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: var(--modal-padding-vertical-half) var(--modal-padding-horizontal) var(--modal-padding-vertical);
|
||||||
|
margin-right: calc(var(--modal-button-spacing) * -1);
|
||||||
|
margin-top: calc(var(--modal-button-spacing) * -1);
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-items: flex-end;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.modal-footer>* {
|
||||||
|
margin-right: var(--modal-button-spacing);
|
||||||
|
margin-top: var(--modal-button-spacing);
|
||||||
|
}
|
||||||
|
.modal-body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: auto;
|
||||||
|
padding: var(--modal-padding-vertical-half) var(--modal-padding-horizontal);
|
||||||
|
}
|
||||||
|
.modal-body-addon {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: var(--modal-padding-vertical-half) var(--modal-padding-horizontal);
|
||||||
|
}
|
||||||
|
.modal-body>.settings-item,
|
||||||
|
.modal-settings-group>.settings-item {
|
||||||
|
margin-left: calc(var(--modal-padding-horizontal) * -1);
|
||||||
|
}
|
||||||
|
.modal-body .settings-item {
|
||||||
|
margin-right: calc(var(--modal-padding-horizontal) * -1);
|
||||||
|
}
|
||||||
|
.modal-body .settings-item+.settings-item {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
.modal-body .settings-item-left {
|
||||||
|
padding-left: var(--modal-padding-horizontal);
|
||||||
|
padding-top: var(--settings-group-inner-horizontal-padding-fourth);
|
||||||
|
padding-bottom: var(--settings-group-inner-horizontal-padding-fourth);
|
||||||
|
}
|
||||||
|
.modal-body .settings-item-right {
|
||||||
|
padding-right: var(--modal-padding-horizontal);
|
||||||
|
padding-top: var(--settings-group-inner-horizontal-padding-fourth);
|
||||||
|
padding-bottom: var(--settings-group-inner-horizontal-padding-fourth);
|
||||||
|
}
|
||||||
|
.modal-body .settings-item-children {
|
||||||
|
padding-left: var(--modal-padding-horizontal);
|
||||||
|
padding-right: var(--modal-padding-horizontal);
|
||||||
|
padding-bottom: var(--settings-group-inner-horizontal-padding-fourth);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.modal-left {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: transparent;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.modal-content-container {
|
||||||
|
pointer-events: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content-container>.modal-content,
|
||||||
|
.modal-content-container>.modal-content-dimmer {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content-container>.modal-content-dimmer {
|
||||||
|
background: var(--custom-css-modal-background);
|
||||||
|
width: var(--custom-css-dim-size);
|
||||||
|
height: 100%;
|
||||||
|
margin-right: calc(100% - var(--custom-css-dim-size));
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header-button-container {
|
||||||
|
margin-top: calc(-1 * var(--modal-padding-vertical-half));
|
||||||
|
margin-bottom: calc(-1 * var(--modal-padding-vertical-half));
|
||||||
|
}
|
||||||
|
.modal-header-button-group {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: var(--icon-button-size);
|
||||||
|
height: var(--icon-button-size);
|
||||||
|
}
|
||||||
|
.modal-header-button {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
button.icon-button.modal-header-button {
|
||||||
|
--button-content-color: var(--button-default-icon-color-light);
|
||||||
|
--button-hover-content-color: var(--button-default-icon-color);
|
||||||
|
--button-active-content-color: var(--button-default-icon-color);
|
||||||
|
}
|
||||||
|
button.icon-button.modal-header-button>.icon-button-inner>.icon {
|
||||||
|
transition: background-color var(--animation-duration) ease-in-out;
|
||||||
|
}
|
||||||
|
.modal-header-button[data-modal-action=expand],
|
||||||
|
.modal-header-button[data-modal-action=collapse] {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 1;
|
||||||
|
transition:
|
||||||
|
opacity var(--animation-duration2) ease-in-out 0s,
|
||||||
|
visibility 0s ease-in-out 0s;
|
||||||
|
}
|
||||||
|
.modal-content.modal-content-full .modal-header-button[data-modal-action=expand],
|
||||||
|
.modal-content:not(.modal-content-full) .modal-header-button[data-modal-action=collapse] {
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
transition:
|
||||||
|
opacity var(--animation-duration2) ease-in-out 0s,
|
||||||
|
visibility 0s ease-in-out var(--animation-duration2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-separator-line {
|
||||||
|
border-top: var(--thin-border-size) solid var(--separator-color1);
|
||||||
|
margin: 0 calc(var(--modal-padding-horizontal) * -1);
|
||||||
|
}
|
||||||
|
.modal-separator-line-light {
|
||||||
|
border-top: var(--thin-border-size) solid var(--separator-color2);
|
||||||
|
margin: 0 calc(var(--modal-padding-horizontal) * -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings styles */
|
||||||
|
.settings-group {
|
||||||
|
margin: 0 var(--settings-group-horizontal-margin);
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: var(--background-color-light);
|
||||||
|
box-shadow: var(--shadow-vertical);
|
||||||
|
border-radius: var(--settings-group-border-radius);
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
.settings-group.settings-group-top-margin {
|
||||||
|
margin-top: 1.0125em;
|
||||||
|
}
|
||||||
|
.settings-item {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.settings-item:not([hidden]) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.settings-item-outer {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.settings-item-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-content: stretch;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.settings-item-inner.settings-item-inner-wrappable {
|
||||||
|
flex-wrap: var(--settings-group-wrap);
|
||||||
|
}
|
||||||
|
.settings-item-left {
|
||||||
|
padding: var(--settings-group-inner-vertical-padding) var(--settings-group-inner-horizontal-padding-half) var(--settings-group-inner-vertical-padding) var(--settings-group-inner-horizontal-padding);
|
||||||
|
flex: 1 1 auto;
|
||||||
|
align-self: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.settings-item-left:last-child {
|
||||||
|
padding-right: var(--settings-group-inner-horizontal-padding);
|
||||||
|
}
|
||||||
|
.settings-item-right {
|
||||||
|
padding: var(--settings-group-inner-vertical-padding) var(--settings-group-inner-horizontal-padding) var(--settings-group-inner-vertical-padding) var(--settings-group-inner-horizontal-padding-half);
|
||||||
|
flex: 0 1 auto;
|
||||||
|
align-self: stretch;
|
||||||
|
max-height: var(--settings-group-right-max-height);
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.settings-item-inner.settings-item-inner-wrappable>.settings-item-left {
|
||||||
|
padding-right: var(--settings-group-inner-horizontal-padding-half-wrappable);
|
||||||
|
}
|
||||||
|
.settings-item-inner.settings-item-inner-wrappable>.settings-item-right {
|
||||||
|
padding-left: var(--settings-group-inner-horizontal-padding-half-wrappable);
|
||||||
|
}
|
||||||
|
.settings-item-center {
|
||||||
|
padding: var(--settings-group-inner-vertical-padding) var(--settings-group-inner-horizontal-padding) var(--settings-group-inner-vertical-padding) var(--settings-group-inner-horizontal-padding);
|
||||||
|
flex: 0 1 100%;
|
||||||
|
align-self: flex-start;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.settings-item+.settings-item {
|
||||||
|
border-top: var(--thin-border-size) solid var(--separator-color2);
|
||||||
|
}
|
||||||
|
.settings-item-description {
|
||||||
|
color: var(--text-color-light2);
|
||||||
|
}
|
||||||
|
.settings-item-right.open-panel-button-container {
|
||||||
|
padding: 0.25em 1em 0.25em 0.75em;
|
||||||
|
max-height: calc(var(--settings-group-right-max-height) + var(--settings-group-inner-vertical-padding) * 2);
|
||||||
|
}
|
||||||
|
.settings-item-children {
|
||||||
|
padding: 0em var(--settings-group-inner-horizontal-padding-half) var(--settings-group-inner-vertical-padding) var(--settings-group-inner-horizontal-padding);
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.settings-item-children.settings-item-children-group {
|
||||||
|
padding: 0 0 0 calc(var(--settings-group-inner-horizontal-padding) + var(--settings-group-inner-horizontal-padding));
|
||||||
|
}
|
||||||
|
.settings-item-children.settings-item-children-group .settings-item {
|
||||||
|
border-top: var(--thin-border-size) solid var(--separator-color2);
|
||||||
|
}
|
||||||
|
.settings-item-children.settings-item-children-group .settings-item-left {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.settings-item-children.settings-item-children-group .settings-item-inner.settings-item-inner-wrappable>.settings-item-left:not(:last-child) {
|
||||||
|
padding-right: calc(var(--settings-group-inner-horizontal-padding-half-wrappable) * 2);
|
||||||
|
}
|
||||||
|
.settings-item-children.settings-item-children-group .settings-item-inner.settings-item-inner-wrappable>.settings-item-right {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.settings-item-children.settings-item-children-group .settings-item-children {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.settings-item.settings-item-button,
|
||||||
|
a.settings-item.settings-item-button {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-color);
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: transparent;
|
||||||
|
transition: background-color var(--animation-duration) ease-in-out;
|
||||||
|
}
|
||||||
|
.settings-item.settings-item-button>.settings-item-inner,
|
||||||
|
.settings-item.settings-item-button>.settings-item-inner>.settings-item-left,
|
||||||
|
.settings-item.settings-item-button>.settings-item-inner>.settings-item-right {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.settings-item.settings-item-button:hover,
|
||||||
|
.settings-item.settings-item-button:active {
|
||||||
|
background-color: var(--background-color);
|
||||||
|
}
|
||||||
|
.settings-item.settings-item-button .icon-button>.icon-button-inner>.icon {
|
||||||
|
transition: background-color var(--animation-duration) ease-in-out;
|
||||||
|
}
|
||||||
|
.settings-item.settings-item-button:hover .icon-button>.icon-button-inner>.icon,
|
||||||
|
.settings-item.settings-item-button:active .icon-button>.icon-button-inner>.icon {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
.settings-item-invalid-indicator {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 0.5em;
|
||||||
|
background-color: var(--danger-color);
|
||||||
|
}
|
||||||
|
.settings-item[data-invalid=true] .settings-item-invalid-indicator {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Settings item groups */
|
||||||
|
.settings-item-group {
|
||||||
|
margin-right: var(--padding-negative);
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.settings-item-group.settings-item-group-wrap {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.settings-item-group-item {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
padding-right: var(--padding);
|
||||||
|
}
|
||||||
|
.settings-item-group-item-label {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
input[type=text].short-width,
|
||||||
|
input[type=number].short-width,
|
||||||
|
select.short-width {
|
||||||
|
width: var(--input-short-width);
|
||||||
|
}
|
||||||
|
input[type=text].medium-width,
|
||||||
|
input[type=number].medium-width,
|
||||||
|
select.medium-width {
|
||||||
|
width: var(--input-medium-width);
|
||||||
|
}
|
||||||
|
input[type=text].short-height,
|
||||||
|
input[type=number].short-height,
|
||||||
|
select.short-height {
|
||||||
|
height: var(--input-short-height);
|
||||||
|
margin-top: calc(var(--settings-group-right-max-height) - var(--input-short-height) - var(--font-size-small));
|
||||||
|
line-height: var(--line-height);
|
||||||
|
}
|
||||||
|
.settings-item-button-group-container {
|
||||||
|
max-height: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.settings-item-button-group {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
max-height: none;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-top: var(--padding-negative);
|
||||||
|
margin-right: var(--padding-negative);
|
||||||
|
}
|
||||||
|
.settings-item-button-group-item {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
padding-top: var(--padding);
|
||||||
|
padding-right: var(--padding);
|
||||||
|
}
|
||||||
|
.settings-item-progress-report {
|
||||||
|
display: none;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4169e1;
|
||||||
|
}
|
||||||
|
.settings-item-error-report {
|
||||||
|
display: none;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #8b0000;
|
||||||
|
}
|
||||||
245
vendor/yomitan/css/search.css
vendored
Normal file
245
vendor/yomitan/css/search.css
vendored
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023-2025 Yomitan Authors
|
||||||
|
* Copyright (C) 2020-2022 Yomichan Authors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Variables */
|
||||||
|
:root {
|
||||||
|
--search-scroll-container-horizontal-padding: 0.72em;
|
||||||
|
--query-horizontal-padding: 0;
|
||||||
|
|
||||||
|
--padding: calc(10em / var(--font-size-no-units));
|
||||||
|
--content-width-search: 700;
|
||||||
|
--content-width: calc(1em * var(--content-width-search) / var(--font-size-no-units));
|
||||||
|
|
||||||
|
--background-color: #ffffff;
|
||||||
|
--separator-color1: #cccccc;
|
||||||
|
|
||||||
|
--search-textbox-height: calc(var(--textarea-line-height) + var(--textarea-padding) * 2);
|
||||||
|
--search-textbox-min-height: calc(var(--textarea-line-height) + var(--textarea-padding) * 2);
|
||||||
|
--search-textbox-max-height: calc(var(--textarea-line-height) * 5 + var(--textarea-padding) * 2);
|
||||||
|
|
||||||
|
--cog-icon-size: 26px;
|
||||||
|
}
|
||||||
|
:root:not([data-loaded=true]) {
|
||||||
|
--animation-duration: 0s;
|
||||||
|
}
|
||||||
|
:root[data-theme=dark] {
|
||||||
|
--separator-color1: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Common styles */
|
||||||
|
:root {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: var(--background-color);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--text-color);
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.search-header {
|
||||||
|
padding-left: var(--search-scroll-container-horizontal-padding);
|
||||||
|
padding-right: var(--search-scroll-container-horizontal-padding);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
line-height: 1.5em;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.25em 0 0;
|
||||||
|
font-weight: normal;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-bottom: calc(1em / (var(--font-size-no-units) * 2)) solid var(--separator-color1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search bar */
|
||||||
|
.search-textbox-container {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
#search-textbox {
|
||||||
|
color: var(--text-color);
|
||||||
|
flex: 1 1 auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: var(--textarea-padding);
|
||||||
|
background-color: var(--input-background-color);
|
||||||
|
border-radius: 0;
|
||||||
|
line-height: var(--textarea-line-height);
|
||||||
|
border: 0;
|
||||||
|
outline: none;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--search-textbox-height);
|
||||||
|
min-height: var(--search-textbox-min-height);
|
||||||
|
max-height: var(--search-textbox-max-height);
|
||||||
|
resize: none;
|
||||||
|
font-size: var(--font-size);
|
||||||
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.search-button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
position: relative;
|
||||||
|
width: 2.5em;
|
||||||
|
height: var(--search-textbox-height);
|
||||||
|
min-height: var(--search-textbox-min-height);
|
||||||
|
max-height: var(--search-textbox-max-height);
|
||||||
|
background-color: var(--input-background-color);
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
transition: background-color var(--animation-duration) ease-in-out;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.search-button:hover,
|
||||||
|
.search-button:focus {
|
||||||
|
background-color: var(--input-background-color-dark);
|
||||||
|
}
|
||||||
|
.search-button:focus:not(:focus-visible):not(:hover) {
|
||||||
|
background-color: var(--input-background-color);
|
||||||
|
}
|
||||||
|
.search-button:focus-visible {
|
||||||
|
background-color: var(--input-background-color-dark);
|
||||||
|
}
|
||||||
|
.search-button:active,
|
||||||
|
.search-button:active:focus {
|
||||||
|
background-color: var(--input-background-color-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button>.icon {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: var(--button-default-icon-color);
|
||||||
|
--icon-size: 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
position: relative;
|
||||||
|
width: 2.5em;
|
||||||
|
height: var(--search-textbox-height);
|
||||||
|
min-height: var(--search-textbox-min-height);
|
||||||
|
max-height: var(--search-textbox-max-height);
|
||||||
|
background-color: var(--input-background-color);
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
transition: background-color var(--animation-duration) ease-in-out;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.clear-button:hover,
|
||||||
|
.clear-button:focus {
|
||||||
|
background-color: var(--input-background-color-dark);
|
||||||
|
}
|
||||||
|
.clear-button:focus:not(:focus-visible):not(:hover) {
|
||||||
|
background-color: var(--input-background-color);
|
||||||
|
}
|
||||||
|
.clear-button:focus-visible {
|
||||||
|
background-color: var(--input-background-color-dark);
|
||||||
|
}
|
||||||
|
.clear-button:active,
|
||||||
|
.clear-button:active:focus {
|
||||||
|
background-color: var(--input-background-color-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-button>.icon {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: var(--button-default-icon-color);
|
||||||
|
--icon-size: 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search options */
|
||||||
|
#search-settings-button>.icon {
|
||||||
|
display: block;
|
||||||
|
background-color: var(--button-default-icon-color);
|
||||||
|
width: var(--cog-icon-size);
|
||||||
|
height: var(--cog-icon-size);
|
||||||
|
transition: var(--animation-duration) filter ease-in-out;
|
||||||
|
}
|
||||||
|
#search-settings-button>.icon:hover,
|
||||||
|
#search-settings-button>.icon:focus {
|
||||||
|
filter: invert(0.5);
|
||||||
|
}
|
||||||
|
#search-settings-button {
|
||||||
|
margin-right: 0;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.search-options-right {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.search-options {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
margin: 0.5em 0 0 0;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.search-option {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
margin: 0.5em 2em 0.5em 0;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.search-option:not([hidden]) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.search-option-label {
|
||||||
|
padding-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search styles */
|
||||||
|
#intro {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#intro>p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
:root[data-search-mode=action-popup] #intro,
|
||||||
|
:root[data-search-mode=action-popup] #search-option-clipboard-monitor-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
:root[data-search-mode=action-popup],
|
||||||
|
:root[data-search-mode=action-popup] body {
|
||||||
|
width: 640px;
|
||||||
|
height: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode before themes are applied
|
||||||
|
DO NOT use this for normal theming */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-loaded=true]) {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
}
|
||||||
|
}
|
||||||
2785
vendor/yomitan/css/settings.css
vendored
Normal file
2785
vendor/yomitan/css/settings.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
259
vendor/yomitan/css/structured-content.css
vendored
Normal file
259
vendor/yomitan/css/structured-content.css
vendored
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023-2025 Yomitan Authors
|
||||||
|
* Copyright (C) 2021-2022 Yomichan Authors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the entrys of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Glossary images */
|
||||||
|
.gloss-image-container {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: top;
|
||||||
|
line-height: 0;
|
||||||
|
font-size: calc(1em / var(--font-size-no-units));
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.gloss-image-link[data-background=true]>.gloss-image-container {
|
||||||
|
background-color: var(--gloss-image-background-color);
|
||||||
|
}
|
||||||
|
.gloss-image-link {
|
||||||
|
cursor: inherit;
|
||||||
|
color: var(--accent-color);
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
line-height: 1;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.gloss-image-link:hover {
|
||||||
|
color: var(--accent-color-dark);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.gloss-image-container-overlay {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-size: calc(1em * var(--font-size-no-units));
|
||||||
|
line-height: var(--line-height);
|
||||||
|
display: table;
|
||||||
|
table-layout: fixed;
|
||||||
|
white-space: normal;
|
||||||
|
color: var(--text-color-light3);
|
||||||
|
}
|
||||||
|
.gloss-image-link[data-has-image=true][data-image-load-state=load-error] .gloss-image-container-overlay::after {
|
||||||
|
content: 'Image failed to load';
|
||||||
|
display: table-cell;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.25em;
|
||||||
|
}
|
||||||
|
.gloss-image-background {
|
||||||
|
--image: none;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--text-color);
|
||||||
|
-webkit-mask-repeat: no-repeat;
|
||||||
|
-webkit-mask-position: center center;
|
||||||
|
-webkit-mask-mode: alpha;
|
||||||
|
-webkit-mask-size: contain;
|
||||||
|
-webkit-mask-image: var(--image);
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center center;
|
||||||
|
mask-mode: alpha;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-image: var(--image);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.gloss-image {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
object-fit: contain;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.gloss-image-link[data-has-aspect-ratio=true] .gloss-image {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.gloss-image-link[data-image-rendering=pixelated] .gloss-image,
|
||||||
|
.gloss-image-link[data-image-rendering=pixelated] .gloss-image-background {
|
||||||
|
image-rendering: auto;
|
||||||
|
image-rendering: -moz-crisp-edges;
|
||||||
|
image-rendering: -webkit-optimize-contrast;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
}
|
||||||
|
.gloss-image-link[data-image-rendering=crisp-edges] .gloss-image,
|
||||||
|
.gloss-image-link[data-image-rendering=crisp-edges] .gloss-image-background {
|
||||||
|
image-rendering: auto;
|
||||||
|
image-rendering: -moz-crisp-edges;
|
||||||
|
image-rendering: -webkit-optimize-contrast;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
}
|
||||||
|
:root[data-browser=firefox] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image,
|
||||||
|
:root[data-browser=firefox] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image-background,
|
||||||
|
:root[data-browser=firefox-mobile] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image,
|
||||||
|
:root[data-browser=firefox-mobile] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image-background {
|
||||||
|
image-rendering: auto;
|
||||||
|
}
|
||||||
|
.gloss-image-link[data-has-aspect-ratio=true] .gloss-image-sizer {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0;
|
||||||
|
vertical-align: top;
|
||||||
|
font-size: 0;
|
||||||
|
}
|
||||||
|
.gloss-image-link-text {
|
||||||
|
display: none;
|
||||||
|
line-height: var(--line-height);
|
||||||
|
}
|
||||||
|
.gloss-image-link-text::before {
|
||||||
|
content: '[';
|
||||||
|
}
|
||||||
|
.gloss-image-link-text::after {
|
||||||
|
content: ']';
|
||||||
|
}
|
||||||
|
.gloss-image-description {
|
||||||
|
display: block;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gloss-image-link[data-appearance=monochrome] .gloss-image {
|
||||||
|
/* Workaround for coloring monochrome gloss images due to issues with masking using a canvas without loading extra media */
|
||||||
|
/* drop-shadow with 0.01px blur is at minimum required for Firefox to render the shadow when used on a canvas */
|
||||||
|
--shadow-settings: 0 0 0.01px var(--text-color);
|
||||||
|
filter: grayscale(1) opacity(0.5) drop-shadow(var(--shadow-settings)) drop-shadow(var(--shadow-settings)) saturate(1000%) brightness(1000%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gloss-image-link[data-size-units=em] .gloss-image-container {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gloss-image-link[data-vertical-align=baseline] { vertical-align: baseline; }
|
||||||
|
.gloss-image-link[data-vertical-align=sub] { vertical-align: sub; }
|
||||||
|
.gloss-image-link[data-vertical-align=super] { vertical-align: super; }
|
||||||
|
.gloss-image-link[data-vertical-align=text-top] { vertical-align: top; }
|
||||||
|
.gloss-image-link[data-vertical-align=text-bottom] { vertical-align: bottom; }
|
||||||
|
.gloss-image-link[data-vertical-align=middle] { vertical-align: middle; }
|
||||||
|
.gloss-image-link[data-vertical-align=top] { vertical-align: top; }
|
||||||
|
.gloss-image-link[data-vertical-align=bottom] { vertical-align: bottom; }
|
||||||
|
.gloss-image-link[data-collapsed=true],
|
||||||
|
:root[data-glossary-layout-mode^=compact] .gloss-image-link[data-collapsible=true] {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gloss-image-link[data-collapsed=true] .gloss-image-container,
|
||||||
|
:root[data-glossary-layout-mode^=compact] .gloss-image-link[data-collapsible=true] .gloss-image-container {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.entry:nth-last-of-type(1):not(:nth-of-type(1)) .gloss-image-link[data-collapsed=true] .gloss-image-container,
|
||||||
|
:root[data-glossary-layout-mode^=compact] .entry:nth-last-of-type(1):not(:nth-of-type(1)) .gloss-image-link[data-collapsible=true] .gloss-image-container,
|
||||||
|
:root[data-glossary-layout-mode^=compact] .definition-item:nth-last-of-type(1) .gloss-image-link[data-collapsible=true] .gloss-image-container {
|
||||||
|
bottom: 100%;
|
||||||
|
top: auto;
|
||||||
|
}
|
||||||
|
.gloss-image-link[data-collapsed=true]:hover .gloss-image-container,
|
||||||
|
.gloss-image-link[data-collapsed=true]:focus .gloss-image-container,
|
||||||
|
:root[data-glossary-layout-mode^=compact] .gloss-image-link[data-collapsible=true]:hover .gloss-image-container,
|
||||||
|
:root[data-glossary-layout-mode^=compact] .gloss-image-link[data-collapsible=true]:focus .gloss-image-container {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.gloss-image-link[data-collapsed=true] .gloss-image-link-text,
|
||||||
|
:root[data-glossary-layout-mode^=compact] .gloss-image-link[data-collapsible=true] .gloss-image-link-text {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.gloss-image-link[data-collapsed=true]~.gloss-image-description,
|
||||||
|
:root[data-glossary-layout-mode^=compact] .gloss-image-description {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Links */
|
||||||
|
.gloss-link-text {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
.gloss-link-external-icon {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: calc(16em / var(--font-size-no-units));
|
||||||
|
height: calc(16em / var(--font-size-no-units));
|
||||||
|
margin-left: 0.25em;
|
||||||
|
background-color: var(--link-color);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Structured content glossary styles */
|
||||||
|
.gloss-sc-table-container {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.gloss-sc-table {
|
||||||
|
table-layout: auto;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.gloss-sc-thead,
|
||||||
|
.gloss-sc-tfoot,
|
||||||
|
.gloss-sc-th {
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: var(--background-color-dark1);
|
||||||
|
}
|
||||||
|
.gloss-sc-th,
|
||||||
|
.gloss-sc-td {
|
||||||
|
border-width: calc(1em / var(--font-size-no-units));
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--text-color-light2);
|
||||||
|
padding: 0.25em;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.gloss-sc-ol,
|
||||||
|
.gloss-sc-ul {
|
||||||
|
padding-left: var(--list-padding2);
|
||||||
|
}
|
||||||
|
:root[data-glossary-layout-mode^=compact] .gloss-sc-ul[data-sc-content=glossary] {
|
||||||
|
display: inline;
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
:root[data-glossary-layout-mode^=compact] .gloss-sc-ul[data-sc-content=glossary] .gloss-sc-li {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
:root[data-glossary-layout-mode^=compact] .gloss-sc-ul[data-sc-content=glossary] .gloss-sc-li:not(:first-child)::before {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
content: var(--compact-list-separator);
|
||||||
|
display: inline;
|
||||||
|
color: var(--text-color-light3);
|
||||||
|
}
|
||||||
|
.gloss-sc-details {
|
||||||
|
padding-left: var(--list-padding1);
|
||||||
|
}
|
||||||
|
.gloss-sc-summary {
|
||||||
|
list-style-position: outside;
|
||||||
|
}
|
||||||
21
vendor/yomitan/css/visibility-modifiers.css
vendored
Normal file
21
vendor/yomitan/css/visibility-modifiers.css
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
:root:not([data-debug=true]) .debug-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
:root:not([data-advanced=true]) .advanced-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
:root:not([data-advanced=false]) .basic-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
:root:not([data-language=ja]):not([data-language=zh]):not([data-language=yue]) .jpzhyue-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
:root:not([data-language=ja]):not([data-language=zh]):not([data-language=yue]):not([data-language=ko]) .jpzhyueko-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
:root:is([data-language=ja], [data-language=zh], [data-language=yue], [data-language=ko]) .not-jpzhyueko {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
:root:not([data-language=ja]) .jp-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
43
vendor/yomitan/data/anki-compact-gloss-style.js
vendored
Normal file
43
vendor/yomitan/data/anki-compact-gloss-style.js
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023-2025 Yomitan Authors
|
||||||
|
* Copyright (C) 2019-2022 Yomichan Authors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ANKI_COMPACT_GLOSS_STYLES = `
|
||||||
|
ul[data-sc-content="glossary"] > li:not(:first-child)::before {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
content: ' | ';
|
||||||
|
display: inline;
|
||||||
|
color: #777777;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul[data-sc-content="glossary"] > li {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul[data-sc-content="glossary"] {
|
||||||
|
display: inline;
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getAnkiCompactGlossStyles() {
|
||||||
|
return ANKI_COMPACT_GLOSS_STYLES.trim();
|
||||||
|
}
|
||||||
BIN
vendor/yomitan/data/audio/fallback-bloop.mp3
vendored
Normal file
BIN
vendor/yomitan/data/audio/fallback-bloop.mp3
vendored
Normal file
Binary file not shown.
BIN
vendor/yomitan/data/audio/fallback-click.mp3
vendored
Normal file
BIN
vendor/yomitan/data/audio/fallback-click.mp3
vendored
Normal file
Binary file not shown.
BIN
vendor/yomitan/data/fonts/kanji-stroke-orders.ttf
vendored
Normal file
BIN
vendor/yomitan/data/fonts/kanji-stroke-orders.ttf
vendored
Normal file
Binary file not shown.
166
vendor/yomitan/data/pronunciation-style.json
vendored
Normal file
166
vendor/yomitan/data/pronunciation-style.json
vendored
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"selectors": [".pronunciation-downstep-notation"],
|
||||||
|
"styles": [
|
||||||
|
["display", "inline"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selectors": [".pronunciation-text"],
|
||||||
|
"styles": [
|
||||||
|
["display", "inline"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selectors": [".pronunciation-mora"],
|
||||||
|
"styles": [
|
||||||
|
["display", "inline-block"],
|
||||||
|
["position", "relative"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selectors": [".pronunciation-mora-line"],
|
||||||
|
"styles": [
|
||||||
|
["border-color", "currentColor"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selectors": [".pronunciation-mora[data-pitch=high]>.pronunciation-mora-line"],
|
||||||
|
"styles": [
|
||||||
|
["display", "block"],
|
||||||
|
["user-select", "none"],
|
||||||
|
["pointer-events", "none"],
|
||||||
|
["position", "absolute"],
|
||||||
|
["top", "0.1em"],
|
||||||
|
["left", "0"],
|
||||||
|
["right", "0"],
|
||||||
|
["height", "0"],
|
||||||
|
["border-top-width", "0.1em"],
|
||||||
|
["border-top-style", "solid"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selectors": [".pronunciation-mora[data-pitch=high][data-pitch-next=low]>.pronunciation-mora-line"],
|
||||||
|
"styles": [
|
||||||
|
["right", "-0.1em"],
|
||||||
|
["height", "0.4em"],
|
||||||
|
["border-right-width", "0.1em"],
|
||||||
|
["border-right-style", "solid"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selectors": [".pronunciation-mora[data-pitch=high][data-pitch-next=low]"],
|
||||||
|
"styles": [
|
||||||
|
["padding-right", "0.1em"],
|
||||||
|
["margin-right", "0.1em"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selectors": [".pronunciation-devoice-indicator"],
|
||||||
|
"styles": [
|
||||||
|
["display", "block"],
|
||||||
|
["position", "absolute"],
|
||||||
|
["left", "50%"],
|
||||||
|
["top", "50%"],
|
||||||
|
["width", "1.125em"],
|
||||||
|
["height", "1.125em"],
|
||||||
|
["border-radius", "50%"],
|
||||||
|
["box-sizing", "border-box"],
|
||||||
|
["z-index", "1"],
|
||||||
|
["transform", "translate(-50%, -50%)"],
|
||||||
|
["border", "1.5px dotted #c83c28"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selectors": [".pronunciation-nasal-indicator"],
|
||||||
|
"styles": [
|
||||||
|
["display", "block"],
|
||||||
|
["position", "absolute"],
|
||||||
|
["right", "-0.125em"],
|
||||||
|
["top", "0.125em"],
|
||||||
|
["width", "0.375em"],
|
||||||
|
["height", "0.375em"],
|
||||||
|
["border-radius", "50%"],
|
||||||
|
["box-sizing", "border-box"],
|
||||||
|
["z-index", "1"],
|
||||||
|
["border", "1.5px solid #c83c28"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selectors": [".pronunciation-nasal-diacritic"],
|
||||||
|
"styles": [
|
||||||
|
["position", "absolute"],
|
||||||
|
["width", "0"],
|
||||||
|
["height", "0"],
|
||||||
|
["opacity", "0"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selectors": [".pronunciation-character"],
|
||||||
|
"styles": [
|
||||||
|
["display", "inline"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selectors": [".pronunciation-character-group"],
|
||||||
|
"styles": [
|
||||||
|
["display", "inline-block"],
|
||||||
|
["position", "relative"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selectors": [".pronunciation-graph"],
|
||||||
|
"styles": [
|
||||||
|
["display", "inline-block"],
|
||||||
|
["vertical-align", "middle"],
|
||||||
|
["height", "1.5em"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selectors": [
|
||||||
|
".pronunciation-graph-line",
|
||||||
|
".pronunciation-graph-line-tail"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
["fill", "none"],
|
||||||
|
["stroke-width", "5"],
|
||||||
|
["stroke", "currentColor"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selectors": [".pronunciation-graph-line-tail"],
|
||||||
|
"styles": [
|
||||||
|
["stroke-dasharray", "5 5"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selectors": [".pronunciation-graph-dot"],
|
||||||
|
"styles": [
|
||||||
|
["stroke-width", "5"],
|
||||||
|
["fill", "currentColor"],
|
||||||
|
["stroke", "currentColor"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selectors": [".pronunciation-graph-dot-downstep1"],
|
||||||
|
"styles": [
|
||||||
|
["fill", "none"],
|
||||||
|
["stroke-width", "5"],
|
||||||
|
["stroke", "currentColor"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selectors": [".pronunciation-graph-dot-downstep2"],
|
||||||
|
"styles": [
|
||||||
|
["fill", "currentColor"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selectors": [".pronunciation-graph-triangle"],
|
||||||
|
"styles": [
|
||||||
|
["fill", "none"],
|
||||||
|
["stroke-width", "5"],
|
||||||
|
["stroke", "currentColor"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
901
vendor/yomitan/data/recommended-dictionaries.json
vendored
Normal file
901
vendor/yomitan/data/recommended-dictionaries.json
vendored
Normal file
@@ -0,0 +1,901 @@
|
|||||||
|
{
|
||||||
|
"afb": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-afb-en",
|
||||||
|
"description": "Gulf Arabic to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-afb-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"aii": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [
|
||||||
|
{
|
||||||
|
"name": "kty-aii-en-ipa",
|
||||||
|
"description": "Assyrian Neo-Aramaic IPA dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-aii-en-ipa.zip"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-aii-en",
|
||||||
|
"description": "Assyrian Neo-Aramaic to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-aii-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ang": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-ang-en",
|
||||||
|
"description": "Old English to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-ang-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ar": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [
|
||||||
|
{
|
||||||
|
"name": "kty-ar-en-ipa",
|
||||||
|
"description": "Arabic IPA dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-ar-en-ipa.zip"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-ar-en",
|
||||||
|
"description": "Arabic to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-ar-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"arz": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [
|
||||||
|
{
|
||||||
|
"name": "kty-ar-en-ipa",
|
||||||
|
"description": "Arabic IPA dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://github.com/yomidevs/kaikki-to-yomitan/blob/master/downloads.md",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-ar-en-ipa.zip"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-ar-en",
|
||||||
|
"description": "Arabic to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://github.com/yomidevs/kaikki-to-yomitan/blob/master/downloads.md",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-ar-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"cs": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-cs-en",
|
||||||
|
"description": "Czech to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-cs-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"de": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-de-en",
|
||||||
|
"description": "German to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-de-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"el": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-el-en",
|
||||||
|
"description": "Greek to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-el-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-en-en",
|
||||||
|
"description": "English to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-en-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"enm": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-enm-en",
|
||||||
|
"description": "Middle English to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-enm-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"eo": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-eo-en",
|
||||||
|
"description": "Esperanto to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-eo-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-es-en",
|
||||||
|
"description": "Spanish to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-es-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fa": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-fa-en",
|
||||||
|
"description": "Persian to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-fa-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fi": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-fi-en",
|
||||||
|
"description": "Finnish to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-fi-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-fr-en",
|
||||||
|
"description": "French to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-fr-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"grc": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-grc-en",
|
||||||
|
"description": "Ancient Greek to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-grc-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"haw": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "pukui-elbert-1986",
|
||||||
|
"description": "English to Hawaiian Dictionary from Pukui-Elbert in 1986",
|
||||||
|
"homepage": "https://github.com/bee-san/awesome-hawaiian-language",
|
||||||
|
"downloadUrl": "https://github.com/bee-san/awesome-hawaiian-language/releases/download/0.0.1/Pukui-Elbert-1986-Deduped-Yomitan.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "combined-dictionary",
|
||||||
|
"description": "English to Hawaiian Dictionary from Stephen (Kepano) Trussel",
|
||||||
|
"homepage": "https://github.com/bee-san/awesome-hawaiian-language",
|
||||||
|
"downloadUrl": "https://github.com/bee-san/awesome-hawaiian-language/releases/download/0.0.1/Combined-Hawaiian-Dictionary-2020-Mitch-Cleaned.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hawaiian-place-names-2002",
|
||||||
|
"description": "Hawaiian Place Names, published 2002",
|
||||||
|
"homepage": "https://github.com/bee-san/awesome-hawaiian-language",
|
||||||
|
"downloadUrl": "https://github.com/bee-san/awesome-hawaiian-language/releases/download/0.0.1/Hawaii-place-names-2002-yomitan.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"he": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-he-en",
|
||||||
|
"description": "Hebrew to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-he-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hi": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-hi-en",
|
||||||
|
"description": "Hindi to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-hi-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hu": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-hu-en",
|
||||||
|
"description": "Hungarian to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-hu-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-id-en",
|
||||||
|
"description": "Indonesian to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-id-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"it": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-it-en",
|
||||||
|
"description": "Italian to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-it-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"frequency": [
|
||||||
|
{
|
||||||
|
"name": "BCCWJ",
|
||||||
|
"description": "Based on the Balanced Corpus of Contemporary Written Japanese covering books, magazines, newspapers, blogs, forums, textbooks, and legal documents among others.",
|
||||||
|
"homepage": "https://github.com/Kuuuube/yomitan-dictionaries?tab=readme-ov-file#bccwj-suw-luw-combined",
|
||||||
|
"downloadUrl": "https://github.com/Kuuuube/yomitan-dictionaries/releases/download/yomitan-permalink/BCCWJ_SUW_LUW_combined.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "JPDB",
|
||||||
|
"description": "A frequency dictionary based on the corpus from the online Japanese dictionary and SRS system at https://jpdb.io.",
|
||||||
|
"homepage": "https://github.com/Kuuuube/yomitan-dictionaries?tab=readme-ov-file#jpdb-v22-frequency",
|
||||||
|
"downloadUrl": "https://github.com/Kuuuube/yomitan-dictionaries/releases/download/yomitan-permalink/JPDB_v2.2_Frequency_Kana.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jiten",
|
||||||
|
"description": "A frequency dictionary based on the corpus from the media stats database at https://jiten.moe",
|
||||||
|
"homepage": "https://jiten.moe/other",
|
||||||
|
"downloadUrl": "https://api.jiten.moe/api/frequency-list/download?downloadType=yomitan"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [
|
||||||
|
{
|
||||||
|
"name": "KANJIDIC",
|
||||||
|
"description": "An English dictionary with readings, meanings, stroke order diagrams, frequency, grade level, JLPT level and frequency of kanji characters.",
|
||||||
|
"homepage": "https://github.com/yomidevs/jmdict-yomitan?tab=readme-ov-file#kanjidic-for-yomitan",
|
||||||
|
"downloadUrl": "https://github.com/yomidevs/jmdict-yomitan/releases/latest/download/KANJIDIC_english.zip"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "Jitendex",
|
||||||
|
"description": "A free and openly licensed Japanese-to-English dictionary with example sentences, usage notes, etymology notes, cross references, antonyms, definition notes.",
|
||||||
|
"homepage": "https://jitendex.org",
|
||||||
|
"downloadUrl": "https://github.com/stephenmk/stephenmk.github.io/releases/latest/download/jitendex-yomitan.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "JMnedict",
|
||||||
|
"description": "A dictionary of Japanese proper names maintained by the Electronic Dictionary Research and Development Group.",
|
||||||
|
"homepage": "https://github.com/yomidevs/jmdict-yomitan?tab=readme-ov-file#jmnedict-for-yomitan",
|
||||||
|
"downloadUrl": "https://github.com/yomidevs/jmdict-yomitan/releases/latest/download/JMnedict.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"km": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-km-en",
|
||||||
|
"description": "Khmer to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-km-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"kn": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-kn-en",
|
||||||
|
"description": "Kannada to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-kn-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ko": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-ko-en",
|
||||||
|
"description": "Korean to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-ko-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"la": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-la-en",
|
||||||
|
"description": "Latin to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-la-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lv": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-lv-en",
|
||||||
|
"description": "Latvian to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-lv-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mn": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-mn-en",
|
||||||
|
"description": "Mongolian to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-mn-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mt": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [
|
||||||
|
{
|
||||||
|
"name": "kty-mt-en-ipa",
|
||||||
|
"description": "Maltese IPA dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-mt-en-ipa.zip"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-mt-en",
|
||||||
|
"description": "Maltese to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-mt-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nl": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-nl-en",
|
||||||
|
"description": "Dutch to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-nl-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"no": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-nb-en",
|
||||||
|
"description": "Norwegian Bokmål to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-nb-en.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "kty-nn-en",
|
||||||
|
"description": "Norwegian Nynorsk to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-nn-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pl": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-pl-en",
|
||||||
|
"description": "Polish to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-pl-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pt": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-pt-en",
|
||||||
|
"description": "Portuguese to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-pt-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ro": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-ro-en",
|
||||||
|
"description": "Romanian to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-ro-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ru": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-ru-en",
|
||||||
|
"description": "Russian to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-ru-en.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "opr-ru-en",
|
||||||
|
"description": "OpenRussian is a user-contributed, libre Russian dictionary including the accents, examples, audio, related words and synonyms.",
|
||||||
|
"homepage": "https://github.com/ImenaOphelia/openrussian-to-yomitan",
|
||||||
|
"downloadUrl": "https://github.com/ImenaOphelia/openrussian-to-yomitan/releases/latest/download/opr-ru-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scn": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-scn-en",
|
||||||
|
"description": "Sicillian to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-scn-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sga": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-sga-en",
|
||||||
|
"description": "Old Irish to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-sga-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sh": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-sh-en",
|
||||||
|
"description": "Serbo-Croatian to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-sh-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sq": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-sq-en",
|
||||||
|
"description": "Albanian to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-sq-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sv": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-sv-en",
|
||||||
|
"description": "Swedish to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-sv-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"th": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-th-en",
|
||||||
|
"description": "Thai to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-th-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tl": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-tl-en",
|
||||||
|
"description": "Tagalog to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-tl-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tok": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "toki-pona-to-chinese",
|
||||||
|
"description": "Toki Pona to Chinese dictionary",
|
||||||
|
"homepage": "https://github.com/bee-san/awesome-toki-pona",
|
||||||
|
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-chinese.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "toki-pona-to-czech",
|
||||||
|
"description": "Toki Pona to Czech dictionary",
|
||||||
|
"homepage": "https://github.com/bee-san/awesome-toki-pona",
|
||||||
|
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-czech.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "toki-pona-to-dutch",
|
||||||
|
"description": "Toki Pona to Dutch dictionary",
|
||||||
|
"homepage": "https://github.com/bee-san/awesome-toki-pona",
|
||||||
|
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-dutch.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "toki-pona-to-english",
|
||||||
|
"description": "Toki Pona to English dictionary",
|
||||||
|
"homepage": "https://github.com/bee-san/awesome-toki-pona",
|
||||||
|
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-english.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "toki-pona-to-esperanto",
|
||||||
|
"description": "Toki Pona to Esperanto dictionary",
|
||||||
|
"homepage": "https://github.com/bee-san/awesome-toki-pona",
|
||||||
|
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-esperanto.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "toki-pona-to-french",
|
||||||
|
"description": "Toki Pona to French dictionary",
|
||||||
|
"homepage": "https://github.com/bee-san/awesome-toki-pona",
|
||||||
|
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-french.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "toki-pona-to-german",
|
||||||
|
"description": "Toki Pona to German dictionary",
|
||||||
|
"homepage": "https://github.com/bee-san/awesome-toki-pona",
|
||||||
|
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-german.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "toki-pona-to-indonesian",
|
||||||
|
"description": "Toki Pona to Indonesian dictionary",
|
||||||
|
"homepage": "https://github.com/bee-san/awesome-toki-pona",
|
||||||
|
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-indonesian.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "toki-pona-to-italian",
|
||||||
|
"description": "Toki Pona to Italian dictionary",
|
||||||
|
"homepage": "https://github.com/bee-san/awesome-toki-pona",
|
||||||
|
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-italian.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "toki-pona-to-polish",
|
||||||
|
"description": "Toki Pona to Polish dictionary",
|
||||||
|
"homepage": "https://github.com/bee-san/awesome-toki-pona",
|
||||||
|
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-polish.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "toki-pona-to-portuguese",
|
||||||
|
"description": "Toki Pona to Portuguese dictionary",
|
||||||
|
"homepage": "https://github.com/bee-san/awesome-toki-pona",
|
||||||
|
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-portuguese.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "toki-pona-to-russian",
|
||||||
|
"description": "Toki Pona to Russian dictionary",
|
||||||
|
"homepage": "https://github.com/bee-san/awesome-toki-pona",
|
||||||
|
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-russian.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "toki-pona-to-slovak",
|
||||||
|
"description": "Toki Pona to Slovak dictionary",
|
||||||
|
"homepage": "https://github.com/bee-san/awesome-toki-pona",
|
||||||
|
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-slovak.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "toki-pona-to-spanish",
|
||||||
|
"description": "Toki Pona to Spanish dictionary",
|
||||||
|
"homepage": "https://github.com/bee-san/awesome-toki-pona",
|
||||||
|
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-spanish.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "toki-pona-to-turkish",
|
||||||
|
"description": "Toki Pona to Turkish dictionary",
|
||||||
|
"homepage": "https://github.com/bee-san/awesome-toki-pona",
|
||||||
|
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-turkish.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tr": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-tr-en",
|
||||||
|
"description": "Turkish to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-tr-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uk": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-uk-en",
|
||||||
|
"description": "Ukranian to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-uk-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"vi": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "kty-vi-en",
|
||||||
|
"description": "Vietnamese to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-vi-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"yue": {
|
||||||
|
"frequency": [
|
||||||
|
{
|
||||||
|
"name": "Words.hk Frequency",
|
||||||
|
"description": "Frequency list of Cantonese terms and honzi provided by words.hk.",
|
||||||
|
"homepage": "https://github.com/MarvNC/wordshk-yomitan",
|
||||||
|
"downloadUrl": "https://github.com/MarvNC/wordshk-yomitan/releases/download/2024-09-17/YUE.Freq.Words.hk.Frequency.zip"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [
|
||||||
|
{
|
||||||
|
"name": "Words.hk 粵典 漢字",
|
||||||
|
"description": "A free and open Cantonese dictionary with definitions and example sentences in Cantonese and English.",
|
||||||
|
"homepage": "https://github.com/MarvNC/wordshk-yomitan",
|
||||||
|
"downloadUrl": "https://github.com/MarvNC/wordshk-yomitan/releases/download/2025-07-09/Words.hk.Honzi.2025-07-08.zip"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "Words.hk 粵典",
|
||||||
|
"description": "A free and open Cantonese dictionary with definitions and example sentences in Cantonese and English.",
|
||||||
|
"homepage": "https://github.com/MarvNC/wordshk-yomitan",
|
||||||
|
"downloadUrl": "https://github.com/MarvNC/wordshk-yomitan/releases/download/2025-07-09/Words.hk.2025-07-08.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CC-Canto",
|
||||||
|
"description": "CC-Canto is an open source Cantonese dictionary project created by Pleco, intended to be used alongside the CC-CEDICT dictionary. It provides Cantonese specific words and definitions.",
|
||||||
|
"homepage": "https://github.com/MarvNC/cc-cedict-yomitan",
|
||||||
|
"downloadUrl": "https://github.com/MarvNC/cc-cedict-yomitan/releases/latest/download/CC-Canto.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CC-CEDICT Canto",
|
||||||
|
"description": "CC-CEDICT is a continuation of the CEDICT project with the aim to provide a complete downloadable Chinese to English dictionary with pronunciation in pinyin for the Chinese characters. This version includes Cantonese readings provided by Pleco.",
|
||||||
|
"homepage": "https://github.com/MarvNC/cc-cedict-yomitan",
|
||||||
|
"downloadUrl": "https://github.com/MarvNC/cc-cedict-yomitan/releases/latest/download/CC-CEDICT.Canto.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cantodict",
|
||||||
|
"description": "CantoDict was a Cantonese-English dictionary created and maintained by Adam Sheik and public contributors.",
|
||||||
|
"homepage": "https://github.com/MarvNC/yomitan-dictionaries?tab=readme-ov-file#cantodict",
|
||||||
|
"downloadUrl": "https://github.com/MarvNC/yomichan-dictionaries/raw/master/dl/%5BCantonese%5D%20Cantodict.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"zh": {
|
||||||
|
"frequency": [],
|
||||||
|
"grammar": [],
|
||||||
|
"kanji": [
|
||||||
|
{
|
||||||
|
"name": "CC-CEDICT",
|
||||||
|
"description": "A free and open Chinese-English dictionary provided by the CC-CEDICT project.",
|
||||||
|
"homepage": "https://github.com/MarvNC/cc-cedict-yomitan",
|
||||||
|
"downloadUrl": "https://github.com/MarvNC/cc-cedict-yomitan/releases/latest/download/CC-CEDICT.Hanzi.zip"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pronunciation": [],
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"name": "CC-CEDICT",
|
||||||
|
"description": "A free and open Chinese-English dictionary provided by the CC-CEDICT project.",
|
||||||
|
"homepage": "https://github.com/MarvNC/cc-cedict-yomitan",
|
||||||
|
"downloadUrl": "https://github.com/MarvNC/cc-cedict-yomitan/releases/latest/download/CC-CEDICT.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "kty-zh-en",
|
||||||
|
"description": "Chinese to English dictionary created from Wiktionary data.",
|
||||||
|
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
|
||||||
|
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-zh-en.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
862
vendor/yomitan/data/recommended-settings.json
vendored
Normal file
862
vendor/yomitan/data/recommended-settings.json
vendored
Normal file
@@ -0,0 +1,862 @@
|
|||||||
|
{
|
||||||
|
"ar": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"arz": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cs": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"da": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"de": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"el": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.length",
|
||||||
|
"value": 24
|
||||||
|
},
|
||||||
|
"description": "Increase the default scanning length from 16 to 24 characters."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"en": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "sentenceParsing.terminationCharacters",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"character1": "\"",
|
||||||
|
"character2": "\"",
|
||||||
|
"includeCharacterAtStart": false,
|
||||||
|
"includeCharacterAtEnd": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"character1": "'",
|
||||||
|
"character2": "'",
|
||||||
|
"includeCharacterAtStart": false,
|
||||||
|
"includeCharacterAtEnd": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"character1": ".",
|
||||||
|
"character2": null,
|
||||||
|
"includeCharacterAtStart": false,
|
||||||
|
"includeCharacterAtEnd": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"character1": "!",
|
||||||
|
"character2": null,
|
||||||
|
"includeCharacterAtStart": false,
|
||||||
|
"includeCharacterAtEnd": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"character1": "?",
|
||||||
|
"character2": null,
|
||||||
|
"includeCharacterAtStart": false,
|
||||||
|
"includeCharacterAtEnd": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"character1": "…",
|
||||||
|
"character2": null,
|
||||||
|
"includeCharacterAtStart": false,
|
||||||
|
"includeCharacterAtEnd": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Disable apostrophes as a sentence terminator."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"eo": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"es": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fi": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fr": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.textReplacements.groups",
|
||||||
|
"value": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"pattern": "l('|’)",
|
||||||
|
"ignoreCase": true,
|
||||||
|
"replacement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "j('|’)",
|
||||||
|
"ignoreCase": true,
|
||||||
|
"replacement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "d('|’)",
|
||||||
|
"ignoreCase": true,
|
||||||
|
"replacement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "s('|’)",
|
||||||
|
"ignoreCase": true,
|
||||||
|
"replacement": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Separating the l', j', d', s' from the word."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"he": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hi": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hu": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ja": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "character"
|
||||||
|
},
|
||||||
|
"description": "Scan text one character at a time."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "letter"
|
||||||
|
},
|
||||||
|
"description": "Lookup words by letter in the dictionary."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ko": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"kn": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"la": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lv": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mn": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nl": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pl": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pt": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ro": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ru": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sga": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sh": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sq": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sv": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tr": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"uk": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "parsing.enableScanningParser",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"description": "Turn off Yomitan's internal parser for languages with spaces."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vi": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Scan text one word at a time (as opposed to one character)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "word"
|
||||||
|
},
|
||||||
|
"description": "Lookup whole words in the dictionary."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"yue": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "character"
|
||||||
|
},
|
||||||
|
"description": "Scan text one character at a time."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "letter"
|
||||||
|
},
|
||||||
|
"description": "Lookup words by letter in the dictionary."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"zh": [
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "scanning.scanResolution",
|
||||||
|
"value": "character"
|
||||||
|
},
|
||||||
|
"description": "Scan text one character at a time."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modification": {
|
||||||
|
"action": "set",
|
||||||
|
"path": "translation.searchResolution",
|
||||||
|
"value": "letter"
|
||||||
|
},
|
||||||
|
"description": "Lookup words by letter in the dictionary."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
34
vendor/yomitan/data/schemas/custom-audio-list-schema.json
vendored
Normal file
34
vendor/yomitan/data/schemas/custom-audio-list-schema.json
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"$id": "customAudioList",
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"type",
|
||||||
|
"audioSources"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "audioSourceList"
|
||||||
|
},
|
||||||
|
"audioSources": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
125
vendor/yomitan/data/schemas/dictionary-index-schema.json
vendored
Normal file
125
vendor/yomitan/data/schemas/dictionary-index-schema.json
vendored
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
{
|
||||||
|
"$id": "dictionaryIndex",
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"definitions": {
|
||||||
|
"isoLanguageCode": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ISO language code (ISO 639-1 where possible, ISO 639-3 otherwise).",
|
||||||
|
"pattern": "^[a-z]{2,3}$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"description": "Index file containing information about the data contained in the dictionary.",
|
||||||
|
"required": [
|
||||||
|
"title",
|
||||||
|
"revision"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Title of the dictionary."
|
||||||
|
},
|
||||||
|
"revision": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Revision of the dictionary. This value is displayed, and used to check for dictionary updates."
|
||||||
|
},
|
||||||
|
"minimumYomitanVersion": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Minimum version of Yomitan that is compatible with this dictionary."
|
||||||
|
},
|
||||||
|
"sequenced": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Whether or not this dictionary contains sequencing information for related terms."
|
||||||
|
},
|
||||||
|
"format": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Format of data found in the JSON data files.",
|
||||||
|
"enum": [1, 2, 3]
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Alias for format.",
|
||||||
|
"enum": [1, 2, 3]
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Creator of the dictionary."
|
||||||
|
},
|
||||||
|
"isUpdatable": {
|
||||||
|
"type": "boolean",
|
||||||
|
"const": true,
|
||||||
|
"description": "Whether this dictionary contains links to its latest version."
|
||||||
|
},
|
||||||
|
"indexUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "URL for the index file of the latest revision of the dictionary, used to check for updates."
|
||||||
|
},
|
||||||
|
"downloadUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "URL for the download of the latest revision of the dictionary."
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "URL for the source of the dictionary, displayed in the dictionary details."
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Description of the dictionary data."
|
||||||
|
},
|
||||||
|
"attribution": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Attribution information for the dictionary data."
|
||||||
|
},
|
||||||
|
"sourceLanguage": {
|
||||||
|
"$ref": "#/definitions/isoLanguageCode",
|
||||||
|
"description": "Language of the terms in the dictionary."
|
||||||
|
},
|
||||||
|
"targetLanguage": {
|
||||||
|
"$ref": "#/definitions/isoLanguageCode",
|
||||||
|
"description": "Main language of the definitions in the dictionary."
|
||||||
|
},
|
||||||
|
"frequencyMode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["occurrence-based", "rank-based"]
|
||||||
|
},
|
||||||
|
"tagMeta": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Tag information for terms and kanji. This object is obsolete and individual tag files should be used instead.",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Information about a single tag. The object key is the name of the tag.",
|
||||||
|
"properties": {
|
||||||
|
"category": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Category for the tag."
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Sorting order for the tag."
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Notes for the tag."
|
||||||
|
},
|
||||||
|
"score": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Score used to determine popularity. Negative values are more rare and positive values are more frequent. This score is also used to sort search results."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"required": ["format"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": ["version"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"isUpdatable": ["indexUrl", "downloadUrl"]
|
||||||
|
}
|
||||||
|
}
|
||||||
35
vendor/yomitan/data/schemas/dictionary-kanji-bank-v1-schema.json
vendored
Normal file
35
vendor/yomitan/data/schemas/dictionary-kanji-bank-v1-schema.json
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"$id": "dictionaryKanjiBankV1",
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "array",
|
||||||
|
"description": "Data file containing kanji information.",
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Information about a single kanji character.",
|
||||||
|
"minItems": 4,
|
||||||
|
"maxItems": 4,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Kanji character.",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "String of space-separated onyomi readings for the kanji character. An empty string is treated as no readings."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "String of space-separated kunyomi readings for the kanji character. An empty string is treated as no readings."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "String of space-separated tags for the kanji character. An empty string is treated as no tags."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"additionalItems": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A meaning for the kanji character."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
vendor/yomitan/data/schemas/dictionary-kanji-bank-v3-schema.json
vendored
Normal file
47
vendor/yomitan/data/schemas/dictionary-kanji-bank-v3-schema.json
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"$id": "dictionaryKanjiBankV3",
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "array",
|
||||||
|
"description": "Data file containing kanji information.",
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Information about a single kanji character.",
|
||||||
|
"minItems": 6,
|
||||||
|
"maxItems": 6,
|
||||||
|
"additionalItems": false,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Kanji character.",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "String of space-separated onyomi readings for the kanji character. An empty string is treated as no readings."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "String of space-separated kunyomi readings for the kanji character. An empty string is treated as no readings."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "String of space-separated tags for the kanji character. An empty string is treated as no tags."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"description": "Array of meanings for the kanji character.",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A meaning for the kanji character."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"description": "Various stats for the kanji character.",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user