@l1yp/file-viewer 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -6
- package/dist/assets/step.worker-CfDTYOf_.js.map +1 -0
- package/dist/components/CertViewer.js +1 -1
- package/dist/components/CertViewer.js.map +1 -1
- package/dist/components/CertViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/DocxNavigationPane.js +1 -1
- package/dist/components/DocxNavigationPane.js.map +1 -1
- package/dist/components/DocxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/DocxViewer.js +1 -1
- package/dist/components/DocxViewer.js.map +1 -1
- package/dist/components/DocxViewer.vue.d.ts +1 -1
- package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js +80 -78
- package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/EplViewer.js +1 -1
- package/dist/components/EplViewer.js.map +1 -1
- package/dist/components/EplViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/FileViewerBody.js +1 -1
- package/dist/components/FileViewerBody.js.map +1 -1
- package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js +265 -257
- package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/FileViewerPanel.js +1 -1
- package/dist/components/FileViewerPanel.js.map +1 -1
- package/dist/components/FileViewerPanel.vue.d.ts +6 -1
- package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js +42 -40
- package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/HarViewer.js +1 -1
- package/dist/components/HarViewer.js.map +1 -1
- package/dist/components/HarViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/JksViewer.js +1 -1
- package/dist/components/JksViewer.js.map +1 -1
- package/dist/components/JksViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/ModelViewModes.js +9 -0
- package/dist/components/ModelViewModes.js.map +1 -0
- package/dist/components/ModelViewModes.vue.d.ts +11 -0
- package/dist/components/ModelViewModes.vue_vue_type_script_setup_true_lang.js +49 -0
- package/dist/components/ModelViewModes.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/ModelViewer.js +1 -1
- package/dist/components/ModelViewer.js.map +1 -1
- package/dist/components/ModelViewer.vue.d.ts +2 -1
- package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js +183 -96
- package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/PdfNavigationPane.js +1 -1
- package/dist/components/PdfNavigationPane.js.map +1 -1
- package/dist/components/PdfNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/PdfViewer.js +1 -1
- package/dist/components/PdfViewer.js.map +1 -1
- package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/PptxNavigationPane.js +1 -1
- package/dist/components/PptxNavigationPane.js.map +1 -1
- package/dist/components/PptxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/PsdViewer.js +1 -1
- package/dist/components/PsdViewer.js.map +1 -1
- package/dist/components/PsdViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/SqliteViewer.js +1 -1
- package/dist/components/SqliteViewer.js.map +1 -1
- package/dist/components/SqliteViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/ViewerLoadError.js +9 -0
- package/dist/components/ViewerLoadError.js.map +1 -0
- package/dist/components/ViewerLoadError.vue.d.ts +17 -0
- package/dist/components/ViewerLoadError.vue_vue_type_script_setup_true_lang.js +58 -0
- package/dist/components/ViewerLoadError.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/XlsxChart.js +1 -1
- package/dist/components/XlsxChart.js.map +1 -1
- package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js +9 -9
- package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/XlsxImageLayer.js +9 -0
- package/dist/components/XlsxImageLayer.js.map +1 -0
- package/dist/components/XlsxImageLayer.vue.d.ts +9 -0
- package/dist/components/XlsxImageLayer.vue_vue_type_script_setup_true_lang.js +46 -0
- package/dist/components/XlsxImageLayer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/XlsxViewer.js +1 -1
- package/dist/components/XlsxViewer.js.map +1 -1
- package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js +230 -216
- package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/epl/EplResource.js +1 -1
- package/dist/components/epl/EplResource.js.map +1 -1
- package/dist/components/epl/EplResource.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/optionalViewer.d.ts +3 -2
- package/dist/components/optionalViewer.js +6 -3
- package/dist/components/optionalViewer.js.map +1 -1
- package/dist/composables/useFileViewer.d.ts +11 -3
- package/dist/composables/useFileViewer.js.map +1 -1
- package/dist/composables/useTeleportTheme.d.ts +7 -0
- package/dist/composables/useTeleportTheme.js +42 -0
- package/dist/composables/useTeleportTheme.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +29 -28
- package/dist/lib/docxTheme.d.ts +6 -0
- package/dist/lib/docxTheme.js +64 -0
- package/dist/lib/docxTheme.js.map +1 -0
- package/dist/lib/epl/code/structures.js +82 -0
- package/dist/lib/epl/code/structures.js.map +1 -0
- package/dist/lib/epl/code/variable.js +16 -0
- package/dist/lib/epl/code/variable.js.map +1 -0
- package/dist/lib/epl/eplDocument.js +28 -22
- package/dist/lib/epl/eplDocument.js.map +1 -1
- package/dist/lib/epl/index.d.ts +2 -1
- package/dist/lib/epl/index.js.map +1 -1
- package/dist/lib/epl/sections/code.js +37 -0
- package/dist/lib/epl/sections/code.js.map +1 -0
- package/dist/lib/epl/sections/esystemInfo.js +29 -0
- package/dist/lib/epl/sections/esystemInfo.js.map +1 -0
- package/dist/lib/epl/sections/losable.js +33 -0
- package/dist/lib/epl/sections/losable.js.map +1 -0
- package/dist/lib/epl/sections/projectConfig.js +39 -0
- package/dist/lib/epl/sections/projectConfig.js.map +1 -0
- package/dist/lib/epl/sections/resource.js +84 -0
- package/dist/lib/epl/sections/resource.js.map +1 -0
- package/dist/lib/fileType.d.ts +3 -1
- package/dist/lib/fileType.js +150 -137
- package/dist/lib/fileType.js.map +1 -1
- package/dist/lib/modelAppearance.d.ts +7 -0
- package/dist/lib/modelAppearance.js +41 -0
- package/dist/lib/modelAppearance.js.map +1 -0
- package/dist/lib/step.d.ts +10 -0
- package/dist/lib/step.js +35 -0
- package/dist/lib/step.js.map +1 -0
- package/dist/lib/step.worker.d.ts +1 -0
- package/dist/lib/step.worker.js +18 -0
- package/dist/lib/step.worker.js.map +1 -0
- package/dist/lib/stepObject.d.ts +4 -0
- package/dist/lib/stepObject.js +52 -0
- package/dist/lib/stepObject.js.map +1 -0
- package/dist/lib/stepTypes.d.ts +60 -0
- package/dist/lib/viewerKind.js +10 -5
- package/dist/lib/viewerKind.js.map +1 -1
- package/dist/lib/xlsx.d.ts +1 -1
- package/dist/lib/xlsx.js +106 -89
- package/dist/lib/xlsx.js.map +1 -1
- package/dist/lib/xlsxCharts.js +71 -119
- package/dist/lib/xlsxCharts.js.map +1 -1
- package/dist/lib/xlsxImages.d.ts +43 -0
- package/dist/lib/xlsxImages.js +114 -0
- package/dist/lib/xlsxImages.js.map +1 -0
- package/dist/lib/xlsxParts.d.ts +17 -0
- package/dist/lib/xlsxParts.js +86 -0
- package/dist/lib/xlsxParts.js.map +1 -0
- package/dist/style.css +1 -1
- package/package.json +8 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EplViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/EplViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, ref, watch } from 'vue'\nimport { decompileEpl, EplEncryptedError, type EplPreview } from '@/lib/epl'\nimport EplTree from './epl/EplTree.vue'\nimport EplDoc from './epl/EplDoc.vue'\nimport type { EplDocRef, EplDocKind } from './epl/docTypes'\n\n/**\n * IDE-style viewer for e-language (易语言) `.e` / `.ec` project files. Fetches the BYTES and parses + decompiles\n * them entirely in-browser via {@link decompileEpl} (port of OpenEpl/EProjectFile, see lib/epl) — no dependency,\n * like the eml / mht / binobj viewers. Lays the result out like the 易语言 IDE: a「程序」tree on the left\n * (程序集 / 类 + project tables), the selected document on the right (definitions as bordered tables, method\n * bodies with dashed block guides), open documents as bottom tabs, and 支持库/程序 tabs under the tree.\n *\n * Encrypted files surface a friendly message (decryption is a separate path); a large file is gated behind a\n * confirm (all parsing is in-memory). Entry shape mirrors the other self-fetching viewers: a real download URL,\n * or a blob URL minted by an archive browser from extracted bytes.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error' | 'encrypted'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst encryptTip = ref('')\nconst preview = ref<EplPreview | null>(null)\n\nconst SIZE_LIMIT = 32 * 1024 * 1024\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n encryptTip.value = ''\n preview.value = null\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n preview.value = decompileEpl(new Uint8Array(buf))\n status.value = 'ready'\n } catch (e) {\n if (e instanceof EplEncryptedError) {\n status.value = 'encrypted'\n encryptTip.value = e.tip ?? ''\n } else {\n status.value = 'error'\n errorMsg.value = e instanceof Error ? e.message : String(e)\n }\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nwatch(() => props.url, load, { immediate: true })\n\nconst meta = computed(() => preview.value?.meta ?? null)\nconst program = computed(() => preview.value?.program ?? null)\nconst fileTypeLabel = computed(() => {\n const t = meta.value?.fileType\n return t === 'source' ? '源码 .e' : t === 'module' ? '模块 .ec' : '未知'\n})\n\n// ── IDE state: side tab + open document tabs ──────────────────────────────────────────────────────\nconst sideTab = ref<'program' | 'libs'>('program')\nconst openTabs = ref<EplDocRef[]>([])\nconst activeId = ref('')\nconst activeDoc = computed(() => openTabs.value.find((t) => t.id === activeId.value) ?? null)\n\nconst TABLE_TITLE: Record<Exclude<EplDocKind, 'class'>, string> = {\n globals: '全局变量',\n structs: '自定义数据类型',\n dll: 'Dll命令',\n constants: '常量表',\n resources: '图片/声音',\n forms: '资源表',\n libraries: '模块引用表',\n}\n\nfunction defaultDoc(p: NonNullable<typeof preview.value>['program']): EplDocRef | null {\n if (p.classes.length) {\n const c = p.classes[0]\n return { id: `class:${c.id >>> 0}`, kind: 'class', classId: c.id, title: c.name }\n }\n const order: Exclude<EplDocKind, 'class'>[] = [\n 'globals',\n 'structs',\n 'dll',\n 'constants',\n 'resources',\n 'forms',\n 'libraries',\n ]\n const counts: Record<string, number> = {\n globals: p.globals.length,\n structs: p.structs.length,\n dll: p.dllDeclares.length,\n constants: p.constants.length,\n resources: p.resources.length,\n forms: p.forms.length,\n libraries: p.libraries.length,\n }\n for (const k of order) {\n if (counts[k] > 0) return { id: k, kind: k, title: TABLE_TITLE[k] }\n }\n return null\n}\n\nwatch(preview, (p) => {\n openTabs.value = []\n activeId.value = ''\n sideTab.value = 'program'\n if (p) {\n const d = defaultDoc(p.program)\n if (d) openDoc(d)\n }\n})\n\nfunction openDoc(ref: EplDocRef): void {\n if (!openTabs.value.some((t) => t.id === ref.id)) openTabs.value = [...openTabs.value, ref]\n activeId.value = ref.id\n}\n\nfunction closeTab(id: string): void {\n const idx = openTabs.value.findIndex((t) => t.id === id)\n if (idx < 0) return\n const next = openTabs.value.filter((t) => t.id !== id)\n openTabs.value = next\n if (activeId.value === id) {\n const fallback = next[idx] ?? next[idx - 1] ?? null\n activeId.value = fallback ? fallback.id : ''\n }\n}\n\nasync function onSelectMethod(payload: { classId: number; methodId: number }): Promise<void> {\n const p = program.value\n if (!p) return\n const c = p.classes.find((x) => x.id === payload.classId)\n if (!c) return\n openDoc({\n id: `class:${payload.classId >>> 0}`,\n kind: 'class',\n classId: payload.classId,\n title: c.name,\n })\n await nextTick()\n document.getElementById(`epl-m-${payload.methodId >>> 0}`)?.scrollIntoView({ block: 'start' })\n}\n\nfunction openLibraries(): void {\n openDoc({ id: 'libraries', kind: 'libraries', title: '模块引用表' })\n}\n</script>\n\n<template>\n <div class=\"epl\">\n <p v-if=\"status === 'loading'\" class=\"epl__msg\">正在解析易语言工程…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"epl__msg epl__gate\">\n <p>文件较大({{ ((props.size ?? 0) / 1024 / 1024).toFixed(1) }} MB),是否继续解析预览?</p>\n <button type=\"button\" class=\"epl__btn\" @click=\"doLoad\">继续预览</button>\n </div>\n\n <div v-else-if=\"status === 'encrypted'\" class=\"epl__msg epl__gate\">\n <p>这是加密的易语言工程文件,暂未支持解密预览。</p>\n <p v-if=\"encryptTip\" class=\"epl__tip\">密码提示:{{ encryptTip }}</p>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"epl__msg epl__msg--error\">\n <p>解析失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"epl__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <div v-else-if=\"status === 'ready' && meta && program\" class=\"epl__ide\">\n <!-- top bar: project metadata -->\n <div class=\"epl__top\">\n <span class=\"epl__name\">{{ meta.project?.name || props.name }}</span>\n <span class=\"epl__badge\">{{ fileTypeLabel }}</span>\n <span class=\"epl__counts\">\n 程序集 {{ meta.counts.classes }} · 子程序 {{ meta.counts.methods }} · 数据类型\n {{ meta.counts.structs }} · DLL {{ meta.counts.dllDeclares }}\n <template v-if=\"meta.systemVersion\"> · 易语言 {{ meta.systemVersion }}</template>\n </span>\n </div>\n\n <div class=\"epl__body\">\n <!-- left: tree + side tabs -->\n <aside class=\"epl__side\">\n <div class=\"epl__sidebody\">\n <EplTree\n v-show=\"sideTab === 'program'\"\n :program=\"program\"\n :active-id=\"activeId\"\n @select=\"openDoc\"\n @select-method=\"onSelectMethod\"\n />\n <div v-show=\"sideTab === 'libs'\" class=\"epl__libs\">\n <div class=\"epl__libsroot\">支持库</div>\n <button\n v-for=\"(l, i) in program.libraries\"\n :key=\"i\"\n type=\"button\"\n class=\"epl__lib\"\n :title=\"l.fileName\"\n @click=\"openLibraries\"\n >\n <span class=\"epl__libico\">库</span>{{ l.name || l.fileName }}\n </button>\n <p v-if=\"!program.libraries.length\" class=\"epl__libsempty\">(无引用支持库)</p>\n </div>\n </div>\n <div class=\"epl__sidetabs\">\n <button\n type=\"button\"\n :class=\"{ 'is-active': sideTab === 'libs' }\"\n @click=\"sideTab = 'libs'\"\n >\n 支持库\n </button>\n <button\n type=\"button\"\n :class=\"{ 'is-active': sideTab === 'program' }\"\n @click=\"sideTab = 'program'\"\n >\n 程序\n </button>\n </div>\n </aside>\n\n <!-- right: document + bottom tabs -->\n <main class=\"epl__main\">\n <div class=\"epl__docarea\">\n <EplDoc v-if=\"activeDoc\" :program=\"program\" :doc=\"activeDoc\" />\n <p v-else class=\"epl__msg\">从左侧选择程序集、类或表进行查看。</p>\n </div>\n <div v-if=\"openTabs.length\" class=\"epl__doctabs\">\n <div\n v-for=\"t in openTabs\"\n :key=\"t.id\"\n class=\"epl__doctab\"\n :class=\"{ 'is-active': t.id === activeId }\"\n >\n <button type=\"button\" class=\"epl__doctab-label\" @click=\"activeId = t.id\">\n {{ t.title }}\n </button>\n <button type=\"button\" class=\"epl__doctab-close\" title=\"关闭\" @click=\"closeTab(t.id)\">\n ×\n </button>\n </div>\n </div>\n </main>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.epl {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n\n /* Fixed light \"IDE\" palette so the viewer reads like 易语言 regardless of the app theme. */\n --epl-bg: #ffffff;\n --epl-fg: #1f1f1f;\n --epl-border: #c4ccc4;\n --epl-th-bg: #dce8dc;\n --epl-th-fg: #2c3a2c;\n --epl-ident: #1b50c8;\n --epl-keyword: #0a3fb0;\n --epl-string: #c01818;\n --epl-comment: #1f8a3b;\n --epl-check: #1a7a1a;\n --epl-guide: #9aa6b2;\n --epl-gutter: #97a0a8;\n --epl-gutter-bg: #f4f6f4;\n --epl-row-hover: #eef4ff;\n --epl-sel: #cfe0ff;\n --epl-side-bg: #f6f7f6;\n --epl-tabbar-bg: #dfe3df;\n --epl-tab-bg: #e9ece9;\n --epl-tab-active-bg: #ffffff;\n}\n\n/* ── status messages ─────────────────────────────────────────────────────── */\n.epl__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.epl__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.epl__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n}\n.epl__tip {\n color: var(--faint);\n font-size: 12.5px;\n}\n.epl__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.epl__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* ── IDE shell ───────────────────────────────────────────────────────────── */\n.epl__ide {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--epl-bg);\n color: var(--epl-fg);\n}\n.epl__top {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n padding: 6px 12px;\n border-bottom: 1px solid var(--epl-border);\n background: var(--epl-side-bg);\n font-size: 12.5px;\n}\n.epl__name {\n font-weight: 600;\n color: #1c1c1c;\n}\n.epl__badge {\n font-size: 11px;\n color: #0a5;\n background: #e3f3e8;\n border-radius: 999px;\n padding: 1px 8px;\n}\n.epl__counts {\n color: #5a6a5a;\n}\n\n.epl__body {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n}\n\n/* left side */\n.epl__side {\n flex: 0 0 auto;\n width: 248px;\n display: flex;\n flex-direction: column;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-side-bg);\n}\n.epl__sidebody {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.epl__sidetabs {\n flex: 0 0 auto;\n display: flex;\n border-top: 1px solid var(--epl-border);\n background: var(--epl-tabbar-bg);\n}\n.epl__sidetabs button {\n flex: 1 1 0;\n padding: 5px 8px;\n border: none;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-tab-bg);\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n color: #444;\n}\n.epl__sidetabs button:last-child {\n border-right: none;\n}\n.epl__sidetabs button.is-active {\n background: var(--epl-tab-active-bg);\n font-weight: 600;\n color: #1c1c1c;\n}\n.epl__libs {\n padding: 6px 0;\n font-size: 12.5px;\n}\n.epl__libsroot {\n padding: 2px 10px 4px;\n font-weight: 600;\n color: var(--epl-th-fg);\n}\n.epl__lib {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 2px 10px 2px 18px;\n border: none;\n background: none;\n cursor: pointer;\n text-align: left;\n font: inherit;\n color: inherit;\n}\n.epl__lib:hover {\n color: var(--epl-ident);\n}\n.epl__libico {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 15px;\n height: 15px;\n border-radius: 3px;\n font-size: 10px;\n color: #fff;\n background: #c08a4a;\n}\n.epl__libsempty {\n padding: 4px 12px;\n color: var(--epl-gutter);\n}\n\n/* right side */\n.epl__main {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n}\n.epl__docarea {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--epl-bg);\n}\n.epl__doctabs {\n flex: 0 0 auto;\n display: flex;\n overflow-x: auto;\n border-top: 1px solid var(--epl-border);\n background: var(--epl-tabbar-bg);\n}\n.epl__doctab {\n display: flex;\n align-items: center;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-tab-bg);\n white-space: nowrap;\n}\n.epl__doctab.is-active {\n background: var(--epl-tab-active-bg);\n}\n.epl__doctab-label {\n padding: 5px 6px 5px 12px;\n border: none;\n background: none;\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n color: #444;\n max-width: 16em;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.epl__doctab.is-active .epl__doctab-label {\n font-weight: 600;\n color: #1c1c1c;\n}\n.epl__doctab-close {\n padding: 0 8px 0 2px;\n border: none;\n background: none;\n cursor: pointer;\n color: #999;\n font-size: 14px;\n line-height: 1;\n}\n.epl__doctab-close:hover {\n color: var(--epl-string);\n}\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EplViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/EplViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, ref, watch } from 'vue'\nimport { decompileEpl, EplEncryptedError, type EplPreview } from '@/lib/epl'\nimport EplTree from './epl/EplTree.vue'\nimport EplDoc from './epl/EplDoc.vue'\nimport type { EplDocRef, EplDocKind } from './epl/docTypes'\n\n/**\n * IDE-style viewer for e-language (易语言) `.e` / `.ec` project files. Fetches the BYTES and parses + decompiles\n * them entirely in-browser via {@link decompileEpl} (port of OpenEpl/EProjectFile, see lib/epl) — no dependency,\n * like the eml / mht / binobj viewers. Lays the result out like the 易语言 IDE: a「程序」tree on the left\n * (程序集 / 类 + project tables), the selected document on the right (definitions as bordered tables, method\n * bodies with dashed block guides), open documents as bottom tabs, and 支持库/程序 tabs under the tree.\n *\n * Encrypted files surface a friendly message (decryption is a separate path); a large file is gated behind a\n * confirm (all parsing is in-memory). Entry shape mirrors the other self-fetching viewers: a real download URL,\n * or a blob URL minted by an archive browser from extracted bytes.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error' | 'encrypted'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst encryptTip = ref('')\nconst preview = ref<EplPreview | null>(null)\n\nconst SIZE_LIMIT = 32 * 1024 * 1024\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n encryptTip.value = ''\n preview.value = null\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n preview.value = decompileEpl(new Uint8Array(buf))\n status.value = 'ready'\n } catch (e) {\n if (e instanceof EplEncryptedError) {\n status.value = 'encrypted'\n encryptTip.value = e.tip ?? ''\n } else {\n status.value = 'error'\n errorMsg.value = e instanceof Error ? e.message : String(e)\n }\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nwatch(() => props.url, load, { immediate: true })\n\nconst meta = computed(() => preview.value?.meta ?? null)\nconst program = computed(() => preview.value?.program ?? null)\nconst fileTypeLabel = computed(() => {\n const t = meta.value?.fileType\n return t === 'source' ? '源码 .e' : t === 'module' ? '模块 .ec' : '未知'\n})\n\n// ── IDE state: side tab + open document tabs ──────────────────────────────────────────────────────\nconst sideTab = ref<'program' | 'libs'>('program')\nconst openTabs = ref<EplDocRef[]>([])\nconst activeId = ref('')\nconst activeDoc = computed(() => openTabs.value.find((t) => t.id === activeId.value) ?? null)\n\nconst TABLE_TITLE: Record<Exclude<EplDocKind, 'class'>, string> = {\n globals: '全局变量',\n structs: '自定义数据类型',\n dll: 'Dll命令',\n constants: '常量表',\n resources: '图片/声音',\n forms: '资源表',\n libraries: '模块引用表',\n}\n\nfunction defaultDoc(p: NonNullable<typeof preview.value>['program']): EplDocRef | null {\n if (p.classes.length) {\n const c = p.classes[0]\n return { id: `class:${c.id >>> 0}`, kind: 'class', classId: c.id, title: c.name }\n }\n const order: Exclude<EplDocKind, 'class'>[] = [\n 'globals',\n 'structs',\n 'dll',\n 'constants',\n 'resources',\n 'forms',\n 'libraries',\n ]\n const counts: Record<string, number> = {\n globals: p.globals.length,\n structs: p.structs.length,\n dll: p.dllDeclares.length,\n constants: p.constants.length,\n resources: p.resources.length,\n forms: p.forms.length,\n libraries: p.libraries.length,\n }\n for (const k of order) {\n if (counts[k] > 0) return { id: k, kind: k, title: TABLE_TITLE[k] }\n }\n return null\n}\n\nwatch(preview, (p) => {\n openTabs.value = []\n activeId.value = ''\n sideTab.value = 'program'\n if (p) {\n const d = defaultDoc(p.program)\n if (d) openDoc(d)\n }\n})\n\nfunction openDoc(ref: EplDocRef): void {\n if (!openTabs.value.some((t) => t.id === ref.id)) openTabs.value = [...openTabs.value, ref]\n activeId.value = ref.id\n}\n\nfunction closeTab(id: string): void {\n const idx = openTabs.value.findIndex((t) => t.id === id)\n if (idx < 0) return\n const next = openTabs.value.filter((t) => t.id !== id)\n openTabs.value = next\n if (activeId.value === id) {\n const fallback = next[idx] ?? next[idx - 1] ?? null\n activeId.value = fallback ? fallback.id : ''\n }\n}\n\nasync function onSelectMethod(payload: { classId: number; methodId: number }): Promise<void> {\n const p = program.value\n if (!p) return\n const c = p.classes.find((x) => x.id === payload.classId)\n if (!c) return\n openDoc({\n id: `class:${payload.classId >>> 0}`,\n kind: 'class',\n classId: payload.classId,\n title: c.name,\n })\n await nextTick()\n document.getElementById(`epl-m-${payload.methodId >>> 0}`)?.scrollIntoView({ block: 'start' })\n}\n\nfunction openLibraries(): void {\n openDoc({ id: 'libraries', kind: 'libraries', title: '模块引用表' })\n}\n</script>\n\n<template>\n <div class=\"epl\">\n <p v-if=\"status === 'loading'\" class=\"epl__msg\">正在解析易语言工程…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"epl__msg epl__gate\">\n <p>文件较大({{ ((props.size ?? 0) / 1024 / 1024).toFixed(1) }} MB),是否继续解析预览?</p>\n <button type=\"button\" class=\"epl__btn\" @click=\"doLoad\">继续预览</button>\n </div>\n\n <div v-else-if=\"status === 'encrypted'\" class=\"epl__msg epl__gate\">\n <p>这是加密的易语言工程文件,暂未支持解密预览。</p>\n <p v-if=\"encryptTip\" class=\"epl__tip\">密码提示:{{ encryptTip }}</p>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"epl__msg epl__msg--error\">\n <p>解析失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"epl__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <div v-else-if=\"status === 'ready' && meta && program\" class=\"epl__ide\">\n <!-- top bar: project metadata -->\n <div class=\"epl__top\">\n <span class=\"epl__name\">{{ meta.project?.name || props.name }}</span>\n <span class=\"epl__badge\">{{ fileTypeLabel }}</span>\n <span class=\"epl__counts\">\n 程序集 {{ meta.counts.classes }} · 子程序 {{ meta.counts.methods }} · 数据类型\n {{ meta.counts.structs }} · DLL {{ meta.counts.dllDeclares }}\n <template v-if=\"meta.systemVersion\"> · 易语言 {{ meta.systemVersion }}</template>\n </span>\n </div>\n\n <div class=\"epl__body\">\n <!-- left: tree + side tabs -->\n <aside class=\"epl__side\">\n <div class=\"epl__sidebody\">\n <EplTree\n v-show=\"sideTab === 'program'\"\n :program=\"program\"\n :active-id=\"activeId\"\n @select=\"openDoc\"\n @select-method=\"onSelectMethod\"\n />\n <div v-show=\"sideTab === 'libs'\" class=\"epl__libs\">\n <div class=\"epl__libsroot\">支持库</div>\n <button\n v-for=\"(l, i) in program.libraries\"\n :key=\"i\"\n type=\"button\"\n class=\"epl__lib\"\n :title=\"l.fileName\"\n @click=\"openLibraries\"\n >\n <span class=\"epl__libico\">库</span>{{ l.name || l.fileName }}\n </button>\n <p v-if=\"!program.libraries.length\" class=\"epl__libsempty\">(无引用支持库)</p>\n </div>\n </div>\n <div class=\"epl__sidetabs\">\n <button\n type=\"button\"\n :class=\"{ 'is-active': sideTab === 'libs' }\"\n @click=\"sideTab = 'libs'\"\n >\n 支持库\n </button>\n <button\n type=\"button\"\n :class=\"{ 'is-active': sideTab === 'program' }\"\n @click=\"sideTab = 'program'\"\n >\n 程序\n </button>\n </div>\n </aside>\n\n <!-- right: document + bottom tabs -->\n <main class=\"epl__main\">\n <div class=\"epl__docarea\">\n <EplDoc v-if=\"activeDoc\" :program=\"program\" :doc=\"activeDoc\" />\n <p v-else class=\"epl__msg\">从左侧选择程序集、类或表进行查看。</p>\n </div>\n <div v-if=\"openTabs.length\" class=\"epl__doctabs\">\n <div\n v-for=\"t in openTabs\"\n :key=\"t.id\"\n class=\"epl__doctab\"\n :class=\"{ 'is-active': t.id === activeId }\"\n >\n <button type=\"button\" class=\"epl__doctab-label\" @click=\"activeId = t.id\">\n {{ t.title }}\n </button>\n <button type=\"button\" class=\"epl__doctab-close\" title=\"关闭\" @click=\"closeTab(t.id)\">\n ×\n </button>\n </div>\n </div>\n </main>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.epl {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n\n /* IDE roles follow the containing viewer, including explicit themes and host overrides. */\n --epl-bg: var(--surface);\n --epl-fg: var(--text);\n --epl-border: var(--border-2);\n --epl-th-bg: color-mix(in srgb, var(--hl-name) 12%, var(--surface));\n --epl-th-fg: var(--text-strong);\n --epl-ident: var(--hl-attr);\n --epl-keyword: var(--hl-kw);\n --epl-string: var(--hl-str);\n --epl-comment: var(--hl-name);\n --epl-check: var(--hl-name);\n --epl-guide: var(--border-2);\n --epl-gutter: var(--muted);\n --epl-gutter-bg: var(--code-bg);\n --epl-row-hover: var(--accent-soft);\n --epl-sel: color-mix(in srgb, var(--hl-attr) 18%, var(--surface));\n --epl-side-bg: var(--surface-2);\n --epl-tabbar-bg: var(--surface-2);\n --epl-tab-bg: var(--code-head);\n --epl-tab-active-bg: var(--surface);\n}\n\n/* ── status messages ─────────────────────────────────────────────────────── */\n.epl__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.epl__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.epl__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n}\n.epl__tip {\n color: var(--faint);\n font-size: 12.5px;\n}\n.epl__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.epl__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* ── IDE shell ───────────────────────────────────────────────────────────── */\n.epl__ide {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--epl-bg);\n color: var(--epl-fg);\n}\n.epl__top {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n padding: 6px 12px;\n border-bottom: 1px solid var(--epl-border);\n background: var(--epl-side-bg);\n font-size: 12.5px;\n}\n.epl__name {\n font-weight: 600;\n color: var(--text-strong);\n}\n.epl__badge {\n font-size: 11px;\n color: var(--hl-name);\n background: color-mix(in srgb, var(--hl-name) 12%, transparent);\n border-radius: 999px;\n padding: 1px 8px;\n}\n.epl__counts {\n color: var(--muted);\n}\n\n.epl__body {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n}\n\n/* left side */\n.epl__side {\n flex: 0 0 auto;\n width: 248px;\n display: flex;\n flex-direction: column;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-side-bg);\n}\n.epl__sidebody {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.epl__sidetabs {\n flex: 0 0 auto;\n display: flex;\n border-top: 1px solid var(--epl-border);\n background: var(--epl-tabbar-bg);\n}\n.epl__sidetabs button {\n flex: 1 1 0;\n padding: 5px 8px;\n border: none;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-tab-bg);\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n color: var(--text);\n}\n.epl__sidetabs button:last-child {\n border-right: none;\n}\n.epl__sidetabs button.is-active {\n background: var(--epl-tab-active-bg);\n font-weight: 600;\n color: var(--text-strong);\n}\n.epl__libs {\n padding: 6px 0;\n font-size: 12.5px;\n}\n.epl__libsroot {\n padding: 2px 10px 4px;\n font-weight: 600;\n color: var(--epl-th-fg);\n}\n.epl__lib {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 2px 10px 2px 18px;\n border: none;\n background: none;\n cursor: pointer;\n text-align: left;\n font: inherit;\n color: inherit;\n}\n.epl__lib:hover {\n color: var(--epl-ident);\n}\n.epl__libico {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 15px;\n height: 15px;\n border-radius: 3px;\n font-size: 10px;\n color: #fff;\n background: #c08a4a;\n}\n.epl__libsempty {\n padding: 4px 12px;\n color: var(--epl-gutter);\n}\n\n/* right side */\n.epl__main {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n}\n.epl__docarea {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--epl-bg);\n}\n.epl__doctabs {\n flex: 0 0 auto;\n display: flex;\n overflow-x: auto;\n border-top: 1px solid var(--epl-border);\n background: var(--epl-tabbar-bg);\n}\n.epl__doctab {\n display: flex;\n align-items: center;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-tab-bg);\n white-space: nowrap;\n}\n.epl__doctab.is-active {\n background: var(--epl-tab-active-bg);\n}\n.epl__doctab-label {\n padding: 5px 6px 5px 12px;\n border: none;\n background: none;\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n color: var(--text);\n max-width: 16em;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.epl__doctab.is-active .epl__doctab-label {\n font-weight: 600;\n color: var(--text-strong);\n}\n.epl__doctab-close {\n padding: 0 8px 0 2px;\n border: none;\n background: none;\n cursor: pointer;\n color: var(--muted);\n font-size: 14px;\n line-height: 1;\n}\n.epl__doctab-close:hover {\n color: var(--epl-string);\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import e from "../_virtual/_plugin-vue_export-helper.js";
|
|
|
2
2
|
import t from "./FileViewerBody.vue_vue_type_script_setup_true_lang.js";
|
|
3
3
|
/* empty css */
|
|
4
4
|
//#region src/components/FileViewerBody.vue
|
|
5
|
-
var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-
|
|
5
|
+
var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-e2f8d258"]]);
|
|
6
6
|
//#endregion
|
|
7
7
|
export { n as default };
|
|
8
8
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileViewerBody.js","names":[],"sources":["../../src/components/FileViewerBody.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, defineAsyncComponent, onBeforeUnmount, ref, useTemplateRef, watch } from 'vue'\nimport { optionalViewer } from '@/components/optionalViewer'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { skipsTextLoad } from '@/lib/viewerKind'\nimport {\n VIEWER_FORMATTERS as FORMATTERS,\n applyFormatter,\n findFormatter,\n type ViewerFormatter,\n} from '@/lib/viewerFormats'\nimport { fileExt } from '@/lib/fileType'\nimport { looksLikeMindMap } from '@/lib/mindmap'\nimport { parseFrontmatter } from '@/lib/frontmatter'\nimport { formatBytes } from '@/lib/format'\nimport { ansiPlainText } from '@/lib/ansi'\nimport CodeView from '@/components/CodeView.vue'\nimport MarkdownText from '@/components/MarkdownText'\nimport FrontmatterCard from '@/components/FrontmatterCard.vue'\nimport ZoomImage from '@/components/ZoomImage.vue'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * The side viewer's CONTENT area — the type dispatch shared by {@link FileViewerPanel} (a top-level file) and\n * the archive browsers (a file inside a zip/7z/tar/rar, extracted to a blob URL). Given a {@link ViewerFile}\n * it renders the right thing: a self-fetching sub-viewer (docx/pdf/dicom/…), an inline image/video, or — for\n * the text-backed kinds — markdown / html / svg / mind-map / subtitle / geo / csv (rendered), or highlighted,\n * foldable source. It fetches its own text for the text-backed kinds and emits it via `loaded` so the host's\n * copy button can use it. The host owns the header/bar, the render↔source `mode`, and copy/download.\n *\n * Extracted from FileViewerPanel so an archive entry previews with the EXACT same viewers as a top-level file\n * (the old archive detail view only did text/image/office/dicom). The archive browsers import this lazily to\n * break the import cycle (this component lazy-imports them right back for nested archives).\n *\n * \"Open with\" override: ANY file can be force-opened as a different formatter (hex / text / markdown / html /\n * svg / image / audio / video — {@link VIEWER_FORMATTERS}), since auto-detection by extension/mime can be\n * wrong. The choice becomes an EFFECTIVE file ({@link vf}) that re-runs the dispatch. Two entry points drive\n * the SAME override: (1) the host's header menu (FileViewerPanel) calls the exposed `pickFormat`/`resetFormat`\n * — the primary, universal entry; (2) the unpreviewable-binary card's own chip row, for the archive-browser\n * context where there is no host header. The effective file is emitted via `formatchange` (so the host tracks\n * toggle/copy visibility) and the active key via `formatpick` (so the host menu highlights it). `hostChooser`\n * hides the in-body switcher bar when the host already shows the chooser in its header (no duplicate UI).\n */\nconst props = withDefaults(\n defineProps<{\n file: ViewerFile\n mode?: 'render' | 'source'\n hostChooser?: boolean\n /**\n * Render-only HTML whose archive-relative dependencies have already been rewritten. Source/copy continue to\n * use `file.content` (or fetched text), so opening an HTML entry inside a ZIP still shows its original source.\n */\n htmlPreview?: string\n }>(),\n { mode: 'render', hostChooser: false },\n)\nconst emit = defineEmits<{\n loaded: [text: string]\n formatchange: [file: ViewerFile]\n formatpick: [key: string]\n}>()\n\n// Archives import this body; lazy-import them back so the module cycle never evaluates eagerly.\nconst ZipBrowser = defineAsyncComponent(() => import('@/components/ZipBrowser.vue'))\nconst SevenZipBrowser = defineAsyncComponent(() => import('@/components/SevenZipBrowser.vue'))\nconst ArchiveBrowser = defineAsyncComponent(() => import('@/components/ArchiveBrowser.vue'))\nconst BinObjBrowser = defineAsyncComponent(() => import('@/components/BinObjBrowser.vue'))\nconst EplViewer = defineAsyncComponent(() => import('@/components/EplViewer.vue'))\nconst ClassViewer = defineAsyncComponent(() => import('@/components/ClassViewer.vue'))\nconst HprofViewer = defineAsyncComponent(() => import('@/components/HprofViewer.vue'))\nconst MsiViewer = defineAsyncComponent(() => import('@/components/MsiViewer.vue'))\nconst AppxViewer = defineAsyncComponent(() => import('@/components/AppxViewer.vue'))\n// Heavy viewers whose engine is a STATIC optional peer dep: wrap so a missing dep shows an install card.\nconst DicomViewer = optionalViewer(\n () => import('@/components/DicomViewer.vue'),\n 'dicom-parser',\n 'DICOM 影像',\n)\nconst DocxViewer = optionalViewer(\n () => import('@/components/DocxViewer.vue'),\n 'docx-preview',\n 'Word 文档',\n)\nconst XlsxViewer = optionalViewer(\n () => import('@/components/XlsxViewer.vue'),\n ['exceljs', 'numfmt'],\n 'Excel 表格',\n)\nconst PptxViewer = optionalViewer(\n () => import('@/components/PptxViewer.vue'),\n ['@aiden0z/pptx-renderer', 'pdfjs-dist'],\n 'PowerPoint',\n)\nconst RtfViewer = defineAsyncComponent(() => import('@/components/RtfViewer.vue'))\nconst PdfViewer = optionalViewer(() => import('@/components/PdfViewer.vue'), 'pdfjs-dist', 'PDF')\nconst MindMapView = defineAsyncComponent(() => import('@/components/MindMapView.vue'))\nconst XmindViewer = defineAsyncComponent(() => import('@/components/XmindViewer.vue'))\nconst EmlViewer = defineAsyncComponent(() => import('@/components/EmlViewer.vue'))\nconst MhtViewer = defineAsyncComponent(() => import('@/components/MhtViewer.vue'))\nconst HarViewer = defineAsyncComponent(() => import('@/components/HarViewer.vue'))\nconst SqliteViewer = optionalViewer(\n () => import('@/components/SqliteViewer.vue'),\n 'sql.js',\n 'SQLite 数据库',\n)\nconst SubtitleViewer = defineAsyncComponent(() => import('@/components/SubtitleViewer.vue'))\nconst FontViewer = defineAsyncComponent(() => import('@/components/FontViewer.vue'))\nconst NotebookViewer = defineAsyncComponent(() => import('@/components/NotebookViewer.vue'))\nconst GeoViewer = defineAsyncComponent(() => import('@/components/GeoViewer.vue'))\nconst CsvViewer = defineAsyncComponent(() => import('@/components/CsvViewer.vue'))\nconst JsonlViewer = defineAsyncComponent(() => import('@/components/JsonlViewer.vue'))\nconst CertViewer = defineAsyncComponent(() => import('@/components/CertViewer.vue'))\nconst JksViewer = defineAsyncComponent(() => import('@/components/JksViewer.vue'))\nconst AudioViewer = defineAsyncComponent(() => import('@/components/AudioViewer.vue'))\nconst QmcAudioViewer = defineAsyncComponent(() => import('@/components/QmcAudioViewer.vue'))\nconst LrcViewer = defineAsyncComponent(() => import('@/components/LrcViewer.vue'))\nconst LogViewer = defineAsyncComponent(() => import('@/components/LogViewer.vue'))\nconst ModelViewer = optionalViewer(() => import('@/components/ModelViewer.vue'), 'three', '3D 模型')\nconst RasterViewer = defineAsyncComponent(() => import('@/components/RasterViewer.vue'))\nconst PsdViewer = defineAsyncComponent(() => import('@/components/PsdViewer.vue'))\nconst EbookViewer = defineAsyncComponent(() => import('@/components/EbookViewer.vue'))\nconst DrawioViewer = defineAsyncComponent(() => import('@/components/DrawioViewer.vue'))\nconst DxfViewer = defineAsyncComponent(() => import('@/components/DxfViewer.vue'))\nconst HexViewer = defineAsyncComponent(() => import('@/components/HexViewer.vue'))\nconst logViewerRef = useTemplateRef<{ copyText: () => string }>('logViewer')\n\n// ── \"open with\" override ──────────────────────────────────────────────────────────────────────\n// A user-chosen formatter ({@link VIEWER_FORMATTERS}). When set it replaces props.file as the EFFECTIVE file\n// the dispatch (and load) use; cleared whenever a different file is opened. Driven by the host header menu\n// (via the exposed pickFormat/resetFormat) and/or the in-body binary-card chips — both funnel through `pick`.\nconst pickedKey = ref('')\nconst picked = ref<ViewerFile | null>(null)\n// The effective file: the user's chosen formatter, else the file passed in.\nconst vf = computed<ViewerFile>(() => picked.value ?? props.file)\n\nfunction pick(f: ViewerFormatter): void {\n pickedKey.value = f.key\n picked.value = applyFormatter(props.file, f)\n}\n// Host-driven entry points (FileViewerPanel's header menu): pick by key, or fall back to auto-detection.\nfunction pickFormat(key: string): void {\n const f = findFormatter(key)\n if (f) pick(f)\n}\nfunction resetFormat(): void {\n pickedKey.value = ''\n picked.value = null\n}\n// Announce the active formatter so the host header can highlight it (fires on pick, reset, and file change).\nwatch(pickedKey, (k) => emit('formatpick', k))\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst text = ref('')\nconst errorMsg = ref('')\n\n// Mind map only renders the diagram once the loaded text actually looks like one (a non-mind-map `.mm` reads\n// as plain source). Same content sniff the panel used.\nconst isMindMap = computed(\n () =>\n vf.value.mindmap === true &&\n status.value === 'ready' &&\n looksLikeMindMap(text.value, fileExt(vf.value.name)),\n)\n\n// Markdown render: lift any leading YAML frontmatter into a card; the source view still shows the raw file.\nconst frontmatter = computed(() =>\n vf.value.lang === 'markdown' ? parseFrontmatter(text.value) : { data: {}, body: text.value },\n)\nconst hasFrontmatter = computed(() => Object.keys(frontmatter.value.data).length > 0)\n\n// Drop a single trailing newline so the source view doesn't show a phantom last line.\nconst sourceText = computed(() => text.value.replace(/\\n$/, ''))\n\n// SVG render: rasterize the markup via a data-URL <img> (an <img> never runs SVG scripts).\nconst svgSrc = computed(() =>\n text.value ? `data:image/svg+xml,${encodeURIComponent(text.value)}` : '',\n)\n\n// HTML preview, two modes (default = inert):\n// - INERT (default): the iframe carries an empty `sandbox` (no `allow-scripts`) plus an injected\n// `script-src 'none'` CSP, so NO upstream script runs — only layout/CSS/images render.\n// - INTERACTIVE (opt-in via the toolbar): scripts run inside a HARDENED sandbox — `allow-scripts` WITHOUT\n// `allow-same-origin` gives the frame an opaque origin (no access to our cookies/storage/session/DOM); that\n// opaque origin is the REAL isolation. The CSP allows external sub-resources AND network so CDN-based pages\n// fully work — scripts/styles/fonts/images from unpkg/jsdelivr/Google Fonts, plus fetch/XHR/WebSocket and\n// source-map loads (`connect-src` open to https/http/ws/wss). Only the structural hardening stays:\n// `object-src 'none'` (no plugins) and `base-uri 'none'` (no <base> hijack); forms are already blocked by\n// the sandbox (no `allow-forms`). Because the origin is opaque, even with network open the page can't reach\n// our session/cookies — it acts only with its own (already-visible) content. Review the source first\n// (render/source toggle), then enable. The ⛶ button opens this SAME sandboxed frame as a full-screen\n// in-app overlay (still an iframe → still opaque-origin), so a big interactive view stays safe — there's\n// no top-level / new-tab path that could ever run scripts in our origin.\nconst INERT_CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'\">`\nconst INTERACTIVE_CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"default-src 'unsafe-inline' 'unsafe-eval' data: blob: https: http:; ` +\n `connect-src data: blob: https: http: ws: wss:; object-src 'none'; base-uri 'none'\">`\n\nfunction injectCsp(html: string, csp: string): string {\n const head = html.match(/<head[^>]*>/i)\n if (head) {\n const at = (head.index ?? 0) + head[0].length\n return html.slice(0, at) + csp + html.slice(at)\n }\n return `<!doctype html><html><head><meta charset=\"utf-8\">${csp}</head><body>${html}</body></html>`\n}\n\nconst inertBlobUrl = ref('') // the inert (non-interactive) iframe + full-screen overlay\nconst interactiveBlobUrl = ref('') // built only when the user opts in\nconst interactive = ref(false)\nconst fullscreen = ref(false) // in-app full-screen overlay (Teleport to body) over the SAME sandboxed frame\nconst frameBlobUrl = computed(() =>\n interactive.value ? interactiveBlobUrl.value : inertBlobUrl.value,\n)\nconst previewHtml = computed(() => props.htmlPreview ?? text.value)\n\nfunction revokeHtmlBlobs(): void {\n if (inertBlobUrl.value) {\n URL.revokeObjectURL(inertBlobUrl.value)\n inertBlobUrl.value = ''\n }\n if (interactiveBlobUrl.value) {\n URL.revokeObjectURL(interactiveBlobUrl.value)\n interactiveBlobUrl.value = ''\n }\n}\n\nwatch([() => vf.value.html, previewHtml], ([html, preview]) => {\n revokeHtmlBlobs()\n interactive.value = false // a new file / changed text always returns to the safe inert default\n fullscreen.value = false // …and closes any open full-screen overlay\n if (html && typeof URL !== 'undefined' && URL.createObjectURL) {\n inertBlobUrl.value = URL.createObjectURL(\n new Blob([injectCsp(preview, INERT_CSP)], { type: 'text/html' }),\n )\n }\n})\n\n// Toggle the sandboxed iframe between inert and interactive (builds the interactive blob lazily on first use).\nfunction toggleInteractive(): void {\n if (interactive.value) {\n interactive.value = false\n return\n }\n if (typeof URL !== 'undefined' && URL.createObjectURL && !interactiveBlobUrl.value) {\n interactiveBlobUrl.value = URL.createObjectURL(\n new Blob([injectCsp(previewHtml.value, INTERACTIVE_CSP)], { type: 'text/html' }),\n )\n }\n interactive.value = true\n}\n\n// Full-screen overlay key handling + body scroll lock — only while open, SSR-guarded (no window in smoke).\nfunction onFsKey(e: KeyboardEvent): void {\n if (e.key === 'Escape') fullscreen.value = false\n}\nwatch(fullscreen, (on) => {\n if (typeof window === 'undefined') return\n if (on) {\n window.addEventListener('keydown', onFsKey)\n document.body.style.overflow = 'hidden'\n } else {\n window.removeEventListener('keydown', onFsKey)\n document.body.style.overflow = ''\n }\n})\n\nasync function load(noStore = false): Promise<void> {\n const f = vf.value\n // Self-fetching sub-viewers + image/video/binary render straight from the URL — no text to load.\n if (skipsTextLoad(f)) {\n text.value = ''\n status.value = 'ready'\n return\n }\n if (f.content != null) {\n text.value = f.content\n status.value = 'ready'\n emit('loaded', ansiPlainText(f.content))\n return\n }\n if (!f.url) {\n text.value = ''\n status.value = 'ready'\n return\n }\n status.value = 'loading'\n errorMsg.value = ''\n try {\n const res = await fetch(f.url, noStore ? { cache: 'no-store' } : {})\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const t = await res.text()\n text.value = t\n status.value = 'ready'\n emit('loaded', ansiPlainText(t))\n } catch (e) {\n errorMsg.value = String(e)\n status.value = 'error'\n }\n}\n\n// A new file resets any \"open with\" override (registered BEFORE the vf watcher so picked is cleared before the\n// effective file is re-read below).\nwatch(\n () => props.file,\n () => {\n picked.value = null\n pickedKey.value = ''\n },\n)\n// Load + announce the effective file whenever it changes (a new file, or a picked formatter).\nwatch(\n vf,\n (v) => {\n emit('formatchange', v)\n void load()\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => {\n revokeHtmlBlobs()\n if (typeof window !== 'undefined') {\n window.removeEventListener('keydown', onFsKey)\n document.body.style.overflow = ''\n }\n})\n\n// The host's refresh button re-fetches (cache-busting) through here; its header menu drives the \"open with\"\n// override through pickFormat / resetFormat (see the override block above).\ndefineExpose({\n reload: () => load(true),\n pickFormat,\n resetFormat,\n copyText: () =>\n vf.value.log ? (logViewerRef.value?.copyText() ?? '') : ansiPlainText(text.value),\n})\n</script>\n\n<template>\n <div class=\"fvb\">\n <!-- forced-format switcher: shown once the user picks a formatter, so they can swap. Hidden when the host\n provides the chooser in its header (FileViewerPanel) — only the archive browsers render it in-body. -->\n <div v-if=\"picked && !hostChooser\" class=\"fvb__fmtbar\">\n <span class=\"fvb__fmtbar-label\">格式</span>\n <button\n v-for=\"f in FORMATTERS\"\n :key=\"f.key\"\n type=\"button\"\n class=\"fvb__fmtchip\"\n :class=\"{ 'fvb__fmtchip--on': pickedKey === f.key }\"\n @click=\"pick(f)\"\n >\n <Icon :name=\"f.icon\" :size=\"13\" /><span>{{ f.label }}</span>\n </button>\n </div>\n\n <!-- zip / 7z / tar-rar: the archive browsers fetch the bytes, decompress in-browser, and browse entries -->\n <ZipBrowser v-if=\"vf.zip\" :url=\"vf.url ?? ''\" :name=\"vf.name\" />\n <SevenZipBrowser v-else-if=\"vf.sevenZip\" :url=\"vf.url ?? ''\" :name=\"vf.name\" />\n <ArchiveBrowser v-else-if=\"vf.archive\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :kind=\"vf.archive\" />\n\n <!-- native binary object (.so/.dll): parse + list symbols -->\n <BinObjBrowser v-else-if=\"vf.binobj\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- e-language (.e/.ec): parse the binary section format + decompile each method to 易语言 source -->\n <EplViewer v-else-if=\"vf.epl\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- java bytecode (.class): parse the structural tables + list fields/methods (no decompilation) -->\n <ClassViewer v-else-if=\"vf.javaClass\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- jvm heap dump (.hprof): stream-parse → class histogram + per-class object drill-in (paginated/filterable) -->\n <HprofViewer v-else-if=\"vf.hprof\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- windows installer (.msi): parse the OLE compound file → product identity + summary info -->\n <MsiViewer v-else-if=\"vf.msi\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- windows app package (.appx/.msix): unzip the manifest → identity / deps / capabilities -->\n <AppxViewer v-else-if=\"vf.appx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- xmind workbook (.xmind): unzip content.json/xml → render as an interactive mind map -->\n <XmindViewer v-else-if=\"vf.xmind\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- dxf (.dxf): parse the ASCII CAD drawing → SVG with pan/zoom + layer toggles -->\n <DxfViewer v-else-if=\"vf.dxf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <DicomViewer v-else-if=\"vf.dicom\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <DocxViewer v-else-if=\"vf.docx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <XlsxViewer v-else-if=\"vf.xlsx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <PptxViewer v-else-if=\"vf.pptx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <RtfViewer v-else-if=\"vf.rtf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <PdfViewer v-else-if=\"vf.pdf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <EmlViewer v-else-if=\"vf.eml\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <MhtViewer v-else-if=\"vf.mht\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <HarViewer v-else-if=\"vf.har\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <SqliteViewer v-else-if=\"vf.sqlite\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <CertViewer v-else-if=\"vf.cert\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- java keystore (.jks/.keystore/.jceks): parse the inventory + certs; password decrypts private keys -->\n <JksViewer v-else-if=\"vf.jks\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <FontViewer v-else-if=\"vf.font\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <NotebookViewer v-else-if=\"vf.ipynb\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <AudioViewer v-else-if=\"vf.audio\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- QQ Music encrypted audio (.mflac/.mgg): decrypt in-browser → play + download decrypted -->\n <QmcAudioViewer v-else-if=\"vf.qmc\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- LRC lyrics (.lrc): self-fetch + sniff GBK → synced lyric list (own render/source toggle) -->\n <LrcViewer v-else-if=\"vf.lrc\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <LogViewer\n ref=\"logViewer\"\n v-else-if=\"vf.log\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n />\n <ModelViewer v-else-if=\"vf.model\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- PSD: dedicated viewer with a live layer tree (toggle visibility → re-composite); other raster kinds\n (TIFF/HEIC/RAW) use the generic decoding viewer. -->\n <PsdViewer\n v-else-if=\"vf.raster === 'psd'\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n />\n <RasterViewer\n v-else-if=\"vf.raster\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n :kind=\"vf.raster\"\n />\n <EbookViewer\n v-else-if=\"vf.ebook\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n :kind=\"vf.ebook\"\n />\n\n <!-- hex dump: offset + raw bytes + ASCII (the universal \"open with\" fall-back; never auto-detected) -->\n <HexViewer v-else-if=\"vf.hex\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- image (archive entry, or forced via \"open with\"): a zoomable inline picture -->\n <div v-else-if=\"vf.image\" class=\"fvb__imagezoom\">\n <ZoomImage :src=\"vf.url ?? ''\" :alt=\"vf.name\" />\n </div>\n\n <!-- video (archive entry): native player from the blob URL -->\n <div v-else-if=\"vf.video\" class=\"fvb__video\">\n <video class=\"fvb__videoel\" :src=\"vf.url ?? ''\" controls preload=\"metadata\" />\n </div>\n\n <!-- unpreviewable binary (archive entry / unmatched file): download card + \"open with\" formatter chips -->\n <div v-else-if=\"vf.binary\" class=\"fvb__binary\">\n <Icon name=\"archive\" :size=\"34\" />\n <p>无法预览的二进制文件</p>\n <p v-if=\"vf.size\" class=\"fvb__binary-sub\">{{ formatBytes(vf.size) }}</p>\n <a class=\"fvb__dlbtn\" :href=\"vf.url\" :download=\"vf.name\">下载</a>\n <div class=\"fvb__openas\">\n <span class=\"fvb__openas-label\">用查看器打开:</span>\n <div class=\"fvb__openas-grid\">\n <button\n v-for=\"f in FORMATTERS\"\n :key=\"f.key\"\n type=\"button\"\n class=\"fvb__fmtchip\"\n @click=\"pick(f)\"\n >\n <Icon :name=\"f.icon\" :size=\"13\" /><span>{{ f.label }}</span>\n </button>\n </div>\n </div>\n </div>\n\n <p v-else-if=\"status === 'loading'\" class=\"fvb__msg\">加载文件中…</p>\n <div v-else-if=\"status === 'error'\" class=\"fvb__msg fvb__msg--error\">\n <p>加载失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"fvb__retry\" @click=\"load(true)\">重试</button>\n </div>\n\n <!-- markdown render: frontmatter card (if any) + rendered document -->\n <div\n v-else-if=\"vf.lang === 'markdown' && mode === 'render'\"\n class=\"fvb__scroll fvb__scroll--doc\"\n >\n <div class=\"fvb__markdown\">\n <FrontmatterCard v-if=\"hasFrontmatter\" :data=\"frontmatter.data\" />\n <MarkdownText :text=\"frontmatter.body\" kp=\"fv\" />\n </div>\n </div>\n\n <!-- html render: sandboxed iframe fed by a blob. Default INERT (no scripts); the ▶ button opts into a\n HARDENED interactive sandbox (allow-scripts, opaque origin, CDN + network OK), and ⛶ opens the same\n sandboxed frame as a full-screen in-app overlay (no top-level/new-tab path). -->\n <div v-else-if=\"vf.html && mode === 'render'\" class=\"fvb__html\">\n <div class=\"fvb__htmlbar\">\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n :class=\"{ 'fvb__htmlbtn--on': interactive }\"\n :title=\"\n interactive\n ? '交互脚本已启用(点击禁用)—— 隔离沙箱内运行:可加载外部 CDN 并发起网络请求(fetch/XHR/WebSocket),但无法访问本站会话 / cookie(不透明源隔离)'\n : '启用交互脚本(隔离沙箱执行:可加载 CDN、可联网,但无法访问本站会话 / cookie;建议先切到源码视图审查再启用)'\n \"\n @click=\"toggleInteractive\"\n >\n <Icon name=\"play\" :size=\"13\" />\n </button>\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n title=\"全屏预览(在应用内放大查看;可在全屏内开启交互脚本)\"\n @click=\"fullscreen = true\"\n >\n <Icon name=\"maximize\" :size=\"13\" />\n </button>\n </div>\n <!-- :key forces a full remount when toggling interactive: a `sandbox` change on an existing iframe does\n NOT re-apply to the already-loaded document, so the new doc must load on a fresh element that already\n carries `allow-scripts` (set before the element is inserted, i.e. before navigation commits). -->\n <iframe\n :key=\"interactive ? 'interactive' : 'inert'\"\n class=\"fvb__frame\"\n :src=\"frameBlobUrl\"\n title=\"HTML 预览\"\n :sandbox=\"interactive ? 'allow-scripts' : ''\"\n loading=\"lazy\"\n referrerpolicy=\"no-referrer\"\n />\n\n <!-- In-app full-screen overlay: the SAME sandboxed frame at viewport size (an iframe → opaque origin,\n never a top-level same-origin document). Esc / ✕ close it; the toolbar keeps the ▶ interactive\n toggle so scripts can be enabled while full-screen. Teleported to <body> to clear stacking contexts. -->\n <Teleport to=\"body\">\n <div v-if=\"fullscreen\" class=\"fvb__fs\">\n <div class=\"fvb__fsbar\">\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n :class=\"{ 'fvb__htmlbtn--on': interactive }\"\n :title=\"\n interactive\n ? '交互脚本已启用(点击禁用)—— 隔离沙箱内运行,可联网但访问不到本站会话 / cookie'\n : '启用交互脚本(隔离沙箱执行:可加载 CDN、可联网,访问不到本站会话 / cookie)'\n \"\n @click=\"toggleInteractive\"\n >\n <Icon name=\"play\" :size=\"15\" />\n </button>\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n title=\"关闭全屏 (Esc)\"\n @click=\"fullscreen = false\"\n >\n <Icon name=\"close\" :size=\"15\" :stroke-width=\"2.2\" />\n </button>\n </div>\n <iframe\n :key=\"interactive ? 'fs-interactive' : 'fs-inert'\"\n class=\"fvb__fsframe\"\n :src=\"frameBlobUrl\"\n title=\"HTML 全屏预览\"\n :sandbox=\"interactive ? 'allow-scripts' : ''\"\n referrerpolicy=\"no-referrer\"\n />\n </div>\n </Teleport>\n </div>\n\n <!-- svg render: the markup as a picture (data-URL <img>) -->\n <div v-else-if=\"vf.svg && mode === 'render'\" class=\"fvb__svg\">\n <img class=\"fvb__svgimg\" :src=\"svgSrc\" :alt=\"vf.name\" />\n </div>\n\n <!-- mind-map render: the OPML/FreeMind XML drawn as an interactive SVG tree -->\n <div v-else-if=\"isMindMap && mode === 'render'\" class=\"fvb__fill\">\n <MindMapView :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- subtitle render: .srt/.vtt/.ass parsed into a timecoded transcript -->\n <div v-else-if=\"vf.subtitle && mode === 'render'\" class=\"fvb__fill\">\n <SubtitleViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- geo render: .geojson/.kml/.gpx drawn as an offline SVG map -->\n <div v-else-if=\"vf.geo && mode === 'render'\" class=\"fvb__fill\">\n <GeoViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- csv render: .csv/.tsv parsed into a table -->\n <div v-else-if=\"vf.csv && mode === 'render'\" class=\"fvb__fill\">\n <CsvViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- jsonl render: .jsonl/.ndjson parsed into a record list (truncated rows, click to expand) -->\n <div v-else-if=\"vf.jsonl && mode === 'render'\" class=\"fvb__fill\">\n <JsonlViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- diagram render: .drawio/.excalidraw drawn as an SVG (source toggle shows the raw XML/JSON) -->\n <div v-else-if=\"vf.diagram && mode === 'render'\" class=\"fvb__fill\">\n <DrawioViewer :text=\"text\" :name=\"vf.name\" :kind=\"vf.diagram\" />\n </div>\n\n <!-- source: everything else, or markdown/html/svg/… viewed as source — highlighted + folding + format -->\n <CodeView v-else :code=\"sourceText\" :lang=\"vf.lang ?? ''\" :name=\"vf.name\" />\n </div>\n</template>\n\n<style scoped>\n/* Fills the area below the host header/bar; each branch is itself a flex child that grows. */\n.fvb {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n/* Forced-format switcher bar (shown when the user opened an unhandled file via the \"open with\" chooser). */\n.fvb__fmtbar {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px;\n flex-shrink: 0;\n padding: 7px 12px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.fvb__fmtbar-label {\n margin-right: 2px;\n font-size: 11.5px;\n color: var(--faint);\n}\n/* Chip used both in the switcher bar and on the unpreviewable-binary card's \"open with\" row. */\n.fvb__fmtchip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 999px;\n padding: 5px 11px;\n cursor: pointer;\n transition:\n background 0.15s,\n border-color 0.15s,\n color 0.15s;\n}\n.fvb__fmtchip:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.fvb__fmtchip--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.fvb__fmtchip--on:hover {\n color: #fff;\n}\n\n.fvb__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.fvb__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n}\n.fvb__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.fvb__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.fvb__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overscroll-behavior: contain;\n background: var(--code-bg);\n}\n.fvb__scroll--doc {\n overflow: auto;\n}\n.fvb__markdown {\n padding: 18px 24px 80px;\n}\n\n/* HTML render: the iframe fills the area; a small overlay toolbar (top-right) holds the open-in-new-tab\n * button. White canvas so documents that don't set their own background read as a normal page. */\n.fvb__html {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n background: #fff;\n}\n.fvb__htmlbar {\n position: absolute;\n top: 8px;\n right: 8px;\n z-index: 2;\n display: flex;\n align-items: center;\n gap: 4px;\n}\n.fvb__htmlbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n text-decoration: none;\n cursor: pointer;\n box-shadow: var(--shadow);\n}\n.fvb__htmlbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n/* Interactive scripts enabled: accent the toggle so the elevated (but sandboxed) state is obvious. */\n.fvb__htmlbtn--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.fvb__htmlbtn--on:hover {\n color: #fff;\n filter: brightness(1.06);\n}\n.fvb__frame {\n display: block;\n width: 100%;\n height: 100%;\n border: none;\n background: #fff;\n}\n\n/* In-app full-screen HTML overlay (Teleport to body) — same sandboxed frame at viewport size. */\n.fvb__fs {\n position: fixed;\n inset: 0;\n z-index: 2000;\n display: flex;\n flex-direction: column;\n background: rgba(0, 0, 0, 0.82);\n animation: fvb-fs-in 0.12s ease-out;\n}\n@keyframes fvb-fs-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n.fvb__fsbar {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 6px;\n padding: 8px 10px;\n}\n.fvb__fsframe {\n flex: 1 1 auto;\n width: 100%;\n min-height: 0;\n border: none;\n background: #fff;\n}\n\n/* Generic \"viewer owns the area below\" host (mind map / subtitle / geo / csv). */\n.fvb__fill {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n}\n\n.fvb__svg {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n background: var(--surface-2);\n}\n.fvb__svgimg {\n max-width: 100%;\n height: auto;\n object-fit: contain;\n}\n\n.fvb__imagezoom {\n width: 100%;\n min-width: 0;\n flex: 1 1 auto;\n min-height: 0;\n position: relative;\n overflow: hidden;\n background: var(--code-bg);\n}\n\n.fvb__video {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n background: #000;\n}\n.fvb__videoel {\n max-width: 100%;\n max-height: 100%;\n}\n\n.fvb__binary {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 32px;\n color: var(--faint);\n text-align: center;\n}\n.fvb__binary p {\n margin: 0;\n font-size: 13px;\n color: var(--muted);\n}\n.fvb__binary-sub {\n font-size: 11.5px;\n color: var(--faint) !important;\n}\n.fvb__dlbtn {\n margin-top: 8px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 6px 16px;\n text-decoration: none;\n cursor: pointer;\n}\n.fvb__dlbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* \"Open with\" formatter chooser on the unpreviewable-binary card. */\n.fvb__openas {\n margin-top: 18px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 9px;\n}\n.fvb__openas-label {\n font-size: 12px;\n color: var(--muted);\n}\n.fvb__openas-grid {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n gap: 7px;\n max-width: 320px;\n}\n</style>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"FileViewerBody.js","names":[],"sources":["../../src/components/FileViewerBody.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, defineAsyncComponent, onBeforeUnmount, ref, useTemplateRef, watch } from 'vue'\nimport { optionalViewer } from '@/components/optionalViewer'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { useTeleportTheme } from '@/composables/useTeleportTheme'\nimport { skipsTextLoad } from '@/lib/viewerKind'\nimport {\n VIEWER_FORMATTERS as FORMATTERS,\n applyFormatter,\n findFormatter,\n type ViewerFormatter,\n} from '@/lib/viewerFormats'\nimport { fileExt } from '@/lib/fileType'\nimport { looksLikeMindMap } from '@/lib/mindmap'\nimport { parseFrontmatter } from '@/lib/frontmatter'\nimport { formatBytes } from '@/lib/format'\nimport { ansiPlainText } from '@/lib/ansi'\nimport CodeView from '@/components/CodeView.vue'\nimport MarkdownText from '@/components/MarkdownText'\nimport FrontmatterCard from '@/components/FrontmatterCard.vue'\nimport ZoomImage from '@/components/ZoomImage.vue'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * The side viewer's CONTENT area — the type dispatch shared by {@link FileViewerPanel} (a top-level file) and\n * the archive browsers (a file inside a zip/7z/tar/rar, extracted to a blob URL). Given a {@link ViewerFile}\n * it renders the right thing: a self-fetching sub-viewer (docx/pdf/dicom/…), an inline image/video, or — for\n * the text-backed kinds — markdown / html / svg / mind-map / subtitle / geo / csv (rendered), or highlighted,\n * foldable source. It fetches its own text for the text-backed kinds and emits it via `loaded` so the host's\n * copy button can use it. The host owns the header/bar, the render↔source `mode`, and copy/download.\n *\n * Extracted from FileViewerPanel so an archive entry previews with the EXACT same viewers as a top-level file\n * (the old archive detail view only did text/image/office/dicom). The archive browsers import this lazily to\n * break the import cycle (this component lazy-imports them right back for nested archives).\n *\n * \"Open with\" override: ANY file can be force-opened as a different formatter (hex / text / markdown / html /\n * svg / image / audio / video — {@link VIEWER_FORMATTERS}), since auto-detection by extension/mime can be\n * wrong. The choice becomes an EFFECTIVE file ({@link vf}) that re-runs the dispatch. Two entry points drive\n * the SAME override: (1) the host's header menu (FileViewerPanel) calls the exposed `pickFormat`/`resetFormat`\n * — the primary, universal entry; (2) the unpreviewable-binary card's own chip row, for the archive-browser\n * context where there is no host header. The effective file is emitted via `formatchange` (so the host tracks\n * toggle/copy visibility) and the active key via `formatpick` (so the host menu highlights it). `hostChooser`\n * hides the in-body switcher bar when the host already shows the chooser in its header (no duplicate UI).\n */\nconst props = withDefaults(\n defineProps<{\n file: ViewerFile\n mode?: 'render' | 'source'\n hostChooser?: boolean\n /**\n * Render-only HTML whose archive-relative dependencies have already been rewritten. Source/copy continue to\n * use `file.content` (or fetched text), so opening an HTML entry inside a ZIP still shows its original source.\n */\n htmlPreview?: string\n }>(),\n { mode: 'render', hostChooser: false },\n)\nconst emit = defineEmits<{\n loaded: [text: string]\n formatchange: [file: ViewerFile]\n formatpick: [key: string]\n}>()\n\n// Archives import this body; lazy-import them back so the module cycle never evaluates eagerly.\nconst ZipBrowser = defineAsyncComponent(() => import('@/components/ZipBrowser.vue'))\nconst SevenZipBrowser = defineAsyncComponent(() => import('@/components/SevenZipBrowser.vue'))\nconst ArchiveBrowser = defineAsyncComponent(() => import('@/components/ArchiveBrowser.vue'))\nconst BinObjBrowser = defineAsyncComponent(() => import('@/components/BinObjBrowser.vue'))\nconst EplViewer = defineAsyncComponent(() => import('@/components/EplViewer.vue'))\nconst ClassViewer = defineAsyncComponent(() => import('@/components/ClassViewer.vue'))\nconst HprofViewer = defineAsyncComponent(() => import('@/components/HprofViewer.vue'))\nconst MsiViewer = defineAsyncComponent(() => import('@/components/MsiViewer.vue'))\nconst AppxViewer = defineAsyncComponent(() => import('@/components/AppxViewer.vue'))\n// Heavy viewers whose engine is a STATIC optional peer dep: wrap so a missing dep shows an install card.\nconst DicomViewer = optionalViewer(\n () => import('@/components/DicomViewer.vue'),\n 'dicom-parser',\n 'DICOM 影像',\n)\nconst DocxViewer = optionalViewer(\n () => import('@/components/DocxViewer.vue'),\n 'docx-preview',\n 'Word 文档',\n)\nconst XlsxViewer = optionalViewer(\n () => import('@/components/XlsxViewer.vue'),\n ['exceljs', 'numfmt'],\n 'Excel 表格',\n)\nconst PptxViewer = optionalViewer(\n () => import('@/components/PptxViewer.vue'),\n ['@aiden0z/pptx-renderer', 'pdfjs-dist'],\n 'PowerPoint',\n)\nconst RtfViewer = defineAsyncComponent(() => import('@/components/RtfViewer.vue'))\nconst PdfViewer = optionalViewer(() => import('@/components/PdfViewer.vue'), 'pdfjs-dist', 'PDF')\nconst MindMapView = defineAsyncComponent(() => import('@/components/MindMapView.vue'))\nconst XmindViewer = defineAsyncComponent(() => import('@/components/XmindViewer.vue'))\nconst EmlViewer = defineAsyncComponent(() => import('@/components/EmlViewer.vue'))\nconst MhtViewer = defineAsyncComponent(() => import('@/components/MhtViewer.vue'))\nconst HarViewer = defineAsyncComponent(() => import('@/components/HarViewer.vue'))\nconst SqliteViewer = optionalViewer(\n () => import('@/components/SqliteViewer.vue'),\n 'sql.js',\n 'SQLite 数据库',\n)\nconst SubtitleViewer = defineAsyncComponent(() => import('@/components/SubtitleViewer.vue'))\nconst FontViewer = defineAsyncComponent(() => import('@/components/FontViewer.vue'))\nconst NotebookViewer = defineAsyncComponent(() => import('@/components/NotebookViewer.vue'))\nconst GeoViewer = defineAsyncComponent(() => import('@/components/GeoViewer.vue'))\nconst CsvViewer = defineAsyncComponent(() => import('@/components/CsvViewer.vue'))\nconst JsonlViewer = defineAsyncComponent(() => import('@/components/JsonlViewer.vue'))\nconst CertViewer = defineAsyncComponent(() => import('@/components/CertViewer.vue'))\nconst JksViewer = defineAsyncComponent(() => import('@/components/JksViewer.vue'))\nconst AudioViewer = defineAsyncComponent(() => import('@/components/AudioViewer.vue'))\nconst QmcAudioViewer = defineAsyncComponent(() => import('@/components/QmcAudioViewer.vue'))\nconst LrcViewer = defineAsyncComponent(() => import('@/components/LrcViewer.vue'))\nconst LogViewer = defineAsyncComponent(() => import('@/components/LogViewer.vue'))\nconst ModelViewer = optionalViewer(() => import('@/components/ModelViewer.vue'), 'three', '3D 模型')\nconst RasterViewer = defineAsyncComponent(() => import('@/components/RasterViewer.vue'))\nconst PsdViewer = defineAsyncComponent(() => import('@/components/PsdViewer.vue'))\nconst EbookViewer = defineAsyncComponent(() => import('@/components/EbookViewer.vue'))\nconst DrawioViewer = defineAsyncComponent(() => import('@/components/DrawioViewer.vue'))\nconst DxfViewer = defineAsyncComponent(() => import('@/components/DxfViewer.vue'))\nconst HexViewer = defineAsyncComponent(() => import('@/components/HexViewer.vue'))\nconst logViewerRef = useTemplateRef<{ copyText: () => string }>('logViewer')\n\n// ── \"open with\" override ──────────────────────────────────────────────────────────────────────\n// A user-chosen formatter ({@link VIEWER_FORMATTERS}). When set it replaces props.file as the EFFECTIVE file\n// the dispatch (and load) use; cleared whenever a different file is opened. Driven by the host header menu\n// (via the exposed pickFormat/resetFormat) and/or the in-body binary-card chips — both funnel through `pick`.\nconst pickedKey = ref('')\nconst picked = ref<ViewerFile | null>(null)\n// The effective file: the user's chosen formatter, else the file passed in.\nconst vf = computed<ViewerFile>(() => picked.value ?? props.file)\n\nfunction pick(f: ViewerFormatter): void {\n pickedKey.value = f.key\n picked.value = applyFormatter(props.file, f)\n}\n// Host-driven entry points (FileViewerPanel's header menu): pick by key, or fall back to auto-detection.\nfunction pickFormat(key: string): void {\n const f = findFormatter(key)\n if (f) pick(f)\n}\nfunction resetFormat(): void {\n pickedKey.value = ''\n picked.value = null\n}\n// Announce the active formatter so the host header can highlight it (fires on pick, reset, and file change).\nwatch(pickedKey, (k) => emit('formatpick', k))\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst text = ref('')\nconst errorMsg = ref('')\n\n// Mind map only renders the diagram once the loaded text actually looks like one (a non-mind-map `.mm` reads\n// as plain source). Same content sniff the panel used.\nconst isMindMap = computed(\n () =>\n vf.value.mindmap === true &&\n status.value === 'ready' &&\n looksLikeMindMap(text.value, fileExt(vf.value.name)),\n)\n\n// Markdown render: lift any leading YAML frontmatter into a card; the source view still shows the raw file.\nconst frontmatter = computed(() =>\n vf.value.lang === 'markdown' ? parseFrontmatter(text.value) : { data: {}, body: text.value },\n)\nconst hasFrontmatter = computed(() => Object.keys(frontmatter.value.data).length > 0)\n\n// Drop a single trailing newline so the source view doesn't show a phantom last line.\nconst sourceText = computed(() => text.value.replace(/\\n$/, ''))\n\n// SVG render: rasterize the markup via a data-URL <img> (an <img> never runs SVG scripts).\nconst svgSrc = computed(() =>\n text.value ? `data:image/svg+xml,${encodeURIComponent(text.value)}` : '',\n)\n\n// HTML preview, two modes (default = inert):\n// - INERT (default): the iframe carries an empty `sandbox` (no `allow-scripts`) plus an injected\n// `script-src 'none'` CSP, so NO upstream script runs — only layout/CSS/images render.\n// - INTERACTIVE (opt-in via the toolbar): scripts run inside a HARDENED sandbox — `allow-scripts` WITHOUT\n// `allow-same-origin` gives the frame an opaque origin (no access to our cookies/storage/session/DOM); that\n// opaque origin is the REAL isolation. The CSP allows external sub-resources AND network so CDN-based pages\n// fully work — scripts/styles/fonts/images from unpkg/jsdelivr/Google Fonts, plus fetch/XHR/WebSocket and\n// source-map loads (`connect-src` open to https/http/ws/wss). Only the structural hardening stays:\n// `object-src 'none'` (no plugins) and `base-uri 'none'` (no <base> hijack); forms are already blocked by\n// the sandbox (no `allow-forms`). Because the origin is opaque, even with network open the page can't reach\n// our session/cookies — it acts only with its own (already-visible) content. Review the source first\n// (render/source toggle), then enable. The ⛶ button opens this SAME sandboxed frame as a full-screen\n// in-app overlay (still an iframe → still opaque-origin), so a big interactive view stays safe — there's\n// no top-level / new-tab path that could ever run scripts in our origin.\nconst INERT_CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'\">`\nconst INTERACTIVE_CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"default-src 'unsafe-inline' 'unsafe-eval' data: blob: https: http:; ` +\n `connect-src data: blob: https: http: ws: wss:; object-src 'none'; base-uri 'none'\">`\n\nfunction injectCsp(html: string, csp: string): string {\n const head = html.match(/<head[^>]*>/i)\n if (head) {\n const at = (head.index ?? 0) + head[0].length\n return html.slice(0, at) + csp + html.slice(at)\n }\n return `<!doctype html><html><head><meta charset=\"utf-8\">${csp}</head><body>${html}</body></html>`\n}\n\nconst inertBlobUrl = ref('') // the inert (non-interactive) iframe + full-screen overlay\nconst interactiveBlobUrl = ref('') // built only when the user opts in\nconst interactive = ref(false)\nconst fullscreen = ref(false) // in-app full-screen overlay (Teleport to body) over the SAME sandboxed frame\nconst viewerElement = useTemplateRef<HTMLElement>('viewerElement')\nconst fullscreenTheme = useTeleportTheme(viewerElement, fullscreen)\nconst frameBlobUrl = computed(() =>\n interactive.value ? interactiveBlobUrl.value : inertBlobUrl.value,\n)\nconst previewHtml = computed(() => props.htmlPreview ?? text.value)\n\nfunction revokeHtmlBlobs(): void {\n if (inertBlobUrl.value) {\n URL.revokeObjectURL(inertBlobUrl.value)\n inertBlobUrl.value = ''\n }\n if (interactiveBlobUrl.value) {\n URL.revokeObjectURL(interactiveBlobUrl.value)\n interactiveBlobUrl.value = ''\n }\n}\n\nwatch([() => vf.value.html, previewHtml], ([html, preview]) => {\n revokeHtmlBlobs()\n interactive.value = false // a new file / changed text always returns to the safe inert default\n fullscreen.value = false // …and closes any open full-screen overlay\n if (html && typeof URL !== 'undefined' && URL.createObjectURL) {\n inertBlobUrl.value = URL.createObjectURL(\n new Blob([injectCsp(preview, INERT_CSP)], { type: 'text/html' }),\n )\n }\n})\n\n// Toggle the sandboxed iframe between inert and interactive (builds the interactive blob lazily on first use).\nfunction toggleInteractive(): void {\n if (interactive.value) {\n interactive.value = false\n return\n }\n if (typeof URL !== 'undefined' && URL.createObjectURL && !interactiveBlobUrl.value) {\n interactiveBlobUrl.value = URL.createObjectURL(\n new Blob([injectCsp(previewHtml.value, INTERACTIVE_CSP)], { type: 'text/html' }),\n )\n }\n interactive.value = true\n}\n\n// Full-screen overlay key handling + body scroll lock — only while open, SSR-guarded (no window in smoke).\nfunction onFsKey(e: KeyboardEvent): void {\n if (e.key === 'Escape') fullscreen.value = false\n}\nwatch(fullscreen, (on) => {\n if (typeof window === 'undefined') return\n if (on) {\n window.addEventListener('keydown', onFsKey)\n document.body.style.overflow = 'hidden'\n } else {\n window.removeEventListener('keydown', onFsKey)\n document.body.style.overflow = ''\n }\n})\n\nasync function load(noStore = false): Promise<void> {\n const f = vf.value\n // Self-fetching sub-viewers + image/video/binary render straight from the URL — no text to load.\n if (skipsTextLoad(f)) {\n text.value = ''\n status.value = 'ready'\n return\n }\n if (f.content != null) {\n text.value = f.content\n status.value = 'ready'\n emit('loaded', ansiPlainText(f.content))\n return\n }\n if (!f.url) {\n text.value = ''\n status.value = 'ready'\n return\n }\n status.value = 'loading'\n errorMsg.value = ''\n try {\n const res = await fetch(f.url, noStore ? { cache: 'no-store' } : {})\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const t = await res.text()\n text.value = t\n status.value = 'ready'\n emit('loaded', ansiPlainText(t))\n } catch (e) {\n errorMsg.value = String(e)\n status.value = 'error'\n }\n}\n\n// A new file resets any \"open with\" override (registered BEFORE the vf watcher so picked is cleared before the\n// effective file is re-read below).\nwatch(\n () => props.file,\n () => {\n picked.value = null\n pickedKey.value = ''\n },\n)\n// Load + announce the effective file whenever it changes (a new file, or a picked formatter).\nwatch(\n vf,\n (v) => {\n emit('formatchange', v)\n void load()\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => {\n revokeHtmlBlobs()\n if (typeof window !== 'undefined') {\n window.removeEventListener('keydown', onFsKey)\n document.body.style.overflow = ''\n }\n})\n\n// The host's refresh button re-fetches (cache-busting) through here; its header menu drives the \"open with\"\n// override through pickFormat / resetFormat (see the override block above).\ndefineExpose({\n reload: () => load(true),\n pickFormat,\n resetFormat,\n copyText: () =>\n vf.value.log ? (logViewerRef.value?.copyText() ?? '') : ansiPlainText(text.value),\n})\n</script>\n\n<template>\n <div ref=\"viewerElement\" class=\"fvb\">\n <!-- forced-format switcher: shown once the user picks a formatter, so they can swap. Hidden when the host\n provides the chooser in its header (FileViewerPanel) — only the archive browsers render it in-body. -->\n <div v-if=\"picked && !hostChooser\" class=\"fvb__fmtbar\">\n <span class=\"fvb__fmtbar-label\">格式</span>\n <button\n v-for=\"f in FORMATTERS\"\n :key=\"f.key\"\n type=\"button\"\n class=\"fvb__fmtchip\"\n :class=\"{ 'fvb__fmtchip--on': pickedKey === f.key }\"\n @click=\"pick(f)\"\n >\n <Icon :name=\"f.icon\" :size=\"13\" /><span>{{ f.label }}</span>\n </button>\n </div>\n\n <!-- zip / 7z / tar-rar: the archive browsers fetch the bytes, decompress in-browser, and browse entries -->\n <ZipBrowser v-if=\"vf.zip\" :url=\"vf.url ?? ''\" :name=\"vf.name\" />\n <SevenZipBrowser v-else-if=\"vf.sevenZip\" :url=\"vf.url ?? ''\" :name=\"vf.name\" />\n <ArchiveBrowser v-else-if=\"vf.archive\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :kind=\"vf.archive\" />\n\n <!-- native binary object (.so/.dll): parse + list symbols -->\n <BinObjBrowser v-else-if=\"vf.binobj\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- e-language (.e/.ec): parse the binary section format + decompile each method to 易语言 source -->\n <EplViewer v-else-if=\"vf.epl\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- java bytecode (.class): parse the structural tables + list fields/methods (no decompilation) -->\n <ClassViewer v-else-if=\"vf.javaClass\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- jvm heap dump (.hprof): stream-parse → class histogram + per-class object drill-in (paginated/filterable) -->\n <HprofViewer v-else-if=\"vf.hprof\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- windows installer (.msi): parse the OLE compound file → product identity + summary info -->\n <MsiViewer v-else-if=\"vf.msi\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- windows app package (.appx/.msix): unzip the manifest → identity / deps / capabilities -->\n <AppxViewer v-else-if=\"vf.appx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- xmind workbook (.xmind): unzip content.json/xml → render as an interactive mind map -->\n <XmindViewer v-else-if=\"vf.xmind\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- dxf (.dxf): parse the ASCII CAD drawing → SVG with pan/zoom + layer toggles -->\n <DxfViewer v-else-if=\"vf.dxf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <DicomViewer v-else-if=\"vf.dicom\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <DocxViewer v-else-if=\"vf.docx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <XlsxViewer v-else-if=\"vf.xlsx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <PptxViewer v-else-if=\"vf.pptx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <RtfViewer v-else-if=\"vf.rtf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <PdfViewer v-else-if=\"vf.pdf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <EmlViewer v-else-if=\"vf.eml\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <MhtViewer v-else-if=\"vf.mht\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <HarViewer v-else-if=\"vf.har\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <SqliteViewer v-else-if=\"vf.sqlite\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <CertViewer v-else-if=\"vf.cert\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- java keystore (.jks/.keystore/.jceks): parse the inventory + certs; password decrypts private keys -->\n <JksViewer v-else-if=\"vf.jks\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <FontViewer v-else-if=\"vf.font\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <NotebookViewer v-else-if=\"vf.ipynb\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <AudioViewer v-else-if=\"vf.audio\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- QQ Music encrypted audio (.mflac/.mgg): decrypt in-browser → play + download decrypted -->\n <QmcAudioViewer v-else-if=\"vf.qmc\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- LRC lyrics (.lrc): self-fetch + sniff GBK → synced lyric list (own render/source toggle) -->\n <LrcViewer v-else-if=\"vf.lrc\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <LogViewer\n ref=\"logViewer\"\n v-else-if=\"vf.log\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n />\n <ModelViewer\n v-else-if=\"vf.model\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n :format=\"vf.modelFormat\"\n />\n <!-- PSD: dedicated viewer with a live layer tree (toggle visibility → re-composite); other raster kinds\n (TIFF/HEIC/RAW) use the generic decoding viewer. -->\n <PsdViewer\n v-else-if=\"vf.raster === 'psd'\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n />\n <RasterViewer\n v-else-if=\"vf.raster\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n :kind=\"vf.raster\"\n />\n <EbookViewer\n v-else-if=\"vf.ebook\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n :kind=\"vf.ebook\"\n />\n\n <!-- hex dump: offset + raw bytes + ASCII (the universal \"open with\" fall-back; never auto-detected) -->\n <HexViewer v-else-if=\"vf.hex\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- image (archive entry, or forced via \"open with\"): a zoomable inline picture -->\n <div v-else-if=\"vf.image\" class=\"fvb__imagezoom\">\n <ZoomImage :src=\"vf.url ?? ''\" :alt=\"vf.name\" />\n </div>\n\n <!-- video (archive entry): native player from the blob URL -->\n <div v-else-if=\"vf.video\" class=\"fvb__video\">\n <video class=\"fvb__videoel\" :src=\"vf.url ?? ''\" controls preload=\"metadata\" />\n </div>\n\n <!-- unpreviewable binary (archive entry / unmatched file): download card + \"open with\" formatter chips -->\n <div v-else-if=\"vf.binary\" class=\"fvb__binary\">\n <Icon name=\"archive\" :size=\"34\" />\n <p>无法预览的二进制文件</p>\n <p v-if=\"vf.size\" class=\"fvb__binary-sub\">{{ formatBytes(vf.size) }}</p>\n <a class=\"fvb__dlbtn\" :href=\"vf.url\" :download=\"vf.name\">下载</a>\n <div class=\"fvb__openas\">\n <span class=\"fvb__openas-label\">用查看器打开:</span>\n <div class=\"fvb__openas-grid\">\n <button\n v-for=\"f in FORMATTERS\"\n :key=\"f.key\"\n type=\"button\"\n class=\"fvb__fmtchip\"\n @click=\"pick(f)\"\n >\n <Icon :name=\"f.icon\" :size=\"13\" /><span>{{ f.label }}</span>\n </button>\n </div>\n </div>\n </div>\n\n <p v-else-if=\"status === 'loading'\" class=\"fvb__msg\">加载文件中…</p>\n <div v-else-if=\"status === 'error'\" class=\"fvb__msg fvb__msg--error\">\n <p>加载失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"fvb__retry\" @click=\"load(true)\">重试</button>\n </div>\n\n <!-- markdown render: frontmatter card (if any) + rendered document -->\n <div\n v-else-if=\"vf.lang === 'markdown' && mode === 'render'\"\n class=\"fvb__scroll fvb__scroll--doc\"\n >\n <div class=\"fvb__markdown\">\n <FrontmatterCard v-if=\"hasFrontmatter\" :data=\"frontmatter.data\" />\n <MarkdownText :text=\"frontmatter.body\" kp=\"fv\" />\n </div>\n </div>\n\n <!-- html render: sandboxed iframe fed by a blob. Default INERT (no scripts); the ▶ button opts into a\n HARDENED interactive sandbox (allow-scripts, opaque origin, CDN + network OK), and ⛶ opens the same\n sandboxed frame as a full-screen in-app overlay (no top-level/new-tab path). -->\n <div v-else-if=\"vf.html && mode === 'render'\" class=\"fvb__html\">\n <div class=\"fvb__htmlbar\">\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n :class=\"{ 'fvb__htmlbtn--on': interactive }\"\n :title=\"\n interactive\n ? '交互脚本已启用(点击禁用)—— 隔离沙箱内运行:可加载外部 CDN 并发起网络请求(fetch/XHR/WebSocket),但无法访问本站会话 / cookie(不透明源隔离)'\n : '启用交互脚本(隔离沙箱执行:可加载 CDN、可联网,但无法访问本站会话 / cookie;建议先切到源码视图审查再启用)'\n \"\n @click=\"toggleInteractive\"\n >\n <Icon name=\"play\" :size=\"13\" />\n </button>\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n title=\"全屏预览(在应用内放大查看;可在全屏内开启交互脚本)\"\n @click=\"fullscreen = true\"\n >\n <Icon name=\"maximize\" :size=\"13\" />\n </button>\n </div>\n <!-- :key forces a full remount when toggling interactive: a `sandbox` change on an existing iframe does\n NOT re-apply to the already-loaded document, so the new doc must load on a fresh element that already\n carries `allow-scripts` (set before the element is inserted, i.e. before navigation commits). -->\n <iframe\n :key=\"interactive ? 'interactive' : 'inert'\"\n class=\"fvb__frame\"\n :src=\"frameBlobUrl\"\n title=\"HTML 预览\"\n :sandbox=\"interactive ? 'allow-scripts' : ''\"\n loading=\"lazy\"\n referrerpolicy=\"no-referrer\"\n />\n\n <!-- In-app full-screen overlay: the SAME sandboxed frame at viewport size (an iframe → opaque origin,\n never a top-level same-origin document). Esc / ✕ close it; the toolbar keeps the ▶ interactive\n toggle so scripts can be enabled while full-screen. Teleported to <body> to clear stacking contexts. -->\n <Teleport to=\"body\">\n <div v-if=\"fullscreen\" class=\"fv-root fvb__fs\" :style=\"fullscreenTheme\">\n <div class=\"fvb__fsbar\">\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n :class=\"{ 'fvb__htmlbtn--on': interactive }\"\n :title=\"\n interactive\n ? '交互脚本已启用(点击禁用)—— 隔离沙箱内运行,可联网但访问不到本站会话 / cookie'\n : '启用交互脚本(隔离沙箱执行:可加载 CDN、可联网,访问不到本站会话 / cookie)'\n \"\n @click=\"toggleInteractive\"\n >\n <Icon name=\"play\" :size=\"15\" />\n </button>\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n title=\"关闭全屏 (Esc)\"\n @click=\"fullscreen = false\"\n >\n <Icon name=\"close\" :size=\"15\" :stroke-width=\"2.2\" />\n </button>\n </div>\n <iframe\n :key=\"interactive ? 'fs-interactive' : 'fs-inert'\"\n class=\"fvb__fsframe\"\n :src=\"frameBlobUrl\"\n title=\"HTML 全屏预览\"\n :sandbox=\"interactive ? 'allow-scripts' : ''\"\n referrerpolicy=\"no-referrer\"\n />\n </div>\n </Teleport>\n </div>\n\n <!-- svg render: the markup as a picture (data-URL <img>) -->\n <div v-else-if=\"vf.svg && mode === 'render'\" class=\"fvb__svg\">\n <img class=\"fvb__svgimg\" :src=\"svgSrc\" :alt=\"vf.name\" />\n </div>\n\n <!-- mind-map render: the OPML/FreeMind XML drawn as an interactive SVG tree -->\n <div v-else-if=\"isMindMap && mode === 'render'\" class=\"fvb__fill\">\n <MindMapView :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- subtitle render: .srt/.vtt/.ass parsed into a timecoded transcript -->\n <div v-else-if=\"vf.subtitle && mode === 'render'\" class=\"fvb__fill\">\n <SubtitleViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- geo render: .geojson/.kml/.gpx drawn as an offline SVG map -->\n <div v-else-if=\"vf.geo && mode === 'render'\" class=\"fvb__fill\">\n <GeoViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- csv render: .csv/.tsv parsed into a table -->\n <div v-else-if=\"vf.csv && mode === 'render'\" class=\"fvb__fill\">\n <CsvViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- jsonl render: .jsonl/.ndjson parsed into a record list (truncated rows, click to expand) -->\n <div v-else-if=\"vf.jsonl && mode === 'render'\" class=\"fvb__fill\">\n <JsonlViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- diagram render: .drawio/.excalidraw drawn as an SVG (source toggle shows the raw XML/JSON) -->\n <div v-else-if=\"vf.diagram && mode === 'render'\" class=\"fvb__fill\">\n <DrawioViewer :text=\"text\" :name=\"vf.name\" :kind=\"vf.diagram\" />\n </div>\n\n <!-- source: everything else, or markdown/html/svg/… viewed as source — highlighted + folding + format -->\n <CodeView v-else :code=\"sourceText\" :lang=\"vf.lang ?? ''\" :name=\"vf.name\" />\n </div>\n</template>\n\n<style scoped>\n/* Fills the area below the host header/bar; each branch is itself a flex child that grows. */\n.fvb {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n/* Forced-format switcher bar (shown when the user opened an unhandled file via the \"open with\" chooser). */\n.fvb__fmtbar {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px;\n flex-shrink: 0;\n padding: 7px 12px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.fvb__fmtbar-label {\n margin-right: 2px;\n font-size: 11.5px;\n color: var(--faint);\n}\n/* Chip used both in the switcher bar and on the unpreviewable-binary card's \"open with\" row. */\n.fvb__fmtchip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 999px;\n padding: 5px 11px;\n cursor: pointer;\n transition:\n background 0.15s,\n border-color 0.15s,\n color 0.15s;\n}\n.fvb__fmtchip:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.fvb__fmtchip--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.fvb__fmtchip--on:hover {\n color: #fff;\n}\n\n.fvb__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.fvb__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n}\n.fvb__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.fvb__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.fvb__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overscroll-behavior: contain;\n background: var(--code-bg);\n}\n.fvb__scroll--doc {\n overflow: auto;\n}\n.fvb__markdown {\n padding: 18px 24px 80px;\n}\n\n/* HTML render: the iframe fills the area; a small overlay toolbar (top-right) holds the open-in-new-tab\n * button. White canvas so documents that don't set their own background read as a normal page. */\n.fvb__html {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n background: #fff;\n}\n.fvb__htmlbar {\n position: absolute;\n top: 8px;\n right: 8px;\n z-index: 2;\n display: flex;\n align-items: center;\n gap: 4px;\n}\n.fvb__htmlbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n text-decoration: none;\n cursor: pointer;\n box-shadow: var(--shadow);\n}\n.fvb__htmlbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n/* Interactive scripts enabled: accent the toggle so the elevated (but sandboxed) state is obvious. */\n.fvb__htmlbtn--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.fvb__htmlbtn--on:hover {\n color: #fff;\n filter: brightness(1.06);\n}\n.fvb__frame {\n display: block;\n width: 100%;\n height: 100%;\n border: none;\n background: #fff;\n}\n\n/* In-app full-screen HTML overlay (Teleport to body) — same sandboxed frame at viewport size. */\n.fvb__fs {\n position: fixed;\n inset: 0;\n z-index: 2000;\n display: flex;\n flex-direction: column;\n background: rgba(0, 0, 0, 0.82);\n animation: fvb-fs-in 0.12s ease-out;\n}\n@keyframes fvb-fs-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n.fvb__fsbar {\n background: var(--surface);\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 6px;\n padding: 8px 10px;\n}\n.fvb__fsframe {\n flex: 1 1 auto;\n width: 100%;\n min-height: 0;\n border: none;\n background: #fff;\n}\n\n/* Generic \"viewer owns the area below\" host (mind map / subtitle / geo / csv). */\n.fvb__fill {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n}\n\n.fvb__svg {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n background: var(--surface-2);\n}\n.fvb__svgimg {\n max-width: 100%;\n height: auto;\n object-fit: contain;\n}\n\n.fvb__imagezoom {\n width: 100%;\n min-width: 0;\n flex: 1 1 auto;\n min-height: 0;\n position: relative;\n overflow: hidden;\n background: var(--code-bg);\n}\n\n.fvb__video {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n background: #000;\n}\n.fvb__videoel {\n max-width: 100%;\n max-height: 100%;\n}\n\n.fvb__binary {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 32px;\n color: var(--faint);\n text-align: center;\n}\n.fvb__binary p {\n margin: 0;\n font-size: 13px;\n color: var(--muted);\n}\n.fvb__binary-sub {\n font-size: 11.5px;\n color: var(--faint) !important;\n}\n.fvb__dlbtn {\n margin-top: 8px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 6px 16px;\n text-decoration: none;\n cursor: pointer;\n}\n.fvb__dlbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* \"Open with\" formatter chooser on the unpreviewable-binary card. */\n.fvb__openas {\n margin-top: 18px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 9px;\n}\n.fvb__openas-label {\n font-size: 12px;\n color: var(--muted);\n}\n.fvb__openas-grid {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n gap: 7px;\n max-width: 320px;\n}\n</style>\n"],"mappings":""}
|