Fix renderer overlay loading and modularize renderer

This commit is contained in:
2026-02-11 18:27:29 -08:00
parent ee21c77fd0
commit 8a82a1b5f9
29 changed files with 3150 additions and 2741 deletions

307
src/renderer/modals/kiku.ts Normal file
View File

@@ -0,0 +1,307 @@
import type {
KikuDuplicateCardInfo,
KikuFieldGroupingChoice,
KikuMergePreviewResponse,
} from "../../types";
import type { ModalStateReader, RendererContext } from "../context";
export function createKikuModal(
ctx: RendererContext,
options: {
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(" | ");
}
function updateKikuCardSelection(): void {
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 {
ctx.state.kikuModalStep = step;
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";
}
function updateKikuPreviewToggle(): void {
ctx.dom.kikuPreviewCompactButton.classList.toggle(
"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.kikuPreviewCompactData
: ctx.state.kikuPreviewFullData;
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");
return;
}
ctx.dom.kikuPreviewError.textContent = message;
ctx.dom.kikuPreviewError.classList.remove("hidden");
}
function openKikuFieldGroupingModal(data: {
original: KikuDuplicateCardInfo;
duplicate: KikuDuplicateCardInfo;
}): void {
if (ctx.platform.isInvisibleLayer) return;
if (ctx.state.kikuModalOpen) return;
ctx.state.kikuModalOpen = true;
ctx.state.kikuOriginalData = data.original;
ctx.state.kikuDuplicateData = data.duplicate;
ctx.state.kikuSelectedCard = 1;
ctx.dom.kikuCard1Expression.textContent = data.original.expression;
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.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";
renderKikuPreview();
setKikuPreviewError(null);
setKikuModalStep("select");
updateKikuCardSelection();
options.syncSettingsModalSubtitleSuppression();
ctx.dom.overlay.classList.add("interactive");
ctx.dom.kikuModal.classList.remove("hidden");
ctx.dom.kikuModal.setAttribute("aria-hidden", "false");
}
function closeKikuFieldGroupingModal(): void {
if (!ctx.state.kikuModalOpen) return;
ctx.state.kikuModalOpen = false;
options.syncSettingsModalSubtitleSuppression();
ctx.dom.kikuModal.classList.add("hidden");
ctx.dom.kikuModal.setAttribute("aria-hidden", "true");
setKikuPreviewError(null);
ctx.dom.kikuPreviewJson.textContent = "";
ctx.state.kikuPendingChoice = null;
ctx.state.kikuPreviewCompactData = null;
ctx.state.kikuPreviewFullData = null;
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");
}
}
async function confirmKikuSelection(): Promise<void> {
if (!ctx.state.kikuOriginalData || !ctx.state.kikuDuplicateData) return;
const keepData =
ctx.state.kikuSelectedCard === 1
? ctx.state.kikuOriginalData
: ctx.state.kikuDuplicateData;
const deleteData =
ctx.state.kikuSelectedCard === 1
? ctx.state.kikuDuplicateData
: ctx.state.kikuOriginalData;
const choice: KikuFieldGroupingChoice = {
keepNoteId: keepData.noteId,
deleteNoteId: deleteData.noteId,
deleteDuplicate: ctx.dom.kikuDeleteDuplicateCheckbox.checked,
cancelled: false,
};
ctx.state.kikuPendingChoice = choice;
setKikuPreviewError(null);
ctx.dom.kikuConfirmButton.disabled = true;
try {
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");
return;
}
ctx.state.kikuPreviewCompactData = preview.compact || {};
ctx.state.kikuPreviewFullData = preview.full || {};
ctx.state.kikuPreviewMode = "compact";
renderKikuPreview();
setKikuModalStep("preview");
} finally {
ctx.dom.kikuConfirmButton.disabled = false;
}
}
function confirmKikuMerge(): void {
if (!ctx.state.kikuPendingChoice) return;
window.electronAPI.kikuFieldGroupingRespond(ctx.state.kikuPendingChoice);
closeKikuFieldGroupingModal();
}
function goBackFromKikuPreview(): void {
setKikuPreviewError(null);
setKikuModalStep("select");
}
function cancelKikuFieldGrouping(): void {
const choice: KikuFieldGroupingChoice = {
keepNoteId: 0,
deleteNoteId: 0,
deleteDuplicate: true,
cancelled: true,
};
window.electronAPI.kikuFieldGroupingRespond(choice);
closeKikuFieldGroupingModal();
}
function handleKikuKeydown(e: KeyboardEvent): boolean {
if (ctx.state.kikuModalStep === "preview") {
if (e.key === "Escape") {
e.preventDefault();
cancelKikuFieldGrouping();
return true;
}
if (e.key === "Backspace") {
e.preventDefault();
goBackFromKikuPreview();
return true;
}
if (e.key === "Enter") {
e.preventDefault();
confirmKikuMerge();
return true;
}
return true;
}
if (e.key === "Escape") {
e.preventDefault();
cancelKikuFieldGrouping();
return true;
}
if (e.key === "1") {
e.preventDefault();
ctx.state.kikuSelectedCard = 1;
updateKikuCardSelection();
return true;
}
if (e.key === "2") {
e.preventDefault();
ctx.state.kikuSelectedCard = 2;
updateKikuCardSelection();
return true;
}
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") {
e.preventDefault();
void confirmKikuSelection();
return true;
}
return true;
}
function wireDomEvents(): void {
ctx.dom.kikuCard1.addEventListener("click", () => {
ctx.state.kikuSelectedCard = 1;
updateKikuCardSelection();
});
ctx.dom.kikuCard1.addEventListener("dblclick", () => {
ctx.state.kikuSelectedCard = 1;
void confirmKikuSelection();
});
ctx.dom.kikuCard2.addEventListener("click", () => {
ctx.state.kikuSelectedCard = 2;
updateKikuCardSelection();
});
ctx.dom.kikuCard2.addEventListener("dblclick", () => {
ctx.state.kikuSelectedCard = 2;
void confirmKikuSelection();
});
ctx.dom.kikuConfirmButton.addEventListener("click", () => {
void confirmKikuSelection();
});
ctx.dom.kikuCancelButton.addEventListener("click", () => {
cancelKikuFieldGrouping();
});
ctx.dom.kikuBackButton.addEventListener("click", () => {
goBackFromKikuPreview();
});
ctx.dom.kikuFinalConfirmButton.addEventListener("click", () => {
confirmKikuMerge();
});
ctx.dom.kikuFinalCancelButton.addEventListener("click", () => {
cancelKikuFieldGrouping();
});
ctx.dom.kikuPreviewCompactButton.addEventListener("click", () => {
ctx.state.kikuPreviewMode = "compact";
renderKikuPreview();
});
ctx.dom.kikuPreviewFullButton.addEventListener("click", () => {
ctx.state.kikuPreviewMode = "full";
renderKikuPreview();
});
}
return {
closeKikuFieldGroupingModal,
handleKikuKeydown,
openKikuFieldGroupingModal,
wireDomEvents,
};
}