dev: apply review suggestions and make auto-preview toggleable

This commit is contained in:
QuadnucYard 2025-11-15 21:24:20 +08:00
parent fcaf7a7e9a
commit 25b8a77ae8
9 changed files with 61 additions and 63 deletions

View file

@ -1,5 +1,5 @@
{ {
"$schema": "https://biomejs.dev/schemas/2.3.1/schema.json", "$schema": "https://biomejs.dev/schemas/2.3.5/schema.json",
"vcs": { "vcs": {
"enabled": false, "enabled": false,
"clientKind": "git", "clientKind": "git",

View file

@ -282,7 +282,8 @@ export const messageHandlers: Record<string, MessageHandler> = {
} }
} }
} catch (error) { } catch (error) {
await vscode.window.showErrorMessage(`Export failed: ${error}`); const message = error instanceof Error ? error.message : String(error);
await vscode.window.showErrorMessage(`Export failed: ${message}`);
} }
}, },
@ -298,17 +299,14 @@ export const messageHandlers: Record<string, MessageHandler> = {
}); });
}; };
console.log(`Generating preview for format=${format}, extraArgs=`, extraArgs);
try { try {
const ops = exportOps(extraArgs); const ops = exportOps(extraArgs);
const formatProvider = provideFormats(extraArgs); const formatProvider = provideFormats(extraArgs);
// await vscode.window.showInformationMessage(`Active `)
// Get the active document // Get the active document
const uri = ops.resolveInputPath(); const uri = ops.resolveInputPath();
if (!uri) { if (!uri) {
// sendError("No active document found"); // Just ignore if no active document
return; return;
} }
@ -332,7 +330,6 @@ export const messageHandlers: Record<string, MessageHandler> = {
return; return;
} }
console.log(`Generating preview for format=${format}, extraArgs=${extraArgs}, uri=${uri}`);
// For visual formats, generate PNG/SVG previews // For visual formats, generate PNG/SVG previews
if (format === "pdf" || format === "png" || format === "svg") { if (format === "pdf" || format === "png" || format === "svg") {
// Extract base64 data from the response // Extract base64 data from the response
@ -362,7 +359,6 @@ export const messageHandlers: Record<string, MessageHandler> = {
} }
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : String(error); const message = error instanceof Error ? error.message : String(error);
console.log("Preview generation failed:", error);
sendError(`Preview generation failed: ${message}`); sendError(`Preview generation failed: ${message}`);
} }
}, },

View file

@ -12,34 +12,6 @@ export const ActionButtons = ({ onExport }: ActionButtonsProps) => {
h3("Export Actions"), h3("Export Actions"),
// Task Label Input
/* div(
{ class: "card flex flex-col gap-xs" },
label(
{
class: "text-sm font-medium",
for: "task-label",
style: "margin-bottom: 0.5rem; display: block;",
},
"Custom Task Label (Optional)",
),
input({
class: "input",
type: "text",
id: "task-label",
placeholder: `Export to ${exportConfig.val.format.label}`,
value: customTaskLabel,
oninput: (e: Event) => {
const target = e.target as HTMLInputElement;
customTaskLabel.val = target.value;
},
}),
div(
{ class: "text-xs text-desc" },
"This will be used as the task name in tasks.json. Leave empty for default naming.",
),
), */
// Action Buttons // Action Buttons
div( div(
{ class: "action-buttons flex items-center gap-md" }, { class: "action-buttons flex items-center gap-md" },

View file

@ -23,20 +23,8 @@ export const Header =
{ class: "flex flex-col sm:flex-row sm:justify-between sm:items-center gap-sm" }, { class: "flex flex-col sm:flex-row sm:justify-between sm:items-center gap-sm" },
div( div(
{ class: "flex flex-col gap-xs" }, { class: "flex flex-col gap-xs" },
h1( h1({ class: "text-xl font-semibold text-base-content" }, title),
{ p({ class: "text-desc font-sm" }, description),
class: "text-xl font-semibold text-base-content",
style: "margin: 0; font-size: 1.25rem; font-weight: 600;",
},
title,
),
p(
{
class: "text-desc",
style: "margin: 0; font-size: 0.875rem;",
},
description,
),
), ),
actions ? div({ class: "flex gap-xs" }, actions) : null, actions ? div({ class: "flex gap-xs" }, actions) : null,
), ),

View file

@ -67,7 +67,7 @@ const OptionField = (schema: OptionSchema, valueState: State<Scalar>) => {
const renderInput = ( const renderInput = (
schema: OptionSchema, schema: OptionSchema,
valueState: State<Scalar>, valueState: State<Scalar | undefined>,
validationError: State<string | undefined>, validationError: State<string | undefined>,
) => { ) => {
const { type, key, options: selectOptions, min, max } = schema; const { type, key, options: selectOptions, min, max } = schema;
@ -102,7 +102,7 @@ const renderInput = (
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
// Call custom validation function if provided // Call custom validation function if provided
validationError.val = schema.validate?.(target.value); validationError.val = schema.validate?.(target.value);
valueState.val = parseFloat(target.value); valueState.val = target.value === "" ? undefined : parseFloat(target.value);
}, },
}); });

View file

@ -1,17 +1,18 @@
import van, { type State } from "vanjs-core"; import van, { type State } from "vanjs-core";
import type { ExportFormat, PreviewData, PreviewPage } from "../types"; import type { ExportFormat, PreviewData, PreviewPage } from "../types";
const { div, h3, img, span, button } = van.tags; const { div, h3, img, span, button, label, input } = van.tags;
interface PreviewGridProps { interface PreviewGridProps {
format: State<ExportFormat>; format: State<ExportFormat>;
previewData: State<PreviewData>; previewData: State<PreviewData>;
previewGenerating: State<boolean>; previewGenerating: State<boolean>;
autoPreview: State<boolean>;
onPreview: () => void; onPreview: () => void;
} }
export const PreviewGrid = (props: PreviewGridProps) => { export const PreviewGrid = (props: PreviewGridProps) => {
const { format, previewData, previewGenerating, onPreview } = props; const { format, previewData, previewGenerating, autoPreview, onPreview } = props;
const thumbnailZoom = van.state<number>(100); // Percentage for thumbnail sizing const thumbnailZoom = van.state<number>(100); // Percentage for thumbnail sizing
@ -19,14 +20,13 @@ export const PreviewGrid = (props: PreviewGridProps) => {
// Preview Header // Preview Header
div( div(
{ class: "flex justify-between items-center mb-md" }, { class: "flex justify-between items-center mb-md" },
h3({ class: "text-lg font-semibold" }, () => `Preview (${format.val.label})`), div(
() => { class: "flex items-center gap-sm" },
div(
{ class: "flex items-center gap-sm", style: "min-height: 2rem;" }, h3({ class: "text-lg font-semibold" }, () => `Preview (${format.val.label})`),
// Only show zoom controls for image content (thumbnails)
!previewGenerating.val && previewData.val.pages && previewData.val.pages.length > 0 // Generate Preview Button
? ZoomControls(thumbnailZoom) () =>
: null,
button( button(
{ {
class: "btn btn-secondary", class: "btn btn-secondary",
@ -35,6 +35,31 @@ export const PreviewGrid = (props: PreviewGridProps) => {
}, },
previewGenerating.val ? "Generating..." : "Generate Preview", previewGenerating.val ? "Generating..." : "Generate Preview",
), ),
// Auto-preview toggle
label(
{ class: "flex items-center gap-xs cursor-pointer", style: "user-select: none;" },
input({
type: "checkbox",
class: "toggle",
checked: autoPreview,
onchange: (e: Event) => {
const target = e.target as HTMLInputElement;
autoPreview.val = target.checked;
},
}),
span({ class: "text-sm" }, "Auto"),
),
),
() =>
div(
{ class: "flex items-center gap-sm", style: "min-height: 2rem;" },
// Only show zoom controls for image content (thumbnails)
!previewGenerating.val && previewData.val.pages && previewData.val.pages.length > 0
? ZoomControls(thumbnailZoom)
: null,
), ),
), ),

View file

@ -14,6 +14,7 @@ export function useExporter() {
let previewVersion = 0; let previewVersion = 0;
const previewGenerating = van.state(false); const previewGenerating = van.state(false);
const previewData = van.state<PreviewData>({}); const previewData = van.state<PreviewData>({});
const autoPreview = van.state(true);
const buildOptions = () => { const buildOptions = () => {
const extraOpts = Object.fromEntries( const extraOpts = Object.fromEntries(
@ -52,6 +53,8 @@ export function useExporter() {
// Regenerate preview automatically when format or options change // Regenerate preview automatically when format or options change
van.derive(() => { van.derive(() => {
if (!autoPreview.val) return;
if (format.oldVal !== format.val) { if (format.oldVal !== format.val) {
// Clear previous preview data when format changes // Clear previous preview data when format changes
previewData.val = {}; previewData.val = {};
@ -71,6 +74,10 @@ export function useExporter() {
}; };
window?.addEventListener("message", handleMessage); window?.addEventListener("message", handleMessage);
const cleanup = () => {
window?.removeEventListener("message", handleMessage);
};
return { return {
inputPath, inputPath,
outputPath, outputPath,
@ -78,8 +85,10 @@ export function useExporter() {
optionStates, optionStates,
previewGenerating, previewGenerating,
previewData, previewData,
autoPreview,
exportDocument, exportDocument,
generatePreview, generatePreview,
cleanup,
}; };
} }

View file

@ -45,9 +45,11 @@ export const EXPORT_FORMATS: ExportFormat[] = [
if (value.trim() === "") { if (value.trim() === "") {
return; // Allow empty input return; // Allow empty input
} }
if (!/^\d+$/.test(value)) {
return "Creation timestamp must be a valid non-negative integer UNIX timestamp";
}
const num = Number(value); const num = Number(value);
if (Number.isNaN(num) || !Number.isInteger(num) || num < 0) { if (Number.isNaN(num) || !Number.isInteger(num) || num < 0) {
// fixme: it still accepts floating point numbers like "1e5"
return "Creation timestamp must be a valid non-negative integer UNIX timestamp"; return "Creation timestamp must be a valid non-negative integer UNIX timestamp";
} }
}, },

View file

@ -23,10 +23,15 @@ const ExportTool = () => {
optionStates, optionStates,
previewGenerating, previewGenerating,
previewData, previewData,
autoPreview,
exportDocument, exportDocument,
generatePreview, generatePreview,
} = useExporter(); } = useExporter();
// Note: cleanup() should be called when the component is unmounted
// In the current single-page architecture, this might not be needed,
// but it's available for future use if the tool becomes part of a larger app
return div( return div(
{ class: "export-tool-container flex flex-col gap-lg text-base-content" }, { class: "export-tool-container flex flex-col gap-lg text-base-content" },
@ -49,6 +54,7 @@ const ExportTool = () => {
format: format, format: format,
previewData, previewData,
previewGenerating, previewGenerating,
autoPreview,
onPreview: generatePreview, onPreview: generatePreview,
}), }),