mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
feat: improve background startup and launcher control
Detach --background launches from terminals with quieter runtime output, make wrapper/plugin overlay start explicit, and allow trailing commas in JSONC configs for safer hot-reload edits. Includes pending Anki/docs/backlog updates in this unreleased batch.
This commit is contained in:
@@ -66,7 +66,8 @@ For macOS builds and platform details, see the [installation docs](docs/installa
|
|||||||
```
|
```
|
||||||
3. Launch SubMiner:
|
3. Launch SubMiner:
|
||||||
```bash
|
```bash
|
||||||
subminer video.mkv
|
subminer --start video.mkv
|
||||||
|
SubMiner.AppImage --background # tray/background mode (desktop launcher default)
|
||||||
```
|
```
|
||||||
|
|
||||||
Config tip: while SubMiner is running, safe config edits (subtitle style, keybindings, shortcuts, secondary subtitle default mode, and `ankiConnect.ai`) hot-reload automatically.
|
Config tip: while SubMiner is running, safe config edits (subtitle style, keybindings, shortcuts, secondary subtitle default mode, and `ankiConnect.ai`) hot-reload automatically.
|
||||||
@@ -100,7 +101,7 @@ Use `subminer <subcommand> -h` for command-specific help pages (for example `sub
|
|||||||
|
|
||||||
- Use `--log-level` to control logger verbosity (for example `--log-level debug`).
|
- Use `--log-level` to control logger verbosity (for example `--log-level debug`).
|
||||||
- Use `--dev` and `--debug` only for app/dev-mode behavior; they are not tied to logging level.
|
- Use `--dev` and `--debug` only for app/dev-mode behavior; they are not tied to logging level.
|
||||||
- Default logging remains `info` unless you pass `--log-level`.
|
- Default logging is `info`, except `--background` mode defaults to `warn` unless `--log-level` is set.
|
||||||
|
|
||||||
### Overlay Queue Controls
|
### Overlay Queue Controls
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
id: TASK-66
|
||||||
|
title: Add AnkiConnect tagging for mined and updated cards
|
||||||
|
status: Done
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-02-18 09:23'
|
||||||
|
updated_date: '2026-02-18 09:24'
|
||||||
|
labels:
|
||||||
|
- anki
|
||||||
|
- config
|
||||||
|
- enhancement
|
||||||
|
dependencies: []
|
||||||
|
references:
|
||||||
|
- src/anki-connect.ts
|
||||||
|
- src/anki-integration.ts
|
||||||
|
- src/anki-integration/card-creation.ts
|
||||||
|
- src/config/definitions.ts
|
||||||
|
- src/config/service.ts
|
||||||
|
- src/config/config.test.ts
|
||||||
|
- docs/configuration.md
|
||||||
|
- config.example.jsonc
|
||||||
|
- docs/public/config.example.jsonc
|
||||||
|
priority: medium
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Support configuring tags applied to cards created or updated through SubMiner's AnkiConnect workflow. Default behavior should add the `SubMiner` tag to all mined/updated cards, while allowing users to override or disable automatic tagging via config.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 AnkiConnect note creation supports passing tags.
|
||||||
|
- [x] #2 Existing-card update flows add configured tags via AnkiConnect after updates.
|
||||||
|
- [x] #3 Default config includes `ankiConnect.tags: ["SubMiner"]`.
|
||||||
|
- [x] #4 Config parsing validates `ankiConnect.tags` and falls back safely on invalid values.
|
||||||
|
- [x] #5 User-facing docs/config examples mention the new tags option and default behavior.
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Implemented configurable AnkiConnect tagging across SubMiner card creation and update workflows. Added `ankiConnect.tags` config (default `['SubMiner']`), validation/fallback in config parsing, AnkiConnect client support for `addNote` tags + `addTags`, and integration hooks so created/updated/merged cards receive configured tags. Updated docs and regenerated config examples; config test suite passes.
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
id: TASK-65
|
||||||
|
title: Run Electron app as background tray service with IPC startup
|
||||||
|
status: Done
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-02-18 08:48'
|
||||||
|
updated_date: '2026-02-18 10:17'
|
||||||
|
labels:
|
||||||
|
- electron
|
||||||
|
- tray
|
||||||
|
- ipc
|
||||||
|
- desktop-entry
|
||||||
|
dependencies: []
|
||||||
|
priority: high
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Allow launching the app from desktop/application entry so it starts in background with minimal logging, waits for IPC connection, and exposes tray icon for settings/configuration.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 Launching via app/desktop file starts app without foreground window by default
|
||||||
|
- [x] #2 Process runs in background with minimal startup logging
|
||||||
|
- [x] #3 Main process initializes IPC server/client wait loop for incoming connection
|
||||||
|
- [x] #4 Tray icon is visible and provides menu actions for settings/configuration and quit
|
||||||
|
- [x] #5 Behavior documented for desktop launch usage
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
Implemented `--background` CLI mode to keep app running without overlay windows, default to quieter logging, and preserve IPC client startup.
|
||||||
|
|
||||||
|
Added persistent tray runtime (icon + menu actions for overlay, Yomitan settings, runtime options, Jellyfin setup, AniList setup, quit).
|
||||||
|
|
||||||
|
Disabled window-all-closed auto-quit while in background mode; desktop Linux launcher now passes `--background` by default via electron-builder desktop Exec.
|
||||||
|
|
||||||
|
Updated usage/installation docs and CLI help; validated with `bun run build && bun run test:fast`.
|
||||||
|
|
||||||
|
Follow-up packaging fix: replaced invalid `build.linux.desktop.Exec` override with supported electron-builder `build.linux.executableArgs: ["--background"]` after AppImage schema validation failure on electron-builder 26.7.0.
|
||||||
|
|
||||||
|
Re-verified Linux packaging with `bun run build:appimage`; background launcher argument is now applied without config validation errors.
|
||||||
|
|
||||||
|
Correction: any earlier mention of `desktop Exec` is obsolete; final shipped config uses `build.linux.executableArgs` only.
|
||||||
|
|
||||||
|
Background launch now detaches from terminal via new `src/main-entry.ts` bootstrap: `--background` parent process spawns detached child and exits, so Ctrl+C no longer stops the running tray process.
|
||||||
|
|
||||||
|
Background detached child now suppresses Node runtime warnings (`NODE_NO_WARNINGS=1`) and strips `VK_INSTANCE_LAYERS` when it contains `lsfg` to reduce non-actionable startup noise in background mode.
|
||||||
|
|
||||||
|
Updated package entrypoint to `dist/main-entry.js` and docs usage note for detached background behavior.
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Added an always-on background startup mode for desktop launches by introducing `--background`, wiring startup/lifecycle state to keep the process alive without windows, and defaulting this mode to quieter logging unless explicitly overridden. Added a tray icon/menu for settings and runtime configuration entry points and updated Linux desktop packaging so launcher executions use background mode by default, with docs and tests updated accordingly.
|
||||||
|
|
||||||
|
Added detached background bootstrap behavior for `--background` launches so terminal invocations return immediately while the tray process continues independently, and reduced background startup noise by suppressing Node warnings and removing problematic lsfg Vulkan layer env in detached child startups.
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
<!-- DOD:BEGIN -->
|
||||||
|
- [x] #1 Implementation verified locally with launch command or desktop entry simulation
|
||||||
|
<!-- DOD:END -->
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
id: TASK-67
|
||||||
|
title: Make wrapper stop auto-sending --start by default
|
||||||
|
status: Done
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-02-18 09:47'
|
||||||
|
updated_date: '2026-02-18 10:02'
|
||||||
|
labels:
|
||||||
|
- launcher
|
||||||
|
- wrapper
|
||||||
|
- cli
|
||||||
|
- background-mode
|
||||||
|
dependencies: []
|
||||||
|
priority: high
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Update the subminer wrapper flow so it no longer appends --start automatically when launching the app, requiring explicit start semantics and allowing background AppImage workflow to be primary.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 Wrapper launch path does not append --start unless explicitly requested
|
||||||
|
- [x] #2 Existing explicit startup commands still work and are documented
|
||||||
|
- [x] #3 Tests updated for new wrapper behavior
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
Launcher wrapper no longer auto-starts overlay from mpv plugin `auto_start` setting; only explicit `--start`/`--start-overlay` trigger wrapper-managed startup.
|
||||||
|
|
||||||
|
Simplified plugin runtime config parsing in launcher to consume `socket_path` only for wrapper behavior.
|
||||||
|
|
||||||
|
Updated docs examples and descriptions to make explicit startup flow clear (`subminer --start video.mkv`), and rebuilt bundled `subminer` script.
|
||||||
|
|
||||||
|
Validated with `bun run build && make build-launcher && bun run test:fast`.
|
||||||
|
|
||||||
|
Follow-up: updated mpv plugin command builder (`plugin/subminer.lua`) to stop forcing `--start` for toggle/show/hide actions; only explicit `start` action now sends start context. This avoids second-instance command failures when app is already running in background mode.
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Updated the `subminer` wrapper to stop implicitly issuing app `--start` based on plugin auto-start settings, so background AppImage usage is the default and overlay startup happens only on explicit wrapper flags (`--start`/`--start-overlay`) or manual plugin commands. Also updated launcher docs/examples to reflect explicit startup semantics and regenerated the bundled `subminer` script.
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
<!-- DOD:BEGIN -->
|
||||||
|
- [x] #1 Validated with launcher-related tests or command simulation
|
||||||
|
<!-- DOD:END -->
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
id: TASK-68
|
||||||
|
title: Allow trailing commas in JSONC config parsing
|
||||||
|
status: Done
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-02-18 10:13'
|
||||||
|
updated_date: '2026-02-18 10:13'
|
||||||
|
labels:
|
||||||
|
- config
|
||||||
|
- jsonc
|
||||||
|
dependencies: []
|
||||||
|
priority: medium
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Permit trailing commas in `config.jsonc` parsing so normal JSONC edits do not fail strict reload/watcher startup.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 Config parser accepts trailing commas in JSONC objects/arrays
|
||||||
|
- [x] #2 Invalid malformed JSONC still fails strict reload
|
||||||
|
- [x] #3 Coverage added to prevent regression
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
Enabled `allowTrailingComma: true` in `src/config/service.ts` JSONC parse options while preserving strict parse error handling.
|
||||||
|
|
||||||
|
Added regression test `accepts trailing commas in jsonc` in `src/config/config.test.ts`.
|
||||||
|
|
||||||
|
Updated docs note in `docs/configuration.md` that JSONC config supports comments and trailing commas.
|
||||||
|
|
||||||
|
Validated with `bun run build && bun run test:config:dist`.
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
SubMiner now accepts trailing commas in `config.jsonc` by enabling JSONC parser trailing-comma support in the strict config load path. Strict reload behavior remains intact for malformed JSON/JSONC, and a regression test now covers trailing-comma acceptance. Documentation was updated accordingly.
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
<!-- DOD:BEGIN -->
|
||||||
|
- [x] #1 Config test suite passes after parser change
|
||||||
|
<!-- DOD:END -->
|
||||||
@@ -56,6 +56,9 @@
|
|||||||
"enabled": false,
|
"enabled": false,
|
||||||
"url": "http://127.0.0.1:8765",
|
"url": "http://127.0.0.1:8765",
|
||||||
"pollingRate": 3000,
|
"pollingRate": 3000,
|
||||||
|
"tags": [
|
||||||
|
"SubMiner"
|
||||||
|
],
|
||||||
"fields": {
|
"fields": {
|
||||||
"audio": "ExpressionAudio",
|
"audio": "ExpressionAudio",
|
||||||
"image": "Picture",
|
"image": "Picture",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ SubMiner.AppImage --generate-config --backup-overwrite
|
|||||||
```
|
```
|
||||||
|
|
||||||
- `--generate-config` writes a default JSONC config template.
|
- `--generate-config` writes a default JSONC config template.
|
||||||
|
- JSONC config supports comments and trailing commas.
|
||||||
- If the target file exists, SubMiner prompts to create a timestamped backup and overwrite.
|
- If the target file exists, SubMiner prompts to create a timestamped backup and overwrite.
|
||||||
- In non-interactive shells, use `--backup-overwrite` to explicitly back up and overwrite.
|
- In non-interactive shells, use `--backup-overwrite` to explicitly back up and overwrite.
|
||||||
- `bun run generate:config-example` regenerates both repository `config.example.jsonc` and docs-served `/config.example.jsonc` from the same centralized defaults.
|
- `bun run generate:config-example` regenerates both repository `config.example.jsonc` and docs-served `/config.example.jsonc` from the same centralized defaults.
|
||||||
@@ -94,6 +95,7 @@ Enable automatic Anki card creation and updates with media generation:
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "http://127.0.0.1:8765",
|
"url": "http://127.0.0.1:8765",
|
||||||
"pollingRate": 3000,
|
"pollingRate": 3000,
|
||||||
|
"tags": ["SubMiner"],
|
||||||
"deck": "Learning::Japanese",
|
"deck": "Learning::Japanese",
|
||||||
"fields": {
|
"fields": {
|
||||||
"audio": "ExpressionAudio",
|
"audio": "ExpressionAudio",
|
||||||
@@ -159,6 +161,7 @@ This example is intentionally compact. The option table below documents availabl
|
|||||||
| `enabled` | `true`, `false` | Enable AnkiConnect integration (default: `false`) |
|
| `enabled` | `true`, `false` | Enable AnkiConnect integration (default: `false`) |
|
||||||
| `url` | string (URL) | AnkiConnect API URL (default: `http://127.0.0.1:8765`) |
|
| `url` | string (URL) | AnkiConnect API URL (default: `http://127.0.0.1:8765`) |
|
||||||
| `pollingRate` | number (ms) | How often to check for new cards (default: `3000`) |
|
| `pollingRate` | number (ms) | How often to check for new cards (default: `3000`) |
|
||||||
|
| `tags` | array of strings | Tags automatically added to cards mined/updated by SubMiner (default: `['SubMiner']`; set `[]` to disable automatic tagging). |
|
||||||
| `deck` | string | Anki deck to monitor for new cards |
|
| `deck` | string | Anki deck to monitor for new cards |
|
||||||
| `ankiConnect.nPlusOne.decks` | array of strings | Decks used for N+1 known-word cache lookups. When omitted/empty, falls back to `ankiConnect.deck`. |
|
| `ankiConnect.nPlusOne.decks` | array of strings | Decks used for N+1 known-word cache lookups. When omitted/empty, falls back to `ankiConnect.deck`. |
|
||||||
| `fields.audio` | string | Card field for audio files (default: `ExpressionAudio`) |
|
| `fields.audio` | string | Card field for audio files (default: `ExpressionAudio`) |
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ make build-macos-unsigned # macOS DMG + ZIP (unsigned)
|
|||||||
```bash
|
```bash
|
||||||
bun run dev # builds + launches with --start --dev
|
bun run dev # builds + launches with --start --dev
|
||||||
electron . --start --dev --log-level debug # equivalent Electron launch with verbose logging
|
electron . --start --dev --log-level debug # equivalent Electron launch with verbose logging
|
||||||
|
electron . --background # tray/background mode, minimal default logging
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
@@ -97,6 +98,7 @@ Run `make help` for a full list of targets. Key ones:
|
|||||||
- To add or change a config option, update `src/config/definitions.ts` first. Defaults, runtime-option metadata, and generated `config.example.jsonc` are derived from this centralized source.
|
- To add or change a config option, update `src/config/definitions.ts` first. Defaults, runtime-option metadata, and generated `config.example.jsonc` are derived from this centralized source.
|
||||||
- Overlay window/visibility state is owned by `src/core/services/overlay-manager.ts`.
|
- Overlay window/visibility state is owned by `src/core/services/overlay-manager.ts`.
|
||||||
- Main process composition is now split across `src/main/` modules (`startup.ts`, `app-lifecycle.ts`, `startup-lifecycle.ts`, `state.ts`, `ipc-runtime.ts`, `cli-runtime.ts`, `overlay-runtime.ts`, `subsync-runtime.ts`).
|
- Main process composition is now split across `src/main/` modules (`startup.ts`, `app-lifecycle.ts`, `startup-lifecycle.ts`, `state.ts`, `ipc-runtime.ts`, `cli-runtime.ts`, `overlay-runtime.ts`, `subsync-runtime.ts`).
|
||||||
|
- Linux packaged desktop launches pass `--background` using electron-builder `build.linux.executableArgs` in `package.json`.
|
||||||
- MPV service has been split into transport, protocol, state, and properties layers in `src/core/services/`.
|
- MPV service has been split into transport, protocol, state, and properties layers in `src/core/services/`.
|
||||||
- Prefer direct inline deps objects in `src/main/` modules for simple pass-through wiring.
|
- Prefer direct inline deps objects in `src/main/` modules for simple pass-through wiring.
|
||||||
- Add a helper/adapter service only when it performs meaningful adaptation, validation, or reuse (not identity mapping).
|
- Add a helper/adapter service only when it performs meaningful adaptation, validation, or reuse (not identity mapping).
|
||||||
|
|||||||
@@ -183,13 +183,14 @@ After installing, confirm SubMiner is working:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start the overlay (connects to mpv IPC)
|
# Start the overlay (connects to mpv IPC)
|
||||||
subminer video.mkv
|
subminer --start video.mkv
|
||||||
|
|
||||||
# Useful launch modes for troubleshooting
|
# Useful launch modes for troubleshooting
|
||||||
subminer --log-level debug video.mkv
|
subminer --log-level debug video.mkv
|
||||||
SubMiner.AppImage --start --log-level debug
|
SubMiner.AppImage --start --log-level debug
|
||||||
|
|
||||||
# Or with direct AppImage control
|
# Or with direct AppImage control
|
||||||
|
SubMiner.AppImage --background # Background tray service mode
|
||||||
SubMiner.AppImage --start
|
SubMiner.AppImage --start
|
||||||
SubMiner.AppImage --start --dev
|
SubMiner.AppImage --start --dev
|
||||||
SubMiner.AppImage --help # Show all CLI options
|
SubMiner.AppImage --help # Show all CLI options
|
||||||
|
|||||||
@@ -56,6 +56,9 @@
|
|||||||
"enabled": false,
|
"enabled": false,
|
||||||
"url": "http://127.0.0.1:8765",
|
"url": "http://127.0.0.1:8765",
|
||||||
"pollingRate": 3000,
|
"pollingRate": 3000,
|
||||||
|
"tags": [
|
||||||
|
"SubMiner"
|
||||||
|
],
|
||||||
"fields": {
|
"fields": {
|
||||||
"audio": "ExpressionAudio",
|
"audio": "ExpressionAudio",
|
||||||
"image": "Picture",
|
"image": "Picture",
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ There are two ways to use SubMiner — the `subminer` wrapper script or the mpv
|
|||||||
|
|
||||||
| Approach | Best For |
|
| Approach | Best For |
|
||||||
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **subminer script** | All-in-one solution. Handles video selection, launches MPV with the correct socket, starts the overlay automatically, and cleans up on exit. |
|
| **subminer script** | All-in-one solution. Handles video selection, launches MPV with the correct socket, and manages app commands. Overlay start is explicit (`--start`, `-S`, or `y-s`). |
|
||||||
| **MPV plugin** | When you launch MPV yourself or from other tools. Provides in-MPV chord keybindings (e.g. `y-y` for menu) to control visible and invisible overlay layers. Requires `--input-ipc-server=/tmp/subminer-socket`. |
|
| **MPV plugin** | When you launch MPV yourself or from other tools. Provides in-MPV chord keybindings (e.g. `y-y` for menu) to control visible and invisible overlay layers. Requires `--input-ipc-server=/tmp/subminer-socket`. |
|
||||||
|
|
||||||
You can use both together—install the plugin for on-demand control, but use `subminer` when you want the streamlined workflow.
|
You can use both together—install the plugin for on-demand control, but use `subminer` when you want the streamlined workflow.
|
||||||
|
|
||||||
`subminer` is implemented as a Bun script and runs directly via shebang (no `bun run` needed), for example: `subminer video.mkv`.
|
`subminer` is implemented as a Bun script and runs directly via shebang (no `bun run` needed), for example: `subminer --start video.mkv`.
|
||||||
|
|
||||||
## Live Config Reload
|
## Live Config Reload
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ subminer -R # Use rofi instead of fzf
|
|||||||
subminer -d ~/Videos # Specific directory
|
subminer -d ~/Videos # Specific directory
|
||||||
subminer -r -d ~/Anime # Recursive search
|
subminer -r -d ~/Anime # Recursive search
|
||||||
subminer video.mkv # Play specific file
|
subminer video.mkv # Play specific file
|
||||||
|
subminer --start video.mkv # Play + explicitly start overlay
|
||||||
subminer https://youtu.be/... # Play a YouTube URL
|
subminer https://youtu.be/... # Play a YouTube URL
|
||||||
subminer ytsearch:"jp news" # Play first YouTube search result
|
subminer ytsearch:"jp news" # Play first YouTube search result
|
||||||
subminer --log-level debug video.mkv # Enable verbose logs for launch/debugging
|
subminer --log-level debug video.mkv # Enable verbose logs for launch/debugging
|
||||||
@@ -90,7 +91,8 @@ SubMiner.AppImage --help # Show all options
|
|||||||
- `--log-level` controls logger verbosity.
|
- `--log-level` controls logger verbosity.
|
||||||
- `--dev` and `--debug` are app/dev-mode switches; they are not log-level aliases.
|
- `--dev` and `--debug` are app/dev-mode switches; they are not log-level aliases.
|
||||||
- `--background` defaults to quieter logging (`warn`) unless `--log-level` is set.
|
- `--background` defaults to quieter logging (`warn`) unless `--log-level` is set.
|
||||||
- Linux desktop launcher starts SubMiner with `--background` by default.
|
- `--background` launched from a terminal detaches and returns the prompt; stop it with tray Quit or `SubMiner.AppImage --stop`.
|
||||||
|
- Linux desktop launcher starts SubMiner with `--background` by default (via electron-builder `linux.executableArgs`).
|
||||||
- Use both when needed, for example `SubMiner.AppImage --start --dev --log-level debug`.
|
- Use both when needed, for example `SubMiner.AppImage --start --dev --log-level debug`.
|
||||||
|
|
||||||
### Launcher Subcommands
|
### Launcher Subcommands
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
resolvePathMaybe,
|
resolvePathMaybe,
|
||||||
isUrlTarget,
|
isUrlTarget,
|
||||||
uniqueNormalizedLangCodes,
|
uniqueNormalizedLangCodes,
|
||||||
parseBoolLike,
|
|
||||||
inferWhisperLanguage,
|
inferWhisperLanguage,
|
||||||
} from './util.js';
|
} from './util.js';
|
||||||
|
|
||||||
@@ -160,7 +159,6 @@ function getPluginConfigCandidates(): string[] {
|
|||||||
|
|
||||||
export function readPluginRuntimeConfig(logLevel: LogLevel): PluginRuntimeConfig {
|
export function readPluginRuntimeConfig(logLevel: LogLevel): PluginRuntimeConfig {
|
||||||
const runtimeConfig: PluginRuntimeConfig = {
|
const runtimeConfig: PluginRuntimeConfig = {
|
||||||
autoStartOverlay: false,
|
|
||||||
socketPath: DEFAULT_SOCKET_PATH,
|
socketPath: DEFAULT_SOCKET_PATH,
|
||||||
};
|
};
|
||||||
const candidates = getPluginConfigCandidates();
|
const candidates = getPluginConfigCandidates();
|
||||||
@@ -173,16 +171,6 @@ export function readPluginRuntimeConfig(logLevel: LogLevel): PluginRuntimeConfig
|
|||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (trimmed.length === 0 || trimmed.startsWith('#')) continue;
|
if (trimmed.length === 0 || trimmed.startsWith('#')) continue;
|
||||||
const autoStartMatch = trimmed.match(/^auto_start\s*=\s*(.+)$/i);
|
|
||||||
if (autoStartMatch) {
|
|
||||||
const value = (autoStartMatch[1] || '').split('#', 1)[0]?.trim() || '';
|
|
||||||
const parsed = parseBoolLike(value);
|
|
||||||
if (parsed !== null) {
|
|
||||||
runtimeConfig.autoStartOverlay = parsed;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const socketMatch = trimmed.match(/^socket_path\s*=\s*(.+)$/i);
|
const socketMatch = trimmed.match(/^socket_path\s*=\s*(.+)$/i);
|
||||||
if (socketMatch) {
|
if (socketMatch) {
|
||||||
const value = (socketMatch[1] || '').split('#', 1)[0]?.trim() || '';
|
const value = (socketMatch[1] || '').split('#', 1)[0]?.trim() || '';
|
||||||
@@ -192,7 +180,7 @@ export function readPluginRuntimeConfig(logLevel: LogLevel): PluginRuntimeConfig
|
|||||||
log(
|
log(
|
||||||
'debug',
|
'debug',
|
||||||
logLevel,
|
logLevel,
|
||||||
`Using mpv plugin settings from ${configPath}: auto_start=${runtimeConfig.autoStartOverlay ? 'yes' : 'no'} socket_path=${runtimeConfig.socketPath}`,
|
`Using mpv plugin settings from ${configPath}: socket_path=${runtimeConfig.socketPath}`,
|
||||||
);
|
);
|
||||||
return runtimeConfig;
|
return runtimeConfig;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -204,7 +192,7 @@ export function readPluginRuntimeConfig(logLevel: LogLevel): PluginRuntimeConfig
|
|||||||
log(
|
log(
|
||||||
'debug',
|
'debug',
|
||||||
logLevel,
|
logLevel,
|
||||||
`No mpv subminer.conf found; using launcher defaults (auto_start=no socket_path=${runtimeConfig.socketPath})`,
|
`No mpv subminer.conf found; using launcher defaults (socket_path=${runtimeConfig.socketPath})`,
|
||||||
);
|
);
|
||||||
return runtimeConfig;
|
return runtimeConfig;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -364,8 +364,7 @@ async function main(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ready = await waitForSocket(mpvSocketPath);
|
const ready = await waitForSocket(mpvSocketPath);
|
||||||
const shouldStartOverlay =
|
const shouldStartOverlay = args.startOverlay || args.autoStartOverlay;
|
||||||
args.startOverlay || args.autoStartOverlay || pluginRuntimeConfig.autoStartOverlay;
|
|
||||||
if (shouldStartOverlay) {
|
if (shouldStartOverlay) {
|
||||||
if (ready) {
|
if (ready) {
|
||||||
log('info', args.logLevel, 'MPV IPC socket ready, starting SubMiner overlay');
|
log('info', args.logLevel, 'MPV IPC socket ready, starting SubMiner overlay');
|
||||||
|
|||||||
@@ -121,7 +121,6 @@ export interface LauncherJellyfinConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginRuntimeConfig {
|
export interface PluginRuntimeConfig {
|
||||||
autoStartOverlay: boolean;
|
|
||||||
socketPath: string;
|
socketPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.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.js",
|
"main": "dist/main-entry.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"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 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",
|
||||||
|
|||||||
@@ -281,25 +281,9 @@ local function build_command_args(action, overrides)
|
|||||||
table.insert(args, log_level)
|
table.insert(args, log_level)
|
||||||
end
|
end
|
||||||
|
|
||||||
local needs_start_context = (
|
local needs_start_context = action == "start"
|
||||||
action == "start"
|
|
||||||
or action == "toggle"
|
|
||||||
or action == "show"
|
|
||||||
or action == "hide"
|
|
||||||
or action == "toggle-visible-overlay"
|
|
||||||
or action == "show-visible-overlay"
|
|
||||||
or action == "hide-visible-overlay"
|
|
||||||
or action == "toggle-invisible-overlay"
|
|
||||||
or action == "show-invisible-overlay"
|
|
||||||
or action == "hide-invisible-overlay"
|
|
||||||
)
|
|
||||||
|
|
||||||
if needs_start_context then
|
if needs_start_context then
|
||||||
-- Explicitly request MPV IPC connection for active overlay control.
|
|
||||||
if action ~= "start" then
|
|
||||||
table.insert(args, "--start")
|
|
||||||
end
|
|
||||||
|
|
||||||
local backend = resolve_backend(overrides.backend)
|
local backend = resolve_backend(overrides.backend)
|
||||||
if backend and backend ~= "" then
|
if backend and backend ~= "" then
|
||||||
table.insert(args, "--backend")
|
table.insert(args, "--backend")
|
||||||
|
|||||||
@@ -192,13 +192,35 @@ export class AnkiConnectClient {
|
|||||||
deckName: string,
|
deckName: string,
|
||||||
modelName: string,
|
modelName: string,
|
||||||
fields: Record<string, string>,
|
fields: Record<string, string>,
|
||||||
|
tags: string[] = [],
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
|
const note: {
|
||||||
|
deckName: string;
|
||||||
|
modelName: string;
|
||||||
|
fields: Record<string, string>;
|
||||||
|
tags?: string[];
|
||||||
|
} = { deckName, modelName, fields };
|
||||||
|
if (tags.length > 0) {
|
||||||
|
note.tags = tags;
|
||||||
|
}
|
||||||
|
|
||||||
const result = await this.invoke('addNote', {
|
const result = await this.invoke('addNote', {
|
||||||
note: { deckName, modelName, fields },
|
note,
|
||||||
});
|
});
|
||||||
return result as number;
|
return result as number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addTags(noteIds: number[], tags: string[]): Promise<void> {
|
||||||
|
if (noteIds.length === 0 || tags.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.invoke('addTags', {
|
||||||
|
notes: noteIds,
|
||||||
|
tags: tags.join(' '),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async deleteNotes(noteIds: number[]): Promise<void> {
|
async deleteNotes(noteIds: number[]): Promise<void> {
|
||||||
await this.invoke('deleteNotes', { notes: noteIds });
|
await this.invoke('deleteNotes', { notes: noteIds });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,7 +175,9 @@ export class AnkiIntegration {
|
|||||||
getMpvClient: () => this.mpvClient,
|
getMpvClient: () => this.mpvClient,
|
||||||
getDeck: () => this.config.deck,
|
getDeck: () => this.config.deck,
|
||||||
client: {
|
client: {
|
||||||
addNote: (deck, modelName, fields) => this.client.addNote(deck, modelName, fields),
|
addNote: (deck, modelName, fields, tags) =>
|
||||||
|
this.client.addNote(deck, modelName, fields, tags),
|
||||||
|
addTags: (noteIds, tags) => this.client.addTags(noteIds, tags),
|
||||||
notesInfo: async (noteIds) => (await this.client.notesInfo(noteIds)) as unknown,
|
notesInfo: async (noteIds) => (await this.client.notesInfo(noteIds)) as unknown,
|
||||||
updateNoteFields: (noteId, fields) =>
|
updateNoteFields: (noteId, fields) =>
|
||||||
this.client.updateNoteFields(noteId, fields) as Promise<void>,
|
this.client.updateNoteFields(noteId, fields) as Promise<void>,
|
||||||
@@ -295,6 +297,25 @@ export class AnkiIntegration {
|
|||||||
this.knownWordCache.stopLifecycle();
|
this.knownWordCache.stopLifecycle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getConfiguredAnkiTags(): string[] {
|
||||||
|
if (!Array.isArray(this.config.tags)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [...new Set(this.config.tags.map((tag) => tag.trim()).filter((tag) => tag.length > 0))];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async addConfiguredTagsToNote(noteId: number): Promise<void> {
|
||||||
|
const tags = this.getConfiguredAnkiTags();
|
||||||
|
if (tags.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.client.addTags([noteId], tags);
|
||||||
|
} catch (error) {
|
||||||
|
log.warn('Failed to add tags to card:', (error as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async refreshKnownWordCache(): Promise<void> {
|
async refreshKnownWordCache(): Promise<void> {
|
||||||
return this.knownWordCache.refresh(true);
|
return this.knownWordCache.refresh(true);
|
||||||
}
|
}
|
||||||
@@ -512,6 +533,7 @@ export class AnkiIntegration {
|
|||||||
|
|
||||||
if (updatePerformed) {
|
if (updatePerformed) {
|
||||||
await this.client.updateNoteFields(noteId, updatedFields);
|
await this.client.updateNoteFields(noteId, updatedFields);
|
||||||
|
await this.addConfiguredTagsToNote(noteId);
|
||||||
log.info('Updated card fields for:', expressionText);
|
log.info('Updated card fields for:', expressionText);
|
||||||
await this.showNotification(noteId, expressionText);
|
await this.showNotification(noteId, expressionText);
|
||||||
}
|
}
|
||||||
@@ -1465,6 +1487,7 @@ export class AnkiIntegration {
|
|||||||
|
|
||||||
if (Object.keys(mergedFields).length > 0) {
|
if (Object.keys(mergedFields).length > 0) {
|
||||||
await this.client.updateNoteFields(keepNoteId, mergedFields);
|
await this.client.updateNoteFields(keepNoteId, mergedFields);
|
||||||
|
await this.addConfiguredTagsToNote(keepNoteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deleteDuplicate) {
|
if (deleteDuplicate) {
|
||||||
@@ -1621,6 +1644,7 @@ export class AnkiIntegration {
|
|||||||
await this.client.updateNoteFields(noteId, {
|
await this.client.updateNoteFields(noteId, {
|
||||||
[resolvedMiscField]: nextValue,
|
[resolvedMiscField]: nextValue,
|
||||||
});
|
});
|
||||||
|
await this.addConfiguredTagsToNote(noteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
applyRuntimeConfigPatch(patch: Partial<AnkiConnectConfig>): void {
|
applyRuntimeConfigPatch(patch: Partial<AnkiConnectConfig>): void {
|
||||||
|
|||||||
@@ -16,7 +16,13 @@ export interface CardCreationNoteInfo {
|
|||||||
type CardKind = 'sentence' | 'audio';
|
type CardKind = 'sentence' | 'audio';
|
||||||
|
|
||||||
interface CardCreationClient {
|
interface CardCreationClient {
|
||||||
addNote(deck: string, modelName: string, fields: Record<string, string>): Promise<number>;
|
addNote(
|
||||||
|
deck: string,
|
||||||
|
modelName: string,
|
||||||
|
fields: Record<string, string>,
|
||||||
|
tags?: string[],
|
||||||
|
): Promise<number>;
|
||||||
|
addTags(noteIds: number[], tags: string[]): Promise<void>;
|
||||||
notesInfo(noteIds: number[]): Promise<unknown>;
|
notesInfo(noteIds: number[]): Promise<unknown>;
|
||||||
updateNoteFields(noteId: number, fields: Record<string, string>): Promise<void>;
|
updateNoteFields(noteId: number, fields: Record<string, string>): Promise<void>;
|
||||||
storeMediaFile(filename: string, data: Buffer): Promise<void>;
|
storeMediaFile(filename: string, data: Buffer): Promise<void>;
|
||||||
@@ -101,6 +107,26 @@ interface CardCreationDeps {
|
|||||||
export class CardCreationService {
|
export class CardCreationService {
|
||||||
constructor(private readonly deps: CardCreationDeps) {}
|
constructor(private readonly deps: CardCreationDeps) {}
|
||||||
|
|
||||||
|
private getConfiguredAnkiTags(): string[] {
|
||||||
|
const tags = this.deps.getConfig().tags;
|
||||||
|
if (!Array.isArray(tags)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [...new Set(tags.map((tag) => tag.trim()).filter((tag) => tag.length > 0))];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async addConfiguredTagsToNote(noteId: number): Promise<void> {
|
||||||
|
const tags = this.getConfiguredAnkiTags();
|
||||||
|
if (tags.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.deps.client.addTags([noteId], tags);
|
||||||
|
} catch (error) {
|
||||||
|
log.warn('Failed to add tags to card:', (error as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async updateLastAddedFromClipboard(clipboardText: string): Promise<void> {
|
async updateLastAddedFromClipboard(clipboardText: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (!clipboardText || !clipboardText.trim()) {
|
if (!clipboardText || !clipboardText.trim()) {
|
||||||
@@ -272,6 +298,7 @@ export class CardCreationService {
|
|||||||
|
|
||||||
if (updatePerformed) {
|
if (updatePerformed) {
|
||||||
await this.deps.client.updateNoteFields(noteId, updatedFields);
|
await this.deps.client.updateNoteFields(noteId, updatedFields);
|
||||||
|
await this.addConfiguredTagsToNote(noteId);
|
||||||
const label = expressionText || noteId;
|
const label = expressionText || noteId;
|
||||||
log.info('Updated card from clipboard:', label);
|
log.info('Updated card from clipboard:', label);
|
||||||
const errorSuffix = errors.length > 0 ? `${errors.join(', ')} failed` : undefined;
|
const errorSuffix = errors.length > 0 ? `${errors.join(', ')} failed` : undefined;
|
||||||
@@ -408,6 +435,7 @@ export class CardCreationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.deps.client.updateNoteFields(noteId, updatedFields);
|
await this.deps.client.updateNoteFields(noteId, updatedFields);
|
||||||
|
await this.addConfiguredTagsToNote(noteId);
|
||||||
const label = expressionText || noteId;
|
const label = expressionText || noteId;
|
||||||
log.info('Marked card as audio card:', label);
|
log.info('Marked card as audio card:', label);
|
||||||
const errorSuffix = errors.length > 0 ? `${errors.join(', ')} failed` : undefined;
|
const errorSuffix = errors.length > 0 ? `${errors.join(', ')} failed` : undefined;
|
||||||
@@ -490,7 +518,12 @@ export class CardCreationService {
|
|||||||
const deck = this.deps.getConfig().deck || 'Default';
|
const deck = this.deps.getConfig().deck || 'Default';
|
||||||
let noteId: number;
|
let noteId: number;
|
||||||
try {
|
try {
|
||||||
noteId = await this.deps.client.addNote(deck, sentenceCardModel, fields);
|
noteId = await this.deps.client.addNote(
|
||||||
|
deck,
|
||||||
|
sentenceCardModel,
|
||||||
|
fields,
|
||||||
|
this.getConfiguredAnkiTags(),
|
||||||
|
);
|
||||||
log.info('Created sentence card:', noteId);
|
log.info('Created sentence card:', noteId);
|
||||||
this.deps.trackLastAddedNoteId?.(noteId);
|
this.deps.trackLastAddedNoteId?.(noteId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { hasExplicitCommand, parseArgs, shouldStartApp } from './args';
|
|||||||
|
|
||||||
test('parseArgs parses booleans and value flags', () => {
|
test('parseArgs parses booleans and value flags', () => {
|
||||||
const args = parseArgs([
|
const args = parseArgs([
|
||||||
|
'--background',
|
||||||
'--start',
|
'--start',
|
||||||
'--socket',
|
'--socket',
|
||||||
'/tmp/mpv.sock',
|
'/tmp/mpv.sock',
|
||||||
@@ -22,6 +23,7 @@ test('parseArgs parses booleans and value flags', () => {
|
|||||||
'2',
|
'2',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
assert.equal(args.background, true);
|
||||||
assert.equal(args.start, true);
|
assert.equal(args.start, true);
|
||||||
assert.equal(args.socketPath, '/tmp/mpv.sock');
|
assert.equal(args.socketPath, '/tmp/mpv.sock');
|
||||||
assert.equal(args.backend, 'hyprland');
|
assert.equal(args.backend, 'hyprland');
|
||||||
@@ -93,4 +95,9 @@ test('hasExplicitCommand and shouldStartApp preserve command intent', () => {
|
|||||||
assert.equal(jellyfinRemoteAnnounce.jellyfinRemoteAnnounce, true);
|
assert.equal(jellyfinRemoteAnnounce.jellyfinRemoteAnnounce, true);
|
||||||
assert.equal(hasExplicitCommand(jellyfinRemoteAnnounce), true);
|
assert.equal(hasExplicitCommand(jellyfinRemoteAnnounce), true);
|
||||||
assert.equal(shouldStartApp(jellyfinRemoteAnnounce), false);
|
assert.equal(shouldStartApp(jellyfinRemoteAnnounce), false);
|
||||||
|
|
||||||
|
const background = parseArgs(['--background']);
|
||||||
|
assert.equal(background.background, true);
|
||||||
|
assert.equal(hasExplicitCommand(background), true);
|
||||||
|
assert.equal(shouldStartApp(background), true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export interface CliArgs {
|
export interface CliArgs {
|
||||||
|
background: boolean;
|
||||||
start: boolean;
|
start: boolean;
|
||||||
stop: boolean;
|
stop: boolean;
|
||||||
toggle: boolean;
|
toggle: boolean;
|
||||||
@@ -61,6 +62,7 @@ export type CliCommandSource = 'initial' | 'second-instance';
|
|||||||
|
|
||||||
export function parseArgs(argv: string[]): CliArgs {
|
export function parseArgs(argv: string[]): CliArgs {
|
||||||
const args: CliArgs = {
|
const args: CliArgs = {
|
||||||
|
background: false,
|
||||||
start: false,
|
start: false,
|
||||||
stop: false,
|
stop: false,
|
||||||
toggle: false,
|
toggle: false,
|
||||||
@@ -115,7 +117,8 @@ export function parseArgs(argv: string[]): CliArgs {
|
|||||||
const arg = argv[i];
|
const arg = argv[i];
|
||||||
if (!arg.startsWith('--')) continue;
|
if (!arg.startsWith('--')) continue;
|
||||||
|
|
||||||
if (arg === '--start') args.start = true;
|
if (arg === '--background') args.background = true;
|
||||||
|
else if (arg === '--start') args.start = true;
|
||||||
else if (arg === '--stop') args.stop = true;
|
else if (arg === '--stop') args.stop = true;
|
||||||
else if (arg === '--toggle') args.toggle = true;
|
else if (arg === '--toggle') args.toggle = true;
|
||||||
else if (arg === '--toggle-visible-overlay') args.toggleVisibleOverlay = true;
|
else if (arg === '--toggle-visible-overlay') args.toggleVisibleOverlay = true;
|
||||||
@@ -255,6 +258,7 @@ export function parseArgs(argv: string[]): CliArgs {
|
|||||||
|
|
||||||
export function hasExplicitCommand(args: CliArgs): boolean {
|
export function hasExplicitCommand(args: CliArgs): boolean {
|
||||||
return (
|
return (
|
||||||
|
args.background ||
|
||||||
args.start ||
|
args.start ||
|
||||||
args.stop ||
|
args.stop ||
|
||||||
args.toggle ||
|
args.toggle ||
|
||||||
@@ -299,6 +303,7 @@ export function hasExplicitCommand(args: CliArgs): boolean {
|
|||||||
export function shouldStartApp(args: CliArgs): boolean {
|
export function shouldStartApp(args: CliArgs): boolean {
|
||||||
if (args.stop && !args.start) return false;
|
if (args.stop && !args.start) return false;
|
||||||
if (
|
if (
|
||||||
|
args.background ||
|
||||||
args.start ||
|
args.start ||
|
||||||
args.toggle ||
|
args.toggle ||
|
||||||
args.toggleVisibleOverlay ||
|
args.toggleVisibleOverlay ||
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ ${B}SubMiner${R} — Japanese sentence mining with mpv + Yomitan
|
|||||||
${B}Usage:${R} subminer ${D}[command] [options]${R}
|
${B}Usage:${R} subminer ${D}[command] [options]${R}
|
||||||
|
|
||||||
${B}Session${R}
|
${B}Session${R}
|
||||||
|
--background Start in tray/background mode
|
||||||
--start Connect to mpv and launch overlay
|
--start Connect to mpv and launch overlay
|
||||||
--stop Stop the running instance
|
--stop Stop the running instance
|
||||||
--texthooker Start texthooker server only ${D}(no overlay)${R}
|
--texthooker Start texthooker server only ${D}(no overlay)${R}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ test('loads defaults when config is missing', () => {
|
|||||||
const config = service.getConfig();
|
const config = service.getConfig();
|
||||||
assert.equal(config.websocket.port, DEFAULT_CONFIG.websocket.port);
|
assert.equal(config.websocket.port, DEFAULT_CONFIG.websocket.port);
|
||||||
assert.equal(config.ankiConnect.behavior.autoUpdateNewCards, true);
|
assert.equal(config.ankiConnect.behavior.autoUpdateNewCards, true);
|
||||||
|
assert.deepEqual(config.ankiConnect.tags, ['SubMiner']);
|
||||||
assert.equal(config.anilist.enabled, false);
|
assert.equal(config.anilist.enabled, false);
|
||||||
assert.equal(config.jellyfin.remoteControlEnabled, true);
|
assert.equal(config.jellyfin.remoteControlEnabled, true);
|
||||||
assert.equal(config.jellyfin.remoteControlAutoConnect, true);
|
assert.equal(config.jellyfin.remoteControlAutoConnect, true);
|
||||||
@@ -258,6 +259,28 @@ test('parses jsonc and warns/falls back on invalid value', () => {
|
|||||||
assert.ok(service.getWarnings().some((w) => w.path === 'websocket.port'));
|
assert.ok(service.getWarnings().some((w) => w.path === 'websocket.port'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('accepts trailing commas in jsonc', () => {
|
||||||
|
const dir = makeTempDir();
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'config.jsonc'),
|
||||||
|
`{
|
||||||
|
"websocket": {
|
||||||
|
"enabled": "auto",
|
||||||
|
"port": 7788,
|
||||||
|
},
|
||||||
|
"youtubeSubgen": {
|
||||||
|
"primarySubLanguages": ["ja", "en",],
|
||||||
|
},
|
||||||
|
}`,
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const service = new ConfigService(dir);
|
||||||
|
const config = service.getConfig();
|
||||||
|
assert.equal(config.websocket.port, 7788);
|
||||||
|
assert.deepEqual(config.youtubeSubgen.primarySubLanguages, ['ja', 'en']);
|
||||||
|
});
|
||||||
|
|
||||||
test('reloadConfigStrict rejects invalid jsonc and preserves previous config', () => {
|
test('reloadConfigStrict rejects invalid jsonc and preserves previous config', () => {
|
||||||
const dir = makeTempDir();
|
const dir = makeTempDir();
|
||||||
const configPath = path.join(dir, 'config.jsonc');
|
const configPath = path.join(dir, 'config.jsonc');
|
||||||
@@ -631,6 +654,44 @@ test('accepts valid ankiConnect n+1 deck list', () => {
|
|||||||
assert.deepEqual(config.ankiConnect.nPlusOne.decks, ['Deck One', 'Deck Two']);
|
assert.deepEqual(config.ankiConnect.nPlusOne.decks, ['Deck One', 'Deck Two']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('accepts valid ankiConnect tags list', () => {
|
||||||
|
const dir = makeTempDir();
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'config.jsonc'),
|
||||||
|
`{
|
||||||
|
"ankiConnect": {
|
||||||
|
"tags": ["SubMiner", "Mining"]
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const service = new ConfigService(dir);
|
||||||
|
const config = service.getConfig();
|
||||||
|
|
||||||
|
assert.deepEqual(config.ankiConnect.tags, ['SubMiner', 'Mining']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('falls back to default when ankiConnect tags list is invalid', () => {
|
||||||
|
const dir = makeTempDir();
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'config.jsonc'),
|
||||||
|
`{
|
||||||
|
"ankiConnect": {
|
||||||
|
"tags": ["SubMiner", 123]
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const service = new ConfigService(dir);
|
||||||
|
const config = service.getConfig();
|
||||||
|
const warnings = service.getWarnings();
|
||||||
|
|
||||||
|
assert.deepEqual(config.ankiConnect.tags, ['SubMiner']);
|
||||||
|
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.tags'));
|
||||||
|
});
|
||||||
|
|
||||||
test('falls back to default when ankiConnect n+1 deck list is invalid', () => {
|
test('falls back to default when ankiConnect n+1 deck list is invalid', () => {
|
||||||
const dir = makeTempDir();
|
const dir = makeTempDir();
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
url: 'http://127.0.0.1:8765',
|
url: 'http://127.0.0.1:8765',
|
||||||
pollingRate: 3000,
|
pollingRate: 3000,
|
||||||
|
tags: ['SubMiner'],
|
||||||
fields: {
|
fields: {
|
||||||
audio: 'ExpressionAudio',
|
audio: 'ExpressionAudio',
|
||||||
image: 'Picture',
|
image: 'Picture',
|
||||||
@@ -397,6 +398,13 @@ export const CONFIG_OPTION_REGISTRY: ConfigOptionRegistryEntry[] = [
|
|||||||
defaultValue: DEFAULT_CONFIG.ankiConnect.pollingRate,
|
defaultValue: DEFAULT_CONFIG.ankiConnect.pollingRate,
|
||||||
description: 'Polling interval in milliseconds.',
|
description: 'Polling interval in milliseconds.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'ankiConnect.tags',
|
||||||
|
kind: 'array',
|
||||||
|
defaultValue: DEFAULT_CONFIG.ankiConnect.tags,
|
||||||
|
description:
|
||||||
|
'Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'ankiConnect.behavior.autoUpdateNewCards',
|
path: 'ankiConnect.behavior.autoUpdateNewCards',
|
||||||
kind: 'boolean',
|
kind: 'boolean',
|
||||||
|
|||||||
@@ -174,7 +174,10 @@ export class ConfigService {
|
|||||||
const parsed = configPath.endsWith('.jsonc')
|
const parsed = configPath.endsWith('.jsonc')
|
||||||
? (() => {
|
? (() => {
|
||||||
const errors: ParseError[] = [];
|
const errors: ParseError[] = [];
|
||||||
const result = parseJsonc(data, errors);
|
const result = parseJsonc(data, errors, {
|
||||||
|
allowTrailingComma: true,
|
||||||
|
disallowComments: false,
|
||||||
|
});
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
throw new Error(`Invalid JSONC (${errors[0]?.error ?? 'unknown'})`);
|
throw new Error(`Invalid JSONC (${errors[0]?.error ?? 'unknown'})`);
|
||||||
}
|
}
|
||||||
@@ -889,6 +892,32 @@ export class ConfigService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (Array.isArray(ac.tags)) {
|
||||||
|
const normalizedTags = ac.tags
|
||||||
|
.filter((entry): entry is string => typeof entry === 'string')
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter((entry) => entry.length > 0);
|
||||||
|
if (normalizedTags.length === ac.tags.length) {
|
||||||
|
resolved.ankiConnect.tags = [...new Set(normalizedTags)];
|
||||||
|
} else {
|
||||||
|
resolved.ankiConnect.tags = DEFAULT_CONFIG.ankiConnect.tags;
|
||||||
|
warn(
|
||||||
|
'ankiConnect.tags',
|
||||||
|
ac.tags,
|
||||||
|
resolved.ankiConnect.tags,
|
||||||
|
'Expected an array of non-empty strings.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (ac.tags !== undefined) {
|
||||||
|
resolved.ankiConnect.tags = DEFAULT_CONFIG.ankiConnect.tags;
|
||||||
|
warn(
|
||||||
|
'ankiConnect.tags',
|
||||||
|
ac.tags,
|
||||||
|
resolved.ankiConnect.tags,
|
||||||
|
'Expected an array of strings.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const legacy = ac as Record<string, unknown>;
|
const legacy = ac as Record<string, unknown>;
|
||||||
const mapLegacy = (key: string, apply: (value: unknown) => void): void => {
|
const mapLegacy = (key: string, apply: (value: unknown) => void): void => {
|
||||||
if (legacy[key] !== undefined) apply(legacy[key]);
|
if (legacy[key] !== undefined) apply(legacy[key]);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface AppLifecycleServiceDeps {
|
|||||||
onWillQuitCleanup: () => void;
|
onWillQuitCleanup: () => void;
|
||||||
shouldRestoreWindowsOnActivate: () => boolean;
|
shouldRestoreWindowsOnActivate: () => boolean;
|
||||||
restoreWindowsOnActivate: () => void;
|
restoreWindowsOnActivate: () => void;
|
||||||
|
shouldQuitOnWindowAllClosed: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppLike {
|
interface AppLike {
|
||||||
@@ -42,6 +43,7 @@ export interface AppLifecycleDepsRuntimeOptions {
|
|||||||
onWillQuitCleanup: () => void;
|
onWillQuitCleanup: () => void;
|
||||||
shouldRestoreWindowsOnActivate: () => boolean;
|
shouldRestoreWindowsOnActivate: () => boolean;
|
||||||
restoreWindowsOnActivate: () => void;
|
restoreWindowsOnActivate: () => void;
|
||||||
|
shouldQuitOnWindowAllClosed: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAppLifecycleDepsRuntime(
|
export function createAppLifecycleDepsRuntime(
|
||||||
@@ -80,6 +82,7 @@ export function createAppLifecycleDepsRuntime(
|
|||||||
onWillQuitCleanup: options.onWillQuitCleanup,
|
onWillQuitCleanup: options.onWillQuitCleanup,
|
||||||
shouldRestoreWindowsOnActivate: options.shouldRestoreWindowsOnActivate,
|
shouldRestoreWindowsOnActivate: options.shouldRestoreWindowsOnActivate,
|
||||||
restoreWindowsOnActivate: options.restoreWindowsOnActivate,
|
restoreWindowsOnActivate: options.restoreWindowsOnActivate,
|
||||||
|
shouldQuitOnWindowAllClosed: options.shouldQuitOnWindowAllClosed,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +122,7 @@ export function startAppLifecycle(initialArgs: CliArgs, deps: AppLifecycleServic
|
|||||||
});
|
});
|
||||||
|
|
||||||
deps.onWindowAllClosed(() => {
|
deps.onWindowAllClosed(() => {
|
||||||
if (!deps.isDarwinPlatform()) {
|
if (!deps.isDarwinPlatform() && deps.shouldQuitOnWindowAllClosed()) {
|
||||||
deps.quitApp();
|
deps.quitApp();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { CliCommandServiceDeps, handleCliCommand } from './cli-command';
|
|||||||
|
|
||||||
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||||
return {
|
return {
|
||||||
|
background: false,
|
||||||
start: false,
|
start: false,
|
||||||
stop: false,
|
stop: false,
|
||||||
toggle: false,
|
toggle: false,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { CliArgs } from '../../cli/args';
|
|||||||
|
|
||||||
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||||
return {
|
return {
|
||||||
|
background: false,
|
||||||
start: false,
|
start: false,
|
||||||
stop: false,
|
stop: false,
|
||||||
toggle: false,
|
toggle: false,
|
||||||
@@ -80,6 +81,7 @@ test('runStartupBootstrapRuntime configures startup state and starts lifecycle',
|
|||||||
assert.equal(result.backendOverride, 'x11');
|
assert.equal(result.backendOverride, 'x11');
|
||||||
assert.equal(result.autoStartOverlay, true);
|
assert.equal(result.autoStartOverlay, true);
|
||||||
assert.equal(result.texthookerOnlyMode, true);
|
assert.equal(result.texthookerOnlyMode, true);
|
||||||
|
assert.equal(result.backgroundMode, false);
|
||||||
assert.deepEqual(calls, ['setLog:debug:cli', 'forceX11', 'enforceWayland', 'startLifecycle']);
|
assert.deepEqual(calls, ['setLog:debug:cli', 'forceX11', 'enforceWayland', 'startLifecycle']);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -130,6 +132,7 @@ test('runStartupBootstrapRuntime remains lifecycle-stable with Jellyfin CLI flag
|
|||||||
assert.equal(result.backendOverride, null);
|
assert.equal(result.backendOverride, null);
|
||||||
assert.equal(result.autoStartOverlay, false);
|
assert.equal(result.autoStartOverlay, false);
|
||||||
assert.equal(result.texthookerOnlyMode, false);
|
assert.equal(result.texthookerOnlyMode, false);
|
||||||
|
assert.equal(result.backgroundMode, false);
|
||||||
assert.deepEqual(calls, ['forceX11', 'enforceWayland', 'startLifecycle']);
|
assert.deepEqual(calls, ['forceX11', 'enforceWayland', 'startLifecycle']);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -173,5 +176,26 @@ test('runStartupBootstrapRuntime skips lifecycle when generate-config flow handl
|
|||||||
assert.equal(result.mpvSocketPath, '/tmp/default.sock');
|
assert.equal(result.mpvSocketPath, '/tmp/default.sock');
|
||||||
assert.equal(result.texthookerPort, 5174);
|
assert.equal(result.texthookerPort, 5174);
|
||||||
assert.equal(result.backendOverride, null);
|
assert.equal(result.backendOverride, null);
|
||||||
|
assert.equal(result.backgroundMode, false);
|
||||||
assert.deepEqual(calls, ['setLog:warn:cli', 'forceX11', 'enforceWayland']);
|
assert.deepEqual(calls, ['setLog:warn:cli', 'forceX11', 'enforceWayland']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('runStartupBootstrapRuntime enables quiet background mode by default', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const args = makeArgs({ background: true });
|
||||||
|
|
||||||
|
const result = runStartupBootstrapRuntime({
|
||||||
|
argv: ['node', 'main.ts', '--background'],
|
||||||
|
parseArgs: () => args,
|
||||||
|
setLogLevel: (level, source) => calls.push(`setLog:${level}:${source}`),
|
||||||
|
forceX11Backend: () => calls.push('forceX11'),
|
||||||
|
enforceUnsupportedWaylandMode: () => calls.push('enforceWayland'),
|
||||||
|
getDefaultSocketPath: () => '/tmp/default.sock',
|
||||||
|
defaultTexthookerPort: 5174,
|
||||||
|
runGenerateConfigFlow: () => false,
|
||||||
|
startAppLifecycle: () => calls.push('startLifecycle'),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.backgroundMode, true);
|
||||||
|
assert.deepEqual(calls, ['setLog:warn:cli', 'forceX11', 'enforceWayland', 'startLifecycle']);
|
||||||
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface StartupBootstrapRuntimeState {
|
|||||||
backendOverride: string | null;
|
backendOverride: string | null;
|
||||||
autoStartOverlay: boolean;
|
autoStartOverlay: boolean;
|
||||||
texthookerOnlyMode: boolean;
|
texthookerOnlyMode: boolean;
|
||||||
|
backgroundMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RuntimeAutoUpdateOptionManagerLike {
|
interface RuntimeAutoUpdateOptionManagerLike {
|
||||||
@@ -47,6 +48,8 @@ export function runStartupBootstrapRuntime(
|
|||||||
|
|
||||||
if (initialArgs.logLevel) {
|
if (initialArgs.logLevel) {
|
||||||
deps.setLogLevel(initialArgs.logLevel, 'cli');
|
deps.setLogLevel(initialArgs.logLevel, 'cli');
|
||||||
|
} else if (initialArgs.background) {
|
||||||
|
deps.setLogLevel('warn', 'cli');
|
||||||
}
|
}
|
||||||
|
|
||||||
deps.forceX11Backend(initialArgs);
|
deps.forceX11Backend(initialArgs);
|
||||||
@@ -59,6 +62,7 @@ export function runStartupBootstrapRuntime(
|
|||||||
backendOverride: initialArgs.backend ?? null,
|
backendOverride: initialArgs.backend ?? null,
|
||||||
autoStartOverlay: initialArgs.autoStartOverlay,
|
autoStartOverlay: initialArgs.autoStartOverlay,
|
||||||
texthookerOnlyMode: initialArgs.texthooker,
|
texthookerOnlyMode: initialArgs.texthooker,
|
||||||
|
backgroundMode: initialArgs.background,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!deps.runGenerateConfigFlow(initialArgs)) {
|
if (!deps.runGenerateConfigFlow(initialArgs)) {
|
||||||
|
|||||||
35
src/main-entry.ts
Normal file
35
src/main-entry.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
|
||||||
|
const BACKGROUND_ARG = '--background';
|
||||||
|
const BACKGROUND_CHILD_ENV = 'SUBMINER_BACKGROUND_CHILD';
|
||||||
|
|
||||||
|
function shouldDetachBackgroundLaunch(argv: string[], env: NodeJS.ProcessEnv): boolean {
|
||||||
|
if (env.ELECTRON_RUN_AS_NODE === '1') return false;
|
||||||
|
if (!argv.includes(BACKGROUND_ARG)) return false;
|
||||||
|
if (env[BACKGROUND_CHILD_ENV] === '1') return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeBackgroundEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||||
|
const env = { ...baseEnv };
|
||||||
|
env[BACKGROUND_CHILD_ENV] = '1';
|
||||||
|
if (!env.NODE_NO_WARNINGS) {
|
||||||
|
env.NODE_NO_WARNINGS = '1';
|
||||||
|
}
|
||||||
|
if (typeof env.VK_INSTANCE_LAYERS === 'string' && /lsfg/i.test(env.VK_INSTANCE_LAYERS)) {
|
||||||
|
delete env.VK_INSTANCE_LAYERS;
|
||||||
|
}
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldDetachBackgroundLaunch(process.argv, process.env)) {
|
||||||
|
const child = spawn(process.execPath, process.argv.slice(1), {
|
||||||
|
detached: true,
|
||||||
|
stdio: 'ignore',
|
||||||
|
env: sanitizeBackgroundEnv(process.env),
|
||||||
|
});
|
||||||
|
child.unref();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
require('./main.js');
|
||||||
@@ -15,6 +15,7 @@ export interface AppLifecycleRuntimeDepsFactoryInput {
|
|||||||
onWillQuitCleanup: () => void;
|
onWillQuitCleanup: () => void;
|
||||||
shouldRestoreWindowsOnActivate: () => boolean;
|
shouldRestoreWindowsOnActivate: () => boolean;
|
||||||
restoreWindowsOnActivate: () => void;
|
restoreWindowsOnActivate: () => void;
|
||||||
|
shouldQuitOnWindowAllClosed: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppReadyRuntimeDepsFactoryInput {
|
export interface AppReadyRuntimeDepsFactoryInput {
|
||||||
@@ -59,6 +60,7 @@ export function createAppLifecycleRuntimeDeps(
|
|||||||
onWillQuitCleanup: params.onWillQuitCleanup,
|
onWillQuitCleanup: params.onWillQuitCleanup,
|
||||||
shouldRestoreWindowsOnActivate: params.shouldRestoreWindowsOnActivate,
|
shouldRestoreWindowsOnActivate: params.shouldRestoreWindowsOnActivate,
|
||||||
restoreWindowsOnActivate: params.restoreWindowsOnActivate,
|
restoreWindowsOnActivate: params.restoreWindowsOnActivate,
|
||||||
|
shouldQuitOnWindowAllClosed: params.shouldQuitOnWindowAllClosed,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export interface AppLifecycleRuntimeRunnerParams {
|
|||||||
onWillQuitCleanup: () => void;
|
onWillQuitCleanup: () => void;
|
||||||
shouldRestoreWindowsOnActivate: () => boolean;
|
shouldRestoreWindowsOnActivate: () => boolean;
|
||||||
restoreWindowsOnActivate: () => void;
|
restoreWindowsOnActivate: () => void;
|
||||||
|
shouldQuitOnWindowAllClosed: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAppLifecycleRuntimeRunner(
|
export function createAppLifecycleRuntimeRunner(
|
||||||
@@ -37,6 +38,7 @@ export function createAppLifecycleRuntimeRunner(
|
|||||||
onWillQuitCleanup: params.onWillQuitCleanup,
|
onWillQuitCleanup: params.onWillQuitCleanup,
|
||||||
shouldRestoreWindowsOnActivate: params.shouldRestoreWindowsOnActivate,
|
shouldRestoreWindowsOnActivate: params.shouldRestoreWindowsOnActivate,
|
||||||
restoreWindowsOnActivate: params.restoreWindowsOnActivate,
|
restoreWindowsOnActivate: params.restoreWindowsOnActivate,
|
||||||
|
shouldQuitOnWindowAllClosed: params.shouldQuitOnWindowAllClosed,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export interface AppState {
|
|||||||
backendOverride: string | null;
|
backendOverride: string | null;
|
||||||
autoStartOverlay: boolean;
|
autoStartOverlay: boolean;
|
||||||
texthookerOnlyMode: boolean;
|
texthookerOnlyMode: boolean;
|
||||||
|
backgroundMode: boolean;
|
||||||
jlptLevelLookup: (term: string) => JlptLevel | null;
|
jlptLevelLookup: (term: string) => JlptLevel | null;
|
||||||
frequencyRankLookup: FrequencyDictionaryLookup;
|
frequencyRankLookup: FrequencyDictionaryLookup;
|
||||||
anilistSetupPageOpened: boolean;
|
anilistSetupPageOpened: boolean;
|
||||||
@@ -90,6 +91,7 @@ export interface AppStateInitialValues {
|
|||||||
backendOverride?: string | null;
|
backendOverride?: string | null;
|
||||||
autoStartOverlay?: boolean;
|
autoStartOverlay?: boolean;
|
||||||
texthookerOnlyMode?: boolean;
|
texthookerOnlyMode?: boolean;
|
||||||
|
backgroundMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StartupState {
|
export interface StartupState {
|
||||||
@@ -99,6 +101,7 @@ export interface StartupState {
|
|||||||
backendOverride: AppState['backendOverride'];
|
backendOverride: AppState['backendOverride'];
|
||||||
autoStartOverlay: AppState['autoStartOverlay'];
|
autoStartOverlay: AppState['autoStartOverlay'];
|
||||||
texthookerOnlyMode: AppState['texthookerOnlyMode'];
|
texthookerOnlyMode: AppState['texthookerOnlyMode'];
|
||||||
|
backgroundMode: AppState['backgroundMode'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAppState(values: AppStateInitialValues): AppState {
|
export function createAppState(values: AppStateInitialValues): AppState {
|
||||||
@@ -152,6 +155,7 @@ export function createAppState(values: AppStateInitialValues): AppState {
|
|||||||
backendOverride: values.backendOverride ?? null,
|
backendOverride: values.backendOverride ?? null,
|
||||||
autoStartOverlay: values.autoStartOverlay ?? false,
|
autoStartOverlay: values.autoStartOverlay ?? false,
|
||||||
texthookerOnlyMode: values.texthookerOnlyMode ?? false,
|
texthookerOnlyMode: values.texthookerOnlyMode ?? false,
|
||||||
|
backgroundMode: values.backgroundMode ?? false,
|
||||||
jlptLevelLookup: () => null,
|
jlptLevelLookup: () => null,
|
||||||
frequencyRankLookup: () => null,
|
frequencyRankLookup: () => null,
|
||||||
anilistSetupPageOpened: false,
|
anilistSetupPageOpened: false,
|
||||||
@@ -172,4 +176,5 @@ export function applyStartupState(appState: AppState, startupState: StartupState
|
|||||||
appState.backendOverride = startupState.backendOverride;
|
appState.backendOverride = startupState.backendOverride;
|
||||||
appState.autoStartOverlay = startupState.autoStartOverlay;
|
appState.autoStartOverlay = startupState.autoStartOverlay;
|
||||||
appState.texthookerOnlyMode = startupState.texthookerOnlyMode;
|
appState.texthookerOnlyMode = startupState.texthookerOnlyMode;
|
||||||
|
appState.backgroundMode = startupState.backgroundMode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ export interface AnkiConnectConfig {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
url?: string;
|
url?: string;
|
||||||
pollingRate?: number;
|
pollingRate?: number;
|
||||||
|
tags?: string[];
|
||||||
fields?: {
|
fields?: {
|
||||||
audio?: string;
|
audio?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
@@ -423,6 +424,7 @@ export interface ResolvedConfig {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
url: string;
|
url: string;
|
||||||
pollingRate: number;
|
pollingRate: number;
|
||||||
|
tags: string[];
|
||||||
fields: {
|
fields: {
|
||||||
audio: string;
|
audio: string;
|
||||||
image: string;
|
image: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user