mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-02 06:22:42 -08:00
feat: bind overlay state to secondary subtitle mpv visibility
This commit is contained in:
@@ -2,6 +2,11 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { createRendererRecoveryController } from './error-recovery.js';
|
||||
import {
|
||||
YOMITAN_POPUP_IFRAME_SELECTOR,
|
||||
hasYomitanPopupIframe,
|
||||
isYomitanPopupIframe,
|
||||
} from './yomitan-popup.js';
|
||||
import { resolvePlatformInfo } from './utils/platform.js';
|
||||
|
||||
test('handleError logs context and recovers overlay state', () => {
|
||||
@@ -26,7 +31,6 @@ test('handleError logs context and recovers overlay state', () => {
|
||||
secondarySubtitlePreview: 'secondary',
|
||||
isOverlayInteractive: true,
|
||||
isOverSubtitle: true,
|
||||
invisiblePositionEditMode: false,
|
||||
overlayLayer: 'visible',
|
||||
}),
|
||||
logError: (payload) => {
|
||||
@@ -72,8 +76,7 @@ test('handleError normalizes non-Error values', () => {
|
||||
secondarySubtitlePreview: '',
|
||||
isOverlayInteractive: false,
|
||||
isOverSubtitle: false,
|
||||
invisiblePositionEditMode: false,
|
||||
overlayLayer: 'invisible',
|
||||
overlayLayer: 'visible',
|
||||
}),
|
||||
logError: (payload) => {
|
||||
payloads.push(payload);
|
||||
@@ -107,7 +110,6 @@ test('nested recovery errors are ignored while current recovery is active', () =
|
||||
secondarySubtitlePreview: '',
|
||||
isOverlayInteractive: true,
|
||||
isOverSubtitle: false,
|
||||
invisiblePositionEditMode: true,
|
||||
overlayLayer: 'visible',
|
||||
}),
|
||||
logError: (payload) => {
|
||||
@@ -130,7 +132,7 @@ test('resolvePlatformInfo prefers query layer over preload layer', () => {
|
||||
configurable: true,
|
||||
value: {
|
||||
electronAPI: {
|
||||
getOverlayLayer: () => 'invisible',
|
||||
getOverlayLayer: () => 'modal',
|
||||
},
|
||||
location: { search: '?layer=visible' },
|
||||
},
|
||||
@@ -146,7 +148,6 @@ test('resolvePlatformInfo prefers query layer over preload layer', () => {
|
||||
try {
|
||||
const info = resolvePlatformInfo();
|
||||
assert.equal(info.overlayLayer, 'visible');
|
||||
assert.equal(info.isInvisibleLayer, false);
|
||||
} finally {
|
||||
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
||||
Object.defineProperty(globalThis, 'navigator', {
|
||||
@@ -156,7 +157,7 @@ test('resolvePlatformInfo prefers query layer over preload layer', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('resolvePlatformInfo supports secondary layer and disables mouse-ignore toggles', () => {
|
||||
test('resolvePlatformInfo ignores legacy secondary layer and falls back to visible', () => {
|
||||
const previousWindow = (globalThis as { window?: unknown }).window;
|
||||
const previousNavigator = (globalThis as { navigator?: unknown }).navigator;
|
||||
|
||||
@@ -179,9 +180,8 @@ test('resolvePlatformInfo supports secondary layer and disables mouse-ignore tog
|
||||
|
||||
try {
|
||||
const info = resolvePlatformInfo();
|
||||
assert.equal(info.overlayLayer, 'secondary');
|
||||
assert.equal(info.isSecondaryLayer, true);
|
||||
assert.equal(info.shouldToggleMouseIgnore, false);
|
||||
assert.equal(info.overlayLayer, 'visible');
|
||||
assert.equal(info.shouldToggleMouseIgnore, true);
|
||||
} finally {
|
||||
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
||||
Object.defineProperty(globalThis, 'navigator', {
|
||||
@@ -225,3 +225,59 @@ test('resolvePlatformInfo supports modal layer and disables mouse-ignore toggles
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('isYomitanPopupIframe matches modern popup class and legacy id prefix', () => {
|
||||
const createElement = (options: {
|
||||
tagName: string;
|
||||
id?: string;
|
||||
classNames?: string[];
|
||||
}): Element =>
|
||||
({
|
||||
tagName: options.tagName,
|
||||
id: options.id ?? '',
|
||||
classList: {
|
||||
contains: (className: string) => (options.classNames ?? []).includes(className),
|
||||
},
|
||||
}) as unknown as Element;
|
||||
|
||||
assert.equal(
|
||||
isYomitanPopupIframe(
|
||||
createElement({
|
||||
tagName: 'IFRAME',
|
||||
classNames: ['yomitan-popup'],
|
||||
}),
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
isYomitanPopupIframe(
|
||||
createElement({
|
||||
tagName: 'IFRAME',
|
||||
id: 'yomitan-popup-123',
|
||||
}),
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
isYomitanPopupIframe(
|
||||
createElement({
|
||||
tagName: 'IFRAME',
|
||||
id: 'something-else',
|
||||
}),
|
||||
),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('hasYomitanPopupIframe queries for modern + legacy selector', () => {
|
||||
let selector = '';
|
||||
const root = {
|
||||
querySelector: (value: string) => {
|
||||
selector = value;
|
||||
return {};
|
||||
},
|
||||
} as unknown as ParentNode;
|
||||
|
||||
assert.equal(hasYomitanPopupIframe(root), true);
|
||||
assert.equal(selector, YOMITAN_POPUP_IFRAME_SELECTOR);
|
||||
});
|
||||
|
||||
@@ -16,8 +16,7 @@ export type RendererRecoverySnapshot = {
|
||||
secondarySubtitlePreview: string;
|
||||
isOverlayInteractive: boolean;
|
||||
isOverSubtitle: boolean;
|
||||
invisiblePositionEditMode: boolean;
|
||||
overlayLayer: 'visible' | 'invisible' | 'secondary' | 'modal';
|
||||
overlayLayer: 'visible' | 'modal';
|
||||
};
|
||||
|
||||
type NormalizedRendererError = {
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
import type {
|
||||
KikuDuplicateCardInfo,
|
||||
MpvSubtitleRenderMetrics,
|
||||
RuntimeOptionState,
|
||||
SecondarySubMode,
|
||||
SubtitleData,
|
||||
@@ -84,10 +83,7 @@ function syncSettingsModalSubtitleSuppression(): void {
|
||||
|
||||
const subtitleRenderer = createSubtitleRenderer(ctx);
|
||||
const measurementReporter = createOverlayContentMeasurementReporter(ctx);
|
||||
const positioning = createPositioningController(ctx, {
|
||||
modalStateReader: { isAnySettingsModalOpen },
|
||||
applySubtitleFontSize: subtitleRenderer.applySubtitleFontSize,
|
||||
});
|
||||
const positioning = createPositioningController(ctx);
|
||||
const runtimeOptionsModal = createRuntimeOptionsModal(ctx, {
|
||||
modalStateReader: { isAnyModalOpen },
|
||||
syncSettingsModalSubtitleSuppression,
|
||||
@@ -115,25 +111,15 @@ const keyboardHandlers = createKeyboardHandlers(ctx, {
|
||||
handleJimakuKeydown: jimakuModal.handleJimakuKeydown,
|
||||
handleSessionHelpKeydown: sessionHelpModal.handleSessionHelpKeydown,
|
||||
openSessionHelpModal: sessionHelpModal.openSessionHelpModal,
|
||||
saveInvisiblePositionEdit: positioning.saveInvisiblePositionEdit,
|
||||
cancelInvisiblePositionEdit: positioning.cancelInvisiblePositionEdit,
|
||||
setInvisiblePositionEditMode: positioning.setInvisiblePositionEditMode,
|
||||
applyInvisibleSubtitleOffsetPosition: positioning.applyInvisibleSubtitleOffsetPosition,
|
||||
updateInvisiblePositionEditHud: positioning.updateInvisiblePositionEditHud,
|
||||
appendClipboardVideoToQueue: () => {
|
||||
void window.electronAPI.appendClipboardVideoToQueue();
|
||||
},
|
||||
});
|
||||
const mouseHandlers = createMouseHandlers(ctx, {
|
||||
modalStateReader: { isAnySettingsModalOpen, isAnyModalOpen },
|
||||
applyInvisibleSubtitleLayoutFromMpvMetrics:
|
||||
positioning.applyInvisibleSubtitleLayoutFromMpvMetrics,
|
||||
applyYPercent: positioning.applyYPercent,
|
||||
getCurrentYPercent: positioning.getCurrentYPercent,
|
||||
persistSubtitlePositionPatch: positioning.persistSubtitlePositionPatch,
|
||||
reportHoveredTokenIndex: (tokenIndex: number | null) => {
|
||||
window.electronAPI.reportHoveredSubtitleToken(tokenIndex);
|
||||
},
|
||||
});
|
||||
|
||||
let lastSubtitlePreview = '';
|
||||
@@ -179,9 +165,6 @@ function dismissActiveUiAfterError(): void {
|
||||
|
||||
function restoreOverlayInteractionAfterError(): void {
|
||||
ctx.state.isOverSubtitle = false;
|
||||
if (ctx.state.invisiblePositionEditMode) {
|
||||
positioning.setInvisiblePositionEditMode(false);
|
||||
}
|
||||
ctx.dom.overlay.classList.remove('interactive');
|
||||
if (ctx.platform.shouldToggleMouseIgnore) {
|
||||
window.electronAPI.setIgnoreMouseEvents(true, { forward: true });
|
||||
@@ -212,7 +195,6 @@ const recovery = createRendererRecoveryController({
|
||||
secondarySubtitlePreview: lastSecondarySubtitlePreview,
|
||||
isOverlayInteractive: ctx.dom.overlay.classList.contains('interactive'),
|
||||
isOverSubtitle: ctx.state.isOverSubtitle,
|
||||
invisiblePositionEditMode: ctx.state.invisiblePositionEditMode,
|
||||
overlayLayer: ctx.platform.overlayLayer,
|
||||
}),
|
||||
logError: (payload) => {
|
||||
@@ -222,6 +204,41 @@ const recovery = createRendererRecoveryController({
|
||||
|
||||
registerRendererGlobalErrorHandlers(window, recovery);
|
||||
|
||||
function registerModalOpenHandlers(): void {
|
||||
window.electronAPI.onOpenRuntimeOptions(() => {
|
||||
runGuardedAsync('runtime-options:open', async () => {
|
||||
try {
|
||||
await runtimeOptionsModal.openRuntimeOptionsModal();
|
||||
window.electronAPI.notifyOverlayModalOpened('runtime-options');
|
||||
} catch {
|
||||
runtimeOptionsModal.setRuntimeOptionsStatus('Failed to load runtime options', true);
|
||||
window.electronAPI.notifyOverlayModalClosed('runtime-options');
|
||||
syncSettingsModalSubtitleSuppression();
|
||||
}
|
||||
});
|
||||
});
|
||||
window.electronAPI.onOpenJimaku(() => {
|
||||
runGuarded('jimaku:open', () => {
|
||||
jimakuModal.openJimakuModal();
|
||||
window.electronAPI.notifyOverlayModalOpened('jimaku');
|
||||
});
|
||||
});
|
||||
window.electronAPI.onSubsyncManualOpen((payload: SubsyncManualPayload) => {
|
||||
runGuarded('subsync:manual-open', () => {
|
||||
subsyncModal.openSubsyncModal(payload);
|
||||
window.electronAPI.notifyOverlayModalOpened('subsync');
|
||||
});
|
||||
});
|
||||
window.electronAPI.onKikuFieldGroupingRequest(
|
||||
(data: { original: KikuDuplicateCardInfo; duplicate: KikuDuplicateCardInfo }) => {
|
||||
runGuarded('kiku:field-grouping-open', () => {
|
||||
kikuModal.openKikuFieldGroupingModal(data);
|
||||
window.electronAPI.notifyOverlayModalOpened('kiku');
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function runGuarded(action: string, fn: () => void): void {
|
||||
try {
|
||||
fn();
|
||||
@@ -238,6 +255,8 @@ function runGuardedAsync(action: string, fn: () => Promise<void> | void): void {
|
||||
});
|
||||
}
|
||||
|
||||
registerModalOpenHandlers();
|
||||
|
||||
async function init(): Promise<void> {
|
||||
document.body.classList.add(`layer-${ctx.platform.overlayLayer}`);
|
||||
if (ctx.platform.isMacOSPlatform) {
|
||||
@@ -252,41 +271,17 @@ async function init(): Promise<void> {
|
||||
lastSubtitlePreview = truncateForErrorLog(data.text);
|
||||
}
|
||||
subtitleRenderer.renderSubtitle(data);
|
||||
if (ctx.platform.isInvisibleLayer && ctx.state.mpvSubtitleRenderMetrics) {
|
||||
positioning.applyInvisibleSubtitleLayoutFromMpvMetrics(
|
||||
ctx.state.mpvSubtitleRenderMetrics,
|
||||
'subtitle-change',
|
||||
);
|
||||
}
|
||||
measurementReporter.schedule();
|
||||
});
|
||||
});
|
||||
|
||||
window.electronAPI.onSubtitlePosition((position: SubtitlePosition | null) => {
|
||||
runGuarded('subtitle-position:update', () => {
|
||||
if (ctx.platform.isInvisibleLayer) {
|
||||
positioning.applyInvisibleStoredSubtitlePosition(position, 'media-change');
|
||||
} else {
|
||||
positioning.applyStoredSubtitlePosition(position, 'media-change');
|
||||
}
|
||||
positioning.applyStoredSubtitlePosition(position, 'media-change');
|
||||
measurementReporter.schedule();
|
||||
});
|
||||
});
|
||||
|
||||
if (ctx.platform.isInvisibleLayer) {
|
||||
window.electronAPI.onMpvSubtitleRenderMetrics((metrics: MpvSubtitleRenderMetrics) => {
|
||||
runGuarded('mpv-metrics:update', () => {
|
||||
positioning.applyInvisibleSubtitleLayoutFromMpvMetrics(metrics, 'event');
|
||||
measurementReporter.schedule();
|
||||
});
|
||||
});
|
||||
window.electronAPI.onOverlayDebugVisualization((enabled: boolean) => {
|
||||
runGuarded('overlay-debug-visualization:update', () => {
|
||||
document.body.classList.toggle('debug-invisible-visualization', enabled);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const initialSubtitle = await window.electronAPI.getCurrentSubtitleRaw();
|
||||
lastSubtitlePreview = truncateForErrorLog(initialSubtitle);
|
||||
subtitleRenderer.renderSubtitle(initialSubtitle);
|
||||
@@ -310,17 +305,11 @@ async function init(): Promise<void> {
|
||||
subtitleRenderer.renderSecondarySub(await window.electronAPI.getCurrentSecondarySub());
|
||||
measurementReporter.schedule();
|
||||
|
||||
const hoverTarget = ctx.platform.isInvisibleLayer
|
||||
? ctx.dom.subtitleRoot
|
||||
: ctx.dom.subtitleContainer;
|
||||
hoverTarget.addEventListener('mouseenter', mouseHandlers.handleMouseEnter);
|
||||
hoverTarget.addEventListener('mouseleave', mouseHandlers.handleMouseLeave);
|
||||
ctx.dom.subtitleContainer.addEventListener('mouseenter', mouseHandlers.handleMouseEnter);
|
||||
ctx.dom.subtitleContainer.addEventListener('mouseleave', mouseHandlers.handleMouseLeave);
|
||||
ctx.dom.secondarySubContainer.addEventListener('mouseenter', mouseHandlers.handleMouseEnter);
|
||||
ctx.dom.secondarySubContainer.addEventListener('mouseleave', mouseHandlers.handleMouseLeave);
|
||||
|
||||
mouseHandlers.setupInvisibleHoverSelection();
|
||||
mouseHandlers.setupInvisibleTokenHoverReporter();
|
||||
positioning.setupInvisiblePositionEditHud();
|
||||
mouseHandlers.setupResizeHandler();
|
||||
mouseHandlers.setupSelectionObserver();
|
||||
mouseHandlers.setupYomitanObserver();
|
||||
@@ -348,59 +337,14 @@ async function init(): Promise<void> {
|
||||
measurementReporter.schedule();
|
||||
});
|
||||
});
|
||||
window.electronAPI.onOpenRuntimeOptions(() => {
|
||||
runGuardedAsync('runtime-options:open', async () => {
|
||||
try {
|
||||
await runtimeOptionsModal.openRuntimeOptionsModal();
|
||||
} catch {
|
||||
runtimeOptionsModal.setRuntimeOptionsStatus('Failed to load runtime options', true);
|
||||
window.electronAPI.notifyOverlayModalClosed('runtime-options');
|
||||
syncSettingsModalSubtitleSuppression();
|
||||
}
|
||||
});
|
||||
});
|
||||
window.electronAPI.onOpenJimaku(() => {
|
||||
runGuarded('jimaku:open', () => {
|
||||
jimakuModal.openJimakuModal();
|
||||
});
|
||||
});
|
||||
window.electronAPI.onSubsyncManualOpen((payload: SubsyncManualPayload) => {
|
||||
runGuarded('subsync:manual-open', () => {
|
||||
subsyncModal.openSubsyncModal(payload);
|
||||
});
|
||||
});
|
||||
window.electronAPI.onKikuFieldGroupingRequest(
|
||||
(data: { original: KikuDuplicateCardInfo; duplicate: KikuDuplicateCardInfo }) => {
|
||||
runGuarded('kiku:field-grouping-open', () => {
|
||||
kikuModal.openKikuFieldGroupingModal(data);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if (!ctx.platform.isInvisibleLayer) {
|
||||
mouseHandlers.setupDragging();
|
||||
}
|
||||
mouseHandlers.setupDragging();
|
||||
|
||||
await keyboardHandlers.setupMpvInputForwarding();
|
||||
|
||||
subtitleRenderer.applySubtitleStyle(await window.electronAPI.getSubtitleStyle());
|
||||
|
||||
if (ctx.platform.isInvisibleLayer) {
|
||||
positioning.applyInvisibleStoredSubtitlePosition(
|
||||
await window.electronAPI.getSubtitlePosition(),
|
||||
'startup',
|
||||
);
|
||||
positioning.applyInvisibleSubtitleLayoutFromMpvMetrics(
|
||||
await window.electronAPI.getMpvSubtitleRenderMetrics(),
|
||||
'startup',
|
||||
);
|
||||
} else {
|
||||
positioning.applyStoredSubtitlePosition(
|
||||
await window.electronAPI.getSubtitlePosition(),
|
||||
'startup',
|
||||
);
|
||||
measurementReporter.schedule();
|
||||
}
|
||||
positioning.applyStoredSubtitlePosition(await window.electronAPI.getSubtitlePosition(), 'startup');
|
||||
measurementReporter.schedule();
|
||||
|
||||
if (ctx.platform.shouldToggleMouseIgnore) {
|
||||
window.electronAPI.setIgnoreMouseEvents(true, { forward: true });
|
||||
@@ -421,7 +365,7 @@ function setupDragDropToMpvQueue(): void {
|
||||
|
||||
const clearDropInteractive = (): void => {
|
||||
dragDepth = 0;
|
||||
if (isAnyModalOpen() || ctx.state.isOverSubtitle || ctx.state.invisiblePositionEditMode) {
|
||||
if (isAnyModalOpen() || ctx.state.isOverSubtitle) {
|
||||
return;
|
||||
}
|
||||
ctx.dom.overlay.classList.remove('interactive');
|
||||
|
||||
@@ -280,6 +280,8 @@ body {
|
||||
text-align: center;
|
||||
font-size: 35px;
|
||||
line-height: var(--visible-sub-line-height, 1.32);
|
||||
overflow-wrap: anywhere;
|
||||
word-break: keep-all;
|
||||
color: #cad3f5;
|
||||
--subtitle-known-word-color: #a6da95;
|
||||
--subtitle-n-plus-one-color: #c6a0f6;
|
||||
@@ -288,6 +290,8 @@ body {
|
||||
--subtitle-jlpt-n3-color: #f9e2af;
|
||||
--subtitle-jlpt-n4-color: #a6e3a1;
|
||||
--subtitle-jlpt-n5-color: #8aadf4;
|
||||
--subtitle-hover-token-color: #f4dbd6;
|
||||
--subtitle-hover-token-background-color: rgba(54, 58, 79, 0.84);
|
||||
--subtitle-frequency-single-color: #f5a97f;
|
||||
--subtitle-frequency-band-1-color: #ed8796;
|
||||
--subtitle-frequency-band-2-color: #f5a97f;
|
||||
@@ -300,6 +304,7 @@ body {
|
||||
/* Enable text selection for Yomitan */
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
-webkit-text-fill-color: currentColor;
|
||||
}
|
||||
|
||||
#subtitleRoot:empty {
|
||||
@@ -318,16 +323,21 @@ body.settings-modal-open #subtitleContainer {
|
||||
#subtitleRoot .c {
|
||||
display: inline;
|
||||
position: relative;
|
||||
color: inherit;
|
||||
-webkit-text-fill-color: currentColor !important;
|
||||
}
|
||||
|
||||
#subtitleRoot .c:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
background: var(--subtitle-hover-token-background-color, rgba(54, 58, 79, 0.84));
|
||||
color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||
-webkit-text-fill-color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#subtitleRoot .word {
|
||||
display: inline;
|
||||
position: relative;
|
||||
-webkit-text-fill-color: currentColor !important;
|
||||
}
|
||||
|
||||
#subtitleRoot .word.word-known {
|
||||
@@ -418,9 +428,103 @@ body.settings-modal-open #subtitleContainer {
|
||||
color: var(--subtitle-frequency-band-5-color, #8aadf4);
|
||||
}
|
||||
|
||||
#subtitleRoot .word:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
#subtitleRoot .word:not(.word-known):not(.word-n-plus-one):not(.word-frequency-single):not(
|
||||
.word-frequency-band-1
|
||||
):not(.word-frequency-band-2):not(.word-frequency-band-3):not(.word-frequency-band-4):not(
|
||||
.word-frequency-band-5
|
||||
):hover {
|
||||
background: var(--subtitle-hover-token-background-color, rgba(54, 58, 79, 0.84));
|
||||
border-radius: 3px;
|
||||
color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||
-webkit-text-fill-color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||
}
|
||||
|
||||
#subtitleRoot .word.word-known:hover,
|
||||
#subtitleRoot .word.word-n-plus-one:hover,
|
||||
#subtitleRoot .word.word-frequency-single:hover,
|
||||
#subtitleRoot .word.word-frequency-band-1:hover,
|
||||
#subtitleRoot .word.word-frequency-band-2:hover,
|
||||
#subtitleRoot .word.word-frequency-band-3:hover,
|
||||
#subtitleRoot .word.word-frequency-band-4:hover,
|
||||
#subtitleRoot .word.word-frequency-band-5:hover {
|
||||
background: var(--subtitle-hover-token-background-color, rgba(54, 58, 79, 0.84));
|
||||
border-radius: 3px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
#subtitleRoot .word.word-known .c:hover,
|
||||
#subtitleRoot .word.word-n-plus-one .c:hover,
|
||||
#subtitleRoot .word.word-frequency-single .c:hover,
|
||||
#subtitleRoot .word.word-frequency-band-1 .c:hover,
|
||||
#subtitleRoot .word.word-frequency-band-2 .c:hover,
|
||||
#subtitleRoot .word.word-frequency-band-3 .c:hover,
|
||||
#subtitleRoot .word.word-frequency-band-4 .c:hover,
|
||||
#subtitleRoot .word.word-frequency-band-5 .c:hover {
|
||||
background: transparent;
|
||||
color: inherit !important;
|
||||
-webkit-text-fill-color: currentColor !important;
|
||||
}
|
||||
|
||||
#subtitleRoot::selection,
|
||||
#subtitleRoot .word::selection,
|
||||
#subtitleRoot .c::selection {
|
||||
background: var(--subtitle-hover-token-background-color, rgba(54, 58, 79, 0.84));
|
||||
color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||
-webkit-text-fill-color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||
}
|
||||
|
||||
#subtitleRoot *::selection {
|
||||
background: var(--subtitle-hover-token-background-color, rgba(54, 58, 79, 0.84)) !important;
|
||||
color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||
-webkit-text-fill-color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||
}
|
||||
|
||||
#subtitleRoot .word.word-known::selection,
|
||||
#subtitleRoot .word.word-known .c::selection {
|
||||
color: var(--subtitle-known-word-color, #a6da95) !important;
|
||||
-webkit-text-fill-color: var(--subtitle-known-word-color, #a6da95) !important;
|
||||
}
|
||||
|
||||
#subtitleRoot .word.word-n-plus-one::selection,
|
||||
#subtitleRoot .word.word-n-plus-one .c::selection {
|
||||
color: var(--subtitle-n-plus-one-color, #c6a0f6) !important;
|
||||
-webkit-text-fill-color: var(--subtitle-n-plus-one-color, #c6a0f6) !important;
|
||||
}
|
||||
|
||||
#subtitleRoot .word.word-frequency-single::selection,
|
||||
#subtitleRoot .word.word-frequency-single .c::selection {
|
||||
color: var(--subtitle-frequency-single-color, #f5a97f) !important;
|
||||
-webkit-text-fill-color: var(--subtitle-frequency-single-color, #f5a97f) !important;
|
||||
}
|
||||
|
||||
#subtitleRoot .word.word-frequency-band-1::selection,
|
||||
#subtitleRoot .word.word-frequency-band-1 .c::selection {
|
||||
color: var(--subtitle-frequency-band-1-color, #ed8796) !important;
|
||||
-webkit-text-fill-color: var(--subtitle-frequency-band-1-color, #ed8796) !important;
|
||||
}
|
||||
|
||||
#subtitleRoot .word.word-frequency-band-2::selection,
|
||||
#subtitleRoot .word.word-frequency-band-2 .c::selection {
|
||||
color: var(--subtitle-frequency-band-2-color, #f5a97f) !important;
|
||||
-webkit-text-fill-color: var(--subtitle-frequency-band-2-color, #f5a97f) !important;
|
||||
}
|
||||
|
||||
#subtitleRoot .word.word-frequency-band-3::selection,
|
||||
#subtitleRoot .word.word-frequency-band-3 .c::selection {
|
||||
color: var(--subtitle-frequency-band-3-color, #f9e2af) !important;
|
||||
-webkit-text-fill-color: var(--subtitle-frequency-band-3-color, #f9e2af) !important;
|
||||
}
|
||||
|
||||
#subtitleRoot .word.word-frequency-band-4::selection,
|
||||
#subtitleRoot .word.word-frequency-band-4 .c::selection {
|
||||
color: var(--subtitle-frequency-band-4-color, #a6e3a1) !important;
|
||||
-webkit-text-fill-color: var(--subtitle-frequency-band-4-color, #a6e3a1) !important;
|
||||
}
|
||||
|
||||
#subtitleRoot .word.word-frequency-band-5::selection,
|
||||
#subtitleRoot .word.word-frequency-band-5 .c::selection {
|
||||
color: var(--subtitle-frequency-band-5-color, #8aadf4) !important;
|
||||
-webkit-text-fill-color: var(--subtitle-frequency-band-5-color, #8aadf4) !important;
|
||||
}
|
||||
|
||||
#subtitleRoot br {
|
||||
@@ -439,93 +543,6 @@ body.platform-macos.layer-visible #subtitleRoot {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
body.layer-invisible #subtitleContainer {
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
body.layer-invisible #subtitleRoot,
|
||||
body.layer-invisible #subtitleRoot .word,
|
||||
body.layer-invisible #subtitleRoot .c {
|
||||
color: transparent !important;
|
||||
text-shadow: none !important;
|
||||
-webkit-text-stroke: 0 !important;
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
background: transparent !important;
|
||||
caret-color: transparent !important;
|
||||
line-height: var(--invisible-sub-line-height, normal) !important;
|
||||
font-kerning: auto;
|
||||
letter-spacing: normal;
|
||||
font-variant-ligatures: normal;
|
||||
font-feature-settings: normal;
|
||||
text-rendering: auto;
|
||||
}
|
||||
|
||||
body.layer-invisible #subtitleRoot br {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
body.layer-invisible #subtitleRoot .word:hover,
|
||||
body.layer-invisible #subtitleRoot .c:hover,
|
||||
body.layer-invisible #subtitleRoot.has-selection .word:hover,
|
||||
body.layer-invisible #subtitleRoot.has-selection .c:hover {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
body.layer-invisible #subtitleRoot::selection,
|
||||
body.layer-invisible #subtitleRoot .word::selection,
|
||||
body.layer-invisible #subtitleRoot .c::selection {
|
||||
background: transparent !important;
|
||||
color: transparent !important;
|
||||
}
|
||||
|
||||
body.layer-invisible.debug-invisible-visualization #subtitleRoot,
|
||||
body.layer-invisible.debug-invisible-visualization #subtitleRoot .word,
|
||||
body.layer-invisible.debug-invisible-visualization #subtitleRoot .c {
|
||||
color: #ed8796 !important;
|
||||
-webkit-text-fill-color: #ed8796 !important;
|
||||
-webkit-text-stroke: var(--sub-border-size, 2px) rgba(0, 0, 0, 0.85) !important;
|
||||
paint-order: stroke fill !important;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
.invisible-position-edit-hud {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 30;
|
||||
max-width: min(90vw, 1100px);
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
background: rgba(22, 24, 36, 0.88);
|
||||
border: 1px solid rgba(130, 150, 255, 0.55);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 120ms ease;
|
||||
}
|
||||
|
||||
body.layer-invisible.invisible-position-edit .invisible-position-edit-hud {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
body.layer-invisible.invisible-position-edit #subtitleRoot,
|
||||
body.layer-invisible.invisible-position-edit #subtitleRoot .word,
|
||||
body.layer-invisible.invisible-position-edit #subtitleRoot .c {
|
||||
color: #ed8796 !important;
|
||||
-webkit-text-fill-color: #ed8796 !important;
|
||||
-webkit-text-stroke: var(--sub-border-size, 2px) rgba(0, 0, 0, 0.85) !important;
|
||||
paint-order: stroke fill !important;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
#secondarySubContainer {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
@@ -538,40 +555,6 @@ body.layer-invisible.invisible-position-edit #subtitleRoot .c {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
body.layer-visible #secondarySubContainer,
|
||||
body.layer-invisible #secondarySubContainer {
|
||||
display: none !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
body.layer-secondary #subtitleContainer,
|
||||
body.layer-secondary .modal,
|
||||
body.layer-secondary .overlay-error-toast {
|
||||
display: none !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
body.layer-secondary #overlay {
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
body.layer-secondary #secondarySubContainer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
transform: none;
|
||||
max-width: 100%;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
body.layer-modal #subtitleContainer,
|
||||
body.layer-modal #secondarySubContainer {
|
||||
display: none !important;
|
||||
@@ -597,10 +580,6 @@ body.layer-modal #overlay {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
body.layer-secondary #secondarySubRoot {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#secondarySubRoot:empty {
|
||||
display: none;
|
||||
}
|
||||
@@ -644,11 +623,7 @@ body.settings-modal-open #secondarySubContainer {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
body.layer-secondary #secondarySubContainer.secondary-sub-hover {
|
||||
padding: 8px 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
iframe.yomitan-popup,
|
||||
iframe[id^='yomitan-popup'] {
|
||||
pointer-events: auto !important;
|
||||
z-index: 2147483647 !important;
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
buildInvisibleTokenHoverRanges,
|
||||
computeWordClass,
|
||||
normalizeSubtitle,
|
||||
sanitizeSubtitleHoverTokenColor,
|
||||
shouldRenderTokenizedSubtitle,
|
||||
} from './subtitle-render.js';
|
||||
import { resolveInvisibleLineHeight } from './positioning/invisible-layout-helpers.js';
|
||||
|
||||
function createToken(overrides: Partial<MergedToken>): MergedToken {
|
||||
return {
|
||||
@@ -210,6 +210,17 @@ test('computeWordClass skips frequency class when rank is out of topX', () => {
|
||||
assert.equal(actual, 'word');
|
||||
});
|
||||
|
||||
test('sanitizeSubtitleHoverTokenColor falls back for pure black values', () => {
|
||||
assert.equal(sanitizeSubtitleHoverTokenColor('#000000'), '#f4dbd6');
|
||||
assert.equal(sanitizeSubtitleHoverTokenColor('000000'), '#f4dbd6');
|
||||
assert.equal(sanitizeSubtitleHoverTokenColor('#0000'), '#f4dbd6');
|
||||
});
|
||||
|
||||
test('sanitizeSubtitleHoverTokenColor keeps non-black color values', () => {
|
||||
assert.equal(sanitizeSubtitleHoverTokenColor('#ff00ff'), '#ff00ff');
|
||||
assert.equal(sanitizeSubtitleHoverTokenColor(undefined), '#f4dbd6');
|
||||
});
|
||||
|
||||
test('alignTokensToSourceText preserves newline separators between adjacent token surfaces', () => {
|
||||
const tokens = [
|
||||
createToken({ surface: 'キリキリと', reading: 'きりきりと', headword: 'キリキリと' }),
|
||||
@@ -285,20 +296,16 @@ test('normalizeSubtitle collapses explicit line breaks when collapseLineBreaks i
|
||||
);
|
||||
});
|
||||
|
||||
test('shouldRenderTokenizedSubtitle disables token rendering on invisible layer', () => {
|
||||
assert.equal(shouldRenderTokenizedSubtitle(true, 5), false);
|
||||
});
|
||||
|
||||
test('shouldRenderTokenizedSubtitle enables token rendering on visible layer when tokens exist', () => {
|
||||
assert.equal(shouldRenderTokenizedSubtitle(false, 5), true);
|
||||
assert.equal(shouldRenderTokenizedSubtitle(false, 0), false);
|
||||
test('shouldRenderTokenizedSubtitle enables token rendering when tokens exist', () => {
|
||||
assert.equal(shouldRenderTokenizedSubtitle(5), true);
|
||||
assert.equal(shouldRenderTokenizedSubtitle(0), false);
|
||||
});
|
||||
|
||||
test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => {
|
||||
const distCssPath = path.join(process.cwd(), 'dist', 'renderer', 'style.css');
|
||||
const srcCssPath = path.join(process.cwd(), 'src', 'renderer', 'style.css');
|
||||
|
||||
const cssPath = fs.existsSync(distCssPath) ? distCssPath : srcCssPath;
|
||||
const cssPath = fs.existsSync(srcCssPath) ? srcCssPath : distCssPath;
|
||||
if (!fs.existsSync(cssPath)) {
|
||||
assert.fail(
|
||||
'JLPT CSS file missing. Run `bun run build` first, or ensure src/renderer/style.css exists.',
|
||||
@@ -330,31 +337,86 @@ test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => {
|
||||
assert.match(block, /color:\s*var\(/);
|
||||
}
|
||||
|
||||
const invisibleBlock = extractClassBlock(
|
||||
cssText,
|
||||
'body.layer-invisible #subtitleRoot',
|
||||
);
|
||||
assert.match(
|
||||
invisibleBlock,
|
||||
/line-height:\s*var\(--invisible-sub-line-height,\s*normal\)\s*!important;/,
|
||||
);
|
||||
|
||||
const visibleMacBlock = extractClassBlock(
|
||||
cssText,
|
||||
'body.platform-macos.layer-visible #subtitleRoot',
|
||||
);
|
||||
assert.match(visibleMacBlock, /--visible-sub-line-height:\s*1\.64;/);
|
||||
assert.match(visibleMacBlock, /--visible-sub-line-gap:\s*0\.54em;/);
|
||||
});
|
||||
|
||||
test('invisible overlay uses looser line height on macOS for multi-line subtitles', () => {
|
||||
assert.equal(resolveInvisibleLineHeight(1, true), '1.08');
|
||||
assert.equal(resolveInvisibleLineHeight(2, true), '1.5');
|
||||
assert.equal(resolveInvisibleLineHeight(3, true), '1.62');
|
||||
});
|
||||
const subtitleRootBlock = extractClassBlock(cssText, '#subtitleRoot');
|
||||
assert.match(
|
||||
subtitleRootBlock,
|
||||
/--subtitle-hover-token-color:\s*#f4dbd6;/,
|
||||
);
|
||||
assert.match(
|
||||
subtitleRootBlock,
|
||||
/--subtitle-hover-token-background-color:\s*rgba\(54,\s*58,\s*79,\s*0\.84\);/,
|
||||
);
|
||||
assert.match(subtitleRootBlock, /-webkit-text-fill-color:\s*currentColor;/);
|
||||
|
||||
test('invisible overlay keeps default line height on non-macOS platforms', () => {
|
||||
assert.equal(resolveInvisibleLineHeight(1, false), 'normal');
|
||||
assert.equal(resolveInvisibleLineHeight(2, false), 'normal');
|
||||
assert.equal(resolveInvisibleLineHeight(4, false), 'normal');
|
||||
const charBlock = extractClassBlock(cssText, '#subtitleRoot .c');
|
||||
assert.match(charBlock, /-webkit-text-fill-color:\s*currentColor\s*!important;/);
|
||||
|
||||
const wordBlock = extractClassBlock(cssText, '#subtitleRoot .word');
|
||||
assert.match(wordBlock, /-webkit-text-fill-color:\s*currentColor\s*!important;/);
|
||||
|
||||
assert.match(
|
||||
cssText,
|
||||
/#subtitleRoot \.word:not\(\.word-known\):not\(\.word-n-plus-one\):not\(\.word-frequency-single\):not\(\s*\.word-frequency-band-1\s*\):not\(\.word-frequency-band-2\):not\(\.word-frequency-band-3\):not\(\.word-frequency-band-4\):not\(\s*\.word-frequency-band-5\s*\):hover\s*\{[\s\S]*?background:\s*var\(--subtitle-hover-token-background-color,\s*rgba\(54,\s*58,\s*79,\s*0\.84\)\);[\s\S]*?color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;[\s\S]*?-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
||||
);
|
||||
|
||||
const coloredWordHoverBlock = extractClassBlock(cssText, '#subtitleRoot .word.word-known:hover');
|
||||
assert.match(
|
||||
coloredWordHoverBlock,
|
||||
/background:\s*var\(--subtitle-hover-token-background-color,\s*rgba\(54,\s*58,\s*79,\s*0\.84\)\);/,
|
||||
);
|
||||
assert.match(coloredWordHoverBlock, /border-radius:\s*3px;/);
|
||||
assert.match(coloredWordHoverBlock, /font-weight:\s*800;/);
|
||||
assert.doesNotMatch(coloredWordHoverBlock, /color:\s*var\(--subtitle-hover-token-color/);
|
||||
assert.doesNotMatch(coloredWordHoverBlock, /-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color/);
|
||||
|
||||
const coloredWordSelectionBlock = extractClassBlock(cssText, '#subtitleRoot .word.word-known::selection');
|
||||
assert.match(
|
||||
coloredWordSelectionBlock,
|
||||
/color:\s*var\(--subtitle-known-word-color,\s*#a6da95\)\s*!important;/,
|
||||
);
|
||||
assert.match(
|
||||
coloredWordSelectionBlock,
|
||||
/-webkit-text-fill-color:\s*var\(--subtitle-known-word-color,\s*#a6da95\)\s*!important;/,
|
||||
);
|
||||
|
||||
const coloredCharHoverBlock = extractClassBlock(cssText, '#subtitleRoot .word.word-known .c:hover');
|
||||
assert.match(coloredCharHoverBlock, /background:\s*transparent;/);
|
||||
assert.match(coloredCharHoverBlock, /color:\s*inherit\s*!important;/);
|
||||
|
||||
const selectionBlock = extractClassBlock(cssText, '#subtitleRoot::selection');
|
||||
assert.match(
|
||||
selectionBlock,
|
||||
/background:\s*var\(--subtitle-hover-token-background-color,\s*rgba\(54,\s*58,\s*79,\s*0\.84\)\);/,
|
||||
);
|
||||
assert.match(selectionBlock, /color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/);
|
||||
assert.match(
|
||||
selectionBlock,
|
||||
/-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
||||
);
|
||||
|
||||
const descendantSelectionBlock = extractClassBlock(cssText, '#subtitleRoot *::selection');
|
||||
assert.match(
|
||||
descendantSelectionBlock,
|
||||
/background:\s*var\(--subtitle-hover-token-background-color,\s*rgba\(54,\s*58,\s*79,\s*0\.84\)\)\s*!important;/,
|
||||
);
|
||||
assert.match(
|
||||
descendantSelectionBlock,
|
||||
/color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
||||
);
|
||||
assert.match(
|
||||
descendantSelectionBlock,
|
||||
/-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
cssText,
|
||||
/body\.layer-visible\s+#secondarySubContainer\s*\{[^}]*display:\s*none/i,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -15,11 +15,8 @@ export type InvisibleTokenHoverRange = {
|
||||
tokenIndex: number;
|
||||
};
|
||||
|
||||
export function shouldRenderTokenizedSubtitle(
|
||||
isInvisibleLayer: boolean,
|
||||
tokenCount: number,
|
||||
): boolean {
|
||||
return !isInvisibleLayer && tokenCount > 0;
|
||||
export function shouldRenderTokenizedSubtitle(tokenCount: number): boolean {
|
||||
return tokenCount > 0;
|
||||
}
|
||||
|
||||
function isWhitespaceOnly(value: string): boolean {
|
||||
@@ -47,6 +44,23 @@ function sanitizeHexColor(value: unknown, fallback: string): string {
|
||||
: fallback;
|
||||
}
|
||||
|
||||
export function sanitizeSubtitleHoverTokenColor(value: unknown): string {
|
||||
const sanitized = sanitizeHexColor(value, '#f4dbd6');
|
||||
const normalized = sanitized.replace(/^#/, '').toLowerCase();
|
||||
if (normalized === '000' || normalized === '0000' || normalized === '000000' || normalized === '00000000') {
|
||||
return '#f4dbd6';
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
function sanitizeSubtitleHoverTokenBackgroundColor(value: unknown): string {
|
||||
if (typeof value !== 'string') {
|
||||
return 'rgba(54, 58, 79, 0.84)';
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : 'rgba(54, 58, 79, 0.84)';
|
||||
}
|
||||
|
||||
const DEFAULT_FREQUENCY_RENDER_SETTINGS: FrequencyRenderSettings = {
|
||||
enabled: false,
|
||||
topX: 1000,
|
||||
@@ -79,6 +93,54 @@ function sanitizeFrequencyBandedColors(
|
||||
];
|
||||
}
|
||||
|
||||
function applyInlineStyleDeclarations(
|
||||
target: HTMLElement,
|
||||
declarations: Record<string, unknown>,
|
||||
excludedKeys: ReadonlySet<string> = new Set<string>(),
|
||||
): void {
|
||||
for (const [key, value] of Object.entries(declarations)) {
|
||||
if (excludedKeys.has(key)) {
|
||||
continue;
|
||||
}
|
||||
if (value === null || value === undefined || typeof value === 'object') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cssValue = String(value);
|
||||
if (key.startsWith('-') || key.includes('-')) {
|
||||
target.style.setProperty(key, cssValue);
|
||||
if (key === '--webkit-text-stroke') {
|
||||
target.style.setProperty('-webkit-text-stroke', cssValue);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const styleTarget = target.style as unknown as Record<string, string>;
|
||||
styleTarget[key] = cssValue;
|
||||
}
|
||||
}
|
||||
|
||||
function pickInlineStyleDeclarations(
|
||||
declarations: Record<string, unknown>,
|
||||
includedKeys: ReadonlySet<string>,
|
||||
): Record<string, unknown> {
|
||||
const picked: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(declarations)) {
|
||||
if (!includedKeys.has(key)) continue;
|
||||
picked[key] = value;
|
||||
}
|
||||
return picked;
|
||||
}
|
||||
|
||||
const CONTAINER_STYLE_KEYS = new Set<string>([
|
||||
'background',
|
||||
'backgroundColor',
|
||||
'backdropFilter',
|
||||
'WebkitBackdropFilter',
|
||||
'webkitBackdropFilter',
|
||||
'-webkit-backdrop-filter',
|
||||
]);
|
||||
|
||||
function getFrequencyDictionaryClass(
|
||||
token: MergedToken,
|
||||
settings: FrequencyRenderSettings,
|
||||
@@ -337,11 +399,8 @@ function renderPlainTextPreserveLineBreaks(root: ParentNode, text: string): void
|
||||
}
|
||||
|
||||
export function createSubtitleRenderer(ctx: RendererContext) {
|
||||
function renderSubtitle(data: SubtitleData | string): void {
|
||||
function renderSubtitle(data: SubtitleData | string): void {
|
||||
ctx.dom.subtitleRoot.innerHTML = '';
|
||||
ctx.state.lastHoverSelectionKey = '';
|
||||
ctx.state.lastHoverSelectionNode = null;
|
||||
ctx.state.lastHoveredTokenIndex = null;
|
||||
|
||||
let text: string;
|
||||
let tokens: MergedToken[] | null;
|
||||
@@ -358,22 +417,8 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
||||
|
||||
if (!text) return;
|
||||
|
||||
if (ctx.platform.isInvisibleLayer) {
|
||||
// Keep natural kerning/shaping in invisible layer to match mpv glyph placement.
|
||||
const normalizedInvisible = normalizeSubtitle(text, false);
|
||||
ctx.state.currentInvisibleSubtitleLineCount = Math.max(
|
||||
1,
|
||||
normalizedInvisible.split('\n').length,
|
||||
);
|
||||
ctx.state.invisibleTokenHoverSourceText = normalizedInvisible;
|
||||
ctx.state.invisibleTokenHoverRanges =
|
||||
tokens && tokens.length > 0 ? buildInvisibleTokenHoverRanges(tokens, normalizedInvisible) : [];
|
||||
renderPlainTextPreserveLineBreaks(ctx.dom.subtitleRoot, normalizedInvisible);
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = normalizeSubtitle(text, true, !ctx.state.preserveSubtitleLineBreaks);
|
||||
if (shouldRenderTokenizedSubtitle(ctx.platform.isInvisibleLayer, tokens?.length ?? 0) && tokens) {
|
||||
if (shouldRenderTokenizedSubtitle(tokens?.length ?? 0) && tokens) {
|
||||
renderWithTokens(
|
||||
ctx.dom.subtitleRoot,
|
||||
tokens,
|
||||
@@ -444,17 +489,30 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
||||
function applySubtitleStyle(style: SubtitleStyleConfig | null): void {
|
||||
if (!style) return;
|
||||
|
||||
const styleDeclarations = style as Record<string, unknown>;
|
||||
applyInlineStyleDeclarations(
|
||||
ctx.dom.subtitleRoot,
|
||||
styleDeclarations,
|
||||
CONTAINER_STYLE_KEYS,
|
||||
);
|
||||
applyInlineStyleDeclarations(
|
||||
ctx.dom.subtitleContainer,
|
||||
pickInlineStyleDeclarations(styleDeclarations, CONTAINER_STYLE_KEYS),
|
||||
);
|
||||
|
||||
if (style.fontFamily) ctx.dom.subtitleRoot.style.fontFamily = style.fontFamily;
|
||||
if (style.fontSize) ctx.dom.subtitleRoot.style.fontSize = `${style.fontSize}px`;
|
||||
if (style.fontColor) ctx.dom.subtitleRoot.style.color = style.fontColor;
|
||||
if (style.fontColor) {
|
||||
ctx.dom.subtitleRoot.style.color = style.fontColor;
|
||||
}
|
||||
if (style.fontWeight) ctx.dom.subtitleRoot.style.fontWeight = style.fontWeight;
|
||||
if (style.fontStyle) ctx.dom.subtitleRoot.style.fontStyle = style.fontStyle;
|
||||
if (style.backgroundColor) {
|
||||
ctx.dom.subtitleContainer.style.background = style.backgroundColor;
|
||||
}
|
||||
|
||||
const knownWordColor = style.knownWordColor ?? ctx.state.knownWordColor ?? '#a6da95';
|
||||
const nPlusOneColor = style.nPlusOneColor ?? ctx.state.nPlusOneColor ?? '#c6a0f6';
|
||||
const hoverTokenColor = sanitizeSubtitleHoverTokenColor(style.hoverTokenColor);
|
||||
const hoverTokenBackgroundColor = sanitizeSubtitleHoverTokenBackgroundColor(
|
||||
style.hoverTokenBackgroundColor,
|
||||
);
|
||||
const jlptColors = {
|
||||
N1: ctx.state.jlptN1Color ?? '#ed8796',
|
||||
N2: ctx.state.jlptN2Color ?? '#f5a97f',
|
||||
@@ -476,6 +534,11 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
||||
ctx.state.nPlusOneColor = nPlusOneColor;
|
||||
ctx.dom.subtitleRoot.style.setProperty('--subtitle-known-word-color', knownWordColor);
|
||||
ctx.dom.subtitleRoot.style.setProperty('--subtitle-n-plus-one-color', nPlusOneColor);
|
||||
ctx.dom.subtitleRoot.style.setProperty('--subtitle-hover-token-color', hoverTokenColor);
|
||||
ctx.dom.subtitleRoot.style.setProperty(
|
||||
'--subtitle-hover-token-background-color',
|
||||
hoverTokenBackgroundColor,
|
||||
);
|
||||
ctx.state.jlptN1Color = jlptColors.N1;
|
||||
ctx.state.jlptN2Color = jlptColors.N2;
|
||||
ctx.state.jlptN3Color = jlptColors.N3;
|
||||
@@ -551,6 +614,17 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
||||
const secondaryStyle = style.secondary;
|
||||
if (!secondaryStyle) return;
|
||||
|
||||
const secondaryStyleDeclarations = secondaryStyle as Record<string, unknown>;
|
||||
applyInlineStyleDeclarations(
|
||||
ctx.dom.secondarySubRoot,
|
||||
secondaryStyleDeclarations,
|
||||
CONTAINER_STYLE_KEYS,
|
||||
);
|
||||
applyInlineStyleDeclarations(
|
||||
ctx.dom.secondarySubContainer,
|
||||
pickInlineStyleDeclarations(secondaryStyleDeclarations, CONTAINER_STYLE_KEYS),
|
||||
);
|
||||
|
||||
if (secondaryStyle.fontFamily) {
|
||||
ctx.dom.secondarySubRoot.style.fontFamily = secondaryStyle.fontFamily;
|
||||
}
|
||||
@@ -566,9 +640,6 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
||||
if (secondaryStyle.fontStyle) {
|
||||
ctx.dom.secondarySubRoot.style.fontStyle = secondaryStyle.fontStyle;
|
||||
}
|
||||
if (secondaryStyle.backgroundColor) {
|
||||
ctx.dom.secondarySubContainer.style.background = secondaryStyle.backgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,40 +1,25 @@
|
||||
export type OverlayLayer = 'visible' | 'invisible' | 'secondary' | 'modal';
|
||||
export type OverlayLayer = 'visible' | 'modal';
|
||||
|
||||
export type PlatformInfo = {
|
||||
overlayLayer: OverlayLayer;
|
||||
isInvisibleLayer: boolean;
|
||||
isSecondaryLayer: boolean;
|
||||
isModalLayer: boolean;
|
||||
isLinuxPlatform: boolean;
|
||||
isMacOSPlatform: boolean;
|
||||
shouldToggleMouseIgnore: boolean;
|
||||
invisiblePositionEditToggleCode: string;
|
||||
invisiblePositionStepPx: number;
|
||||
invisiblePositionStepFastPx: number;
|
||||
};
|
||||
|
||||
export function resolvePlatformInfo(): PlatformInfo {
|
||||
const overlayLayerFromPreload = window.electronAPI.getOverlayLayer();
|
||||
const queryLayer = new URLSearchParams(window.location.search).get('layer');
|
||||
const overlayLayerFromQuery: OverlayLayer | null =
|
||||
queryLayer === 'visible' ||
|
||||
queryLayer === 'invisible' ||
|
||||
queryLayer === 'secondary' ||
|
||||
queryLayer === 'modal'
|
||||
? queryLayer
|
||||
: null;
|
||||
queryLayer === 'visible' || queryLayer === 'modal' ? queryLayer : null;
|
||||
|
||||
const overlayLayer: OverlayLayer =
|
||||
overlayLayerFromQuery ??
|
||||
(overlayLayerFromPreload === 'visible' ||
|
||||
overlayLayerFromPreload === 'invisible' ||
|
||||
overlayLayerFromPreload === 'secondary' ||
|
||||
overlayLayerFromPreload === 'modal'
|
||||
(overlayLayerFromPreload === 'visible' || overlayLayerFromPreload === 'modal'
|
||||
? overlayLayerFromPreload
|
||||
: 'visible');
|
||||
|
||||
const isInvisibleLayer = overlayLayer === 'invisible';
|
||||
const isSecondaryLayer = overlayLayer === 'secondary';
|
||||
const isModalLayer = overlayLayer === 'modal';
|
||||
const isLinuxPlatform = navigator.platform.toLowerCase().includes('linux');
|
||||
const isMacOSPlatform =
|
||||
@@ -42,14 +27,9 @@ export function resolvePlatformInfo(): PlatformInfo {
|
||||
|
||||
return {
|
||||
overlayLayer,
|
||||
isInvisibleLayer,
|
||||
isSecondaryLayer,
|
||||
isModalLayer,
|
||||
isLinuxPlatform,
|
||||
isMacOSPlatform,
|
||||
shouldToggleMouseIgnore: !isLinuxPlatform && !isSecondaryLayer && !isModalLayer,
|
||||
invisiblePositionEditToggleCode: 'KeyP',
|
||||
invisiblePositionStepPx: 1,
|
||||
invisiblePositionStepFastPx: 4,
|
||||
shouldToggleMouseIgnore: !isLinuxPlatform && !isModalLayer,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user