From 4c21ac56db49cfc4ad133f20db62ef38a46c72d8 Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Thu, 2 Jul 2026 00:01:58 +0800 Subject: [PATCH 1/7] Fix QR download helpers --- .../qr-code-generator/browser-actions.ts | 93 ++++++++++++++++--- 1 file changed, 80 insertions(+), 13 deletions(-) diff --git a/src/features/tools/qr-code-generator/browser-actions.ts b/src/features/tools/qr-code-generator/browser-actions.ts index 83fa23ce..d17b729a 100644 --- a/src/features/tools/qr-code-generator/browser-actions.ts +++ b/src/features/tools/qr-code-generator/browser-actions.ts @@ -1,6 +1,15 @@ import type { ErrorCorrectionLevel } from "./types" import { FILE_INPUT_POLICIES, validateFileAgainstPolicy } from "@/core/files/file-input-policy" +const OBJECT_URL_REVOKE_DELAY_MS = 1_000 + +export type BrowserDownloadResult = { + ok: true +} | { + ok: false + error: string +} + let qrCodePromise: Promise | null = null let toastPromise: Promise | null = null @@ -64,11 +73,75 @@ export function readFileAsDataUrl(file: File): Promise { }) } -export function downloadDataUrl(dataUrl: string, filename: string) { - const a = document.createElement("a") - a.href = dataUrl - a.download = filename - a.click() +function deferObjectUrlRevoke(url: string) { + window.setTimeout(() => URL.revokeObjectURL(url), OBJECT_URL_REVOKE_DELAY_MS) +} + +function formatDownloadError(error: unknown): string { + return error instanceof Error ? error.message : String(error || "Browser download failed") +} + +export function downloadUrl(url: string, filename: string, options: { revokeObjectUrl?: boolean } = {}): BrowserDownloadResult { + if (typeof document === "undefined") { + return { ok: false, error: "Downloads require a browser document." } + } + + const anchor = document.createElement("a") + anchor.href = url + anchor.download = filename + anchor.rel = "noopener" + anchor.style.display = "none" + + try { + document.body.appendChild(anchor) + anchor.click() + return { ok: true } + } catch (error) { + return { ok: false, error: formatDownloadError(error) } + } finally { + anchor.remove() + if (options.revokeObjectUrl) { + deferObjectUrlRevoke(url) + } + } +} + +export function downloadBlob(blob: Blob, filename: string): BrowserDownloadResult { + try { + const url = URL.createObjectURL(blob) + return downloadUrl(url, filename, { revokeObjectUrl: true }) + } catch (error) { + return { ok: false, error: formatDownloadError(error) } + } +} + +export function canvasToPngBlob(canvas: HTMLCanvasElement): Promise { + if (typeof canvas.toBlob === "function") { + return new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + if (blob) { + resolve(blob) + return + } + reject(new Error("Canvas did not produce a PNG blob.")) + }, "image/png") + }) + } + + return fetch(canvas.toDataURL("image/png")).then((response) => response.blob()) +} + +export async function downloadCanvasPng(canvas: HTMLCanvasElement, filename: string): Promise { + try { + const blob = await canvasToPngBlob(canvas) + return downloadBlob(blob, filename) + } catch (error) { + return { ok: false, error: formatDownloadError(error) } + } +} + +export function downloadDataUrl(dataUrl: string, filename: string): BrowserDownloadResult { + return downloadUrl(dataUrl, filename) } export async function buildQrSvg(options: { @@ -99,12 +172,6 @@ export async function buildQrSvg(options: { : svg } -export function downloadSvg(svg: string, filename: string) { - const blob = new Blob([svg], { type: "image/svg+xml" }) - const url = URL.createObjectURL(blob) - const a = document.createElement("a") - a.href = url - a.download = filename - a.click() - URL.revokeObjectURL(url) +export function downloadSvg(svg: string, filename: string): BrowserDownloadResult { + return downloadBlob(new Blob([svg], { type: "image/svg+xml" }), filename) } From ec8a27546a20071be39a583320e63d5c2a2f33c9 Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Thu, 2 Jul 2026 00:02:15 +0800 Subject: [PATCH 2/7] Make QR generator empty first by default --- src/features/tools/qr-code-generator/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/tools/qr-code-generator/constants.ts b/src/features/tools/qr-code-generator/constants.ts index 149c5e73..69a3aa92 100644 --- a/src/features/tools/qr-code-generator/constants.ts +++ b/src/features/tools/qr-code-generator/constants.ts @@ -13,7 +13,7 @@ export const BUTTON_SIZE_CLASS = { } as const export const MAX_LOGO_SIZE = 2 * 1024 * 1024 -export const DEFAULT_QR_TEXT = "https://example.com/r/42" +export const DEFAULT_QR_TEXT = "" export const SAMPLE_QR_TEXT = "https://example.com/qr?id=42" export const PRESETS: QrPreset[] = [ From 5d2f7e51fe450275181142445153306878a5f5eb Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Thu, 2 Jul 2026 00:03:54 +0800 Subject: [PATCH 3/7] Make QR input empty first and robust downloads --- src/features/tools/qr-code-generator/page.tsx | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/src/features/tools/qr-code-generator/page.tsx b/src/features/tools/qr-code-generator/page.tsx index 8048d8a3..e70b0c79 100644 --- a/src/features/tools/qr-code-generator/page.tsx +++ b/src/features/tools/qr-code-generator/page.tsx @@ -13,13 +13,13 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" -import { ToolActionBar, type ToolAction } from "@/features/tool-shell/tool-action-bar" +import { ToolActionBar, type ToolAction, type ToolActionResult } from "@/features/tool-shell/tool-action-bar" import { ToolPreviewArea } from "@/features/tool-shell/tool-preview-area" import { RelatedTools } from "@/core/seo/components/related-tools" import { safeClipboardWrite } from "@/core/clipboard/clipboard" import { buildQrSvg, - downloadDataUrl, + downloadCanvasPng, downloadSvg as downloadSvgFile, drawRoundedRect, loadImage, @@ -89,7 +89,10 @@ export function QrCodeGeneratorPage() { } const canvas = canvasRef.current - if (!canvas) return + if (!canvas) { + setDataUrl("") + return + } const qrCode = await loadQRCode() @@ -175,14 +178,24 @@ export function QrCodeGeneratorPage() { setLogoEnabled(false) } - const downloadPng = () => { - if (!dataUrl) return - downloadDataUrl(dataUrl, "qr-code.png") - void notifySuccess(textFor("downloaded_png")) + const downloadPng = async (): Promise => { + const canvas = canvasRef.current + if (!canvas || !text.trim()) { + return { status: "failed", message: textFor("download_error") } + } + + const result = await downloadCanvasPng(canvas, "qr-code.png") + if (!result.ok) { + await notifyError(textFor("download_error")) + return { status: "failed", message: textFor("download_error"), description: result.error } + } + + await notifySuccess(textFor("downloaded_png")) + return { status: "success", message: textFor("downloaded_png") } } - const downloadSvg = async () => { - if (!text.trim()) return + const downloadSvg = async (): Promise => { + if (!text.trim()) return { status: "failed", message: textFor("download_error") } try { const finalSvg = await buildQrSvg({ @@ -196,10 +209,16 @@ export function QrCodeGeneratorPage() { logoEnabled, logoScale, }) - downloadSvgFile(finalSvg, "qr-code.svg") + const result = downloadSvgFile(finalSvg, "qr-code.svg") + if (!result.ok) { + await notifyError(textFor("download_error")) + return { status: "failed", message: textFor("download_error"), description: result.error } + } await notifySuccess(textFor("downloaded_svg")) + return { status: "success", message: textFor("downloaded_svg") } } catch { await notifyError(textFor("download_error")) + return { status: "failed", message: textFor("download_error") } } } @@ -239,14 +258,14 @@ export function QrCodeGeneratorPage() { onClick: handleSample, }, { - id: "png", + id: "download_png", label: "PNG", icon: Download, onClick: downloadPng, disabled: !dataUrl, }, { - id: "svg", + id: "download_svg", label: "SVG", icon: Download, onClick: downloadSvg, @@ -296,9 +315,10 @@ export function QrCodeGeneratorPage() {