mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
Normalize shortcut spaces before fil
This commit is contained in:
@@ -21,8 +21,10 @@ export interface IpcServiceDeps {
|
||||
setMecabEnabled: (enabled: boolean) => void;
|
||||
handleMpvCommand: (command: Array<string | number>) => void;
|
||||
getKeybindings: () => unknown;
|
||||
getConfiguredShortcuts: () => unknown;
|
||||
getSecondarySubMode: () => unknown;
|
||||
getCurrentSecondarySub: () => string;
|
||||
focusMainWindow: () => void;
|
||||
runSubsyncManual: (request: unknown) => Promise<unknown>;
|
||||
getAnkiConnectStatus: () => boolean;
|
||||
getRuntimeOptions: () => unknown;
|
||||
@@ -33,6 +35,7 @@ export interface IpcServiceDeps {
|
||||
|
||||
interface WindowLike {
|
||||
isDestroyed: () => boolean;
|
||||
focus: () => void;
|
||||
setIgnoreMouseEvents: (
|
||||
ignore: boolean,
|
||||
options?: { forward?: boolean },
|
||||
@@ -69,8 +72,10 @@ export interface IpcDepsRuntimeOptions {
|
||||
getMecabTokenizer: () => MecabTokenizerLike | null;
|
||||
handleMpvCommand: (command: Array<string | number>) => void;
|
||||
getKeybindings: () => unknown;
|
||||
getConfiguredShortcuts: () => unknown;
|
||||
getSecondarySubMode: () => unknown;
|
||||
getMpvClient: () => MpvClientLike | null;
|
||||
focusMainWindow: () => void;
|
||||
runSubsyncManual: (request: unknown) => Promise<unknown>;
|
||||
getAnkiConnectStatus: () => boolean;
|
||||
getRuntimeOptions: () => unknown;
|
||||
@@ -120,9 +125,15 @@ export function createIpcDepsRuntimeService(
|
||||
},
|
||||
handleMpvCommand: options.handleMpvCommand,
|
||||
getKeybindings: options.getKeybindings,
|
||||
getConfiguredShortcuts: options.getConfiguredShortcuts,
|
||||
getSecondarySubMode: options.getSecondarySubMode,
|
||||
getCurrentSecondarySub: () =>
|
||||
options.getMpvClient()?.currentSecondarySubText || "",
|
||||
focusMainWindow: () => {
|
||||
const mainWindow = options.getMainWindow();
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||
mainWindow.focus();
|
||||
},
|
||||
runSubsyncManual: options.runSubsyncManual,
|
||||
getAnkiConnectStatus: options.getAnkiConnectStatus,
|
||||
getRuntimeOptions: options.getRuntimeOptions,
|
||||
@@ -229,6 +240,10 @@ export function registerIpcHandlersService(deps: IpcServiceDeps): void {
|
||||
return deps.getKeybindings();
|
||||
});
|
||||
|
||||
ipcMain.handle("get-config-shortcuts", () => {
|
||||
return deps.getConfiguredShortcuts();
|
||||
});
|
||||
|
||||
ipcMain.handle("get-secondary-sub-mode", () => {
|
||||
return deps.getSecondarySubMode();
|
||||
});
|
||||
@@ -237,6 +252,10 @@ export function registerIpcHandlersService(deps: IpcServiceDeps): void {
|
||||
return deps.getCurrentSecondarySub();
|
||||
});
|
||||
|
||||
ipcMain.handle("focus-main-window", () => {
|
||||
deps.focusMainWindow();
|
||||
});
|
||||
|
||||
ipcMain.handle("subsync:run-manual", async (_event, request: unknown) => {
|
||||
return await deps.runSubsyncManual(request);
|
||||
});
|
||||
|
||||
@@ -1324,6 +1324,13 @@ registerIpcRuntimeServices({
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
getVisibleOverlayVisibility: () => overlayManager.getVisibleOverlayVisible(),
|
||||
getInvisibleOverlayVisibility: () => overlayManager.getInvisibleOverlayVisible(),
|
||||
focusMainWindow: () => {
|
||||
const mainWindow = overlayManager.getMainWindow();
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||
if (!mainWindow.isFocused()) {
|
||||
mainWindow.focus();
|
||||
}
|
||||
},
|
||||
onOverlayModalClosed: (modal: string) => {
|
||||
handleOverlayModalClosed(modal as OverlayHostedModal);
|
||||
},
|
||||
@@ -1353,6 +1360,7 @@ registerIpcRuntimeServices({
|
||||
handleMpvCommand: (command: (string | number)[]) =>
|
||||
handleMpvCommandFromIpc(command),
|
||||
getKeybindings: () => appState.keybindings,
|
||||
getConfiguredShortcuts: () => getConfiguredShortcuts(),
|
||||
getSecondarySubMode: () => appState.secondarySubMode,
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
runSubsyncManual: (request: unknown) =>
|
||||
|
||||
@@ -73,6 +73,7 @@ export interface MainIpcRuntimeServiceDepsParams {
|
||||
toggleVisibleOverlay: IpcDepsRuntimeOptions["toggleVisibleOverlay"];
|
||||
tokenizeCurrentSubtitle: IpcDepsRuntimeOptions["tokenizeCurrentSubtitle"];
|
||||
getCurrentSubtitleAss: IpcDepsRuntimeOptions["getCurrentSubtitleAss"];
|
||||
focusMainWindow?: IpcDepsRuntimeOptions["focusMainWindow"];
|
||||
getMpvSubtitleRenderMetrics: IpcDepsRuntimeOptions["getMpvSubtitleRenderMetrics"];
|
||||
getSubtitlePosition: IpcDepsRuntimeOptions["getSubtitlePosition"];
|
||||
getSubtitleStyle: IpcDepsRuntimeOptions["getSubtitleStyle"];
|
||||
@@ -80,6 +81,7 @@ export interface MainIpcRuntimeServiceDepsParams {
|
||||
getMecabTokenizer: IpcDepsRuntimeOptions["getMecabTokenizer"];
|
||||
handleMpvCommand: IpcDepsRuntimeOptions["handleMpvCommand"];
|
||||
getKeybindings: IpcDepsRuntimeOptions["getKeybindings"];
|
||||
getConfiguredShortcuts: IpcDepsRuntimeOptions["getConfiguredShortcuts"];
|
||||
getSecondarySubMode: IpcDepsRuntimeOptions["getSecondarySubMode"];
|
||||
getMpvClient: IpcDepsRuntimeOptions["getMpvClient"];
|
||||
runSubsyncManual: IpcDepsRuntimeOptions["runSubsyncManual"];
|
||||
@@ -204,6 +206,8 @@ export function createMainIpcRuntimeServiceDeps(
|
||||
getMecabTokenizer: params.getMecabTokenizer,
|
||||
handleMpvCommand: params.handleMpvCommand,
|
||||
getKeybindings: params.getKeybindings,
|
||||
getConfiguredShortcuts: params.getConfiguredShortcuts,
|
||||
focusMainWindow: params.focusMainWindow ?? (() => {}),
|
||||
getSecondarySubMode: params.getSecondarySubMode,
|
||||
getMpvClient: params.getMpvClient,
|
||||
runSubsyncManual: params.runSubsyncManual,
|
||||
|
||||
@@ -46,6 +46,7 @@ import type {
|
||||
RuntimeOptionValue,
|
||||
MpvSubtitleRenderMetrics,
|
||||
OverlayContentMeasurement,
|
||||
ShortcutsConfig,
|
||||
} from "./types";
|
||||
|
||||
const overlayLayerArg = process.argv.find((arg) =>
|
||||
@@ -145,6 +146,8 @@ const electronAPI: ElectronAPI = {
|
||||
|
||||
getKeybindings: (): Promise<Keybinding[]> =>
|
||||
ipcRenderer.invoke("get-keybindings"),
|
||||
getConfiguredShortcuts: (): Promise<Required<ShortcutsConfig>> =>
|
||||
ipcRenderer.invoke("get-config-shortcuts"),
|
||||
|
||||
getJimakuMediaInfo: (): Promise<JimakuMediaInfo> =>
|
||||
ipcRenderer.invoke("jimaku:get-media-info"),
|
||||
@@ -200,6 +203,8 @@ const electronAPI: ElectronAPI = {
|
||||
ipcRenderer.invoke("get-secondary-sub-mode"),
|
||||
getCurrentSecondarySub: (): Promise<string> =>
|
||||
ipcRenderer.invoke("get-current-secondary-sub"),
|
||||
focusMainWindow: () =>
|
||||
ipcRenderer.invoke("focus-main-window") as Promise<void>,
|
||||
getSubtitleStyle: (): Promise<SubtitleStyleConfig | null> =>
|
||||
ipcRenderer.invoke("get-subtitle-style"),
|
||||
onSubsyncManualOpen: (callback: (payload: SubsyncManualPayload) => void) => {
|
||||
|
||||
@@ -8,6 +8,12 @@ export function createKeyboardHandlers(
|
||||
handleSubsyncKeydown: (e: KeyboardEvent) => boolean;
|
||||
handleKikuKeydown: (e: KeyboardEvent) => boolean;
|
||||
handleJimakuKeydown: (e: KeyboardEvent) => boolean;
|
||||
handleSessionHelpKeydown: (e: KeyboardEvent) => boolean;
|
||||
openSessionHelpModal: (opening: {
|
||||
bindingKey: "KeyH" | "KeyK";
|
||||
fallbackUsed: boolean;
|
||||
fallbackUnavailable: boolean;
|
||||
}) => void;
|
||||
saveInvisiblePositionEdit: () => void;
|
||||
cancelInvisiblePositionEdit: () => void;
|
||||
setInvisiblePositionEditMode: (enabled: boolean) => void;
|
||||
@@ -62,6 +68,47 @@ export function createKeyboardHandlers(
|
||||
);
|
||||
}
|
||||
|
||||
function resolveSessionHelpChordBinding(): {
|
||||
bindingKey: "KeyH" | "KeyK";
|
||||
fallbackUsed: boolean;
|
||||
fallbackUnavailable: boolean;
|
||||
} {
|
||||
const firstChoice = "KeyH";
|
||||
if (!ctx.state.keybindingsMap.has("KeyH")) {
|
||||
return {
|
||||
bindingKey: firstChoice,
|
||||
fallbackUsed: false,
|
||||
fallbackUnavailable: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (ctx.state.keybindingsMap.has("KeyK")) {
|
||||
return {
|
||||
bindingKey: "KeyK",
|
||||
fallbackUsed: true,
|
||||
fallbackUnavailable: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
bindingKey: "KeyK",
|
||||
fallbackUsed: true,
|
||||
fallbackUnavailable: false,
|
||||
};
|
||||
}
|
||||
|
||||
function applySessionHelpChordBinding(): void {
|
||||
CHORD_MAP.delete("KeyH");
|
||||
CHORD_MAP.delete("KeyK");
|
||||
const info = resolveSessionHelpChordBinding();
|
||||
CHORD_MAP.set(info.bindingKey, {
|
||||
type: "electron",
|
||||
action: () => {
|
||||
options.openSessionHelpModal(info);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleInvisiblePositionEditKeydown(e: KeyboardEvent): boolean {
|
||||
if (!ctx.platform.isInvisibleLayer) return false;
|
||||
|
||||
@@ -163,6 +210,10 @@ export function createKeyboardHandlers(
|
||||
options.handleJimakuKeydown(e);
|
||||
return;
|
||||
}
|
||||
if (ctx.state.sessionHelpModalOpen) {
|
||||
options.handleSessionHelpKeydown(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.state.chordPending) {
|
||||
const modifierKeys = [
|
||||
@@ -202,6 +253,7 @@ export function createKeyboardHandlers(
|
||||
!e.repeat
|
||||
) {
|
||||
e.preventDefault();
|
||||
applySessionHelpChordBinding();
|
||||
ctx.state.chordPending = true;
|
||||
ctx.state.chordTimeout = setTimeout(() => {
|
||||
resetChord();
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="overlay">
|
||||
<div id="overlay" tabindex="-1">
|
||||
<div id="secondarySubContainer" class="secondary-sub-hidden">
|
||||
<div id="secondarySubRoot"></div>
|
||||
</div>
|
||||
@@ -259,6 +259,30 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sessionHelpModal" class="modal hidden" aria-hidden="true">
|
||||
<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>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="sessionHelpShortcut" class="session-help-shortcut"></div>
|
||||
<div id="sessionHelpWarning" class="session-help-warning"></div>
|
||||
<div id="sessionHelpStatus" class="session-help-status"></div>
|
||||
<input
|
||||
id="sessionHelpFilter"
|
||||
class="session-help-filter"
|
||||
type="text"
|
||||
placeholder="Type / to search shortcuts"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<div id="sessionHelpContent" class="session-help-content-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="renderer.js"></script>
|
||||
</body>
|
||||
|
||||
741
src/renderer/modals/session-help.ts
Normal file
741
src/renderer/modals/session-help.ts
Normal file
@@ -0,0 +1,741 @@
|
||||
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";
|
||||
fallbackUsed: boolean;
|
||||
fallbackUnavailable: boolean;
|
||||
};
|
||||
|
||||
type SessionHelpItem = {
|
||||
shortcut: string;
|
||||
action: string;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
type SessionHelpSection = {
|
||||
title: string;
|
||||
rows: SessionHelpItem[];
|
||||
};
|
||||
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 FALLBACK_COLORS = {
|
||||
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",
|
||||
};
|
||||
|
||||
function normalizeColor(value: unknown, fallback: string): string {
|
||||
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);
|
||||
return token;
|
||||
}
|
||||
|
||||
function formatKeybinding(rawBinding: string): string {
|
||||
const parts = rawBinding.split("+");
|
||||
const key = parts.pop();
|
||||
if (!key) return rawBinding;
|
||||
const normalized = [...parts, normalizeKeyToken(key)];
|
||||
return normalized.join(" + ");
|
||||
}
|
||||
|
||||
const OVERLAY_SHORTCUTS: Array<{
|
||||
key: keyof RuntimeShortcutConfig;
|
||||
label: string;
|
||||
}> = [
|
||||
{ key: "copySubtitle", label: "Copy subtitle" },
|
||||
{ key: "copySubtitleMultiple", label: "Copy subtitle (multi)" },
|
||||
{ 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" },
|
||||
];
|
||||
|
||||
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 (keybind.trim().length === 0) continue;
|
||||
|
||||
rows.push({
|
||||
shortcut: formatKeybinding(keybind),
|
||||
action: shortcut.label,
|
||||
});
|
||||
}
|
||||
|
||||
if (rows.length === 0) return [];
|
||||
return [{ title: "Overlay shortcuts", rows }];
|
||||
}
|
||||
|
||||
function describeCommand(command: (string | number)[]): string {
|
||||
const first = command[0];
|
||||
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 === "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.startsWith(SPECIAL_COMMANDS.RUNTIME_OPTION_CYCLE_PREFIX)) {
|
||||
const [, rawId, rawDirection] = first.split(":");
|
||||
return `Cycle runtime option ${rawId || "option"} ${rawDirection === "prev" ? "previous" : "next"}`;
|
||||
}
|
||||
|
||||
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 (
|
||||
first === "cycle" ||
|
||||
first === "seek" ||
|
||||
first === "sub-seek" ||
|
||||
first === SPECIAL_COMMANDS.REPLAY_SUBTITLE ||
|
||||
first === SPECIAL_COMMANDS.PLAY_NEXT_SUBTITLE
|
||||
) {
|
||||
return "Playback and navigation";
|
||||
}
|
||||
|
||||
if (first === "show-text" || first === "show-progress" || first.startsWith("osd")) {
|
||||
return "Visual feedback";
|
||||
}
|
||||
|
||||
if (first === SPECIAL_COMMANDS.SUBSYNC_TRIGGER) {
|
||||
return "Subtitle sync";
|
||||
}
|
||||
|
||||
if (
|
||||
first === SPECIAL_COMMANDS.RUNTIME_OPTIONS_OPEN ||
|
||||
first.startsWith(SPECIAL_COMMANDS.RUNTIME_OPTION_CYCLE_PREFIX)
|
||||
) {
|
||||
return "Runtime settings";
|
||||
}
|
||||
|
||||
if (first === "quit") return "System actions";
|
||||
return "Other shortcuts";
|
||||
}
|
||||
|
||||
function buildBindingSections(keybindings: Keybinding[]): SessionHelpSection[] {
|
||||
const grouped = new Map<string, SessionHelpItem[]>();
|
||||
|
||||
for (const binding of keybindings) {
|
||||
const section = sectionForCommand(binding.command ?? []);
|
||||
const row: SessionHelpItem = {
|
||||
shortcut: formatKeybinding(binding.key),
|
||||
action: describeCommand(binding.command ?? []),
|
||||
};
|
||||
grouped.set(section, [...(grouped.get(section) ?? []), row]);
|
||||
}
|
||||
|
||||
const sectionOrder = [
|
||||
"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]);
|
||||
const bIdx = sectionOrder.indexOf(b[0]);
|
||||
if (aIdx === -1 && bIdx === -1) return a[0].localeCompare(b[0]);
|
||||
if (aIdx === -1) return 1;
|
||||
if (bIdx === -1) return -1;
|
||||
return aIdx - bIdx;
|
||||
});
|
||||
|
||||
return sectionEntries.map(([title, rows]) => ({ title, rows }));
|
||||
}
|
||||
|
||||
function buildColorSection(style: {
|
||||
knownWordColor?: unknown;
|
||||
nPlusOneColor?: unknown;
|
||||
jlptColors?: {
|
||||
N1?: unknown;
|
||||
N2?: unknown;
|
||||
N3?: unknown;
|
||||
N4?: unknown;
|
||||
N5?: unknown;
|
||||
};
|
||||
}): SessionHelpSection {
|
||||
return {
|
||||
title: "Color legend",
|
||||
rows: [
|
||||
{
|
||||
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: "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 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 N5",
|
||||
action: normalizeColor(style.jlptColors?.N5, FALLBACK_COLORS.jlptN5Color),
|
||||
color: normalizeColor(style.jlptColors?.N5, FALLBACK_COLORS.jlptN5Color),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
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, "");
|
||||
const normalized = normalize(query);
|
||||
if (!normalized) return sections;
|
||||
|
||||
return sections
|
||||
.map((section) => {
|
||||
if (normalize(section.title).includes(normalized)) {
|
||||
return section;
|
||||
}
|
||||
|
||||
const rows = section.rows.filter(
|
||||
(row) =>
|
||||
normalize(row.shortcut).includes(normalized) ||
|
||||
normalize(row.action).includes(normalized),
|
||||
);
|
||||
if (rows.length === 0) return null;
|
||||
return { ...section, rows };
|
||||
})
|
||||
.filter((section): section is SessionHelpSection => section !== null)
|
||||
.filter((section) => section.rows.length > 0);
|
||||
}
|
||||
|
||||
function formatBindingHint(info: SessionHelpBindingInfo): string {
|
||||
if (info.bindingKey === "KeyK" && info.fallbackUsed) {
|
||||
return info.fallbackUnavailable
|
||||
? "Y-K (fallback and conflict noted)"
|
||||
: "Y-K (fallback)";
|
||||
}
|
||||
return "Y-H";
|
||||
}
|
||||
|
||||
function createShortcutRow(row: SessionHelpItem): HTMLButtonElement {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "session-help-item";
|
||||
button.tabIndex = -1;
|
||||
|
||||
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";
|
||||
action.textContent = row.action;
|
||||
right.appendChild(action);
|
||||
|
||||
if (row.color) {
|
||||
const dot = document.createElement("span");
|
||||
dot.className = "session-help-color-dot";
|
||||
dot.style.backgroundColor = row.color;
|
||||
right.insertBefore(dot, action);
|
||||
}
|
||||
|
||||
button.appendChild(left);
|
||||
button.appendChild(right);
|
||||
return button;
|
||||
}
|
||||
|
||||
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": "◈",
|
||||
};
|
||||
|
||||
function createSectionNode(
|
||||
section: SessionHelpSection,
|
||||
sectionIndex: number,
|
||||
onSelect: (index: number) => void,
|
||||
globalIndexMap: number[],
|
||||
): HTMLElement {
|
||||
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] ?? "•";
|
||||
title.textContent = `${icon} ${section.title}`;
|
||||
sectionNode.appendChild(title);
|
||||
|
||||
const list = document.createElement("div");
|
||||
list.className = "session-help-item-list";
|
||||
|
||||
section.rows.forEach((row, rowIndex) => {
|
||||
const button = createShortcutRow(row);
|
||||
const globalIndex = globalIndexMap[sectionIndex] + rowIndex;
|
||||
button.addEventListener("click", () => onSelect(globalIndex));
|
||||
list.appendChild(button);
|
||||
});
|
||||
|
||||
sectionNode.appendChild(list);
|
||||
return sectionNode;
|
||||
}
|
||||
|
||||
export function createSessionHelpModal(
|
||||
ctx: RendererContext,
|
||||
options: {
|
||||
modalStateReader: Pick<ModalStateReader, "isAnyModalOpen">;
|
||||
syncSettingsModalSubtitleSuppression: () => void;
|
||||
},
|
||||
) {
|
||||
let priorFocus: Element | null = null;
|
||||
let openBinding: SessionHelpBindingInfo = {
|
||||
bindingKey: "KeyH",
|
||||
fallbackUsed: false,
|
||||
fallbackUnavailable: false,
|
||||
};
|
||||
let helpFilterValue = "";
|
||||
let helpSections: SessionHelpSection[] = [];
|
||||
let focusGuard: ((event: FocusEvent) => void) | null = null;
|
||||
let windowFocusGuard: (() => void) | null = null;
|
||||
let modalPointerFocusGuard: ((event: Event) => void) | null = null;
|
||||
|
||||
function getItems(): HTMLButtonElement[] {
|
||||
return Array.from(
|
||||
ctx.dom.sessionHelpContent.querySelectorAll(".session-help-item"),
|
||||
) as HTMLButtonElement[];
|
||||
}
|
||||
|
||||
function setSelected(index: number): void {
|
||||
const items = getItems();
|
||||
if (items.length === 0) return;
|
||||
|
||||
const next =
|
||||
((index % items.length) + items.length) % items.length;
|
||||
ctx.state.sessionHelpSelectedIndex = next;
|
||||
|
||||
items.forEach((item, idx) => {
|
||||
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",
|
||||
});
|
||||
}
|
||||
|
||||
function isSessionHelpModalFocusTarget(target: EventTarget | null): boolean {
|
||||
return (
|
||||
target instanceof Element &&
|
||||
ctx.dom.sessionHelpModal.contains(target)
|
||||
);
|
||||
}
|
||||
|
||||
function focusFallbackTarget(): void {
|
||||
void window.electronAPI.focusMainWindow();
|
||||
const items = getItems();
|
||||
const firstItem = items.find((item) => item.offsetParent !== null);
|
||||
if (firstItem) {
|
||||
firstItem.focus({ preventScroll: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.dom.sessionHelpClose instanceof HTMLElement) {
|
||||
ctx.dom.sessionHelpClose.focus({ preventScroll: true });
|
||||
return;
|
||||
}
|
||||
|
||||
window.focus();
|
||||
}
|
||||
|
||||
function enforceModalFocus(): void {
|
||||
if (!ctx.state.sessionHelpModalOpen) return;
|
||||
if (!isSessionHelpModalFocusTarget(document.activeElement)) {
|
||||
focusFallbackTarget();
|
||||
}
|
||||
}
|
||||
|
||||
function isFilterInputFocused(): boolean {
|
||||
return document.activeElement === ctx.dom.sessionHelpFilter;
|
||||
}
|
||||
|
||||
function focusFilterInput(): void {
|
||||
ctx.dom.sessionHelpFilter.focus({ preventScroll: true });
|
||||
ctx.dom.sessionHelpFilter.select();
|
||||
}
|
||||
|
||||
function applyFilterAndRender(): void {
|
||||
const sections = filterSections(helpSections, helpFilterValue);
|
||||
const indexOffsets: number[] = [];
|
||||
let running = 0;
|
||||
for (const section of sections) {
|
||||
indexOffsets.push(running);
|
||||
running += section.rows.length;
|
||||
}
|
||||
|
||||
ctx.dom.sessionHelpContent.innerHTML = "";
|
||||
sections.forEach((section, sectionIndex) => {
|
||||
const sectionNode = createSectionNode(
|
||||
section,
|
||||
sectionIndex,
|
||||
(selectionIndex) => {
|
||||
setSelected(selectionIndex);
|
||||
},
|
||||
indexOffsets,
|
||||
);
|
||||
ctx.dom.sessionHelpContent.appendChild(sectionNode);
|
||||
});
|
||||
|
||||
if (getItems().length === 0) {
|
||||
ctx.dom.sessionHelpContent.classList.add("session-help-content-no-results");
|
||||
ctx.dom.sessionHelpContent.textContent = helpFilterValue
|
||||
? "No matching shortcuts found."
|
||||
: "No active session shortcuts found.";
|
||||
ctx.state.sessionHelpSelectedIndex = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.dom.sessionHelpContent.classList.remove("session-help-content-no-results");
|
||||
|
||||
if (isFilterInputFocused()) return;
|
||||
|
||||
setSelected(0);
|
||||
}
|
||||
|
||||
function requestOverlayFocus(): void {
|
||||
void window.electronAPI.focusMainWindow();
|
||||
}
|
||||
|
||||
function addPointerFocusListener(): void {
|
||||
if (modalPointerFocusGuard) return;
|
||||
|
||||
modalPointerFocusGuard = () => {
|
||||
requestOverlayFocus();
|
||||
enforceModalFocus();
|
||||
};
|
||||
ctx.dom.sessionHelpModal.addEventListener("pointerdown", modalPointerFocusGuard);
|
||||
ctx.dom.sessionHelpModal.addEventListener("mousedown", modalPointerFocusGuard);
|
||||
ctx.dom.sessionHelpModal.addEventListener("click", modalPointerFocusGuard);
|
||||
}
|
||||
|
||||
function removePointerFocusListener(): void {
|
||||
if (!modalPointerFocusGuard) return;
|
||||
ctx.dom.sessionHelpModal.removeEventListener("pointerdown", modalPointerFocusGuard);
|
||||
ctx.dom.sessionHelpModal.removeEventListener("mousedown", modalPointerFocusGuard);
|
||||
ctx.dom.sessionHelpModal.removeEventListener("click", modalPointerFocusGuard);
|
||||
modalPointerFocusGuard = null;
|
||||
}
|
||||
|
||||
function startFocusRecoveryGuards(): void {
|
||||
if (windowFocusGuard) return;
|
||||
|
||||
windowFocusGuard = () => {
|
||||
requestOverlayFocus();
|
||||
enforceModalFocus();
|
||||
};
|
||||
window.addEventListener("blur", windowFocusGuard);
|
||||
window.addEventListener("focus", windowFocusGuard);
|
||||
}
|
||||
|
||||
function stopFocusRecoveryGuards(): void {
|
||||
if (!windowFocusGuard) return;
|
||||
window.removeEventListener("blur", windowFocusGuard);
|
||||
window.removeEventListener("focus", windowFocusGuard);
|
||||
windowFocusGuard = null;
|
||||
}
|
||||
|
||||
async function render(): Promise<void> {
|
||||
const [keybindings, styleConfig, shortcuts] = await Promise.all([
|
||||
window.electronAPI.getKeybindings(),
|
||||
window.electronAPI.getSubtitleStyle(),
|
||||
window.electronAPI.getConfiguredShortcuts(),
|
||||
]);
|
||||
|
||||
const bindingSections = buildBindingSections(keybindings);
|
||||
if (bindingSections.length > 0) {
|
||||
const playback = bindingSections.find(
|
||||
(section) => section.title === "Playback and navigation",
|
||||
);
|
||||
if (playback) {
|
||||
playback.title = "MPV shortcuts";
|
||||
}
|
||||
}
|
||||
|
||||
const shortcutSections = buildOverlayShortcutSections(shortcuts);
|
||||
if (shortcutSections.length > 0) {
|
||||
shortcutSections[0].title = "Overlay shortcuts (configurable)";
|
||||
}
|
||||
const colorSection = buildColorSection(styleConfig ?? {});
|
||||
helpSections = [...bindingSections, ...shortcutSections, colorSection];
|
||||
applyFilterAndRender();
|
||||
}
|
||||
|
||||
async function openSessionHelpModal(opening: SessionHelpBindingInfo): Promise<void> {
|
||||
openBinding = opening;
|
||||
priorFocus = document.activeElement;
|
||||
|
||||
await render();
|
||||
|
||||
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.";
|
||||
} else if (openBinding.fallbackUsed) {
|
||||
ctx.dom.sessionHelpWarning.textContent = "Y-H is already bound; using Y-K as fallback.";
|
||||
} else {
|
||||
ctx.dom.sessionHelpWarning.textContent = "";
|
||||
}
|
||||
ctx.dom.sessionHelpStatus.textContent =
|
||||
"Use Arrow keys, J/K/H/L, mouse, click, or / then type to filter. Esc closes.";
|
||||
|
||||
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 = "";
|
||||
if (ctx.platform.shouldToggleMouseIgnore) {
|
||||
window.electronAPI.setIgnoreMouseEvents(false);
|
||||
}
|
||||
|
||||
if (focusGuard === null) {
|
||||
focusGuard = (event: FocusEvent) => {
|
||||
if (!ctx.state.sessionHelpModalOpen) return;
|
||||
if (!isSessionHelpModalFocusTarget(event.target)) {
|
||||
event.preventDefault();
|
||||
enforceModalFocus();
|
||||
}
|
||||
};
|
||||
document.addEventListener("focusin", focusGuard);
|
||||
}
|
||||
|
||||
addPointerFocusListener();
|
||||
startFocusRecoveryGuards();
|
||||
requestOverlayFocus();
|
||||
window.focus();
|
||||
enforceModalFocus();
|
||||
}
|
||||
|
||||
function closeSessionHelpModal(): void {
|
||||
if (!ctx.state.sessionHelpModalOpen) return;
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
if (focusGuard) {
|
||||
document.removeEventListener("focusin", focusGuard);
|
||||
focusGuard = null;
|
||||
}
|
||||
removePointerFocusListener();
|
||||
stopFocusRecoveryGuards();
|
||||
|
||||
if (priorFocus instanceof HTMLElement && priorFocus.isConnected) {
|
||||
priorFocus.focus({ preventScroll: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.dom.overlay instanceof HTMLElement) {
|
||||
ctx.dom.overlay.focus({ preventScroll: true });
|
||||
}
|
||||
if (ctx.platform.shouldToggleMouseIgnore) {
|
||||
if (!ctx.state.isOverSubtitle && !options.modalStateReader.isAnyModalOpen()) {
|
||||
window.electronAPI.setIgnoreMouseEvents(true, { forward: true });
|
||||
} else {
|
||||
window.electronAPI.setIgnoreMouseEvents(false);
|
||||
}
|
||||
}
|
||||
ctx.dom.sessionHelpFilter.value = "";
|
||||
helpFilterValue = "";
|
||||
window.focus();
|
||||
}
|
||||
|
||||
function handleSessionHelpKeydown(e: KeyboardEvent): boolean {
|
||||
if (!ctx.state.sessionHelpModalOpen) return false;
|
||||
|
||||
if (isFilterInputFocused()) {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
if (!helpFilterValue) {
|
||||
closeSessionHelpModal();
|
||||
return true;
|
||||
}
|
||||
|
||||
helpFilterValue = "";
|
||||
ctx.dom.sessionHelpFilter.value = "";
|
||||
applyFilterAndRender();
|
||||
focusFallbackTarget();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
closeSessionHelpModal();
|
||||
return true;
|
||||
}
|
||||
|
||||
const items = getItems();
|
||||
if (items.length === 0) return true;
|
||||
|
||||
if (
|
||||
e.key === "/" &&
|
||||
!e.ctrlKey &&
|
||||
!e.metaKey &&
|
||||
!e.altKey &&
|
||||
!e.shiftKey
|
||||
) {
|
||||
e.preventDefault();
|
||||
focusFilterInput();
|
||||
return true;
|
||||
}
|
||||
|
||||
const key = e.key.toLowerCase();
|
||||
|
||||
if (
|
||||
key === "arrowdown" ||
|
||||
key === "j" ||
|
||||
key === "l"
|
||||
) {
|
||||
e.preventDefault();
|
||||
setSelected(ctx.state.sessionHelpSelectedIndex + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
key === "arrowup" ||
|
||||
key === "k" ||
|
||||
key === "h"
|
||||
) {
|
||||
e.preventDefault();
|
||||
setSelected(ctx.state.sessionHelpSelectedIndex - 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function wireDomEvents(): void {
|
||||
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.sessionHelpClose.addEventListener("click", () => {
|
||||
closeSessionHelpModal();
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
closeSessionHelpModal,
|
||||
handleSessionHelpKeydown,
|
||||
openSessionHelpModal,
|
||||
wireDomEvents,
|
||||
};
|
||||
}
|
||||
@@ -29,6 +29,7 @@ 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";
|
||||
@@ -49,7 +50,8 @@ function isAnySettingsModalOpen(): boolean {
|
||||
ctx.state.runtimeOptionsModalOpen ||
|
||||
ctx.state.subsyncModalOpen ||
|
||||
ctx.state.kikuModalOpen ||
|
||||
ctx.state.jimakuModalOpen
|
||||
ctx.state.jimakuModalOpen ||
|
||||
ctx.state.sessionHelpModalOpen
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,7 +60,8 @@ function isAnyModalOpen(): boolean {
|
||||
ctx.state.jimakuModalOpen ||
|
||||
ctx.state.kikuModalOpen ||
|
||||
ctx.state.runtimeOptionsModalOpen ||
|
||||
ctx.state.subsyncModalOpen
|
||||
ctx.state.subsyncModalOpen ||
|
||||
ctx.state.sessionHelpModalOpen
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,6 +87,10 @@ const subsyncModal = createSubsyncModal(ctx, {
|
||||
modalStateReader: { isAnyModalOpen },
|
||||
syncSettingsModalSubtitleSuppression,
|
||||
});
|
||||
const sessionHelpModal = createSessionHelpModal(ctx, {
|
||||
modalStateReader: { isAnyModalOpen },
|
||||
syncSettingsModalSubtitleSuppression,
|
||||
});
|
||||
const kikuModal = createKikuModal(ctx, {
|
||||
modalStateReader: { isAnyModalOpen },
|
||||
syncSettingsModalSubtitleSuppression,
|
||||
@@ -97,6 +104,8 @@ const keyboardHandlers = createKeyboardHandlers(ctx, {
|
||||
handleSubsyncKeydown: subsyncModal.handleSubsyncKeydown,
|
||||
handleKikuKeydown: kikuModal.handleKikuKeydown,
|
||||
handleJimakuKeydown: jimakuModal.handleJimakuKeydown,
|
||||
handleSessionHelpKeydown: sessionHelpModal.handleSessionHelpKeydown,
|
||||
openSessionHelpModal: sessionHelpModal.openSessionHelpModal,
|
||||
saveInvisiblePositionEdit: positioning.saveInvisiblePositionEdit,
|
||||
cancelInvisiblePositionEdit: positioning.cancelInvisiblePositionEdit,
|
||||
setInvisiblePositionEditMode: positioning.setInvisiblePositionEditMode,
|
||||
@@ -178,6 +187,7 @@ async function init(): Promise<void> {
|
||||
kikuModal.wireDomEvents();
|
||||
runtimeOptionsModal.wireDomEvents();
|
||||
subsyncModal.wireDomEvents();
|
||||
sessionHelpModal.wireDomEvents();
|
||||
|
||||
window.electronAPI.onRuntimeOptionsChanged((options: RuntimeOptionState[]) => {
|
||||
runtimeOptionsModal.updateRuntimeOptions(options);
|
||||
|
||||
@@ -54,6 +54,9 @@ export type RendererState = {
|
||||
subsyncSourceTracks: SubsyncSourceTrack[];
|
||||
subsyncSubmitting: boolean;
|
||||
|
||||
sessionHelpModalOpen: boolean;
|
||||
sessionHelpSelectedIndex: number;
|
||||
|
||||
mpvSubtitleRenderMetrics: MpvSubtitleRenderMetrics | null;
|
||||
invisiblePositionEditMode: boolean;
|
||||
invisiblePositionEditStartX: number;
|
||||
@@ -118,6 +121,9 @@ export function createRendererState(): RendererState {
|
||||
subsyncSourceTracks: [],
|
||||
subsyncSubmitting: false,
|
||||
|
||||
sessionHelpModalOpen: false,
|
||||
sessionHelpSelectedIndex: 0,
|
||||
|
||||
mpvSubtitleRenderMetrics: null,
|
||||
invisiblePositionEditMode: false,
|
||||
invisiblePositionEditStartX: 0,
|
||||
|
||||
@@ -807,8 +807,198 @@ iframe[id^="yomitan-popup"] {
|
||||
color: #ff8f8f;
|
||||
}
|
||||
|
||||
.session-help-content {
|
||||
width: min(760px, 92%);
|
||||
max-height: 84%;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.session-help-shortcut,
|
||||
.session-help-warning,
|
||||
.session-help-status {
|
||||
min-height: 18px;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.session-help-shortcut {
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.97);
|
||||
}
|
||||
|
||||
.session-help-warning {
|
||||
color: #f8a100;
|
||||
}
|
||||
|
||||
.session-help-content-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
max-height: calc(84vh - 220px);
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.session-help-filter {
|
||||
width: 100%;
|
||||
min-height: 32px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.session-help-filter::placeholder {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.session-help-filter:focus {
|
||||
outline: none;
|
||||
border-color: rgba(137, 180, 255, 0.6);
|
||||
box-shadow: 0 0 0 2px rgba(137, 180, 255, 0.2);
|
||||
}
|
||||
|
||||
.session-help-content-no-results {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.session-help-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
backdrop-filter: blur(1px);
|
||||
}
|
||||
|
||||
.session-help-section-title {
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 800;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.session-help-item-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.session-help-item {
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
padding: 10px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
text-align: left;
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.session-help-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.session-help-item:hover,
|
||||
.session-help-item:focus-visible,
|
||||
.session-help-item.active {
|
||||
background: rgba(137, 180, 255, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.session-help-item.active {
|
||||
box-shadow: inset 3px 0 0 0 rgba(137, 180, 255, 0.9);
|
||||
}
|
||||
|
||||
.session-help-item-left {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.session-help-item-right {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.session-help-key {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
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;
|
||||
border-radius: 999px;
|
||||
background: rgba(137, 180, 255, 0.16);
|
||||
border: 1px solid rgba(137, 180, 255, 0.35);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.session-help-action {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.84);
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.session-help-color-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.kiku-cards-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.session-help-content-list {
|
||||
max-height: calc(84vh - 190px);
|
||||
}
|
||||
|
||||
.session-help-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.session-help-item-right {
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.session-help-key {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,14 @@ export type RendererDom = {
|
||||
subsyncSourceSelect: HTMLSelectElement;
|
||||
subsyncRunButton: HTMLButtonElement;
|
||||
subsyncStatus: HTMLDivElement;
|
||||
|
||||
sessionHelpModal: HTMLDivElement;
|
||||
sessionHelpClose: HTMLButtonElement;
|
||||
sessionHelpShortcut: HTMLDivElement;
|
||||
sessionHelpWarning: HTMLDivElement;
|
||||
sessionHelpStatus: HTMLDivElement;
|
||||
sessionHelpFilter: HTMLInputElement;
|
||||
sessionHelpContent: HTMLDivElement;
|
||||
};
|
||||
|
||||
function getRequiredElement<T extends HTMLElement>(id: string): T {
|
||||
@@ -127,5 +135,13 @@ export function resolveRendererDom(): RendererDom {
|
||||
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"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -626,6 +626,7 @@ export interface ElectronAPI {
|
||||
setMecabEnabled: (enabled: boolean) => void;
|
||||
sendMpvCommand: (command: (string | number)[]) => void;
|
||||
getKeybindings: () => Promise<Keybinding[]>;
|
||||
getConfiguredShortcuts: () => Promise<Required<ShortcutsConfig>>;
|
||||
getJimakuMediaInfo: () => Promise<JimakuMediaInfo>;
|
||||
jimakuSearchEntries: (
|
||||
query: JimakuSearchQuery,
|
||||
@@ -646,6 +647,7 @@ export interface ElectronAPI {
|
||||
onSecondarySubMode: (callback: (mode: SecondarySubMode) => void) => void;
|
||||
getSecondarySubMode: () => Promise<SecondarySubMode>;
|
||||
getCurrentSecondarySub: () => Promise<string>;
|
||||
focusMainWindow: () => Promise<void>;
|
||||
getSubtitleStyle: () => Promise<SubtitleStyleConfig | null>;
|
||||
onSubsyncManualOpen: (
|
||||
callback: (payload: SubsyncManualPayload) => void,
|
||||
|
||||
Reference in New Issue
Block a user