mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
pretty
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import type { RendererState } from "./state";
|
||||
import type { RendererDom } from "./utils/dom";
|
||||
import type { PlatformInfo } from "./utils/platform";
|
||||
import type { RendererState } from './state';
|
||||
import type { RendererDom } from './utils/dom';
|
||||
import type { PlatformInfo } from './utils/platform';
|
||||
|
||||
export type RendererContext = {
|
||||
dom: RendererDom;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Keybinding } from "../../types";
|
||||
import type { RendererContext } from "../context";
|
||||
import type { Keybinding } from '../../types';
|
||||
import type { RendererContext } from '../context';
|
||||
|
||||
export function createKeyboardHandlers(
|
||||
ctx: RendererContext,
|
||||
@@ -10,7 +10,7 @@ export function createKeyboardHandlers(
|
||||
handleJimakuKeydown: (e: KeyboardEvent) => boolean;
|
||||
handleSessionHelpKeydown: (e: KeyboardEvent) => boolean;
|
||||
openSessionHelpModal: (opening: {
|
||||
bindingKey: "KeyH" | "KeyK";
|
||||
bindingKey: 'KeyH' | 'KeyK';
|
||||
fallbackUsed: boolean;
|
||||
fallbackUnavailable: boolean;
|
||||
}) => void;
|
||||
@@ -26,56 +26,40 @@ export function createKeyboardHandlers(
|
||||
|
||||
const CHORD_MAP = new Map<
|
||||
string,
|
||||
{ type: "mpv" | "electron"; command?: string[]; action?: () => void }
|
||||
{ type: 'mpv' | 'electron'; command?: string[]; action?: () => void }
|
||||
>([
|
||||
["KeyS", { type: "mpv", command: ["script-message", "subminer-start"] }],
|
||||
[
|
||||
"Shift+KeyS",
|
||||
{ type: "mpv", command: ["script-message", "subminer-stop"] },
|
||||
],
|
||||
["KeyT", { type: "mpv", command: ["script-message", "subminer-toggle"] }],
|
||||
[
|
||||
"KeyI",
|
||||
{ type: "mpv", command: ["script-message", "subminer-toggle-invisible"] },
|
||||
],
|
||||
[
|
||||
"Shift+KeyI",
|
||||
{ type: "mpv", command: ["script-message", "subminer-show-invisible"] },
|
||||
],
|
||||
[
|
||||
"KeyU",
|
||||
{ type: "mpv", command: ["script-message", "subminer-hide-invisible"] },
|
||||
],
|
||||
["KeyO", { type: "mpv", command: ["script-message", "subminer-options"] }],
|
||||
["KeyR", { type: "mpv", command: ["script-message", "subminer-restart"] }],
|
||||
["KeyC", { type: "mpv", command: ["script-message", "subminer-status"] }],
|
||||
["KeyY", { type: "mpv", command: ["script-message", "subminer-menu"] }],
|
||||
[
|
||||
"KeyD",
|
||||
{ type: "electron", action: () => window.electronAPI.toggleDevTools() },
|
||||
],
|
||||
['KeyS', { type: 'mpv', command: ['script-message', 'subminer-start'] }],
|
||||
['Shift+KeyS', { type: 'mpv', command: ['script-message', 'subminer-stop'] }],
|
||||
['KeyT', { type: 'mpv', command: ['script-message', 'subminer-toggle'] }],
|
||||
['KeyI', { type: 'mpv', command: ['script-message', 'subminer-toggle-invisible'] }],
|
||||
['Shift+KeyI', { type: 'mpv', command: ['script-message', 'subminer-show-invisible'] }],
|
||||
['KeyU', { type: 'mpv', command: ['script-message', 'subminer-hide-invisible'] }],
|
||||
['KeyO', { type: 'mpv', command: ['script-message', 'subminer-options'] }],
|
||||
['KeyR', { type: 'mpv', command: ['script-message', 'subminer-restart'] }],
|
||||
['KeyC', { type: 'mpv', command: ['script-message', 'subminer-status'] }],
|
||||
['KeyY', { type: 'mpv', command: ['script-message', 'subminer-menu'] }],
|
||||
['KeyD', { type: 'electron', action: () => window.electronAPI.toggleDevTools() }],
|
||||
]);
|
||||
|
||||
function isInteractiveTarget(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof Element)) return false;
|
||||
if (target.closest(".modal")) return true;
|
||||
if (target.closest('.modal')) return true;
|
||||
if (ctx.dom.subtitleContainer.contains(target)) return true;
|
||||
if (target.tagName === "IFRAME" && target.id?.startsWith("yomitan-popup")) {
|
||||
if (target.tagName === 'IFRAME' && target.id?.startsWith('yomitan-popup')) {
|
||||
return true;
|
||||
}
|
||||
if (target.closest && target.closest('iframe[id^="yomitan-popup"]'))
|
||||
return true;
|
||||
if (target.closest && target.closest('iframe[id^="yomitan-popup"]')) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function keyEventToString(e: KeyboardEvent): string {
|
||||
const parts: string[] = [];
|
||||
if (e.ctrlKey) parts.push("Ctrl");
|
||||
if (e.altKey) parts.push("Alt");
|
||||
if (e.shiftKey) parts.push("Shift");
|
||||
if (e.metaKey) parts.push("Meta");
|
||||
if (e.ctrlKey) parts.push('Ctrl');
|
||||
if (e.altKey) parts.push('Alt');
|
||||
if (e.shiftKey) parts.push('Shift');
|
||||
if (e.metaKey) parts.push('Meta');
|
||||
parts.push(e.code);
|
||||
return parts.join("+");
|
||||
return parts.join('+');
|
||||
}
|
||||
|
||||
function isInvisiblePositionToggleShortcut(e: KeyboardEvent): boolean {
|
||||
@@ -88,12 +72,12 @@ export function createKeyboardHandlers(
|
||||
}
|
||||
|
||||
function resolveSessionHelpChordBinding(): {
|
||||
bindingKey: "KeyH" | "KeyK";
|
||||
bindingKey: 'KeyH' | 'KeyK';
|
||||
fallbackUsed: boolean;
|
||||
fallbackUnavailable: boolean;
|
||||
} {
|
||||
const firstChoice = "KeyH";
|
||||
if (!ctx.state.keybindingsMap.has("KeyH")) {
|
||||
const firstChoice = 'KeyH';
|
||||
if (!ctx.state.keybindingsMap.has('KeyH')) {
|
||||
return {
|
||||
bindingKey: firstChoice,
|
||||
fallbackUsed: false,
|
||||
@@ -101,27 +85,27 @@ export function createKeyboardHandlers(
|
||||
};
|
||||
}
|
||||
|
||||
if (ctx.state.keybindingsMap.has("KeyK")) {
|
||||
if (ctx.state.keybindingsMap.has('KeyK')) {
|
||||
return {
|
||||
bindingKey: "KeyK",
|
||||
bindingKey: 'KeyK',
|
||||
fallbackUsed: true,
|
||||
fallbackUnavailable: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
bindingKey: "KeyK",
|
||||
bindingKey: 'KeyK',
|
||||
fallbackUsed: true,
|
||||
fallbackUnavailable: false,
|
||||
};
|
||||
}
|
||||
|
||||
function applySessionHelpChordBinding(): void {
|
||||
CHORD_MAP.delete("KeyH");
|
||||
CHORD_MAP.delete("KeyK");
|
||||
CHORD_MAP.delete('KeyH');
|
||||
CHORD_MAP.delete('KeyK');
|
||||
const info = resolveSessionHelpChordBinding();
|
||||
CHORD_MAP.set(info.bindingKey, {
|
||||
type: "electron",
|
||||
type: 'electron',
|
||||
action: () => {
|
||||
options.openSessionHelpModal(info);
|
||||
},
|
||||
@@ -147,40 +131,40 @@ export function createKeyboardHandlers(
|
||||
? ctx.platform.invisiblePositionStepFastPx
|
||||
: ctx.platform.invisiblePositionStepPx;
|
||||
|
||||
if (e.key === "Escape") {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
options.cancelInvisiblePositionEdit();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e.key === "Enter" || ((e.ctrlKey || e.metaKey) && e.code === "KeyS")) {
|
||||
if (e.key === 'Enter' || ((e.ctrlKey || e.metaKey) && e.code === 'KeyS')) {
|
||||
e.preventDefault();
|
||||
options.saveInvisiblePositionEdit();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
e.key === "ArrowUp" ||
|
||||
e.key === "ArrowDown" ||
|
||||
e.key === "ArrowLeft" ||
|
||||
e.key === "ArrowRight" ||
|
||||
e.key === "h" ||
|
||||
e.key === "j" ||
|
||||
e.key === "k" ||
|
||||
e.key === "l" ||
|
||||
e.key === "H" ||
|
||||
e.key === "J" ||
|
||||
e.key === "K" ||
|
||||
e.key === "L"
|
||||
e.key === 'ArrowUp' ||
|
||||
e.key === 'ArrowDown' ||
|
||||
e.key === 'ArrowLeft' ||
|
||||
e.key === 'ArrowRight' ||
|
||||
e.key === 'h' ||
|
||||
e.key === 'j' ||
|
||||
e.key === 'k' ||
|
||||
e.key === 'l' ||
|
||||
e.key === 'H' ||
|
||||
e.key === 'J' ||
|
||||
e.key === 'K' ||
|
||||
e.key === 'L'
|
||||
) {
|
||||
e.preventDefault();
|
||||
if (e.key === "ArrowUp" || e.key === "k" || e.key === "K") {
|
||||
if (e.key === 'ArrowUp' || e.key === 'k' || e.key === 'K') {
|
||||
ctx.state.invisibleSubtitleOffsetYPx += step;
|
||||
} else if (e.key === "ArrowDown" || e.key === "j" || e.key === "J") {
|
||||
} else if (e.key === 'ArrowDown' || e.key === 'j' || e.key === 'J') {
|
||||
ctx.state.invisibleSubtitleOffsetYPx -= step;
|
||||
} else if (e.key === "ArrowLeft" || e.key === "h" || e.key === "H") {
|
||||
} else if (e.key === 'ArrowLeft' || e.key === 'h' || e.key === 'H') {
|
||||
ctx.state.invisibleSubtitleOffsetXPx -= step;
|
||||
} else if (e.key === "ArrowRight" || e.key === "l" || e.key === "L") {
|
||||
} else if (e.key === 'ArrowRight' || e.key === 'l' || e.key === 'L') {
|
||||
ctx.state.invisibleSubtitleOffsetXPx += step;
|
||||
}
|
||||
options.applyInvisibleSubtitleOffsetPosition();
|
||||
@@ -208,10 +192,8 @@ export function createKeyboardHandlers(
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", (e: KeyboardEvent) => {
|
||||
const yomitanPopup = document.querySelector(
|
||||
'iframe[id^="yomitan-popup"]',
|
||||
);
|
||||
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
const yomitanPopup = document.querySelector('iframe[id^="yomitan-popup"]');
|
||||
if (yomitanPopup) return;
|
||||
if (handleInvisiblePositionEditKeydown(e)) return;
|
||||
|
||||
@@ -238,14 +220,14 @@ export function createKeyboardHandlers(
|
||||
|
||||
if (ctx.state.chordPending) {
|
||||
const modifierKeys = [
|
||||
"ShiftLeft",
|
||||
"ShiftRight",
|
||||
"ControlLeft",
|
||||
"ControlRight",
|
||||
"AltLeft",
|
||||
"AltRight",
|
||||
"MetaLeft",
|
||||
"MetaRight",
|
||||
'ShiftLeft',
|
||||
'ShiftRight',
|
||||
'ControlLeft',
|
||||
'ControlRight',
|
||||
'AltLeft',
|
||||
'AltRight',
|
||||
'MetaLeft',
|
||||
'MetaRight',
|
||||
];
|
||||
if (modifierKeys.includes(e.code)) {
|
||||
return;
|
||||
@@ -256,23 +238,16 @@ export function createKeyboardHandlers(
|
||||
const action = CHORD_MAP.get(secondKey);
|
||||
resetChord();
|
||||
if (action) {
|
||||
if (action.type === "mpv" && action.command) {
|
||||
if (action.type === 'mpv' && action.command) {
|
||||
window.electronAPI.sendMpvCommand(action.command);
|
||||
} else if (action.type === "electron" && action.action) {
|
||||
} else if (action.type === 'electron' && action.action) {
|
||||
action.action();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
e.code === "KeyY" &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey &&
|
||||
!e.shiftKey &&
|
||||
!e.metaKey &&
|
||||
!e.repeat
|
||||
) {
|
||||
if (e.code === 'KeyY' && !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey && !e.repeat) {
|
||||
e.preventDefault();
|
||||
applySessionHelpChordBinding();
|
||||
ctx.state.chordPending = true;
|
||||
@@ -291,14 +266,14 @@ export function createKeyboardHandlers(
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("mousedown", (e: MouseEvent) => {
|
||||
document.addEventListener('mousedown', (e: MouseEvent) => {
|
||||
if (e.button === 2 && !isInteractiveTarget(e.target)) {
|
||||
e.preventDefault();
|
||||
window.electronAPI.sendMpvCommand(["cycle", "pause"]);
|
||||
window.electronAPI.sendMpvCommand(['cycle', 'pause']);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("contextmenu", (e: Event) => {
|
||||
document.addEventListener('contextmenu', (e: Event) => {
|
||||
if (!isInteractiveTarget(e.target)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
import type { ModalStateReader, RendererContext } from "../context";
|
||||
import type { ModalStateReader, RendererContext } from '../context';
|
||||
|
||||
export function createMouseHandlers(
|
||||
ctx: RendererContext,
|
||||
options: {
|
||||
modalStateReader: ModalStateReader;
|
||||
applyInvisibleSubtitleLayoutFromMpvMetrics: (
|
||||
metrics: any,
|
||||
source: string,
|
||||
) => void;
|
||||
applyInvisibleSubtitleLayoutFromMpvMetrics: (metrics: any, source: string) => void;
|
||||
applyYPercent: (yPercent: number) => void;
|
||||
getCurrentYPercent: () => number;
|
||||
persistSubtitlePositionPatch: (patch: { yPercent: number }) => void;
|
||||
},
|
||||
) {
|
||||
const wordSegmenter =
|
||||
typeof Intl !== "undefined" && "Segmenter" in Intl
|
||||
? new Intl.Segmenter(undefined, { granularity: "word" })
|
||||
typeof Intl !== 'undefined' && 'Segmenter' in Intl
|
||||
? new Intl.Segmenter(undefined, { granularity: 'word' })
|
||||
: null;
|
||||
|
||||
function handleMouseEnter(): void {
|
||||
ctx.state.isOverSubtitle = true;
|
||||
ctx.dom.overlay.classList.add("interactive");
|
||||
ctx.dom.overlay.classList.add('interactive');
|
||||
if (ctx.platform.shouldToggleMouseIgnore) {
|
||||
window.electronAPI.setIgnoreMouseEvents(false);
|
||||
}
|
||||
@@ -34,7 +31,7 @@ export function createMouseHandlers(
|
||||
!options.modalStateReader.isAnyModalOpen() &&
|
||||
!ctx.state.invisiblePositionEditMode
|
||||
) {
|
||||
ctx.dom.overlay.classList.remove("interactive");
|
||||
ctx.dom.overlay.classList.remove('interactive');
|
||||
if (ctx.platform.shouldToggleMouseIgnore) {
|
||||
window.electronAPI.setIgnoreMouseEvents(true, { forward: true });
|
||||
}
|
||||
@@ -42,17 +39,17 @@ export function createMouseHandlers(
|
||||
}
|
||||
|
||||
function setupDragging(): void {
|
||||
ctx.dom.subtitleContainer.addEventListener("mousedown", (e: MouseEvent) => {
|
||||
ctx.dom.subtitleContainer.addEventListener('mousedown', (e: MouseEvent) => {
|
||||
if (e.button === 2) {
|
||||
e.preventDefault();
|
||||
ctx.state.isDragging = true;
|
||||
ctx.state.dragStartY = e.clientY;
|
||||
ctx.state.startYPercent = options.getCurrentYPercent();
|
||||
ctx.dom.subtitleContainer.style.cursor = "grabbing";
|
||||
ctx.dom.subtitleContainer.style.cursor = 'grabbing';
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("mousemove", (e: MouseEvent) => {
|
||||
document.addEventListener('mousemove', (e: MouseEvent) => {
|
||||
if (!ctx.state.isDragging) return;
|
||||
|
||||
const deltaY = ctx.state.dragStartY - e.clientY;
|
||||
@@ -62,25 +59,22 @@ export function createMouseHandlers(
|
||||
options.applyYPercent(newYPercent);
|
||||
});
|
||||
|
||||
document.addEventListener("mouseup", (e: MouseEvent) => {
|
||||
document.addEventListener('mouseup', (e: MouseEvent) => {
|
||||
if (ctx.state.isDragging && e.button === 2) {
|
||||
ctx.state.isDragging = false;
|
||||
ctx.dom.subtitleContainer.style.cursor = "";
|
||||
ctx.dom.subtitleContainer.style.cursor = '';
|
||||
|
||||
const yPercent = options.getCurrentYPercent();
|
||||
options.persistSubtitlePositionPatch({ yPercent });
|
||||
}
|
||||
});
|
||||
|
||||
ctx.dom.subtitleContainer.addEventListener("contextmenu", (e: Event) => {
|
||||
ctx.dom.subtitleContainer.addEventListener('contextmenu', (e: Event) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
function getCaretTextPointRange(
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
): Range | null {
|
||||
function getCaretTextPointRange(clientX: number, clientY: number): Range | null {
|
||||
const documentWithCaretApi = document as Document & {
|
||||
caretRangeFromPoint?: (x: number, y: number) => Range | null;
|
||||
caretPositionFromPoint?: (
|
||||
@@ -89,15 +83,12 @@ export function createMouseHandlers(
|
||||
) => { offsetNode: Node; offset: number } | null;
|
||||
};
|
||||
|
||||
if (typeof documentWithCaretApi.caretRangeFromPoint === "function") {
|
||||
if (typeof documentWithCaretApi.caretRangeFromPoint === 'function') {
|
||||
return documentWithCaretApi.caretRangeFromPoint(clientX, clientY);
|
||||
}
|
||||
|
||||
if (typeof documentWithCaretApi.caretPositionFromPoint === "function") {
|
||||
const caretPosition = documentWithCaretApi.caretPositionFromPoint(
|
||||
clientX,
|
||||
clientY,
|
||||
);
|
||||
if (typeof documentWithCaretApi.caretPositionFromPoint === 'function') {
|
||||
const caretPosition = documentWithCaretApi.caretPositionFromPoint(clientX, clientY);
|
||||
if (!caretPosition) return null;
|
||||
const range = document.createRange();
|
||||
range.setStart(caretPosition.offsetNode, caretPosition.offset);
|
||||
@@ -115,10 +106,7 @@ export function createMouseHandlers(
|
||||
if (!text || text.length === 0) return null;
|
||||
|
||||
const clampedOffset = Math.max(0, Math.min(offset, text.length));
|
||||
const probeIndex =
|
||||
clampedOffset >= text.length
|
||||
? Math.max(0, text.length - 1)
|
||||
: clampedOffset;
|
||||
const probeIndex = clampedOffset >= text.length ? Math.max(0, text.length - 1) : clampedOffset;
|
||||
|
||||
if (wordSegmenter) {
|
||||
for (const part of wordSegmenter.segment(text)) {
|
||||
@@ -132,9 +120,7 @@ export function createMouseHandlers(
|
||||
}
|
||||
|
||||
const isBoundary = (char: string): boolean =>
|
||||
/[\s\u3000.,!?;:()[\]{}"'`~<>/\\|@#$%^&*+=\-、。・「」『』【】〈〉《》]/.test(
|
||||
char,
|
||||
);
|
||||
/[\s\u3000.,!?;:()[\]{}"'`~<>/\\|@#$%^&*+=\-、。・「」『』【】〈〉《》]/.test(char);
|
||||
|
||||
const probeChar = text[probeIndex];
|
||||
if (!probeChar || isBoundary(probeChar)) return null;
|
||||
@@ -165,10 +151,7 @@ export function createMouseHandlers(
|
||||
if (!ctx.dom.subtitleRoot.contains(caretRange.startContainer)) return;
|
||||
|
||||
const textNode = caretRange.startContainer as Text;
|
||||
const wordBounds = getWordBoundsAtOffset(
|
||||
textNode.data,
|
||||
caretRange.startOffset,
|
||||
);
|
||||
const wordBounds = getWordBoundsAtOffset(textNode.data, caretRange.startOffset);
|
||||
if (!wordBounds) return;
|
||||
|
||||
const selectionKey = `${wordBounds.start}:${wordBounds.end}:${textNode.data.slice(
|
||||
@@ -198,23 +181,23 @@ export function createMouseHandlers(
|
||||
function setupInvisibleHoverSelection(): void {
|
||||
if (!ctx.platform.isInvisibleLayer || !ctx.platform.isMacOSPlatform) return;
|
||||
|
||||
ctx.dom.subtitleRoot.addEventListener("mousemove", (event: MouseEvent) => {
|
||||
ctx.dom.subtitleRoot.addEventListener('mousemove', (event: MouseEvent) => {
|
||||
updateHoverWordSelection(event);
|
||||
});
|
||||
|
||||
ctx.dom.subtitleRoot.addEventListener("mouseleave", () => {
|
||||
ctx.state.lastHoverSelectionKey = "";
|
||||
ctx.dom.subtitleRoot.addEventListener('mouseleave', () => {
|
||||
ctx.state.lastHoverSelectionKey = '';
|
||||
ctx.state.lastHoverSelectionNode = null;
|
||||
});
|
||||
}
|
||||
|
||||
function setupResizeHandler(): void {
|
||||
window.addEventListener("resize", () => {
|
||||
window.addEventListener('resize', () => {
|
||||
if (ctx.platform.isInvisibleLayer) {
|
||||
if (!ctx.state.mpvSubtitleRenderMetrics) return;
|
||||
options.applyInvisibleSubtitleLayoutFromMpvMetrics(
|
||||
ctx.state.mpvSubtitleRenderMetrics,
|
||||
"resize",
|
||||
'resize',
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -223,15 +206,14 @@ export function createMouseHandlers(
|
||||
}
|
||||
|
||||
function setupSelectionObserver(): void {
|
||||
document.addEventListener("selectionchange", () => {
|
||||
document.addEventListener('selectionchange', () => {
|
||||
const selection = window.getSelection();
|
||||
const hasSelection =
|
||||
selection && selection.rangeCount > 0 && !selection.isCollapsed;
|
||||
const hasSelection = selection && selection.rangeCount > 0 && !selection.isCollapsed;
|
||||
|
||||
if (hasSelection) {
|
||||
ctx.dom.subtitleRoot.classList.add("has-selection");
|
||||
ctx.dom.subtitleRoot.classList.add('has-selection');
|
||||
} else {
|
||||
ctx.dom.subtitleRoot.classList.remove("has-selection");
|
||||
ctx.dom.subtitleRoot.classList.remove('has-selection');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -243,11 +225,11 @@ export function createMouseHandlers(
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
const element = node as Element;
|
||||
if (
|
||||
element.tagName === "IFRAME" &&
|
||||
element.tagName === 'IFRAME' &&
|
||||
element.id &&
|
||||
element.id.startsWith("yomitan-popup")
|
||||
element.id.startsWith('yomitan-popup')
|
||||
) {
|
||||
ctx.dom.overlay.classList.add("interactive");
|
||||
ctx.dom.overlay.classList.add('interactive');
|
||||
if (ctx.platform.shouldToggleMouseIgnore) {
|
||||
window.electronAPI.setIgnoreMouseEvents(false);
|
||||
}
|
||||
@@ -258,15 +240,12 @@ export function createMouseHandlers(
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
const element = node as Element;
|
||||
if (
|
||||
element.tagName === "IFRAME" &&
|
||||
element.tagName === 'IFRAME' &&
|
||||
element.id &&
|
||||
element.id.startsWith("yomitan-popup")
|
||||
element.id.startsWith('yomitan-popup')
|
||||
) {
|
||||
if (
|
||||
!ctx.state.isOverSubtitle &&
|
||||
!options.modalStateReader.isAnyModalOpen()
|
||||
) {
|
||||
ctx.dom.overlay.classList.remove("interactive");
|
||||
if (!ctx.state.isOverSubtitle && !options.modalStateReader.isAnyModalOpen()) {
|
||||
ctx.dom.overlay.classList.remove('interactive');
|
||||
if (ctx.platform.shouldToggleMouseIgnore) {
|
||||
window.electronAPI.setIgnoreMouseEvents(true, {
|
||||
forward: true,
|
||||
|
||||
@@ -40,9 +40,7 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">Jimaku Subtitles</div>
|
||||
<button id="jimakuClose" class="modal-close" type="button">
|
||||
Close
|
||||
</button>
|
||||
<button id="jimakuClose" class="modal-close" type="button">Close</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="jimaku-form">
|
||||
@@ -52,25 +50,13 @@
|
||||
</label>
|
||||
<label class="jimaku-field">
|
||||
<span>Season</span>
|
||||
<input
|
||||
id="jimakuSeason"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="1"
|
||||
/>
|
||||
<input id="jimakuSeason" type="number" min="1" placeholder="1" />
|
||||
</label>
|
||||
<label class="jimaku-field">
|
||||
<span>Episode</span>
|
||||
<input
|
||||
id="jimakuEpisode"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="1"
|
||||
/>
|
||||
<input id="jimakuEpisode" type="number" min="1" placeholder="1" />
|
||||
</label>
|
||||
<button id="jimakuSearch" class="jimaku-button" type="button">
|
||||
Search
|
||||
</button>
|
||||
<button id="jimakuSearch" class="jimaku-button" type="button">Search</button>
|
||||
</div>
|
||||
<div id="jimakuStatus" class="jimaku-status"></div>
|
||||
<div id="jimakuEntriesSection" class="jimaku-section hidden">
|
||||
@@ -80,11 +66,7 @@
|
||||
<div id="jimakuFilesSection" class="jimaku-section hidden">
|
||||
<div class="jimaku-section-title">Files</div>
|
||||
<ul id="jimakuFiles" class="jimaku-list"></ul>
|
||||
<button
|
||||
id="jimakuBroaden"
|
||||
class="jimaku-link hidden"
|
||||
type="button"
|
||||
>
|
||||
<button id="jimakuBroaden" class="jimaku-link hidden" type="button">
|
||||
Broaden search (all files)
|
||||
</button>
|
||||
</div>
|
||||
@@ -99,26 +81,20 @@
|
||||
<div class="modal-body">
|
||||
<div id="kikuSelectionStep">
|
||||
<div class="kiku-info-text">
|
||||
A card with the same expression already exists. Select which
|
||||
card to keep. The other card's content will be merged using Kiku
|
||||
field grouping. You can choose whether to delete the duplicate.
|
||||
A card with the same expression already exists. Select which card to keep. The other
|
||||
card's content will be merged using Kiku field grouping. You can choose whether to
|
||||
delete the duplicate.
|
||||
</div>
|
||||
<div class="kiku-cards-container">
|
||||
<div id="kikuCard1" class="kiku-card active" tabindex="0">
|
||||
<div class="kiku-card-label">1 — Original Card</div>
|
||||
<div
|
||||
class="kiku-card-expression"
|
||||
id="kikuCard1Expression"
|
||||
></div>
|
||||
<div class="kiku-card-expression" id="kikuCard1Expression"></div>
|
||||
<div class="kiku-card-sentence" id="kikuCard1Sentence"></div>
|
||||
<div class="kiku-card-meta" id="kikuCard1Meta"></div>
|
||||
</div>
|
||||
<div id="kikuCard2" class="kiku-card" tabindex="0">
|
||||
<div class="kiku-card-label">2 — New Card</div>
|
||||
<div
|
||||
class="kiku-card-expression"
|
||||
id="kikuCard2Expression"
|
||||
></div>
|
||||
<div class="kiku-card-expression" id="kikuCard2Expression"></div>
|
||||
<div class="kiku-card-sentence" id="kikuCard2Sentence"></div>
|
||||
<div class="kiku-card-meta" id="kikuCard2Meta"></div>
|
||||
</div>
|
||||
@@ -128,18 +104,10 @@
|
||||
<input id="kikuDeleteDuplicate" type="checkbox" checked />
|
||||
Delete duplicate card after merge
|
||||
</label>
|
||||
<button
|
||||
id="kikuConfirmButton"
|
||||
class="kiku-confirm-button"
|
||||
type="button"
|
||||
>
|
||||
<button id="kikuConfirmButton" class="kiku-confirm-button" type="button">
|
||||
Continue
|
||||
</button>
|
||||
<button
|
||||
id="kikuCancelButton"
|
||||
class="kiku-cancel-button"
|
||||
type="button"
|
||||
>
|
||||
<button id="kikuCancelButton" class="kiku-cancel-button" type="button">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
@@ -153,39 +121,21 @@
|
||||
<button id="kikuPreviewFull" type="button">Full</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="kikuPreviewError"
|
||||
class="kiku-preview-error hidden"
|
||||
></div>
|
||||
<div id="kikuPreviewError" class="kiku-preview-error hidden"></div>
|
||||
<pre id="kikuPreviewJson" class="kiku-preview-json"></pre>
|
||||
<div class="kiku-footer">
|
||||
<button
|
||||
id="kikuBackButton"
|
||||
class="kiku-cancel-button"
|
||||
type="button"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
id="kikuFinalConfirmButton"
|
||||
class="kiku-confirm-button"
|
||||
type="button"
|
||||
>
|
||||
<button id="kikuBackButton" class="kiku-cancel-button" type="button">Back</button>
|
||||
<button id="kikuFinalConfirmButton" class="kiku-confirm-button" type="button">
|
||||
Confirm Merge
|
||||
</button>
|
||||
<button
|
||||
id="kikuFinalCancelButton"
|
||||
class="kiku-cancel-button"
|
||||
type="button"
|
||||
>
|
||||
<button id="kikuFinalCancelButton" class="kiku-cancel-button" type="button">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="kikuHint" class="kiku-hint">
|
||||
Press 1 or 2 to select · Enter to confirm · Esc to
|
||||
cancel
|
||||
Press 1 or 2 to select · Enter to confirm · Esc to cancel
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -194,18 +144,11 @@
|
||||
<div class="modal-content runtime-modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">Runtime Options</div>
|
||||
<button
|
||||
id="runtimeOptionsClose"
|
||||
class="modal-close"
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button id="runtimeOptionsClose" class="modal-close" type="button">Close</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="runtimeOptionsHint" class="runtime-options-hint">
|
||||
Arrow keys: select/change · Enter or double-click: apply · Esc:
|
||||
close
|
||||
Arrow keys: select/change · Enter or double-click: apply · Esc: close
|
||||
</div>
|
||||
<ul id="runtimeOptionsList" class="runtime-options-list"></ul>
|
||||
<div id="runtimeOptionsStatus" class="runtime-options-status"></div>
|
||||
@@ -216,29 +159,18 @@
|
||||
<div class="modal-content subsync-modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">Auto Subtitle Sync</div>
|
||||
<button id="subsyncClose" class="modal-close" type="button">
|
||||
Close
|
||||
</button>
|
||||
<button id="subsyncClose" class="modal-close" type="button">Close</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="subsync-form">
|
||||
<div class="subsync-field">
|
||||
<span>Engine</span>
|
||||
<label class="subsync-radio">
|
||||
<input
|
||||
id="subsyncEngineAlass"
|
||||
type="radio"
|
||||
name="subsyncEngine"
|
||||
checked
|
||||
/>
|
||||
<input id="subsyncEngineAlass" type="radio" name="subsyncEngine" checked />
|
||||
alass
|
||||
</label>
|
||||
<label class="subsync-radio">
|
||||
<input
|
||||
id="subsyncEngineFfsubsync"
|
||||
type="radio"
|
||||
name="subsyncEngine"
|
||||
/>
|
||||
<input id="subsyncEngineFfsubsync" type="radio" name="subsyncEngine" />
|
||||
ffsubsync
|
||||
</label>
|
||||
</div>
|
||||
@@ -249,13 +181,7 @@
|
||||
</div>
|
||||
<div id="subsyncStatus" class="runtime-options-status"></div>
|
||||
<div class="subsync-footer">
|
||||
<button
|
||||
id="subsyncRun"
|
||||
class="kiku-confirm-button"
|
||||
type="button"
|
||||
>
|
||||
Run Sync
|
||||
</button>
|
||||
<button id="subsyncRun" class="kiku-confirm-button" type="button">Run Sync</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -264,9 +190,7 @@
|
||||
<div class="modal-content session-help-content">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">Session Help</div>
|
||||
<button id="sessionHelpClose" class="modal-close" type="button">
|
||||
Close
|
||||
</button>
|
||||
<button id="sessionHelpClose" class="modal-close" type="button">Close</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="sessionHelpShortcut" class="session-help-shortcut"></div>
|
||||
|
||||
@@ -4,21 +4,21 @@ import type {
|
||||
JimakuEntry,
|
||||
JimakuFileEntry,
|
||||
JimakuMediaInfo,
|
||||
} from "../../types";
|
||||
import type { ModalStateReader, RendererContext } from "../context";
|
||||
} from '../../types';
|
||||
import type { ModalStateReader, RendererContext } from '../context';
|
||||
|
||||
export function createJimakuModal(
|
||||
ctx: RendererContext,
|
||||
options: {
|
||||
modalStateReader: Pick<ModalStateReader, "isAnyModalOpen">;
|
||||
modalStateReader: Pick<ModalStateReader, 'isAnyModalOpen'>;
|
||||
syncSettingsModalSubtitleSuppression: () => void;
|
||||
},
|
||||
) {
|
||||
function setJimakuStatus(message: string, isError = false): void {
|
||||
ctx.dom.jimakuStatus.textContent = message;
|
||||
ctx.dom.jimakuStatus.style.color = isError
|
||||
? "rgba(255, 120, 120, 0.95)"
|
||||
: "rgba(255, 255, 255, 0.8)";
|
||||
? 'rgba(255, 120, 120, 0.95)'
|
||||
: 'rgba(255, 255, 255, 0.8)';
|
||||
}
|
||||
|
||||
function resetJimakuLists(): void {
|
||||
@@ -28,11 +28,11 @@ export function createJimakuModal(
|
||||
ctx.state.selectedFileIndex = 0;
|
||||
ctx.state.currentEntryId = null;
|
||||
|
||||
ctx.dom.jimakuEntriesList.innerHTML = "";
|
||||
ctx.dom.jimakuFilesList.innerHTML = "";
|
||||
ctx.dom.jimakuEntriesSection.classList.add("hidden");
|
||||
ctx.dom.jimakuFilesSection.classList.add("hidden");
|
||||
ctx.dom.jimakuBroadenButton.classList.add("hidden");
|
||||
ctx.dom.jimakuEntriesList.innerHTML = '';
|
||||
ctx.dom.jimakuFilesList.innerHTML = '';
|
||||
ctx.dom.jimakuEntriesSection.classList.add('hidden');
|
||||
ctx.dom.jimakuFilesSection.classList.add('hidden');
|
||||
ctx.dom.jimakuBroadenButton.classList.add('hidden');
|
||||
}
|
||||
|
||||
function formatEntryLabel(entry: JimakuEntry): string {
|
||||
@@ -43,29 +43,29 @@ export function createJimakuModal(
|
||||
}
|
||||
|
||||
function renderEntries(): void {
|
||||
ctx.dom.jimakuEntriesList.innerHTML = "";
|
||||
ctx.dom.jimakuEntriesList.innerHTML = '';
|
||||
if (ctx.state.jimakuEntries.length === 0) {
|
||||
ctx.dom.jimakuEntriesSection.classList.add("hidden");
|
||||
ctx.dom.jimakuEntriesSection.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.dom.jimakuEntriesSection.classList.remove("hidden");
|
||||
ctx.dom.jimakuEntriesSection.classList.remove('hidden');
|
||||
ctx.state.jimakuEntries.forEach((entry, index) => {
|
||||
const li = document.createElement("li");
|
||||
const li = document.createElement('li');
|
||||
li.textContent = formatEntryLabel(entry);
|
||||
|
||||
if (entry.japanese_name) {
|
||||
const sub = document.createElement("div");
|
||||
sub.className = "jimaku-subtext";
|
||||
const sub = document.createElement('div');
|
||||
sub.className = 'jimaku-subtext';
|
||||
sub.textContent = entry.japanese_name;
|
||||
li.appendChild(sub);
|
||||
}
|
||||
|
||||
if (index === ctx.state.selectedEntryIndex) {
|
||||
li.classList.add("active");
|
||||
li.classList.add('active');
|
||||
}
|
||||
|
||||
li.addEventListener("click", () => {
|
||||
li.addEventListener('click', () => {
|
||||
selectEntry(index);
|
||||
});
|
||||
|
||||
@@ -74,8 +74,8 @@ export function createJimakuModal(
|
||||
}
|
||||
|
||||
function formatBytes(size: number): string {
|
||||
if (!Number.isFinite(size)) return "";
|
||||
const units = ["B", "KB", "MB", "GB"];
|
||||
if (!Number.isFinite(size)) return '';
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let value = size;
|
||||
let idx = 0;
|
||||
while (value >= 1024 && idx < units.length - 1) {
|
||||
@@ -86,27 +86,27 @@ export function createJimakuModal(
|
||||
}
|
||||
|
||||
function renderFiles(): void {
|
||||
ctx.dom.jimakuFilesList.innerHTML = "";
|
||||
ctx.dom.jimakuFilesList.innerHTML = '';
|
||||
if (ctx.state.jimakuFiles.length === 0) {
|
||||
ctx.dom.jimakuFilesSection.classList.add("hidden");
|
||||
ctx.dom.jimakuFilesSection.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.dom.jimakuFilesSection.classList.remove("hidden");
|
||||
ctx.dom.jimakuFilesSection.classList.remove('hidden');
|
||||
ctx.state.jimakuFiles.forEach((file, index) => {
|
||||
const li = document.createElement("li");
|
||||
const li = document.createElement('li');
|
||||
li.textContent = file.name;
|
||||
|
||||
const sub = document.createElement("div");
|
||||
sub.className = "jimaku-subtext";
|
||||
const sub = document.createElement('div');
|
||||
sub.className = 'jimaku-subtext';
|
||||
sub.textContent = `${formatBytes(file.size)} • ${file.last_modified}`;
|
||||
li.appendChild(sub);
|
||||
|
||||
if (index === ctx.state.selectedFileIndex) {
|
||||
li.classList.add("active");
|
||||
li.classList.add('active');
|
||||
}
|
||||
|
||||
li.addEventListener("click", () => {
|
||||
li.addEventListener('click', () => {
|
||||
void selectFile(index);
|
||||
});
|
||||
|
||||
@@ -125,20 +125,21 @@ export function createJimakuModal(
|
||||
async function performJimakuSearch(): Promise<void> {
|
||||
const { query, episode } = getSearchQuery();
|
||||
if (!query) {
|
||||
setJimakuStatus("Enter a title before searching.", true);
|
||||
setJimakuStatus('Enter a title before searching.', true);
|
||||
return;
|
||||
}
|
||||
|
||||
resetJimakuLists();
|
||||
setJimakuStatus("Searching Jimaku...");
|
||||
setJimakuStatus('Searching Jimaku...');
|
||||
ctx.state.currentEpisodeFilter = episode;
|
||||
|
||||
const response: JimakuApiResponse<JimakuEntry[]> =
|
||||
await window.electronAPI.jimakuSearchEntries({ query });
|
||||
const response: JimakuApiResponse<JimakuEntry[]> = await window.electronAPI.jimakuSearchEntries(
|
||||
{ query },
|
||||
);
|
||||
if (!response.ok) {
|
||||
const retry = response.error.retryAfter
|
||||
? ` Retry after ${response.error.retryAfter.toFixed(1)}s.`
|
||||
: "";
|
||||
: '';
|
||||
setJimakuStatus(`${response.error.error}${retry}`, true);
|
||||
return;
|
||||
}
|
||||
@@ -147,37 +148,35 @@ export function createJimakuModal(
|
||||
ctx.state.selectedEntryIndex = 0;
|
||||
|
||||
if (ctx.state.jimakuEntries.length === 0) {
|
||||
setJimakuStatus("No entries found.");
|
||||
setJimakuStatus('No entries found.');
|
||||
return;
|
||||
}
|
||||
|
||||
setJimakuStatus("Select an entry.");
|
||||
setJimakuStatus('Select an entry.');
|
||||
renderEntries();
|
||||
if (ctx.state.jimakuEntries.length === 1) {
|
||||
void selectEntry(0);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFiles(
|
||||
entryId: number,
|
||||
episode: number | null,
|
||||
): Promise<void> {
|
||||
setJimakuStatus("Loading files...");
|
||||
async function loadFiles(entryId: number, episode: number | null): Promise<void> {
|
||||
setJimakuStatus('Loading files...');
|
||||
ctx.state.jimakuFiles = [];
|
||||
ctx.state.selectedFileIndex = 0;
|
||||
|
||||
ctx.dom.jimakuFilesList.innerHTML = "";
|
||||
ctx.dom.jimakuFilesSection.classList.add("hidden");
|
||||
ctx.dom.jimakuFilesList.innerHTML = '';
|
||||
ctx.dom.jimakuFilesSection.classList.add('hidden');
|
||||
|
||||
const response: JimakuApiResponse<JimakuFileEntry[]> =
|
||||
await window.electronAPI.jimakuListFiles({
|
||||
const response: JimakuApiResponse<JimakuFileEntry[]> = await window.electronAPI.jimakuListFiles(
|
||||
{
|
||||
entryId,
|
||||
episode,
|
||||
});
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
const retry = response.error.retryAfter
|
||||
? ` Retry after ${response.error.retryAfter.toFixed(1)}s.`
|
||||
: "";
|
||||
: '';
|
||||
setJimakuStatus(`${response.error.error}${retry}`, true);
|
||||
return;
|
||||
}
|
||||
@@ -185,16 +184,16 @@ export function createJimakuModal(
|
||||
ctx.state.jimakuFiles = response.data;
|
||||
if (ctx.state.jimakuFiles.length === 0) {
|
||||
if (episode !== null) {
|
||||
setJimakuStatus("No files found for this episode.");
|
||||
ctx.dom.jimakuBroadenButton.classList.remove("hidden");
|
||||
setJimakuStatus('No files found for this episode.');
|
||||
ctx.dom.jimakuBroadenButton.classList.remove('hidden');
|
||||
} else {
|
||||
setJimakuStatus("No files found.");
|
||||
setJimakuStatus('No files found.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.dom.jimakuBroadenButton.classList.add("hidden");
|
||||
setJimakuStatus("Select a subtitle file.");
|
||||
ctx.dom.jimakuBroadenButton.classList.add('hidden');
|
||||
setJimakuStatus('Select a subtitle file.');
|
||||
renderFiles();
|
||||
if (ctx.state.jimakuFiles.length === 1) {
|
||||
await selectFile(0);
|
||||
@@ -220,19 +219,18 @@ export function createJimakuModal(
|
||||
renderFiles();
|
||||
|
||||
if (ctx.state.currentEntryId === null) {
|
||||
setJimakuStatus("Select an entry first.", true);
|
||||
setJimakuStatus('Select an entry first.', true);
|
||||
return;
|
||||
}
|
||||
|
||||
const file = ctx.state.jimakuFiles[index];
|
||||
setJimakuStatus("Downloading subtitle...");
|
||||
setJimakuStatus('Downloading subtitle...');
|
||||
|
||||
const result: JimakuDownloadResult =
|
||||
await window.electronAPI.jimakuDownloadFile({
|
||||
entryId: ctx.state.currentEntryId,
|
||||
url: file.url,
|
||||
name: file.name,
|
||||
});
|
||||
const result: JimakuDownloadResult = await window.electronAPI.jimakuDownloadFile({
|
||||
entryId: ctx.state.currentEntryId,
|
||||
url: file.url,
|
||||
name: file.name,
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
setJimakuStatus(`Downloaded and loaded: ${result.path}`);
|
||||
@@ -241,7 +239,7 @@ export function createJimakuModal(
|
||||
|
||||
const retry = result.error.retryAfter
|
||||
? ` Retry after ${result.error.retryAfter.toFixed(1)}s.`
|
||||
: "";
|
||||
: '';
|
||||
setJimakuStatus(`${result.error.error}${retry}`, true);
|
||||
}
|
||||
|
||||
@@ -249,7 +247,7 @@ export function createJimakuModal(
|
||||
const active = document.activeElement;
|
||||
if (!active) return false;
|
||||
const tag = active.tagName.toLowerCase();
|
||||
return tag === "input" || tag === "textarea";
|
||||
return tag === 'input' || tag === 'textarea';
|
||||
}
|
||||
|
||||
function openJimakuModal(): void {
|
||||
@@ -258,35 +256,31 @@ export function createJimakuModal(
|
||||
|
||||
ctx.state.jimakuModalOpen = true;
|
||||
options.syncSettingsModalSubtitleSuppression();
|
||||
ctx.dom.overlay.classList.add("interactive");
|
||||
ctx.dom.jimakuModal.classList.remove("hidden");
|
||||
ctx.dom.jimakuModal.setAttribute("aria-hidden", "false");
|
||||
ctx.dom.overlay.classList.add('interactive');
|
||||
ctx.dom.jimakuModal.classList.remove('hidden');
|
||||
ctx.dom.jimakuModal.setAttribute('aria-hidden', 'false');
|
||||
|
||||
setJimakuStatus("Loading media info...");
|
||||
setJimakuStatus('Loading media info...');
|
||||
resetJimakuLists();
|
||||
|
||||
window.electronAPI
|
||||
.getJimakuMediaInfo()
|
||||
.then((info: JimakuMediaInfo) => {
|
||||
ctx.dom.jimakuTitleInput.value = info.title || "";
|
||||
ctx.dom.jimakuSeasonInput.value = info.season
|
||||
? String(info.season)
|
||||
: "";
|
||||
ctx.dom.jimakuEpisodeInput.value = info.episode
|
||||
? String(info.episode)
|
||||
: "";
|
||||
ctx.dom.jimakuTitleInput.value = info.title || '';
|
||||
ctx.dom.jimakuSeasonInput.value = info.season ? String(info.season) : '';
|
||||
ctx.dom.jimakuEpisodeInput.value = info.episode ? String(info.episode) : '';
|
||||
ctx.state.currentEpisodeFilter = info.episode ?? null;
|
||||
|
||||
if (info.confidence === "high" && info.title && info.episode) {
|
||||
if (info.confidence === 'high' && info.title && info.episode) {
|
||||
void performJimakuSearch();
|
||||
} else if (info.title) {
|
||||
setJimakuStatus("Check title/season/episode and press Search.");
|
||||
setJimakuStatus('Check title/season/episode and press Search.');
|
||||
} else {
|
||||
setJimakuStatus("Enter title/season/episode and press Search.");
|
||||
setJimakuStatus('Enter title/season/episode and press Search.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setJimakuStatus("Failed to load media info.", true);
|
||||
setJimakuStatus('Failed to load media info.', true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -295,36 +289,33 @@ export function createJimakuModal(
|
||||
|
||||
ctx.state.jimakuModalOpen = false;
|
||||
options.syncSettingsModalSubtitleSuppression();
|
||||
ctx.dom.jimakuModal.classList.add("hidden");
|
||||
ctx.dom.jimakuModal.setAttribute("aria-hidden", "true");
|
||||
window.electronAPI.notifyOverlayModalClosed("jimaku");
|
||||
ctx.dom.jimakuModal.classList.add('hidden');
|
||||
ctx.dom.jimakuModal.setAttribute('aria-hidden', 'true');
|
||||
window.electronAPI.notifyOverlayModalClosed('jimaku');
|
||||
|
||||
if (
|
||||
!ctx.state.isOverSubtitle &&
|
||||
!options.modalStateReader.isAnyModalOpen()
|
||||
) {
|
||||
ctx.dom.overlay.classList.remove("interactive");
|
||||
if (!ctx.state.isOverSubtitle && !options.modalStateReader.isAnyModalOpen()) {
|
||||
ctx.dom.overlay.classList.remove('interactive');
|
||||
}
|
||||
|
||||
resetJimakuLists();
|
||||
}
|
||||
|
||||
function handleJimakuKeydown(e: KeyboardEvent): boolean {
|
||||
if (e.key === "Escape") {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
closeJimakuModal();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isTextInputFocused()) {
|
||||
if (e.key === "Enter") {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
void performJimakuSearch();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e.key === "ArrowDown") {
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
if (ctx.state.jimakuFiles.length > 0) {
|
||||
ctx.state.selectedFileIndex = Math.min(
|
||||
@@ -342,25 +333,19 @@ export function createJimakuModal(
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e.key === "ArrowUp") {
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
if (ctx.state.jimakuFiles.length > 0) {
|
||||
ctx.state.selectedFileIndex = Math.max(
|
||||
0,
|
||||
ctx.state.selectedFileIndex - 1,
|
||||
);
|
||||
ctx.state.selectedFileIndex = Math.max(0, ctx.state.selectedFileIndex - 1);
|
||||
renderFiles();
|
||||
} else if (ctx.state.jimakuEntries.length > 0) {
|
||||
ctx.state.selectedEntryIndex = Math.max(
|
||||
0,
|
||||
ctx.state.selectedEntryIndex - 1,
|
||||
);
|
||||
ctx.state.selectedEntryIndex = Math.max(0, ctx.state.selectedEntryIndex - 1);
|
||||
renderEntries();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e.key === "Enter") {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (ctx.state.jimakuFiles.length > 0) {
|
||||
void selectFile(ctx.state.selectedFileIndex);
|
||||
@@ -376,15 +361,15 @@ export function createJimakuModal(
|
||||
}
|
||||
|
||||
function wireDomEvents(): void {
|
||||
ctx.dom.jimakuSearchButton.addEventListener("click", () => {
|
||||
ctx.dom.jimakuSearchButton.addEventListener('click', () => {
|
||||
void performJimakuSearch();
|
||||
});
|
||||
ctx.dom.jimakuCloseButton.addEventListener("click", () => {
|
||||
ctx.dom.jimakuCloseButton.addEventListener('click', () => {
|
||||
closeJimakuModal();
|
||||
});
|
||||
ctx.dom.jimakuBroadenButton.addEventListener("click", () => {
|
||||
ctx.dom.jimakuBroadenButton.addEventListener('click', () => {
|
||||
if (ctx.state.currentEntryId !== null) {
|
||||
ctx.dom.jimakuBroadenButton.classList.add("hidden");
|
||||
ctx.dom.jimakuBroadenButton.classList.add('hidden');
|
||||
void loadFiles(ctx.state.currentEntryId, null);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,75 +2,64 @@ import type {
|
||||
KikuDuplicateCardInfo,
|
||||
KikuFieldGroupingChoice,
|
||||
KikuMergePreviewResponse,
|
||||
} from "../../types";
|
||||
import type { ModalStateReader, RendererContext } from "../context";
|
||||
} from '../../types';
|
||||
import type { ModalStateReader, RendererContext } from '../context';
|
||||
|
||||
export function createKikuModal(
|
||||
ctx: RendererContext,
|
||||
options: {
|
||||
modalStateReader: Pick<ModalStateReader, "isAnyModalOpen">;
|
||||
modalStateReader: Pick<ModalStateReader, 'isAnyModalOpen'>;
|
||||
syncSettingsModalSubtitleSuppression: () => void;
|
||||
},
|
||||
) {
|
||||
function formatMediaMeta(card: KikuDuplicateCardInfo): string {
|
||||
const parts: string[] = [];
|
||||
parts.push(card.hasAudio ? "Audio: Yes" : "Audio: No");
|
||||
parts.push(card.hasImage ? "Image: Yes" : "Image: No");
|
||||
return parts.join(" | ");
|
||||
parts.push(card.hasAudio ? 'Audio: Yes' : 'Audio: No');
|
||||
parts.push(card.hasImage ? 'Image: Yes' : 'Image: No');
|
||||
return parts.join(' | ');
|
||||
}
|
||||
|
||||
function updateKikuCardSelection(): void {
|
||||
ctx.dom.kikuCard1.classList.toggle(
|
||||
"active",
|
||||
ctx.state.kikuSelectedCard === 1,
|
||||
);
|
||||
ctx.dom.kikuCard2.classList.toggle(
|
||||
"active",
|
||||
ctx.state.kikuSelectedCard === 2,
|
||||
);
|
||||
ctx.dom.kikuCard1.classList.toggle('active', ctx.state.kikuSelectedCard === 1);
|
||||
ctx.dom.kikuCard2.classList.toggle('active', ctx.state.kikuSelectedCard === 2);
|
||||
}
|
||||
|
||||
function setKikuModalStep(step: "select" | "preview"): void {
|
||||
function setKikuModalStep(step: 'select' | 'preview'): void {
|
||||
ctx.state.kikuModalStep = step;
|
||||
const isSelect = step === "select";
|
||||
ctx.dom.kikuSelectionStep.classList.toggle("hidden", !isSelect);
|
||||
ctx.dom.kikuPreviewStep.classList.toggle("hidden", isSelect);
|
||||
const isSelect = step === 'select';
|
||||
ctx.dom.kikuSelectionStep.classList.toggle('hidden', !isSelect);
|
||||
ctx.dom.kikuPreviewStep.classList.toggle('hidden', isSelect);
|
||||
ctx.dom.kikuHint.textContent = isSelect
|
||||
? "Press 1 or 2 to select · Enter to continue · Esc to cancel"
|
||||
: "Enter to confirm merge · Backspace to go back · Esc to cancel";
|
||||
? 'Press 1 or 2 to select · Enter to continue · Esc to cancel'
|
||||
: 'Enter to confirm merge · Backspace to go back · Esc to cancel';
|
||||
}
|
||||
|
||||
function updateKikuPreviewToggle(): void {
|
||||
ctx.dom.kikuPreviewCompactButton.classList.toggle(
|
||||
"active",
|
||||
ctx.state.kikuPreviewMode === "compact",
|
||||
);
|
||||
ctx.dom.kikuPreviewFullButton.classList.toggle(
|
||||
"active",
|
||||
ctx.state.kikuPreviewMode === "full",
|
||||
'active',
|
||||
ctx.state.kikuPreviewMode === 'compact',
|
||||
);
|
||||
ctx.dom.kikuPreviewFullButton.classList.toggle('active', ctx.state.kikuPreviewMode === 'full');
|
||||
}
|
||||
|
||||
function renderKikuPreview(): void {
|
||||
const payload =
|
||||
ctx.state.kikuPreviewMode === "compact"
|
||||
ctx.state.kikuPreviewMode === 'compact'
|
||||
? ctx.state.kikuPreviewCompactData
|
||||
: ctx.state.kikuPreviewFullData;
|
||||
ctx.dom.kikuPreviewJson.textContent = payload
|
||||
? JSON.stringify(payload, null, 2)
|
||||
: "{}";
|
||||
ctx.dom.kikuPreviewJson.textContent = payload ? JSON.stringify(payload, null, 2) : '{}';
|
||||
updateKikuPreviewToggle();
|
||||
}
|
||||
|
||||
function setKikuPreviewError(message: string | null): void {
|
||||
if (!message) {
|
||||
ctx.dom.kikuPreviewError.textContent = "";
|
||||
ctx.dom.kikuPreviewError.classList.add("hidden");
|
||||
ctx.dom.kikuPreviewError.textContent = '';
|
||||
ctx.dom.kikuPreviewError.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.dom.kikuPreviewError.textContent = message;
|
||||
ctx.dom.kikuPreviewError.classList.remove("hidden");
|
||||
ctx.dom.kikuPreviewError.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function openKikuFieldGroupingModal(data: {
|
||||
@@ -86,30 +75,28 @@ export function createKikuModal(
|
||||
ctx.state.kikuSelectedCard = 1;
|
||||
|
||||
ctx.dom.kikuCard1Expression.textContent = data.original.expression;
|
||||
ctx.dom.kikuCard1Sentence.textContent =
|
||||
data.original.sentencePreview || "(no sentence)";
|
||||
ctx.dom.kikuCard1Sentence.textContent = data.original.sentencePreview || '(no sentence)';
|
||||
ctx.dom.kikuCard1Meta.textContent = formatMediaMeta(data.original);
|
||||
|
||||
ctx.dom.kikuCard2Expression.textContent = data.duplicate.expression;
|
||||
ctx.dom.kikuCard2Sentence.textContent =
|
||||
data.duplicate.sentencePreview || "(current subtitle)";
|
||||
ctx.dom.kikuCard2Sentence.textContent = data.duplicate.sentencePreview || '(current subtitle)';
|
||||
ctx.dom.kikuCard2Meta.textContent = formatMediaMeta(data.duplicate);
|
||||
|
||||
ctx.dom.kikuDeleteDuplicateCheckbox.checked = true;
|
||||
ctx.state.kikuPendingChoice = null;
|
||||
ctx.state.kikuPreviewCompactData = null;
|
||||
ctx.state.kikuPreviewFullData = null;
|
||||
ctx.state.kikuPreviewMode = "compact";
|
||||
ctx.state.kikuPreviewMode = 'compact';
|
||||
|
||||
renderKikuPreview();
|
||||
setKikuPreviewError(null);
|
||||
setKikuModalStep("select");
|
||||
setKikuModalStep('select');
|
||||
updateKikuCardSelection();
|
||||
|
||||
options.syncSettingsModalSubtitleSuppression();
|
||||
ctx.dom.overlay.classList.add("interactive");
|
||||
ctx.dom.kikuModal.classList.remove("hidden");
|
||||
ctx.dom.kikuModal.setAttribute("aria-hidden", "false");
|
||||
ctx.dom.overlay.classList.add('interactive');
|
||||
ctx.dom.kikuModal.classList.remove('hidden');
|
||||
ctx.dom.kikuModal.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
|
||||
function closeKikuFieldGroupingModal(): void {
|
||||
@@ -118,25 +105,22 @@ export function createKikuModal(
|
||||
ctx.state.kikuModalOpen = false;
|
||||
options.syncSettingsModalSubtitleSuppression();
|
||||
|
||||
ctx.dom.kikuModal.classList.add("hidden");
|
||||
ctx.dom.kikuModal.setAttribute("aria-hidden", "true");
|
||||
ctx.dom.kikuModal.classList.add('hidden');
|
||||
ctx.dom.kikuModal.setAttribute('aria-hidden', 'true');
|
||||
|
||||
setKikuPreviewError(null);
|
||||
ctx.dom.kikuPreviewJson.textContent = "";
|
||||
ctx.dom.kikuPreviewJson.textContent = '';
|
||||
|
||||
ctx.state.kikuPendingChoice = null;
|
||||
ctx.state.kikuPreviewCompactData = null;
|
||||
ctx.state.kikuPreviewFullData = null;
|
||||
ctx.state.kikuPreviewMode = "compact";
|
||||
setKikuModalStep("select");
|
||||
ctx.state.kikuPreviewMode = 'compact';
|
||||
setKikuModalStep('select');
|
||||
ctx.state.kikuOriginalData = null;
|
||||
ctx.state.kikuDuplicateData = null;
|
||||
|
||||
if (
|
||||
!ctx.state.isOverSubtitle &&
|
||||
!options.modalStateReader.isAnyModalOpen()
|
||||
) {
|
||||
ctx.dom.overlay.classList.remove("interactive");
|
||||
if (!ctx.state.isOverSubtitle && !options.modalStateReader.isAnyModalOpen()) {
|
||||
ctx.dom.overlay.classList.remove('interactive');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,13 +128,9 @@ export function createKikuModal(
|
||||
if (!ctx.state.kikuOriginalData || !ctx.state.kikuDuplicateData) return;
|
||||
|
||||
const keepData =
|
||||
ctx.state.kikuSelectedCard === 1
|
||||
? ctx.state.kikuOriginalData
|
||||
: ctx.state.kikuDuplicateData;
|
||||
ctx.state.kikuSelectedCard === 1 ? ctx.state.kikuOriginalData : ctx.state.kikuDuplicateData;
|
||||
const deleteData =
|
||||
ctx.state.kikuSelectedCard === 1
|
||||
? ctx.state.kikuDuplicateData
|
||||
: ctx.state.kikuOriginalData;
|
||||
ctx.state.kikuSelectedCard === 1 ? ctx.state.kikuDuplicateData : ctx.state.kikuOriginalData;
|
||||
|
||||
const choice: KikuFieldGroupingChoice = {
|
||||
keepNoteId: keepData.noteId,
|
||||
@@ -164,23 +144,22 @@ export function createKikuModal(
|
||||
ctx.dom.kikuConfirmButton.disabled = true;
|
||||
|
||||
try {
|
||||
const preview: KikuMergePreviewResponse =
|
||||
await window.electronAPI.kikuBuildMergePreview({
|
||||
keepNoteId: choice.keepNoteId,
|
||||
deleteNoteId: choice.deleteNoteId,
|
||||
deleteDuplicate: choice.deleteDuplicate,
|
||||
});
|
||||
const preview: KikuMergePreviewResponse = await window.electronAPI.kikuBuildMergePreview({
|
||||
keepNoteId: choice.keepNoteId,
|
||||
deleteNoteId: choice.deleteNoteId,
|
||||
deleteDuplicate: choice.deleteDuplicate,
|
||||
});
|
||||
|
||||
if (!preview.ok) {
|
||||
setKikuPreviewError(preview.error || "Failed to build merge preview");
|
||||
setKikuPreviewError(preview.error || 'Failed to build merge preview');
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.state.kikuPreviewCompactData = preview.compact || {};
|
||||
ctx.state.kikuPreviewFullData = preview.full || {};
|
||||
ctx.state.kikuPreviewMode = "compact";
|
||||
ctx.state.kikuPreviewMode = 'compact';
|
||||
renderKikuPreview();
|
||||
setKikuModalStep("preview");
|
||||
setKikuModalStep('preview');
|
||||
} finally {
|
||||
ctx.dom.kikuConfirmButton.disabled = false;
|
||||
}
|
||||
@@ -194,7 +173,7 @@ export function createKikuModal(
|
||||
|
||||
function goBackFromKikuPreview(): void {
|
||||
setKikuPreviewError(null);
|
||||
setKikuModalStep("select");
|
||||
setKikuModalStep('select');
|
||||
}
|
||||
|
||||
function cancelKikuFieldGrouping(): void {
|
||||
@@ -210,18 +189,18 @@ export function createKikuModal(
|
||||
}
|
||||
|
||||
function handleKikuKeydown(e: KeyboardEvent): boolean {
|
||||
if (ctx.state.kikuModalStep === "preview") {
|
||||
if (e.key === "Escape") {
|
||||
if (ctx.state.kikuModalStep === 'preview') {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
cancelKikuFieldGrouping();
|
||||
return true;
|
||||
}
|
||||
if (e.key === "Backspace") {
|
||||
if (e.key === 'Backspace') {
|
||||
e.preventDefault();
|
||||
goBackFromKikuPreview();
|
||||
return true;
|
||||
}
|
||||
if (e.key === "Enter") {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
confirmKikuMerge();
|
||||
return true;
|
||||
@@ -229,34 +208,34 @@ export function createKikuModal(
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e.key === "Escape") {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
cancelKikuFieldGrouping();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e.key === "1") {
|
||||
if (e.key === '1') {
|
||||
e.preventDefault();
|
||||
ctx.state.kikuSelectedCard = 1;
|
||||
updateKikuCardSelection();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e.key === "2") {
|
||||
if (e.key === '2') {
|
||||
e.preventDefault();
|
||||
ctx.state.kikuSelectedCard = 2;
|
||||
updateKikuCardSelection();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
ctx.state.kikuSelectedCard = ctx.state.kikuSelectedCard === 1 ? 2 : 1;
|
||||
updateKikuCardSelection();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e.key === "Enter") {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
void confirmKikuSelection();
|
||||
return true;
|
||||
@@ -266,46 +245,46 @@ export function createKikuModal(
|
||||
}
|
||||
|
||||
function wireDomEvents(): void {
|
||||
ctx.dom.kikuCard1.addEventListener("click", () => {
|
||||
ctx.dom.kikuCard1.addEventListener('click', () => {
|
||||
ctx.state.kikuSelectedCard = 1;
|
||||
updateKikuCardSelection();
|
||||
});
|
||||
ctx.dom.kikuCard1.addEventListener("dblclick", () => {
|
||||
ctx.dom.kikuCard1.addEventListener('dblclick', () => {
|
||||
ctx.state.kikuSelectedCard = 1;
|
||||
void confirmKikuSelection();
|
||||
});
|
||||
|
||||
ctx.dom.kikuCard2.addEventListener("click", () => {
|
||||
ctx.dom.kikuCard2.addEventListener('click', () => {
|
||||
ctx.state.kikuSelectedCard = 2;
|
||||
updateKikuCardSelection();
|
||||
});
|
||||
ctx.dom.kikuCard2.addEventListener("dblclick", () => {
|
||||
ctx.dom.kikuCard2.addEventListener('dblclick', () => {
|
||||
ctx.state.kikuSelectedCard = 2;
|
||||
void confirmKikuSelection();
|
||||
});
|
||||
|
||||
ctx.dom.kikuConfirmButton.addEventListener("click", () => {
|
||||
ctx.dom.kikuConfirmButton.addEventListener('click', () => {
|
||||
void confirmKikuSelection();
|
||||
});
|
||||
ctx.dom.kikuCancelButton.addEventListener("click", () => {
|
||||
ctx.dom.kikuCancelButton.addEventListener('click', () => {
|
||||
cancelKikuFieldGrouping();
|
||||
});
|
||||
ctx.dom.kikuBackButton.addEventListener("click", () => {
|
||||
ctx.dom.kikuBackButton.addEventListener('click', () => {
|
||||
goBackFromKikuPreview();
|
||||
});
|
||||
ctx.dom.kikuFinalConfirmButton.addEventListener("click", () => {
|
||||
ctx.dom.kikuFinalConfirmButton.addEventListener('click', () => {
|
||||
confirmKikuMerge();
|
||||
});
|
||||
ctx.dom.kikuFinalCancelButton.addEventListener("click", () => {
|
||||
ctx.dom.kikuFinalCancelButton.addEventListener('click', () => {
|
||||
cancelKikuFieldGrouping();
|
||||
});
|
||||
|
||||
ctx.dom.kikuPreviewCompactButton.addEventListener("click", () => {
|
||||
ctx.state.kikuPreviewMode = "compact";
|
||||
ctx.dom.kikuPreviewCompactButton.addEventListener('click', () => {
|
||||
ctx.state.kikuPreviewMode = 'compact';
|
||||
renderKikuPreview();
|
||||
});
|
||||
ctx.dom.kikuPreviewFullButton.addEventListener("click", () => {
|
||||
ctx.state.kikuPreviewMode = "full";
|
||||
ctx.dom.kikuPreviewFullButton.addEventListener('click', () => {
|
||||
ctx.state.kikuPreviewMode = 'full';
|
||||
renderKikuPreview();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,90 +1,79 @@
|
||||
import type {
|
||||
RuntimeOptionApplyResult,
|
||||
RuntimeOptionState,
|
||||
RuntimeOptionValue,
|
||||
} from "../../types";
|
||||
import type { ModalStateReader, RendererContext } from "../context";
|
||||
import type { RuntimeOptionApplyResult, RuntimeOptionState, RuntimeOptionValue } from '../../types';
|
||||
import type { ModalStateReader, RendererContext } from '../context';
|
||||
|
||||
export function createRuntimeOptionsModal(
|
||||
ctx: RendererContext,
|
||||
options: {
|
||||
modalStateReader: Pick<ModalStateReader, "isAnyModalOpen">;
|
||||
modalStateReader: Pick<ModalStateReader, 'isAnyModalOpen'>;
|
||||
syncSettingsModalSubtitleSuppression: () => void;
|
||||
},
|
||||
) {
|
||||
function formatRuntimeOptionValue(value: RuntimeOptionValue): string {
|
||||
if (typeof value === "boolean") {
|
||||
return value ? "On" : "Off";
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? 'On' : 'Off';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function setRuntimeOptionsStatus(message: string, isError = false): void {
|
||||
ctx.dom.runtimeOptionsStatus.textContent = message;
|
||||
ctx.dom.runtimeOptionsStatus.classList.toggle("error", isError);
|
||||
ctx.dom.runtimeOptionsStatus.classList.toggle('error', isError);
|
||||
}
|
||||
|
||||
function getRuntimeOptionDisplayValue(
|
||||
option: RuntimeOptionState,
|
||||
): RuntimeOptionValue {
|
||||
function getRuntimeOptionDisplayValue(option: RuntimeOptionState): RuntimeOptionValue {
|
||||
return ctx.state.runtimeOptionDraftValues.get(option.id) ?? option.value;
|
||||
}
|
||||
|
||||
function getSelectedRuntimeOption(): RuntimeOptionState | null {
|
||||
if (ctx.state.runtimeOptions.length === 0) return null;
|
||||
if (ctx.state.runtimeOptionSelectedIndex < 0) return null;
|
||||
if (
|
||||
ctx.state.runtimeOptionSelectedIndex >= ctx.state.runtimeOptions.length
|
||||
) {
|
||||
if (ctx.state.runtimeOptionSelectedIndex >= ctx.state.runtimeOptions.length) {
|
||||
return null;
|
||||
}
|
||||
return ctx.state.runtimeOptions[ctx.state.runtimeOptionSelectedIndex];
|
||||
}
|
||||
|
||||
function renderRuntimeOptionsList(): void {
|
||||
ctx.dom.runtimeOptionsList.innerHTML = "";
|
||||
ctx.dom.runtimeOptionsList.innerHTML = '';
|
||||
ctx.state.runtimeOptions.forEach((option, index) => {
|
||||
const li = document.createElement("li");
|
||||
li.className = "runtime-options-item";
|
||||
li.classList.toggle(
|
||||
"active",
|
||||
index === ctx.state.runtimeOptionSelectedIndex,
|
||||
);
|
||||
const li = document.createElement('li');
|
||||
li.className = 'runtime-options-item';
|
||||
li.classList.toggle('active', index === ctx.state.runtimeOptionSelectedIndex);
|
||||
|
||||
const label = document.createElement("div");
|
||||
label.className = "runtime-options-label";
|
||||
const label = document.createElement('div');
|
||||
label.className = 'runtime-options-label';
|
||||
label.textContent = option.label;
|
||||
|
||||
const value = document.createElement("div");
|
||||
value.className = "runtime-options-value";
|
||||
const value = document.createElement('div');
|
||||
value.className = 'runtime-options-value';
|
||||
value.textContent = `Value: ${formatRuntimeOptionValue(getRuntimeOptionDisplayValue(option))}`;
|
||||
value.title = "Click to cycle value, right-click to cycle backward";
|
||||
value.title = 'Click to cycle value, right-click to cycle backward';
|
||||
|
||||
const allowed = document.createElement("div");
|
||||
allowed.className = "runtime-options-allowed";
|
||||
const allowed = document.createElement('div');
|
||||
allowed.className = 'runtime-options-allowed';
|
||||
allowed.textContent = `Allowed: ${option.allowedValues
|
||||
.map((entry) => formatRuntimeOptionValue(entry))
|
||||
.join(" | ")}`;
|
||||
.join(' | ')}`;
|
||||
|
||||
li.appendChild(label);
|
||||
li.appendChild(value);
|
||||
li.appendChild(allowed);
|
||||
|
||||
li.addEventListener("click", () => {
|
||||
li.addEventListener('click', () => {
|
||||
ctx.state.runtimeOptionSelectedIndex = index;
|
||||
renderRuntimeOptionsList();
|
||||
});
|
||||
li.addEventListener("dblclick", () => {
|
||||
li.addEventListener('dblclick', () => {
|
||||
ctx.state.runtimeOptionSelectedIndex = index;
|
||||
void applySelectedRuntimeOption();
|
||||
});
|
||||
|
||||
value.addEventListener("click", (event) => {
|
||||
value.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
ctx.state.runtimeOptionSelectedIndex = index;
|
||||
cycleRuntimeDraftValue(1);
|
||||
});
|
||||
value.addEventListener("contextmenu", (event) => {
|
||||
value.addEventListener('contextmenu', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
ctx.state.runtimeOptionSelectedIndex = index;
|
||||
@@ -107,9 +96,7 @@ export function createRuntimeOptionsModal(
|
||||
ctx.state.runtimeOptionDraftValues.set(option.id, option.value);
|
||||
}
|
||||
|
||||
const nextIndex = ctx.state.runtimeOptions.findIndex(
|
||||
(option) => option.id === previousId,
|
||||
);
|
||||
const nextIndex = ctx.state.runtimeOptions.findIndex((option) => option.id === previousId);
|
||||
ctx.state.runtimeOptionSelectedIndex = nextIndex >= 0 ? nextIndex : 0;
|
||||
|
||||
renderRuntimeOptionsList();
|
||||
@@ -120,20 +107,14 @@ export function createRuntimeOptionsModal(
|
||||
if (!option || option.allowedValues.length === 0) return;
|
||||
|
||||
const currentValue = getRuntimeOptionDisplayValue(option);
|
||||
const currentIndex = option.allowedValues.findIndex(
|
||||
(value) => value === currentValue,
|
||||
);
|
||||
const currentIndex = option.allowedValues.findIndex((value) => value === currentValue);
|
||||
const safeIndex = currentIndex >= 0 ? currentIndex : 0;
|
||||
const nextIndex =
|
||||
direction === 1
|
||||
? (safeIndex + 1) % option.allowedValues.length
|
||||
: (safeIndex - 1 + option.allowedValues.length) %
|
||||
option.allowedValues.length;
|
||||
: (safeIndex - 1 + option.allowedValues.length) % option.allowedValues.length;
|
||||
|
||||
ctx.state.runtimeOptionDraftValues.set(
|
||||
option.id,
|
||||
option.allowedValues[nextIndex],
|
||||
);
|
||||
ctx.state.runtimeOptionDraftValues.set(option.id, option.allowedValues[nextIndex]);
|
||||
renderRuntimeOptionsList();
|
||||
setRuntimeOptionsStatus(
|
||||
`Selected ${option.label}: ${formatRuntimeOptionValue(option.allowedValues[nextIndex])}`,
|
||||
@@ -145,23 +126,22 @@ export function createRuntimeOptionsModal(
|
||||
if (!option) return;
|
||||
|
||||
const nextValue = getRuntimeOptionDisplayValue(option);
|
||||
const result: RuntimeOptionApplyResult =
|
||||
await window.electronAPI.setRuntimeOptionValue(option.id, nextValue);
|
||||
const result: RuntimeOptionApplyResult = await window.electronAPI.setRuntimeOptionValue(
|
||||
option.id,
|
||||
nextValue,
|
||||
);
|
||||
if (!result.ok) {
|
||||
setRuntimeOptionsStatus(result.error || "Failed to apply option", true);
|
||||
setRuntimeOptionsStatus(result.error || 'Failed to apply option', true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.option) {
|
||||
ctx.state.runtimeOptionDraftValues.set(
|
||||
result.option.id,
|
||||
result.option.value,
|
||||
);
|
||||
ctx.state.runtimeOptionDraftValues.set(result.option.id, result.option.value);
|
||||
}
|
||||
|
||||
const latest = await window.electronAPI.getRuntimeOptions();
|
||||
updateRuntimeOptions(latest);
|
||||
setRuntimeOptionsStatus(result.osdMessage || "Option applied.");
|
||||
setRuntimeOptionsStatus(result.osdMessage || 'Option applied.');
|
||||
}
|
||||
|
||||
function closeRuntimeOptionsModal(): void {
|
||||
@@ -170,17 +150,14 @@ export function createRuntimeOptionsModal(
|
||||
ctx.state.runtimeOptionsModalOpen = false;
|
||||
options.syncSettingsModalSubtitleSuppression();
|
||||
|
||||
ctx.dom.runtimeOptionsModal.classList.add("hidden");
|
||||
ctx.dom.runtimeOptionsModal.setAttribute("aria-hidden", "true");
|
||||
window.electronAPI.notifyOverlayModalClosed("runtime-options");
|
||||
ctx.dom.runtimeOptionsModal.classList.add('hidden');
|
||||
ctx.dom.runtimeOptionsModal.setAttribute('aria-hidden', 'true');
|
||||
window.electronAPI.notifyOverlayModalClosed('runtime-options');
|
||||
|
||||
setRuntimeOptionsStatus("");
|
||||
setRuntimeOptionsStatus('');
|
||||
|
||||
if (
|
||||
!ctx.state.isOverSubtitle &&
|
||||
!options.modalStateReader.isAnyModalOpen()
|
||||
) {
|
||||
ctx.dom.overlay.classList.remove("interactive");
|
||||
if (!ctx.state.isOverSubtitle && !options.modalStateReader.isAnyModalOpen()) {
|
||||
ctx.dom.overlay.classList.remove('interactive');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,27 +170,27 @@ export function createRuntimeOptionsModal(
|
||||
ctx.state.runtimeOptionsModalOpen = true;
|
||||
options.syncSettingsModalSubtitleSuppression();
|
||||
|
||||
ctx.dom.overlay.classList.add("interactive");
|
||||
ctx.dom.runtimeOptionsModal.classList.remove("hidden");
|
||||
ctx.dom.runtimeOptionsModal.setAttribute("aria-hidden", "false");
|
||||
ctx.dom.overlay.classList.add('interactive');
|
||||
ctx.dom.runtimeOptionsModal.classList.remove('hidden');
|
||||
ctx.dom.runtimeOptionsModal.setAttribute('aria-hidden', 'false');
|
||||
|
||||
setRuntimeOptionsStatus(
|
||||
"Use arrow keys. Click value to cycle. Enter or double-click to apply.",
|
||||
'Use arrow keys. Click value to cycle. Enter or double-click to apply.',
|
||||
);
|
||||
}
|
||||
|
||||
function handleRuntimeOptionsKeydown(e: KeyboardEvent): boolean {
|
||||
if (e.key === "Escape") {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
closeRuntimeOptionsModal();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
e.key === "ArrowDown" ||
|
||||
e.key === "j" ||
|
||||
e.key === "J" ||
|
||||
(e.ctrlKey && (e.key === "n" || e.key === "N"))
|
||||
e.key === 'ArrowDown' ||
|
||||
e.key === 'j' ||
|
||||
e.key === 'J' ||
|
||||
(e.ctrlKey && (e.key === 'n' || e.key === 'N'))
|
||||
) {
|
||||
e.preventDefault();
|
||||
if (ctx.state.runtimeOptions.length > 0) {
|
||||
@@ -227,10 +204,10 @@ export function createRuntimeOptionsModal(
|
||||
}
|
||||
|
||||
if (
|
||||
e.key === "ArrowUp" ||
|
||||
e.key === "k" ||
|
||||
e.key === "K" ||
|
||||
(e.ctrlKey && (e.key === "p" || e.key === "P"))
|
||||
e.key === 'ArrowUp' ||
|
||||
e.key === 'k' ||
|
||||
e.key === 'K' ||
|
||||
(e.ctrlKey && (e.key === 'p' || e.key === 'P'))
|
||||
) {
|
||||
e.preventDefault();
|
||||
if (ctx.state.runtimeOptions.length > 0) {
|
||||
@@ -243,19 +220,19 @@ export function createRuntimeOptionsModal(
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e.key === "ArrowRight" || e.key === "l" || e.key === "L") {
|
||||
if (e.key === 'ArrowRight' || e.key === 'l' || e.key === 'L') {
|
||||
e.preventDefault();
|
||||
cycleRuntimeDraftValue(1);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e.key === "ArrowLeft" || e.key === "h" || e.key === "H") {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'h' || e.key === 'H') {
|
||||
e.preventDefault();
|
||||
cycleRuntimeDraftValue(-1);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e.key === "Enter") {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
void applySelectedRuntimeOption();
|
||||
return true;
|
||||
@@ -265,7 +242,7 @@ export function createRuntimeOptionsModal(
|
||||
}
|
||||
|
||||
function wireDomEvents(): void {
|
||||
ctx.dom.runtimeOptionsClose.addEventListener("click", () => {
|
||||
ctx.dom.runtimeOptionsClose.addEventListener('click', () => {
|
||||
closeRuntimeOptionsModal();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Keybinding } from "../../types";
|
||||
import type { ShortcutsConfig } from "../../types";
|
||||
import { SPECIAL_COMMANDS } from "../../config/definitions";
|
||||
import type { ModalStateReader, RendererContext } from "../context";
|
||||
import type { Keybinding } from '../../types';
|
||||
import type { ShortcutsConfig } from '../../types';
|
||||
import { SPECIAL_COMMANDS } from '../../config/definitions';
|
||||
import type { ModalStateReader, RendererContext } from '../context';
|
||||
|
||||
type SessionHelpBindingInfo = {
|
||||
bindingKey: "KeyH" | "KeyK";
|
||||
bindingKey: 'KeyH' | 'KeyK';
|
||||
fallbackUsed: boolean;
|
||||
fallbackUnavailable: boolean;
|
||||
};
|
||||
@@ -19,98 +19,92 @@ type SessionHelpSection = {
|
||||
title: string;
|
||||
rows: SessionHelpItem[];
|
||||
};
|
||||
type RuntimeShortcutConfig = Omit<
|
||||
Required<ShortcutsConfig>,
|
||||
"multiCopyTimeoutMs"
|
||||
>;
|
||||
type RuntimeShortcutConfig = Omit<Required<ShortcutsConfig>, 'multiCopyTimeoutMs'>;
|
||||
|
||||
const HEX_COLOR_RE =
|
||||
/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
||||
const HEX_COLOR_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
||||
|
||||
// Fallbacks mirror the session overlay's default subtitle/word color scheme.
|
||||
const FALLBACK_COLORS = {
|
||||
knownWordColor: "#a6da95",
|
||||
nPlusOneColor: "#c6a0f6",
|
||||
jlptN1Color: "#ed8796",
|
||||
jlptN2Color: "#f5a97f",
|
||||
jlptN3Color: "#f9e2af",
|
||||
jlptN4Color: "#a6e3a1",
|
||||
jlptN5Color: "#8aadf4",
|
||||
knownWordColor: '#a6da95',
|
||||
nPlusOneColor: '#c6a0f6',
|
||||
jlptN1Color: '#ed8796',
|
||||
jlptN2Color: '#f5a97f',
|
||||
jlptN3Color: '#f9e2af',
|
||||
jlptN4Color: '#a6e3a1',
|
||||
jlptN5Color: '#8aadf4',
|
||||
};
|
||||
|
||||
const KEY_NAME_MAP: Record<string, string> = {
|
||||
Space: "Space",
|
||||
ArrowUp: "↑",
|
||||
ArrowDown: "↓",
|
||||
ArrowLeft: "←",
|
||||
ArrowRight: "→",
|
||||
Escape: "Esc",
|
||||
Tab: "Tab",
|
||||
Enter: "Enter",
|
||||
CommandOrControl: "Cmd/Ctrl",
|
||||
Ctrl: "Ctrl",
|
||||
Control: "Ctrl",
|
||||
Command: "Cmd",
|
||||
Cmd: "Cmd",
|
||||
Shift: "Shift",
|
||||
Alt: "Alt",
|
||||
Super: "Meta",
|
||||
Meta: "Meta",
|
||||
Backspace: "Backspace",
|
||||
Space: 'Space',
|
||||
ArrowUp: '↑',
|
||||
ArrowDown: '↓',
|
||||
ArrowLeft: '←',
|
||||
ArrowRight: '→',
|
||||
Escape: 'Esc',
|
||||
Tab: 'Tab',
|
||||
Enter: 'Enter',
|
||||
CommandOrControl: 'Cmd/Ctrl',
|
||||
Ctrl: 'Ctrl',
|
||||
Control: 'Ctrl',
|
||||
Command: 'Cmd',
|
||||
Cmd: 'Cmd',
|
||||
Shift: 'Shift',
|
||||
Alt: 'Alt',
|
||||
Super: 'Meta',
|
||||
Meta: 'Meta',
|
||||
Backspace: 'Backspace',
|
||||
};
|
||||
|
||||
function normalizeColor(value: unknown, fallback: string): string {
|
||||
if (typeof value !== "string") return fallback;
|
||||
if (typeof value !== 'string') return fallback;
|
||||
const next = value.trim();
|
||||
return HEX_COLOR_RE.test(next) ? next : fallback;
|
||||
}
|
||||
|
||||
function normalizeKeyToken(token: string): string {
|
||||
if (KEY_NAME_MAP[token]) return KEY_NAME_MAP[token];
|
||||
if (token.startsWith("Key")) return token.slice(3);
|
||||
if (token.startsWith("Digit")) return token.slice(5);
|
||||
if (token.startsWith("Numpad")) return token.slice(6);
|
||||
if (token.startsWith('Key')) return token.slice(3);
|
||||
if (token.startsWith('Digit')) return token.slice(5);
|
||||
if (token.startsWith('Numpad')) return token.slice(6);
|
||||
return token;
|
||||
}
|
||||
|
||||
function formatKeybinding(rawBinding: string): string {
|
||||
const parts = rawBinding.split("+");
|
||||
const parts = rawBinding.split('+');
|
||||
const key = parts.pop();
|
||||
if (!key) return rawBinding;
|
||||
const normalized = [...parts, normalizeKeyToken(key)];
|
||||
return normalized.join(" + ");
|
||||
return normalized.join(' + ');
|
||||
}
|
||||
|
||||
const OVERLAY_SHORTCUTS: Array<{
|
||||
key: keyof RuntimeShortcutConfig;
|
||||
label: string;
|
||||
}> = [
|
||||
{ key: "copySubtitle", label: "Copy subtitle" },
|
||||
{ key: "copySubtitleMultiple", label: "Copy subtitle (multi)" },
|
||||
{ key: 'copySubtitle', label: 'Copy subtitle' },
|
||||
{ key: 'copySubtitleMultiple', label: 'Copy subtitle (multi)' },
|
||||
{
|
||||
key: "updateLastCardFromClipboard",
|
||||
label: "Update last card from clipboard",
|
||||
key: 'updateLastCardFromClipboard',
|
||||
label: 'Update last card from clipboard',
|
||||
},
|
||||
{ key: "triggerFieldGrouping", label: "Trigger field grouping" },
|
||||
{ key: "triggerSubsync", label: "Open subtitle sync controls" },
|
||||
{ key: "mineSentence", label: "Mine sentence" },
|
||||
{ key: "mineSentenceMultiple", label: "Mine sentence (multi)" },
|
||||
{ key: "toggleSecondarySub", label: "Toggle secondary subtitle mode" },
|
||||
{ key: "markAudioCard", label: "Mark audio card" },
|
||||
{ key: "openRuntimeOptions", label: "Open runtime options" },
|
||||
{ key: "openJimaku", label: "Open jimaku" },
|
||||
{ key: "toggleVisibleOverlayGlobal", label: "Show/hide visible overlay" },
|
||||
{ key: "toggleInvisibleOverlayGlobal", label: "Show/hide invisible overlay" },
|
||||
{ key: 'triggerFieldGrouping', label: 'Trigger field grouping' },
|
||||
{ key: 'triggerSubsync', label: 'Open subtitle sync controls' },
|
||||
{ key: 'mineSentence', label: 'Mine sentence' },
|
||||
{ key: 'mineSentenceMultiple', label: 'Mine sentence (multi)' },
|
||||
{ key: 'toggleSecondarySub', label: 'Toggle secondary subtitle mode' },
|
||||
{ key: 'markAudioCard', label: 'Mark audio card' },
|
||||
{ key: 'openRuntimeOptions', label: 'Open runtime options' },
|
||||
{ key: 'openJimaku', label: 'Open jimaku' },
|
||||
{ key: 'toggleVisibleOverlayGlobal', label: 'Show/hide visible overlay' },
|
||||
{ key: 'toggleInvisibleOverlayGlobal', label: 'Show/hide invisible overlay' },
|
||||
];
|
||||
|
||||
function buildOverlayShortcutSections(
|
||||
shortcuts: RuntimeShortcutConfig,
|
||||
): SessionHelpSection[] {
|
||||
function buildOverlayShortcutSections(shortcuts: RuntimeShortcutConfig): SessionHelpSection[] {
|
||||
const rows: SessionHelpItem[] = [];
|
||||
|
||||
for (const shortcut of OVERLAY_SHORTCUTS) {
|
||||
const keybind = shortcuts[shortcut.key];
|
||||
if (typeof keybind !== "string") continue;
|
||||
if (typeof keybind !== 'string') continue;
|
||||
if (keybind.trim().length === 0) continue;
|
||||
|
||||
rows.push({
|
||||
@@ -120,71 +114,63 @@ function buildOverlayShortcutSections(
|
||||
}
|
||||
|
||||
if (rows.length === 0) return [];
|
||||
return [{ title: "Overlay shortcuts", rows }];
|
||||
return [{ title: 'Overlay shortcuts', rows }];
|
||||
}
|
||||
|
||||
function describeCommand(command: (string | number)[]): string {
|
||||
const first = command[0];
|
||||
if (typeof first !== "string") return "Unknown action";
|
||||
if (typeof first !== 'string') return 'Unknown action';
|
||||
|
||||
if (first === "cycle" && command[1] === "pause") return "Toggle playback";
|
||||
if (first === "seek" && typeof command[1] === "number") {
|
||||
return `Seek ${command[1] > 0 ? "+" : ""}${command[1]} second(s)`;
|
||||
if (first === 'cycle' && command[1] === 'pause') return 'Toggle playback';
|
||||
if (first === 'seek' && typeof command[1] === 'number') {
|
||||
return `Seek ${command[1] > 0 ? '+' : ''}${command[1]} second(s)`;
|
||||
}
|
||||
if (first === "sub-seek" && typeof command[1] === "number") {
|
||||
if (first === 'sub-seek' && typeof command[1] === 'number') {
|
||||
return `Shift subtitle by ${command[1]} ms`;
|
||||
}
|
||||
if (first === SPECIAL_COMMANDS.SUBSYNC_TRIGGER)
|
||||
return "Open subtitle sync controls";
|
||||
if (first === SPECIAL_COMMANDS.RUNTIME_OPTIONS_OPEN)
|
||||
return "Open runtime options";
|
||||
if (first === SPECIAL_COMMANDS.REPLAY_SUBTITLE)
|
||||
return "Replay current subtitle";
|
||||
if (first === SPECIAL_COMMANDS.PLAY_NEXT_SUBTITLE)
|
||||
return "Play next subtitle";
|
||||
if (first === SPECIAL_COMMANDS.SUBSYNC_TRIGGER) return 'Open subtitle sync controls';
|
||||
if (first === SPECIAL_COMMANDS.RUNTIME_OPTIONS_OPEN) return 'Open runtime options';
|
||||
if (first === SPECIAL_COMMANDS.REPLAY_SUBTITLE) return 'Replay current subtitle';
|
||||
if (first === SPECIAL_COMMANDS.PLAY_NEXT_SUBTITLE) return 'Play next subtitle';
|
||||
if (first.startsWith(SPECIAL_COMMANDS.RUNTIME_OPTION_CYCLE_PREFIX)) {
|
||||
const [, rawId, rawDirection] = first.split(":");
|
||||
return `Cycle runtime option ${rawId || "option"} ${rawDirection === "prev" ? "previous" : "next"}`;
|
||||
const [, rawId, rawDirection] = first.split(':');
|
||||
return `Cycle runtime option ${rawId || 'option'} ${rawDirection === 'prev' ? 'previous' : 'next'}`;
|
||||
}
|
||||
|
||||
return `MPV command: ${command.map((entry) => String(entry)).join(" ")}`;
|
||||
return `MPV command: ${command.map((entry) => String(entry)).join(' ')}`;
|
||||
}
|
||||
|
||||
function sectionForCommand(command: (string | number)[]): string {
|
||||
const first = command[0];
|
||||
if (typeof first !== "string") return "Other shortcuts";
|
||||
if (typeof first !== 'string') return 'Other shortcuts';
|
||||
|
||||
if (
|
||||
first === "cycle" ||
|
||||
first === "seek" ||
|
||||
first === "sub-seek" ||
|
||||
first === 'cycle' ||
|
||||
first === 'seek' ||
|
||||
first === 'sub-seek' ||
|
||||
first === SPECIAL_COMMANDS.REPLAY_SUBTITLE ||
|
||||
first === SPECIAL_COMMANDS.PLAY_NEXT_SUBTITLE
|
||||
) {
|
||||
return "Playback and navigation";
|
||||
return 'Playback and navigation';
|
||||
}
|
||||
|
||||
if (
|
||||
first === "show-text" ||
|
||||
first === "show-progress" ||
|
||||
first.startsWith("osd")
|
||||
) {
|
||||
return "Visual feedback";
|
||||
if (first === 'show-text' || first === 'show-progress' || first.startsWith('osd')) {
|
||||
return 'Visual feedback';
|
||||
}
|
||||
|
||||
if (first === SPECIAL_COMMANDS.SUBSYNC_TRIGGER) {
|
||||
return "Subtitle sync";
|
||||
return 'Subtitle sync';
|
||||
}
|
||||
|
||||
if (
|
||||
first === SPECIAL_COMMANDS.RUNTIME_OPTIONS_OPEN ||
|
||||
first.startsWith(SPECIAL_COMMANDS.RUNTIME_OPTION_CYCLE_PREFIX)
|
||||
) {
|
||||
return "Runtime settings";
|
||||
return 'Runtime settings';
|
||||
}
|
||||
|
||||
if (first === "quit") return "System actions";
|
||||
return "Other shortcuts";
|
||||
if (first === 'quit') return 'System actions';
|
||||
return 'Other shortcuts';
|
||||
}
|
||||
|
||||
function buildBindingSections(keybindings: Keybinding[]): SessionHelpSection[] {
|
||||
@@ -200,12 +186,12 @@ function buildBindingSections(keybindings: Keybinding[]): SessionHelpSection[] {
|
||||
}
|
||||
|
||||
const sectionOrder = [
|
||||
"Playback and navigation",
|
||||
"Visual feedback",
|
||||
"Subtitle sync",
|
||||
"Runtime settings",
|
||||
"System actions",
|
||||
"Other shortcuts",
|
||||
'Playback and navigation',
|
||||
'Visual feedback',
|
||||
'Subtitle sync',
|
||||
'Runtime settings',
|
||||
'System actions',
|
||||
'Other shortcuts',
|
||||
];
|
||||
const sectionEntries = Array.from(grouped.entries()).sort((a, b) => {
|
||||
const aIdx = sectionOrder.indexOf(a[0]);
|
||||
@@ -231,99 +217,54 @@ function buildColorSection(style: {
|
||||
};
|
||||
}): SessionHelpSection {
|
||||
return {
|
||||
title: "Color legend",
|
||||
title: 'Color legend',
|
||||
rows: [
|
||||
{
|
||||
shortcut: "Known words",
|
||||
action: normalizeColor(
|
||||
style.knownWordColor,
|
||||
FALLBACK_COLORS.knownWordColor,
|
||||
),
|
||||
color: normalizeColor(
|
||||
style.knownWordColor,
|
||||
FALLBACK_COLORS.knownWordColor,
|
||||
),
|
||||
shortcut: 'Known words',
|
||||
action: normalizeColor(style.knownWordColor, FALLBACK_COLORS.knownWordColor),
|
||||
color: normalizeColor(style.knownWordColor, FALLBACK_COLORS.knownWordColor),
|
||||
},
|
||||
{
|
||||
shortcut: "N+1 words",
|
||||
action: normalizeColor(
|
||||
style.nPlusOneColor,
|
||||
FALLBACK_COLORS.nPlusOneColor,
|
||||
),
|
||||
color: normalizeColor(
|
||||
style.nPlusOneColor,
|
||||
FALLBACK_COLORS.nPlusOneColor,
|
||||
),
|
||||
shortcut: 'N+1 words',
|
||||
action: normalizeColor(style.nPlusOneColor, FALLBACK_COLORS.nPlusOneColor),
|
||||
color: normalizeColor(style.nPlusOneColor, FALLBACK_COLORS.nPlusOneColor),
|
||||
},
|
||||
{
|
||||
shortcut: "JLPT N1",
|
||||
action: normalizeColor(
|
||||
style.jlptColors?.N1,
|
||||
FALLBACK_COLORS.jlptN1Color,
|
||||
),
|
||||
color: normalizeColor(
|
||||
style.jlptColors?.N1,
|
||||
FALLBACK_COLORS.jlptN1Color,
|
||||
),
|
||||
shortcut: 'JLPT N1',
|
||||
action: normalizeColor(style.jlptColors?.N1, FALLBACK_COLORS.jlptN1Color),
|
||||
color: normalizeColor(style.jlptColors?.N1, FALLBACK_COLORS.jlptN1Color),
|
||||
},
|
||||
{
|
||||
shortcut: "JLPT N2",
|
||||
action: normalizeColor(
|
||||
style.jlptColors?.N2,
|
||||
FALLBACK_COLORS.jlptN2Color,
|
||||
),
|
||||
color: normalizeColor(
|
||||
style.jlptColors?.N2,
|
||||
FALLBACK_COLORS.jlptN2Color,
|
||||
),
|
||||
shortcut: 'JLPT N2',
|
||||
action: normalizeColor(style.jlptColors?.N2, FALLBACK_COLORS.jlptN2Color),
|
||||
color: normalizeColor(style.jlptColors?.N2, FALLBACK_COLORS.jlptN2Color),
|
||||
},
|
||||
{
|
||||
shortcut: "JLPT N3",
|
||||
action: normalizeColor(
|
||||
style.jlptColors?.N3,
|
||||
FALLBACK_COLORS.jlptN3Color,
|
||||
),
|
||||
color: normalizeColor(
|
||||
style.jlptColors?.N3,
|
||||
FALLBACK_COLORS.jlptN3Color,
|
||||
),
|
||||
shortcut: 'JLPT N3',
|
||||
action: normalizeColor(style.jlptColors?.N3, FALLBACK_COLORS.jlptN3Color),
|
||||
color: normalizeColor(style.jlptColors?.N3, FALLBACK_COLORS.jlptN3Color),
|
||||
},
|
||||
{
|
||||
shortcut: "JLPT N4",
|
||||
action: normalizeColor(
|
||||
style.jlptColors?.N4,
|
||||
FALLBACK_COLORS.jlptN4Color,
|
||||
),
|
||||
color: normalizeColor(
|
||||
style.jlptColors?.N4,
|
||||
FALLBACK_COLORS.jlptN4Color,
|
||||
),
|
||||
shortcut: 'JLPT N4',
|
||||
action: normalizeColor(style.jlptColors?.N4, FALLBACK_COLORS.jlptN4Color),
|
||||
color: normalizeColor(style.jlptColors?.N4, FALLBACK_COLORS.jlptN4Color),
|
||||
},
|
||||
{
|
||||
shortcut: "JLPT N5",
|
||||
action: normalizeColor(
|
||||
style.jlptColors?.N5,
|
||||
FALLBACK_COLORS.jlptN5Color,
|
||||
),
|
||||
color: normalizeColor(
|
||||
style.jlptColors?.N5,
|
||||
FALLBACK_COLORS.jlptN5Color,
|
||||
),
|
||||
shortcut: 'JLPT N5',
|
||||
action: normalizeColor(style.jlptColors?.N5, FALLBACK_COLORS.jlptN5Color),
|
||||
color: normalizeColor(style.jlptColors?.N5, FALLBACK_COLORS.jlptN5Color),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function filterSections(
|
||||
sections: SessionHelpSection[],
|
||||
query: string,
|
||||
): SessionHelpSection[] {
|
||||
function filterSections(sections: SessionHelpSection[], query: string): SessionHelpSection[] {
|
||||
const normalize = (value: string): string =>
|
||||
value
|
||||
.toLowerCase()
|
||||
.replace(/commandorcontrol/gu, "ctrl")
|
||||
.replace(/cmd\/ctrl/gu, "ctrl")
|
||||
.replace(/[\s+\-_/]/gu, "");
|
||||
.replace(/commandorcontrol/gu, 'ctrl')
|
||||
.replace(/cmd\/ctrl/gu, 'ctrl')
|
||||
.replace(/[\s+\-_/]/gu, '');
|
||||
const normalized = normalize(query);
|
||||
if (!normalized) return sections;
|
||||
|
||||
@@ -346,41 +287,36 @@ function filterSections(
|
||||
}
|
||||
|
||||
function formatBindingHint(info: SessionHelpBindingInfo): string {
|
||||
if (info.bindingKey === "KeyK" && info.fallbackUsed) {
|
||||
return info.fallbackUnavailable
|
||||
? "Y-K (fallback and conflict noted)"
|
||||
: "Y-K (fallback)";
|
||||
if (info.bindingKey === 'KeyK' && info.fallbackUsed) {
|
||||
return info.fallbackUnavailable ? 'Y-K (fallback and conflict noted)' : 'Y-K (fallback)';
|
||||
}
|
||||
return "Y-H";
|
||||
return 'Y-H';
|
||||
}
|
||||
|
||||
function createShortcutRow(
|
||||
row: SessionHelpItem,
|
||||
globalIndex: number,
|
||||
): HTMLButtonElement {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "session-help-item";
|
||||
function createShortcutRow(row: SessionHelpItem, globalIndex: number): HTMLButtonElement {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'session-help-item';
|
||||
button.tabIndex = -1;
|
||||
button.dataset.sessionHelpIndex = String(globalIndex);
|
||||
|
||||
const left = document.createElement("div");
|
||||
left.className = "session-help-item-left";
|
||||
const shortcut = document.createElement("span");
|
||||
shortcut.className = "session-help-key";
|
||||
const left = document.createElement('div');
|
||||
left.className = 'session-help-item-left';
|
||||
const shortcut = document.createElement('span');
|
||||
shortcut.className = 'session-help-key';
|
||||
shortcut.textContent = row.shortcut;
|
||||
left.appendChild(shortcut);
|
||||
|
||||
const right = document.createElement("div");
|
||||
right.className = "session-help-item-right";
|
||||
const action = document.createElement("span");
|
||||
action.className = "session-help-action";
|
||||
const right = document.createElement('div');
|
||||
right.className = 'session-help-item-right';
|
||||
const action = document.createElement('span');
|
||||
action.className = 'session-help-action';
|
||||
action.textContent = row.action;
|
||||
right.appendChild(action);
|
||||
|
||||
if (row.color) {
|
||||
const dot = document.createElement("span");
|
||||
dot.className = "session-help-color-dot";
|
||||
const dot = document.createElement('span');
|
||||
dot.className = 'session-help-color-dot';
|
||||
dot.style.backgroundColor = row.color;
|
||||
right.insertBefore(dot, action);
|
||||
}
|
||||
@@ -391,16 +327,16 @@ function createShortcutRow(
|
||||
}
|
||||
|
||||
const SECTION_ICON: Record<string, string> = {
|
||||
"MPV shortcuts": "⚙",
|
||||
"Playback and navigation": "▶",
|
||||
"Visual feedback": "◉",
|
||||
"Subtitle sync": "⟲",
|
||||
"Runtime settings": "⚙",
|
||||
"System actions": "◆",
|
||||
"Other shortcuts": "…",
|
||||
"Overlay shortcuts (configurable)": "✦",
|
||||
"Overlay shortcuts": "✦",
|
||||
"Color legend": "◈",
|
||||
'MPV shortcuts': '⚙',
|
||||
'Playback and navigation': '▶',
|
||||
'Visual feedback': '◉',
|
||||
'Subtitle sync': '⟲',
|
||||
'Runtime settings': '⚙',
|
||||
'System actions': '◆',
|
||||
'Other shortcuts': '…',
|
||||
'Overlay shortcuts (configurable)': '✦',
|
||||
'Overlay shortcuts': '✦',
|
||||
'Color legend': '◈',
|
||||
};
|
||||
|
||||
function createSectionNode(
|
||||
@@ -408,17 +344,17 @@ function createSectionNode(
|
||||
sectionIndex: number,
|
||||
globalIndexMap: number[],
|
||||
): HTMLElement {
|
||||
const sectionNode = document.createElement("section");
|
||||
sectionNode.className = "session-help-section";
|
||||
const sectionNode = document.createElement('section');
|
||||
sectionNode.className = 'session-help-section';
|
||||
|
||||
const title = document.createElement("h3");
|
||||
title.className = "session-help-section-title";
|
||||
const icon = SECTION_ICON[section.title] ?? "•";
|
||||
const title = document.createElement('h3');
|
||||
title.className = 'session-help-section-title';
|
||||
const icon = SECTION_ICON[section.title] ?? '•';
|
||||
title.textContent = `${icon} ${section.title}`;
|
||||
sectionNode.appendChild(title);
|
||||
|
||||
const list = document.createElement("div");
|
||||
list.className = "session-help-item-list";
|
||||
const list = document.createElement('div');
|
||||
list.className = 'session-help-item-list';
|
||||
|
||||
section.rows.forEach((row, rowIndex) => {
|
||||
const globalIndex = globalIndexMap[sectionIndex] + rowIndex;
|
||||
@@ -433,17 +369,17 @@ function createSectionNode(
|
||||
export function createSessionHelpModal(
|
||||
ctx: RendererContext,
|
||||
options: {
|
||||
modalStateReader: Pick<ModalStateReader, "isAnyModalOpen">;
|
||||
modalStateReader: Pick<ModalStateReader, 'isAnyModalOpen'>;
|
||||
syncSettingsModalSubtitleSuppression: () => void;
|
||||
},
|
||||
) {
|
||||
let priorFocus: Element | null = null;
|
||||
let openBinding: SessionHelpBindingInfo = {
|
||||
bindingKey: "KeyH",
|
||||
bindingKey: 'KeyH',
|
||||
fallbackUsed: false,
|
||||
fallbackUnavailable: false,
|
||||
};
|
||||
let helpFilterValue = "";
|
||||
let helpFilterValue = '';
|
||||
let helpSections: SessionHelpSection[] = [];
|
||||
let focusGuard: ((event: FocusEvent) => void) | null = null;
|
||||
let windowFocusGuard: (() => void) | null = null;
|
||||
@@ -453,7 +389,7 @@ export function createSessionHelpModal(
|
||||
|
||||
function getItems(): HTMLButtonElement[] {
|
||||
return Array.from(
|
||||
ctx.dom.sessionHelpContent.querySelectorAll(".session-help-item"),
|
||||
ctx.dom.sessionHelpContent.querySelectorAll('.session-help-item'),
|
||||
) as HTMLButtonElement[];
|
||||
}
|
||||
|
||||
@@ -466,21 +402,19 @@ export function createSessionHelpModal(
|
||||
ctx.state.sessionHelpSelectedIndex = next;
|
||||
|
||||
items.forEach((item, idx) => {
|
||||
item.classList.toggle("active", idx === next);
|
||||
item.classList.toggle('active', idx === next);
|
||||
item.tabIndex = idx === next ? 0 : -1;
|
||||
});
|
||||
const activeItem = items[next];
|
||||
activeItem.focus({ preventScroll: true });
|
||||
activeItem.scrollIntoView({
|
||||
block: "nearest",
|
||||
inline: "nearest",
|
||||
block: 'nearest',
|
||||
inline: 'nearest',
|
||||
});
|
||||
}
|
||||
|
||||
function isSessionHelpModalFocusTarget(target: EventTarget | null): boolean {
|
||||
return (
|
||||
target instanceof Element && ctx.dom.sessionHelpModal.contains(target)
|
||||
);
|
||||
return target instanceof Element && ctx.dom.sessionHelpModal.contains(target);
|
||||
}
|
||||
|
||||
function focusFallbackTarget(): boolean {
|
||||
@@ -537,30 +471,22 @@ export function createSessionHelpModal(
|
||||
running += section.rows.length;
|
||||
}
|
||||
|
||||
ctx.dom.sessionHelpContent.innerHTML = "";
|
||||
ctx.dom.sessionHelpContent.innerHTML = '';
|
||||
sections.forEach((section, sectionIndex) => {
|
||||
const sectionNode = createSectionNode(
|
||||
section,
|
||||
sectionIndex,
|
||||
indexOffsets,
|
||||
);
|
||||
const sectionNode = createSectionNode(section, sectionIndex, indexOffsets);
|
||||
ctx.dom.sessionHelpContent.appendChild(sectionNode);
|
||||
});
|
||||
|
||||
if (getItems().length === 0) {
|
||||
ctx.dom.sessionHelpContent.classList.add(
|
||||
"session-help-content-no-results",
|
||||
);
|
||||
ctx.dom.sessionHelpContent.classList.add('session-help-content-no-results');
|
||||
ctx.dom.sessionHelpContent.textContent = helpFilterValue
|
||||
? "No matching shortcuts found."
|
||||
: "No active session shortcuts found.";
|
||||
? 'No matching shortcuts found.'
|
||||
: 'No active session shortcuts found.';
|
||||
ctx.state.sessionHelpSelectedIndex = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.dom.sessionHelpContent.classList.remove(
|
||||
"session-help-content-no-results",
|
||||
);
|
||||
ctx.dom.sessionHelpContent.classList.remove('session-help-content-no-results');
|
||||
|
||||
if (isFilterInputFocused()) return;
|
||||
|
||||
@@ -578,23 +504,14 @@ export function createSessionHelpModal(
|
||||
requestOverlayFocus();
|
||||
enforceModalFocus();
|
||||
};
|
||||
ctx.dom.sessionHelpModal.addEventListener(
|
||||
"pointerdown",
|
||||
modalPointerFocusGuard,
|
||||
);
|
||||
ctx.dom.sessionHelpModal.addEventListener("click", modalPointerFocusGuard);
|
||||
ctx.dom.sessionHelpModal.addEventListener('pointerdown', modalPointerFocusGuard);
|
||||
ctx.dom.sessionHelpModal.addEventListener('click', modalPointerFocusGuard);
|
||||
}
|
||||
|
||||
function removePointerFocusListener(): void {
|
||||
if (!modalPointerFocusGuard) return;
|
||||
ctx.dom.sessionHelpModal.removeEventListener(
|
||||
"pointerdown",
|
||||
modalPointerFocusGuard,
|
||||
);
|
||||
ctx.dom.sessionHelpModal.removeEventListener(
|
||||
"click",
|
||||
modalPointerFocusGuard,
|
||||
);
|
||||
ctx.dom.sessionHelpModal.removeEventListener('pointerdown', modalPointerFocusGuard);
|
||||
ctx.dom.sessionHelpModal.removeEventListener('click', modalPointerFocusGuard);
|
||||
modalPointerFocusGuard = null;
|
||||
}
|
||||
|
||||
@@ -605,22 +522,22 @@ export function createSessionHelpModal(
|
||||
requestOverlayFocus();
|
||||
enforceModalFocus();
|
||||
};
|
||||
window.addEventListener("blur", windowFocusGuard);
|
||||
window.addEventListener("focus", windowFocusGuard);
|
||||
window.addEventListener('blur', windowFocusGuard);
|
||||
window.addEventListener('focus', windowFocusGuard);
|
||||
}
|
||||
|
||||
function stopFocusRecoveryGuards(): void {
|
||||
if (!windowFocusGuard) return;
|
||||
window.removeEventListener("blur", windowFocusGuard);
|
||||
window.removeEventListener("focus", windowFocusGuard);
|
||||
window.removeEventListener('blur', windowFocusGuard);
|
||||
window.removeEventListener('focus', windowFocusGuard);
|
||||
windowFocusGuard = null;
|
||||
}
|
||||
|
||||
function showRenderError(message: string): void {
|
||||
helpSections = [];
|
||||
helpFilterValue = "";
|
||||
ctx.dom.sessionHelpFilter.value = "";
|
||||
ctx.dom.sessionHelpContent.classList.add("session-help-content-no-results");
|
||||
helpFilterValue = '';
|
||||
ctx.dom.sessionHelpFilter.value = '';
|
||||
ctx.dom.sessionHelpContent.classList.add('session-help-content-no-results');
|
||||
ctx.dom.sessionHelpContent.textContent = message;
|
||||
ctx.state.sessionHelpSelectedIndex = 0;
|
||||
}
|
||||
@@ -636,34 +553,29 @@ export function createSessionHelpModal(
|
||||
const bindingSections = buildBindingSections(keybindings);
|
||||
if (bindingSections.length > 0) {
|
||||
const playback = bindingSections.find(
|
||||
(section) => section.title === "Playback and navigation",
|
||||
(section) => section.title === 'Playback and navigation',
|
||||
);
|
||||
if (playback) {
|
||||
playback.title = "MPV shortcuts";
|
||||
playback.title = 'MPV shortcuts';
|
||||
}
|
||||
}
|
||||
|
||||
const shortcutSections = buildOverlayShortcutSections(shortcuts);
|
||||
if (shortcutSections.length > 0) {
|
||||
shortcutSections[0].title = "Overlay shortcuts (configurable)";
|
||||
shortcutSections[0].title = 'Overlay shortcuts (configurable)';
|
||||
}
|
||||
const colorSection = buildColorSection(styleConfig ?? {});
|
||||
helpSections = [...bindingSections, ...shortcutSections, colorSection];
|
||||
applyFilterAndRender();
|
||||
return true;
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Unable to load session help data.";
|
||||
const message = error instanceof Error ? error.message : 'Unable to load session help data.';
|
||||
showRenderError(`Session help failed to load: ${message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function openSessionHelpModal(
|
||||
opening: SessionHelpBindingInfo,
|
||||
): Promise<void> {
|
||||
async function openSessionHelpModal(opening: SessionHelpBindingInfo): Promise<void> {
|
||||
openBinding = opening;
|
||||
priorFocus = document.activeElement;
|
||||
|
||||
@@ -672,31 +584,30 @@ export function createSessionHelpModal(
|
||||
ctx.dom.sessionHelpShortcut.textContent = `Session help opened with ${formatBindingHint(openBinding)}`;
|
||||
if (openBinding.fallbackUnavailable) {
|
||||
ctx.dom.sessionHelpWarning.textContent =
|
||||
"Both Y-H and Y-K are bound; Y-K remains the fallback for this session.";
|
||||
'Both Y-H and Y-K are bound; Y-K remains the fallback for this session.';
|
||||
} else if (openBinding.fallbackUsed) {
|
||||
ctx.dom.sessionHelpWarning.textContent =
|
||||
"Y-H is already bound; using Y-K as fallback.";
|
||||
ctx.dom.sessionHelpWarning.textContent = 'Y-H is already bound; using Y-K as fallback.';
|
||||
} else {
|
||||
ctx.dom.sessionHelpWarning.textContent = "";
|
||||
ctx.dom.sessionHelpWarning.textContent = '';
|
||||
}
|
||||
if (dataLoaded) {
|
||||
ctx.dom.sessionHelpStatus.textContent =
|
||||
"Use Arrow keys, J/K/H/L, mouse, click, or / then type to filter. Esc closes.";
|
||||
'Use Arrow keys, J/K/H/L, mouse, click, or / then type to filter. Esc closes.';
|
||||
} else {
|
||||
ctx.dom.sessionHelpStatus.textContent =
|
||||
"Session help data is unavailable right now. Press Esc to close.";
|
||||
'Session help data is unavailable right now. Press Esc to close.';
|
||||
ctx.dom.sessionHelpWarning.textContent =
|
||||
"Unable to load latest shortcut settings from the runtime.";
|
||||
'Unable to load latest shortcut settings from the runtime.';
|
||||
}
|
||||
|
||||
ctx.state.sessionHelpModalOpen = true;
|
||||
options.syncSettingsModalSubtitleSuppression();
|
||||
ctx.dom.overlay.classList.add("interactive");
|
||||
ctx.dom.sessionHelpModal.classList.remove("hidden");
|
||||
ctx.dom.sessionHelpModal.setAttribute("aria-hidden", "false");
|
||||
ctx.dom.sessionHelpModal.setAttribute("tabindex", "-1");
|
||||
ctx.dom.sessionHelpFilter.value = "";
|
||||
helpFilterValue = "";
|
||||
ctx.dom.overlay.classList.add('interactive');
|
||||
ctx.dom.sessionHelpModal.classList.remove('hidden');
|
||||
ctx.dom.sessionHelpModal.setAttribute('aria-hidden', 'false');
|
||||
ctx.dom.sessionHelpModal.setAttribute('tabindex', '-1');
|
||||
ctx.dom.sessionHelpFilter.value = '';
|
||||
helpFilterValue = '';
|
||||
if (ctx.platform.shouldToggleMouseIgnore) {
|
||||
window.electronAPI.setIgnoreMouseEvents(false);
|
||||
}
|
||||
@@ -709,7 +620,7 @@ export function createSessionHelpModal(
|
||||
enforceModalFocus();
|
||||
}
|
||||
};
|
||||
document.addEventListener("focusin", focusGuard);
|
||||
document.addEventListener('focusin', focusGuard);
|
||||
}
|
||||
|
||||
addPointerFocusListener();
|
||||
@@ -724,17 +635,14 @@ export function createSessionHelpModal(
|
||||
|
||||
ctx.state.sessionHelpModalOpen = false;
|
||||
options.syncSettingsModalSubtitleSuppression();
|
||||
ctx.dom.sessionHelpModal.classList.add("hidden");
|
||||
ctx.dom.sessionHelpModal.setAttribute("aria-hidden", "true");
|
||||
if (
|
||||
!ctx.state.isOverSubtitle &&
|
||||
!options.modalStateReader.isAnyModalOpen()
|
||||
) {
|
||||
ctx.dom.overlay.classList.remove("interactive");
|
||||
ctx.dom.sessionHelpModal.classList.add('hidden');
|
||||
ctx.dom.sessionHelpModal.setAttribute('aria-hidden', 'true');
|
||||
if (!ctx.state.isOverSubtitle && !options.modalStateReader.isAnyModalOpen()) {
|
||||
ctx.dom.overlay.classList.remove('interactive');
|
||||
}
|
||||
|
||||
if (focusGuard) {
|
||||
document.removeEventListener("focusin", focusGuard);
|
||||
document.removeEventListener('focusin', focusGuard);
|
||||
focusGuard = null;
|
||||
}
|
||||
removePointerFocusListener();
|
||||
@@ -750,17 +658,14 @@ export function createSessionHelpModal(
|
||||
ctx.dom.overlay.focus({ preventScroll: true });
|
||||
}
|
||||
if (ctx.platform.shouldToggleMouseIgnore) {
|
||||
if (
|
||||
!ctx.state.isOverSubtitle &&
|
||||
!options.modalStateReader.isAnyModalOpen()
|
||||
) {
|
||||
if (!ctx.state.isOverSubtitle && !options.modalStateReader.isAnyModalOpen()) {
|
||||
window.electronAPI.setIgnoreMouseEvents(true, { forward: true });
|
||||
} else {
|
||||
window.electronAPI.setIgnoreMouseEvents(false);
|
||||
}
|
||||
}
|
||||
ctx.dom.sessionHelpFilter.value = "";
|
||||
helpFilterValue = "";
|
||||
ctx.dom.sessionHelpFilter.value = '';
|
||||
helpFilterValue = '';
|
||||
window.focus();
|
||||
}
|
||||
|
||||
@@ -768,15 +673,15 @@ export function createSessionHelpModal(
|
||||
if (!ctx.state.sessionHelpModalOpen) return false;
|
||||
|
||||
if (isFilterInputFocused()) {
|
||||
if (e.key === "Escape") {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
if (!helpFilterValue) {
|
||||
closeSessionHelpModal();
|
||||
return true;
|
||||
}
|
||||
|
||||
helpFilterValue = "";
|
||||
ctx.dom.sessionHelpFilter.value = "";
|
||||
helpFilterValue = '';
|
||||
ctx.dom.sessionHelpFilter.value = '';
|
||||
applyFilterAndRender();
|
||||
focusFallbackTarget();
|
||||
return true;
|
||||
@@ -784,7 +689,7 @@ export function createSessionHelpModal(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.key === "Escape") {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
closeSessionHelpModal();
|
||||
return true;
|
||||
@@ -793,7 +698,7 @@ export function createSessionHelpModal(
|
||||
const items = getItems();
|
||||
if (items.length === 0) return true;
|
||||
|
||||
if (e.key === "/" && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) {
|
||||
if (e.key === '/' && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
focusFilterInput();
|
||||
return true;
|
||||
@@ -801,13 +706,13 @@ export function createSessionHelpModal(
|
||||
|
||||
const key = e.key.toLowerCase();
|
||||
|
||||
if (key === "arrowdown" || key === "j" || key === "l") {
|
||||
if (key === 'arrowdown' || key === 'j' || key === 'l') {
|
||||
e.preventDefault();
|
||||
setSelected(ctx.state.sessionHelpSelectedIndex + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (key === "arrowup" || key === "k" || key === "h") {
|
||||
if (key === 'arrowup' || key === 'k' || key === 'h') {
|
||||
e.preventDefault();
|
||||
setSelected(ctx.state.sessionHelpSelectedIndex - 1);
|
||||
return true;
|
||||
@@ -817,35 +722,29 @@ export function createSessionHelpModal(
|
||||
}
|
||||
|
||||
function wireDomEvents(): void {
|
||||
ctx.dom.sessionHelpFilter.addEventListener("input", () => {
|
||||
ctx.dom.sessionHelpFilter.addEventListener('input', () => {
|
||||
helpFilterValue = ctx.dom.sessionHelpFilter.value;
|
||||
applyFilterAndRender();
|
||||
});
|
||||
|
||||
ctx.dom.sessionHelpFilter.addEventListener(
|
||||
"keydown",
|
||||
(event: KeyboardEvent) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
focusFallbackTarget();
|
||||
}
|
||||
},
|
||||
);
|
||||
ctx.dom.sessionHelpFilter.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
focusFallbackTarget();
|
||||
}
|
||||
});
|
||||
|
||||
ctx.dom.sessionHelpContent.addEventListener(
|
||||
"click",
|
||||
(event: MouseEvent) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof Element)) return;
|
||||
const row = target.closest(".session-help-item") as HTMLElement | null;
|
||||
if (!row) return;
|
||||
const index = Number.parseInt(row.dataset.sessionHelpIndex ?? "", 10);
|
||||
if (!Number.isFinite(index)) return;
|
||||
setSelected(index);
|
||||
},
|
||||
);
|
||||
ctx.dom.sessionHelpContent.addEventListener('click', (event: MouseEvent) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof Element)) return;
|
||||
const row = target.closest('.session-help-item') as HTMLElement | null;
|
||||
if (!row) return;
|
||||
const index = Number.parseInt(row.dataset.sessionHelpIndex ?? '', 10);
|
||||
if (!Number.isFinite(index)) return;
|
||||
setSelected(index);
|
||||
});
|
||||
|
||||
ctx.dom.sessionHelpClose.addEventListener("click", () => {
|
||||
ctx.dom.sessionHelpClose.addEventListener('click', () => {
|
||||
closeSessionHelpModal();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
import type { SubsyncManualPayload } from "../../types";
|
||||
import type { ModalStateReader, RendererContext } from "../context";
|
||||
import type { SubsyncManualPayload } from '../../types';
|
||||
import type { ModalStateReader, RendererContext } from '../context';
|
||||
|
||||
export function createSubsyncModal(
|
||||
ctx: RendererContext,
|
||||
options: {
|
||||
modalStateReader: Pick<ModalStateReader, "isAnyModalOpen">;
|
||||
modalStateReader: Pick<ModalStateReader, 'isAnyModalOpen'>;
|
||||
syncSettingsModalSubtitleSuppression: () => void;
|
||||
},
|
||||
) {
|
||||
function setSubsyncStatus(message: string, isError = false): void {
|
||||
ctx.dom.subsyncStatus.textContent = message;
|
||||
ctx.dom.subsyncStatus.classList.toggle("error", isError);
|
||||
ctx.dom.subsyncStatus.classList.toggle('error', isError);
|
||||
}
|
||||
|
||||
function updateSubsyncSourceVisibility(): void {
|
||||
const useAlass = ctx.dom.subsyncEngineAlass.checked;
|
||||
ctx.dom.subsyncSourceLabel.classList.toggle("hidden", !useAlass);
|
||||
ctx.dom.subsyncSourceLabel.classList.toggle('hidden', !useAlass);
|
||||
}
|
||||
|
||||
function renderSubsyncSourceTracks(): void {
|
||||
ctx.dom.subsyncSourceSelect.innerHTML = "";
|
||||
ctx.dom.subsyncSourceSelect.innerHTML = '';
|
||||
for (const track of ctx.state.subsyncSourceTracks) {
|
||||
const option = document.createElement("option");
|
||||
const option = document.createElement('option');
|
||||
option.value = String(track.id);
|
||||
option.textContent = track.label;
|
||||
ctx.dom.subsyncSourceSelect.appendChild(option);
|
||||
}
|
||||
ctx.dom.subsyncSourceSelect.disabled =
|
||||
ctx.state.subsyncSourceTracks.length === 0;
|
||||
ctx.dom.subsyncSourceSelect.disabled = ctx.state.subsyncSourceTracks.length === 0;
|
||||
}
|
||||
|
||||
function closeSubsyncModal(): void {
|
||||
@@ -36,15 +35,12 @@ export function createSubsyncModal(
|
||||
ctx.state.subsyncModalOpen = false;
|
||||
options.syncSettingsModalSubtitleSuppression();
|
||||
|
||||
ctx.dom.subsyncModal.classList.add("hidden");
|
||||
ctx.dom.subsyncModal.setAttribute("aria-hidden", "true");
|
||||
window.electronAPI.notifyOverlayModalClosed("subsync");
|
||||
ctx.dom.subsyncModal.classList.add('hidden');
|
||||
ctx.dom.subsyncModal.setAttribute('aria-hidden', 'true');
|
||||
window.electronAPI.notifyOverlayModalClosed('subsync');
|
||||
|
||||
if (
|
||||
!ctx.state.isOverSubtitle &&
|
||||
!options.modalStateReader.isAnyModalOpen()
|
||||
) {
|
||||
ctx.dom.overlay.classList.remove("interactive");
|
||||
if (!ctx.state.isOverSubtitle && !options.modalStateReader.isAnyModalOpen()) {
|
||||
ctx.dom.overlay.classList.remove('interactive');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,30 +60,30 @@ export function createSubsyncModal(
|
||||
|
||||
setSubsyncStatus(
|
||||
hasSources
|
||||
? "Choose engine and source, then run."
|
||||
: "No source subtitles available for alass. Use ffsubsync.",
|
||||
? 'Choose engine and source, then run.'
|
||||
: 'No source subtitles available for alass. Use ffsubsync.',
|
||||
false,
|
||||
);
|
||||
|
||||
ctx.state.subsyncModalOpen = true;
|
||||
options.syncSettingsModalSubtitleSuppression();
|
||||
|
||||
ctx.dom.overlay.classList.add("interactive");
|
||||
ctx.dom.subsyncModal.classList.remove("hidden");
|
||||
ctx.dom.subsyncModal.setAttribute("aria-hidden", "false");
|
||||
ctx.dom.overlay.classList.add('interactive');
|
||||
ctx.dom.subsyncModal.classList.remove('hidden');
|
||||
ctx.dom.subsyncModal.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
|
||||
async function runSubsyncManualFromModal(): Promise<void> {
|
||||
if (ctx.state.subsyncSubmitting) return;
|
||||
|
||||
const engine = ctx.dom.subsyncEngineAlass.checked ? "alass" : "ffsubsync";
|
||||
const engine = ctx.dom.subsyncEngineAlass.checked ? 'alass' : 'ffsubsync';
|
||||
const sourceTrackId =
|
||||
engine === "alass" && ctx.dom.subsyncSourceSelect.value
|
||||
engine === 'alass' && ctx.dom.subsyncSourceSelect.value
|
||||
? Number.parseInt(ctx.dom.subsyncSourceSelect.value, 10)
|
||||
: null;
|
||||
|
||||
if (engine === "alass" && !Number.isFinite(sourceTrackId)) {
|
||||
setSubsyncStatus("Select a source subtitle track for alass.", true);
|
||||
if (engine === 'alass' && !Number.isFinite(sourceTrackId)) {
|
||||
setSubsyncStatus('Select a source subtitle track for alass.', true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -107,13 +103,13 @@ export function createSubsyncModal(
|
||||
}
|
||||
|
||||
function handleSubsyncKeydown(e: KeyboardEvent): boolean {
|
||||
if (e.key === "Escape") {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
closeSubsyncModal();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e.key === "Enter") {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
void runSubsyncManualFromModal();
|
||||
return true;
|
||||
@@ -123,16 +119,16 @@ export function createSubsyncModal(
|
||||
}
|
||||
|
||||
function wireDomEvents(): void {
|
||||
ctx.dom.subsyncCloseButton.addEventListener("click", () => {
|
||||
ctx.dom.subsyncCloseButton.addEventListener('click', () => {
|
||||
closeSubsyncModal();
|
||||
});
|
||||
ctx.dom.subsyncEngineAlass.addEventListener("change", () => {
|
||||
ctx.dom.subsyncEngineAlass.addEventListener('change', () => {
|
||||
updateSubsyncSourceVisibility();
|
||||
});
|
||||
ctx.dom.subsyncEngineFfsubsync.addEventListener("change", () => {
|
||||
ctx.dom.subsyncEngineFfsubsync.addEventListener('change', () => {
|
||||
updateSubsyncSourceVisibility();
|
||||
});
|
||||
ctx.dom.subsyncRunButton.addEventListener("click", () => {
|
||||
ctx.dom.subsyncRunButton.addEventListener('click', () => {
|
||||
void runSubsyncManualFromModal();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { OverlayContentMeasurement, OverlayContentRect } from "../types";
|
||||
import type { RendererContext } from "./context";
|
||||
import type { OverlayContentMeasurement, OverlayContentRect } from '../types';
|
||||
import type { RendererContext } from './context';
|
||||
|
||||
const MEASUREMENT_DEBOUNCE_MS = 80;
|
||||
|
||||
@@ -26,10 +26,7 @@ function toMeasuredRect(rect: DOMRect): OverlayContentRect | null {
|
||||
};
|
||||
}
|
||||
|
||||
function unionRects(
|
||||
a: OverlayContentRect,
|
||||
b: OverlayContentRect,
|
||||
): OverlayContentRect {
|
||||
function unionRects(a: OverlayContentRect, b: OverlayContentRect): OverlayContentRect {
|
||||
const left = Math.min(a.x, b.x);
|
||||
const top = Math.min(a.y, b.y);
|
||||
const right = Math.max(a.x + a.width, b.x + b.width);
|
||||
@@ -51,9 +48,7 @@ function collectContentRect(ctx: RendererContext): OverlayContentRect | null {
|
||||
|
||||
const subtitleHasContent = hasVisibleTextContent(ctx.dom.subtitleRoot);
|
||||
if (subtitleHasContent) {
|
||||
const subtitleRect = toMeasuredRect(
|
||||
ctx.dom.subtitleRoot.getBoundingClientRect(),
|
||||
);
|
||||
const subtitleRect = toMeasuredRect(ctx.dom.subtitleRoot.getBoundingClientRect());
|
||||
if (subtitleRect) {
|
||||
combinedRect = subtitleRect;
|
||||
}
|
||||
@@ -61,13 +56,9 @@ function collectContentRect(ctx: RendererContext): OverlayContentRect | null {
|
||||
|
||||
const secondaryHasContent = hasVisibleTextContent(ctx.dom.secondarySubRoot);
|
||||
if (secondaryHasContent) {
|
||||
const secondaryRect = toMeasuredRect(
|
||||
ctx.dom.secondarySubContainer.getBoundingClientRect(),
|
||||
);
|
||||
const secondaryRect = toMeasuredRect(ctx.dom.secondarySubContainer.getBoundingClientRect());
|
||||
if (secondaryRect) {
|
||||
combinedRect = combinedRect
|
||||
? unionRects(combinedRect, secondaryRect)
|
||||
: secondaryRect;
|
||||
combinedRect = combinedRect ? unionRects(combinedRect, secondaryRect) : secondaryRect;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { createPositioningController } from "./positioning/controller.js";
|
||||
export { createPositioningController } from './positioning/controller.js';
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import type { ModalStateReader, RendererContext } from "../context";
|
||||
import type { ModalStateReader, RendererContext } from '../context';
|
||||
import {
|
||||
createInMemorySubtitlePositionController,
|
||||
type SubtitlePositionController,
|
||||
} from "./position-state.js";
|
||||
} from './position-state.js';
|
||||
import {
|
||||
createInvisibleOffsetController,
|
||||
type InvisibleOffsetController,
|
||||
} from "./invisible-offset.js";
|
||||
} from './invisible-offset.js';
|
||||
import {
|
||||
createMpvSubtitleLayoutController,
|
||||
type MpvSubtitleLayoutController,
|
||||
} from "./invisible-layout.js";
|
||||
} from './invisible-layout.js';
|
||||
|
||||
type PositioningControllerOptions = {
|
||||
modalStateReader: Pick<ModalStateReader, "isAnySettingsModalOpen">;
|
||||
modalStateReader: Pick<ModalStateReader, 'isAnySettingsModalOpen'>;
|
||||
applySubtitleFontSize: (fontSize: number) => void;
|
||||
};
|
||||
|
||||
@@ -22,26 +22,15 @@ export function createPositioningController(
|
||||
options: PositioningControllerOptions,
|
||||
) {
|
||||
const visible = createInMemorySubtitlePositionController(ctx);
|
||||
const invisibleOffset = createInvisibleOffsetController(
|
||||
ctx,
|
||||
options.modalStateReader,
|
||||
);
|
||||
const invisibleLayout = createMpvSubtitleLayoutController(
|
||||
ctx,
|
||||
options.applySubtitleFontSize,
|
||||
{
|
||||
applyInvisibleSubtitleOffsetPosition:
|
||||
invisibleOffset.applyInvisibleSubtitleOffsetPosition,
|
||||
updateInvisiblePositionEditHud:
|
||||
invisibleOffset.updateInvisiblePositionEditHud,
|
||||
},
|
||||
);
|
||||
const invisibleOffset = createInvisibleOffsetController(ctx, options.modalStateReader);
|
||||
const invisibleLayout = createMpvSubtitleLayoutController(ctx, options.applySubtitleFontSize, {
|
||||
applyInvisibleSubtitleOffsetPosition: invisibleOffset.applyInvisibleSubtitleOffsetPosition,
|
||||
updateInvisiblePositionEditHud: invisibleOffset.updateInvisiblePositionEditHud,
|
||||
});
|
||||
|
||||
return {
|
||||
...visible,
|
||||
...invisibleOffset,
|
||||
...invisibleLayout,
|
||||
} as SubtitlePositionController &
|
||||
InvisibleOffsetController &
|
||||
MpvSubtitleLayoutController;
|
||||
} as SubtitlePositionController & InvisibleOffsetController & MpvSubtitleLayoutController;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { MpvSubtitleRenderMetrics } from "../../types";
|
||||
import type { RendererContext } from "../context";
|
||||
import type { MpvSubtitleRenderMetrics } from '../../types';
|
||||
import type { RendererContext } from '../context';
|
||||
|
||||
const INVISIBLE_MACOS_VERTICAL_NUDGE_PX = 5;
|
||||
const INVISIBLE_MACOS_LINE_HEIGHT_SINGLE = "0.92";
|
||||
const INVISIBLE_MACOS_LINE_HEIGHT_MULTI = "1.2";
|
||||
const INVISIBLE_MACOS_LINE_HEIGHT_MULTI_DENSE = "1.3";
|
||||
const INVISIBLE_MACOS_LINE_HEIGHT_SINGLE = '0.92';
|
||||
const INVISIBLE_MACOS_LINE_HEIGHT_MULTI = '1.2';
|
||||
const INVISIBLE_MACOS_LINE_HEIGHT_MULTI_DENSE = '1.3';
|
||||
|
||||
export function applyContainerBaseLayout(
|
||||
ctx: RendererContext,
|
||||
@@ -17,32 +17,32 @@ export function applyContainerBaseLayout(
|
||||
): void {
|
||||
const { horizontalAvailable, leftInset, marginX, hAlign } = params;
|
||||
|
||||
ctx.dom.subtitleContainer.style.position = "absolute";
|
||||
ctx.dom.subtitleContainer.style.position = 'absolute';
|
||||
ctx.dom.subtitleContainer.style.maxWidth = `${horizontalAvailable}px`;
|
||||
ctx.dom.subtitleContainer.style.width = `${horizontalAvailable}px`;
|
||||
ctx.dom.subtitleContainer.style.padding = "0";
|
||||
ctx.dom.subtitleContainer.style.background = "transparent";
|
||||
ctx.dom.subtitleContainer.style.marginBottom = "0";
|
||||
ctx.dom.subtitleContainer.style.pointerEvents = "none";
|
||||
ctx.dom.subtitleContainer.style.padding = '0';
|
||||
ctx.dom.subtitleContainer.style.background = 'transparent';
|
||||
ctx.dom.subtitleContainer.style.marginBottom = '0';
|
||||
ctx.dom.subtitleContainer.style.pointerEvents = 'none';
|
||||
ctx.dom.subtitleContainer.style.left = `${leftInset + marginX}px`;
|
||||
ctx.dom.subtitleContainer.style.right = "";
|
||||
ctx.dom.subtitleContainer.style.transform = "";
|
||||
ctx.dom.subtitleContainer.style.textAlign = "";
|
||||
ctx.dom.subtitleContainer.style.right = '';
|
||||
ctx.dom.subtitleContainer.style.transform = '';
|
||||
ctx.dom.subtitleContainer.style.textAlign = '';
|
||||
|
||||
if (hAlign === 0) {
|
||||
ctx.dom.subtitleContainer.style.textAlign = "left";
|
||||
ctx.dom.subtitleRoot.style.textAlign = "left";
|
||||
ctx.dom.subtitleContainer.style.textAlign = 'left';
|
||||
ctx.dom.subtitleRoot.style.textAlign = 'left';
|
||||
} else if (hAlign === 2) {
|
||||
ctx.dom.subtitleContainer.style.textAlign = "right";
|
||||
ctx.dom.subtitleRoot.style.textAlign = "right";
|
||||
ctx.dom.subtitleContainer.style.textAlign = 'right';
|
||||
ctx.dom.subtitleRoot.style.textAlign = 'right';
|
||||
} else {
|
||||
ctx.dom.subtitleContainer.style.textAlign = "center";
|
||||
ctx.dom.subtitleRoot.style.textAlign = "center";
|
||||
ctx.dom.subtitleContainer.style.textAlign = 'center';
|
||||
ctx.dom.subtitleRoot.style.textAlign = 'center';
|
||||
}
|
||||
|
||||
ctx.dom.subtitleRoot.style.display = "inline-block";
|
||||
ctx.dom.subtitleRoot.style.maxWidth = "100%";
|
||||
ctx.dom.subtitleRoot.style.pointerEvents = "auto";
|
||||
ctx.dom.subtitleRoot.style.display = 'inline-block';
|
||||
ctx.dom.subtitleRoot.style.maxWidth = '100%';
|
||||
ctx.dom.subtitleRoot.style.pointerEvents = 'auto';
|
||||
}
|
||||
|
||||
export function applyVerticalPosition(
|
||||
@@ -59,38 +59,30 @@ export function applyVerticalPosition(
|
||||
): void {
|
||||
const lineCount = Math.max(1, ctx.state.currentInvisibleSubtitleLineCount);
|
||||
const multiline = lineCount > 1;
|
||||
const baselineCompensationFactor =
|
||||
lineCount >= 3 ? 0.46 : multiline ? 0.58 : 0.7;
|
||||
const baselineCompensationPx = Math.max(
|
||||
0,
|
||||
params.effectiveFontSize * baselineCompensationFactor,
|
||||
);
|
||||
const baselineCompensationFactor = lineCount >= 3 ? 0.46 : multiline ? 0.58 : 0.7;
|
||||
const baselineCompensationPx = Math.max(0, params.effectiveFontSize * baselineCompensationFactor);
|
||||
|
||||
if (params.vAlign === 2) {
|
||||
ctx.dom.subtitleContainer.style.top = `${Math.max(
|
||||
0,
|
||||
params.topInset + params.marginY - baselineCompensationPx,
|
||||
)}px`;
|
||||
ctx.dom.subtitleContainer.style.bottom = "";
|
||||
ctx.dom.subtitleContainer.style.bottom = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (params.vAlign === 1) {
|
||||
ctx.dom.subtitleContainer.style.top = "50%";
|
||||
ctx.dom.subtitleContainer.style.bottom = "";
|
||||
ctx.dom.subtitleContainer.style.transform = "translateY(-50%)";
|
||||
ctx.dom.subtitleContainer.style.top = '50%';
|
||||
ctx.dom.subtitleContainer.style.bottom = '';
|
||||
ctx.dom.subtitleContainer.style.transform = 'translateY(-50%)';
|
||||
return;
|
||||
}
|
||||
|
||||
const subPosMargin =
|
||||
((100 - params.metrics.subPos) / 100) * params.renderAreaHeight;
|
||||
const subPosMargin = ((100 - params.metrics.subPos) / 100) * params.renderAreaHeight;
|
||||
const effectiveMargin = Math.max(params.marginY, subPosMargin);
|
||||
const bottomPx = Math.max(
|
||||
0,
|
||||
params.bottomInset + effectiveMargin + baselineCompensationPx,
|
||||
);
|
||||
const bottomPx = Math.max(0, params.bottomInset + effectiveMargin + baselineCompensationPx);
|
||||
|
||||
ctx.dom.subtitleContainer.style.top = "";
|
||||
ctx.dom.subtitleContainer.style.top = '';
|
||||
ctx.dom.subtitleContainer.style.bottom = `${bottomPx}px`;
|
||||
}
|
||||
|
||||
@@ -98,7 +90,7 @@ function resolveFontFamily(rawFont: string): string {
|
||||
const strippedFont = rawFont
|
||||
.replace(
|
||||
/\s+(Regular|Bold|Italic|Light|Medium|Semi\s*Bold|Extra\s*Bold|Extra\s*Light|Thin|Black|Heavy|Demi\s*Bold|Book|Condensed)\s*$/i,
|
||||
"",
|
||||
'',
|
||||
)
|
||||
.trim();
|
||||
|
||||
@@ -107,11 +99,8 @@ function resolveFontFamily(rawFont: string): string {
|
||||
: `"${rawFont}", sans-serif`;
|
||||
}
|
||||
|
||||
function resolveLineHeight(
|
||||
lineCount: number,
|
||||
isMacOSPlatform: boolean,
|
||||
): string {
|
||||
if (!isMacOSPlatform) return "normal";
|
||||
function resolveLineHeight(lineCount: number, isMacOSPlatform: boolean): string {
|
||||
if (!isMacOSPlatform) return 'normal';
|
||||
if (lineCount >= 3) return INVISIBLE_MACOS_LINE_HEIGHT_MULTI_DENSE;
|
||||
if (lineCount >= 2) return INVISIBLE_MACOS_LINE_HEIGHT_MULTI;
|
||||
return INVISIBLE_MACOS_LINE_HEIGHT_SINGLE;
|
||||
@@ -126,20 +115,15 @@ function resolveLetterSpacing(
|
||||
return `${spacing * pxPerScaledPixel * (isMacOSPlatform ? 0.7 : 1)}px`;
|
||||
}
|
||||
|
||||
return isMacOSPlatform ? "-0.02em" : "0px";
|
||||
return isMacOSPlatform ? '-0.02em' : '0px';
|
||||
}
|
||||
|
||||
function applyComputedLineHeightCompensation(
|
||||
ctx: RendererContext,
|
||||
effectiveFontSize: number,
|
||||
): void {
|
||||
const computedLineHeight = parseFloat(
|
||||
getComputedStyle(ctx.dom.subtitleRoot).lineHeight,
|
||||
);
|
||||
if (
|
||||
!Number.isFinite(computedLineHeight) ||
|
||||
computedLineHeight <= effectiveFontSize
|
||||
) {
|
||||
const computedLineHeight = parseFloat(getComputedStyle(ctx.dom.subtitleRoot).lineHeight);
|
||||
if (!Number.isFinite(computedLineHeight) || computedLineHeight <= effectiveFontSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -182,31 +166,21 @@ export function applyTypography(
|
||||
const isMacOSPlatform = ctx.platform.isMacOSPlatform;
|
||||
|
||||
ctx.dom.subtitleRoot.style.setProperty(
|
||||
"line-height",
|
||||
'line-height',
|
||||
resolveLineHeight(lineCount, isMacOSPlatform),
|
||||
isMacOSPlatform ? "important" : "",
|
||||
);
|
||||
ctx.dom.subtitleRoot.style.fontFamily = resolveFontFamily(
|
||||
params.metrics.subFont,
|
||||
isMacOSPlatform ? 'important' : '',
|
||||
);
|
||||
ctx.dom.subtitleRoot.style.fontFamily = resolveFontFamily(params.metrics.subFont);
|
||||
ctx.dom.subtitleRoot.style.setProperty(
|
||||
"letter-spacing",
|
||||
resolveLetterSpacing(
|
||||
params.metrics.subSpacing,
|
||||
params.pxPerScaledPixel,
|
||||
isMacOSPlatform,
|
||||
),
|
||||
isMacOSPlatform ? "important" : "",
|
||||
'letter-spacing',
|
||||
resolveLetterSpacing(params.metrics.subSpacing, params.pxPerScaledPixel, isMacOSPlatform),
|
||||
isMacOSPlatform ? 'important' : '',
|
||||
);
|
||||
ctx.dom.subtitleRoot.style.fontKerning = isMacOSPlatform ? "auto" : "none";
|
||||
ctx.dom.subtitleRoot.style.fontWeight = params.metrics.subBold
|
||||
? "700"
|
||||
: "400";
|
||||
ctx.dom.subtitleRoot.style.fontStyle = params.metrics.subItalic
|
||||
? "italic"
|
||||
: "normal";
|
||||
ctx.dom.subtitleRoot.style.transform = "";
|
||||
ctx.dom.subtitleRoot.style.transformOrigin = "";
|
||||
ctx.dom.subtitleRoot.style.fontKerning = isMacOSPlatform ? 'auto' : 'none';
|
||||
ctx.dom.subtitleRoot.style.fontWeight = params.metrics.subBold ? '700' : '400';
|
||||
ctx.dom.subtitleRoot.style.fontStyle = params.metrics.subItalic ? 'italic' : 'normal';
|
||||
ctx.dom.subtitleRoot.style.transform = '';
|
||||
ctx.dom.subtitleRoot.style.transformOrigin = '';
|
||||
|
||||
applyComputedLineHeightCompensation(ctx, params.effectiveFontSize);
|
||||
applyMacOSAdjustments(ctx);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MpvSubtitleRenderMetrics } from "../../types";
|
||||
import type { RendererContext } from "../context";
|
||||
import type { MpvSubtitleRenderMetrics } from '../../types';
|
||||
import type { RendererContext } from '../context';
|
||||
|
||||
export type SubtitleAlignment = { hAlign: 0 | 1 | 2; vAlign: 0 | 1 | 2 };
|
||||
|
||||
@@ -30,10 +30,9 @@ export function calculateOsdScale(
|
||||
return devicePixelRatio;
|
||||
}
|
||||
|
||||
const ratios = [
|
||||
dims.w / Math.max(1, viewportWidth),
|
||||
dims.h / Math.max(1, viewportHeight),
|
||||
].filter((value) => Number.isFinite(value) && value > 0);
|
||||
const ratios = [dims.w / Math.max(1, viewportWidth), dims.h / Math.max(1, viewportHeight)].filter(
|
||||
(value) => Number.isFinite(value) && value > 0,
|
||||
);
|
||||
|
||||
const avgRatio =
|
||||
ratios.length > 0
|
||||
@@ -74,10 +73,7 @@ export function applyPlatformFontCompensation(
|
||||
function calculateGeometry(
|
||||
metrics: MpvSubtitleRenderMetrics,
|
||||
osdToCssScale: number,
|
||||
): Omit<
|
||||
SubtitleLayoutGeometry,
|
||||
"marginY" | "marginX" | "pxPerScaledPixel" | "effectiveFontSize"
|
||||
> {
|
||||
): Omit<SubtitleLayoutGeometry, 'marginY' | 'marginX' | 'pxPerScaledPixel' | 'effectiveFontSize'> {
|
||||
const dims = metrics.osdDimensions;
|
||||
const renderAreaHeight = dims ? dims.h / osdToCssScale : window.innerHeight;
|
||||
const renderAreaWidth = dims ? dims.w / osdToCssScale : window.innerWidth;
|
||||
@@ -91,10 +87,7 @@ function calculateGeometry(
|
||||
const rightInset = anchorToVideoArea ? videoRightInset : 0;
|
||||
const topInset = anchorToVideoArea ? videoTopInset : 0;
|
||||
const bottomInset = anchorToVideoArea ? videoBottomInset : 0;
|
||||
const horizontalAvailable = Math.max(
|
||||
0,
|
||||
renderAreaWidth - leftInset - rightInset,
|
||||
);
|
||||
const horizontalAvailable = Math.max(0, renderAreaWidth - leftInset - rightInset);
|
||||
|
||||
return {
|
||||
renderAreaHeight,
|
||||
@@ -119,16 +112,11 @@ export function calculateSubtitleMetrics(
|
||||
window.devicePixelRatio || 1,
|
||||
);
|
||||
const geometry = calculateGeometry(metrics, osdToCssScale);
|
||||
const videoHeight =
|
||||
geometry.renderAreaHeight - geometry.topInset - geometry.bottomInset;
|
||||
const scaleRefHeight = metrics.subScaleByWindow
|
||||
? geometry.renderAreaHeight
|
||||
: videoHeight;
|
||||
const videoHeight = geometry.renderAreaHeight - geometry.topInset - geometry.bottomInset;
|
||||
const scaleRefHeight = metrics.subScaleByWindow ? geometry.renderAreaHeight : videoHeight;
|
||||
const pxPerScaledPixel = Math.max(0.1, scaleRefHeight / 720);
|
||||
const computedFontSize =
|
||||
metrics.subFontSize *
|
||||
metrics.subScale *
|
||||
(ctx.platform.isLinuxPlatform ? 1 : pxPerScaledPixel);
|
||||
metrics.subFontSize * metrics.subScale * (ctx.platform.isLinuxPlatform ? 1 : pxPerScaledPixel);
|
||||
const effectiveFontSize = applyPlatformFontCompensation(
|
||||
computedFontSize,
|
||||
ctx.platform.isMacOSPlatform,
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import type { MpvSubtitleRenderMetrics } from "../../types";
|
||||
import type { RendererContext } from "../context";
|
||||
import type { MpvSubtitleRenderMetrics } from '../../types';
|
||||
import type { RendererContext } from '../context';
|
||||
import {
|
||||
applyContainerBaseLayout,
|
||||
applyTypography,
|
||||
applyVerticalPosition,
|
||||
} from "./invisible-layout-helpers.js";
|
||||
import {
|
||||
calculateSubtitleMetrics,
|
||||
calculateSubtitlePosition,
|
||||
} from "./invisible-layout-metrics.js";
|
||||
} from './invisible-layout-helpers.js';
|
||||
import { calculateSubtitleMetrics, calculateSubtitlePosition } from './invisible-layout-metrics.js';
|
||||
|
||||
export type MpvSubtitleLayoutController = {
|
||||
applyInvisibleSubtitleLayoutFromMpvMetrics: (
|
||||
@@ -32,20 +29,12 @@ export function createMpvSubtitleLayoutController(
|
||||
ctx.state.mpvSubtitleRenderMetrics = metrics;
|
||||
|
||||
const geometry = calculateSubtitleMetrics(ctx, metrics);
|
||||
const alignment = calculateSubtitlePosition(
|
||||
metrics,
|
||||
geometry.pxPerScaledPixel,
|
||||
2,
|
||||
);
|
||||
const alignment = calculateSubtitlePosition(metrics, geometry.pxPerScaledPixel, 2);
|
||||
|
||||
applySubtitleFontSize(geometry.effectiveFontSize);
|
||||
const effectiveBorderSize =
|
||||
metrics.subBorderSize * geometry.pxPerScaledPixel;
|
||||
const effectiveBorderSize = metrics.subBorderSize * geometry.pxPerScaledPixel;
|
||||
|
||||
document.documentElement.style.setProperty(
|
||||
"--sub-border-size",
|
||||
`${effectiveBorderSize}px`,
|
||||
);
|
||||
document.documentElement.style.setProperty('--sub-border-size', `${effectiveBorderSize}px`);
|
||||
|
||||
applyContainerBaseLayout(ctx, {
|
||||
horizontalAvailable: Math.max(
|
||||
@@ -73,26 +62,18 @@ export function createMpvSubtitleLayoutController(
|
||||
effectiveFontSize: geometry.effectiveFontSize,
|
||||
});
|
||||
|
||||
ctx.state.invisibleLayoutBaseLeftPx =
|
||||
parseFloat(ctx.dom.subtitleContainer.style.left) || 0;
|
||||
ctx.state.invisibleLayoutBaseLeftPx = parseFloat(ctx.dom.subtitleContainer.style.left) || 0;
|
||||
|
||||
const parsedBottom = parseFloat(ctx.dom.subtitleContainer.style.bottom);
|
||||
ctx.state.invisibleLayoutBaseBottomPx = Number.isFinite(parsedBottom)
|
||||
? parsedBottom
|
||||
: null;
|
||||
ctx.state.invisibleLayoutBaseBottomPx = Number.isFinite(parsedBottom) ? parsedBottom : null;
|
||||
|
||||
const parsedTop = parseFloat(ctx.dom.subtitleContainer.style.top);
|
||||
ctx.state.invisibleLayoutBaseTopPx = Number.isFinite(parsedTop)
|
||||
? parsedTop
|
||||
: null;
|
||||
ctx.state.invisibleLayoutBaseTopPx = Number.isFinite(parsedTop) ? parsedTop : null;
|
||||
|
||||
options.applyInvisibleSubtitleOffsetPosition();
|
||||
options.updateInvisiblePositionEditHud();
|
||||
|
||||
console.log(
|
||||
"[invisible-overlay] Applied mpv subtitle render metrics from",
|
||||
source,
|
||||
);
|
||||
console.log('[invisible-overlay] Applied mpv subtitle render metrics from', source);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import type { SubtitlePosition } from "../../types";
|
||||
import type { ModalStateReader, RendererContext } from "../context";
|
||||
import type { SubtitlePosition } from '../../types';
|
||||
import type { ModalStateReader, RendererContext } from '../context';
|
||||
|
||||
export type InvisibleOffsetController = {
|
||||
applyInvisibleStoredSubtitlePosition: (
|
||||
position: SubtitlePosition | null,
|
||||
source: string,
|
||||
) => void;
|
||||
applyInvisibleStoredSubtitlePosition: (position: SubtitlePosition | null, source: string) => void;
|
||||
applyInvisibleSubtitleOffsetPosition: () => void;
|
||||
updateInvisiblePositionEditHud: () => void;
|
||||
setInvisiblePositionEditMode: (enabled: boolean) => void;
|
||||
@@ -26,17 +23,15 @@ function createEditPositionText(ctx: RendererContext): string {
|
||||
}
|
||||
|
||||
function applyOffsetByBasePosition(ctx: RendererContext): void {
|
||||
const nextLeft =
|
||||
ctx.state.invisibleLayoutBaseLeftPx + ctx.state.invisibleSubtitleOffsetXPx;
|
||||
const nextLeft = ctx.state.invisibleLayoutBaseLeftPx + ctx.state.invisibleSubtitleOffsetXPx;
|
||||
ctx.dom.subtitleContainer.style.left = `${nextLeft}px`;
|
||||
|
||||
if (ctx.state.invisibleLayoutBaseBottomPx !== null) {
|
||||
ctx.dom.subtitleContainer.style.bottom = `${Math.max(
|
||||
0,
|
||||
ctx.state.invisibleLayoutBaseBottomPx +
|
||||
ctx.state.invisibleSubtitleOffsetYPx,
|
||||
ctx.state.invisibleLayoutBaseBottomPx + ctx.state.invisibleSubtitleOffsetYPx,
|
||||
)}px`;
|
||||
ctx.dom.subtitleContainer.style.top = "";
|
||||
ctx.dom.subtitleContainer.style.top = '';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -45,36 +40,31 @@ function applyOffsetByBasePosition(ctx: RendererContext): void {
|
||||
0,
|
||||
ctx.state.invisibleLayoutBaseTopPx - ctx.state.invisibleSubtitleOffsetYPx,
|
||||
)}px`;
|
||||
ctx.dom.subtitleContainer.style.bottom = "";
|
||||
ctx.dom.subtitleContainer.style.bottom = '';
|
||||
}
|
||||
}
|
||||
|
||||
export function createInvisibleOffsetController(
|
||||
ctx: RendererContext,
|
||||
modalStateReader: Pick<ModalStateReader, "isAnySettingsModalOpen">,
|
||||
modalStateReader: Pick<ModalStateReader, 'isAnySettingsModalOpen'>,
|
||||
): InvisibleOffsetController {
|
||||
function setInvisiblePositionEditMode(enabled: boolean): void {
|
||||
if (!ctx.platform.isInvisibleLayer) return;
|
||||
if (ctx.state.invisiblePositionEditMode === enabled) return;
|
||||
|
||||
ctx.state.invisiblePositionEditMode = enabled;
|
||||
document.body.classList.toggle("invisible-position-edit", enabled);
|
||||
document.body.classList.toggle('invisible-position-edit', enabled);
|
||||
|
||||
if (enabled) {
|
||||
ctx.state.invisiblePositionEditStartX =
|
||||
ctx.state.invisibleSubtitleOffsetXPx;
|
||||
ctx.state.invisiblePositionEditStartY =
|
||||
ctx.state.invisibleSubtitleOffsetYPx;
|
||||
ctx.dom.overlay.classList.add("interactive");
|
||||
ctx.state.invisiblePositionEditStartX = ctx.state.invisibleSubtitleOffsetXPx;
|
||||
ctx.state.invisiblePositionEditStartY = ctx.state.invisibleSubtitleOffsetYPx;
|
||||
ctx.dom.overlay.classList.add('interactive');
|
||||
if (ctx.platform.shouldToggleMouseIgnore) {
|
||||
window.electronAPI.setIgnoreMouseEvents(false);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
!ctx.state.isOverSubtitle &&
|
||||
!modalStateReader.isAnySettingsModalOpen()
|
||||
) {
|
||||
ctx.dom.overlay.classList.remove("interactive");
|
||||
if (!ctx.state.isOverSubtitle && !modalStateReader.isAnySettingsModalOpen()) {
|
||||
ctx.dom.overlay.classList.remove('interactive');
|
||||
if (ctx.platform.shouldToggleMouseIgnore) {
|
||||
window.electronAPI.setIgnoreMouseEvents(true, { forward: true });
|
||||
}
|
||||
@@ -86,8 +76,7 @@ export function createInvisibleOffsetController(
|
||||
|
||||
function updateInvisiblePositionEditHud(): void {
|
||||
if (!ctx.state.invisiblePositionEditHud) return;
|
||||
ctx.state.invisiblePositionEditHud.textContent =
|
||||
createEditPositionText(ctx);
|
||||
ctx.state.invisiblePositionEditHud.textContent = createEditPositionText(ctx);
|
||||
}
|
||||
|
||||
function applyInvisibleSubtitleOffsetPosition(): void {
|
||||
@@ -98,11 +87,7 @@ export function createInvisibleOffsetController(
|
||||
position: SubtitlePosition | null,
|
||||
source: string,
|
||||
): void {
|
||||
if (
|
||||
position &&
|
||||
typeof position.yPercent === "number" &&
|
||||
Number.isFinite(position.yPercent)
|
||||
) {
|
||||
if (position && typeof position.yPercent === 'number' && Number.isFinite(position.yPercent)) {
|
||||
ctx.state.persistedSubtitlePosition = {
|
||||
...ctx.state.persistedSubtitlePosition,
|
||||
yPercent: position.yPercent,
|
||||
@@ -111,12 +96,12 @@ export function createInvisibleOffsetController(
|
||||
|
||||
if (position) {
|
||||
const nextX =
|
||||
typeof position.invisibleOffsetXPx === "number" &&
|
||||
typeof position.invisibleOffsetXPx === 'number' &&
|
||||
Number.isFinite(position.invisibleOffsetXPx)
|
||||
? position.invisibleOffsetXPx
|
||||
: 0;
|
||||
const nextY =
|
||||
typeof position.invisibleOffsetYPx === "number" &&
|
||||
typeof position.invisibleOffsetYPx === 'number' &&
|
||||
Number.isFinite(position.invisibleOffsetYPx)
|
||||
? position.invisibleOffsetYPx
|
||||
: 0;
|
||||
@@ -129,7 +114,7 @@ export function createInvisibleOffsetController(
|
||||
|
||||
applyOffsetByBasePosition(ctx);
|
||||
console.log(
|
||||
"[invisible-overlay] Applied subtitle offset from",
|
||||
'[invisible-overlay] Applied subtitle offset from',
|
||||
source,
|
||||
`${ctx.state.invisibleSubtitleOffsetXPx}px`,
|
||||
`${ctx.state.invisibleSubtitleOffsetYPx}px`,
|
||||
@@ -148,19 +133,17 @@ export function createInvisibleOffsetController(
|
||||
}
|
||||
|
||||
function cancelInvisiblePositionEdit(): void {
|
||||
ctx.state.invisibleSubtitleOffsetXPx =
|
||||
ctx.state.invisiblePositionEditStartX;
|
||||
ctx.state.invisibleSubtitleOffsetYPx =
|
||||
ctx.state.invisiblePositionEditStartY;
|
||||
ctx.state.invisibleSubtitleOffsetXPx = ctx.state.invisiblePositionEditStartX;
|
||||
ctx.state.invisibleSubtitleOffsetYPx = ctx.state.invisiblePositionEditStartY;
|
||||
applyOffsetByBasePosition(ctx);
|
||||
setInvisiblePositionEditMode(false);
|
||||
}
|
||||
|
||||
function setupInvisiblePositionEditHud(): void {
|
||||
if (!ctx.platform.isInvisibleLayer) return;
|
||||
const hud = document.createElement("div");
|
||||
hud.id = "invisiblePositionEditHud";
|
||||
hud.className = "invisible-position-edit-hud";
|
||||
const hud = document.createElement('div');
|
||||
hud.id = 'invisiblePositionEditHud';
|
||||
hud.className = 'invisible-position-edit-hud';
|
||||
ctx.dom.overlay.appendChild(hud);
|
||||
ctx.state.invisiblePositionEditHud = hud;
|
||||
updateInvisiblePositionEditHud();
|
||||
|
||||
@@ -1,35 +1,22 @@
|
||||
import type { SubtitlePosition } from "../../types";
|
||||
import type { RendererContext } from "../context";
|
||||
import type { SubtitlePosition } from '../../types';
|
||||
import type { RendererContext } from '../context';
|
||||
|
||||
const PREFERRED_Y_PERCENT_MIN = 2;
|
||||
const PREFERRED_Y_PERCENT_MAX = 80;
|
||||
|
||||
export type SubtitlePositionController = {
|
||||
applyStoredSubtitlePosition: (
|
||||
position: SubtitlePosition | null,
|
||||
source: string,
|
||||
) => void;
|
||||
applyStoredSubtitlePosition: (position: SubtitlePosition | null, source: string) => void;
|
||||
getCurrentYPercent: () => number;
|
||||
applyYPercent: (yPercent: number) => void;
|
||||
persistSubtitlePositionPatch: (patch: Partial<SubtitlePosition>) => void;
|
||||
};
|
||||
|
||||
function clampYPercent(yPercent: number): number {
|
||||
return Math.max(
|
||||
PREFERRED_Y_PERCENT_MIN,
|
||||
Math.min(PREFERRED_Y_PERCENT_MAX, yPercent),
|
||||
);
|
||||
return Math.max(PREFERRED_Y_PERCENT_MIN, Math.min(PREFERRED_Y_PERCENT_MAX, yPercent));
|
||||
}
|
||||
|
||||
function getPersistedYPercent(
|
||||
ctx: RendererContext,
|
||||
position: SubtitlePosition | null,
|
||||
): number {
|
||||
if (
|
||||
!position ||
|
||||
typeof position.yPercent !== "number" ||
|
||||
!Number.isFinite(position.yPercent)
|
||||
) {
|
||||
function getPersistedYPercent(ctx: RendererContext, position: SubtitlePosition | null): number {
|
||||
if (!position || typeof position.yPercent !== 'number' || !Number.isFinite(position.yPercent)) {
|
||||
return ctx.state.persistedSubtitlePosition.yPercent;
|
||||
}
|
||||
|
||||
@@ -39,13 +26,9 @@ function getPersistedYPercent(
|
||||
function getPersistedOffset(
|
||||
ctx: RendererContext,
|
||||
position: SubtitlePosition | null,
|
||||
key: "invisibleOffsetXPx" | "invisibleOffsetYPx",
|
||||
key: 'invisibleOffsetXPx' | 'invisibleOffsetYPx',
|
||||
): number {
|
||||
if (
|
||||
position &&
|
||||
typeof position[key] === "number" &&
|
||||
Number.isFinite(position[key])
|
||||
) {
|
||||
if (position && typeof position[key] === 'number' && Number.isFinite(position[key])) {
|
||||
return position[key];
|
||||
}
|
||||
|
||||
@@ -58,8 +41,8 @@ function updatePersistedSubtitlePosition(
|
||||
): void {
|
||||
ctx.state.persistedSubtitlePosition = {
|
||||
yPercent: getPersistedYPercent(ctx, position),
|
||||
invisibleOffsetXPx: getPersistedOffset(ctx, position, "invisibleOffsetXPx"),
|
||||
invisibleOffsetYPx: getPersistedOffset(ctx, position, "invisibleOffsetYPx"),
|
||||
invisibleOffsetXPx: getPersistedOffset(ctx, position, 'invisibleOffsetXPx'),
|
||||
invisibleOffsetYPx: getPersistedOffset(ctx, position, 'invisibleOffsetYPx'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -69,17 +52,15 @@ function getNextPersistedPosition(
|
||||
): SubtitlePosition {
|
||||
return {
|
||||
yPercent:
|
||||
typeof patch.yPercent === "number" && Number.isFinite(patch.yPercent)
|
||||
typeof patch.yPercent === 'number' && Number.isFinite(patch.yPercent)
|
||||
? patch.yPercent
|
||||
: ctx.state.persistedSubtitlePosition.yPercent,
|
||||
invisibleOffsetXPx:
|
||||
typeof patch.invisibleOffsetXPx === "number" &&
|
||||
Number.isFinite(patch.invisibleOffsetXPx)
|
||||
typeof patch.invisibleOffsetXPx === 'number' && Number.isFinite(patch.invisibleOffsetXPx)
|
||||
? patch.invisibleOffsetXPx
|
||||
: (ctx.state.persistedSubtitlePosition.invisibleOffsetXPx ?? 0),
|
||||
invisibleOffsetYPx:
|
||||
typeof patch.invisibleOffsetYPx === "number" &&
|
||||
Number.isFinite(patch.invisibleOffsetYPx)
|
||||
typeof patch.invisibleOffsetYPx === 'number' && Number.isFinite(patch.invisibleOffsetYPx)
|
||||
? patch.invisibleOffsetYPx
|
||||
: (ctx.state.persistedSubtitlePosition.invisibleOffsetYPx ?? 0),
|
||||
};
|
||||
@@ -93,11 +74,8 @@ export function createInMemorySubtitlePositionController(
|
||||
return ctx.state.currentYPercent;
|
||||
}
|
||||
|
||||
const marginBottom =
|
||||
parseFloat(ctx.dom.subtitleContainer.style.marginBottom) || 60;
|
||||
ctx.state.currentYPercent = clampYPercent(
|
||||
(marginBottom / window.innerHeight) * 100,
|
||||
);
|
||||
const marginBottom = parseFloat(ctx.dom.subtitleContainer.style.marginBottom) || 60;
|
||||
ctx.state.currentYPercent = clampYPercent((marginBottom / window.innerHeight) * 100);
|
||||
return ctx.state.currentYPercent;
|
||||
}
|
||||
|
||||
@@ -106,43 +84,32 @@ export function createInMemorySubtitlePositionController(
|
||||
ctx.state.currentYPercent = clampedPercent;
|
||||
const marginBottom = (clampedPercent / 100) * window.innerHeight;
|
||||
|
||||
ctx.dom.subtitleContainer.style.position = "";
|
||||
ctx.dom.subtitleContainer.style.left = "";
|
||||
ctx.dom.subtitleContainer.style.top = "";
|
||||
ctx.dom.subtitleContainer.style.right = "";
|
||||
ctx.dom.subtitleContainer.style.transform = "";
|
||||
ctx.dom.subtitleContainer.style.position = '';
|
||||
ctx.dom.subtitleContainer.style.left = '';
|
||||
ctx.dom.subtitleContainer.style.top = '';
|
||||
ctx.dom.subtitleContainer.style.right = '';
|
||||
ctx.dom.subtitleContainer.style.transform = '';
|
||||
ctx.dom.subtitleContainer.style.marginBottom = `${marginBottom}px`;
|
||||
}
|
||||
|
||||
function persistSubtitlePositionPatch(
|
||||
patch: Partial<SubtitlePosition>,
|
||||
): void {
|
||||
function persistSubtitlePositionPatch(patch: Partial<SubtitlePosition>): void {
|
||||
const nextPosition = getNextPersistedPosition(ctx, patch);
|
||||
ctx.state.persistedSubtitlePosition = nextPosition;
|
||||
window.electronAPI.saveSubtitlePosition(nextPosition);
|
||||
}
|
||||
|
||||
function applyStoredSubtitlePosition(
|
||||
position: SubtitlePosition | null,
|
||||
source: string,
|
||||
): void {
|
||||
function applyStoredSubtitlePosition(position: SubtitlePosition | null, source: string): void {
|
||||
updatePersistedSubtitlePosition(ctx, position);
|
||||
if (position && position.yPercent !== undefined) {
|
||||
applyYPercent(position.yPercent);
|
||||
console.log(
|
||||
"Applied subtitle position from",
|
||||
source,
|
||||
":",
|
||||
position.yPercent,
|
||||
"%",
|
||||
);
|
||||
console.log('Applied subtitle position from', source, ':', position.yPercent, '%');
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultMarginBottom = 60;
|
||||
const defaultYPercent = (defaultMarginBottom / window.innerHeight) * 100;
|
||||
applyYPercent(defaultYPercent);
|
||||
console.log("Applied default subtitle position from", source);
|
||||
console.log('Applied default subtitle position from', source);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -24,20 +24,20 @@ import type {
|
||||
SubtitleData,
|
||||
SubtitlePosition,
|
||||
SubsyncManualPayload,
|
||||
} from "../types";
|
||||
import { createKeyboardHandlers } from "./handlers/keyboard.js";
|
||||
import { createMouseHandlers } from "./handlers/mouse.js";
|
||||
import { createJimakuModal } from "./modals/jimaku.js";
|
||||
import { createKikuModal } from "./modals/kiku.js";
|
||||
import { createSessionHelpModal } from "./modals/session-help.js";
|
||||
import { createRuntimeOptionsModal } from "./modals/runtime-options.js";
|
||||
import { createSubsyncModal } from "./modals/subsync.js";
|
||||
import { createPositioningController } from "./positioning.js";
|
||||
import { createOverlayContentMeasurementReporter } from "./overlay-content-measurement.js";
|
||||
import { createRendererState } from "./state.js";
|
||||
import { createSubtitleRenderer } from "./subtitle-render.js";
|
||||
import { resolveRendererDom } from "./utils/dom.js";
|
||||
import { resolvePlatformInfo } from "./utils/platform.js";
|
||||
} from '../types';
|
||||
import { createKeyboardHandlers } from './handlers/keyboard.js';
|
||||
import { createMouseHandlers } from './handlers/mouse.js';
|
||||
import { createJimakuModal } from './modals/jimaku.js';
|
||||
import { createKikuModal } from './modals/kiku.js';
|
||||
import { createSessionHelpModal } from './modals/session-help.js';
|
||||
import { createRuntimeOptionsModal } from './modals/runtime-options.js';
|
||||
import { createSubsyncModal } from './modals/subsync.js';
|
||||
import { createPositioningController } from './positioning.js';
|
||||
import { createOverlayContentMeasurementReporter } from './overlay-content-measurement.js';
|
||||
import { createRendererState } from './state.js';
|
||||
import { createSubtitleRenderer } from './subtitle-render.js';
|
||||
import { resolveRendererDom } from './utils/dom.js';
|
||||
import { resolvePlatformInfo } from './utils/platform.js';
|
||||
|
||||
const ctx = {
|
||||
dom: resolveRendererDom(),
|
||||
@@ -67,7 +67,7 @@ function isAnyModalOpen(): boolean {
|
||||
|
||||
function syncSettingsModalSubtitleSuppression(): void {
|
||||
const suppressSubtitles = isAnySettingsModalOpen();
|
||||
document.body.classList.toggle("settings-modal-open", suppressSubtitles);
|
||||
document.body.classList.toggle('settings-modal-open', suppressSubtitles);
|
||||
if (suppressSubtitles) {
|
||||
ctx.state.isOverSubtitle = false;
|
||||
}
|
||||
@@ -109,8 +109,7 @@ const keyboardHandlers = createKeyboardHandlers(ctx, {
|
||||
saveInvisiblePositionEdit: positioning.saveInvisiblePositionEdit,
|
||||
cancelInvisiblePositionEdit: positioning.cancelInvisiblePositionEdit,
|
||||
setInvisiblePositionEditMode: positioning.setInvisiblePositionEditMode,
|
||||
applyInvisibleSubtitleOffsetPosition:
|
||||
positioning.applyInvisibleSubtitleOffsetPosition,
|
||||
applyInvisibleSubtitleOffsetPosition: positioning.applyInvisibleSubtitleOffsetPosition,
|
||||
updateInvisiblePositionEditHud: positioning.updateInvisiblePositionEditHud,
|
||||
});
|
||||
const mouseHandlers = createMouseHandlers(ctx, {
|
||||
@@ -132,28 +131,20 @@ async function init(): Promise<void> {
|
||||
|
||||
window.electronAPI.onSubtitlePosition((position: SubtitlePosition | null) => {
|
||||
if (ctx.platform.isInvisibleLayer) {
|
||||
positioning.applyInvisibleStoredSubtitlePosition(
|
||||
position,
|
||||
"media-change",
|
||||
);
|
||||
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) => {
|
||||
positioning.applyInvisibleSubtitleLayoutFromMpvMetrics(
|
||||
metrics,
|
||||
"event",
|
||||
);
|
||||
measurementReporter.schedule();
|
||||
},
|
||||
);
|
||||
window.electronAPI.onMpvSubtitleRenderMetrics((metrics: MpvSubtitleRenderMetrics) => {
|
||||
positioning.applyInvisibleSubtitleLayoutFromMpvMetrics(metrics, 'event');
|
||||
measurementReporter.schedule();
|
||||
});
|
||||
window.electronAPI.onOverlayDebugVisualization((enabled: boolean) => {
|
||||
document.body.classList.toggle("debug-invisible-visualization", enabled);
|
||||
document.body.classList.toggle('debug-invisible-visualization', enabled);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -170,34 +161,24 @@ async function init(): Promise<void> {
|
||||
measurementReporter.schedule();
|
||||
});
|
||||
|
||||
subtitleRenderer.updateSecondarySubMode(
|
||||
await window.electronAPI.getSecondarySubMode(),
|
||||
);
|
||||
subtitleRenderer.renderSecondarySub(
|
||||
await window.electronAPI.getCurrentSecondarySub(),
|
||||
);
|
||||
subtitleRenderer.updateSecondarySubMode(await window.electronAPI.getSecondarySubMode());
|
||||
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.secondarySubContainer.addEventListener(
|
||||
"mouseenter",
|
||||
mouseHandlers.handleMouseEnter,
|
||||
);
|
||||
ctx.dom.secondarySubContainer.addEventListener(
|
||||
"mouseleave",
|
||||
mouseHandlers.handleMouseLeave,
|
||||
);
|
||||
hoverTarget.addEventListener('mouseenter', mouseHandlers.handleMouseEnter);
|
||||
hoverTarget.addEventListener('mouseleave', mouseHandlers.handleMouseLeave);
|
||||
ctx.dom.secondarySubContainer.addEventListener('mouseenter', mouseHandlers.handleMouseEnter);
|
||||
ctx.dom.secondarySubContainer.addEventListener('mouseleave', mouseHandlers.handleMouseLeave);
|
||||
|
||||
mouseHandlers.setupInvisibleHoverSelection();
|
||||
positioning.setupInvisiblePositionEditHud();
|
||||
mouseHandlers.setupResizeHandler();
|
||||
mouseHandlers.setupSelectionObserver();
|
||||
mouseHandlers.setupYomitanObserver();
|
||||
window.addEventListener("resize", () => {
|
||||
window.addEventListener('resize', () => {
|
||||
measurementReporter.schedule();
|
||||
});
|
||||
|
||||
@@ -207,18 +188,13 @@ async function init(): Promise<void> {
|
||||
subsyncModal.wireDomEvents();
|
||||
sessionHelpModal.wireDomEvents();
|
||||
|
||||
window.electronAPI.onRuntimeOptionsChanged(
|
||||
(options: RuntimeOptionState[]) => {
|
||||
runtimeOptionsModal.updateRuntimeOptions(options);
|
||||
},
|
||||
);
|
||||
window.electronAPI.onRuntimeOptionsChanged((options: RuntimeOptionState[]) => {
|
||||
runtimeOptionsModal.updateRuntimeOptions(options);
|
||||
});
|
||||
window.electronAPI.onOpenRuntimeOptions(() => {
|
||||
runtimeOptionsModal.openRuntimeOptionsModal().catch(() => {
|
||||
runtimeOptionsModal.setRuntimeOptionsStatus(
|
||||
"Failed to load runtime options",
|
||||
true,
|
||||
);
|
||||
window.electronAPI.notifyOverlayModalClosed("runtime-options");
|
||||
runtimeOptionsModal.setRuntimeOptionsStatus('Failed to load runtime options', true);
|
||||
window.electronAPI.notifyOverlayModalClosed('runtime-options');
|
||||
syncSettingsModalSubtitleSuppression();
|
||||
});
|
||||
});
|
||||
@@ -229,10 +205,7 @@ async function init(): Promise<void> {
|
||||
subsyncModal.openSubsyncModal(payload);
|
||||
});
|
||||
window.electronAPI.onKikuFieldGroupingRequest(
|
||||
(data: {
|
||||
original: KikuDuplicateCardInfo;
|
||||
duplicate: KikuDuplicateCardInfo;
|
||||
}) => {
|
||||
(data: { original: KikuDuplicateCardInfo; duplicate: KikuDuplicateCardInfo }) => {
|
||||
kikuModal.openKikuFieldGroupingModal(data);
|
||||
},
|
||||
);
|
||||
@@ -243,23 +216,21 @@ async function init(): Promise<void> {
|
||||
|
||||
await keyboardHandlers.setupMpvInputForwarding();
|
||||
|
||||
subtitleRenderer.applySubtitleStyle(
|
||||
await window.electronAPI.getSubtitleStyle(),
|
||||
);
|
||||
subtitleRenderer.applySubtitleStyle(await window.electronAPI.getSubtitleStyle());
|
||||
|
||||
if (ctx.platform.isInvisibleLayer) {
|
||||
positioning.applyInvisibleStoredSubtitlePosition(
|
||||
await window.electronAPI.getSubtitlePosition(),
|
||||
"startup",
|
||||
'startup',
|
||||
);
|
||||
positioning.applyInvisibleSubtitleLayoutFromMpvMetrics(
|
||||
await window.electronAPI.getMpvSubtitleRenderMetrics(),
|
||||
"startup",
|
||||
'startup',
|
||||
);
|
||||
} else {
|
||||
positioning.applyStoredSubtitlePosition(
|
||||
await window.electronAPI.getSubtitlePosition(),
|
||||
"startup",
|
||||
'startup',
|
||||
);
|
||||
measurementReporter.schedule();
|
||||
}
|
||||
@@ -271,8 +242,8 @@ async function init(): Promise<void> {
|
||||
measurementReporter.emitNow();
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
void init();
|
||||
}
|
||||
|
||||
@@ -9,15 +9,15 @@ import type {
|
||||
RuntimeOptionValue,
|
||||
SubtitlePosition,
|
||||
SubsyncSourceTrack,
|
||||
} from "../types";
|
||||
} from '../types';
|
||||
|
||||
export type KikuModalStep = "select" | "preview";
|
||||
export type KikuPreviewMode = "compact" | "full";
|
||||
export type KikuModalStep = 'select' | 'preview';
|
||||
export type KikuPreviewMode = 'compact' | 'full';
|
||||
|
||||
export type ChordAction =
|
||||
| { type: "mpv"; command: string[] }
|
||||
| { type: "electron"; action: () => void }
|
||||
| { type: "noop" };
|
||||
| { type: 'mpv'; command: string[] }
|
||||
| { type: 'electron'; action: () => void }
|
||||
| { type: 'noop' };
|
||||
|
||||
export type RendererState = {
|
||||
isOverSubtitle: boolean;
|
||||
@@ -81,7 +81,7 @@ export type RendererState = {
|
||||
jlptN5Color: string;
|
||||
frequencyDictionaryEnabled: boolean;
|
||||
frequencyDictionaryTopX: number;
|
||||
frequencyDictionaryMode: "single" | "banded";
|
||||
frequencyDictionaryMode: 'single' | 'banded';
|
||||
frequencyDictionarySingleColor: string;
|
||||
frequencyDictionaryBand1Color: string;
|
||||
frequencyDictionaryBand2Color: string;
|
||||
@@ -115,8 +115,8 @@ export function createRendererState(): RendererState {
|
||||
kikuSelectedCard: 1,
|
||||
kikuOriginalData: null,
|
||||
kikuDuplicateData: null,
|
||||
kikuModalStep: "select",
|
||||
kikuPreviewMode: "compact",
|
||||
kikuModalStep: 'select',
|
||||
kikuPreviewMode: 'compact',
|
||||
kikuPendingChoice: null,
|
||||
kikuPreviewCompactData: null,
|
||||
kikuPreviewFullData: null,
|
||||
@@ -145,25 +145,25 @@ export function createRendererState(): RendererState {
|
||||
invisiblePositionEditHud: null,
|
||||
currentInvisibleSubtitleLineCount: 1,
|
||||
|
||||
lastHoverSelectionKey: "",
|
||||
lastHoverSelectionKey: '',
|
||||
lastHoverSelectionNode: null,
|
||||
|
||||
knownWordColor: "#a6da95",
|
||||
nPlusOneColor: "#c6a0f6",
|
||||
jlptN1Color: "#ed8796",
|
||||
jlptN2Color: "#f5a97f",
|
||||
jlptN3Color: "#f9e2af",
|
||||
jlptN4Color: "#a6e3a1",
|
||||
jlptN5Color: "#8aadf4",
|
||||
knownWordColor: '#a6da95',
|
||||
nPlusOneColor: '#c6a0f6',
|
||||
jlptN1Color: '#ed8796',
|
||||
jlptN2Color: '#f5a97f',
|
||||
jlptN3Color: '#f9e2af',
|
||||
jlptN4Color: '#a6e3a1',
|
||||
jlptN5Color: '#8aadf4',
|
||||
frequencyDictionaryEnabled: false,
|
||||
frequencyDictionaryTopX: 1000,
|
||||
frequencyDictionaryMode: "single",
|
||||
frequencyDictionarySingleColor: "#f5a97f",
|
||||
frequencyDictionaryBand1Color: "#ed8796",
|
||||
frequencyDictionaryBand2Color: "#f5a97f",
|
||||
frequencyDictionaryBand3Color: "#f9e2af",
|
||||
frequencyDictionaryBand4Color: "#a6e3a1",
|
||||
frequencyDictionaryBand5Color: "#8aadf4",
|
||||
frequencyDictionaryMode: 'single',
|
||||
frequencyDictionarySingleColor: '#f5a97f',
|
||||
frequencyDictionaryBand1Color: '#ed8796',
|
||||
frequencyDictionaryBand2Color: '#f5a97f',
|
||||
frequencyDictionaryBand3Color: '#f9e2af',
|
||||
frequencyDictionaryBand4Color: '#a6e3a1',
|
||||
frequencyDictionaryBand5Color: '#8aadf4',
|
||||
|
||||
keybindingsMap: new Map(),
|
||||
chordPending: false,
|
||||
|
||||
@@ -29,8 +29,7 @@ body {
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
font-family:
|
||||
"Noto Sans CJK JP Regular", "Noto Sans CJK JP", "Arial Unicode MS", Arial,
|
||||
sans-serif;
|
||||
'Noto Sans CJK JP Regular', 'Noto Sans CJK JP', 'Arial Unicode MS', Arial, sans-serif;
|
||||
}
|
||||
|
||||
#overlay {
|
||||
@@ -392,7 +391,7 @@ body.settings-modal-open #subtitleContainer {
|
||||
|
||||
#subtitleRoot br {
|
||||
display: block;
|
||||
content: "";
|
||||
content: '';
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
|
||||
@@ -558,7 +557,7 @@ body.settings-modal-open #secondarySubContainer {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
iframe[id^="yomitan-popup"] {
|
||||
iframe[id^='yomitan-popup'] {
|
||||
pointer-events: auto !important;
|
||||
z-index: 2147483647 !important;
|
||||
}
|
||||
@@ -991,7 +990,9 @@ iframe[id^="yomitan-popup"] {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
white-space: nowrap;
|
||||
padding: 4px 9px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||
monospace;
|
||||
border-radius: 999px;
|
||||
background: rgba(137, 180, 255, 0.16);
|
||||
border: 1px solid rgba(137, 180, 255, 0.35);
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import type { MergedToken } from "../types";
|
||||
import { PartOfSpeech } from "../types.js";
|
||||
import { computeWordClass } from "./subtitle-render.js";
|
||||
import type { MergedToken } from '../types';
|
||||
import { PartOfSpeech } from '../types.js';
|
||||
import { computeWordClass } from './subtitle-render.js';
|
||||
|
||||
function createToken(overrides: Partial<MergedToken>): MergedToken {
|
||||
return {
|
||||
surface: "",
|
||||
reading: "",
|
||||
headword: "",
|
||||
surface: '',
|
||||
reading: '',
|
||||
headword: '',
|
||||
startPos: 0,
|
||||
endPos: 0,
|
||||
partOfSpeech: PartOfSpeech.other,
|
||||
@@ -25,14 +25,14 @@ function createToken(overrides: Partial<MergedToken>): MergedToken {
|
||||
function extractClassBlock(cssText: string, selector: string): string {
|
||||
const ruleRegex = /([^{}]+)\{([^}]*)\}/g;
|
||||
let match: RegExpExecArray | null = null;
|
||||
let fallbackBlock = "";
|
||||
let fallbackBlock = '';
|
||||
|
||||
while ((match = ruleRegex.exec(cssText)) !== null) {
|
||||
const selectorsBlock = match[1]?.trim() ?? "";
|
||||
const selectorBlock = match[2] ?? "";
|
||||
const selectorsBlock = match[1]?.trim() ?? '';
|
||||
const selectorBlock = match[2] ?? '';
|
||||
|
||||
const selectors = selectorsBlock
|
||||
.split(",")
|
||||
.split(',')
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => entry.length > 0);
|
||||
|
||||
@@ -51,221 +51,173 @@ function extractClassBlock(cssText: string, selector: string): string {
|
||||
return fallbackBlock;
|
||||
}
|
||||
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
|
||||
test("computeWordClass preserves known and n+1 classes while adding JLPT classes", () => {
|
||||
test('computeWordClass preserves known and n+1 classes while adding JLPT classes', () => {
|
||||
const knownJlpt = createToken({
|
||||
isKnown: true,
|
||||
jlptLevel: "N1",
|
||||
surface: "猫",
|
||||
jlptLevel: 'N1',
|
||||
surface: '猫',
|
||||
});
|
||||
const nPlusOneJlpt = createToken({
|
||||
isNPlusOneTarget: true,
|
||||
jlptLevel: "N2",
|
||||
surface: "犬",
|
||||
jlptLevel: 'N2',
|
||||
surface: '犬',
|
||||
});
|
||||
|
||||
assert.equal(computeWordClass(knownJlpt), "word word-known word-jlpt-n1");
|
||||
assert.equal(
|
||||
computeWordClass(nPlusOneJlpt),
|
||||
"word word-n-plus-one word-jlpt-n2",
|
||||
);
|
||||
assert.equal(computeWordClass(knownJlpt), 'word word-known word-jlpt-n1');
|
||||
assert.equal(computeWordClass(nPlusOneJlpt), 'word word-n-plus-one word-jlpt-n2');
|
||||
});
|
||||
|
||||
test("computeWordClass does not add frequency class to known or N+1 terms", () => {
|
||||
test('computeWordClass does not add frequency class to known or N+1 terms', () => {
|
||||
const known = createToken({
|
||||
isKnown: true,
|
||||
frequencyRank: 10,
|
||||
surface: "既知",
|
||||
surface: '既知',
|
||||
});
|
||||
const nPlusOne = createToken({
|
||||
isNPlusOneTarget: true,
|
||||
frequencyRank: 10,
|
||||
surface: "目標",
|
||||
surface: '目標',
|
||||
});
|
||||
const frequency = createToken({
|
||||
frequencyRank: 10,
|
||||
surface: "頻度",
|
||||
surface: '頻度',
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
computeWordClass(known, {
|
||||
enabled: true,
|
||||
topX: 100,
|
||||
mode: "single",
|
||||
singleColor: "#000000",
|
||||
bandedColors: [
|
||||
"#000000",
|
||||
"#000000",
|
||||
"#000000",
|
||||
"#000000",
|
||||
"#000000",
|
||||
] as const,
|
||||
mode: 'single',
|
||||
singleColor: '#000000',
|
||||
bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const,
|
||||
}),
|
||||
"word word-known",
|
||||
'word word-known',
|
||||
);
|
||||
assert.equal(
|
||||
computeWordClass(nPlusOne, {
|
||||
enabled: true,
|
||||
topX: 100,
|
||||
mode: "single",
|
||||
singleColor: "#000000",
|
||||
bandedColors: [
|
||||
"#000000",
|
||||
"#000000",
|
||||
"#000000",
|
||||
"#000000",
|
||||
"#000000",
|
||||
] as const,
|
||||
mode: 'single',
|
||||
singleColor: '#000000',
|
||||
bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const,
|
||||
}),
|
||||
"word word-n-plus-one",
|
||||
'word word-n-plus-one',
|
||||
);
|
||||
assert.equal(
|
||||
computeWordClass(frequency, {
|
||||
enabled: true,
|
||||
topX: 100,
|
||||
mode: "single",
|
||||
singleColor: "#000000",
|
||||
bandedColors: [
|
||||
"#000000",
|
||||
"#000000",
|
||||
"#000000",
|
||||
"#000000",
|
||||
"#000000",
|
||||
] as const,
|
||||
mode: 'single',
|
||||
singleColor: '#000000',
|
||||
bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const,
|
||||
}),
|
||||
"word word-frequency-single",
|
||||
'word word-frequency-single',
|
||||
);
|
||||
});
|
||||
|
||||
test("computeWordClass adds frequency class for single mode when rank is within topX", () => {
|
||||
test('computeWordClass adds frequency class for single mode when rank is within topX', () => {
|
||||
const token = createToken({
|
||||
surface: "猫",
|
||||
surface: '猫',
|
||||
frequencyRank: 50,
|
||||
});
|
||||
|
||||
const actual = computeWordClass(token, {
|
||||
enabled: true,
|
||||
topX: 100,
|
||||
mode: "single",
|
||||
singleColor: "#000000",
|
||||
bandedColors: [
|
||||
"#000000",
|
||||
"#000000",
|
||||
"#000000",
|
||||
"#000000",
|
||||
"#000000",
|
||||
] as const,
|
||||
mode: 'single',
|
||||
singleColor: '#000000',
|
||||
bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const,
|
||||
});
|
||||
|
||||
assert.equal(actual, "word word-frequency-single");
|
||||
assert.equal(actual, 'word word-frequency-single');
|
||||
});
|
||||
|
||||
test("computeWordClass adds frequency class when rank equals topX", () => {
|
||||
test('computeWordClass adds frequency class when rank equals topX', () => {
|
||||
const token = createToken({
|
||||
surface: "水",
|
||||
surface: '水',
|
||||
frequencyRank: 100,
|
||||
});
|
||||
|
||||
const actual = computeWordClass(token, {
|
||||
enabled: true,
|
||||
topX: 100,
|
||||
mode: "single",
|
||||
singleColor: "#000000",
|
||||
bandedColors: [
|
||||
"#000000",
|
||||
"#000000",
|
||||
"#000000",
|
||||
"#000000",
|
||||
"#000000",
|
||||
] as const,
|
||||
mode: 'single',
|
||||
singleColor: '#000000',
|
||||
bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const,
|
||||
});
|
||||
|
||||
assert.equal(actual, "word word-frequency-single");
|
||||
assert.equal(actual, 'word word-frequency-single');
|
||||
});
|
||||
|
||||
test("computeWordClass adds frequency class for banded mode", () => {
|
||||
test('computeWordClass adds frequency class for banded mode', () => {
|
||||
const token = createToken({
|
||||
surface: "犬",
|
||||
surface: '犬',
|
||||
frequencyRank: 250,
|
||||
});
|
||||
|
||||
const actual = computeWordClass(token, {
|
||||
enabled: true,
|
||||
topX: 1000,
|
||||
mode: "banded",
|
||||
singleColor: "#000000",
|
||||
bandedColors: [
|
||||
"#111111",
|
||||
"#222222",
|
||||
"#333333",
|
||||
"#444444",
|
||||
"#555555",
|
||||
] as const,
|
||||
mode: 'banded',
|
||||
singleColor: '#000000',
|
||||
bandedColors: ['#111111', '#222222', '#333333', '#444444', '#555555'] as const,
|
||||
});
|
||||
|
||||
assert.equal(actual, "word word-frequency-band-2");
|
||||
assert.equal(actual, 'word word-frequency-band-2');
|
||||
});
|
||||
|
||||
test("computeWordClass uses configured band count for banded mode", () => {
|
||||
test('computeWordClass uses configured band count for banded mode', () => {
|
||||
const token = createToken({
|
||||
surface: "犬",
|
||||
surface: '犬',
|
||||
frequencyRank: 2,
|
||||
});
|
||||
|
||||
const actual = computeWordClass(token, {
|
||||
enabled: true,
|
||||
topX: 4,
|
||||
mode: "banded",
|
||||
singleColor: "#000000",
|
||||
bandedColors: ["#111111", "#222222", "#333333", "#444444", "#555555"],
|
||||
mode: 'banded',
|
||||
singleColor: '#000000',
|
||||
bandedColors: ['#111111', '#222222', '#333333', '#444444', '#555555'],
|
||||
} as any);
|
||||
|
||||
assert.equal(actual, "word word-frequency-band-3");
|
||||
assert.equal(actual, 'word word-frequency-band-3');
|
||||
});
|
||||
|
||||
test("computeWordClass skips frequency class when rank is out of topX", () => {
|
||||
test('computeWordClass skips frequency class when rank is out of topX', () => {
|
||||
const token = createToken({
|
||||
surface: "犬",
|
||||
surface: '犬',
|
||||
frequencyRank: 1200,
|
||||
});
|
||||
|
||||
const actual = computeWordClass(token, {
|
||||
enabled: true,
|
||||
topX: 1000,
|
||||
mode: "single",
|
||||
singleColor: "#000000",
|
||||
bandedColors: [
|
||||
"#000000",
|
||||
"#000000",
|
||||
"#000000",
|
||||
"#000000",
|
||||
"#000000",
|
||||
] as const,
|
||||
mode: 'single',
|
||||
singleColor: '#000000',
|
||||
bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const,
|
||||
});
|
||||
|
||||
assert.equal(actual, "word");
|
||||
assert.equal(actual, 'word');
|
||||
});
|
||||
|
||||
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");
|
||||
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;
|
||||
if (!fs.existsSync(cssPath)) {
|
||||
assert.fail(
|
||||
"JLPT CSS file missing. Run `bun run build` first, or ensure src/renderer/style.css exists.",
|
||||
'JLPT CSS file missing. Run `bun run build` first, or ensure src/renderer/style.css exists.',
|
||||
);
|
||||
}
|
||||
|
||||
const cssText = fs.readFileSync(cssPath, "utf-8");
|
||||
const cssText = fs.readFileSync(cssPath, 'utf-8');
|
||||
|
||||
for (let level = 1; level <= 5; level += 1) {
|
||||
const block = extractClassBlock(
|
||||
cssText,
|
||||
`#subtitleRoot .word.word-jlpt-n${level}`,
|
||||
);
|
||||
const block = extractClassBlock(cssText, `#subtitleRoot .word.word-jlpt-n${level}`);
|
||||
assert.ok(block.length > 0, `word-jlpt-n${level} class should exist`);
|
||||
assert.match(block, /text-decoration-line:\s*underline;/);
|
||||
assert.match(block, /text-decoration-thickness:\s*2px;/);
|
||||
@@ -277,12 +229,12 @@ test("JLPT CSS rules use underline-only styling in renderer stylesheet", () => {
|
||||
const block = extractClassBlock(
|
||||
cssText,
|
||||
band === 1
|
||||
? "#subtitleRoot .word.word-frequency-single"
|
||||
? '#subtitleRoot .word.word-frequency-single'
|
||||
: `#subtitleRoot .word.word-frequency-band-${band}`,
|
||||
);
|
||||
assert.ok(
|
||||
block.length > 0,
|
||||
`frequency class word-frequency-${band === 1 ? "single" : `band-${band}`} should exist`,
|
||||
`frequency class word-frequency-${band === 1 ? 'single' : `band-${band}`} should exist`,
|
||||
);
|
||||
assert.match(block, /color:\s*var\(/);
|
||||
}
|
||||
|
||||
@@ -1,33 +1,27 @@
|
||||
import type {
|
||||
MergedToken,
|
||||
SecondarySubMode,
|
||||
SubtitleData,
|
||||
SubtitleStyleConfig,
|
||||
} from "../types";
|
||||
import type { RendererContext } from "./context";
|
||||
import type { MergedToken, SecondarySubMode, SubtitleData, SubtitleStyleConfig } from '../types';
|
||||
import type { RendererContext } from './context';
|
||||
|
||||
type FrequencyRenderSettings = {
|
||||
enabled: boolean;
|
||||
topX: number;
|
||||
mode: "single" | "banded";
|
||||
mode: 'single' | 'banded';
|
||||
singleColor: string;
|
||||
bandedColors: [string, string, string, string, string];
|
||||
};
|
||||
|
||||
function normalizeSubtitle(text: string, trim = true): string {
|
||||
if (!text) return "";
|
||||
if (!text) return '';
|
||||
|
||||
let normalized = text.replace(/\\N/g, "\n").replace(/\\n/g, "\n");
|
||||
normalized = normalized.replace(/\{[^}]*\}/g, "");
|
||||
let normalized = text.replace(/\\N/g, '\n').replace(/\\n/g, '\n');
|
||||
normalized = normalized.replace(/\{[^}]*\}/g, '');
|
||||
|
||||
return trim ? normalized.trim() : normalized;
|
||||
}
|
||||
|
||||
const HEX_COLOR_PATTERN =
|
||||
/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
||||
const HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
||||
|
||||
function sanitizeHexColor(value: unknown, fallback: string): string {
|
||||
return typeof value === "string" && HEX_COLOR_PATTERN.test(value.trim())
|
||||
return typeof value === 'string' && HEX_COLOR_PATTERN.test(value.trim())
|
||||
? value.trim()
|
||||
: fallback;
|
||||
}
|
||||
@@ -35,13 +29,13 @@ function sanitizeHexColor(value: unknown, fallback: string): string {
|
||||
const DEFAULT_FREQUENCY_RENDER_SETTINGS: FrequencyRenderSettings = {
|
||||
enabled: false,
|
||||
topX: 1000,
|
||||
mode: "single",
|
||||
singleColor: "#f5a97f",
|
||||
bandedColors: ["#ed8796", "#f5a97f", "#f9e2af", "#a6e3a1", "#8aadf4"],
|
||||
mode: 'single',
|
||||
singleColor: '#f5a97f',
|
||||
bandedColors: ['#ed8796', '#f5a97f', '#f9e2af', '#a6e3a1', '#8aadf4'],
|
||||
};
|
||||
|
||||
function sanitizeFrequencyTopX(value: unknown, fallback: number): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.max(1, Math.floor(value));
|
||||
@@ -49,8 +43,8 @@ function sanitizeFrequencyTopX(value: unknown, fallback: number): number {
|
||||
|
||||
function sanitizeFrequencyBandedColors(
|
||||
value: unknown,
|
||||
fallback: FrequencyRenderSettings["bandedColors"],
|
||||
): FrequencyRenderSettings["bandedColors"] {
|
||||
fallback: FrequencyRenderSettings['bandedColors'],
|
||||
): FrequencyRenderSettings['bandedColors'] {
|
||||
if (!Array.isArray(value) || value.length !== 5) {
|
||||
return fallback;
|
||||
}
|
||||
@@ -69,33 +63,27 @@ function getFrequencyDictionaryClass(
|
||||
settings: FrequencyRenderSettings,
|
||||
): string {
|
||||
if (!settings.enabled) {
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
|
||||
if (
|
||||
typeof token.frequencyRank !== "number" ||
|
||||
!Number.isFinite(token.frequencyRank)
|
||||
) {
|
||||
return "";
|
||||
if (typeof token.frequencyRank !== 'number' || !Number.isFinite(token.frequencyRank)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const rank = Math.max(1, Math.floor(token.frequencyRank));
|
||||
const topX = sanitizeFrequencyTopX(
|
||||
settings.topX,
|
||||
DEFAULT_FREQUENCY_RENDER_SETTINGS.topX,
|
||||
);
|
||||
const topX = sanitizeFrequencyTopX(settings.topX, DEFAULT_FREQUENCY_RENDER_SETTINGS.topX);
|
||||
if (rank > topX) {
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
|
||||
if (settings.mode === "banded") {
|
||||
if (settings.mode === 'banded') {
|
||||
const bandCount = settings.bandedColors.length;
|
||||
const normalizedBand = Math.ceil((rank / topX) * bandCount);
|
||||
const band = Math.min(bandCount, Math.max(1, normalizedBand));
|
||||
return `word-frequency-band-${band}`;
|
||||
}
|
||||
|
||||
return "word-frequency-single";
|
||||
return 'word-frequency-single';
|
||||
}
|
||||
|
||||
function renderWithTokens(
|
||||
@@ -125,28 +113,25 @@ function renderWithTokens(
|
||||
for (const token of tokens) {
|
||||
const surface = token.surface;
|
||||
|
||||
if (surface.includes("\n")) {
|
||||
const parts = surface.split("\n");
|
||||
if (surface.includes('\n')) {
|
||||
const parts = surface.split('\n');
|
||||
for (let i = 0; i < parts.length; i += 1) {
|
||||
if (parts[i]) {
|
||||
const span = document.createElement("span");
|
||||
span.className = computeWordClass(
|
||||
token,
|
||||
resolvedFrequencyRenderSettings,
|
||||
);
|
||||
const span = document.createElement('span');
|
||||
span.className = computeWordClass(token, resolvedFrequencyRenderSettings);
|
||||
span.textContent = parts[i];
|
||||
if (token.reading) span.dataset.reading = token.reading;
|
||||
if (token.headword) span.dataset.headword = token.headword;
|
||||
fragment.appendChild(span);
|
||||
}
|
||||
if (i < parts.length - 1) {
|
||||
fragment.appendChild(document.createElement("br"));
|
||||
fragment.appendChild(document.createElement('br'));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const span = document.createElement("span");
|
||||
const span = document.createElement('span');
|
||||
span.className = computeWordClass(token, resolvedFrequencyRenderSettings);
|
||||
span.textContent = surface;
|
||||
if (token.reading) span.dataset.reading = token.reading;
|
||||
@@ -168,22 +153,19 @@ export function computeWordClass(
|
||||
frequencySettings?.bandedColors,
|
||||
DEFAULT_FREQUENCY_RENDER_SETTINGS.bandedColors,
|
||||
),
|
||||
topX: sanitizeFrequencyTopX(
|
||||
frequencySettings?.topX,
|
||||
DEFAULT_FREQUENCY_RENDER_SETTINGS.topX,
|
||||
),
|
||||
topX: sanitizeFrequencyTopX(frequencySettings?.topX, DEFAULT_FREQUENCY_RENDER_SETTINGS.topX),
|
||||
singleColor: sanitizeHexColor(
|
||||
frequencySettings?.singleColor,
|
||||
DEFAULT_FREQUENCY_RENDER_SETTINGS.singleColor,
|
||||
),
|
||||
};
|
||||
|
||||
const classes = ["word"];
|
||||
const classes = ['word'];
|
||||
|
||||
if (token.isNPlusOneTarget) {
|
||||
classes.push("word-n-plus-one");
|
||||
classes.push('word-n-plus-one');
|
||||
} else if (token.isKnown) {
|
||||
classes.push("word-known");
|
||||
classes.push('word-known');
|
||||
}
|
||||
|
||||
if (token.jlptLevel) {
|
||||
@@ -191,28 +173,25 @@ export function computeWordClass(
|
||||
}
|
||||
|
||||
if (!token.isKnown && !token.isNPlusOneTarget) {
|
||||
const frequencyClass = getFrequencyDictionaryClass(
|
||||
token,
|
||||
resolvedFrequencySettings,
|
||||
);
|
||||
const frequencyClass = getFrequencyDictionaryClass(token, resolvedFrequencySettings);
|
||||
if (frequencyClass) {
|
||||
classes.push(frequencyClass);
|
||||
}
|
||||
}
|
||||
|
||||
return classes.join(" ");
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
function renderCharacterLevel(root: HTMLElement, text: string): void {
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
for (const char of text) {
|
||||
if (char === "\n") {
|
||||
fragment.appendChild(document.createElement("br"));
|
||||
if (char === '\n') {
|
||||
fragment.appendChild(document.createElement('br'));
|
||||
continue;
|
||||
}
|
||||
const span = document.createElement("span");
|
||||
span.className = "c";
|
||||
const span = document.createElement('span');
|
||||
span.className = 'c';
|
||||
span.textContent = char;
|
||||
fragment.appendChild(span);
|
||||
}
|
||||
@@ -220,17 +199,14 @@ function renderCharacterLevel(root: HTMLElement, text: string): void {
|
||||
root.appendChild(fragment);
|
||||
}
|
||||
|
||||
function renderPlainTextPreserveLineBreaks(
|
||||
root: HTMLElement,
|
||||
text: string,
|
||||
): void {
|
||||
const lines = text.split("\n");
|
||||
function renderPlainTextPreserveLineBreaks(root: HTMLElement, text: string): void {
|
||||
const lines = text.split('\n');
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
for (let i = 0; i < lines.length; i += 1) {
|
||||
fragment.appendChild(document.createTextNode(lines[i]));
|
||||
if (i < lines.length - 1) {
|
||||
fragment.appendChild(document.createElement("br"));
|
||||
fragment.appendChild(document.createElement('br'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,17 +215,17 @@ function renderPlainTextPreserveLineBreaks(
|
||||
|
||||
export function createSubtitleRenderer(ctx: RendererContext) {
|
||||
function renderSubtitle(data: SubtitleData | string): void {
|
||||
ctx.dom.subtitleRoot.innerHTML = "";
|
||||
ctx.state.lastHoverSelectionKey = "";
|
||||
ctx.dom.subtitleRoot.innerHTML = '';
|
||||
ctx.state.lastHoverSelectionKey = '';
|
||||
ctx.state.lastHoverSelectionNode = null;
|
||||
|
||||
let text: string;
|
||||
let tokens: MergedToken[] | null;
|
||||
|
||||
if (typeof data === "string") {
|
||||
if (typeof data === 'string') {
|
||||
text = data;
|
||||
tokens = null;
|
||||
} else if (data && typeof data === "object") {
|
||||
} else if (data && typeof data === 'object') {
|
||||
text = data.text;
|
||||
tokens = data.tokens;
|
||||
} else {
|
||||
@@ -262,22 +238,15 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
||||
const normalizedInvisible = normalizeSubtitle(text, false);
|
||||
ctx.state.currentInvisibleSubtitleLineCount = Math.max(
|
||||
1,
|
||||
normalizedInvisible.split("\n").length,
|
||||
);
|
||||
renderPlainTextPreserveLineBreaks(
|
||||
ctx.dom.subtitleRoot,
|
||||
normalizedInvisible,
|
||||
normalizedInvisible.split('\n').length,
|
||||
);
|
||||
renderPlainTextPreserveLineBreaks(ctx.dom.subtitleRoot, normalizedInvisible);
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = normalizeSubtitle(text);
|
||||
if (tokens && tokens.length > 0) {
|
||||
renderWithTokens(
|
||||
ctx.dom.subtitleRoot,
|
||||
tokens,
|
||||
getFrequencyRenderSettings(),
|
||||
);
|
||||
renderWithTokens(ctx.dom.subtitleRoot, tokens, getFrequencyRenderSettings());
|
||||
return;
|
||||
}
|
||||
renderCharacterLevel(ctx.dom.subtitleRoot, normalized);
|
||||
@@ -300,33 +269,33 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
||||
}
|
||||
|
||||
function renderSecondarySub(text: string): void {
|
||||
ctx.dom.secondarySubRoot.innerHTML = "";
|
||||
ctx.dom.secondarySubRoot.innerHTML = '';
|
||||
if (!text) return;
|
||||
|
||||
const normalized = text
|
||||
.replace(/\\N/g, "\n")
|
||||
.replace(/\\n/g, "\n")
|
||||
.replace(/\{[^}]*\}/g, "")
|
||||
.replace(/\\N/g, '\n')
|
||||
.replace(/\\n/g, '\n')
|
||||
.replace(/\{[^}]*\}/g, '')
|
||||
.trim();
|
||||
|
||||
if (!normalized) return;
|
||||
|
||||
const lines = normalized.split("\n");
|
||||
const lines = normalized.split('\n');
|
||||
for (let i = 0; i < lines.length; i += 1) {
|
||||
if (lines[i]) {
|
||||
ctx.dom.secondarySubRoot.appendChild(document.createTextNode(lines[i]));
|
||||
}
|
||||
if (i < lines.length - 1) {
|
||||
ctx.dom.secondarySubRoot.appendChild(document.createElement("br"));
|
||||
ctx.dom.secondarySubRoot.appendChild(document.createElement('br'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateSecondarySubMode(mode: SecondarySubMode): void {
|
||||
ctx.dom.secondarySubContainer.classList.remove(
|
||||
"secondary-sub-hidden",
|
||||
"secondary-sub-visible",
|
||||
"secondary-sub-hover",
|
||||
'secondary-sub-hidden',
|
||||
'secondary-sub-visible',
|
||||
'secondary-sub-hover',
|
||||
);
|
||||
ctx.dom.secondarySubContainer.classList.add(`secondary-sub-${mode}`);
|
||||
}
|
||||
@@ -334,37 +303,29 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
||||
function applySubtitleFontSize(fontSize: number): void {
|
||||
const clampedSize = Math.max(10, fontSize);
|
||||
ctx.dom.subtitleRoot.style.fontSize = `${clampedSize}px`;
|
||||
document.documentElement.style.setProperty(
|
||||
"--subtitle-font-size",
|
||||
`${clampedSize}px`,
|
||||
);
|
||||
document.documentElement.style.setProperty('--subtitle-font-size', `${clampedSize}px`);
|
||||
}
|
||||
|
||||
function applySubtitleStyle(style: SubtitleStyleConfig | null): void {
|
||||
if (!style) return;
|
||||
|
||||
if (style.fontFamily)
|
||||
ctx.dom.subtitleRoot.style.fontFamily = style.fontFamily;
|
||||
if (style.fontSize)
|
||||
ctx.dom.subtitleRoot.style.fontSize = `${style.fontSize}px`;
|
||||
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.fontWeight)
|
||||
ctx.dom.subtitleRoot.style.fontWeight = style.fontWeight;
|
||||
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 knownWordColor = style.knownWordColor ?? ctx.state.knownWordColor ?? '#a6da95';
|
||||
const nPlusOneColor = style.nPlusOneColor ?? ctx.state.nPlusOneColor ?? '#c6a0f6';
|
||||
const jlptColors = {
|
||||
N1: ctx.state.jlptN1Color ?? "#ed8796",
|
||||
N2: ctx.state.jlptN2Color ?? "#f5a97f",
|
||||
N3: ctx.state.jlptN3Color ?? "#f9e2af",
|
||||
N4: ctx.state.jlptN4Color ?? "#a6e3a1",
|
||||
N5: ctx.state.jlptN5Color ?? "#8aadf4",
|
||||
N1: ctx.state.jlptN1Color ?? '#ed8796',
|
||||
N2: ctx.state.jlptN2Color ?? '#f5a97f',
|
||||
N3: ctx.state.jlptN3Color ?? '#f9e2af',
|
||||
N4: ctx.state.jlptN4Color ?? '#a6e3a1',
|
||||
N5: ctx.state.jlptN5Color ?? '#8aadf4',
|
||||
...(style.jlptColors
|
||||
? {
|
||||
N1: sanitizeHexColor(style.jlptColors?.N1, ctx.state.jlptN1Color),
|
||||
@@ -378,43 +339,21 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
||||
|
||||
ctx.state.knownWordColor = knownWordColor;
|
||||
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-known-word-color', knownWordColor);
|
||||
ctx.dom.subtitleRoot.style.setProperty('--subtitle-n-plus-one-color', nPlusOneColor);
|
||||
ctx.state.jlptN1Color = jlptColors.N1;
|
||||
ctx.state.jlptN2Color = jlptColors.N2;
|
||||
ctx.state.jlptN3Color = jlptColors.N3;
|
||||
ctx.state.jlptN4Color = jlptColors.N4;
|
||||
ctx.state.jlptN5Color = jlptColors.N5;
|
||||
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-n3-color",
|
||||
jlptColors.N3,
|
||||
);
|
||||
ctx.dom.subtitleRoot.style.setProperty(
|
||||
"--subtitle-jlpt-n4-color",
|
||||
jlptColors.N4,
|
||||
);
|
||||
ctx.dom.subtitleRoot.style.setProperty(
|
||||
"--subtitle-jlpt-n5-color",
|
||||
jlptColors.N5,
|
||||
);
|
||||
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-n3-color', jlptColors.N3);
|
||||
ctx.dom.subtitleRoot.style.setProperty('--subtitle-jlpt-n4-color', jlptColors.N4);
|
||||
ctx.dom.subtitleRoot.style.setProperty('--subtitle-jlpt-n5-color', jlptColors.N5);
|
||||
const frequencyDictionarySettings = style.frequencyDictionary ?? {};
|
||||
const frequencyEnabled =
|
||||
frequencyDictionarySettings.enabled ??
|
||||
ctx.state.frequencyDictionaryEnabled;
|
||||
frequencyDictionarySettings.enabled ?? ctx.state.frequencyDictionaryEnabled;
|
||||
const frequencyTopX = sanitizeFrequencyTopX(
|
||||
frequencyDictionarySettings.topX,
|
||||
ctx.state.frequencyDictionaryTopX,
|
||||
@@ -449,27 +388,27 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
||||
ctx.state.frequencyDictionaryBand5Color,
|
||||
] = frequencyBandedColors;
|
||||
ctx.dom.subtitleRoot.style.setProperty(
|
||||
"--subtitle-frequency-single-color",
|
||||
'--subtitle-frequency-single-color',
|
||||
frequencySingleColor,
|
||||
);
|
||||
ctx.dom.subtitleRoot.style.setProperty(
|
||||
"--subtitle-frequency-band-1-color",
|
||||
'--subtitle-frequency-band-1-color',
|
||||
frequencyBandedColors[0],
|
||||
);
|
||||
ctx.dom.subtitleRoot.style.setProperty(
|
||||
"--subtitle-frequency-band-2-color",
|
||||
'--subtitle-frequency-band-2-color',
|
||||
frequencyBandedColors[1],
|
||||
);
|
||||
ctx.dom.subtitleRoot.style.setProperty(
|
||||
"--subtitle-frequency-band-3-color",
|
||||
'--subtitle-frequency-band-3-color',
|
||||
frequencyBandedColors[2],
|
||||
);
|
||||
ctx.dom.subtitleRoot.style.setProperty(
|
||||
"--subtitle-frequency-band-4-color",
|
||||
'--subtitle-frequency-band-4-color',
|
||||
frequencyBandedColors[3],
|
||||
);
|
||||
ctx.dom.subtitleRoot.style.setProperty(
|
||||
"--subtitle-frequency-band-5-color",
|
||||
'--subtitle-frequency-band-5-color',
|
||||
frequencyBandedColors[4],
|
||||
);
|
||||
|
||||
@@ -492,8 +431,7 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
||||
ctx.dom.secondarySubRoot.style.fontStyle = secondaryStyle.fontStyle;
|
||||
}
|
||||
if (secondaryStyle.backgroundColor) {
|
||||
ctx.dom.secondarySubContainer.style.background =
|
||||
secondaryStyle.backgroundColor;
|
||||
ctx.dom.secondarySubContainer.style.background = secondaryStyle.backgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,104 +74,68 @@ function getRequiredElement<T extends HTMLElement>(id: string): T {
|
||||
|
||||
export function resolveRendererDom(): RendererDom {
|
||||
return {
|
||||
subtitleRoot: getRequiredElement<HTMLElement>("subtitleRoot"),
|
||||
subtitleContainer: getRequiredElement<HTMLElement>("subtitleContainer"),
|
||||
overlay: getRequiredElement<HTMLElement>("overlay"),
|
||||
secondarySubContainer: getRequiredElement<HTMLElement>(
|
||||
"secondarySubContainer",
|
||||
),
|
||||
secondarySubRoot: getRequiredElement<HTMLElement>("secondarySubRoot"),
|
||||
subtitleRoot: getRequiredElement<HTMLElement>('subtitleRoot'),
|
||||
subtitleContainer: getRequiredElement<HTMLElement>('subtitleContainer'),
|
||||
overlay: getRequiredElement<HTMLElement>('overlay'),
|
||||
secondarySubContainer: getRequiredElement<HTMLElement>('secondarySubContainer'),
|
||||
secondarySubRoot: getRequiredElement<HTMLElement>('secondarySubRoot'),
|
||||
|
||||
jimakuModal: getRequiredElement<HTMLDivElement>("jimakuModal"),
|
||||
jimakuTitleInput: getRequiredElement<HTMLInputElement>("jimakuTitle"),
|
||||
jimakuSeasonInput: getRequiredElement<HTMLInputElement>("jimakuSeason"),
|
||||
jimakuEpisodeInput: getRequiredElement<HTMLInputElement>("jimakuEpisode"),
|
||||
jimakuSearchButton: getRequiredElement<HTMLButtonElement>("jimakuSearch"),
|
||||
jimakuCloseButton: getRequiredElement<HTMLButtonElement>("jimakuClose"),
|
||||
jimakuStatus: getRequiredElement<HTMLDivElement>("jimakuStatus"),
|
||||
jimakuEntriesSection: getRequiredElement<HTMLDivElement>(
|
||||
"jimakuEntriesSection",
|
||||
),
|
||||
jimakuEntriesList: getRequiredElement<HTMLUListElement>("jimakuEntries"),
|
||||
jimakuFilesSection:
|
||||
getRequiredElement<HTMLDivElement>("jimakuFilesSection"),
|
||||
jimakuFilesList: getRequiredElement<HTMLUListElement>("jimakuFiles"),
|
||||
jimakuBroadenButton: getRequiredElement<HTMLButtonElement>("jimakuBroaden"),
|
||||
jimakuModal: getRequiredElement<HTMLDivElement>('jimakuModal'),
|
||||
jimakuTitleInput: getRequiredElement<HTMLInputElement>('jimakuTitle'),
|
||||
jimakuSeasonInput: getRequiredElement<HTMLInputElement>('jimakuSeason'),
|
||||
jimakuEpisodeInput: getRequiredElement<HTMLInputElement>('jimakuEpisode'),
|
||||
jimakuSearchButton: getRequiredElement<HTMLButtonElement>('jimakuSearch'),
|
||||
jimakuCloseButton: getRequiredElement<HTMLButtonElement>('jimakuClose'),
|
||||
jimakuStatus: getRequiredElement<HTMLDivElement>('jimakuStatus'),
|
||||
jimakuEntriesSection: getRequiredElement<HTMLDivElement>('jimakuEntriesSection'),
|
||||
jimakuEntriesList: getRequiredElement<HTMLUListElement>('jimakuEntries'),
|
||||
jimakuFilesSection: getRequiredElement<HTMLDivElement>('jimakuFilesSection'),
|
||||
jimakuFilesList: getRequiredElement<HTMLUListElement>('jimakuFiles'),
|
||||
jimakuBroadenButton: getRequiredElement<HTMLButtonElement>('jimakuBroaden'),
|
||||
|
||||
kikuModal: getRequiredElement<HTMLDivElement>("kikuFieldGroupingModal"),
|
||||
kikuCard1: getRequiredElement<HTMLDivElement>("kikuCard1"),
|
||||
kikuCard2: getRequiredElement<HTMLDivElement>("kikuCard2"),
|
||||
kikuCard1Expression: getRequiredElement<HTMLDivElement>(
|
||||
"kikuCard1Expression",
|
||||
),
|
||||
kikuCard2Expression: getRequiredElement<HTMLDivElement>(
|
||||
"kikuCard2Expression",
|
||||
),
|
||||
kikuCard1Sentence: getRequiredElement<HTMLDivElement>("kikuCard1Sentence"),
|
||||
kikuCard2Sentence: getRequiredElement<HTMLDivElement>("kikuCard2Sentence"),
|
||||
kikuCard1Meta: getRequiredElement<HTMLDivElement>("kikuCard1Meta"),
|
||||
kikuCard2Meta: getRequiredElement<HTMLDivElement>("kikuCard2Meta"),
|
||||
kikuConfirmButton:
|
||||
getRequiredElement<HTMLButtonElement>("kikuConfirmButton"),
|
||||
kikuCancelButton: getRequiredElement<HTMLButtonElement>("kikuCancelButton"),
|
||||
kikuDeleteDuplicateCheckbox: getRequiredElement<HTMLInputElement>(
|
||||
"kikuDeleteDuplicate",
|
||||
),
|
||||
kikuSelectionStep: getRequiredElement<HTMLDivElement>("kikuSelectionStep"),
|
||||
kikuPreviewStep: getRequiredElement<HTMLDivElement>("kikuPreviewStep"),
|
||||
kikuPreviewJson: getRequiredElement<HTMLPreElement>("kikuPreviewJson"),
|
||||
kikuPreviewCompactButton:
|
||||
getRequiredElement<HTMLButtonElement>("kikuPreviewCompact"),
|
||||
kikuPreviewFullButton:
|
||||
getRequiredElement<HTMLButtonElement>("kikuPreviewFull"),
|
||||
kikuPreviewError: getRequiredElement<HTMLDivElement>("kikuPreviewError"),
|
||||
kikuBackButton: getRequiredElement<HTMLButtonElement>("kikuBackButton"),
|
||||
kikuFinalConfirmButton: getRequiredElement<HTMLButtonElement>(
|
||||
"kikuFinalConfirmButton",
|
||||
),
|
||||
kikuFinalCancelButton: getRequiredElement<HTMLButtonElement>(
|
||||
"kikuFinalCancelButton",
|
||||
),
|
||||
kikuHint: getRequiredElement<HTMLDivElement>("kikuHint"),
|
||||
kikuModal: getRequiredElement<HTMLDivElement>('kikuFieldGroupingModal'),
|
||||
kikuCard1: getRequiredElement<HTMLDivElement>('kikuCard1'),
|
||||
kikuCard2: getRequiredElement<HTMLDivElement>('kikuCard2'),
|
||||
kikuCard1Expression: getRequiredElement<HTMLDivElement>('kikuCard1Expression'),
|
||||
kikuCard2Expression: getRequiredElement<HTMLDivElement>('kikuCard2Expression'),
|
||||
kikuCard1Sentence: getRequiredElement<HTMLDivElement>('kikuCard1Sentence'),
|
||||
kikuCard2Sentence: getRequiredElement<HTMLDivElement>('kikuCard2Sentence'),
|
||||
kikuCard1Meta: getRequiredElement<HTMLDivElement>('kikuCard1Meta'),
|
||||
kikuCard2Meta: getRequiredElement<HTMLDivElement>('kikuCard2Meta'),
|
||||
kikuConfirmButton: getRequiredElement<HTMLButtonElement>('kikuConfirmButton'),
|
||||
kikuCancelButton: getRequiredElement<HTMLButtonElement>('kikuCancelButton'),
|
||||
kikuDeleteDuplicateCheckbox: getRequiredElement<HTMLInputElement>('kikuDeleteDuplicate'),
|
||||
kikuSelectionStep: getRequiredElement<HTMLDivElement>('kikuSelectionStep'),
|
||||
kikuPreviewStep: getRequiredElement<HTMLDivElement>('kikuPreviewStep'),
|
||||
kikuPreviewJson: getRequiredElement<HTMLPreElement>('kikuPreviewJson'),
|
||||
kikuPreviewCompactButton: getRequiredElement<HTMLButtonElement>('kikuPreviewCompact'),
|
||||
kikuPreviewFullButton: getRequiredElement<HTMLButtonElement>('kikuPreviewFull'),
|
||||
kikuPreviewError: getRequiredElement<HTMLDivElement>('kikuPreviewError'),
|
||||
kikuBackButton: getRequiredElement<HTMLButtonElement>('kikuBackButton'),
|
||||
kikuFinalConfirmButton: getRequiredElement<HTMLButtonElement>('kikuFinalConfirmButton'),
|
||||
kikuFinalCancelButton: getRequiredElement<HTMLButtonElement>('kikuFinalCancelButton'),
|
||||
kikuHint: getRequiredElement<HTMLDivElement>('kikuHint'),
|
||||
|
||||
runtimeOptionsModal: getRequiredElement<HTMLDivElement>(
|
||||
"runtimeOptionsModal",
|
||||
),
|
||||
runtimeOptionsClose: getRequiredElement<HTMLButtonElement>(
|
||||
"runtimeOptionsClose",
|
||||
),
|
||||
runtimeOptionsList:
|
||||
getRequiredElement<HTMLUListElement>("runtimeOptionsList"),
|
||||
runtimeOptionsStatus: getRequiredElement<HTMLDivElement>(
|
||||
"runtimeOptionsStatus",
|
||||
),
|
||||
runtimeOptionsModal: getRequiredElement<HTMLDivElement>('runtimeOptionsModal'),
|
||||
runtimeOptionsClose: getRequiredElement<HTMLButtonElement>('runtimeOptionsClose'),
|
||||
runtimeOptionsList: getRequiredElement<HTMLUListElement>('runtimeOptionsList'),
|
||||
runtimeOptionsStatus: getRequiredElement<HTMLDivElement>('runtimeOptionsStatus'),
|
||||
|
||||
subsyncModal: getRequiredElement<HTMLDivElement>("subsyncModal"),
|
||||
subsyncCloseButton: getRequiredElement<HTMLButtonElement>("subsyncClose"),
|
||||
subsyncEngineAlass:
|
||||
getRequiredElement<HTMLInputElement>("subsyncEngineAlass"),
|
||||
subsyncEngineFfsubsync: getRequiredElement<HTMLInputElement>(
|
||||
"subsyncEngineFfsubsync",
|
||||
),
|
||||
subsyncSourceLabel:
|
||||
getRequiredElement<HTMLLabelElement>("subsyncSourceLabel"),
|
||||
subsyncSourceSelect: getRequiredElement<HTMLSelectElement>(
|
||||
"subsyncSourceSelect",
|
||||
),
|
||||
subsyncRunButton: getRequiredElement<HTMLButtonElement>("subsyncRun"),
|
||||
subsyncStatus: getRequiredElement<HTMLDivElement>("subsyncStatus"),
|
||||
subsyncModal: getRequiredElement<HTMLDivElement>('subsyncModal'),
|
||||
subsyncCloseButton: getRequiredElement<HTMLButtonElement>('subsyncClose'),
|
||||
subsyncEngineAlass: getRequiredElement<HTMLInputElement>('subsyncEngineAlass'),
|
||||
subsyncEngineFfsubsync: getRequiredElement<HTMLInputElement>('subsyncEngineFfsubsync'),
|
||||
subsyncSourceLabel: getRequiredElement<HTMLLabelElement>('subsyncSourceLabel'),
|
||||
subsyncSourceSelect: getRequiredElement<HTMLSelectElement>('subsyncSourceSelect'),
|
||||
subsyncRunButton: getRequiredElement<HTMLButtonElement>('subsyncRun'),
|
||||
subsyncStatus: getRequiredElement<HTMLDivElement>('subsyncStatus'),
|
||||
|
||||
sessionHelpModal: getRequiredElement<HTMLDivElement>("sessionHelpModal"),
|
||||
sessionHelpClose: getRequiredElement<HTMLButtonElement>("sessionHelpClose"),
|
||||
sessionHelpShortcut: getRequiredElement<HTMLDivElement>(
|
||||
"sessionHelpShortcut",
|
||||
),
|
||||
sessionHelpWarning:
|
||||
getRequiredElement<HTMLDivElement>("sessionHelpWarning"),
|
||||
sessionHelpStatus: getRequiredElement<HTMLDivElement>("sessionHelpStatus"),
|
||||
sessionHelpFilter:
|
||||
getRequiredElement<HTMLInputElement>("sessionHelpFilter"),
|
||||
sessionHelpContent:
|
||||
getRequiredElement<HTMLDivElement>("sessionHelpContent"),
|
||||
sessionHelpModal: getRequiredElement<HTMLDivElement>('sessionHelpModal'),
|
||||
sessionHelpClose: getRequiredElement<HTMLButtonElement>('sessionHelpClose'),
|
||||
sessionHelpShortcut: getRequiredElement<HTMLDivElement>('sessionHelpShortcut'),
|
||||
sessionHelpWarning: getRequiredElement<HTMLDivElement>('sessionHelpWarning'),
|
||||
sessionHelpStatus: getRequiredElement<HTMLDivElement>('sessionHelpStatus'),
|
||||
sessionHelpFilter: getRequiredElement<HTMLInputElement>('sessionHelpFilter'),
|
||||
sessionHelpContent: getRequiredElement<HTMLDivElement>('sessionHelpContent'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type OverlayLayer = "visible" | "invisible";
|
||||
export type OverlayLayer = 'visible' | 'invisible';
|
||||
|
||||
export type PlatformInfo = {
|
||||
overlayLayer: OverlayLayer;
|
||||
@@ -14,21 +14,19 @@ export type PlatformInfo = {
|
||||
export function resolvePlatformInfo(): PlatformInfo {
|
||||
const overlayLayerFromPreload = window.electronAPI.getOverlayLayer();
|
||||
const overlayLayerFromQuery =
|
||||
new URLSearchParams(window.location.search).get("layer") === "invisible"
|
||||
? "invisible"
|
||||
: "visible";
|
||||
new URLSearchParams(window.location.search).get('layer') === 'invisible'
|
||||
? 'invisible'
|
||||
: 'visible';
|
||||
|
||||
const overlayLayer: OverlayLayer =
|
||||
overlayLayerFromPreload === "visible" ||
|
||||
overlayLayerFromPreload === "invisible"
|
||||
overlayLayerFromPreload === 'visible' || overlayLayerFromPreload === 'invisible'
|
||||
? overlayLayerFromPreload
|
||||
: overlayLayerFromQuery;
|
||||
|
||||
const isInvisibleLayer = overlayLayer === "invisible";
|
||||
const isLinuxPlatform = navigator.platform.toLowerCase().includes("linux");
|
||||
const isInvisibleLayer = overlayLayer === 'invisible';
|
||||
const isLinuxPlatform = navigator.platform.toLowerCase().includes('linux');
|
||||
const isMacOSPlatform =
|
||||
navigator.platform.toLowerCase().includes("mac") ||
|
||||
/mac/i.test(navigator.userAgent);
|
||||
navigator.platform.toLowerCase().includes('mac') || /mac/i.test(navigator.userAgent);
|
||||
|
||||
return {
|
||||
overlayLayer,
|
||||
@@ -36,7 +34,7 @@ export function resolvePlatformInfo(): PlatformInfo {
|
||||
isLinuxPlatform,
|
||||
isMacOSPlatform,
|
||||
shouldToggleMouseIgnore: !isLinuxPlatform,
|
||||
invisiblePositionEditToggleCode: "KeyP",
|
||||
invisiblePositionEditToggleCode: 'KeyP',
|
||||
invisiblePositionStepPx: 1,
|
||||
invisiblePositionStepFastPx: 4,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user