From 942113bc78976dde83dc4a8588d6e08e9eb65849 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Fri, 26 Jun 2026 20:56:39 +0800 Subject: [PATCH 01/22] refactor(asset): extract asset method Signed-off-by: MadDogOwner --- src/pages/home/previews/doc.tsx | 45 +++------------------------------ src/pages/home/previews/ppt.tsx | 45 +++------------------------------ src/pages/home/previews/xls.tsx | 42 ++++++------------------------ src/utils/asset.ts | 39 ++++++++++++++++++++++++++++ src/utils/index.ts | 1 + 5 files changed, 56 insertions(+), 116 deletions(-) create mode 100644 src/utils/asset.ts diff --git a/src/pages/home/previews/doc.tsx b/src/pages/home/previews/doc.tsx index 775ce7f0a..fee0ae6d1 100644 --- a/src/pages/home/previews/doc.tsx +++ b/src/pages/home/previews/doc.tsx @@ -1,8 +1,8 @@ import { BoxWithFullScreen, FullLoading, Error as Erro } from "~/components" -import { objStore } from "~/store" import { Box, IconButton, Tooltip } from "@hope-ui/solid" +import { loadScript } from "~/utils" import { createSignal, onMount, onCleanup, Show } from "solid-js" -import { useT } from "~/hooks" +import { useLink, useT } from "~/hooks" import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" // 声明全局docx类型 @@ -14,50 +14,13 @@ declare global { const DocViewerApp = () => { const t = useT() + const { currentObjLink } = useLink() const [loading, setLoading] = createSignal(true) const [error, setError] = createSignal(false) const [isFullscreen, setIsFullscreen] = createSignal(false) let containerRef: HTMLDivElement | undefined let resultRef: HTMLDivElement | undefined - // 加载外部脚本 - const loadScript = (src: string, id: string): Promise => { - return new Promise((resolve, reject) => { - // 检查脚本是否已加载 - if (document.getElementById(id)) { - resolve() - return - } - - const script = document.createElement("script") - script.src = src - script.id = id - script.type = "text/javascript" - script.onload = () => resolve() - script.onerror = () => reject(new Error(`Failed to load script: ${src}`)) - document.head.appendChild(script) - }) - } - - // 加载CSS文件 - const loadCSS = (href: string, id: string): Promise => { - return new Promise((resolve, reject) => { - // 检查CSS是否已加载 - if (document.getElementById(id)) { - resolve() - return - } - - const link = document.createElement("link") - link.rel = "stylesheet" - link.href = href - link.id = id - link.onload = () => resolve() - link.onerror = () => reject(new Error(`Failed to load CSS: ${href}`)) - document.head.appendChild(link) - }) - } - // 初始化DOCX预览 const initDocViewer = async () => { try { @@ -80,7 +43,7 @@ const DocViewerApp = () => { } // 获取文件URL并下载 - const fileUrl = objStore.raw_url + const fileUrl = currentObjLink() const response = await fetch(fileUrl) if (!response.ok) { throw new Error("Failed to fetch document file") diff --git a/src/pages/home/previews/ppt.tsx b/src/pages/home/previews/ppt.tsx index d0033803b..da6bd0983 100644 --- a/src/pages/home/previews/ppt.tsx +++ b/src/pages/home/previews/ppt.tsx @@ -1,8 +1,8 @@ import { BoxWithFullScreen, Error as Erro, FullLoading } from "~/components" -import { objStore } from "~/store" import { Box, IconButton, Tooltip } from "@hope-ui/solid" +import { loadScript, loadCSS } from "~/utils" import { createSignal, onMount, onCleanup, Show } from "solid-js" -import { useT } from "~/hooks" +import { useLink, useT } from "~/hooks" import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" // 声明全局jQuery和pptxToHtml方法 @@ -15,50 +15,13 @@ declare global { const PPTViewerApp = () => { const t = useT() + const { currentObjLink } = useLink() const [loading, setLoading] = createSignal(true) const [error, setError] = createSignal(false) const [isFullscreen, setIsFullscreen] = createSignal(false) let containerRef: HTMLDivElement | undefined let resultRef: HTMLDivElement | undefined - // 加载外部脚本 - const loadScript = (src: string, id: string): Promise => { - return new Promise((resolve, reject) => { - // 检查脚本是否已加载 - if (document.getElementById(id)) { - resolve() - return - } - - const script = document.createElement("script") - script.src = src - script.id = id - script.type = "text/javascript" - script.onload = () => resolve() - script.onerror = () => reject(new Error(`Failed to load script: ${src}`)) - document.head.appendChild(script) - }) - } - - // 加载CSS文件 - const loadCSS = (href: string, id: string): Promise => { - return new Promise((resolve, reject) => { - // 检查CSS是否已加载 - if (document.getElementById(id)) { - resolve() - return - } - - const link = document.createElement("link") - link.rel = "stylesheet" - link.href = href - link.id = id - link.onload = () => resolve() - link.onerror = () => reject(new Error(`Failed to load CSS: ${href}`)) - document.head.appendChild(link) - }) - } - // 初始化PPT预览 const initPPTViewer = async () => { try { @@ -94,7 +57,7 @@ const PPTViewerApp = () => { // 初始化pptxToHtml if (resultRef) { window.$(resultRef).pptxToHtml({ - pptxFileUrl: objStore.raw_url, + pptxFileUrl: currentObjLink(), slideMode: false, keyBoardShortCut: false, slideModeConfig: { diff --git a/src/pages/home/previews/xls.tsx b/src/pages/home/previews/xls.tsx index d8c4d44b1..48864e7f9 100644 --- a/src/pages/home/previews/xls.tsx +++ b/src/pages/home/previews/xls.tsx @@ -1,8 +1,8 @@ import { BoxWithFullScreen, Error as Erro, FullLoading } from "~/components" -import { objStore } from "~/store" import { Box, IconButton, Tooltip, Button, HStack } from "@hope-ui/solid" +import { loadScript } from "~/utils" import { createSignal, onMount, For, Show } from "solid-js" -import { useT } from "~/hooks" +import { useLink, useT } from "~/hooks" import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" // 声明全局ExcelJS类型 @@ -30,6 +30,7 @@ interface SheetData { const ExcelViewerApp = () => { const t = useT() + const { currentObjLink } = useLink() const [loading, setLoading] = createSignal(true) const [error, setError] = createSignal(false) const [isFullscreen, setIsFullscreen] = createSignal(false) @@ -37,36 +38,6 @@ const ExcelViewerApp = () => { const [currentSheetIndex, setCurrentSheetIndex] = createSignal(0) let containerRef: HTMLDivElement | undefined - // 动态加载ExcelJS库 - const loadExcelJSScript = () => { - return new Promise((resolve, reject) => { - // 检查是否已经加载 - if (window.ExcelJS) { - resolve() - return - } - - // 检查脚本标签是否已存在 - const existingScript = document.getElementById("exceljs-script") - if (existingScript) { - // 脚本正在加载中,等待加载完成 - existingScript.addEventListener("load", () => resolve()) - existingScript.addEventListener("error", () => - reject(new Error("Failed to load ExcelJS library")), - ) - return - } - - const script = document.createElement("script") - script.src = "https://res.oplist.org.cn/exceljs/exceljs.min.js" - script.id = "exceljs-script" - script.async = true - script.onload = () => resolve() - script.onerror = () => reject(new Error("Failed to load ExcelJS library")) - document.head.appendChild(script) - }) - } - // 加载并解析Excel文件 const loadExcelFile = async () => { try { @@ -74,10 +45,13 @@ const ExcelViewerApp = () => { setError(false) // 先加载ExcelJS库 - await loadExcelJSScript() + await loadScript( + "https://res.oplist.org.cn/exceljs/exceljs.min.js", + "exceljs-script", + ) // 获取文件URL - const fileUrl = objStore.raw_url + const fileUrl = currentObjLink() // 下载文件 const response = await fetch(fileUrl) diff --git a/src/utils/asset.ts b/src/utils/asset.ts new file mode 100644 index 000000000..e4df6e1cd --- /dev/null +++ b/src/utils/asset.ts @@ -0,0 +1,39 @@ +/** + * Load an external script dynamically + * @returns Promise that resolves when the script is loaded + */ +export const loadScript = (src: string, id: string): Promise => { + return new Promise((resolve, reject) => { + if (document.getElementById(id)) { + resolve() + return + } + const script = document.createElement("script") + script.src = src + script.id = id + script.type = "text/javascript" + script.onload = () => resolve() + script.onerror = () => reject(new Error(`Failed to load script: ${src}`)) + document.head.appendChild(script) + }) +} + +/** + * Load an external CSS file dynamically + * @returns Promise that resolves when the CSS is loaded + */ +export const loadCSS = (href: string, id: string): Promise => { + return new Promise((resolve, reject) => { + if (document.getElementById(id)) { + resolve() + return + } + const link = document.createElement("link") + link.rel = "stylesheet" + link.href = href + link.id = id + link.onload = () => resolve() + link.onerror = () => reject(new Error(`Failed to load CSS: ${href}`)) + document.head.appendChild(link) + }) +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 94e7f8f45..1e8364ed0 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -13,3 +13,4 @@ export * from "./hash" export * from "./compatibility" export * from "./share" export * from "./storage" +export * from "./asset" From 13cf0151b027182795a3538202299fc490540cd3 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Fri, 26 Jun 2026 21:29:20 +0800 Subject: [PATCH 02/22] fix: add missing i18n for previews Signed-off-by: MadDogOwner --- src/lang/en/home.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lang/en/home.json b/src/lang/en/home.json index 64a73c8a4..86d26c003 100644 --- a/src/lang/en/home.json +++ b/src/lang/en/home.json @@ -23,6 +23,8 @@ "tr-installing": "TrollStore Installing", "open_in_new_window": "Open in new window", "auto_next": "Auto next", + "fullscreen": "Fullscreen", + "exit_fullscreen": "Exit fullscreen", "names": { "download": "Direct Download", "html": "HTML Render", From a286ac3e105f02069e962b2596a432a783eeb5ca Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Fri, 26 Jun 2026 21:42:32 +0800 Subject: [PATCH 03/22] fix: setup responsive scale after init Signed-off-by: MadDogOwner --- src/pages/home/previews/doc.tsx | 26 +++++++- src/pages/home/previews/ppt.tsx | 110 +++++++++++++++++++++----------- 2 files changed, 98 insertions(+), 38 deletions(-) diff --git a/src/pages/home/previews/doc.tsx b/src/pages/home/previews/doc.tsx index fee0ae6d1..860f651c9 100644 --- a/src/pages/home/previews/doc.tsx +++ b/src/pages/home/previews/doc.tsx @@ -103,8 +103,31 @@ const DocViewerApp = () => { } } + // 响应式缩放docx内容以适配移动端 + const setupResponsiveScale = () => { + if (!resultRef || !containerRef) return + const result = resultRef + const container = containerRef + const observer = new ResizeObserver(() => { + const wrapper = result.querySelector( + ".docx-preview-container", + ) as HTMLElement + if (!wrapper) return + const containerWidth = container.clientWidth + const contentWidth = wrapper.scrollWidth + if (contentWidth > containerWidth) { + wrapper.style.zoom = `${containerWidth / contentWidth}` + } else { + wrapper.style.zoom = "" + } + }) + observer.observe(container) + onCleanup(() => observer.disconnect()) + } + onMount(() => { initDocViewer() + setupResponsiveScale() document.addEventListener("fullscreenchange", handleFullscreenChange) }) @@ -158,8 +181,7 @@ const DocViewerApp = () => { ref={resultRef} id="docx-container" style={{ - width: "100%", - height: "100%", + "min-width": "0", padding: "20px", display: loading() || error() ? "none" : "block", }} diff --git a/src/pages/home/previews/ppt.tsx b/src/pages/home/previews/ppt.tsx index da6bd0983..f63de0f08 100644 --- a/src/pages/home/previews/ppt.tsx +++ b/src/pages/home/previews/ppt.tsx @@ -20,8 +20,18 @@ const PPTViewerApp = () => { const [error, setError] = createSignal(false) const [isFullscreen, setIsFullscreen] = createSignal(false) let containerRef: HTMLDivElement | undefined + let shadowHostRef: HTMLDivElement | undefined let resultRef: HTMLDivElement | undefined + // 将已加载的CSS注入Shadow DOM + const injectStylesToShadow = (shadow: ShadowRoot) => { + document.querySelectorAll('link[id$="-css"]').forEach((el) => { + if (!shadow.getElementById(el.id)) { + shadow.appendChild(el.cloneNode(true)) + } + }) + } + // 初始化PPT预览 const initPPTViewer = async () => { try { @@ -38,7 +48,7 @@ const PPTViewerApp = () => { // 按顺序加载JS文件 await loadScript(`${baseUrl}/js/jquery-1.11.3.min.js`, "jquery-script") - // 使用JSZip 3.x版本,与docx预览器保持一致 + // 使用JSZip 2.x版本,不支持3.x await loadScript( "https://unpkg.com/jszip@2.6.1/dist/jszip.min.js", "jszip-script", @@ -54,7 +64,12 @@ const PPTViewerApp = () => { throw new Error("jQuery not loaded") } - // 初始化pptxToHtml + // 将CSS注入Shadow DOM + if (shadowHostRef?.shadowRoot) { + injectStylesToShadow(shadowHostRef.shadowRoot) + } + + // 初始化pptxToHtml(渲染到Shadow DOM内的resultRef) if (resultRef) { window.$(resultRef).pptxToHtml({ pptxFileUrl: currentObjLink(), @@ -98,7 +113,6 @@ const PPTViewerApp = () => { if (!document.fullscreenElement) { containerRef.requestFullscreen().then(() => { setIsFullscreen(true) - // 调整幻灯片大小 if (resultRef) { const slides = resultRef.querySelectorAll(".slide") slides.forEach((slide: any) => { @@ -110,7 +124,6 @@ const PPTViewerApp = () => { } else { document.exitFullscreen().then(() => { setIsFullscreen(false) - // 恢复幻灯片大小 if (resultRef) { const slides = resultRef.querySelectorAll(".slide") slides.forEach((slide: any) => { @@ -126,7 +139,6 @@ const PPTViewerApp = () => { const handleFullscreenChange = () => { if (!document.fullscreenElement) { setIsFullscreen(false) - // 恢复幻灯片大小 if (resultRef) { const slides = resultRef.querySelectorAll(".slide") slides.forEach((slide: any) => { @@ -137,46 +149,44 @@ const PPTViewerApp = () => { } } + // 响应式缩放ppt内容以适配移动端 + const setupResponsiveScale = () => { + if (!resultRef || !containerRef) return + const result = resultRef + const container = containerRef + const observer = new ResizeObserver(() => { + const containerWidth = container.clientWidth + const contentWidth = result.scrollWidth + if (contentWidth > containerWidth) { + result.style.zoom = `${containerWidth / contentWidth}` + } else { + result.style.zoom = "" + } + }) + observer.observe(container) + onCleanup(() => observer.disconnect()) + } + onMount(() => { + // 创建Shadow DOM,将PPT内容隔离在里面,防止pptxjs的高z-index影响外部 + if (shadowHostRef) { + const shadow = shadowHostRef.attachShadow({ mode: "open" }) + resultRef = document.createElement("div") + resultRef.id = "ppt-result" + resultRef.style.cssText = "width:100%;height:100%;" + shadow.appendChild(resultRef) + } initPPTViewer() + setupResponsiveScale() document.addEventListener("fullscreenchange", handleFullscreenChange) }) onCleanup(() => { document.removeEventListener("fullscreenchange", handleFullscreenChange) - // 清理加载的脚本和样式(可选) }) return ( - {/* 全屏按钮 */} - - - : } - onClick={toggleFullscreen} - /> - - - {/* PPT容器 */}
{ background: "#f5f5f5", }} > + {/* Shadow DOM宿主 - 隔离pptxjs生成的高z-index元素 */}
{
+ + {/* 全屏按钮 */} + + + : } + onClick={toggleFullscreen} + /> + + ) } From 8b9cef7546baaea43bb2ea9c83e8fe310397f219 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Fri, 26 Jun 2026 22:20:10 +0800 Subject: [PATCH 04/22] feat: add zoom control UI Signed-off-by: MadDogOwner --- src/lang/en/home.json | 2 + src/pages/home/previews/doc.tsx | 98 +++++++++++++++++++++++++++++---- src/pages/home/previews/ppt.tsx | 94 ++++++++++++++++++++++++++++--- 3 files changed, 174 insertions(+), 20 deletions(-) diff --git a/src/lang/en/home.json b/src/lang/en/home.json index 86d26c003..6af72105c 100644 --- a/src/lang/en/home.json +++ b/src/lang/en/home.json @@ -25,6 +25,8 @@ "auto_next": "Auto next", "fullscreen": "Fullscreen", "exit_fullscreen": "Exit fullscreen", + "auto_fit": "Auto fit", + "reset_zoom": "Reset zoom", "names": { "download": "Direct Download", "html": "HTML Render", diff --git a/src/pages/home/previews/doc.tsx b/src/pages/home/previews/doc.tsx index 860f651c9..7537eaa67 100644 --- a/src/pages/home/previews/doc.tsx +++ b/src/pages/home/previews/doc.tsx @@ -1,9 +1,13 @@ import { BoxWithFullScreen, FullLoading, Error as Erro } from "~/components" -import { Box, IconButton, Tooltip } from "@hope-ui/solid" +import { Box, Button, IconButton, Tooltip } from "@hope-ui/solid" import { loadScript } from "~/utils" import { createSignal, onMount, onCleanup, Show } from "solid-js" import { useLink, useT } from "~/hooks" import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" +import { + HiOutlineMagnifyingGlassPlus, + HiOutlineMagnifyingGlassMinus, +} from "solid-icons/hi" // 声明全局docx类型 declare global { @@ -18,6 +22,8 @@ const DocViewerApp = () => { const [loading, setLoading] = createSignal(true) const [error, setError] = createSignal(false) const [isFullscreen, setIsFullscreen] = createSignal(false) + // null = auto-fit, number = manual zoom level + const [zoom, setZoom] = createSignal(null) let containerRef: HTMLDivElement | undefined let resultRef: HTMLDivElement | undefined @@ -101,25 +107,54 @@ const DocViewerApp = () => { if (!document.fullscreenElement) { setIsFullscreen(false) } + applyScale() } - // 响应式缩放docx内容以适配移动端 - const setupResponsiveScale = () => { + // 应用缩放 + const applyScale = () => { if (!resultRef || !containerRef) return - const result = resultRef - const container = containerRef - const observer = new ResizeObserver(() => { - const wrapper = result.querySelector( - ".docx-preview-container", - ) as HTMLElement - if (!wrapper) return - const containerWidth = container.clientWidth + const wrapper = resultRef.querySelector( + ".docx-preview-container", + ) as HTMLElement + if (!wrapper) return + const z = zoom() + if (z === null) { + // auto-fit: 缩放到容器宽度 + const containerWidth = containerRef.clientWidth const contentWidth = wrapper.scrollWidth if (contentWidth > containerWidth) { wrapper.style.zoom = `${containerWidth / contentWidth}` } else { wrapper.style.zoom = "" } + } else { + wrapper.style.zoom = `${z}` + } + } + + // 缩放控制 + const zoomStep = 0.1 + const zoomIn = () => { + const current = zoom() ?? 1 + setZoom(Math.min(current + zoomStep, 3)) + applyScale() + } + const zoomOut = () => { + const current = zoom() ?? 1 + setZoom(Math.max(current - zoomStep, 0.3)) + applyScale() + } + const zoomReset = () => { + setZoom(null) + applyScale() + } + + const setupResponsiveScale = () => { + if (!resultRef || !containerRef) return + const result = resultRef + const container = containerRef + const observer = new ResizeObserver(() => { + if (zoom() === null) applyScale() }) observer.observe(container) onCleanup(() => observer.disconnect()) @@ -138,6 +173,47 @@ const DocViewerApp = () => { return ( + {/* 缩放控制 */} + + } + onClick={zoomOut} + /> + + + + } + onClick={zoomIn} + /> + + {/* 全屏按钮 */} { const [loading, setLoading] = createSignal(true) const [error, setError] = createSignal(false) const [isFullscreen, setIsFullscreen] = createSignal(false) + // null = auto-fit, number = manual zoom level + const [zoom, setZoom] = createSignal(null) let containerRef: HTMLDivElement | undefined let shadowHostRef: HTMLDivElement | undefined let resultRef: HTMLDivElement | undefined @@ -147,21 +153,50 @@ const PPTViewerApp = () => { }) } } + applyScale() + } + + // 应用缩放 + const applyScale = () => { + if (!resultRef || !containerRef) return + const z = zoom() + if (z === null) { + // auto-fit: 缩放到容器宽度 + const containerWidth = containerRef.clientWidth + const contentWidth = resultRef.scrollWidth + if (contentWidth > containerWidth) { + resultRef.style.zoom = `${containerWidth / contentWidth}` + } else { + resultRef.style.zoom = "" + } + } else { + resultRef.style.zoom = `${z}` + } + } + + // 缩放控制 + const zoomStep = 0.1 + const zoomIn = () => { + const current = zoom() ?? 1 + setZoom(Math.min(current + zoomStep, 3)) + applyScale() + } + const zoomOut = () => { + const current = zoom() ?? 1 + setZoom(Math.max(current - zoomStep, 0.3)) + applyScale() + } + const zoomReset = () => { + setZoom(null) + applyScale() } - // 响应式缩放ppt内容以适配移动端 const setupResponsiveScale = () => { if (!resultRef || !containerRef) return const result = resultRef const container = containerRef const observer = new ResizeObserver(() => { - const containerWidth = container.clientWidth - const contentWidth = result.scrollWidth - if (contentWidth > containerWidth) { - result.style.zoom = `${containerWidth / contentWidth}` - } else { - result.style.zoom = "" - } + if (zoom() === null) applyScale() }) observer.observe(container) onCleanup(() => observer.disconnect()) @@ -219,6 +254,47 @@ const PPTViewerApp = () => {
+ {/* 缩放控制 */} + + } + onClick={zoomOut} + /> + + + + } + onClick={zoomIn} + /> + + {/* 全屏按钮 */} Date: Fri, 26 Jun 2026 22:26:57 +0800 Subject: [PATCH 05/22] refactor: move load_external to home/previews Signed-off-by: MadDogOwner --- src/pages/home/previews/doc.tsx | 2 +- src/pages/home/previews/heic.tsx | 15 ++------------- .../home/previews/load_external.ts} | 0 src/pages/home/previews/ppt.tsx | 2 +- src/pages/home/previews/xls.tsx | 2 +- src/utils/index.ts | 1 - 6 files changed, 5 insertions(+), 17 deletions(-) rename src/{utils/asset.ts => pages/home/previews/load_external.ts} (100%) diff --git a/src/pages/home/previews/doc.tsx b/src/pages/home/previews/doc.tsx index 7537eaa67..5bff2caad 100644 --- a/src/pages/home/previews/doc.tsx +++ b/src/pages/home/previews/doc.tsx @@ -1,6 +1,6 @@ import { BoxWithFullScreen, FullLoading, Error as Erro } from "~/components" import { Box, Button, IconButton, Tooltip } from "@hope-ui/solid" -import { loadScript } from "~/utils" +import { loadScript } from "./load_external" import { createSignal, onMount, onCleanup, Show } from "solid-js" import { useLink, useT } from "~/hooks" import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" diff --git a/src/pages/home/previews/heic.tsx b/src/pages/home/previews/heic.tsx index 0f14cf7a1..244f907ef 100644 --- a/src/pages/home/previews/heic.tsx +++ b/src/pages/home/previews/heic.tsx @@ -1,11 +1,11 @@ import { Error, FullLoading } from "~/components" -import { useCDN, useRouter, useT } from "~/hooks" +import { useCDN, useT } from "~/hooks" +import { loadScript } from "./load_external" import { objStore } from "~/store" import { onCleanup, onMount, createSignal, Show } from "solid-js" const Preview = () => { const t = useT() - const { replace } = useRouter() const [loading, setLoading] = createSignal(true) const [error, setError] = createSignal(false) const { libHeifPath } = useCDN() @@ -74,17 +74,6 @@ const Preview = () => { } } - // 加载脚本 - const loadScript = (src: string, id: string) => - new Promise((resolve, reject) => { - const script = document.createElement("script") - script.src = src - script.id = id - script.onload = () => resolve() - script.onerror = () => reject(`脚本加载失败: ${src}`) - document.head.appendChild(script) - }) - // 获取WASM文件 const fetchWasm = async (url: string) => { const response = await fetch(url) diff --git a/src/utils/asset.ts b/src/pages/home/previews/load_external.ts similarity index 100% rename from src/utils/asset.ts rename to src/pages/home/previews/load_external.ts diff --git a/src/pages/home/previews/ppt.tsx b/src/pages/home/previews/ppt.tsx index 8d79e3e4a..3e13d181d 100644 --- a/src/pages/home/previews/ppt.tsx +++ b/src/pages/home/previews/ppt.tsx @@ -1,6 +1,6 @@ import { BoxWithFullScreen, Error as Erro, FullLoading } from "~/components" import { Box, Button, IconButton, Tooltip } from "@hope-ui/solid" -import { loadScript, loadCSS } from "~/utils" +import { loadScript, loadCSS } from "./load_external" import { createSignal, onMount, onCleanup, Show } from "solid-js" import { useLink, useT } from "~/hooks" import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" diff --git a/src/pages/home/previews/xls.tsx b/src/pages/home/previews/xls.tsx index 48864e7f9..7e0c99502 100644 --- a/src/pages/home/previews/xls.tsx +++ b/src/pages/home/previews/xls.tsx @@ -1,6 +1,6 @@ import { BoxWithFullScreen, Error as Erro, FullLoading } from "~/components" import { Box, IconButton, Tooltip, Button, HStack } from "@hope-ui/solid" -import { loadScript } from "~/utils" +import { loadScript } from "./load_external" import { createSignal, onMount, For, Show } from "solid-js" import { useLink, useT } from "~/hooks" import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" diff --git a/src/utils/index.ts b/src/utils/index.ts index 1e8364ed0..94e7f8f45 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -13,4 +13,3 @@ export * from "./hash" export * from "./compatibility" export * from "./share" export * from "./storage" -export * from "./asset" From 8938b8878c328c357f6c39e6c17130f419268176 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Fri, 26 Jun 2026 22:27:37 +0800 Subject: [PATCH 06/22] refactor: rename loadScript to loadScriptIIFE for understanding Signed-off-by: MadDogOwner --- src/pages/home/previews/doc.tsx | 6 +++--- src/pages/home/previews/heic.tsx | 4 ++-- src/pages/home/previews/load_external.ts | 2 +- src/pages/home/previews/ppt.tsx | 19 +++++++++++-------- src/pages/home/previews/xls.tsx | 4 ++-- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/pages/home/previews/doc.tsx b/src/pages/home/previews/doc.tsx index 5bff2caad..49eaf840c 100644 --- a/src/pages/home/previews/doc.tsx +++ b/src/pages/home/previews/doc.tsx @@ -1,6 +1,6 @@ import { BoxWithFullScreen, FullLoading, Error as Erro } from "~/components" import { Box, Button, IconButton, Tooltip } from "@hope-ui/solid" -import { loadScript } from "./load_external" +import { loadScriptIIFE } from "./load_external" import { createSignal, onMount, onCleanup, Show } from "solid-js" import { useLink, useT } from "~/hooks" import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" @@ -34,11 +34,11 @@ const DocViewerApp = () => { setError(false) // 加载jszip和docx-preview库 - await loadScript( + await loadScriptIIFE( "https://unpkg.com/jszip/dist/jszip.min.js", "jszip-script", ) - await loadScript( + await loadScriptIIFE( "https://res.oplist.org.cn/docxjs/dist/docx-preview.min.js", "docx-preview-script", ) diff --git a/src/pages/home/previews/heic.tsx b/src/pages/home/previews/heic.tsx index 244f907ef..672f299d0 100644 --- a/src/pages/home/previews/heic.tsx +++ b/src/pages/home/previews/heic.tsx @@ -1,6 +1,6 @@ import { Error, FullLoading } from "~/components" import { useCDN, useT } from "~/hooks" -import { loadScript } from "./load_external" +import { loadScriptIIFE } from "./load_external" import { objStore } from "~/store" import { onCleanup, onMount, createSignal, Show } from "solid-js" @@ -55,7 +55,7 @@ const Preview = () => { try { // 动态加载libheif脚本 if (!window.libheif) { - await loadScript(`${libHeifPath()}/libheif.js`, "libheif-script") + await loadScriptIIFE(`${libHeifPath()}/libheif.js`, "libheif-script") } // 加载WASM文件 diff --git a/src/pages/home/previews/load_external.ts b/src/pages/home/previews/load_external.ts index e4df6e1cd..9033cf1f5 100644 --- a/src/pages/home/previews/load_external.ts +++ b/src/pages/home/previews/load_external.ts @@ -2,7 +2,7 @@ * Load an external script dynamically * @returns Promise that resolves when the script is loaded */ -export const loadScript = (src: string, id: string): Promise => { +export const loadScriptIIFE = (src: string, id: string): Promise => { return new Promise((resolve, reject) => { if (document.getElementById(id)) { resolve() diff --git a/src/pages/home/previews/ppt.tsx b/src/pages/home/previews/ppt.tsx index 3e13d181d..93a6ed7ce 100644 --- a/src/pages/home/previews/ppt.tsx +++ b/src/pages/home/previews/ppt.tsx @@ -1,6 +1,6 @@ import { BoxWithFullScreen, Error as Erro, FullLoading } from "~/components" import { Box, Button, IconButton, Tooltip } from "@hope-ui/solid" -import { loadScript, loadCSS } from "./load_external" +import { loadScriptIIFE, loadCSS } from "./load_external" import { createSignal, onMount, onCleanup, Show } from "solid-js" import { useLink, useT } from "~/hooks" import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" @@ -53,17 +53,20 @@ const PPTViewerApp = () => { ]) // 按顺序加载JS文件 - await loadScript(`${baseUrl}/js/jquery-1.11.3.min.js`, "jquery-script") + await loadScriptIIFE( + `${baseUrl}/js/jquery-1.11.3.min.js`, + "jquery-script", + ) // 使用JSZip 2.x版本,不支持3.x - await loadScript( + await loadScriptIIFE( "https://unpkg.com/jszip@2.6.1/dist/jszip.min.js", "jszip-script", ) - await loadScript(`${baseUrl}/js/filereader.js`, "filereader-script") - await loadScript(`${baseUrl}/js/d3.min.js`, "d3-script") - await loadScript(`${baseUrl}/js/nv.d3.min.js`, "nv-d3-script") - await loadScript(`${baseUrl}/js/pptxjs.js`, "pptxjs-script") - await loadScript(`${baseUrl}/js/divs2slides.js`, "divs2slides-script") + await loadScriptIIFE(`${baseUrl}/js/filereader.js`, "filereader-script") + await loadScriptIIFE(`${baseUrl}/js/d3.min.js`, "d3-script") + await loadScriptIIFE(`${baseUrl}/js/nv.d3.min.js`, "nv-d3-script") + await loadScriptIIFE(`${baseUrl}/js/pptxjs.js`, "pptxjs-script") + await loadScriptIIFE(`${baseUrl}/js/divs2slides.js`, "divs2slides-script") // 等待jQuery加载完成 if (!window.$ || !window.jQuery) { diff --git a/src/pages/home/previews/xls.tsx b/src/pages/home/previews/xls.tsx index 7e0c99502..1d719c523 100644 --- a/src/pages/home/previews/xls.tsx +++ b/src/pages/home/previews/xls.tsx @@ -1,6 +1,6 @@ import { BoxWithFullScreen, Error as Erro, FullLoading } from "~/components" import { Box, IconButton, Tooltip, Button, HStack } from "@hope-ui/solid" -import { loadScript } from "./load_external" +import { loadScriptIIFE } from "./load_external" import { createSignal, onMount, For, Show } from "solid-js" import { useLink, useT } from "~/hooks" import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" @@ -45,7 +45,7 @@ const ExcelViewerApp = () => { setError(false) // 先加载ExcelJS库 - await loadScript( + await loadScriptIIFE( "https://res.oplist.org.cn/exceljs/exceljs.min.js", "exceljs-script", ) From 31893aea7fb14a377a066021a7ef2a2b64732b16 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Fri, 26 Jun 2026 22:38:09 +0800 Subject: [PATCH 07/22] refactor: move load_external to utils Signed-off-by: MadDogOwner --- src/pages/home/previews/doc.tsx | 2 +- src/pages/home/previews/heic.tsx | 2 +- src/pages/home/previews/ppt.tsx | 2 +- src/pages/home/previews/xls.tsx | 2 +- src/utils/index.ts | 1 + src/{pages/home/previews => utils}/load_external.ts | 0 6 files changed, 5 insertions(+), 4 deletions(-) rename src/{pages/home/previews => utils}/load_external.ts (100%) diff --git a/src/pages/home/previews/doc.tsx b/src/pages/home/previews/doc.tsx index 49eaf840c..fbd1d0bff 100644 --- a/src/pages/home/previews/doc.tsx +++ b/src/pages/home/previews/doc.tsx @@ -1,6 +1,6 @@ import { BoxWithFullScreen, FullLoading, Error as Erro } from "~/components" import { Box, Button, IconButton, Tooltip } from "@hope-ui/solid" -import { loadScriptIIFE } from "./load_external" +import { loadScriptIIFE } from "~/utils" import { createSignal, onMount, onCleanup, Show } from "solid-js" import { useLink, useT } from "~/hooks" import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" diff --git a/src/pages/home/previews/heic.tsx b/src/pages/home/previews/heic.tsx index 672f299d0..fa3720857 100644 --- a/src/pages/home/previews/heic.tsx +++ b/src/pages/home/previews/heic.tsx @@ -1,6 +1,6 @@ import { Error, FullLoading } from "~/components" import { useCDN, useT } from "~/hooks" -import { loadScriptIIFE } from "./load_external" +import { loadScriptIIFE } from "~/utils" import { objStore } from "~/store" import { onCleanup, onMount, createSignal, Show } from "solid-js" diff --git a/src/pages/home/previews/ppt.tsx b/src/pages/home/previews/ppt.tsx index 93a6ed7ce..20536f2f1 100644 --- a/src/pages/home/previews/ppt.tsx +++ b/src/pages/home/previews/ppt.tsx @@ -1,6 +1,6 @@ import { BoxWithFullScreen, Error as Erro, FullLoading } from "~/components" import { Box, Button, IconButton, Tooltip } from "@hope-ui/solid" -import { loadScriptIIFE, loadCSS } from "./load_external" +import { loadScriptIIFE, loadCSS } from "~/utils" import { createSignal, onMount, onCleanup, Show } from "solid-js" import { useLink, useT } from "~/hooks" import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" diff --git a/src/pages/home/previews/xls.tsx b/src/pages/home/previews/xls.tsx index 1d719c523..fe72d5f6a 100644 --- a/src/pages/home/previews/xls.tsx +++ b/src/pages/home/previews/xls.tsx @@ -1,6 +1,6 @@ import { BoxWithFullScreen, Error as Erro, FullLoading } from "~/components" import { Box, IconButton, Tooltip, Button, HStack } from "@hope-ui/solid" -import { loadScriptIIFE } from "./load_external" +import { loadScriptIIFE } from "~/utils" import { createSignal, onMount, For, Show } from "solid-js" import { useLink, useT } from "~/hooks" import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" diff --git a/src/utils/index.ts b/src/utils/index.ts index 94e7f8f45..ca8dcbf12 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -13,3 +13,4 @@ export * from "./hash" export * from "./compatibility" export * from "./share" export * from "./storage" +export * from "./load_external" diff --git a/src/pages/home/previews/load_external.ts b/src/utils/load_external.ts similarity index 100% rename from src/pages/home/previews/load_external.ts rename to src/utils/load_external.ts From b910f99ba3b054f3d4c53c1e2e75873f575b8e4c Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Fri, 26 Jun 2026 22:43:37 +0800 Subject: [PATCH 08/22] refactor: loading of KaTeX and Mermaid scripts to use utility functions in markdown Signed-off-by: MadDogOwner --- src/components/Markdown.tsx | 42 ++++++++++++++----------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/src/components/Markdown.tsx b/src/components/Markdown.tsx index e1c66c47f..803842ad1 100644 --- a/src/components/Markdown.tsx +++ b/src/components/Markdown.tsx @@ -1,7 +1,6 @@ import { Anchor, Box, List, ListItem, useColorModeValue } from "@hope-ui/solid" import { createStorageSignal } from "@solid-primitives/storage" import { clsx } from "clsx" -import once from "just-once" import rehypeRaw from "rehype-raw" import rehypeSanitize, { defaultSchema } from "rehype-sanitize" import rehypeStringify from "rehype-stringify" @@ -14,7 +13,15 @@ import { unified } from "unified" import { useCDN, useParseText, useRouter } from "~/hooks" import { useScrollListener } from "~/pages/home/toolbar/BackTop.jsx" import { getMainColor, getSettingBool, me } from "~/store" -import { api, notify, pathDir, pathJoin, pathResolve } from "~/utils" +import { + api, + loadCSS, + loadScriptIIFE, + notify, + pathDir, + pathJoin, + pathResolve, +} from "~/utils" import { isMobile } from "~/utils/compatibility.js" import hljs from "highlight.js" import { EncodingSelect } from "." @@ -148,25 +155,6 @@ function MarkdownToc(props: { const { katexCSSPath, mermaidJSPath } = useCDN() -const insertKatexCSS = once(() => { - const link = document.createElement("link") - link.rel = "stylesheet" - link.href = katexCSSPath() - document.head.appendChild(link) -}) - -const loadMermaidJS = once( - () => - new Promise((resolve, reject) => { - if (window.mermaid) return resolve() - const script = document.createElement("script") - script.src = mermaidJSPath() - script.onload = () => resolve() - script.onerror = () => reject(new Error("Failed to load mermaid")) - document.body.appendChild(script) - }), -) - async function renderMarkdown( content: string, sanitize: boolean, @@ -180,15 +168,17 @@ async function renderMarkdown( if (hasMath) { const { default: remarkMath } = await import("remark-math") processor.use(remarkMath) - insertKatexCSS() - } - if (hasMermaid) { - await loadMermaidJS().catch(() => + await loadCSS(katexCSSPath(), "katex").catch(() => notify.error( - "Failed to load mermaid.js, mermaid diagrams will not be rendered", + "Failed to load KaTeX CSS, math formulas will not be rendered", ), ) } + if (hasMermaid) { + await loadScriptIIFE(mermaidJSPath(), "mermaid").catch(() => + notify.error("Failed to load Mermaid JS, diagrams will not be rendered"), + ) + } processor.use(remarkRehype, { allowDangerousHtml: true }).use(rehypeRaw) From 4dc42a6f28537263323aa7c2d5216e724332808d Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Fri, 26 Jun 2026 22:45:09 +0800 Subject: [PATCH 09/22] chore: remove unused dependency `just-once` Signed-off-by: MadDogOwner --- package.json | 1 - pnpm-lock.yaml | 12 ++---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 5116e8002..f3befb288 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,6 @@ "highlight.js": "^11.11.1", "hls.js": "^1.6.16", "ini": "^7.0.0", - "just-once": "^2.2.0", "katex": "^0.16.45", "libheif-js": "^1.19.8", "lightgallery": "^2.9.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6fca3a96..ad0444211 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,9 +95,6 @@ importers: ini: specifier: ^7.0.0 version: 7.0.0 - just-once: - specifier: ^2.2.0 - version: 2.2.0 katex: specifier: ^0.16.45 version: 0.16.45 @@ -2589,9 +2586,6 @@ packages: jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} - just-once@2.2.0: - resolution: {integrity: sha512-Wo547FgUOUZ98jbrZ1KX8nRezdEdtgIlC2NK1u1RvR1oZ/WoU++FjprP8J8hRbaox776MHyeMZZED4DvhhHVjg==} - katex@0.16.45: resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==} hasBin: true @@ -3697,9 +3691,9 @@ snapshots: '@babel/helper-compilation-targets@7.28.6': dependencies: - '@babel/compat-data': 7.29.0 + '@babel/compat-data': 7.29.3 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.1 + browserslist: 4.28.2 lru-cache: 5.1.1 semver: 6.3.1 @@ -6260,8 +6254,6 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 - just-once@2.2.0: {} - katex@0.16.45: dependencies: commander: 8.3.0 From 999fe5a3721f75066a567eda16026524509415da Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Fri, 26 Jun 2026 23:16:09 +0800 Subject: [PATCH 10/22] refactor: unify ppt doc xls assets management to useCDN hook Signed-off-by: MadDogOwner --- src/hooks/useCDN.ts | 19 ++++++++++++++++++- src/pages/home/previews/doc.tsx | 10 ++++------ src/pages/home/previews/ppt.tsx | 7 ++++--- src/pages/home/previews/xls.tsx | 8 +++----- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/hooks/useCDN.ts b/src/hooks/useCDN.ts index 46148cf14..65f9b9b2a 100644 --- a/src/hooks/useCDN.ts +++ b/src/hooks/useCDN.ts @@ -4,8 +4,12 @@ import packageJson from "../../package.json" export const useCDN = () => { const static_path = joinBase("static") + // OpenList Resource CDN: https://github.com/OpenListTeam/OpenList-Resource + const resource = "https://res.oplist.org.cn" + + // npmmirror CDN, whitelist + // Available: https://github.com/cnpm/unpkg-white-list const npm = (name: string, version: string, path: string) => { - // Available: https://github.com/cnpm/unpkg-white-list // https://registry.npmmirror.com/monaco-editor/0.55.1/files/min/vs/loader.js return `https://registry.npmmirror.com/${name}/${version}/files/${path}` @@ -13,6 +17,10 @@ export const useCDN = () => { // return `https://cdn.jsdelivr.net/npm/${name}@${version}/${path}` } + const res = (path: string) => { + return `${resource}/${path}` + } + const monacoPath = () => { return import.meta.env.VITE_LITE === "true" ? npm("monaco-editor", "0.55.1", "min/vs") @@ -49,13 +57,22 @@ export const useCDN = () => { : `${static_path}/fonts` } + // Office preview libs — always served from resource CDN (not bundled locally) + const pptBasePath = () => res("ppt.js") + const docxPreviewPath = () => res("docxjs/dist/docx-preview.min.js") + const excelJSPath = () => res("exceljs/exceljs.min.js") + return { npm, + res, monacoPath, katexCSSPath, mermaidJSPath, libHeifPath, libAssPath, fontsPath, + pptBasePath, + docxPreviewPath, + excelJSPath, } } diff --git a/src/pages/home/previews/doc.tsx b/src/pages/home/previews/doc.tsx index fbd1d0bff..d779b618f 100644 --- a/src/pages/home/previews/doc.tsx +++ b/src/pages/home/previews/doc.tsx @@ -2,7 +2,7 @@ import { BoxWithFullScreen, FullLoading, Error as Erro } from "~/components" import { Box, Button, IconButton, Tooltip } from "@hope-ui/solid" import { loadScriptIIFE } from "~/utils" import { createSignal, onMount, onCleanup, Show } from "solid-js" -import { useLink, useT } from "~/hooks" +import { useLink, useT, useCDN } from "~/hooks" import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" import { HiOutlineMagnifyingGlassPlus, @@ -19,6 +19,7 @@ declare global { const DocViewerApp = () => { const t = useT() const { currentObjLink } = useLink() + const { npm, docxPreviewPath } = useCDN() const [loading, setLoading] = createSignal(true) const [error, setError] = createSignal(false) const [isFullscreen, setIsFullscreen] = createSignal(false) @@ -35,13 +36,10 @@ const DocViewerApp = () => { // 加载jszip和docx-preview库 await loadScriptIIFE( - "https://unpkg.com/jszip/dist/jszip.min.js", + npm("jszip", "3.10.1", "dist/jszip.min.js"), "jszip-script", ) - await loadScriptIIFE( - "https://res.oplist.org.cn/docxjs/dist/docx-preview.min.js", - "docx-preview-script", - ) + await loadScriptIIFE(docxPreviewPath(), "docx-preview-script") // 等待docx库加载完成 if (!window.docx) { diff --git a/src/pages/home/previews/ppt.tsx b/src/pages/home/previews/ppt.tsx index 20536f2f1..85de80597 100644 --- a/src/pages/home/previews/ppt.tsx +++ b/src/pages/home/previews/ppt.tsx @@ -2,7 +2,7 @@ import { BoxWithFullScreen, Error as Erro, FullLoading } from "~/components" import { Box, Button, IconButton, Tooltip } from "@hope-ui/solid" import { loadScriptIIFE, loadCSS } from "~/utils" import { createSignal, onMount, onCleanup, Show } from "solid-js" -import { useLink, useT } from "~/hooks" +import { useLink, useT, useCDN } from "~/hooks" import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" import { HiOutlineMagnifyingGlassPlus, @@ -20,6 +20,7 @@ declare global { const PPTViewerApp = () => { const t = useT() const { currentObjLink } = useLink() + const { npm, pptBasePath } = useCDN() const [loading, setLoading] = createSignal(true) const [error, setError] = createSignal(false) const [isFullscreen, setIsFullscreen] = createSignal(false) @@ -44,7 +45,7 @@ const PPTViewerApp = () => { setLoading(true) setError(false) - const baseUrl = "https://res.oplist.org.cn/ppt.js" + const baseUrl = pptBasePath() // 加载CSS文件 await Promise.all([ @@ -59,7 +60,7 @@ const PPTViewerApp = () => { ) // 使用JSZip 2.x版本,不支持3.x await loadScriptIIFE( - "https://unpkg.com/jszip@2.6.1/dist/jszip.min.js", + npm("jszip", "2.6.1", "dist/jszip.min.js"), "jszip-script", ) await loadScriptIIFE(`${baseUrl}/js/filereader.js`, "filereader-script") diff --git a/src/pages/home/previews/xls.tsx b/src/pages/home/previews/xls.tsx index fe72d5f6a..3c8552fd8 100644 --- a/src/pages/home/previews/xls.tsx +++ b/src/pages/home/previews/xls.tsx @@ -2,7 +2,7 @@ import { BoxWithFullScreen, Error as Erro, FullLoading } from "~/components" import { Box, IconButton, Tooltip, Button, HStack } from "@hope-ui/solid" import { loadScriptIIFE } from "~/utils" import { createSignal, onMount, For, Show } from "solid-js" -import { useLink, useT } from "~/hooks" +import { useLink, useT, useCDN } from "~/hooks" import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" // 声明全局ExcelJS类型 @@ -31,6 +31,7 @@ interface SheetData { const ExcelViewerApp = () => { const t = useT() const { currentObjLink } = useLink() + const { excelJSPath } = useCDN() const [loading, setLoading] = createSignal(true) const [error, setError] = createSignal(false) const [isFullscreen, setIsFullscreen] = createSignal(false) @@ -45,10 +46,7 @@ const ExcelViewerApp = () => { setError(false) // 先加载ExcelJS库 - await loadScriptIIFE( - "https://res.oplist.org.cn/exceljs/exceljs.min.js", - "exceljs-script", - ) + await loadScriptIIFE(excelJSPath(), "exceljs-script") // 获取文件URL const fileUrl = currentObjLink() From 284f621cb1ad8deb4961bf2f55b8697614351ac7 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Fri, 26 Jun 2026 23:39:57 +0800 Subject: [PATCH 11/22] feat: sync npmmirror version with package.json Signed-off-by: MadDogOwner --- src/hooks/useCDN.ts | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/hooks/useCDN.ts b/src/hooks/useCDN.ts index 65f9b9b2a..c659ab7a2 100644 --- a/src/hooks/useCDN.ts +++ b/src/hooks/useCDN.ts @@ -1,5 +1,9 @@ import { joinBase } from "~/utils" -import packageJson from "../../package.json" +import { + name as pkgName, + version as pkgVersion, + dependencies as pkgDeps, +} from "../../package.json" export const useCDN = () => { const static_path = joinBase("static") @@ -17,43 +21,53 @@ export const useCDN = () => { // return `https://cdn.jsdelivr.net/npm/${name}@${version}/${path}` } + // Read version from package.json dependencies (strips ^ ~ prefixes) + const dep = (name: string) => { + const ver = (pkgDeps as Record)[name] + if (!ver) + throw new Error( + `[useCDN] "${name}" not found in package.json dependencies`, + ) + return ver.replace(/^[^\d]*/, "") + } + const res = (path: string) => { return `${resource}/${path}` } const monacoPath = () => { return import.meta.env.VITE_LITE === "true" - ? npm("monaco-editor", "0.55.1", "min/vs") + ? npm("monaco-editor", dep("monaco-editor"), "min/vs") : `${static_path}/monaco-editor/vs` } const katexCSSPath = () => { return import.meta.env.VITE_LITE === "true" - ? npm("katex", "0.16.45", "dist/katex.min.css") + ? npm("katex", dep("katex"), "dist/katex.min.css") : `${static_path}/katex/katex.min.css` } const mermaidJSPath = () => { return import.meta.env.VITE_LITE === "true" - ? npm("mermaid", "11.15.0", "dist/mermaid.min.js") + ? npm("mermaid", dep("mermaid"), "dist/mermaid.min.js") : `${static_path}/mermaid/mermaid.min.js` } const libHeifPath = () => { return import.meta.env.VITE_LITE === "true" - ? npm(packageJson.name, packageJson.version, "dist/static/libheif") + ? npm(pkgName, pkgVersion, "dist/static/libheif") : `${static_path}/libheif` } const libAssPath = () => { return import.meta.env.VITE_LITE === "true" - ? npm(packageJson.name, packageJson.version, "dist/static/libass-wasm") + ? npm(pkgName, pkgVersion, "dist/static/libass-wasm") : `${static_path}/libass-wasm` } const fontsPath = () => { return import.meta.env.VITE_LITE === "true" - ? npm(packageJson.name, packageJson.version, "dist/static/fonts") + ? npm(pkgName, pkgVersion, "dist/static/fonts") : `${static_path}/fonts` } From 6ff24abaf8a295cc48dcff3201d140bf0cf44202 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Sat, 27 Jun 2026 01:07:48 +0800 Subject: [PATCH 12/22] fix: remove unsupported exts Signed-off-by: MadDogOwner --- src/pages/home/previews/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/previews/index.ts b/src/pages/home/previews/index.ts index 8fef26f15..00f374f75 100644 --- a/src/pages/home/previews/index.ts +++ b/src/pages/home/previews/index.ts @@ -143,13 +143,13 @@ const previews: Preview[] = [ }, { key: "xls", - exts: ["xlsx", "xls"], + exts: ["xlsx"], component: lazy(() => import("./xls")), prior: true, }, { key: "doc", - exts: ["docx", "doc"], + exts: ["docx"], component: lazy(() => import("./doc")), prior: true, }, From fbcde78a6a31386e86b8f1f99bd39aed1e4127bb Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Sat, 27 Jun 2026 02:10:40 +0800 Subject: [PATCH 13/22] feat: add fullscreen toggle in BoxWithFullScreen Signed-off-by: MadDogOwner --- src/components/Base.tsx | 103 ++++++++++++++--- src/hooks/useCDN.ts | 2 + src/lang/en/home.json | 3 + src/pages/home/previews/doc.tsx | 60 +--------- src/pages/home/previews/flash.tsx | 85 ++++++-------- src/pages/home/previews/pdf.tsx | 7 +- src/pages/home/previews/ppt.tsx | 79 ------------- src/pages/home/previews/xls.tsx | 181 ++++++++++++------------------ 8 files changed, 211 insertions(+), 309 deletions(-) diff --git a/src/components/Base.tsx b/src/components/Base.tsx index b065f693d..72271371f 100644 --- a/src/components/Base.tsx +++ b/src/components/Base.tsx @@ -14,12 +14,25 @@ import { SelectOptionText, SelectTrigger, SelectValue, - Icon, + IconButton, + Tooltip, + VStack, } from "@hope-ui/solid" import { SwitchColorMode } from "./SwitchColorMode" -import { ComponentProps, For, mergeProps, Show, JSXElement } from "solid-js" +import { + ComponentProps, + For, + mergeProps, + Show, + JSXElement, + createSignal, + onMount, + onCleanup, +} from "solid-js" import { AiOutlineFullscreen, AiOutlineFullscreenExit } from "solid-icons/ai" -import { hoverColor } from "~/utils" +import { BsFullscreen, BsFullscreenExit } from "solid-icons/bs" +import { useT } from "~/hooks" +import { notify } from "~/utils" export const Error = (props: { msg: string @@ -65,8 +78,27 @@ export const Error = (props: { export const BoxWithFullScreen = (props: Parameters[0]) => { const { isOpen, onToggle } = createDisclosure() + const [isNativeFullscreen, setIsNativeFullscreen] = createSignal(false) + let containerRef: HTMLDivElement + const t = useT() + + const toggleFullscreen = () => { + if (!document.fullscreenElement) { + containerRef!.requestFullscreen() + } else { + document.exitFullscreen() + } + } + + onMount(() => { + const handler = () => setIsNativeFullscreen(!!document.fullscreenElement) + document.addEventListener("fullscreenchange", handler) + onCleanup(() => document.removeEventListener("fullscreenchange", handler)) + }) + return ( [0]) => { }} > {props.children} - + spacing="$2" + opacity="0.7" + _hover={{ opacity: "1" }} + > + {/* Full view toggle */} + + : } + onClick={onToggle} + colorScheme="neutral" + size="sm" + /> + + + {/* Native fullscreen toggle */} + + + ) : ( + + ) + } + onClick={toggleFullscreen} + colorScheme="neutral" + size="sm" + /> + + ) } diff --git a/src/hooks/useCDN.ts b/src/hooks/useCDN.ts index c659ab7a2..05138b880 100644 --- a/src/hooks/useCDN.ts +++ b/src/hooks/useCDN.ts @@ -75,6 +75,7 @@ export const useCDN = () => { const pptBasePath = () => res("ppt.js") const docxPreviewPath = () => res("docxjs/dist/docx-preview.min.js") const excelJSPath = () => res("exceljs/exceljs.min.js") + const rufflePath = () => res("ruffle/ruffle.js") return { npm, @@ -88,5 +89,6 @@ export const useCDN = () => { pptBasePath, docxPreviewPath, excelJSPath, + rufflePath, } } diff --git a/src/lang/en/home.json b/src/lang/en/home.json index 6af72105c..b6dff495f 100644 --- a/src/lang/en/home.json +++ b/src/lang/en/home.json @@ -24,7 +24,10 @@ "open_in_new_window": "Open in new window", "auto_next": "Auto next", "fullscreen": "Fullscreen", + "fullscreen_failed": "Failed to toggle fullscreen mode", "exit_fullscreen": "Exit fullscreen", + "fullview": "Full view", + "exit_fullview": "Exit Full view", "auto_fit": "Auto fit", "reset_zoom": "Reset zoom", "names": { diff --git a/src/pages/home/previews/doc.tsx b/src/pages/home/previews/doc.tsx index d779b618f..291411500 100644 --- a/src/pages/home/previews/doc.tsx +++ b/src/pages/home/previews/doc.tsx @@ -3,7 +3,7 @@ import { Box, Button, IconButton, Tooltip } from "@hope-ui/solid" import { loadScriptIIFE } from "~/utils" import { createSignal, onMount, onCleanup, Show } from "solid-js" import { useLink, useT, useCDN } from "~/hooks" -import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" + import { HiOutlineMagnifyingGlassPlus, HiOutlineMagnifyingGlassMinus, @@ -22,7 +22,6 @@ const DocViewerApp = () => { const { npm, docxPreviewPath } = useCDN() const [loading, setLoading] = createSignal(true) const [error, setError] = createSignal(false) - const [isFullscreen, setIsFullscreen] = createSignal(false) // null = auto-fit, number = manual zoom level const [zoom, setZoom] = createSignal(null) let containerRef: HTMLDivElement | undefined @@ -85,29 +84,6 @@ const DocViewerApp = () => { } } - // 全屏切换 - const toggleFullscreen = () => { - if (!containerRef) return - - if (!document.fullscreenElement) { - containerRef.requestFullscreen().then(() => { - setIsFullscreen(true) - }) - } else { - document.exitFullscreen().then(() => { - setIsFullscreen(false) - }) - } - } - - // 监听全屏变化 - const handleFullscreenChange = () => { - if (!document.fullscreenElement) { - setIsFullscreen(false) - } - applyScale() - } - // 应用缩放 const applyScale = () => { if (!resultRef || !containerRef) return @@ -161,12 +137,6 @@ const DocViewerApp = () => { onMount(() => { initDocViewer() setupResponsiveScale() - document.addEventListener("fullscreenchange", handleFullscreenChange) - }) - - onCleanup(() => { - document.removeEventListener("fullscreenchange", handleFullscreenChange) - // 清理加载的脚本和样式(可选) }) return ( @@ -212,34 +182,6 @@ const DocViewerApp = () => { /> - {/* 全屏按钮 */} - - - : } - onClick={toggleFullscreen} - /> - - - {/* DOCX容器 */}
{ @@ -8,7 +9,7 @@ const Preview = () => { const { replace } = useRouter() const [loading, setLoading] = createSignal(true) const [error, setError] = createSignal(false) - + const { rufflePath } = useCDN() // 获取当前目录下所有SWF文件 let swfFiles = objStore.objs.filter((obj) => obj.name.toLowerCase().endsWith(".swf"), @@ -41,7 +42,7 @@ const Preview = () => { ruffleScript?.remove() }) - const initRufflePlayer = () => { + const initRufflePlayer = async () => { setLoading(true) setError(false) @@ -49,30 +50,16 @@ const Preview = () => { const oldPlayer = document.getElementById("ruffle-player") oldPlayer?.remove() - // 检查是否已加载Ruffle - if (window.RufflePlayer) { - createPlayer() - return - } - // 动态加载Ruffle脚本 - const script = document.createElement("script") - // script.src = "https://unpkg.com/@ruffle-rs/ruffle" - script.src = "https://res.oplist.org.cn/ruffle/ruffle.js" - script.async = true - script.id = "ruffle-script" - - script.onload = () => { - createPlayer() - } - - script.onerror = () => { - setError(true) - setLoading(false) - console.error("无法加载Ruffle播放器") - } - - document.head.appendChild(script) + await loadScriptIIFE(rufflePath(), "ruffle-script") + .then(() => { + createPlayer() + }) + .catch((err) => { + console.error("Failed to load Ruffle script:", err) + setError(true) + setLoading(false) + }) } const createPlayer = () => { @@ -107,27 +94,29 @@ const Preview = () => { } return ( -
- {/* 加载状态 */} - - - - - {/* 错误状态 */} - - - -
+ +
+ {/* 加载状态 */} + + + + + {/* 错误状态 */} + + + +
+
) } diff --git a/src/pages/home/previews/pdf.tsx b/src/pages/home/previews/pdf.tsx index df88bd59d..b67aa9316 100644 --- a/src/pages/home/previews/pdf.tsx +++ b/src/pages/home/previews/pdf.tsx @@ -3,6 +3,7 @@ import pdfiumWasmUrl from "@embedpdf/snippet/dist/pdfium.wasm?url" import { Box, useColorMode } from "@hope-ui/solid" import { onMount } from "solid-js" import { currentLang } from "~/app/i18n" +import { BoxWithFullScreen } from "~/components" import { objStore } from "~/store" import { base_path } from "~/utils" @@ -30,7 +31,11 @@ const PDFViewer = () => { }) } }) - return (ref = el)} /> + return ( + + + + ) } export default PDFViewer diff --git a/src/pages/home/previews/ppt.tsx b/src/pages/home/previews/ppt.tsx index 85de80597..3364fce93 100644 --- a/src/pages/home/previews/ppt.tsx +++ b/src/pages/home/previews/ppt.tsx @@ -3,7 +3,6 @@ import { Box, Button, IconButton, Tooltip } from "@hope-ui/solid" import { loadScriptIIFE, loadCSS } from "~/utils" import { createSignal, onMount, onCleanup, Show } from "solid-js" import { useLink, useT, useCDN } from "~/hooks" -import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" import { HiOutlineMagnifyingGlassPlus, HiOutlineMagnifyingGlassMinus, @@ -23,7 +22,6 @@ const PPTViewerApp = () => { const { npm, pptBasePath } = useCDN() const [loading, setLoading] = createSignal(true) const [error, setError] = createSignal(false) - const [isFullscreen, setIsFullscreen] = createSignal(false) // null = auto-fit, number = manual zoom level const [zoom, setZoom] = createSignal(null) let containerRef: HTMLDivElement | undefined @@ -116,50 +114,6 @@ const PPTViewerApp = () => { } } - // 全屏切换 - const toggleFullscreen = () => { - if (!containerRef) return - - if (!document.fullscreenElement) { - containerRef.requestFullscreen().then(() => { - setIsFullscreen(true) - if (resultRef) { - const slides = resultRef.querySelectorAll(".slide") - slides.forEach((slide: any) => { - slide.style.width = "99%" - slide.style.margin = "0 auto" - }) - } - }) - } else { - document.exitFullscreen().then(() => { - setIsFullscreen(false) - if (resultRef) { - const slides = resultRef.querySelectorAll(".slide") - slides.forEach((slide: any) => { - slide.style.width = "" - slide.style.margin = "" - }) - } - }) - } - } - - // 监听全屏变化 - const handleFullscreenChange = () => { - if (!document.fullscreenElement) { - setIsFullscreen(false) - if (resultRef) { - const slides = resultRef.querySelectorAll(".slide") - slides.forEach((slide: any) => { - slide.style.width = "" - slide.style.margin = "" - }) - } - } - applyScale() - } - // 应用缩放 const applyScale = () => { if (!resultRef || !containerRef) return @@ -217,11 +171,6 @@ const PPTViewerApp = () => { } initPPTViewer() setupResponsiveScale() - document.addEventListener("fullscreenchange", handleFullscreenChange) - }) - - onCleanup(() => { - document.removeEventListener("fullscreenchange", handleFullscreenChange) }) return ( @@ -298,34 +247,6 @@ const PPTViewerApp = () => { onClick={zoomIn} /> - - {/* 全屏按钮 */} - - - : } - onClick={toggleFullscreen} - /> - - ) } diff --git a/src/pages/home/previews/xls.tsx b/src/pages/home/previews/xls.tsx index 3c8552fd8..1a4d48176 100644 --- a/src/pages/home/previews/xls.tsx +++ b/src/pages/home/previews/xls.tsx @@ -1,9 +1,8 @@ import { BoxWithFullScreen, Error as Erro, FullLoading } from "~/components" -import { Box, IconButton, Tooltip, Button, HStack } from "@hope-ui/solid" +import { Box, Button, HStack } from "@hope-ui/solid" import { loadScriptIIFE } from "~/utils" import { createSignal, onMount, For, Show } from "solid-js" import { useLink, useT, useCDN } from "~/hooks" -import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" // 声明全局ExcelJS类型 declare global { @@ -34,7 +33,6 @@ const ExcelViewerApp = () => { const { excelJSPath } = useCDN() const [loading, setLoading] = createSignal(true) const [error, setError] = createSignal(false) - const [isFullscreen, setIsFullscreen] = createSignal(false) const [sheets, setSheets] = createSignal([]) const [currentSheetIndex, setCurrentSheetIndex] = createSignal(0) let containerRef: HTMLDivElement | undefined @@ -125,21 +123,6 @@ const ExcelViewerApp = () => { } } - // 全屏切换 - const toggleFullscreen = () => { - if (!containerRef) return - - if (!document.fullscreenElement) { - containerRef.requestFullscreen().then(() => { - setIsFullscreen(true) - }) - } else { - document.exitFullscreen().then(() => { - setIsFullscreen(false) - }) - } - } - onMount(() => { loadExcelFile() }) @@ -178,98 +161,78 @@ const ExcelViewerApp = () => { - {/* 全屏按钮 */} - - - : } - onClick={toggleFullscreen} - /> - - + {/* Excel表格容器 */} +
+ {/* 表格内容 */} + 0}> +
+ + + + {(row) => ( + + + {(cell) => ( + + )} + + + )} + + +
+ {cell.value} +
+
+
+ + {/* 加载状态 */} + + + + + {/* 错误状态 */} + + + +
- - {/* Excel表格容器 */} -
- {/* 表格内容 */} - 0}> -
- - - - {(row) => ( - - - {(cell) => ( - - )} - - - )} - - -
- {cell.value} -
-
-
- - {/* 加载状态 */} - - - - - {/* 错误状态 */} - - - -
) } From 3146abca7eaf7558c4f79a02ffdde05ad1545758 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Sat, 27 Jun 2026 12:30:09 +0800 Subject: [PATCH 14/22] fix: improve fullscreen toggle functionality and visibility Signed-off-by: MadDogOwner --- src/components/Base.tsx | 49 +++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/components/Base.tsx b/src/components/Base.tsx index 72271371f..1bb36be9e 100644 --- a/src/components/Base.tsx +++ b/src/components/Base.tsx @@ -91,9 +91,11 @@ export const BoxWithFullScreen = (props: Parameters[0]) => { } onMount(() => { - const handler = () => setIsNativeFullscreen(!!document.fullscreenElement) - document.addEventListener("fullscreenchange", handler) - onCleanup(() => document.removeEventListener("fullscreenchange", handler)) + const fsHandler = () => setIsNativeFullscreen(!!document.fullscreenElement) + document.addEventListener("fullscreenchange", fsHandler) + onCleanup(() => { + document.removeEventListener("fullscreenchange", fsHandler) + }) }) return ( @@ -116,30 +118,35 @@ export const BoxWithFullScreen = (props: Parameters[0]) => { right="$2" bottom="$2" spacing="$2" - opacity="0.7" + opacity="0.2" _hover={{ opacity: "1" }} + transition="opacity 0.3s ease" + pointerEvents="auto" + zIndex="$sticky" > - {/* Full view toggle */} - - + {/* Full view toggle */} + : } - onClick={onToggle} - colorScheme="neutral" - size="sm" - /> - + withArrow + > + : } + onClick={onToggle} + colorScheme="neutral" + size="sm" + /> + + {/* Native fullscreen toggle */} Date: Sat, 27 Jun 2026 02:26:05 +0800 Subject: [PATCH 15/22] refactor: merge heic into image Signed-off-by: MadDogOwner --- src/lang/en/home.json | 1 - src/pages/home/previews/heic.tsx | 176 ------------------------------ src/pages/home/previews/image.tsx | 136 +++++++++++++++++++++-- src/pages/home/previews/index.ts | 8 +- 4 files changed, 128 insertions(+), 193 deletions(-) delete mode 100644 src/pages/home/previews/heic.tsx diff --git a/src/lang/en/home.json b/src/lang/en/home.json index b6dff495f..104cc2e83 100644 --- a/src/lang/en/home.json +++ b/src/lang/en/home.json @@ -44,7 +44,6 @@ "audio": "Audio Player", "ipa": "IPA Installer", "plist": "Plist Installer", - "heic": "HEIC Viewer", "pdf": "PDF Previewer", "ppt": "PPT Previewer", "xls": "XLS Previewer", diff --git a/src/pages/home/previews/heic.tsx b/src/pages/home/previews/heic.tsx deleted file mode 100644 index fa3720857..000000000 --- a/src/pages/home/previews/heic.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import { Error, FullLoading } from "~/components" -import { useCDN, useT } from "~/hooks" -import { loadScriptIIFE } from "~/utils" -import { objStore } from "~/store" -import { onCleanup, onMount, createSignal, Show } from "solid-js" - -const Preview = () => { - const t = useT() - const [loading, setLoading] = createSignal(true) - const [error, setError] = createSignal(false) - const { libHeifPath } = useCDN() - - // 获取当前目录下所有HEIC文件 - let heicFiles = objStore.objs.filter((obj) => - /\.(heic|heif|avif|vvc|avc|jpeg|jpg)$/i.test(obj.name), - ) - - if (heicFiles.length === 0) { - heicFiles = [objStore.obj] - } - - // // 键盘导航功能:左右箭头切换文件 - // const onKeydown = (e: KeyboardEvent) => { - // const index = heicFiles.findIndex((f) => f.name === objStore.obj.name) - // if (e.key === "ArrowLeft" && index > 0) { - // replace(heicFiles[index - 1].name) - // } else if (e.key === "ArrowRight" && index < heicFiles.length - 1) { - // replace(heicFiles[index + 1].name) - // } - // } - - let libheif: any - let decoder: any - let canvas: HTMLCanvasElement | undefined - - onMount(() => { - // window.addEventListener("keydown", onKeydown) - initLibheif() - }) - - onCleanup(() => { - // window.removeEventListener("keydown", onKeydown) - if (libheif && decoder) { - // decoder.free() - decoder = null - } - libheif = null - }) - - // 初始化libheif库 - const initLibheif = async () => { - setLoading(true) - setError(false) - - try { - // 动态加载libheif脚本 - if (!window.libheif) { - await loadScriptIIFE(`${libHeifPath()}/libheif.js`, "libheif-script") - } - - // 加载WASM文件 - const wasmBinary = await fetchWasm(`${libHeifPath()}/libheif.wasm`) - - // 初始化libheif - libheif = window.libheif({ wasmBinary }) - decoder = new libheif.HeifDecoder() - - // 加载并解码当前HEIC文件 - await loadAndDecode(objStore.raw_url) - } catch (e) { - console.error("HEIC初始化失败:", e) - setError(true) - setLoading(false) - } - } - - // 获取WASM文件 - const fetchWasm = async (url: string) => { - const response = await fetch(url) - if (!response.ok) throw `WASM加载失败: ${url}` - return await response.arrayBuffer() - } - - // 加载并解码HEIC文件 - const loadAndDecode = async (url: string) => { - try { - setLoading(true) - setError(false) - - // 获取HEIC文件 - const response = await fetch(url) - if (!response.ok) throw "文件获取失败" - const buffer = await response.arrayBuffer() - - // 解码HEIC文件 - const images = decoder.decode(buffer) - if (!images || images.length === 0) { - throw "没有可解码的图像" - } - - // 显示第一张图像 - const image = images[0] - await displayImage(image) - setLoading(false) - } catch (e) { - console.error("HEIC解码失败:", e) - setError(true) - setLoading(false) - } - } - - // 在canvas上显示图像 - const displayImage = (image: any) => { - return new Promise((resolve) => { - if (!canvas) return resolve() - - const width = image.get_width() - const height = image.get_height() - - // 调整canvas尺寸 - canvas.width = width - canvas.height = height - - // 创建ImageData对象 - const imageData = new ImageData(width, height) - - // 显示图像 - image.display(imageData, (displayData: ImageData | null) => { - if (!displayData || !canvas) return resolve() - - const ctx = canvas.getContext("2d") - if (!ctx) return resolve() - - ctx.putImageData(displayData, 0, 0) - resolve() - }) - }) - } - - return ( -
- - - {/* 加载状态 */} - - - - - {/* 错误状态 */} - - - -
- ) -} - -export default Preview diff --git a/src/pages/home/previews/image.tsx b/src/pages/home/previews/image.tsx index 44bf9a21b..ab4f1fea8 100644 --- a/src/pages/home/previews/image.tsx +++ b/src/pages/home/previews/image.tsx @@ -1,19 +1,126 @@ import { Error, FullLoading, ImageWithError } from "~/components" -import { useRouter, useT } from "~/hooks" +import { useCDN, useRouter, useT } from "~/hooks" +import { ext, loadScriptIIFE } from "~/utils" import { objStore } from "~/store" import { Obj, ObjType } from "~/types" -import { onCleanup, onMount } from "solid-js" +import { + createEffect, + createSignal, + Match, + onCleanup, + onMount, + Show, + Switch, +} from "solid-js" + +const HEIF_EXTS = new Set(["heic", "heif", "avif", "vvc", "avc"]) +const isHeif = (name: string) => HEIF_EXTS.has(ext(name)) interface PreviewProps { images?: Obj[] navigate?: (name: string) => void } +const HeifView = (props: { src: string }) => { + const t = useT() + const { libHeifPath } = useCDN() + const [loading, setLoading] = createSignal(true) + const [error, setError] = createSignal(false) + let canvas: HTMLCanvasElement | undefined + let libheif: any + let decoder: any + + const decode = async (url: string) => { + try { + setLoading(true) + setError(false) + if (!window.libheif) { + await loadScriptIIFE(`${libHeifPath()}/libheif.js`, "libheif-script") + } + if (!libheif) { + const wasm = await fetch(`${libHeifPath()}/libheif.wasm`).then((r) => { + if (!r.ok) throw "WASM load failed" + return r.arrayBuffer() + }) + libheif = window.libheif({ wasmBinary: wasm }) + decoder = new libheif.HeifDecoder() + } + const buffer = await fetch(url).then((r) => { + if (!r.ok) throw "File fetch failed" + return r.arrayBuffer() + }) + const images = decoder.decode(buffer) + if (!images?.length) throw "No decodable image" + const img = images[0] + const w = img.get_width() + const h = img.get_height() + if (!canvas) return + canvas.width = w + canvas.height = h + const imageData = new ImageData(w, h) + await new Promise((resolve) => { + img.display(imageData, (data: ImageData | null) => { + if (!data || !canvas) return resolve() + canvas.getContext("2d")?.putImageData(data, 0, 0) + resolve() + }) + }) + setLoading(false) + } catch (e) { + console.error("HEIF decode failed:", e) + setError(true) + setLoading(false) + } + } + + createEffect(() => { + if (props.src) decode(props.src) + }) + + onCleanup(() => { + decoder = null + libheif = null + }) + + return ( +
+ + + + + + + +
+ ) +} + const Preview = (props: PreviewProps) => { const t = useT() const { replace } = useRouter() let images = - props.images || objStore.objs.filter((obj) => obj.type === ObjType.IMAGE) + props.images || + objStore.objs.filter( + (obj) => obj.type === ObjType.IMAGE || isHeif(obj.name), + ) if (images.length === 0) { images = [objStore.obj] } @@ -53,14 +160,23 @@ const Preview = (props: PreviewProps) => { onCleanup(() => { window.removeEventListener("keydown", onKeydown) }) + return ( - } - fallbackErr={} - /> + } + fallbackErr={} + /> + } + > + + + + ) } diff --git a/src/pages/home/previews/index.ts b/src/pages/home/previews/index.ts index 00f374f75..687133f7b 100644 --- a/src/pages/home/previews/index.ts +++ b/src/pages/home/previews/index.ts @@ -92,6 +92,7 @@ const previews: Preview[] = [ { key: "image", type: ObjType.IMAGE, + exts: ["heic", "heif", "avif", "vvc", "avc"], // libheif component: lazy(() => import("./image")), prior: true, }, @@ -119,12 +120,7 @@ const previews: Preview[] = [ component: lazy(() => import("./plist")), prior: true, }, - { - key: "heic", - exts: ["heic", "heif", "avif", "vvc", "avc", "jpeg", "jpg"], - component: lazy(() => import("./heic")), - prior: true, - }, + ...(import.meta.env.VITE_LITE === "true" ? [] : [ From b06d041ee958bde59e2d0201a29be426f5c010fc Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Sat, 27 Jun 2026 13:43:02 +0800 Subject: [PATCH 16/22] feat: enhance image preview with toolbar and navigation controls Signed-off-by: MadDogOwner --- src/pages/home/previews/image.tsx | 427 +++++++++++++++++++++++++----- 1 file changed, 362 insertions(+), 65 deletions(-) diff --git a/src/pages/home/previews/image.tsx b/src/pages/home/previews/image.tsx index ab4f1fea8..3cf120221 100644 --- a/src/pages/home/previews/image.tsx +++ b/src/pages/home/previews/image.tsx @@ -1,8 +1,22 @@ -import { Error, FullLoading, ImageWithError } from "~/components" -import { useCDN, useRouter, useT } from "~/hooks" -import { ext, loadScriptIIFE } from "~/utils" -import { objStore } from "~/store" -import { Obj, ObjType } from "~/types" +import { + Box, + Center, + Flex, + HStack, + IconButton, + Spacer, + Text, + Tooltip, + VStack, +} from "@hope-ui/solid" +import { + BsArrowClockwise, + BsArrowCounterclockwise, + BsInfoCircle, + BsZoomIn, + BsZoomOut, +} from "solid-icons/bs" +import { FaSolidAngleLeft, FaSolidAngleRight } from "solid-icons/fa" import { createEffect, createSignal, @@ -12,16 +26,40 @@ import { Show, Switch, } from "solid-js" +import { + BoxWithFullScreen, + Error, + FullLoading, + ImageWithError, +} from "~/components" +import { useCDN, useRouter, useT } from "~/hooks" +import { objStore } from "~/store" +import { Obj, ObjType } from "~/types" +import { + alphaBgColor, + ext, + formatDate, + getFileSize, + loadScriptIIFE, +} from "~/utils" const HEIF_EXTS = new Set(["heic", "heif", "avif", "vvc", "avc"]) const isHeif = (name: string) => HEIF_EXTS.has(ext(name)) +const ZOOM_MIN = 0.1 +const ZOOM_MAX = 10 +const ZOOM_WHEEL_FACTOR = 1.08 +const ZOOM_BTN_STEP = 0.25 interface PreviewProps { images?: Obj[] navigate?: (name: string) => void } -const HeifView = (props: { src: string }) => { +// ── HEIF decoder ──────────────────────────────────────────────────── +const HeifView = (props: { + src: string + onLoad?: (w: number, h: number) => void +}) => { const t = useT() const { libHeifPath } = useCDN() const [loading, setLoading] = createSignal(true) @@ -65,6 +103,7 @@ const HeifView = (props: { src: string }) => { resolve() }) }) + props.onLoad?.(w, h) setLoading(false) } catch (e) { console.error("HEIF decode failed:", e) @@ -76,24 +115,13 @@ const HeifView = (props: { src: string }) => { createEffect(() => { if (props.src) decode(props.src) }) - onCleanup(() => { decoder = null libheif = null }) return ( -
+ <> { - + -
+ ) } +// ── Preview ───────────────────────────────────────────────────────── const Preview = (props: PreviewProps) => { const t = useT() const { replace } = useRouter() + + const [scale, setScale] = createSignal(1) + const [rotation, setRotation] = createSignal(0) + const [tx, setTx] = createSignal(0) + const [ty, setTy] = createSignal(0) + const [dragging, setDragging] = createSignal(false) + const [showInfo, setShowInfo] = createSignal(false) + const [imgSize, setImgSize] = createSignal({ w: 0, h: 0 }) + const [isFullscreen, setIsFullscreen] = createSignal(false) + + let containerRef!: HTMLDivElement + let areaRef!: HTMLDivElement + let dragOX = 0 + let dragOY = 0 + let startTx = 0 + let startTy = 0 + let images = props.images || - objStore.objs.filter( - (obj) => obj.type === ObjType.IMAGE || isHeif(obj.name), - ) - if (images.length === 0) { - images = [objStore.obj] - } + objStore.objs.filter((o) => o.type === ObjType.IMAGE || isHeif(o.name)) + if (images.length === 0) images = [objStore.obj] + + const curIdx = () => images.findIndex((f) => f.name === objStore.obj.name) + // ── reset on image change ── + createEffect(() => { + objStore.obj.name + setScale(1) + setRotation(0) + setTx(0) + setTy(0) + setImgSize({ w: 0, h: 0 }) + }) + + // ── navigation ── + const goTo = (obj: Obj) => { + if (props.navigate) props.navigate(obj.name) + else replace(obj.name) + } const prev = () => { - const index = images.findIndex((f) => f.name === objStore.obj.name) - if (index > 0) { - if (props.navigate) { - props.navigate(images[index - 1].name) - } else { - replace(images[index - 1].name) - } - } + const i = curIdx() + if (i > 0) goTo(images[i - 1]) } - const next = () => { - const index = images.findIndex((f) => f.name === objStore.obj.name) - if (index < images.length - 1) { - if (props.navigate) { - props.navigate(images[index + 1].name) - } else { - replace(images[index + 1].name) - } - } + const i = curIdx() + if (i < images.length - 1) goTo(images[i + 1]) + } + + // ── transforms ── + const resetTransform = () => { + setScale(1) + setRotation(0) + setTx(0) + setTy(0) + } + const zoomIn = () => setScale((s) => Math.min(s + ZOOM_BTN_STEP, ZOOM_MAX)) + const zoomOut = () => setScale((s) => Math.max(s - ZOOM_BTN_STEP, ZOOM_MIN)) + const rotL = () => setRotation((r) => r - 90) + const rotR = () => setRotation((r) => r + 90) + + // ── wheel zoom (towards cursor) ── + const onWheel = (e: WheelEvent) => { + e.preventDefault() + const rect = areaRef.getBoundingClientRect() + const cx = e.clientX - rect.left - rect.width / 2 + const cy = e.clientY - rect.top - rect.height / 2 + const oldS = scale() + const factor = e.deltaY < 0 ? ZOOM_WHEEL_FACTOR : 1 / ZOOM_WHEEL_FACTOR + const newS = Math.min(Math.max(oldS * factor, ZOOM_MIN), ZOOM_MAX) + const r = newS / oldS + setTx(cx - r * (cx - tx())) + setTy(cy - r * (cy - ty())) + setScale(newS) + } + + // ── drag ── + const onMouseDown = (e: MouseEvent) => { + if (scale() <= 1 || e.button !== 0) return + e.preventDefault() + setDragging(true) + dragOX = e.clientX + dragOY = e.clientY + startTx = tx() + startTy = ty() } + const onMouseMove = (e: MouseEvent) => { + if (!dragging()) return + setTx(startTx + (e.clientX - dragOX)) + setTy(startTy + (e.clientY - dragOY)) + } + const onMouseUp = () => setDragging(false) + const onDblClick = () => (scale() === 1 ? setScale(2) : resetTransform()) + + // ── image load ── + const onImgLoad = (e: Event) => { + const img = e.target as HTMLImageElement + setImgSize({ w: img.naturalWidth, h: img.naturalHeight }) + } + const onHeifLoad = (w: number, h: number) => setImgSize({ w, h }) - const onKeydown = (e: KeyboardEvent) => { - if (e.key === "ArrowLeft") { - prev() - } else if (e.key === "ArrowRight") { - next() + // ── keyboard ── + const onKey = (e: KeyboardEvent) => { + switch (e.key) { + case "ArrowLeft": + return prev() + case "ArrowRight": + return next() + case "+": + case "=": + return zoomIn() + case "-": + return zoomOut() + case "r": + return rotL() + case "R": + return rotR() + case "0": + return resetTransform() + case "i": + return setShowInfo((v) => !v) + case "f": + // TODO toggleFs() } } + + // ── fullscreen detection ── + const updateFullscreen = () => { + const native = !!document.fullscreenElement + setIsFullscreen(native) + } + onMount(() => { - window.addEventListener("keydown", onKeydown) + window.addEventListener("keydown", onKey) + areaRef?.addEventListener("wheel", onWheel, { passive: false }) + document.addEventListener("fullscreenchange", updateFullscreen) + updateFullscreen() }) onCleanup(() => { - window.removeEventListener("keydown", onKeydown) + window.removeEventListener("keydown", onKey) + areaRef?.removeEventListener("wheel", onWheel) + document.removeEventListener("fullscreenchange", updateFullscreen) }) + const imgTransform = () => + `translate(${tx()}px,${ty()}px) scale(${scale()}) rotate(${rotation()}deg)` + const cursor = () => + scale() > 1 ? (dragging() ? "grabbing" : "grab") : "default" + + // ── render ────────────────────────────────────────────────────── return ( - } - fallbackErr={} - /> - } - > - - - - + + + {/* ── Toolbar ── */} + + + 0}> + + } + aria-label="Previous" + variant="ghost" + size="sm" + onClick={prev} + /> + + + + + } + aria-label="Next" + variant="ghost" + size="sm" + onClick={next} + /> + + + + {objStore.obj.name} + + 1}> + + {curIdx() + 1}/{images.length} + + + + + + + } + aria-label="Info" + variant={showInfo() ? "subtle" : "ghost"} + size="sm" + onClick={() => setShowInfo((v) => !v)} + /> + + + } + aria-label="Zoom out" + variant="ghost" + size="sm" + onClick={zoomOut} + /> + + + } + aria-label="Zoom in" + variant="ghost" + size="sm" + onClick={zoomIn} + /> + + + } + aria-label="Rotate left" + variant="ghost" + size="sm" + onClick={rotL} + /> + + + } + aria-label="Rotate right" + variant="ghost" + size="sm" + onClick={rotR} + /> + + + + + {/* ── Image area ── */} +
+
+ } + fallbackErr={ + + } + onLoad={onImgLoad} + objectFit="contain" + /> + } + > + + + + +
+ + {/* ── Info overlay ── */} + + + + {objStore.obj.name} + + {getFileSize(objStore.obj.size)} + 0}> + + {imgSize().w} × {imgSize().h}px + + + + {formatDate(objStore.obj.modified)} + + + +
+
+
) } From 162ac128e97e6c523fa0dd135d03ddca1f1e54e4 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Sat, 27 Jun 2026 14:03:38 +0800 Subject: [PATCH 17/22] fix: prevent loaded jszip version incompatible in doc and ppt Signed-off-by: MadDogOwner --- src/pages/home/previews/doc.tsx | 4 +++- src/pages/home/previews/ppt.tsx | 32 +++++++++++++++++++++++++++----- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/pages/home/previews/doc.tsx b/src/pages/home/previews/doc.tsx index 291411500..db0ac1fef 100644 --- a/src/pages/home/previews/doc.tsx +++ b/src/pages/home/previews/doc.tsx @@ -34,9 +34,11 @@ const DocViewerApp = () => { setError(false) // 加载jszip和docx-preview库 + // 加载前清理其他版本的 jszip,避免全局变量冲突 + document.getElementById("jszip-2.6.1-script")?.remove() await loadScriptIIFE( npm("jszip", "3.10.1", "dist/jszip.min.js"), - "jszip-script", + "jszip-3.10.1-script", ) await loadScriptIIFE(docxPreviewPath(), "docx-preview-script") diff --git a/src/pages/home/previews/ppt.tsx b/src/pages/home/previews/ppt.tsx index 3364fce93..ed2f90db0 100644 --- a/src/pages/home/previews/ppt.tsx +++ b/src/pages/home/previews/ppt.tsx @@ -57,9 +57,11 @@ const PPTViewerApp = () => { "jquery-script", ) // 使用JSZip 2.x版本,不支持3.x + // 加载前清理其他版本的 jszip,避免全局变量冲突 + document.getElementById("jszip-3.10.1-script")?.remove() await loadScriptIIFE( npm("jszip", "2.6.1", "dist/jszip.min.js"), - "jszip-script", + "jszip-2.6.1-script", ) await loadScriptIIFE(`${baseUrl}/js/filereader.js`, "filereader-script") await loadScriptIIFE(`${baseUrl}/js/d3.min.js`, "d3-script") @@ -102,10 +104,31 @@ const PPTViewerApp = () => { }, }) - // 监听加载完成事件 - setTimeout(() => { + // 用 MutationObserver 检测 PPT 内容是否渲染完成 + const RENDER_TIMEOUT = 30000 // 30s fallback + let resolved = false + + const done = () => { + if (resolved) return + resolved = true setLoading(false) - }, 2000) + } + + const observer = new MutationObserver(() => { + // pptxjs 渲染完成后会生成子元素(slides) + if (resultRef!.children.length > 0) { + observer.disconnect() + done() + } + }) + observer.observe(resultRef, { childList: true, subtree: true }) + onCleanup(() => observer.disconnect()) + + // fallback: 若 30s 内 MutationObserver 未触发 + setTimeout(() => { + observer.disconnect() + done() + }, RENDER_TIMEOUT) } } catch (e) { console.error("PPT初始化失败:", e) @@ -151,7 +174,6 @@ const PPTViewerApp = () => { const setupResponsiveScale = () => { if (!resultRef || !containerRef) return - const result = resultRef const container = containerRef const observer = new ResizeObserver(() => { if (zoom() === null) applyScale() From bf9d1e4e76481dec61ddfd4b6eed6f54f9440f8e Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Sat, 27 Jun 2026 14:40:51 +0800 Subject: [PATCH 18/22] fix: unify z-index with hope ui css var Signed-off-by: MadDogOwner --- src/components/Base.tsx | 33 +++++++++++++++---------------- src/components/EncodingSelect.tsx | 2 +- src/components/SwitchLanguage.tsx | 2 +- src/pages/home/previews/image.tsx | 4 ++-- src/pages/home/toolbar/Right.tsx | 1 + src/pages/login/LoginBg.tsx | 2 +- src/pages/login/index.tsx | 2 +- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/components/Base.tsx b/src/components/Base.tsx index 1bb36be9e..babbbc3bb 100644 --- a/src/components/Base.tsx +++ b/src/components/Base.tsx @@ -32,7 +32,6 @@ import { import { AiOutlineFullscreen, AiOutlineFullscreenExit } from "solid-icons/ai" import { BsFullscreen, BsFullscreenExit } from "solid-icons/bs" import { useT } from "~/hooks" -import { notify } from "~/utils" export const Error = (props: { msg: string @@ -77,8 +76,8 @@ export const Error = (props: { } export const BoxWithFullScreen = (props: Parameters[0]) => { - const { isOpen, onToggle } = createDisclosure() - const [isNativeFullscreen, setIsNativeFullscreen] = createSignal(false) + const { isOpen: isFullView, onToggle } = createDisclosure() + const [isFullScreen, setIsFullScreen] = createSignal(false) let containerRef: HTMLDivElement const t = useT() @@ -91,7 +90,7 @@ export const BoxWithFullScreen = (props: Parameters[0]) => { } onMount(() => { - const fsHandler = () => setIsNativeFullscreen(!!document.fullscreenElement) + const fsHandler = () => setIsFullScreen(!!document.fullscreenElement) document.addEventListener("fullscreenchange", fsHandler) onCleanup(() => { document.removeEventListener("fullscreenchange", fsHandler) @@ -101,15 +100,15 @@ export const BoxWithFullScreen = (props: Parameters[0]) => { return ( {props.children} @@ -122,13 +121,13 @@ export const BoxWithFullScreen = (props: Parameters[0]) => { _hover={{ opacity: "1" }} transition="opacity 0.3s ease" pointerEvents="auto" - zIndex="$sticky" + zIndex="$docked" > - + {/* Full view toggle */} [0]) => { > : } + icon={isFullView() ? : } onClick={onToggle} colorScheme="neutral" size="sm" @@ -151,7 +150,7 @@ export const BoxWithFullScreen = (props: Parameters[0]) => { {/* Native fullscreen toggle */} [0]) => { > ) : ( diff --git a/src/components/EncodingSelect.tsx b/src/components/EncodingSelect.tsx index 260b175dc..ae79f1ccb 100644 --- a/src/components/EncodingSelect.tsx +++ b/src/components/EncodingSelect.tsx @@ -80,7 +80,7 @@ export function EncodingSelect(props: { _hover={{ opacity: 1, }} - zIndex={1} + zIndex="$docked" > ({ diff --git a/src/components/SwitchLanguage.tsx b/src/components/SwitchLanguage.tsx index 9611ec387..e4c1d9f91 100644 --- a/src/components/SwitchLanguage.tsx +++ b/src/components/SwitchLanguage.tsx @@ -59,7 +59,7 @@ export const SwitchLanguage = ( pos="fixed" top={0} bg={useColorModeValue("$blackAlpha4", "$whiteAlpha4")()} - zIndex="9000" + zIndex="$notification" > { position={isFullscreen() ? "absolute" : "relative"} top={isFullscreen() ? "0" : undefined} left={isFullscreen() ? "0" : undefined} - zIndex="10" + zIndex="$docked" css={{ "backdrop-filter": "blur(8px)", }} @@ -451,7 +451,7 @@ const Preview = (props: PreviewProps) => { p="$2" bg={alphaBgColor()} borderRadius="$md" - zIndex="10" + zIndex="$docked" fontSize="$sm" css={{ "backdrop-filter": "blur(2px)", diff --git a/src/pages/home/toolbar/Right.tsx b/src/pages/home/toolbar/Right.tsx index 9ae109fcd..f008309f8 100644 --- a/src/pages/home/toolbar/Right.tsx +++ b/src/pages/home/toolbar/Right.tsx @@ -30,6 +30,7 @@ export const Right = () => { pos="fixed" right={margin()} bottom={margin()} + zIndex="calc($modal - 1)" > { top="0" left="0" overflow="hidden" - zIndex="-1" + zIndex="$hide" w="100vw" h="100vh" > diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index 6d22c284f..fec89c08c 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -225,7 +225,7 @@ const Login = () => { } return ( -
+
Date: Sat, 27 Jun 2026 15:01:53 +0800 Subject: [PATCH 19/22] style: update colors for image Signed-off-by: MadDogOwner --- src/components/Base.tsx | 2 +- src/pages/home/previews/image.tsx | 31 ++++++++++++++----------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/components/Base.tsx b/src/components/Base.tsx index babbbc3bb..6fc432093 100644 --- a/src/components/Base.tsx +++ b/src/components/Base.tsx @@ -117,7 +117,7 @@ export const BoxWithFullScreen = (props: Parameters[0]) => { right="$2" bottom="$2" spacing="$2" - opacity="0.2" + opacity="0.7" _hover={{ opacity: "1" }} transition="opacity 0.3s ease" pointerEvents="auto" diff --git a/src/pages/home/previews/image.tsx b/src/pages/home/previews/image.tsx index c3784651a..36d6022dd 100644 --- a/src/pages/home/previews/image.tsx +++ b/src/pages/home/previews/image.tsx @@ -35,13 +35,7 @@ import { import { useCDN, useRouter, useT } from "~/hooks" import { objStore } from "~/store" import { Obj, ObjType } from "~/types" -import { - alphaBgColor, - ext, - formatDate, - getFileSize, - loadScriptIIFE, -} from "~/utils" +import { ext, formatDate, getFileSize, loadScriptIIFE } from "~/utils" const HEIF_EXTS = new Set(["heic", "heif", "avif", "vvc", "avc"]) const isHeif = (name: string) => HEIF_EXTS.has(ext(name)) @@ -300,15 +294,15 @@ const Preview = (props: PreviewProps) => { {/* ── Toolbar ── */} 0}> @@ -342,6 +336,7 @@ const Preview = (props: PreviewProps) => { "text-overflow": "ellipsis", "white-space": "nowrap", }} + display={{ "@initial": "none", "@sm": "block" }} > {objStore.obj.name} @@ -449,24 +444,26 @@ const Preview = (props: PreviewProps) => { bottom="$2" left="$2" p="$2" - bg={alphaBgColor()} + bg="$blackAlpha9" borderRadius="$md" zIndex="$docked" fontSize="$sm" css={{ - "backdrop-filter": "blur(2px)", + "backdrop-filter": "blur(8px)", }} > - + {objStore.obj.name} - {getFileSize(objStore.obj.size)} + + {getFileSize(objStore.obj.size)} + 0}> - + {imgSize().w} × {imgSize().h}px - + {formatDate(objStore.obj.modified)} From 0276dabf6bbe5b441241886663d5822f5536640d Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Sat, 27 Jun 2026 15:18:25 +0800 Subject: [PATCH 20/22] fix: add extraButtons prop to BoxWithFullScreen Fix the external link button of the iframe overlaps with the fullscreen button in the bottom right corner Signed-off-by: MadDogOwner --- src/components/Base.tsx | 5 +++- src/pages/home/previews/iframe.tsx | 37 +++++++++++++++--------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/components/Base.tsx b/src/components/Base.tsx index 6fc432093..30c996b88 100644 --- a/src/components/Base.tsx +++ b/src/components/Base.tsx @@ -75,7 +75,9 @@ export const Error = (props: { ) } -export const BoxWithFullScreen = (props: Parameters[0]) => { +export const BoxWithFullScreen = ( + props: Parameters[0] & { extraButtons?: JSXElement }, +) => { const { isOpen: isFullView, onToggle } = createDisclosure() const [isFullScreen, setIsFullScreen] = createSignal(false) let containerRef: HTMLDivElement @@ -123,6 +125,7 @@ export const BoxWithFullScreen = (props: Parameters[0]) => { pointerEvents="auto" zIndex="$docked" > + {props.extraButtons} {/* Full view toggle */} { }) }) return ( - + + } + onClick={() => { + window.open(iframeSrc(), "_blank") + }} + colorScheme="neutral" + size="sm" + /> + + } + > - { - window.open(iframeSrc(), "_blank") - }} - cursor="pointer" - rounded="$md" - bgColor={hoverColor()} - p="$1" - boxSize="$7" - /> ) } From ab319af902bf197b114019111e21df36fca85437 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Sat, 27 Jun 2026 15:50:17 +0800 Subject: [PATCH 21/22] feat: add image fit modes Signed-off-by: MadDogOwner --- src/pages/home/previews/image.tsx | 108 ++++++++++++++++++++++++++++-- 1 file changed, 101 insertions(+), 7 deletions(-) diff --git a/src/pages/home/previews/image.tsx b/src/pages/home/previews/image.tsx index 36d6022dd..e9031986d 100644 --- a/src/pages/home/previews/image.tsx +++ b/src/pages/home/previews/image.tsx @@ -17,6 +17,11 @@ import { BsZoomOut, } from "solid-icons/bs" import { FaSolidAngleLeft, FaSolidAngleRight } from "solid-icons/fa" +import { + TbArrowAutofitHeight, + TbArrowAutofitWidth, + TbArrowAutofitContent, +} from "solid-icons/tb" import { createEffect, createSignal, @@ -53,6 +58,7 @@ interface PreviewProps { const HeifView = (props: { src: string onLoad?: (w: number, h: number) => void + style?: any }) => { const t = useT() const { libHeifPath } = useCDN() @@ -119,9 +125,7 @@ const HeifView = (props: { @@ -142,6 +146,9 @@ const Preview = (props: PreviewProps) => { const [scale, setScale] = createSignal(1) const [rotation, setRotation] = createSignal(0) + const [fitMode, setFitMode] = createSignal<"contain" | "height" | "width">( + "contain", + ) const [tx, setTx] = createSignal(0) const [ty, setTy] = createSignal(0) const [dragging, setDragging] = createSignal(false) @@ -198,6 +205,24 @@ const Preview = (props: PreviewProps) => { const zoomOut = () => setScale((s) => Math.max(s - ZOOM_BTN_STEP, ZOOM_MIN)) const rotL = () => setRotation((r) => r - 90) const rotR = () => setRotation((r) => r + 90) + const fitPage = () => { + setFitMode("contain") + setScale(1) + setTx(0) + setTy(0) + } + const fitHeight = () => { + setFitMode("height") + setScale(1) + setTx(0) + setTy(0) + } + const fitWidth = () => { + setFitMode("width") + setScale(1) + setTx(0) + setTy(0) + } // ── wheel zoom (towards cursor) ── const onWheel = (e: WheelEvent) => { @@ -259,6 +284,12 @@ const Preview = (props: PreviewProps) => { return resetTransform() case "i": return setShowInfo((v) => !v) + case "c": + return fitPage() + case "h": + return fitHeight() + case "w": + return fitWidth() case "f": // TODO toggleFs() } @@ -357,6 +388,7 @@ const Preview = (props: PreviewProps) => { onClick={() => setShowInfo((v) => !v)} /> + } @@ -375,6 +407,33 @@ const Preview = (props: PreviewProps) => { onClick={zoomIn} /> + + } + aria-label="Fit page" + variant="ghost" + size="sm" + onClick={fitPage} + /> + + + } + aria-label="Fit height" + variant="ghost" + size="sm" + onClick={fitHeight} + /> + + + } + aria-label="Fit width" + variant="ghost" + size="sm" + onClick={fitWidth} + /> + } @@ -412,8 +471,14 @@ const Preview = (props: PreviewProps) => { onDblClick={onDblClick} >
{ } onLoad={onImgLoad} - objectFit="contain" + {...(fitMode() === "contain" + ? { w: "$full", h: "$full", objectFit: "contain" } + : { + css: + fitMode() === "height" + ? { + height: "100%", + width: "auto", + "max-width": "none", + } + : { + width: "100%", + height: "auto", + "max-height": "none", + }, + })} /> } > - +
From 23688744a18e455a0a41c0f4f2168db57d7f0d74 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Sat, 27 Jun 2026 16:14:41 +0800 Subject: [PATCH 22/22] fix: unset width for hope-notification__list Signed-off-by: MadDogOwner --- src/app/index.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/app/index.css b/src/app/index.css index ce79082f0..0698907d2 100644 --- a/src/app/index.css +++ b/src/app/index.css @@ -48,3 +48,8 @@ .hope-select__option { flex-shrink: 0; } + +/* Fixes: https://github.com/OpenListTeam/OpenList-Frontend/issues/26 */ +.hope-notification__list { + width: unset; +}