mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-06 19:57:26 -08:00
feat(renderer): add optional yomitan popup auto-pause
This commit is contained in:
@@ -4,7 +4,7 @@ title: 'Subtitle hover: auto-pause playback with config toggle'
|
|||||||
status: Done
|
status: Done
|
||||||
assignee: []
|
assignee: []
|
||||||
created_date: '2026-02-28 22:43'
|
created_date: '2026-02-28 22:43'
|
||||||
updated_date: '2026-02-28 22:43'
|
updated_date: '2026-03-04 12:07'
|
||||||
labels: []
|
labels: []
|
||||||
dependencies: []
|
dependencies: []
|
||||||
priority: medium
|
priority: medium
|
||||||
@@ -43,4 +43,11 @@ Scope:
|
|||||||
|
|
||||||
Implemented `subtitleStyle.autoPauseVideoOnHover` with default `true`, wired through config defaults/resolution/types, renderer state/style, and mouse hover handlers. Added playback pause-state IPC (`getPlaybackPaused`) to avoid false resume when media was already paused. Added renderer hover behavior tests (including race/cancel case) and config/resolve tests. Updated config examples and docs (`README`, usage, shortcuts, mining workflow, configuration) to document default hover pause/resume behavior and disable path.
|
Implemented `subtitleStyle.autoPauseVideoOnHover` with default `true`, wired through config defaults/resolution/types, renderer state/style, and mouse hover handlers. Added playback pause-state IPC (`getPlaybackPaused`) to avoid false resume when media was already paused. Added renderer hover behavior tests (including race/cancel case) and config/resolve tests. Updated config examples and docs (`README`, usage, shortcuts, mining workflow, configuration) to document default hover pause/resume behavior and disable path.
|
||||||
|
|
||||||
|
Follow-up adjustments (2026-03-04):
|
||||||
|
|
||||||
|
- Hover pause now resumes immediately when leaving subtitle text (no Yomitan-popup hover retention).
|
||||||
|
- Added `subtitleStyle.autoPauseVideoOnYomitanPopup` (default `false`) to optionally keep playback paused while Yomitan popup is open, with auto-resume on close only when SubMiner initiated the popup pause.
|
||||||
|
- Yomitan popup control keybinds added while popup is open: `J/K` scroll, `M` mine, `P` audio play, `[` previous audio variant, `]` next audio variant (within selected source).
|
||||||
|
- Extension copy drift detection widened so popup runtime changes are reliably re-copied on launch (`popup.js`, `popup-main.js`, `display.js`, `display-audio.js`).
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
|
|||||||
@@ -258,7 +258,8 @@ See `config.example.jsonc` for detailed configuration options.
|
|||||||
| `backgroundColor` | string | Any CSS color, including `"transparent"` (default: `"rgb(30, 32, 48, 0.88)"`) |
|
| `backgroundColor` | string | Any CSS color, including `"transparent"` (default: `"rgb(30, 32, 48, 0.88)"`) |
|
||||||
| `enableJlpt` | boolean | Enable JLPT level underline styling (`false` by default) |
|
| `enableJlpt` | boolean | Enable JLPT level underline styling (`false` by default) |
|
||||||
| `preserveLineBreaks` | boolean | Preserve line breaks in visible overlay subtitle rendering (`false` by default). Enable to mirror mpv line layout. |
|
| `preserveLineBreaks` | boolean | Preserve line breaks in visible overlay subtitle rendering (`false` by default). Enable to mirror mpv line layout. |
|
||||||
| `autoPauseVideoOnHover` | boolean | Pause playback while mouse hovers subtitle text, then resume on leave (`true` by default). |
|
| `autoPauseVideoOnHover` | boolean | Pause playback while mouse hovers subtitle text; resume after leaving subtitle area (`true` by default). |
|
||||||
|
| `autoPauseVideoOnYomitanPopup` | boolean | Pause playback while Yomitan popup is open; resume when popup closes (`false` by default). |
|
||||||
| `hoverTokenColor` | string | Hex color used for hovered subtitle token highlight in mpv (default: catppuccin mauve) |
|
| `hoverTokenColor` | string | Hex color used for hovered subtitle token highlight in mpv (default: catppuccin mauve) |
|
||||||
| `hoverTokenBackgroundColor` | string | CSS color used for hovered subtitle token background highlight (default: semi-transparent dark) |
|
| `hoverTokenBackgroundColor` | string | CSS color used for hovered subtitle token background highlight (default: semi-transparent dark) |
|
||||||
| `frequencyDictionary.enabled` | boolean | Enable frequency highlighting from dictionary lookups (`false` by default) |
|
| `frequencyDictionary.enabled` | boolean | Enable frequency highlighting from dictionary lookups (`false` by default) |
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ The visible overlay renders subtitles as tokenized, clickable word spans. Each w
|
|||||||
|
|
||||||
- Word-level click targets for Yomitan lookup
|
- Word-level click targets for Yomitan lookup
|
||||||
- Auto pause/resume on subtitle hover (enabled by default via `subtitleStyle.autoPauseVideoOnHover`)
|
- Auto pause/resume on subtitle hover (enabled by default via `subtitleStyle.autoPauseVideoOnHover`)
|
||||||
|
- Optional auto-pause while Yomitan popup is open (`subtitleStyle.autoPauseVideoOnYomitanPopup`)
|
||||||
- Right-click to pause/resume
|
- Right-click to pause/resume
|
||||||
- Right-click + drag to reposition subtitles
|
- Right-click + drag to reposition subtitles
|
||||||
- Modal dialogs for Jimaku search, field grouping, subsync, and runtime options
|
- Modal dialogs for Jimaku search, field grouping, subsync, and runtime options
|
||||||
|
|||||||
@@ -107,7 +107,8 @@
|
|||||||
"subtitleStyle": {
|
"subtitleStyle": {
|
||||||
"enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false
|
"enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false
|
||||||
"preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false
|
"preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false
|
||||||
"autoPauseVideoOnHover": true, // Automatically pause mpv playback while hovering subtitle text, then resume on leave. Values: true | false
|
"autoPauseVideoOnHover": true, // Automatically pause mpv playback while hovering subtitle text; resume after leaving subtitle area. Values: true | false
|
||||||
|
"autoPauseVideoOnYomitanPopup": false, // Automatically pause mpv playback while Yomitan popup is open; resume when popup closes. Values: true | false
|
||||||
"hoverTokenColor": "#f4dbd6", // Hex color used for hovered subtitle token highlight in mpv.
|
"hoverTokenColor": "#f4dbd6", // Hex color used for hovered subtitle token highlight in mpv.
|
||||||
"hoverTokenBackgroundColor": "rgba(54, 58, 79, 0.84)", // CSS color used for hovered subtitle token background highlight in mpv.
|
"hoverTokenBackgroundColor": "rgba(54, 58, 79, 0.84)", // CSS color used for hovered subtitle token background highlight in mpv.
|
||||||
"fontFamily": "M PLUS 1 Medium, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
|
"fontFamily": "M PLUS 1 Medium, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
|
||||||
|
|||||||
@@ -58,7 +58,18 @@ These control playback and subtitle display. They require overlay window focus.
|
|||||||
|
|
||||||
These keybindings can be overridden or disabled via the `keybindings` config array.
|
These keybindings can be overridden or disabled via the `keybindings` config array.
|
||||||
|
|
||||||
Mouse-hover playback behavior is configured separately from shortcuts: `subtitleStyle.autoPauseVideoOnHover` defaults to `true` (pause on subtitle hover, resume on leave).
|
Mouse-hover playback behavior is configured separately from shortcuts: `subtitleStyle.autoPauseVideoOnHover` defaults to `true` (pause on subtitle hover; resume after leaving subtitle area). Optional popup behavior: set `subtitleStyle.autoPauseVideoOnYomitanPopup` to `true` to keep playback paused while Yomitan popup is open.
|
||||||
|
|
||||||
|
When a Yomitan popup is open, SubMiner also provides popup control shortcuts:
|
||||||
|
|
||||||
|
| Shortcut | Action |
|
||||||
|
| ------------- | -------------------------------------- |
|
||||||
|
| `J` | Scroll definitions down |
|
||||||
|
| `K` | Scroll definitions up |
|
||||||
|
| `M` | Mine/add selected term |
|
||||||
|
| `P` | Play selected term audio |
|
||||||
|
| `[` | Play previous available audio (selected source) |
|
||||||
|
| `]` | Play next available audio (selected source) |
|
||||||
|
|
||||||
## Subtitle & Feature Shortcuts
|
## Subtitle & Feature Shortcuts
|
||||||
|
|
||||||
|
|||||||
@@ -209,7 +209,11 @@ Notes:
|
|||||||
|
|
||||||
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.
|
||||||
|
|
||||||
By default, hovering over subtitle text pauses mpv playback and leaving the subtitle area resumes playback. Set `subtitleStyle.autoPauseVideoOnHover` to `false` to disable this behavior.
|
By default, hovering over subtitle text pauses mpv playback. Playback resumes as soon as the cursor leaves subtitle text. Set `subtitleStyle.autoPauseVideoOnHover` to `false` to disable this behavior.
|
||||||
|
|
||||||
|
If you want playback to stay paused while a Yomitan popup is open, set `subtitleStyle.autoPauseVideoOnYomitanPopup` to `true`. When enabled, SubMiner auto-resumes on popup close only if SubMiner paused playback for that popup.
|
||||||
|
|
||||||
|
If the Yomitan popup is open, you can control it directly from the overlay: `J/K` scroll definitions, `M` mines/adds the selected term, `P` plays term audio, `[` plays the previous available audio, and `]` plays the next available audio in the selected source.
|
||||||
|
|
||||||
### Drag-and-drop Queueing
|
### Drag-and-drop Queueing
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ test('loads defaults when config is missing', () => {
|
|||||||
assert.equal(config.subtitleStyle.backgroundColor, 'rgb(30, 32, 48, 0.88)');
|
assert.equal(config.subtitleStyle.backgroundColor, 'rgb(30, 32, 48, 0.88)');
|
||||||
assert.equal(config.subtitleStyle.preserveLineBreaks, false);
|
assert.equal(config.subtitleStyle.preserveLineBreaks, false);
|
||||||
assert.equal(config.subtitleStyle.autoPauseVideoOnHover, true);
|
assert.equal(config.subtitleStyle.autoPauseVideoOnHover, true);
|
||||||
|
assert.equal(config.subtitleStyle.autoPauseVideoOnYomitanPopup, false);
|
||||||
assert.equal(config.subtitleStyle.hoverTokenColor, '#f4dbd6');
|
assert.equal(config.subtitleStyle.hoverTokenColor, '#f4dbd6');
|
||||||
assert.equal(config.subtitleStyle.hoverTokenBackgroundColor, 'rgba(54, 58, 79, 0.84)');
|
assert.equal(config.subtitleStyle.hoverTokenBackgroundColor, 'rgba(54, 58, 79, 0.84)');
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@@ -160,6 +161,44 @@ test('parses subtitleStyle.autoPauseVideoOnHover and warns on invalid values', (
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('parses subtitleStyle.autoPauseVideoOnYomitanPopup and warns on invalid values', () => {
|
||||||
|
const validDir = makeTempDir();
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(validDir, 'config.jsonc'),
|
||||||
|
`{
|
||||||
|
"subtitleStyle": {
|
||||||
|
"autoPauseVideoOnYomitanPopup": true
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const validService = new ConfigService(validDir);
|
||||||
|
assert.equal(validService.getConfig().subtitleStyle.autoPauseVideoOnYomitanPopup, true);
|
||||||
|
|
||||||
|
const invalidDir = makeTempDir();
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(invalidDir, 'config.jsonc'),
|
||||||
|
`{
|
||||||
|
"subtitleStyle": {
|
||||||
|
"autoPauseVideoOnYomitanPopup": "yes"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const invalidService = new ConfigService(invalidDir);
|
||||||
|
assert.equal(
|
||||||
|
invalidService.getConfig().subtitleStyle.autoPauseVideoOnYomitanPopup,
|
||||||
|
DEFAULT_CONFIG.subtitleStyle.autoPauseVideoOnYomitanPopup,
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
invalidService
|
||||||
|
.getWarnings()
|
||||||
|
.some((warning) => warning.path === 'subtitleStyle.autoPauseVideoOnYomitanPopup'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('parses subtitleStyle.hoverTokenColor and warns on invalid values', () => {
|
test('parses subtitleStyle.hoverTokenColor and warns on invalid values', () => {
|
||||||
const validDir = makeTempDir();
|
const validDir = makeTempDir();
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle'> = {
|
|||||||
enableJlpt: false,
|
enableJlpt: false,
|
||||||
preserveLineBreaks: false,
|
preserveLineBreaks: false,
|
||||||
autoPauseVideoOnHover: true,
|
autoPauseVideoOnHover: true,
|
||||||
|
autoPauseVideoOnYomitanPopup: false,
|
||||||
hoverTokenColor: '#f4dbd6',
|
hoverTokenColor: '#f4dbd6',
|
||||||
hoverTokenBackgroundColor: 'rgba(54, 58, 79, 0.84)',
|
hoverTokenBackgroundColor: 'rgba(54, 58, 79, 0.84)',
|
||||||
fontFamily: 'M PLUS 1 Medium, Source Han Sans JP, Noto Sans CJK JP',
|
fontFamily: 'M PLUS 1 Medium, Source Han Sans JP, Noto Sans CJK JP',
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ test('config option registry includes critical paths and has unique entries', ()
|
|||||||
'logging.level',
|
'logging.level',
|
||||||
'startupWarmups.lowPowerMode',
|
'startupWarmups.lowPowerMode',
|
||||||
'subtitleStyle.enableJlpt',
|
'subtitleStyle.enableJlpt',
|
||||||
|
'subtitleStyle.autoPauseVideoOnYomitanPopup',
|
||||||
'ankiConnect.enabled',
|
'ankiConnect.enabled',
|
||||||
'immersionTracking.enabled',
|
'immersionTracking.enabled',
|
||||||
]) {
|
]) {
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ export function buildSubtitleConfigOptionRegistry(
|
|||||||
description:
|
description:
|
||||||
'Automatically pause mpv playback while hovering subtitle text, then resume on leave.',
|
'Automatically pause mpv playback while hovering subtitle text, then resume on leave.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'subtitleStyle.autoPauseVideoOnYomitanPopup',
|
||||||
|
kind: 'boolean',
|
||||||
|
defaultValue: defaultConfig.subtitleStyle.autoPauseVideoOnYomitanPopup,
|
||||||
|
description:
|
||||||
|
'Automatically pause mpv playback while Yomitan popup is open, then resume when popup closes.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'subtitleStyle.hoverTokenColor',
|
path: 'subtitleStyle.hoverTokenColor',
|
||||||
kind: 'string',
|
kind: 'string',
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
|||||||
const fallbackSubtitleStyleEnableJlpt = resolved.subtitleStyle.enableJlpt;
|
const fallbackSubtitleStyleEnableJlpt = resolved.subtitleStyle.enableJlpt;
|
||||||
const fallbackSubtitleStylePreserveLineBreaks = resolved.subtitleStyle.preserveLineBreaks;
|
const fallbackSubtitleStylePreserveLineBreaks = resolved.subtitleStyle.preserveLineBreaks;
|
||||||
const fallbackSubtitleStyleAutoPauseVideoOnHover = resolved.subtitleStyle.autoPauseVideoOnHover;
|
const fallbackSubtitleStyleAutoPauseVideoOnHover = resolved.subtitleStyle.autoPauseVideoOnHover;
|
||||||
|
const fallbackSubtitleStyleAutoPauseVideoOnYomitanPopup =
|
||||||
|
resolved.subtitleStyle.autoPauseVideoOnYomitanPopup;
|
||||||
const fallbackSubtitleStyleHoverTokenColor = resolved.subtitleStyle.hoverTokenColor;
|
const fallbackSubtitleStyleHoverTokenColor = resolved.subtitleStyle.hoverTokenColor;
|
||||||
const fallbackSubtitleStyleHoverTokenBackgroundColor =
|
const fallbackSubtitleStyleHoverTokenBackgroundColor =
|
||||||
resolved.subtitleStyle.hoverTokenBackgroundColor;
|
resolved.subtitleStyle.hoverTokenBackgroundColor;
|
||||||
@@ -171,6 +173,26 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const autoPauseVideoOnYomitanPopup = asBoolean(
|
||||||
|
(src.subtitleStyle as { autoPauseVideoOnYomitanPopup?: unknown }).autoPauseVideoOnYomitanPopup,
|
||||||
|
);
|
||||||
|
if (autoPauseVideoOnYomitanPopup !== undefined) {
|
||||||
|
resolved.subtitleStyle.autoPauseVideoOnYomitanPopup = autoPauseVideoOnYomitanPopup;
|
||||||
|
} else if (
|
||||||
|
(src.subtitleStyle as { autoPauseVideoOnYomitanPopup?: unknown })
|
||||||
|
.autoPauseVideoOnYomitanPopup !== undefined
|
||||||
|
) {
|
||||||
|
resolved.subtitleStyle.autoPauseVideoOnYomitanPopup =
|
||||||
|
fallbackSubtitleStyleAutoPauseVideoOnYomitanPopup;
|
||||||
|
warn(
|
||||||
|
'subtitleStyle.autoPauseVideoOnYomitanPopup',
|
||||||
|
(src.subtitleStyle as { autoPauseVideoOnYomitanPopup?: unknown })
|
||||||
|
.autoPauseVideoOnYomitanPopup,
|
||||||
|
resolved.subtitleStyle.autoPauseVideoOnYomitanPopup,
|
||||||
|
'Expected boolean.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const hoverTokenColor = asColor(
|
const hoverTokenColor = asColor(
|
||||||
(src.subtitleStyle as { hoverTokenColor?: unknown }).hoverTokenColor,
|
(src.subtitleStyle as { hoverTokenColor?: unknown }).hoverTokenColor,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -47,6 +47,25 @@ test('subtitleStyle autoPauseVideoOnHover falls back on invalid value', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('subtitleStyle autoPauseVideoOnYomitanPopup falls back on invalid value', () => {
|
||||||
|
const { context, warnings } = createResolveContext({
|
||||||
|
subtitleStyle: {
|
||||||
|
autoPauseVideoOnYomitanPopup: 'invalid' as unknown as boolean,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
applySubtitleDomainConfig(context);
|
||||||
|
|
||||||
|
assert.equal(context.resolved.subtitleStyle.autoPauseVideoOnYomitanPopup, false);
|
||||||
|
assert.ok(
|
||||||
|
warnings.some(
|
||||||
|
(warning) =>
|
||||||
|
warning.path === 'subtitleStyle.autoPauseVideoOnYomitanPopup' &&
|
||||||
|
warning.message === 'Expected boolean.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('subtitleStyle frequencyDictionary.matchMode accepts valid values and warns on invalid', () => {
|
test('subtitleStyle frequencyDictionary.matchMode accepts valid values and warns on invalid', () => {
|
||||||
const valid = createResolveContext({
|
const valid = createResolveContext({
|
||||||
subtitleStyle: {
|
subtitleStyle: {
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import assert from 'node:assert/strict';
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
|
|
||||||
import { createMouseHandlers } from './mouse.js';
|
import { createMouseHandlers } from './mouse.js';
|
||||||
|
import {
|
||||||
|
YOMITAN_POPUP_HIDDEN_EVENT,
|
||||||
|
YOMITAN_POPUP_SHOWN_EVENT,
|
||||||
|
} from '../yomitan-popup.js';
|
||||||
|
|
||||||
function createClassList() {
|
function createClassList() {
|
||||||
const classes = new Set<string>();
|
const classes = new Set<string>();
|
||||||
@@ -28,6 +32,12 @@ function createDeferred<T>() {
|
|||||||
return { promise, resolve };
|
return { promise, resolve };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function waitForNextTick(): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function createMouseTestContext() {
|
function createMouseTestContext() {
|
||||||
const overlayClassList = createClassList();
|
const overlayClassList = createClassList();
|
||||||
const subtitleRootClassList = createClassList();
|
const subtitleRootClassList = createClassList();
|
||||||
@@ -78,6 +88,7 @@ test('auto-pause on subtitle hover pauses on enter and resumes on leave when ena
|
|||||||
getCurrentYPercent: () => 10,
|
getCurrentYPercent: () => 10,
|
||||||
persistSubtitlePositionPatch: () => {},
|
persistSubtitlePositionPatch: () => {},
|
||||||
getSubtitleHoverAutoPauseEnabled: () => true,
|
getSubtitleHoverAutoPauseEnabled: () => true,
|
||||||
|
getYomitanPopupAutoPauseEnabled: () => false,
|
||||||
getPlaybackPaused: async () => false,
|
getPlaybackPaused: async () => false,
|
||||||
sendMpvCommand: (command) => {
|
sendMpvCommand: (command) => {
|
||||||
mpvCommands.push(command);
|
mpvCommands.push(command);
|
||||||
@@ -106,6 +117,7 @@ test('auto-pause on subtitle hover skips when playback is already paused', async
|
|||||||
getCurrentYPercent: () => 10,
|
getCurrentYPercent: () => 10,
|
||||||
persistSubtitlePositionPatch: () => {},
|
persistSubtitlePositionPatch: () => {},
|
||||||
getSubtitleHoverAutoPauseEnabled: () => true,
|
getSubtitleHoverAutoPauseEnabled: () => true,
|
||||||
|
getYomitanPopupAutoPauseEnabled: () => false,
|
||||||
getPlaybackPaused: async () => true,
|
getPlaybackPaused: async () => true,
|
||||||
sendMpvCommand: (command) => {
|
sendMpvCommand: (command) => {
|
||||||
mpvCommands.push(command);
|
mpvCommands.push(command);
|
||||||
@@ -131,6 +143,7 @@ test('auto-pause on subtitle hover is skipped when disabled in config', async ()
|
|||||||
getCurrentYPercent: () => 10,
|
getCurrentYPercent: () => 10,
|
||||||
persistSubtitlePositionPatch: () => {},
|
persistSubtitlePositionPatch: () => {},
|
||||||
getSubtitleHoverAutoPauseEnabled: () => false,
|
getSubtitleHoverAutoPauseEnabled: () => false,
|
||||||
|
getYomitanPopupAutoPauseEnabled: () => false,
|
||||||
getPlaybackPaused: async () => false,
|
getPlaybackPaused: async () => false,
|
||||||
sendMpvCommand: (command) => {
|
sendMpvCommand: (command) => {
|
||||||
mpvCommands.push(command);
|
mpvCommands.push(command);
|
||||||
@@ -157,6 +170,7 @@ test('pending hover pause check is ignored when mouse leaves before pause state
|
|||||||
getCurrentYPercent: () => 10,
|
getCurrentYPercent: () => 10,
|
||||||
persistSubtitlePositionPatch: () => {},
|
persistSubtitlePositionPatch: () => {},
|
||||||
getSubtitleHoverAutoPauseEnabled: () => true,
|
getSubtitleHoverAutoPauseEnabled: () => true,
|
||||||
|
getYomitanPopupAutoPauseEnabled: () => false,
|
||||||
getPlaybackPaused: async () => deferred.promise,
|
getPlaybackPaused: async () => deferred.promise,
|
||||||
sendMpvCommand: (command) => {
|
sendMpvCommand: (command) => {
|
||||||
mpvCommands.push(command);
|
mpvCommands.push(command);
|
||||||
@@ -170,3 +184,273 @@ test('pending hover pause check is ignored when mouse leaves before pause state
|
|||||||
|
|
||||||
assert.deepEqual(mpvCommands, []);
|
assert.deepEqual(mpvCommands, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('hover pause resumes immediately on subtitle leave even when yomitan popup is visible', async () => {
|
||||||
|
const ctx = createMouseTestContext();
|
||||||
|
const mpvCommands: Array<(string | number)[]> = [];
|
||||||
|
const previousWindow = (globalThis as { window?: unknown }).window;
|
||||||
|
const previousDocument = (globalThis as { document?: unknown }).document;
|
||||||
|
const previousMutationObserver = (globalThis as { MutationObserver?: unknown }).MutationObserver;
|
||||||
|
const previousNode = (globalThis as { Node?: unknown }).Node;
|
||||||
|
const windowListeners = new Map<string, Array<() => void>>();
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, 'window', {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
addEventListener: (type: string, listener: () => void) => {
|
||||||
|
const bucket = windowListeners.get(type) ?? [];
|
||||||
|
bucket.push(listener);
|
||||||
|
windowListeners.set(type, bucket);
|
||||||
|
},
|
||||||
|
electronAPI: {
|
||||||
|
setIgnoreMouseEvents: () => {},
|
||||||
|
},
|
||||||
|
focus: () => {},
|
||||||
|
innerHeight: 1000,
|
||||||
|
getSelection: () => null,
|
||||||
|
setTimeout,
|
||||||
|
clearTimeout,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, 'document', {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
querySelector: () => null,
|
||||||
|
querySelectorAll: () => [],
|
||||||
|
body: {},
|
||||||
|
elementFromPoint: () => null,
|
||||||
|
addEventListener: () => {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, 'MutationObserver', {
|
||||||
|
configurable: true,
|
||||||
|
value: class {
|
||||||
|
observe() {}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, 'Node', {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
ELEMENT_NODE: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const handlers = createMouseHandlers(ctx as never, {
|
||||||
|
modalStateReader: {
|
||||||
|
isAnySettingsModalOpen: () => false,
|
||||||
|
isAnyModalOpen: () => false,
|
||||||
|
},
|
||||||
|
applyYPercent: () => {},
|
||||||
|
getCurrentYPercent: () => 10,
|
||||||
|
persistSubtitlePositionPatch: () => {},
|
||||||
|
getSubtitleHoverAutoPauseEnabled: () => true,
|
||||||
|
getYomitanPopupAutoPauseEnabled: () => false,
|
||||||
|
getPlaybackPaused: async () => false,
|
||||||
|
sendMpvCommand: (command) => {
|
||||||
|
mpvCommands.push(command);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
handlers.setupYomitanObserver();
|
||||||
|
for (const listener of windowListeners.get(YOMITAN_POPUP_SHOWN_EVENT) ?? []) {
|
||||||
|
listener();
|
||||||
|
}
|
||||||
|
await handlers.handleMouseEnter();
|
||||||
|
await handlers.handleMouseLeave();
|
||||||
|
|
||||||
|
assert.deepEqual(mpvCommands, [
|
||||||
|
['set_property', 'pause', 'yes'],
|
||||||
|
['set_property', 'pause', 'no'],
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
||||||
|
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
|
||||||
|
Object.defineProperty(globalThis, 'MutationObserver', {
|
||||||
|
configurable: true,
|
||||||
|
value: previousMutationObserver,
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, 'Node', { configurable: true, value: previousNode });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('auto-pause still works when yomitan popup is already visible', async () => {
|
||||||
|
const ctx = createMouseTestContext();
|
||||||
|
const mpvCommands: Array<(string | number)[]> = [];
|
||||||
|
const previousWindow = (globalThis as { window?: unknown }).window;
|
||||||
|
const previousDocument = (globalThis as { document?: unknown }).document;
|
||||||
|
const previousMutationObserver = (globalThis as { MutationObserver?: unknown }).MutationObserver;
|
||||||
|
const previousNode = (globalThis as { Node?: unknown }).Node;
|
||||||
|
const windowListeners = new Map<string, Array<() => void>>();
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, 'window', {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
addEventListener: (type: string, listener: () => void) => {
|
||||||
|
const bucket = windowListeners.get(type) ?? [];
|
||||||
|
bucket.push(listener);
|
||||||
|
windowListeners.set(type, bucket);
|
||||||
|
},
|
||||||
|
electronAPI: {
|
||||||
|
setIgnoreMouseEvents: () => {},
|
||||||
|
},
|
||||||
|
focus: () => {},
|
||||||
|
innerHeight: 1000,
|
||||||
|
getSelection: () => null,
|
||||||
|
setTimeout,
|
||||||
|
clearTimeout,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, 'document', {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
querySelector: () => null,
|
||||||
|
querySelectorAll: () => [],
|
||||||
|
body: {},
|
||||||
|
elementFromPoint: () => null,
|
||||||
|
addEventListener: () => {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, 'MutationObserver', {
|
||||||
|
configurable: true,
|
||||||
|
value: class {
|
||||||
|
observe() {}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, 'Node', {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
ELEMENT_NODE: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const handlers = createMouseHandlers(ctx as never, {
|
||||||
|
modalStateReader: {
|
||||||
|
isAnySettingsModalOpen: () => false,
|
||||||
|
isAnyModalOpen: () => false,
|
||||||
|
},
|
||||||
|
applyYPercent: () => {},
|
||||||
|
getCurrentYPercent: () => 10,
|
||||||
|
persistSubtitlePositionPatch: () => {},
|
||||||
|
getSubtitleHoverAutoPauseEnabled: () => true,
|
||||||
|
getYomitanPopupAutoPauseEnabled: () => false,
|
||||||
|
getPlaybackPaused: async () => false,
|
||||||
|
sendMpvCommand: (command) => {
|
||||||
|
mpvCommands.push(command);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
handlers.setupYomitanObserver();
|
||||||
|
for (const listener of windowListeners.get(YOMITAN_POPUP_SHOWN_EVENT) ?? []) {
|
||||||
|
listener();
|
||||||
|
}
|
||||||
|
await handlers.handleMouseEnter();
|
||||||
|
await handlers.handleMouseLeave();
|
||||||
|
|
||||||
|
assert.deepEqual(mpvCommands, [
|
||||||
|
['set_property', 'pause', 'yes'],
|
||||||
|
['set_property', 'pause', 'no'],
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
||||||
|
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
|
||||||
|
Object.defineProperty(globalThis, 'MutationObserver', {
|
||||||
|
configurable: true,
|
||||||
|
value: previousMutationObserver,
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, 'Node', { configurable: true, value: previousNode });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('popup open pauses and popup close resumes when yomitan popup auto-pause is enabled', async () => {
|
||||||
|
const ctx = createMouseTestContext();
|
||||||
|
const mpvCommands: Array<(string | number)[]> = [];
|
||||||
|
const previousWindow = (globalThis as { window?: unknown }).window;
|
||||||
|
const previousDocument = (globalThis as { document?: unknown }).document;
|
||||||
|
const previousMutationObserver = (globalThis as { MutationObserver?: unknown }).MutationObserver;
|
||||||
|
const previousNode = (globalThis as { Node?: unknown }).Node;
|
||||||
|
const windowListeners = new Map<string, Array<() => void>>();
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, 'window', {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
addEventListener: (type: string, listener: () => void) => {
|
||||||
|
const bucket = windowListeners.get(type) ?? [];
|
||||||
|
bucket.push(listener);
|
||||||
|
windowListeners.set(type, bucket);
|
||||||
|
},
|
||||||
|
electronAPI: {
|
||||||
|
setIgnoreMouseEvents: () => {},
|
||||||
|
},
|
||||||
|
focus: () => {},
|
||||||
|
innerHeight: 1000,
|
||||||
|
getSelection: () => null,
|
||||||
|
setTimeout,
|
||||||
|
clearTimeout,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, 'document', {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
querySelector: () => null,
|
||||||
|
querySelectorAll: () => [],
|
||||||
|
body: {},
|
||||||
|
elementFromPoint: () => null,
|
||||||
|
addEventListener: () => {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, 'MutationObserver', {
|
||||||
|
configurable: true,
|
||||||
|
value: class {
|
||||||
|
observe() {}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, 'Node', {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
ELEMENT_NODE: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const handlers = createMouseHandlers(ctx as never, {
|
||||||
|
modalStateReader: {
|
||||||
|
isAnySettingsModalOpen: () => false,
|
||||||
|
isAnyModalOpen: () => false,
|
||||||
|
},
|
||||||
|
applyYPercent: () => {},
|
||||||
|
getCurrentYPercent: () => 10,
|
||||||
|
persistSubtitlePositionPatch: () => {},
|
||||||
|
getSubtitleHoverAutoPauseEnabled: () => true,
|
||||||
|
getYomitanPopupAutoPauseEnabled: () => true,
|
||||||
|
getPlaybackPaused: async () => false,
|
||||||
|
sendMpvCommand: (command: (string | number)[]) => {
|
||||||
|
mpvCommands.push(command);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
handlers.setupYomitanObserver();
|
||||||
|
|
||||||
|
for (const listener of windowListeners.get(YOMITAN_POPUP_SHOWN_EVENT) ?? []) {
|
||||||
|
listener();
|
||||||
|
}
|
||||||
|
await waitForNextTick();
|
||||||
|
for (const listener of windowListeners.get(YOMITAN_POPUP_HIDDEN_EVENT) ?? []) {
|
||||||
|
listener();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.deepEqual(mpvCommands, [
|
||||||
|
['set_property', 'pause', 'yes'],
|
||||||
|
['set_property', 'pause', 'no'],
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
||||||
|
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
|
||||||
|
Object.defineProperty(globalThis, 'MutationObserver', {
|
||||||
|
configurable: true,
|
||||||
|
value: previousMutationObserver,
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, 'Node', { configurable: true, value: previousNode });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -14,13 +14,68 @@ export function createMouseHandlers(
|
|||||||
getCurrentYPercent: () => number;
|
getCurrentYPercent: () => number;
|
||||||
persistSubtitlePositionPatch: (patch: { yPercent: number }) => void;
|
persistSubtitlePositionPatch: (patch: { yPercent: number }) => void;
|
||||||
getSubtitleHoverAutoPauseEnabled: () => boolean;
|
getSubtitleHoverAutoPauseEnabled: () => boolean;
|
||||||
|
getYomitanPopupAutoPauseEnabled: () => boolean;
|
||||||
getPlaybackPaused: () => Promise<boolean | null>;
|
getPlaybackPaused: () => Promise<boolean | null>;
|
||||||
sendMpvCommand: (command: (string | number)[]) => void;
|
sendMpvCommand: (command: (string | number)[]) => void;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
let yomitanPopupVisible = false;
|
let yomitanPopupVisible = false;
|
||||||
let hoverPauseRequestId = 0;
|
let hoverPauseRequestId = 0;
|
||||||
|
let popupPauseRequestId = 0;
|
||||||
let pausedBySubtitleHover = false;
|
let pausedBySubtitleHover = false;
|
||||||
|
let pausedByYomitanPopup = false;
|
||||||
|
|
||||||
|
function maybeResumeHoverPause(): void {
|
||||||
|
if (!pausedBySubtitleHover) return;
|
||||||
|
if (pausedByYomitanPopup) return;
|
||||||
|
if (ctx.state.isOverSubtitle) return;
|
||||||
|
pausedBySubtitleHover = false;
|
||||||
|
options.sendMpvCommand(['set_property', 'pause', 'no']);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeResumeYomitanPopupPause(): void {
|
||||||
|
if (!pausedByYomitanPopup) return;
|
||||||
|
pausedByYomitanPopup = false;
|
||||||
|
if (ctx.state.isOverSubtitle && options.getSubtitleHoverAutoPauseEnabled()) {
|
||||||
|
pausedBySubtitleHover = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
options.sendMpvCommand(['set_property', 'pause', 'no']);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function maybePauseForYomitanPopup(): Promise<void> {
|
||||||
|
if (!yomitanPopupVisible || !options.getYomitanPopupAutoPauseEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = ++popupPauseRequestId;
|
||||||
|
if (pausedByYomitanPopup) return;
|
||||||
|
|
||||||
|
if (pausedBySubtitleHover) {
|
||||||
|
pausedBySubtitleHover = false;
|
||||||
|
pausedByYomitanPopup = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let paused: boolean | null = null;
|
||||||
|
try {
|
||||||
|
paused = await options.getPlaybackPaused();
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
requestId !== popupPauseRequestId ||
|
||||||
|
!yomitanPopupVisible ||
|
||||||
|
!options.getYomitanPopupAutoPauseEnabled()
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (paused !== false) return;
|
||||||
|
|
||||||
|
options.sendMpvCommand(['set_property', 'pause', 'yes']);
|
||||||
|
pausedByYomitanPopup = true;
|
||||||
|
}
|
||||||
|
|
||||||
function enablePopupInteraction(): void {
|
function enablePopupInteraction(): void {
|
||||||
yomitanPopupVisible = true;
|
yomitanPopupVisible = true;
|
||||||
@@ -40,6 +95,9 @@ export function createMouseHandlers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
yomitanPopupVisible = false;
|
yomitanPopupVisible = false;
|
||||||
|
popupPauseRequestId += 1;
|
||||||
|
maybeResumeYomitanPopupPause();
|
||||||
|
maybeResumeHoverPause();
|
||||||
if (!ctx.state.isOverSubtitle && !options.modalStateReader.isAnyModalOpen()) {
|
if (!ctx.state.isOverSubtitle && !options.modalStateReader.isAnyModalOpen()) {
|
||||||
ctx.dom.overlay.classList.remove('interactive');
|
ctx.dom.overlay.classList.remove('interactive');
|
||||||
if (ctx.platform.shouldToggleMouseIgnore) {
|
if (ctx.platform.shouldToggleMouseIgnore) {
|
||||||
@@ -55,6 +113,10 @@ export function createMouseHandlers(
|
|||||||
window.electronAPI.setIgnoreMouseEvents(false);
|
window.electronAPI.setIgnoreMouseEvents(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (yomitanPopupVisible && options.getYomitanPopupAutoPauseEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!options.getSubtitleHoverAutoPauseEnabled()) {
|
if (!options.getSubtitleHoverAutoPauseEnabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -79,10 +141,7 @@ export function createMouseHandlers(
|
|||||||
async function handleMouseLeave(): Promise<void> {
|
async function handleMouseLeave(): Promise<void> {
|
||||||
ctx.state.isOverSubtitle = false;
|
ctx.state.isOverSubtitle = false;
|
||||||
hoverPauseRequestId += 1;
|
hoverPauseRequestId += 1;
|
||||||
if (pausedBySubtitleHover) {
|
maybeResumeHoverPause();
|
||||||
pausedBySubtitleHover = false;
|
|
||||||
options.sendMpvCommand(['set_property', 'pause', 'no']);
|
|
||||||
}
|
|
||||||
if (yomitanPopupVisible) return;
|
if (yomitanPopupVisible) return;
|
||||||
disablePopupInteractionIfIdle();
|
disablePopupInteractionIfIdle();
|
||||||
}
|
}
|
||||||
@@ -144,9 +203,11 @@ export function createMouseHandlers(
|
|||||||
|
|
||||||
function setupYomitanObserver(): void {
|
function setupYomitanObserver(): void {
|
||||||
yomitanPopupVisible = hasYomitanPopupIframe(document);
|
yomitanPopupVisible = hasYomitanPopupIframe(document);
|
||||||
|
void maybePauseForYomitanPopup();
|
||||||
|
|
||||||
window.addEventListener(YOMITAN_POPUP_SHOWN_EVENT, () => {
|
window.addEventListener(YOMITAN_POPUP_SHOWN_EVENT, () => {
|
||||||
enablePopupInteraction();
|
enablePopupInteraction();
|
||||||
|
void maybePauseForYomitanPopup();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener(YOMITAN_POPUP_HIDDEN_EVENT, () => {
|
window.addEventListener(YOMITAN_POPUP_HIDDEN_EVENT, () => {
|
||||||
@@ -160,6 +221,7 @@ export function createMouseHandlers(
|
|||||||
const element = node as Element;
|
const element = node as Element;
|
||||||
if (isYomitanPopupIframe(element)) {
|
if (isYomitanPopupIframe(element)) {
|
||||||
enablePopupInteraction();
|
enablePopupInteraction();
|
||||||
|
void maybePauseForYomitanPopup();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ const mouseHandlers = createMouseHandlers(ctx, {
|
|||||||
getCurrentYPercent: positioning.getCurrentYPercent,
|
getCurrentYPercent: positioning.getCurrentYPercent,
|
||||||
persistSubtitlePositionPatch: positioning.persistSubtitlePositionPatch,
|
persistSubtitlePositionPatch: positioning.persistSubtitlePositionPatch,
|
||||||
getSubtitleHoverAutoPauseEnabled: () => ctx.state.autoPauseVideoOnSubtitleHover,
|
getSubtitleHoverAutoPauseEnabled: () => ctx.state.autoPauseVideoOnSubtitleHover,
|
||||||
|
getYomitanPopupAutoPauseEnabled: () => ctx.state.autoPauseVideoOnYomitanPopup,
|
||||||
getPlaybackPaused: () => window.electronAPI.getPlaybackPaused(),
|
getPlaybackPaused: () => window.electronAPI.getPlaybackPaused(),
|
||||||
sendMpvCommand: (command) => {
|
sendMpvCommand: (command) => {
|
||||||
window.electronAPI.sendMpvCommand(command);
|
window.electronAPI.sendMpvCommand(command);
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export type RendererState = {
|
|||||||
jlptN5Color: string;
|
jlptN5Color: string;
|
||||||
preserveSubtitleLineBreaks: boolean;
|
preserveSubtitleLineBreaks: boolean;
|
||||||
autoPauseVideoOnSubtitleHover: boolean;
|
autoPauseVideoOnSubtitleHover: boolean;
|
||||||
|
autoPauseVideoOnYomitanPopup: boolean;
|
||||||
frequencyDictionaryEnabled: boolean;
|
frequencyDictionaryEnabled: boolean;
|
||||||
frequencyDictionaryTopX: number;
|
frequencyDictionaryTopX: number;
|
||||||
frequencyDictionaryMode: 'single' | 'banded';
|
frequencyDictionaryMode: 'single' | 'banded';
|
||||||
@@ -128,6 +129,7 @@ export function createRendererState(): RendererState {
|
|||||||
jlptN5Color: '#8aadf4',
|
jlptN5Color: '#8aadf4',
|
||||||
preserveSubtitleLineBreaks: false,
|
preserveSubtitleLineBreaks: false,
|
||||||
autoPauseVideoOnSubtitleHover: false,
|
autoPauseVideoOnSubtitleHover: false,
|
||||||
|
autoPauseVideoOnYomitanPopup: false,
|
||||||
frequencyDictionaryEnabled: false,
|
frequencyDictionaryEnabled: false,
|
||||||
frequencyDictionaryTopX: 1000,
|
frequencyDictionaryTopX: 1000,
|
||||||
frequencyDictionaryMode: 'single',
|
frequencyDictionaryMode: 'single',
|
||||||
|
|||||||
@@ -610,6 +610,7 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
|||||||
ctx.state.jlptN5Color = jlptColors.N5;
|
ctx.state.jlptN5Color = jlptColors.N5;
|
||||||
ctx.state.preserveSubtitleLineBreaks = style.preserveLineBreaks ?? false;
|
ctx.state.preserveSubtitleLineBreaks = style.preserveLineBreaks ?? false;
|
||||||
ctx.state.autoPauseVideoOnSubtitleHover = style.autoPauseVideoOnHover ?? false;
|
ctx.state.autoPauseVideoOnSubtitleHover = style.autoPauseVideoOnHover ?? false;
|
||||||
|
ctx.state.autoPauseVideoOnYomitanPopup = style.autoPauseVideoOnYomitanPopup ?? false;
|
||||||
ctx.dom.subtitleRoot.style.setProperty('--subtitle-jlpt-n1-color', jlptColors.N1);
|
ctx.dom.subtitleRoot.style.setProperty('--subtitle-jlpt-n1-color', jlptColors.N1);
|
||||||
ctx.dom.subtitleRoot.style.setProperty('--subtitle-jlpt-n2-color', jlptColors.N2);
|
ctx.dom.subtitleRoot.style.setProperty('--subtitle-jlpt-n2-color', jlptColors.N2);
|
||||||
ctx.dom.subtitleRoot.style.setProperty('--subtitle-jlpt-n3-color', jlptColors.N3);
|
ctx.dom.subtitleRoot.style.setProperty('--subtitle-jlpt-n3-color', jlptColors.N3);
|
||||||
|
|||||||
@@ -290,6 +290,7 @@ export interface SubtitleStyleConfig {
|
|||||||
enableJlpt?: boolean;
|
enableJlpt?: boolean;
|
||||||
preserveLineBreaks?: boolean;
|
preserveLineBreaks?: boolean;
|
||||||
autoPauseVideoOnHover?: boolean;
|
autoPauseVideoOnHover?: boolean;
|
||||||
|
autoPauseVideoOnYomitanPopup?: boolean;
|
||||||
hoverTokenColor?: string;
|
hoverTokenColor?: string;
|
||||||
hoverTokenBackgroundColor?: string;
|
hoverTokenBackgroundColor?: string;
|
||||||
fontFamily?: string;
|
fontFamily?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user