From 25e8f2615738ff026efc5f31ffd7df6adbe58a19 Mon Sep 17 00:00:00 2001 From: ccccpj Date: Fri, 7 Aug 2026 10:53:18 +0800 Subject: [PATCH] fix: render audio object previews natively --- components/object/preview-modal.tsx | 50 +++++++++++++++---------- lib/object-preview.ts | 33 ++++++++++++++++ tests/lib/object-preview-source.test.js | 40 ++++++++++++++++++++ 3 files changed, 103 insertions(+), 20 deletions(-) create mode 100644 lib/object-preview.ts diff --git a/components/object/preview-modal.tsx b/components/object/preview-modal.tsx index 6aab6eda..72698f57 100644 --- a/components/object/preview-modal.tsx +++ b/components/object/preview-modal.tsx @@ -10,6 +10,7 @@ import { RiFullscreenExitLine, RiFullscreenLine } from "@remixicon/react" import { PdfViewer } from "@/components/object/pdf-viewer" import { ParquetViewer } from "@/components/object/parquet-viewer" import { TiffViewer } from "@/components/object/tiff-viewer" +import { getObjectPreviewMode, normalizePreviewContentType } from "@/lib/object-preview" import Image from "next/image" const SAFE_TEXT_MIMES = [ @@ -27,8 +28,6 @@ const SAFE_TEXT_EXTENSIONS = [".txt", ".json", ".jsonl", ".ndjson", ".xml", ".cs const SAFE_IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".ico", ".tif", ".tiff"] const ALLOWED_SIZE = 1024 * 1024 * 2 // 2MB -type PreviewMode = "text" | "image" | "pdf" | "parquet" | "sandbox" | "download" | "tiff" - const PARQUET_MIMES = ["application/vnd.apache.parquet", "application/x-parquet", "application/parquet"] const PARQUET_EXTENSIONS = [".parquet", ".pq"] @@ -52,10 +51,6 @@ type FullscreenElement = HTMLElement & { webkitRequestFullscreen?: () => Promise | void } -function normalizeContentType(contentType: string) { - return contentType.split(";")[0]?.trim().toLowerCase() ?? "" -} - function isSafeTextPreview(contentType: string, objectKey: string, objectSize: number) { if (objectSize > ALLOWED_SIZE) return false if (SAFE_TEXT_MIMES.includes(contentType)) return true @@ -71,12 +66,6 @@ function isImagePreview(contentType: string, objectKey: string) { return SAFE_IMAGE_EXTENSIONS.some((ext) => keyLower.endsWith(ext)) } -function getPreviewMode(hasPreviewUrl: boolean, canRenderText: boolean, canRenderImage: boolean): PreviewMode { - if (!hasPreviewUrl) return "download" - if (canRenderImage) return "image" - return canRenderText ? "text" : "sandbox" -} - function isPdfPreview(contentType: string) { return contentType === "application/pdf" } @@ -124,6 +113,7 @@ export function ObjectPreviewModal({ show, onShowChange, object }: ObjectPreview const { t } = useTranslation() const [textContent, setTextContent] = React.useState("") const [loading, setLoading] = React.useState(false) + const [audioLoadError, setAudioLoadError] = React.useState(false) const [isFormatted, setIsFormatted] = React.useState(true) const [imageNaturalSize, setImageNaturalSize] = React.useState<{ width: number; height: number } | null>(null) const [imageFitScale, setImageFitScale] = React.useState(1) @@ -139,7 +129,7 @@ export function ObjectPreviewModal({ show, onShowChange, object }: ObjectPreview const objectSize = Number(object?.ContentLength ?? 0) const objectKey = object?.Key ?? "" const objectKeyLower = objectKey.toLowerCase() - const normalizedContentType = normalizeContentType(contentType) + const normalizedContentType = normalizePreviewContentType(contentType) const isJson = normalizedContentType === "application/json" || objectKeyLower.endsWith(".json") const canRenderText = hasPreviewUrl && isSafeTextPreview(normalizedContentType, objectKey, objectSize) @@ -147,13 +137,15 @@ export function ObjectPreviewModal({ show, onShowChange, object }: ObjectPreview const canRenderPdf = hasPreviewUrl && isPdfPreview(normalizedContentType) const canRenderParquet = hasPreviewUrl && isParquetPreview(normalizedContentType, objectKey) const canRenderTiff = hasPreviewUrl && isTiffPreview(objectKey) - const previewMode: PreviewMode = canRenderParquet - ? "parquet" - : canRenderPdf - ? "pdf" - : canRenderTiff - ? "tiff" - : getPreviewMode(hasPreviewUrl, canRenderText, canRenderImage) + const previewMode = getObjectPreviewMode({ + hasPreviewUrl, + contentType, + canRenderText, + canRenderImage, + canRenderPdf, + canRenderParquet, + canRenderTiff, + }) const isImageMode = previewMode === "image" const isSelfScrollMode = isImageMode || previewMode === "parquet" || previewMode === "tiff" @@ -193,6 +185,10 @@ export function ObjectPreviewModal({ show, onShowChange, object }: ObjectPreview } }, [show, previewMode, previewUrl, t]) + React.useEffect(() => { + setAudioLoadError(false) + }, [show, previewUrl]) + React.useEffect(() => { const cachedSize = previewUrl ? imageSizeCacheRef.current[previewUrl] : undefined setImageNaturalSize(cachedSize ?? null) @@ -375,6 +371,20 @@ export function ObjectPreviewModal({ show, onShowChange, object }: ObjectPreview ) + case "audio": + return audioLoadError ? ( +
+ {t("Preview unavailable")} +
+ ) : ( +