mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-01 06:22:44 -08:00
Add MPV overlay queue controls
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
- **N+1 Highlighting** — Marks known vocabulary from your Anki deck so you can spot new words at a glance
|
- **N+1 Highlighting** — Marks known vocabulary from your Anki deck so you can spot new words at a glance
|
||||||
- **Texthooker & WebSocket** — Built-in texthooker page with WebSocket streaming for external tools
|
- **Texthooker & WebSocket** — Built-in texthooker page with WebSocket streaming for external tools
|
||||||
- **Subtitle Download & Sync** — Search Jimaku, sync with alass or ffsubsync — all from the player
|
- **Subtitle Download & Sync** — Search Jimaku, sync with alass or ffsubsync — all from the player
|
||||||
|
- **Queue Control In-Player** — Drop videos on overlay to load/queue in mpv; `Ctrl/Cmd+A` appends clipboard path
|
||||||
- **Keyboard-Driven** — Mine, copy, cycle display modes, and navigate from configurable shortcuts
|
- **Keyboard-Driven** — Mine, copy, cycle display modes, and navigate from configurable shortcuts
|
||||||
- **Japanese Tokenization** — MeCab-powered word boundary detection with smart grouping
|
- **Japanese Tokenization** — MeCab-powered word boundary detection with smart grouping
|
||||||
|
|
||||||
@@ -98,6 +99,13 @@ Use `subminer <subcommand> -h` for command-specific help pages (for example `sub
|
|||||||
- 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 remains `info` unless you pass `--log-level`.
|
||||||
|
|
||||||
|
### Overlay Queue Controls
|
||||||
|
|
||||||
|
- Drag/drop video file(s) onto overlay:
|
||||||
|
- default: replace current playback with first file, append remaining dropped files
|
||||||
|
- hold `Shift`: append all dropped files
|
||||||
|
- Press `Ctrl/Cmd+A` to append the clipboard path when it points to a readable local video file.
|
||||||
|
|
||||||
## MPV Plugin
|
## MPV Plugin
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ status: Done
|
|||||||
assignee:
|
assignee:
|
||||||
- codex
|
- codex
|
||||||
created_date: '2026-02-11 15:45'
|
created_date: '2026-02-11 15:45'
|
||||||
updated_date: '2026-02-11 16:36'
|
updated_date: '2026-02-18 09:29'
|
||||||
labels:
|
labels:
|
||||||
- bug
|
- bug
|
||||||
- macos
|
- macos
|
||||||
@@ -15,22 +15,19 @@ references:
|
|||||||
- src/window-trackers/macos-tracker.ts
|
- src/window-trackers/macos-tracker.ts
|
||||||
- scripts/get-mpv-window-macos.swift
|
- scripts/get-mpv-window-macos.swift
|
||||||
priority: high
|
priority: high
|
||||||
|
ordinal: 58000
|
||||||
---
|
---
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
|
||||||
Overlay windows on macOS are not properly aligned to the mpv window after switching from AppleScript window discovery to native Swift/CoreGraphics bounds retrieval.
|
Overlay windows on macOS are not properly aligned to the mpv window after switching from AppleScript window discovery to native Swift/CoreGraphics bounds retrieval.
|
||||||
|
|
||||||
Implement a robust native bounds strategy that prefers Accessibility window geometry (matching app-window coordinates used previously) and falls back to filtered CoreGraphics windows when Accessibility data is unavailable.
|
Implement a robust native bounds strategy that prefers Accessibility window geometry (matching app-window coordinates used previously) and falls back to filtered CoreGraphics windows when Accessibility data is unavailable.
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
<!-- AC:BEGIN -->
|
<!-- AC:BEGIN -->
|
||||||
|
|
||||||
- [x] #1 Overlay bounds track the active mpv window with correct position and size on macOS.
|
- [x] #1 Overlay bounds track the active mpv window with correct position and size on macOS.
|
||||||
- [x] #2 Helper avoids selecting off-screen/non-primary mpv-related windows.
|
- [x] #2 Helper avoids selecting off-screen/non-primary mpv-related windows.
|
||||||
- [x] #3 Build succeeds with the updated macOS helper.
|
- [x] #3 Build succeeds with the updated macOS helper.
|
||||||
@@ -39,7 +36,6 @@ Implement a robust native bounds strategy that prefers Accessibility window geom
|
|||||||
## Implementation Notes
|
## Implementation Notes
|
||||||
|
|
||||||
<!-- SECTION:NOTES:BEGIN -->
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
|
||||||
Follow-up in progress after packaged app runtime showed fullscreen fallback behavior:
|
Follow-up in progress after packaged app runtime showed fullscreen fallback behavior:
|
||||||
|
|
||||||
- Added packaged-app helper path resolution in tracker (`process.resourcesPath/scripts/get-mpv-window-macos`).
|
- Added packaged-app helper path resolution in tracker (`process.resourcesPath/scripts/get-mpv-window-macos`).
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-13
|
|
||||||
title: Fix second-instance --start when texthooker-only instance is running
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-02-11 23:47'
|
|
||||||
updated_date: '2026-02-18 04:11'
|
|
||||||
labels:
|
|
||||||
- bugfix
|
|
||||||
- cli
|
|
||||||
- overlay
|
|
||||||
dependencies: []
|
|
||||||
priority: high
|
|
||||||
ordinal: 51000
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
When SubMiner is already running in texthooker-only mode, a subsequent `--start` command from a second instance is currently ignored. This can leave users without an initialized overlay runtime even though startup commands were issued. Adjust CLI command handling so `--start` on second-instance initializes overlay runtime when it is not yet initialized, while preserving current ignore behavior when overlay runtime is already active.
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
<!-- AC:BEGIN -->
|
|
||||||
|
|
||||||
- [x] #1 Second-instance `--start` initializes overlay runtime when current instance has deferred/not-initialized overlay runtime.
|
|
||||||
- [x] #2 Second-instance `--start` remains ignored (existing behavior) when overlay runtime is already initialized.
|
|
||||||
- [x] #3 CLI command service tests cover both behaviors and pass.
|
|
||||||
<!-- AC:END -->
|
|
||||||
|
|
||||||
## Implementation Notes
|
|
||||||
|
|
||||||
<!-- SECTION:NOTES:BEGIN -->
|
|
||||||
|
|
||||||
Patched CLI second-instance `--start` handling in `src/core/services/cli-command-service.ts` to initialize overlay runtime when deferred.
|
|
||||||
|
|
||||||
Added regression test for deferred-runtime start path and updated initialized-runtime second-instance tests in `src/core/services/cli-command-service.test.ts`.
|
|
||||||
|
|
||||||
<!-- SECTION:NOTES:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
Fixed overlay startup regression path where a second-instance `--start` could be ignored even when the primary instance was running in texthooker-only/deferred overlay mode.
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
- Updated `handleCliCommandService` logic so `ignoreStart` applies only when source is second-instance, `--start` is present, and overlay runtime is already initialized.
|
|
||||||
- Added explicit overlay-runtime initialization path for second-instance `--start` when runtime is not initialized.
|
|
||||||
- Kept existing behavior for already-initialized runtime (still logs and ignores redundant `--start`).
|
|
||||||
- Added and updated tests in `cli-command-service.test.ts` to validate both deferred and initialized second-instance startup behaviors.
|
|
||||||
|
|
||||||
Validation:
|
|
||||||
|
|
||||||
- `pnpm run build` succeeded.
|
|
||||||
- `node dist/core/services/cli-command-service.test.js` passed (11/11).
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,21 +1,22 @@
|
|||||||
---
|
---
|
||||||
id: TASK-39
|
id: TASK-39
|
||||||
title: Add hot-reload for non-destructive config changes
|
title: Add hot-reload for non-destructive config changes
|
||||||
status: To Do
|
status: Done
|
||||||
assignee: []
|
assignee: []
|
||||||
created_date: '2026-02-14 02:04'
|
created_date: '2026-02-14 02:04'
|
||||||
|
updated_date: '2026-02-18 09:29'
|
||||||
labels:
|
labels:
|
||||||
- config
|
- config
|
||||||
- developer-experience
|
- developer-experience
|
||||||
- quality-of-life
|
- quality-of-life
|
||||||
dependencies: []
|
dependencies: []
|
||||||
priority: low
|
priority: low
|
||||||
|
ordinal: 59000
|
||||||
---
|
---
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
|
||||||
Watch the config file for changes and apply non-destructive updates (colors, font sizes, subtitle modes, overlay opacity, keybindings) without requiring an app restart.
|
Watch the config file for changes and apply non-destructive updates (colors, font sizes, subtitle modes, overlay opacity, keybindings) without requiring an app restart.
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
@@ -49,13 +50,11 @@ Currently all config is loaded at startup. Users tweaking visual settings (font
|
|||||||
<!-- SECTION:DESCRIPTION:END -->
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
<!-- AC:BEGIN -->
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 Config file changes are detected automatically via file watcher.
|
||||||
- [ ] #1 Config file changes are detected automatically via file watcher.
|
- [x] #2 Hot-reloadable fields are applied immediately without restart.
|
||||||
- [ ] #2 Hot-reloadable fields are applied immediately without restart.
|
- [x] #3 Restart-required fields trigger a user-visible notification.
|
||||||
- [ ] #3 Restart-required fields trigger a user-visible notification.
|
- [x] #4 File change events are debounced to handle editor save patterns.
|
||||||
- [ ] #4 File change events are debounced to handle editor save patterns.
|
- [x] #5 Invalid config changes are rejected with an error notification, keeping the previous valid config.
|
||||||
- [ ] #5 Invalid config changes are rejected with an error notification, keeping the previous valid config.
|
- [x] #6 Renderer receives updated styles/settings via IPC without full page reload.
|
||||||
- [ ] #6 Renderer receives updated styles/settings via IPC without full page reload.
|
|
||||||
<!-- AC:END -->
|
<!-- AC:END -->
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
id: TASK-65
|
||||||
|
title: Add overlay drag-drop playlist loading and clipboard append shortcut
|
||||||
|
status: Done
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-02-18 13:10'
|
||||||
|
updated_date: '2026-02-18 13:10'
|
||||||
|
labels: []
|
||||||
|
dependencies: []
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Implement direct playlist control from the overlay:
|
||||||
|
|
||||||
|
- Drag/drop video files onto overlay:
|
||||||
|
- default drop: replace current playback with dropped set (first `replace`, remainder `append`)
|
||||||
|
- `Shift` + drop: append all dropped files
|
||||||
|
- `Ctrl/Cmd+A`: read clipboard text, if it resolves to a supported local video file path, append it to mpv playlist.
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
- [x] Add TDD coverage for drop path parsing, command mode generation, and clipboard path parsing (`src/core/services/overlay-drop.test.ts`).
|
||||||
|
- [x] Implement drop/clipboard parser + mpv command-builder utility (`src/core/services/overlay-drop.ts`).
|
||||||
|
- [x] Wire renderer drag/drop handling and mpv command dispatch (`src/renderer/renderer.ts`).
|
||||||
|
- [x] Add IPC API for clipboard append flow (`src/types.ts`, `src/preload.ts`, `src/core/services/ipc.ts`, `src/main/dependencies.ts`).
|
||||||
|
- [x] Implement main-process clipboard validation + append behavior (`src/main.ts`).
|
||||||
|
- [x] Add fixed keyboard shortcut hook (`Ctrl/Cmd+A`) in renderer keyboard handler (`src/renderer/handlers/keyboard.ts`, `src/renderer/renderer.ts`).
|
||||||
|
- [x] Update docs for new interaction model (`docs/usage.md`, `docs/configuration.md`).
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `bun run build`
|
||||||
|
- `node --test dist/core/services/overlay-drop.test.js dist/core/services/ipc.test.js`
|
||||||
@@ -118,6 +118,8 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// Keyboard Shortcuts
|
// Keyboard Shortcuts
|
||||||
// Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
// Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||||
|
// Fixed (non-configurable) overlay shortcuts:
|
||||||
|
// - Ctrl/Cmd+A: append clipboard video path to MPV playlist
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"toggleVisibleOverlayGlobal": "Alt+Shift+O",
|
"toggleVisibleOverlayGlobal": "Alt+Shift+O",
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ When `behavior.autoUpdateNewCards` is set to `false`, new cards are detected but
|
|||||||
| `Ctrl+Shift+V` | Cycle secondary subtitle display mode (hidden → visible → hover) |
|
| `Ctrl+Shift+V` | Cycle secondary subtitle display mode (hidden → visible → hover) |
|
||||||
| `Ctrl+Shift+A` | Mark the last added Anki card as an audio card (sets IsAudioCard, SentenceAudio, Sentence, Picture) |
|
| `Ctrl+Shift+A` | Mark the last added Anki card as an audio card (sets IsAudioCard, SentenceAudio, Sentence, Picture) |
|
||||||
| `Ctrl+Shift+O` | Open runtime options palette (session-only live toggles) |
|
| `Ctrl+Shift+O` | Open runtime options palette (session-only live toggles) |
|
||||||
|
| `Ctrl/Cmd+A` | Append clipboard video path to MPV playlist (fixed, not currently configurable) |
|
||||||
|
|
||||||
**Multi-line copy workflow:**
|
**Multi-line copy workflow:**
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ Configure in `ankiConnect.isKiku`. See [Anki Integration](/anki-integration#fiel
|
|||||||
|
|
||||||
SubMiner integrates with [Jimaku](https://jimaku.cc) to search and download subtitle files for anime directly from the overlay.
|
SubMiner integrates with [Jimaku](https://jimaku.cc) to search and download subtitle files for anime directly from the overlay.
|
||||||
|
|
||||||
1. Open the Jimaku modal via the configured shortcut (`Ctrl+Alt+J` by default).
|
1. Open the Jimaku modal via the configured shortcut (`Ctrl+Shift+J` by default).
|
||||||
2. SubMiner auto-fills the search from the current video filename (title, season, episode).
|
2. SubMiner auto-fills the search from the current video filename (title, season, episode).
|
||||||
3. Browse matching entries and select a subtitle file to download.
|
3. Browse matching entries and select a subtitle file to download.
|
||||||
4. The subtitle is loaded into mpv as a new track.
|
4. The subtitle is loaded into mpv as a new track.
|
||||||
|
|||||||
@@ -118,6 +118,8 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// Keyboard Shortcuts
|
// Keyboard Shortcuts
|
||||||
// Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
// Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||||
|
// Fixed (non-configurable) overlay shortcuts:
|
||||||
|
// - Ctrl/Cmd+A: append clipboard video path to MPV playlist
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"toggleVisibleOverlayGlobal": "Alt+Shift+O",
|
"toggleVisibleOverlayGlobal": "Alt+Shift+O",
|
||||||
|
|||||||
@@ -174,9 +174,15 @@ Notes:
|
|||||||
| `Arrow keys` | Move invisible subtitles while edit mode is active |
|
| `Arrow keys` | Move invisible subtitles while edit mode is active |
|
||||||
| `Enter` / `Ctrl+S` | Save invisible subtitle position in edit mode |
|
| `Enter` / `Ctrl+S` | Save invisible subtitle position in edit mode |
|
||||||
| `Esc` | Cancel invisible subtitle position edit mode |
|
| `Esc` | Cancel invisible subtitle position edit mode |
|
||||||
|
| `Ctrl/Cmd+A` | Append clipboard video path to MPV playlist |
|
||||||
|
|
||||||
These keybindings only work when the overlay window has focus. See [Configuration](/configuration) for customization.
|
These keybindings only work when the overlay window has focus. See [Configuration](/configuration) for customization.
|
||||||
|
|
||||||
|
### Drag-and-drop Queueing
|
||||||
|
|
||||||
|
- Drag and drop one or more video files onto the overlay to replace current playback (`loadfile ... replace` for first file, then append remainder).
|
||||||
|
- Hold `Shift` while dropping to append all dropped files to the current MPV playlist.
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
1. MPV runs with an IPC socket at `/tmp/subminer-socket`
|
1. MPV runs with an IPC socket at `/tmp/subminer-socket`
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export {
|
|||||||
sanitizeMpvSubtitleRenderMetrics,
|
sanitizeMpvSubtitleRenderMetrics,
|
||||||
} from './mpv-render-metrics';
|
} from './mpv-render-metrics';
|
||||||
export { createOverlayContentMeasurementStore } from './overlay-content-measurement';
|
export { createOverlayContentMeasurementStore } from './overlay-content-measurement';
|
||||||
|
export { parseClipboardVideoPath } from './overlay-drop';
|
||||||
export { handleMpvCommandFromIpc } from './ipc-command';
|
export { handleMpvCommandFromIpc } from './ipc-command';
|
||||||
export { createFieldGroupingOverlayRuntime } from './field-grouping-overlay';
|
export { createFieldGroupingOverlayRuntime } from './field-grouping-overlay';
|
||||||
export { createNumericShortcutRuntime } from './numeric-shortcut';
|
export { createNumericShortcutRuntime } from './numeric-shortcut';
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ test('createIpcDepsRuntime wires AniList handlers', async () => {
|
|||||||
calls.push('retryAnilistQueueNow');
|
calls.push('retryAnilistQueueNow');
|
||||||
return { ok: true, message: 'done' };
|
return { ok: true, message: 'done' };
|
||||||
},
|
},
|
||||||
|
appendClipboardVideoToQueue: () => ({ ok: true, message: 'queued' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(deps.getAnilistStatus(), { tokenStatus: 'resolved' });
|
assert.deepEqual(deps.getAnilistStatus(), { tokenStatus: 'resolved' });
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export interface IpcServiceDeps {
|
|||||||
openAnilistSetup: () => void;
|
openAnilistSetup: () => void;
|
||||||
getAnilistQueueStatus: () => unknown;
|
getAnilistQueueStatus: () => unknown;
|
||||||
retryAnilistQueueNow: () => Promise<{ ok: boolean; message: string }>;
|
retryAnilistQueueNow: () => Promise<{ ok: boolean; message: string }>;
|
||||||
|
appendClipboardVideoToQueue: () => { ok: boolean; message: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WindowLike {
|
interface WindowLike {
|
||||||
@@ -97,6 +98,7 @@ export interface IpcDepsRuntimeOptions {
|
|||||||
openAnilistSetup: () => void;
|
openAnilistSetup: () => void;
|
||||||
getAnilistQueueStatus: () => unknown;
|
getAnilistQueueStatus: () => unknown;
|
||||||
retryAnilistQueueNow: () => Promise<{ ok: boolean; message: string }>;
|
retryAnilistQueueNow: () => Promise<{ ok: boolean; message: string }>;
|
||||||
|
appendClipboardVideoToQueue: () => { ok: boolean; message: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcServiceDeps {
|
export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcServiceDeps {
|
||||||
@@ -157,6 +159,7 @@ export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcService
|
|||||||
openAnilistSetup: options.openAnilistSetup,
|
openAnilistSetup: options.openAnilistSetup,
|
||||||
getAnilistQueueStatus: options.getAnilistQueueStatus,
|
getAnilistQueueStatus: options.getAnilistQueueStatus,
|
||||||
retryAnilistQueueNow: options.retryAnilistQueueNow,
|
retryAnilistQueueNow: options.retryAnilistQueueNow,
|
||||||
|
appendClipboardVideoToQueue: options.appendClipboardVideoToQueue,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,4 +317,8 @@ export function registerIpcHandlers(deps: IpcServiceDeps): void {
|
|||||||
ipcMain.handle('anilist:retry-now', async () => {
|
ipcMain.handle('anilist:retry-now', async () => {
|
||||||
return await deps.retryAnilistQueueNow();
|
return await deps.retryAnilistQueueNow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('clipboard:append-video-to-queue', () => {
|
||||||
|
return deps.appendClipboardVideoToQueue();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
69
src/core/services/overlay-drop.test.ts
Normal file
69
src/core/services/overlay-drop.test.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import {
|
||||||
|
buildMpvLoadfileCommands,
|
||||||
|
collectDroppedVideoPaths,
|
||||||
|
parseClipboardVideoPath,
|
||||||
|
type DropDataTransferLike,
|
||||||
|
} from './overlay-drop';
|
||||||
|
|
||||||
|
function makeTransfer(data: Partial<DropDataTransferLike>): DropDataTransferLike {
|
||||||
|
return {
|
||||||
|
files: data.files,
|
||||||
|
getData: data.getData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('collectDroppedVideoPaths keeps supported dropped file paths in order', () => {
|
||||||
|
const transfer = makeTransfer({
|
||||||
|
files: [
|
||||||
|
{ path: '/videos/ep02.mkv' },
|
||||||
|
{ path: '/videos/notes.txt' },
|
||||||
|
{ path: '/videos/ep03.MP4' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = collectDroppedVideoPaths(transfer);
|
||||||
|
|
||||||
|
assert.deepEqual(result, ['/videos/ep02.mkv', '/videos/ep03.MP4']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('collectDroppedVideoPaths parses text/uri-list entries and de-duplicates', () => {
|
||||||
|
const transfer = makeTransfer({
|
||||||
|
getData: (format: string) =>
|
||||||
|
format === 'text/uri-list'
|
||||||
|
? '#comment\nfile:///tmp/ep01.mkv\nfile:///tmp/ep01.mkv\nfile:///tmp/ep02.webm\nfile:///tmp/readme.md\n'
|
||||||
|
: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = collectDroppedVideoPaths(transfer);
|
||||||
|
|
||||||
|
assert.deepEqual(result, ['/tmp/ep01.mkv', '/tmp/ep02.webm']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildMpvLoadfileCommands replaces first file and appends remainder by default', () => {
|
||||||
|
const commands = buildMpvLoadfileCommands(['/tmp/ep01.mkv', '/tmp/ep02.mkv'], false);
|
||||||
|
|
||||||
|
assert.deepEqual(commands, [
|
||||||
|
['loadfile', '/tmp/ep01.mkv', 'replace'],
|
||||||
|
['loadfile', '/tmp/ep02.mkv', 'append'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildMpvLoadfileCommands uses append mode when shift-drop is used', () => {
|
||||||
|
const commands = buildMpvLoadfileCommands(['/tmp/ep01.mkv', '/tmp/ep02.mkv'], true);
|
||||||
|
|
||||||
|
assert.deepEqual(commands, [
|
||||||
|
['loadfile', '/tmp/ep01.mkv', 'append'],
|
||||||
|
['loadfile', '/tmp/ep02.mkv', 'append'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parseClipboardVideoPath accepts quoted local paths', () => {
|
||||||
|
assert.equal(parseClipboardVideoPath('"/tmp/ep10.mkv"'), '/tmp/ep10.mkv');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parseClipboardVideoPath accepts file URI and rejects non-video', () => {
|
||||||
|
assert.equal(parseClipboardVideoPath('file:///tmp/ep11.mp4'), '/tmp/ep11.mp4');
|
||||||
|
assert.equal(parseClipboardVideoPath('/tmp/notes.txt'), null);
|
||||||
|
});
|
||||||
130
src/core/services/overlay-drop.ts
Normal file
130
src/core/services/overlay-drop.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
export type DropFileLike = { path?: string } | { name: string };
|
||||||
|
|
||||||
|
export interface DropDataTransferLike {
|
||||||
|
files?: ArrayLike<DropFileLike>;
|
||||||
|
getData?: (format: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VIDEO_EXTENSIONS = new Set([
|
||||||
|
'.3gp',
|
||||||
|
'.avi',
|
||||||
|
'.flv',
|
||||||
|
'.m2ts',
|
||||||
|
'.m4v',
|
||||||
|
'.mkv',
|
||||||
|
'.mov',
|
||||||
|
'.mp4',
|
||||||
|
'.mpeg',
|
||||||
|
'.mpg',
|
||||||
|
'.mts',
|
||||||
|
'.ts',
|
||||||
|
'.webm',
|
||||||
|
'.wmv',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function getPathExtension(pathValue: string): string {
|
||||||
|
const normalized = pathValue.split(/[?#]/, 1)[0];
|
||||||
|
const dot = normalized.lastIndexOf('.');
|
||||||
|
return dot >= 0 ? normalized.slice(dot).toLowerCase() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSupportedVideoPath(pathValue: string): boolean {
|
||||||
|
return VIDEO_EXTENSIONS.has(getPathExtension(pathValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseUriList(data: string): string[] {
|
||||||
|
if (!data.trim()) return [];
|
||||||
|
const out: string[] = [];
|
||||||
|
|
||||||
|
for (const line of data.split(/\r?\n/)) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||||
|
if (!trimmed.toLowerCase().startsWith('file://')) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = new URL(trimmed);
|
||||||
|
let filePath = decodeURIComponent(parsed.pathname);
|
||||||
|
if (/^\/[A-Za-z]:\//.test(filePath)) {
|
||||||
|
filePath = filePath.slice(1);
|
||||||
|
}
|
||||||
|
if (filePath && isSupportedVideoPath(filePath)) {
|
||||||
|
out.push(filePath);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseClipboardVideoPath(text: string): string | null {
|
||||||
|
const trimmed = text.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
|
||||||
|
const unquoted =
|
||||||
|
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
||||||
|
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
||||||
|
? trimmed.slice(1, -1).trim()
|
||||||
|
: trimmed;
|
||||||
|
if (!unquoted) return null;
|
||||||
|
|
||||||
|
if (unquoted.toLowerCase().startsWith('file://')) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(unquoted);
|
||||||
|
let filePath = decodeURIComponent(parsed.pathname);
|
||||||
|
if (/^\/[A-Za-z]:\//.test(filePath)) {
|
||||||
|
filePath = filePath.slice(1);
|
||||||
|
}
|
||||||
|
return filePath && isSupportedVideoPath(filePath) ? filePath : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isSupportedVideoPath(unquoted) ? unquoted : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectDroppedVideoPaths(dataTransfer: DropDataTransferLike | null | undefined): string[] {
|
||||||
|
if (!dataTransfer) return [];
|
||||||
|
|
||||||
|
const out: string[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
|
const addPath = (candidate: string | null | undefined): void => {
|
||||||
|
if (!candidate) return;
|
||||||
|
const trimmed = candidate.trim();
|
||||||
|
if (!trimmed || !isSupportedVideoPath(trimmed) || seen.has(trimmed)) return;
|
||||||
|
seen.add(trimmed);
|
||||||
|
out.push(trimmed);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dataTransfer.files) {
|
||||||
|
for (let i = 0; i < dataTransfer.files.length; i += 1) {
|
||||||
|
const file = dataTransfer.files[i] as { path?: string } | undefined;
|
||||||
|
addPath(file?.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof dataTransfer.getData === 'function') {
|
||||||
|
for (const pathValue of parseUriList(dataTransfer.getData('text/uri-list'))) {
|
||||||
|
addPath(pathValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMpvLoadfileCommands(
|
||||||
|
paths: string[],
|
||||||
|
append: boolean,
|
||||||
|
): Array<(string | number)[]> {
|
||||||
|
if (append) {
|
||||||
|
return paths.map((pathValue) => ['loadfile', pathValue, 'append']);
|
||||||
|
}
|
||||||
|
return paths.map((pathValue, index) => [
|
||||||
|
'loadfile',
|
||||||
|
pathValue,
|
||||||
|
index === 0 ? 'replace' : 'append',
|
||||||
|
]);
|
||||||
|
}
|
||||||
26
src/main.ts
26
src/main.ts
@@ -111,6 +111,7 @@ import {
|
|||||||
JellyfinRemoteSessionService,
|
JellyfinRemoteSessionService,
|
||||||
mineSentenceCard as mineSentenceCardCore,
|
mineSentenceCard as mineSentenceCardCore,
|
||||||
openYomitanSettingsWindow,
|
openYomitanSettingsWindow,
|
||||||
|
parseClipboardVideoPath,
|
||||||
playNextSubtitleRuntime,
|
playNextSubtitleRuntime,
|
||||||
registerGlobalShortcuts as registerGlobalShortcutsCore,
|
registerGlobalShortcuts as registerGlobalShortcutsCore,
|
||||||
replayCurrentSubtitleRuntime,
|
replayCurrentSubtitleRuntime,
|
||||||
@@ -2862,6 +2863,30 @@ async function runSubsyncManualFromIpc(request: SubsyncManualRunRequest): Promis
|
|||||||
return runSubsyncManualFromIpcRuntime(request, getSubsyncRuntimeServiceParams());
|
return runSubsyncManualFromIpcRuntime(request, getSubsyncRuntimeServiceParams());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendClipboardVideoToQueue(): { ok: boolean; message: string } {
|
||||||
|
const mpvClient = appState.mpvClient;
|
||||||
|
if (!mpvClient || !mpvClient.connected) {
|
||||||
|
return { ok: false, message: 'MPV is not connected.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const clipboardText = clipboard.readText();
|
||||||
|
const parsedPath = parseClipboardVideoPath(clipboardText);
|
||||||
|
if (!parsedPath) {
|
||||||
|
showMpvOsd('Clipboard does not contain a supported video path.');
|
||||||
|
return { ok: false, message: 'Clipboard does not contain a supported video path.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedPath = path.resolve(parsedPath);
|
||||||
|
if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isFile()) {
|
||||||
|
showMpvOsd('Clipboard path is not a readable file.');
|
||||||
|
return { ok: false, message: 'Clipboard path is not a readable file.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMpvCommandRuntime(mpvClient, ['loadfile', resolvedPath, 'append']);
|
||||||
|
showMpvOsd(`Queued from clipboard: ${path.basename(resolvedPath)}`);
|
||||||
|
return { ok: true, message: `Queued ${resolvedPath}` };
|
||||||
|
}
|
||||||
|
|
||||||
registerIpcRuntimeServices({
|
registerIpcRuntimeServices({
|
||||||
runtimeOptions: {
|
runtimeOptions: {
|
||||||
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
||||||
@@ -2922,6 +2947,7 @@ registerIpcRuntimeServices({
|
|||||||
openAnilistSetup: () => openAnilistSetupWindow(),
|
openAnilistSetup: () => openAnilistSetupWindow(),
|
||||||
getAnilistQueueStatus: () => getAnilistQueueStatusSnapshot(),
|
getAnilistQueueStatus: () => getAnilistQueueStatusSnapshot(),
|
||||||
retryAnilistQueueNow: () => processNextAnilistRetryUpdate(),
|
retryAnilistQueueNow: () => processNextAnilistRetryUpdate(),
|
||||||
|
appendClipboardVideoToQueue: () => appendClipboardVideoToQueue(),
|
||||||
},
|
},
|
||||||
ankiJimakuDeps: createAnkiJimakuIpcRuntimeServiceDeps({
|
ankiJimakuDeps: createAnkiJimakuIpcRuntimeServiceDeps({
|
||||||
patchAnkiConnectEnabled: (enabled: boolean) => {
|
patchAnkiConnectEnabled: (enabled: boolean) => {
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export interface MainIpcRuntimeServiceDepsParams {
|
|||||||
openAnilistSetup: IpcDepsRuntimeOptions['openAnilistSetup'];
|
openAnilistSetup: IpcDepsRuntimeOptions['openAnilistSetup'];
|
||||||
getAnilistQueueStatus: IpcDepsRuntimeOptions['getAnilistQueueStatus'];
|
getAnilistQueueStatus: IpcDepsRuntimeOptions['getAnilistQueueStatus'];
|
||||||
retryAnilistQueueNow: IpcDepsRuntimeOptions['retryAnilistQueueNow'];
|
retryAnilistQueueNow: IpcDepsRuntimeOptions['retryAnilistQueueNow'];
|
||||||
|
appendClipboardVideoToQueue: IpcDepsRuntimeOptions['appendClipboardVideoToQueue'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnkiJimakuIpcRuntimeServiceDepsParams {
|
export interface AnkiJimakuIpcRuntimeServiceDepsParams {
|
||||||
@@ -227,6 +228,7 @@ export function createMainIpcRuntimeServiceDeps(
|
|||||||
openAnilistSetup: params.openAnilistSetup,
|
openAnilistSetup: params.openAnilistSetup,
|
||||||
getAnilistQueueStatus: params.getAnilistQueueStatus,
|
getAnilistQueueStatus: params.getAnilistQueueStatus,
|
||||||
retryAnilistQueueNow: params.retryAnilistQueueNow,
|
retryAnilistQueueNow: params.retryAnilistQueueNow,
|
||||||
|
appendClipboardVideoToQueue: params.appendClipboardVideoToQueue,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import type {
|
|||||||
SubsyncManualPayload,
|
SubsyncManualPayload,
|
||||||
SubsyncManualRunRequest,
|
SubsyncManualRunRequest,
|
||||||
SubsyncResult,
|
SubsyncResult,
|
||||||
|
ClipboardAppendResult,
|
||||||
KikuFieldGroupingRequestData,
|
KikuFieldGroupingRequestData,
|
||||||
KikuFieldGroupingChoice,
|
KikuFieldGroupingChoice,
|
||||||
KikuMergePreviewRequest,
|
KikuMergePreviewRequest,
|
||||||
@@ -227,6 +228,8 @@ const electronAPI: ElectronAPI = {
|
|||||||
callback();
|
callback();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
appendClipboardVideoToQueue: (): Promise<ClipboardAppendResult> =>
|
||||||
|
ipcRenderer.invoke('clipboard:append-video-to-queue'),
|
||||||
notifyOverlayModalClosed: (modal: 'runtime-options' | 'subsync' | 'jimaku') => {
|
notifyOverlayModalClosed: (modal: 'runtime-options' | 'subsync' | 'jimaku') => {
|
||||||
ipcRenderer.send('overlay:modal-closed', modal);
|
ipcRenderer.send('overlay:modal-closed', modal);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export function createKeyboardHandlers(
|
|||||||
setInvisiblePositionEditMode: (enabled: boolean) => void;
|
setInvisiblePositionEditMode: (enabled: boolean) => void;
|
||||||
applyInvisibleSubtitleOffsetPosition: () => void;
|
applyInvisibleSubtitleOffsetPosition: () => void;
|
||||||
updateInvisiblePositionEditHud: () => void;
|
updateInvisiblePositionEditHud: () => void;
|
||||||
|
appendClipboardVideoToQueue: () => void;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
// Timeout for the modal chord capture window (e.g. Y followed by H/K).
|
// Timeout for the modal chord capture window (e.g. Y followed by H/K).
|
||||||
@@ -257,6 +258,18 @@ export function createKeyboardHandlers(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(e.ctrlKey || e.metaKey) &&
|
||||||
|
!e.altKey &&
|
||||||
|
!e.shiftKey &&
|
||||||
|
e.code === 'KeyA' &&
|
||||||
|
!e.repeat
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
options.appendClipboardVideoToQueue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const keyString = keyEventToString(e);
|
const keyString = keyEventToString(e);
|
||||||
const command = ctx.state.keybindingsMap.get(keyString);
|
const command = ctx.state.keybindingsMap.get(keyString);
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { createRendererState } from './state.js';
|
|||||||
import { createSubtitleRenderer } from './subtitle-render.js';
|
import { createSubtitleRenderer } from './subtitle-render.js';
|
||||||
import { resolveRendererDom } from './utils/dom.js';
|
import { resolveRendererDom } from './utils/dom.js';
|
||||||
import { resolvePlatformInfo } from './utils/platform.js';
|
import { resolvePlatformInfo } from './utils/platform.js';
|
||||||
|
import { buildMpvLoadfileCommands, collectDroppedVideoPaths } from '../core/services/overlay-drop.js';
|
||||||
|
|
||||||
const ctx = {
|
const ctx = {
|
||||||
dom: resolveRendererDom(),
|
dom: resolveRendererDom(),
|
||||||
@@ -111,6 +112,9 @@ const keyboardHandlers = createKeyboardHandlers(ctx, {
|
|||||||
setInvisiblePositionEditMode: positioning.setInvisiblePositionEditMode,
|
setInvisiblePositionEditMode: positioning.setInvisiblePositionEditMode,
|
||||||
applyInvisibleSubtitleOffsetPosition: positioning.applyInvisibleSubtitleOffsetPosition,
|
applyInvisibleSubtitleOffsetPosition: positioning.applyInvisibleSubtitleOffsetPosition,
|
||||||
updateInvisiblePositionEditHud: positioning.updateInvisiblePositionEditHud,
|
updateInvisiblePositionEditHud: positioning.updateInvisiblePositionEditHud,
|
||||||
|
appendClipboardVideoToQueue: () => {
|
||||||
|
void window.electronAPI.appendClipboardVideoToQueue();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const mouseHandlers = createMouseHandlers(ctx, {
|
const mouseHandlers = createMouseHandlers(ctx, {
|
||||||
modalStateReader: { isAnySettingsModalOpen, isAnyModalOpen },
|
modalStateReader: { isAnySettingsModalOpen, isAnyModalOpen },
|
||||||
@@ -178,6 +182,7 @@ async function init(): Promise<void> {
|
|||||||
mouseHandlers.setupResizeHandler();
|
mouseHandlers.setupResizeHandler();
|
||||||
mouseHandlers.setupSelectionObserver();
|
mouseHandlers.setupSelectionObserver();
|
||||||
mouseHandlers.setupYomitanObserver();
|
mouseHandlers.setupYomitanObserver();
|
||||||
|
setupDragDropToMpvQueue();
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
measurementReporter.schedule();
|
measurementReporter.schedule();
|
||||||
});
|
});
|
||||||
@@ -242,6 +247,69 @@ async function init(): Promise<void> {
|
|||||||
measurementReporter.emitNow();
|
measurementReporter.emitNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupDragDropToMpvQueue(): void {
|
||||||
|
let dragDepth = 0;
|
||||||
|
|
||||||
|
const setDropInteractive = (): void => {
|
||||||
|
ctx.dom.overlay.classList.add('interactive');
|
||||||
|
if (ctx.platform.shouldToggleMouseIgnore) {
|
||||||
|
window.electronAPI.setIgnoreMouseEvents(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearDropInteractive = (): void => {
|
||||||
|
dragDepth = 0;
|
||||||
|
if (isAnyModalOpen() || ctx.state.isOverSubtitle || ctx.state.invisiblePositionEditMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.dom.overlay.classList.remove('interactive');
|
||||||
|
if (ctx.platform.shouldToggleMouseIgnore) {
|
||||||
|
window.electronAPI.setIgnoreMouseEvents(true, { forward: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('dragenter', (event: DragEvent) => {
|
||||||
|
if (!event.dataTransfer) return;
|
||||||
|
dragDepth += 1;
|
||||||
|
setDropInteractive();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('dragover', (event: DragEvent) => {
|
||||||
|
if (dragDepth <= 0 || !event.dataTransfer) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.dataTransfer.dropEffect = 'copy';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('dragleave', () => {
|
||||||
|
if (dragDepth <= 0) return;
|
||||||
|
dragDepth -= 1;
|
||||||
|
if (dragDepth === 0) {
|
||||||
|
clearDropInteractive();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('drop', (event: DragEvent) => {
|
||||||
|
if (!event.dataTransfer) return;
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const droppedPaths = collectDroppedVideoPaths(event.dataTransfer);
|
||||||
|
const loadCommands = buildMpvLoadfileCommands(droppedPaths, event.shiftKey);
|
||||||
|
for (const command of loadCommands) {
|
||||||
|
window.electronAPI.sendMpvCommand(command);
|
||||||
|
}
|
||||||
|
if (loadCommands.length > 0) {
|
||||||
|
const action = event.shiftKey ? 'Queued' : 'Loaded';
|
||||||
|
window.electronAPI.sendMpvCommand([
|
||||||
|
'show-text',
|
||||||
|
`${action} ${loadCommands.length} file${loadCommands.length === 1 ? '' : 's'}`,
|
||||||
|
'1500',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearDropInteractive();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -586,6 +586,11 @@ export interface SubsyncResult {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ClipboardAppendResult {
|
||||||
|
ok: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SubtitleData {
|
export interface SubtitleData {
|
||||||
text: string;
|
text: string;
|
||||||
tokens: MergedToken[] | null;
|
tokens: MergedToken[] | null;
|
||||||
@@ -755,6 +760,7 @@ export interface ElectronAPI {
|
|||||||
onRuntimeOptionsChanged: (callback: (options: RuntimeOptionState[]) => void) => void;
|
onRuntimeOptionsChanged: (callback: (options: RuntimeOptionState[]) => void) => void;
|
||||||
onOpenRuntimeOptions: (callback: () => void) => void;
|
onOpenRuntimeOptions: (callback: () => void) => void;
|
||||||
onOpenJimaku: (callback: () => void) => void;
|
onOpenJimaku: (callback: () => void) => void;
|
||||||
|
appendClipboardVideoToQueue: () => Promise<ClipboardAppendResult>;
|
||||||
notifyOverlayModalClosed: (modal: 'runtime-options' | 'subsync' | 'jimaku') => void;
|
notifyOverlayModalClosed: (modal: 'runtime-options' | 'subsync' | 'jimaku') => void;
|
||||||
reportOverlayContentBounds: (measurement: OverlayContentMeasurement) => void;
|
reportOverlayContentBounds: (measurement: OverlayContentMeasurement) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user