@l1yp/file-viewer 0.1.2 → 0.1.4

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.
Files changed (56) hide show
  1. package/dist/components/ArchiveBrowser.js +1 -1
  2. package/dist/components/ArchiveBrowser.js.map +1 -1
  3. package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js +105 -94
  4. package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -1
  5. package/dist/components/FileViewerBody.js +1 -1
  6. package/dist/components/FileViewerBody.js.map +1 -1
  7. package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js.map +1 -1
  8. package/dist/components/ImageLightbox.js +1 -1
  9. package/dist/components/ImageLightbox.js.map +1 -1
  10. package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js +101 -55
  11. package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js.map +1 -1
  12. package/dist/components/JsonlViewer.js +1 -1
  13. package/dist/components/JsonlViewer.js.map +1 -1
  14. package/dist/components/JsonlViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  15. package/dist/components/LightboxGalleryStrip.js +9 -0
  16. package/dist/components/LightboxGalleryStrip.js.map +1 -0
  17. package/dist/components/LightboxGalleryStrip.vue.d.ts +12 -0
  18. package/dist/components/LightboxGalleryStrip.vue_vue_type_script_setup_true_lang.js +60 -0
  19. package/dist/components/LightboxGalleryStrip.vue_vue_type_script_setup_true_lang.js.map +1 -0
  20. package/dist/components/PdfNavigationPane.js +9 -0
  21. package/dist/components/PdfNavigationPane.js.map +1 -0
  22. package/dist/components/PdfNavigationPane.vue.d.ts +24 -0
  23. package/dist/components/PdfNavigationPane.vue_vue_type_script_setup_true_lang.js +149 -0
  24. package/dist/components/PdfNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -0
  25. package/dist/components/PdfViewer.js +1 -1
  26. package/dist/components/PdfViewer.js.map +1 -1
  27. package/dist/components/PdfViewer.vue.d.ts +0 -11
  28. package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js +131 -69
  29. package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  30. package/dist/components/SevenZipBrowser.js +1 -1
  31. package/dist/components/SevenZipBrowser.js.map +1 -1
  32. package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js +157 -146
  33. package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -1
  34. package/dist/components/ZipBrowser.js +1 -1
  35. package/dist/components/ZipBrowser.js.map +1 -1
  36. package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js +70 -61
  37. package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -1
  38. package/dist/components/ZoomImage.js +1 -1
  39. package/dist/components/ZoomImage.js.map +1 -1
  40. package/dist/components/ZoomImage.vue.d.ts +2 -0
  41. package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js +104 -95
  42. package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js.map +1 -1
  43. package/dist/composables/useArchiveImageGallery.d.ts +15 -0
  44. package/dist/composables/useArchiveImageGallery.js +62 -0
  45. package/dist/composables/useArchiveImageGallery.js.map +1 -0
  46. package/dist/composables/useLightbox.d.ts +29 -21
  47. package/dist/composables/useLightbox.js +72 -12
  48. package/dist/composables/useLightbox.js.map +1 -1
  49. package/dist/lib/archiveImages.d.ts +14 -0
  50. package/dist/lib/archiveImages.js +15 -0
  51. package/dist/lib/archiveImages.js.map +1 -0
  52. package/dist/lib/pdfNavigation.d.ts +11 -0
  53. package/dist/lib/pdfNavigation.js +32 -0
  54. package/dist/lib/pdfNavigation.js.map +1 -0
  55. package/dist/style.css +1 -1
  56. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"SevenZipBrowser.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/SevenZipBrowser.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport {\n computed,\n defineAsyncComponent,\n nextTick,\n onBeforeUnmount,\n onMounted,\n reactive,\n ref,\n watch,\n} from 'vue'\nimport {\n extractSevenZipEntry,\n readSevenZip,\n SevenZipEncryptedError,\n SevenZipPasswordError,\n type SevenZipArchive,\n type SevenZipEntry,\n} from '@/lib/sevenZip'\nimport type { DicomSlice } from '@/lib/dicom'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { fileExt, isDicomFile, isTextFile } from '@/lib/fileType'\nimport { canToggleView, classifyViewerFile, mediaMimeForName } from '@/lib/viewerKind'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n// A folder of sibling .dcm files browses as one scrollable series — handled directly (the body has no slice model).\nconst DicomViewer = defineAsyncComponent(() => import('@/components/DicomViewer.vue'))\n// The shared side-viewer body renders a selected entry with the SAME viewers as a top-level file. Lazy to break\n// the module cycle (the body lazy-imports this browser back, for nested archives).\nconst FileViewerBody = defineAsyncComponent(() => import('@/components/FileViewerBody.vue'))\n\n/**\n * The side viewer's 7z mode — the LZMA counterpart to {@link ZipBrowser}. Fetches an archive's bytes, parses +\n * decompresses them entirely in-browser (see lib/sevenZip.ts, which carries its own LZMA decoder since browsers\n * ship none), and browses the contents. Selecting an entry extracts just that one, wraps it in a blob URL, and\n * hands it to {@link FileViewerBody} (so a file inside the 7z previews with every viewer a top-level file gets).\n */\nconst props = defineProps<{ url: string; name: string }>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nlet buffer: ArrayBuffer | null = null\nlet archive: SevenZipArchive | null = null\nconst entries = ref<SevenZipEntry[]>([])\n\n// Password state for AES-encrypted 7z. `locked` = the archive header itself is encrypted (encrypted file\n// names), so it can't even be listed until unlocked; `contentEncrypted` = the listing worked but file bytes\n// need a password; `unlocked` = a password has been accepted.\nconst password = ref('')\nconst pwError = ref('')\nconst locked = ref(false)\nconst unlocked = ref(false)\nconst contentEncrypted = ref(false)\n\ninterface TreeNode {\n name: string\n path: string\n isDir: boolean\n size: number\n entry?: SevenZipEntry\n children: TreeNode[]\n}\nconst root = ref<TreeNode>({ name: '', path: '', isDir: true, size: 0, children: [] })\nconst expanded = reactive(new Set<string>())\n\nconst fileCount = computed(() => entries.value.reduce((n, e) => n + (e.isDir ? 0 : 1), 0))\nconst totalSize = computed(() => entries.value.reduce((s, e) => s + (e.isDir ? 0 : e.size), 0))\n\n// Build a folder tree from the flat entry paths (directories may be implicit — derive them either way).\nfunction buildTree(list: SevenZipEntry[]): TreeNode {\n const rootNode: TreeNode = { name: '', path: '', isDir: true, size: 0, children: [] }\n const dirs = new Map<string, TreeNode>([['', rootNode]])\n const ensureDir = (segs: string[]): TreeNode => {\n let cur = rootNode\n let acc = ''\n for (const seg of segs) {\n acc = acc ? `${acc}/${seg}` : seg\n let node = dirs.get(acc)\n if (!node) {\n node = { name: seg, path: acc, isDir: true, size: 0, children: [] }\n dirs.set(acc, node)\n cur.children.push(node)\n }\n cur = node\n }\n return cur\n }\n for (const e of list) {\n const segs = e.path.split('/').filter(Boolean)\n if (e.isDir) {\n ensureDir(segs)\n continue\n }\n const fname = segs.pop()\n if (!fname) continue\n ensureDir(segs).children.push({\n name: fname,\n path: e.path,\n isDir: false,\n size: e.size,\n entry: e,\n children: [],\n })\n }\n sortNode(rootNode)\n return rootNode\n}\n// Folders first, then files, each alphabetical (case-insensitive).\nfunction sortNode(n: TreeNode): void {\n n.children.sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1))\n for (const c of n.children) if (c.isDir) sortNode(c)\n}\n\n// Flatten the tree to the rows currently visible (respecting which folders are expanded).\ninterface Row {\n node: TreeNode\n depth: number\n}\nconst rows = computed<Row[]>(() => {\n const out: Row[] = []\n const walk = (nodes: TreeNode[], depth: number): void => {\n for (const n of nodes) {\n out.push({ node: n, depth })\n if (n.isDir && expanded.has(n.path)) walk(n.children, depth + 1)\n }\n }\n walk(root.value.children, 0)\n return out\n})\n\nasync function load(): Promise<void> {\n status.value = 'loading'\n errorMsg.value = ''\n selected.value = null\n password.value = ''\n pwError.value = ''\n locked.value = false\n unlocked.value = false\n contentEncrypted.value = false\n revokeBlob()\n stackSlices.value = []\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n buffer = await res.arrayBuffer()\n try {\n archive = readSevenZip(buffer)\n } catch (e) {\n if (e instanceof SevenZipEncryptedError) {\n // Encrypted file names: nothing can be listed until the password unlocks the header.\n locked.value = true\n status.value = 'ready'\n return\n }\n throw e\n }\n entries.value = archive.entries\n root.value = buildTree(archive.entries)\n expanded.clear()\n contentEncrypted.value = archive.entries.some((e) => e.encrypted)\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction pwMessage(e: unknown): string {\n if (e instanceof SevenZipPasswordError) return '密码错误'\n if (e instanceof SevenZipEncryptedError) return '需要密码'\n return e instanceof Error ? e.message : String(e)\n}\n\n// Unlock an encrypted archive. An encrypted header re-lists with the password (the KDF runs here, ~100ms);\n// content-only encryption verifies the password by decoding the first encrypted entry's folder.\nasync function submitPassword(): Promise<void> {\n if (!buffer || !password.value) return\n pwError.value = ''\n if (locked.value) {\n status.value = 'loading'\n await nextTick() // let the spinner paint before the synchronous key derivation blocks\n try {\n archive = readSevenZip(buffer, password.value)\n entries.value = archive.entries\n root.value = buildTree(archive.entries)\n expanded.clear()\n contentEncrypted.value = archive.entries.some((e) => e.encrypted)\n locked.value = false\n unlocked.value = true\n } catch (e) {\n pwError.value = pwMessage(e)\n } finally {\n status.value = 'ready'\n }\n return\n }\n try {\n const target = entries.value.find((e) => !e.isDir && e.encrypted)\n if (target && archive) extractSevenZipEntry(buffer, archive, target, password.value)\n unlocked.value = true\n } catch (e) {\n unlocked.value = false\n pwError.value = pwMessage(e)\n }\n}\n\n// ── selected entry (detail view) ──────────────────────────────────────────────────────────────\nconst selected = ref<SevenZipEntry | null>(null)\nconst entryStatus = ref<Status>('ready')\nconst entryErr = ref('')\nconst entryText = ref('')\nconst entryFile = ref<ViewerFile | null>(null)\nconst entryStack = ref(false)\nconst entryMode = ref<'render' | 'source'>('render')\nconst blobUrl = ref('')\n\n// DICOM series stack: a folder of .dcm slices browsed as one scrollable series (see ZipBrowser). 7z decode is\n// synchronous, but slice.load() stays async to match DicomSlice (the viewer awaits it either way).\nconst stackSlices = ref<DicomSlice[]>([])\nconst stackStart = ref(0)\nconst stackName = ref('')\nconst stackTotalSize = computed(() => stackSlices.value.reduce((s, sl) => s + (sl.size ?? 0), 0))\n\nconst canToggle = computed(() => canToggleView(entryFile.value, entryText.value))\n\nfunction revokeBlob(): void {\n if (blobUrl.value) {\n URL.revokeObjectURL(blobUrl.value)\n blobUrl.value = ''\n }\n}\n\nfunction dirOf(path: string): string {\n const i = path.lastIndexOf('/')\n return i < 0 ? '' : path.slice(0, i)\n}\n// Sibling .dcm files in the same folder, natural-sorted (numeric-aware) so e.g. img2 precedes img10.\nfunction dicomSiblings(e: SevenZipEntry): SevenZipEntry[] {\n const dir = dirOf(e.path)\n return entries.value\n .filter((x) => !x.isDir && isDicomFile(x.name) && dirOf(x.path) === dir)\n .sort((a, b) => a.path.localeCompare(b.path, undefined, { numeric: true, sensitivity: 'base' }))\n}\n\n// Treat as text if the name says so; for extensionless entries, sniff the first bytes for NULs.\nfunction looksText(bytes: Uint8Array, name: string): boolean {\n if (isTextFile(name)) return true\n if (fileExt(name)) return false\n const n = Math.min(bytes.length, 4096)\n for (let i = 0; i < n; i++) if (bytes[i] === 0) return false\n return true\n}\n\nfunction selectEntry(e: SevenZipEntry): void {\n selected.value = e\n entryStatus.value = 'loading'\n entryErr.value = ''\n entryText.value = ''\n entryFile.value = null\n entryStack.value = false\n entryMode.value = 'render'\n revokeBlob()\n stackSlices.value = []\n if (!buffer || !archive) return\n try {\n if (isDicomFile(e.name)) {\n const siblings = dicomSiblings(e)\n if (siblings.length > 1) {\n const buf = buffer\n const arc = archive\n stackSlices.value = siblings.map((se) => ({\n name: se.name,\n size: se.size,\n load: async () => {\n const u = extractSevenZipEntry(buf, arc, se, password.value)\n const out = new ArrayBuffer(u.byteLength)\n new Uint8Array(out).set(u)\n return out\n },\n }))\n stackStart.value = Math.max(\n 0,\n siblings.findIndex((se) => se.path === e.path),\n )\n stackName.value = dirOf(e.path).split('/').pop() || e.name\n entryStack.value = true\n entryStatus.value = 'ready'\n return\n }\n }\n const bytes = extractSevenZipEntry(buffer, archive, e, password.value)\n blobUrl.value = URL.createObjectURL(new Blob([bytes], { type: mediaMimeForName(e.name) }))\n entryFile.value = classifyViewerFile({\n name: e.name,\n url: blobUrl.value,\n size: e.size,\n textHint: looksText(bytes, e.name),\n })\n entryStatus.value = 'ready'\n } catch (err) {\n entryErr.value = pwMessage(err)\n entryStatus.value = 'error'\n }\n}\n\nfunction onRow(node: TreeNode): void {\n if (node.isDir) {\n if (expanded.has(node.path)) expanded.delete(node.path)\n else expanded.add(node.path)\n } else if (node.entry) {\n selectEntry(node.entry)\n }\n}\n\nfunction back(): void {\n selected.value = null\n entryText.value = ''\n entryFile.value = null\n entryStack.value = false\n revokeBlob()\n stackSlices.value = []\n}\n\n// Re-extract on demand (the decoded folder is cached in lib/sevenZip, so this stays cheap).\nfunction downloadEntry(e: SevenZipEntry): void {\n if (!buffer || !archive) return\n try {\n const url = URL.createObjectURL(\n new Blob([extractSevenZipEntry(buffer, archive, e, password.value)]),\n )\n const a = document.createElement('a')\n a.href = url\n a.download = e.name\n document.body.appendChild(a)\n a.click()\n a.remove()\n setTimeout(() => URL.revokeObjectURL(url), 1000)\n } catch {\n /* extraction failed — nothing to download */\n }\n}\n\nconst copied = ref(false)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyEntry(): Promise<void> {\n try {\n await navigator.clipboard?.writeText(entryText.value)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copied.value = true\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copied.value = false), 1400)\n}\n\nonMounted(load)\nwatch(() => props.url, load)\nonBeforeUnmount(() => {\n revokeBlob()\n clearTimeout(copyTimer)\n})\n</script>\n\n<template>\n <div class=\"sz\">\n <p v-if=\"status === 'loading'\" class=\"sz__msg\">正在解压…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"sz__msg sz__msg--error\">\n <p>{{ errorMsg }}</p>\n <button type=\"button\" class=\"sz__retry\" @click=\"load\">重试</button>\n </div>\n\n <!-- locked: the header (file names) is encrypted — must unlock before anything can be listed -->\n <div v-else-if=\"locked\" class=\"sz__locked\">\n <Icon name=\"key\" :size=\"26\" class=\"sz__lockicon\" />\n <p class=\"sz__lockmsg\">该 7z 已加密(含文件名),输入密码后查看</p>\n <div class=\"sz__lockform\">\n <input\n v-model=\"password\"\n type=\"password\"\n class=\"sz__pwinput\"\n placeholder=\"输入密码\"\n @keyup.enter=\"submitPassword\"\n />\n <button type=\"button\" class=\"sz__pwbtn\" @click=\"submitPassword\">解锁</button>\n </div>\n <p v-if=\"pwError\" class=\"sz__lockerr\">{{ pwError }}</p>\n </div>\n\n <!-- detail: one selected entry -->\n <template v-else-if=\"selected\">\n <div class=\"sz__bar\">\n <button type=\"button\" class=\"sz__back\" title=\"返回列表\" @click=\"back\">\n <Icon name=\"arrowLeft\" :size=\"16\" />\n </button>\n <span class=\"sz__path\" :title=\"selected.path\">{{ selected.path }}</span>\n <span class=\"sz__bsize\">{{ formatBytes(selected.size) }}</span>\n <div v-if=\"canToggle\" class=\"sz__view\" role=\"group\" aria-label=\"渲染或源码\">\n <button\n type=\"button\"\n class=\"sz__vbtn\"\n :class=\"{ 'sz__vbtn--on': entryMode === 'render' }\"\n title=\"渲染\"\n @click=\"entryMode = 'render'\"\n >\n <Icon name=\"eye\" :size=\"14\" />\n </button>\n <button\n type=\"button\"\n class=\"sz__vbtn\"\n :class=\"{ 'sz__vbtn--on': entryMode === 'source' }\"\n title=\"源码\"\n @click=\"entryMode = 'source'\"\n >\n <Icon name=\"code\" :size=\"14\" />\n </button>\n </div>\n <button\n v-if=\"entryText && entryStatus === 'ready'\"\n type=\"button\"\n class=\"sz__act\"\n :title=\"copied ? '已复制' : '复制'\"\n @click=\"copyEntry\"\n >\n <Icon name=\"copy\" :size=\"14\" />\n </button>\n <button type=\"button\" class=\"sz__act\" title=\"下载\" @click=\"downloadEntry(selected)\">\n <Icon name=\"download\" :size=\"15\" />\n </button>\n </div>\n\n <p v-if=\"entryStatus === 'loading'\" class=\"sz__msg\">读取中…</p>\n <div v-else-if=\"entryStatus === 'error'\" class=\"sz__msg sz__msg--error\">\n <p>{{ entryErr }}</p>\n </div>\n\n <!-- dicom series: a whole folder of .dcm slices browsed as one scrollable series (wheel/cine翻层) -->\n <DicomViewer\n v-else-if=\"entryStack\"\n :slices=\"stackSlices\"\n :start=\"stackStart\"\n :name=\"stackName\"\n :size=\"stackTotalSize\"\n />\n\n <!-- everything else: the shared body picks the right viewer from the extracted blob -->\n <FileViewerBody\n v-else-if=\"entryFile\"\n :file=\"entryFile\"\n :mode=\"entryMode\"\n @loaded=\"entryText = $event\"\n />\n </template>\n\n <!-- list: the file tree -->\n <template v-else>\n <div class=\"sz__summary\">{{ fileCount }} 个文件 · {{ formatBytes(totalSize) }}</div>\n\n <!-- password bar: shown when file bytes are encrypted (but the file names were readable) -->\n <div v-if=\"contentEncrypted\" class=\"sz__pw\" :class=\"{ 'sz__pw--ok': unlocked }\">\n <Icon name=\"key\" :size=\"14\" />\n <input\n v-model=\"password\"\n type=\"password\"\n class=\"sz__pwinput\"\n :placeholder=\"unlocked ? '已解锁(如需可重新输入)' : '此压缩包内容已加密,输入密码解锁'\"\n @keyup.enter=\"submitPassword\"\n />\n <button type=\"button\" class=\"sz__pwbtn\" @click=\"submitPassword\">\n {{ unlocked ? '已解锁' : '解锁' }}\n </button>\n <span v-if=\"pwError\" class=\"sz__pwerr\">{{ pwError }}</span>\n </div>\n\n <div class=\"sz__scroll sz__tree\">\n <p v-if=\"rows.length === 0\" class=\"sz__msg\">空压缩包</p>\n <button\n v-for=\"row in rows\"\n :key=\"row.node.path\"\n type=\"button\"\n class=\"sz__row\"\n :class=\"{ 'sz__row--dir': row.node.isDir }\"\n :style=\"{ paddingLeft: `${12 + row.depth * 14}px` }\"\n @click=\"onRow(row.node)\"\n >\n <span class=\"sz__twisty\">\n <Icon\n v-if=\"row.node.isDir\"\n name=\"chevronRight\"\n :size=\"13\"\n :class=\"{ 'sz__twisty--open': expanded.has(row.node.path) }\"\n />\n </span>\n <span class=\"sz__rowicon\">\n <Icon :name=\"row.node.isDir ? 'folder' : 'fileOutline'\" :size=\"15\" />\n </span>\n <span class=\"sz__rowname\">{{ row.node.name }}</span>\n <Icon\n v-if=\"!row.node.isDir && row.node.entry?.encrypted\"\n name=\"key\"\n :size=\"12\"\n class=\"sz__rowlock\"\n />\n <span v-if=\"!row.node.isDir\" class=\"sz__rowsize\">{{ formatBytes(row.node.size) }}</span>\n </button>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.sz {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.sz__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.sz__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.sz__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.sz__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* List view ------------------------------------------------------------------------------------ */\n.sz__summary {\n flex-shrink: 0;\n padding: 9px 16px;\n border-bottom: 1px solid var(--border);\n font-size: 11.5px;\n letter-spacing: 0.02em;\n color: var(--faint);\n}\n.sz__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.sz__tree {\n padding: 6px 0;\n}\n.sz__row {\n display: flex;\n align-items: center;\n gap: 7px;\n width: 100%;\n padding: 5px 12px 5px 12px;\n background: transparent;\n border: none;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n text-align: left;\n cursor: pointer;\n}\n.sz__row:hover {\n background: var(--surface-2);\n}\n.sz__twisty {\n flex-shrink: 0;\n display: inline-flex;\n width: 13px;\n color: var(--faint);\n}\n.sz__twisty--open {\n transform: rotate(90deg);\n transition: transform 0.12s;\n}\n.sz__rowicon {\n flex-shrink: 0;\n display: inline-flex;\n color: var(--faint);\n}\n.sz__row--dir .sz__rowicon {\n color: var(--accent);\n}\n.sz__rowname {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.sz__row--dir .sz__rowname {\n font-weight: 500;\n}\n.sz__rowsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n\n/* Detail bar ----------------------------------------------------------------------------------- */\n.sz__bar {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 7px 10px 7px 8px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.sz__back,\n.sz__act {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 30px;\n height: 28px;\n background: transparent;\n border: none;\n border-radius: 7px;\n color: var(--muted);\n cursor: pointer;\n transition:\n background 0.15s,\n color 0.15s;\n}\n.sz__back:hover,\n.sz__act:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.sz__path {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n text-align: left;\n font-size: 12.5px;\n font-weight: 500;\n color: var(--text-strong);\n}\n.sz__bsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n.sz__view {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n gap: 2px;\n padding: 2px;\n border-radius: 7px;\n background: var(--surface-2);\n}\n.sz__vbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 24px;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 5px;\n color: var(--faint);\n cursor: pointer;\n}\n.sz__vbtn--on {\n background: var(--surface);\n color: var(--accent);\n box-shadow: var(--shadow);\n}\n\n/* Password UI (encrypted 7z) --------------------------------------------------------------------- */\n.sz__locked {\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: 12px;\n padding: 24px;\n text-align: center;\n}\n.sz__lockicon {\n color: var(--faint);\n}\n.sz__lockmsg {\n margin: 0;\n font-size: 13px;\n color: var(--muted);\n}\n.sz__lockform {\n display: flex;\n gap: 8px;\n width: 100%;\n max-width: 320px;\n}\n.sz__lockerr {\n margin: 0;\n font-size: 12px;\n color: var(--danger);\n}\n/* Inline password bar (content-encrypted, names readable) + shared input/button. */\n.sz__pw {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 8px 12px;\n border-bottom: 1px solid var(--border);\n background: var(--surface-2);\n color: var(--muted);\n}\n.sz__pw--ok {\n color: var(--accent);\n}\n.sz__pwinput {\n flex: 1 1 auto;\n min-width: 0;\n height: 28px;\n padding: 0 9px;\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: 7px;\n}\n.sz__pwinput:focus {\n outline: none;\n border-color: var(--accent);\n}\n.sz__pwbtn {\n flex-shrink: 0;\n height: 28px;\n padding: 0 12px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n}\n.sz__pwbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.sz__pwerr {\n flex-shrink: 0;\n font-size: 11.5px;\n color: var(--danger);\n}\n.sz__rowlock {\n flex-shrink: 0;\n color: var(--faint);\n}\n</style>\n"],"mappings":"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"}
1
+ {"version":3,"file":"SevenZipBrowser.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/SevenZipBrowser.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport {\n computed,\n defineAsyncComponent,\n nextTick,\n onBeforeUnmount,\n onMounted,\n reactive,\n ref,\n watch,\n} from 'vue'\nimport {\n extractSevenZipEntry,\n readSevenZip,\n SevenZipEncryptedError,\n SevenZipPasswordError,\n type SevenZipArchive,\n type SevenZipEntry,\n} from '@/lib/sevenZip'\nimport type { DicomSlice } from '@/lib/dicom'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { useArchiveImageGallery } from '@/composables/useArchiveImageGallery'\nimport { fileExt, isDicomFile, isTextFile } from '@/lib/fileType'\nimport { canToggleView, classifyViewerFile, mediaMimeForName } from '@/lib/viewerKind'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n// A folder of sibling .dcm files browses as one scrollable series — handled directly (the body has no slice model).\nconst DicomViewer = defineAsyncComponent(() => import('@/components/DicomViewer.vue'))\n// The shared side-viewer body renders a selected entry with the SAME viewers as a top-level file. Lazy to break\n// the module cycle (the body lazy-imports this browser back, for nested archives).\nconst FileViewerBody = defineAsyncComponent(() => import('@/components/FileViewerBody.vue'))\n\n/**\n * The side viewer's 7z mode — the LZMA counterpart to {@link ZipBrowser}. Fetches an archive's bytes, parses +\n * decompresses them entirely in-browser (see lib/sevenZip.ts, which carries its own LZMA decoder since browsers\n * ship none), and browses the contents. Selecting an entry extracts just that one, wraps it in a blob URL, and\n * hands it to {@link FileViewerBody} (so a file inside the 7z previews with every viewer a top-level file gets).\n */\nconst props = defineProps<{ url: string; name: string }>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nlet buffer: ArrayBuffer | null = null\nlet archive: SevenZipArchive | null = null\nconst entries = ref<SevenZipEntry[]>([])\n\n// Password state for AES-encrypted 7z. `locked` = the archive header itself is encrypted (encrypted file\n// names), so it can't even be listed until unlocked; `contentEncrypted` = the listing worked but file bytes\n// need a password; `unlocked` = a password has been accepted.\nconst password = ref('')\nconst pwError = ref('')\nconst locked = ref(false)\nconst unlocked = ref(false)\nconst contentEncrypted = ref(false)\n\ninterface TreeNode {\n name: string\n path: string\n isDir: boolean\n size: number\n entry?: SevenZipEntry\n children: TreeNode[]\n}\nconst root = ref<TreeNode>({ name: '', path: '', isDir: true, size: 0, children: [] })\nconst expanded = reactive(new Set<string>())\n\nconst fileCount = computed(() => entries.value.reduce((n, e) => n + (e.isDir ? 0 : 1), 0))\nconst totalSize = computed(() => entries.value.reduce((s, e) => s + (e.isDir ? 0 : e.size), 0))\n\n// Build a folder tree from the flat entry paths (directories may be implicit — derive them either way).\nfunction buildTree(list: SevenZipEntry[]): TreeNode {\n const rootNode: TreeNode = { name: '', path: '', isDir: true, size: 0, children: [] }\n const dirs = new Map<string, TreeNode>([['', rootNode]])\n const ensureDir = (segs: string[]): TreeNode => {\n let cur = rootNode\n let acc = ''\n for (const seg of segs) {\n acc = acc ? `${acc}/${seg}` : seg\n let node = dirs.get(acc)\n if (!node) {\n node = { name: seg, path: acc, isDir: true, size: 0, children: [] }\n dirs.set(acc, node)\n cur.children.push(node)\n }\n cur = node\n }\n return cur\n }\n for (const e of list) {\n const segs = e.path.split('/').filter(Boolean)\n if (e.isDir) {\n ensureDir(segs)\n continue\n }\n const fname = segs.pop()\n if (!fname) continue\n ensureDir(segs).children.push({\n name: fname,\n path: e.path,\n isDir: false,\n size: e.size,\n entry: e,\n children: [],\n })\n }\n sortNode(rootNode)\n return rootNode\n}\n// Folders first, then files, each alphabetical (case-insensitive).\nfunction sortNode(n: TreeNode): void {\n n.children.sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1))\n for (const c of n.children) if (c.isDir) sortNode(c)\n}\n\n// Flatten the tree to the rows currently visible (respecting which folders are expanded).\ninterface Row {\n node: TreeNode\n depth: number\n}\nconst rows = computed<Row[]>(() => {\n const out: Row[] = []\n const walk = (nodes: TreeNode[], depth: number): void => {\n for (const n of nodes) {\n out.push({ node: n, depth })\n if (n.isDir && expanded.has(n.path)) walk(n.children, depth + 1)\n }\n }\n walk(root.value.children, 0)\n return out\n})\n\nasync function load(): Promise<void> {\n imageGallery.clear()\n status.value = 'loading'\n errorMsg.value = ''\n selected.value = null\n password.value = ''\n pwError.value = ''\n locked.value = false\n unlocked.value = false\n contentEncrypted.value = false\n revokeBlob()\n stackSlices.value = []\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n buffer = await res.arrayBuffer()\n try {\n archive = readSevenZip(buffer)\n } catch (e) {\n if (e instanceof SevenZipEncryptedError) {\n // Encrypted file names: nothing can be listed until the password unlocks the header.\n locked.value = true\n status.value = 'ready'\n return\n }\n throw e\n }\n entries.value = archive.entries\n root.value = buildTree(archive.entries)\n expanded.clear()\n contentEncrypted.value = archive.entries.some((e) => e.encrypted)\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction pwMessage(e: unknown): string {\n if (e instanceof SevenZipPasswordError) return '密码错误'\n if (e instanceof SevenZipEncryptedError) return '需要密码'\n return e instanceof Error ? e.message : String(e)\n}\n\n// Unlock an encrypted archive. An encrypted header re-lists with the password (the KDF runs here, ~100ms);\n// content-only encryption verifies the password by decoding the first encrypted entry's folder.\nasync function submitPassword(): Promise<void> {\n if (!buffer || !password.value) return\n pwError.value = ''\n if (locked.value) {\n status.value = 'loading'\n await nextTick() // let the spinner paint before the synchronous key derivation blocks\n try {\n archive = readSevenZip(buffer, password.value)\n entries.value = archive.entries\n root.value = buildTree(archive.entries)\n expanded.clear()\n contentEncrypted.value = archive.entries.some((e) => e.encrypted)\n locked.value = false\n unlocked.value = true\n } catch (e) {\n pwError.value = pwMessage(e)\n } finally {\n status.value = 'ready'\n }\n return\n }\n try {\n const target = entries.value.find((e) => !e.isDir && e.encrypted)\n if (target && archive) extractSevenZipEntry(buffer, archive, target, password.value)\n unlocked.value = true\n } catch (e) {\n unlocked.value = false\n pwError.value = pwMessage(e)\n }\n}\n\n// ── selected entry (detail view) ──────────────────────────────────────────────────────────────\nconst selected = ref<SevenZipEntry | null>(null)\nconst entryStatus = ref<Status>('ready')\nconst entryErr = ref('')\nconst entryText = ref('')\nconst entryFile = ref<ViewerFile | null>(null)\nconst entryStack = ref(false)\nconst entryMode = ref<'render' | 'source'>('render')\nconst blobUrl = ref('')\n\n// DICOM series stack: a folder of .dcm slices browsed as one scrollable series (see ZipBrowser). 7z decode is\n// synchronous, but slice.load() stays async to match DicomSlice (the viewer awaits it either way).\nconst stackSlices = ref<DicomSlice[]>([])\nconst stackStart = ref(0)\nconst stackName = ref('')\nconst stackTotalSize = computed(() => stackSlices.value.reduce((s, sl) => s + (sl.size ?? 0), 0))\n\nconst canToggle = computed(() => canToggleView(entryFile.value, entryText.value))\n\nfunction revokeBlob(): void {\n if (blobUrl.value) {\n URL.revokeObjectURL(blobUrl.value)\n blobUrl.value = ''\n }\n}\n\nfunction dirOf(path: string): string {\n const i = path.lastIndexOf('/')\n return i < 0 ? '' : path.slice(0, i)\n}\n// Sibling .dcm files in the same folder, natural-sorted (numeric-aware) so e.g. img2 precedes img10.\nfunction dicomSiblings(e: SevenZipEntry): SevenZipEntry[] {\n const dir = dirOf(e.path)\n return entries.value\n .filter((x) => !x.isDir && isDicomFile(x.name) && dirOf(x.path) === dir)\n .sort((a, b) => a.path.localeCompare(b.path, undefined, { numeric: true, sensitivity: 'base' }))\n}\n\n// Treat as text if the name says so; for extensionless entries, sniff the first bytes for NULs.\nfunction looksText(bytes: Uint8Array, name: string): boolean {\n if (isTextFile(name)) return true\n if (fileExt(name)) return false\n const n = Math.min(bytes.length, 4096)\n for (let i = 0; i < n; i++) if (bytes[i] === 0) return false\n return true\n}\n\nconst imageGallery = useArchiveImageGallery<SevenZipEntry>({\n entries: () => entries.value,\n archiveName: () => props.name,\n extract: (entry) => {\n if (!buffer || !archive) throw new Error('7z 尚未加载')\n return extractSevenZipEntry(buffer, archive, entry, password.value)\n },\n})\n\nfunction selectEntry(e: SevenZipEntry): void {\n selected.value = e\n entryStatus.value = 'loading'\n entryErr.value = ''\n entryText.value = ''\n entryFile.value = null\n entryStack.value = false\n entryMode.value = 'render'\n revokeBlob()\n stackSlices.value = []\n if (!buffer || !archive) return\n try {\n if (isDicomFile(e.name)) {\n const siblings = dicomSiblings(e)\n if (siblings.length > 1) {\n const buf = buffer\n const arc = archive\n stackSlices.value = siblings.map((se) => ({\n name: se.name,\n size: se.size,\n load: async () => {\n const u = extractSevenZipEntry(buf, arc, se, password.value)\n const out = new ArrayBuffer(u.byteLength)\n new Uint8Array(out).set(u)\n return out\n },\n }))\n stackStart.value = Math.max(\n 0,\n siblings.findIndex((se) => se.path === e.path),\n )\n stackName.value = dirOf(e.path).split('/').pop() || e.name\n entryStack.value = true\n entryStatus.value = 'ready'\n return\n }\n }\n const bytes = extractSevenZipEntry(buffer, archive, e, password.value)\n blobUrl.value = URL.createObjectURL(new Blob([bytes], { type: mediaMimeForName(e.name) }))\n const file = classifyViewerFile({\n name: e.name,\n url: blobUrl.value,\n size: e.size,\n textHint: looksText(bytes, e.name),\n })\n entryFile.value = file\n entryStatus.value = 'ready'\n if (file.image) void imageGallery.open(e, bytes)\n } catch (err) {\n entryErr.value = pwMessage(err)\n entryStatus.value = 'error'\n }\n}\n\nfunction onRow(node: TreeNode): void {\n if (node.isDir) {\n if (expanded.has(node.path)) expanded.delete(node.path)\n else expanded.add(node.path)\n } else if (node.entry) {\n selectEntry(node.entry)\n }\n}\n\nfunction back(): void {\n selected.value = null\n entryText.value = ''\n entryFile.value = null\n entryStack.value = false\n revokeBlob()\n stackSlices.value = []\n}\n\n// Re-extract on demand (the decoded folder is cached in lib/sevenZip, so this stays cheap).\nfunction downloadEntry(e: SevenZipEntry): void {\n if (!buffer || !archive) return\n try {\n const url = URL.createObjectURL(\n new Blob([extractSevenZipEntry(buffer, archive, e, password.value)]),\n )\n const a = document.createElement('a')\n a.href = url\n a.download = e.name\n document.body.appendChild(a)\n a.click()\n a.remove()\n setTimeout(() => URL.revokeObjectURL(url), 1000)\n } catch {\n /* extraction failed — nothing to download */\n }\n}\n\nconst copied = ref(false)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyEntry(): Promise<void> {\n try {\n await navigator.clipboard?.writeText(entryText.value)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copied.value = true\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copied.value = false), 1400)\n}\n\nonMounted(load)\nwatch(() => props.url, load)\nonBeforeUnmount(() => {\n revokeBlob()\n clearTimeout(copyTimer)\n})\n</script>\n\n<template>\n <div class=\"sz\">\n <p v-if=\"status === 'loading'\" class=\"sz__msg\">正在解压…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"sz__msg sz__msg--error\">\n <p>{{ errorMsg }}</p>\n <button type=\"button\" class=\"sz__retry\" @click=\"load\">重试</button>\n </div>\n\n <!-- locked: the header (file names) is encrypted — must unlock before anything can be listed -->\n <div v-else-if=\"locked\" class=\"sz__locked\">\n <Icon name=\"key\" :size=\"26\" class=\"sz__lockicon\" />\n <p class=\"sz__lockmsg\">该 7z 已加密(含文件名),输入密码后查看</p>\n <div class=\"sz__lockform\">\n <input\n v-model=\"password\"\n type=\"password\"\n class=\"sz__pwinput\"\n placeholder=\"输入密码\"\n @keyup.enter=\"submitPassword\"\n />\n <button type=\"button\" class=\"sz__pwbtn\" @click=\"submitPassword\">解锁</button>\n </div>\n <p v-if=\"pwError\" class=\"sz__lockerr\">{{ pwError }}</p>\n </div>\n\n <!-- detail: one selected entry -->\n <template v-else-if=\"selected\">\n <div class=\"sz__bar\">\n <button type=\"button\" class=\"sz__back\" title=\"返回列表\" @click=\"back\">\n <Icon name=\"arrowLeft\" :size=\"16\" />\n </button>\n <span class=\"sz__path\" :title=\"selected.path\">{{ selected.path }}</span>\n <span class=\"sz__bsize\">{{ formatBytes(selected.size) }}</span>\n <div v-if=\"canToggle\" class=\"sz__view\" role=\"group\" aria-label=\"渲染或源码\">\n <button\n type=\"button\"\n class=\"sz__vbtn\"\n :class=\"{ 'sz__vbtn--on': entryMode === 'render' }\"\n title=\"渲染\"\n @click=\"entryMode = 'render'\"\n >\n <Icon name=\"eye\" :size=\"14\" />\n </button>\n <button\n type=\"button\"\n class=\"sz__vbtn\"\n :class=\"{ 'sz__vbtn--on': entryMode === 'source' }\"\n title=\"源码\"\n @click=\"entryMode = 'source'\"\n >\n <Icon name=\"code\" :size=\"14\" />\n </button>\n </div>\n <button\n v-if=\"entryText && entryStatus === 'ready'\"\n type=\"button\"\n class=\"sz__act\"\n :title=\"copied ? '已复制' : '复制'\"\n @click=\"copyEntry\"\n >\n <Icon name=\"copy\" :size=\"14\" />\n </button>\n <button type=\"button\" class=\"sz__act\" title=\"下载\" @click=\"downloadEntry(selected)\">\n <Icon name=\"download\" :size=\"15\" />\n </button>\n </div>\n\n <p v-if=\"entryStatus === 'loading'\" class=\"sz__msg\">读取中…</p>\n <div v-else-if=\"entryStatus === 'error'\" class=\"sz__msg sz__msg--error\">\n <p>{{ entryErr }}</p>\n </div>\n\n <!-- dicom series: a whole folder of .dcm slices browsed as one scrollable series (wheel/cine翻层) -->\n <DicomViewer\n v-else-if=\"entryStack\"\n :slices=\"stackSlices\"\n :start=\"stackStart\"\n :name=\"stackName\"\n :size=\"stackTotalSize\"\n />\n\n <!-- everything else: the shared body picks the right viewer from the extracted blob -->\n <FileViewerBody\n v-else-if=\"entryFile\"\n :file=\"entryFile\"\n :mode=\"entryMode\"\n @loaded=\"entryText = $event\"\n />\n </template>\n\n <!-- list: the file tree -->\n <template v-else>\n <div class=\"sz__summary\">{{ fileCount }} 个文件 · {{ formatBytes(totalSize) }}</div>\n\n <!-- password bar: shown when file bytes are encrypted (but the file names were readable) -->\n <div v-if=\"contentEncrypted\" class=\"sz__pw\" :class=\"{ 'sz__pw--ok': unlocked }\">\n <Icon name=\"key\" :size=\"14\" />\n <input\n v-model=\"password\"\n type=\"password\"\n class=\"sz__pwinput\"\n :placeholder=\"unlocked ? '已解锁(如需可重新输入)' : '此压缩包内容已加密,输入密码解锁'\"\n @keyup.enter=\"submitPassword\"\n />\n <button type=\"button\" class=\"sz__pwbtn\" @click=\"submitPassword\">\n {{ unlocked ? '已解锁' : '解锁' }}\n </button>\n <span v-if=\"pwError\" class=\"sz__pwerr\">{{ pwError }}</span>\n </div>\n\n <div class=\"sz__scroll sz__tree\">\n <p v-if=\"rows.length === 0\" class=\"sz__msg\">空压缩包</p>\n <button\n v-for=\"row in rows\"\n :key=\"row.node.path\"\n type=\"button\"\n class=\"sz__row\"\n :class=\"{ 'sz__row--dir': row.node.isDir }\"\n :style=\"{ paddingLeft: `${12 + row.depth * 14}px` }\"\n @click=\"onRow(row.node)\"\n >\n <span class=\"sz__twisty\">\n <Icon\n v-if=\"row.node.isDir\"\n name=\"chevronRight\"\n :size=\"13\"\n :class=\"{ 'sz__twisty--open': expanded.has(row.node.path) }\"\n />\n </span>\n <span class=\"sz__rowicon\">\n <Icon :name=\"row.node.isDir ? 'folder' : 'fileOutline'\" :size=\"15\" />\n </span>\n <span class=\"sz__rowname\">{{ row.node.name }}</span>\n <Icon\n v-if=\"!row.node.isDir && row.node.entry?.encrypted\"\n name=\"key\"\n :size=\"12\"\n class=\"sz__rowlock\"\n />\n <span v-if=\"!row.node.isDir\" class=\"sz__rowsize\">{{ formatBytes(row.node.size) }}</span>\n </button>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.sz {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.sz__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.sz__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.sz__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.sz__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* List view ------------------------------------------------------------------------------------ */\n.sz__summary {\n flex-shrink: 0;\n padding: 9px 16px;\n border-bottom: 1px solid var(--border);\n font-size: 11.5px;\n letter-spacing: 0.02em;\n color: var(--faint);\n}\n.sz__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.sz__tree {\n padding: 6px 0;\n}\n.sz__row {\n display: flex;\n align-items: center;\n gap: 7px;\n width: 100%;\n padding: 5px 12px 5px 12px;\n background: transparent;\n border: none;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n text-align: left;\n cursor: pointer;\n}\n.sz__row:hover {\n background: var(--surface-2);\n}\n.sz__twisty {\n flex-shrink: 0;\n display: inline-flex;\n width: 13px;\n color: var(--faint);\n}\n.sz__twisty--open {\n transform: rotate(90deg);\n transition: transform 0.12s;\n}\n.sz__rowicon {\n flex-shrink: 0;\n display: inline-flex;\n color: var(--faint);\n}\n.sz__row--dir .sz__rowicon {\n color: var(--accent);\n}\n.sz__rowname {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.sz__row--dir .sz__rowname {\n font-weight: 500;\n}\n.sz__rowsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n\n/* Detail bar ----------------------------------------------------------------------------------- */\n.sz__bar {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 7px 10px 7px 8px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.sz__back,\n.sz__act {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 30px;\n height: 28px;\n background: transparent;\n border: none;\n border-radius: 7px;\n color: var(--muted);\n cursor: pointer;\n transition:\n background 0.15s,\n color 0.15s;\n}\n.sz__back:hover,\n.sz__act:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.sz__path {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n text-align: left;\n font-size: 12.5px;\n font-weight: 500;\n color: var(--text-strong);\n}\n.sz__bsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n.sz__view {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n gap: 2px;\n padding: 2px;\n border-radius: 7px;\n background: var(--surface-2);\n}\n.sz__vbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 24px;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 5px;\n color: var(--faint);\n cursor: pointer;\n}\n.sz__vbtn--on {\n background: var(--surface);\n color: var(--accent);\n box-shadow: var(--shadow);\n}\n\n/* Password UI (encrypted 7z) --------------------------------------------------------------------- */\n.sz__locked {\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: 12px;\n padding: 24px;\n text-align: center;\n}\n.sz__lockicon {\n color: var(--faint);\n}\n.sz__lockmsg {\n margin: 0;\n font-size: 13px;\n color: var(--muted);\n}\n.sz__lockform {\n display: flex;\n gap: 8px;\n width: 100%;\n max-width: 320px;\n}\n.sz__lockerr {\n margin: 0;\n font-size: 12px;\n color: var(--danger);\n}\n/* Inline password bar (content-encrypted, names readable) + shared input/button. */\n.sz__pw {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 8px 12px;\n border-bottom: 1px solid var(--border);\n background: var(--surface-2);\n color: var(--muted);\n}\n.sz__pw--ok {\n color: var(--accent);\n}\n.sz__pwinput {\n flex: 1 1 auto;\n min-width: 0;\n height: 28px;\n padding: 0 9px;\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: 7px;\n}\n.sz__pwinput:focus {\n outline: none;\n border-color: var(--accent);\n}\n.sz__pwbtn {\n flex-shrink: 0;\n height: 28px;\n padding: 0 12px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n}\n.sz__pwbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.sz__pwerr {\n flex-shrink: 0;\n font-size: 11.5px;\n color: var(--danger);\n}\n.sz__rowlock {\n flex-shrink: 0;\n color: var(--faint);\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 "./ZipBrowser.vue_vue_type_script_setup_true_lang.js";
3
3
  /* empty css */
4
4
  //#region src/components/ZipBrowser.vue
5
- var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-3b2fe067"]]);
5
+ var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-6a5afd17"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
8
 
@@ -1 +1 @@
1
- {"version":3,"file":"ZipBrowser.js","names":[],"sources":["../../src/components/ZipBrowser.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport {\n computed,\n defineAsyncComponent,\n onBeforeUnmount,\n onMounted,\n reactive,\n ref,\n watch,\n} from 'vue'\nimport {\n extractZipEntry,\n readZip,\n zipSupported,\n ZipEncryptedError,\n ZipPasswordError,\n type ZipEntry,\n} from '@/lib/zip'\nimport type { DicomSlice } from '@/lib/dicom'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { fileExt, isDicomFile, isTextFile } from '@/lib/fileType'\nimport { canToggleView, classifyViewerFile, mediaMimeForName } from '@/lib/viewerKind'\nimport { buildArchiveHtmlPreview } from '@/lib/archiveHtml'\nimport { decodeTextSniff } from '@/lib/encoding'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n// A folder of sibling .dcm files browses as one scrollable series — FileViewerBody has no slice model, so the\n// DICOM stack stays a direct hand-off here (a lone .dcm goes through the body like any other entry).\nconst DicomViewer = defineAsyncComponent(() => import('@/components/DicomViewer.vue'))\n\n// The shared side-viewer body renders a selected entry with the SAME viewers as a top-level file. Lazy-imported\n// to break the module cycle (the body lazy-imports this browser back, for nested archives).\nconst FileViewerBody = defineAsyncComponent(() => import('@/components/FileViewerBody.vue'))\n\n/**\n * The side viewer's ZIP mode: fetch an archive's bytes, decompress them entirely in-browser (see lib/zip.ts),\n * and browse the contents. A master→detail within the narrow pane — a collapsible file tree, and selecting an\n * entry extracts just that one, wraps it in a blob URL, and hands it to {@link FileViewerBody} (so a file\n * inside the zip previews with every viewer a top-level file gets — code/image/pdf/cert/audio/nested-zip/…).\n * HTML is the one multi-entry case: its relative JS/CSS/image/font dependencies are resolved from the same ZIP\n * and rewritten before entering the existing sandboxed iframe. Other entry bytes are not retained; selecting\n * decodes on demand, and the download button re-extracts.\n */\nconst props = defineProps<{ url: string; name: string }>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nlet buffer: ArrayBuffer | null = null\nconst entries = ref<ZipEntry[]>([])\n\n// Encrypted archives: a password bar appears once any entry is encrypted. The password is held only in memory\n// and passed to each extraction (ZipCrypto / WinZip AES decrypt happens in lib/zip.ts).\nconst password = ref('')\nconst pwError = ref('')\nconst unlocked = ref(false)\nconst anyEncrypted = computed(() => entries.value.some((e) => e.encrypted))\n\ninterface TreeNode {\n name: string\n path: string\n isDir: boolean\n size: number\n entry?: ZipEntry\n children: TreeNode[]\n}\nconst root = ref<TreeNode>({ name: '', path: '', isDir: true, size: 0, children: [] })\nconst expanded = reactive(new Set<string>())\n\nconst fileCount = computed(() => entries.value.reduce((n, e) => n + (e.isDir ? 0 : 1), 0))\nconst totalSize = computed(() => entries.value.reduce((s, e) => s + (e.isDir ? 0 : e.size), 0))\n\n// Build a folder tree from the flat entry paths (directories may be implicit — derive them either way).\nfunction buildTree(list: ZipEntry[]): TreeNode {\n const rootNode: TreeNode = { name: '', path: '', isDir: true, size: 0, children: [] }\n const dirs = new Map<string, TreeNode>([['', rootNode]])\n const ensureDir = (segs: string[]): TreeNode => {\n let cur = rootNode\n let acc = ''\n for (const seg of segs) {\n acc = acc ? `${acc}/${seg}` : seg\n let node = dirs.get(acc)\n if (!node) {\n node = { name: seg, path: acc, isDir: true, size: 0, children: [] }\n dirs.set(acc, node)\n cur.children.push(node)\n }\n cur = node\n }\n return cur\n }\n for (const e of list) {\n const segs = e.path.split('/').filter(Boolean)\n if (e.isDir) {\n ensureDir(segs)\n continue\n }\n const fname = segs.pop()\n if (!fname) continue\n ensureDir(segs).children.push({\n name: fname,\n path: e.path,\n isDir: false,\n size: e.size,\n entry: e,\n children: [],\n })\n }\n sortNode(rootNode)\n return rootNode\n}\n// Folders first, then files, each alphabetical (case-insensitive).\nfunction sortNode(n: TreeNode): void {\n n.children.sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1))\n for (const c of n.children) if (c.isDir) sortNode(c)\n}\n\n// Flatten the tree to the rows currently visible (respecting which folders are expanded).\ninterface Row {\n node: TreeNode\n depth: number\n}\nconst rows = computed<Row[]>(() => {\n const out: Row[] = []\n const walk = (nodes: TreeNode[], depth: number): void => {\n for (const n of nodes) {\n out.push({ node: n, depth })\n if (n.isDir && expanded.has(n.path)) walk(n.children, depth + 1)\n }\n }\n walk(root.value.children, 0)\n return out\n})\n\nasync function load(): Promise<void> {\n selectionRun++\n status.value = 'loading'\n errorMsg.value = ''\n selected.value = null\n entryFile.value = null\n entryHtmlPreview.value = ''\n password.value = ''\n pwError.value = ''\n unlocked.value = false\n revokeBlob()\n stackSlices.value = []\n if (!zipSupported()) {\n errorMsg.value = '当前浏览器不支持在前端解压 ZIP(缺少 DecompressionStream)'\n status.value = 'error'\n return\n }\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n buffer = await res.arrayBuffer()\n const list = readZip(buffer)\n entries.value = list\n root.value = buildTree(list)\n expanded.clear()\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\n// ── selected entry (detail view) ──────────────────────────────────────────────────────────────\nconst selected = ref<ZipEntry | null>(null)\nconst entryStatus = ref<Status>('ready') // covers extraction; the body owns loading once entryFile is set\nconst entryErr = ref('')\nconst entryText = ref('') // the body emits loaded text up so the copy button can use it\nconst entryFile = ref<ViewerFile | null>(null)\nconst entryHtmlPreview = ref('') // dependency-rewritten HTML; source/copy still use the original text\nconst entryStack = ref(false) // DICOM series (handled directly, not via the body)\nconst entryMode = ref<'render' | 'source'>('render')\nconst blobUrl = ref('') // entry blob URL fed to the body (revoked on change/unmount)\nlet selectionRun = 0 // stale guard for async HTML dependency extraction (including selecting the same row twice)\n\n// DICOM series stack: when the clicked .dcm has sibling .dcm files in the same folder, they are browsed as\n// one scrollable series. Slices extract lazily; the DICOM viewer caches/prefetches them.\nconst stackSlices = ref<DicomSlice[]>([])\nconst stackStart = ref(0)\nconst stackName = ref('')\nconst stackTotalSize = computed(() => stackSlices.value.reduce((s, sl) => s + (sl.size ?? 0), 0))\n\nconst canToggle = computed(() => canToggleView(entryFile.value, entryText.value))\n\nfunction revokeBlob(): void {\n if (blobUrl.value) {\n URL.revokeObjectURL(blobUrl.value)\n blobUrl.value = ''\n }\n}\n\nfunction dirOf(path: string): string {\n const i = path.lastIndexOf('/')\n return i < 0 ? '' : path.slice(0, i)\n}\n// Sibling .dcm files in the same folder, natural-sorted (numeric-aware) so e.g. img2 precedes img10.\nfunction dicomSiblings(e: ZipEntry): ZipEntry[] {\n const dir = dirOf(e.path)\n return entries.value\n .filter((x) => !x.isDir && isDicomFile(x.name) && dirOf(x.path) === dir)\n .sort((a, b) => a.path.localeCompare(b.path, undefined, { numeric: true, sensitivity: 'base' }))\n}\n\n// Treat as text if the name says so; for extensionless entries, sniff the first bytes for NULs. Feeds the\n// body's source fall-back so an extensionless text file still opens as source instead of a download card.\nfunction looksText(bytes: Uint8Array, name: string): boolean {\n if (isTextFile(name)) return true\n if (fileExt(name)) return false\n const n = Math.min(bytes.length, 4096)\n for (let i = 0; i < n; i++) if (bytes[i] === 0) return false\n return true\n}\n\nasync function selectEntry(e: ZipEntry): Promise<void> {\n const run = ++selectionRun\n let pendingUrl = ''\n selected.value = e\n entryStatus.value = 'loading'\n entryErr.value = ''\n entryText.value = ''\n entryFile.value = null\n entryHtmlPreview.value = ''\n entryStack.value = false\n entryMode.value = 'render'\n revokeBlob()\n stackSlices.value = []\n if (!buffer) return\n try {\n // DICOM series: a folder holding several .dcm → browse them as one scrollable series (slices on demand).\n if (isDicomFile(e.name)) {\n const siblings = dicomSiblings(e)\n if (siblings.length > 1) {\n stackSlices.value = siblings.map((se) => ({\n name: se.name,\n size: se.size,\n load: async () => {\n const u = await extractZipEntry(buffer as ArrayBuffer, se, password.value)\n const out = new ArrayBuffer(u.byteLength)\n new Uint8Array(out).set(u)\n return out\n },\n }))\n stackStart.value = Math.max(\n 0,\n siblings.findIndex((se) => se.path === e.path),\n )\n stackName.value = dirOf(e.path).split('/').pop() || e.name\n entryStack.value = true\n entryStatus.value = 'ready'\n return\n }\n }\n const archiveBuffer = buffer\n const bytes = await extractZipEntry(archiveBuffer, e, password.value)\n if (selectionRun !== run || selected.value !== e) return\n const url = URL.createObjectURL(new Blob([bytes], { type: mediaMimeForName(e.name) }))\n pendingUrl = url\n const file = classifyViewerFile({\n name: e.name,\n url,\n size: e.size,\n textHint: looksText(bytes, e.name),\n })\n let htmlPreview = ''\n if (file.html) {\n const source = decodeTextSniff(bytes)\n file.content = source\n const preview = await buildArchiveHtmlPreview({\n html: source,\n documentPath: e.path,\n entries: entries.value,\n loadEntry: (dependency) => extractZipEntry(archiveBuffer, dependency, password.value),\n })\n if (selectionRun !== run || selected.value !== e) {\n URL.revokeObjectURL(url)\n pendingUrl = ''\n return\n }\n htmlPreview = preview.html\n }\n if (selectionRun !== run || selected.value !== e) {\n URL.revokeObjectURL(url)\n pendingUrl = ''\n return\n }\n blobUrl.value = url\n pendingUrl = ''\n entryHtmlPreview.value = htmlPreview\n entryFile.value = file\n entryStatus.value = 'ready'\n } catch (err) {\n if (pendingUrl) URL.revokeObjectURL(pendingUrl)\n if (err instanceof ZipEncryptedError)\n entryErr.value = '该文件已加密:请返回列表并在顶部输入密码'\n else if (err instanceof ZipPasswordError) {\n entryErr.value = '密码错误'\n unlocked.value = false\n } else entryErr.value = err instanceof Error ? err.message : String(err)\n entryStatus.value = 'error'\n }\n}\n\n// Validate the entered password against the first encrypted entry, then unlock (and re-open any selected file).\nasync function applyPassword(): Promise<void> {\n pwError.value = ''\n const enc = entries.value.find((e) => !e.isDir && e.encrypted && e.size > 0)\n if (!enc || !buffer) {\n unlocked.value = true\n return\n }\n try {\n await extractZipEntry(buffer, enc, password.value)\n unlocked.value = true\n if (selected.value) void selectEntry(selected.value)\n } catch (e) {\n unlocked.value = false\n pwError.value =\n e instanceof ZipPasswordError ? '密码错误' : e instanceof Error ? e.message : String(e)\n }\n}\n\nfunction onRow(node: TreeNode): void {\n if (node.isDir) {\n if (expanded.has(node.path)) expanded.delete(node.path)\n else expanded.add(node.path)\n } else if (node.entry) {\n void selectEntry(node.entry)\n }\n}\n\nfunction back(): void {\n selectionRun++\n selected.value = null\n entryText.value = ''\n entryFile.value = null\n entryHtmlPreview.value = ''\n entryStack.value = false\n revokeBlob()\n stackSlices.value = []\n}\n\n// Re-extract on demand so we don't retain decompressed bytes for the whole session.\nasync function downloadEntry(e: ZipEntry): Promise<void> {\n if (!buffer) return\n try {\n const url = URL.createObjectURL(new Blob([await extractZipEntry(buffer, e, password.value)]))\n const a = document.createElement('a')\n a.href = url\n a.download = e.name\n document.body.appendChild(a)\n a.click()\n a.remove()\n setTimeout(() => URL.revokeObjectURL(url), 1000)\n } catch {\n /* extraction failed — nothing to download */\n }\n}\n\nconst copied = ref(false)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyEntry(): Promise<void> {\n try {\n await navigator.clipboard?.writeText(entryText.value)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copied.value = true\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copied.value = false), 1400)\n}\n\nonMounted(load)\nwatch(() => props.url, load)\nonBeforeUnmount(() => {\n selectionRun++\n revokeBlob()\n clearTimeout(copyTimer)\n})\n</script>\n\n<template>\n <div class=\"zip\">\n <p v-if=\"status === 'loading'\" class=\"zip__msg\">正在解压…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"zip__msg zip__msg--error\">\n <p>{{ errorMsg }}</p>\n <button type=\"button\" class=\"zip__retry\" @click=\"load\">重试</button>\n </div>\n\n <!-- detail: one selected entry -->\n <template v-else-if=\"selected\">\n <div class=\"zip__bar\">\n <button type=\"button\" class=\"zip__back\" title=\"返回列表\" @click=\"back\">\n <Icon name=\"arrowLeft\" :size=\"16\" />\n </button>\n <span class=\"zip__path\" :title=\"selected.path\">{{ selected.path }}</span>\n <span class=\"zip__bsize\">{{ formatBytes(selected.size) }}</span>\n <div v-if=\"canToggle\" class=\"zip__view\" role=\"group\" aria-label=\"渲染或源码\">\n <button\n type=\"button\"\n class=\"zip__vbtn\"\n :class=\"{ 'zip__vbtn--on': entryMode === 'render' }\"\n title=\"渲染\"\n @click=\"entryMode = 'render'\"\n >\n <Icon name=\"eye\" :size=\"14\" />\n </button>\n <button\n type=\"button\"\n class=\"zip__vbtn\"\n :class=\"{ 'zip__vbtn--on': entryMode === 'source' }\"\n title=\"源码\"\n @click=\"entryMode = 'source'\"\n >\n <Icon name=\"code\" :size=\"14\" />\n </button>\n </div>\n <button\n v-if=\"entryText && entryStatus === 'ready'\"\n type=\"button\"\n class=\"zip__act\"\n :title=\"copied ? '已复制' : '复制'\"\n @click=\"copyEntry\"\n >\n <Icon name=\"copy\" :size=\"14\" />\n </button>\n <button type=\"button\" class=\"zip__act\" title=\"下载\" @click=\"downloadEntry(selected)\">\n <Icon name=\"download\" :size=\"15\" />\n </button>\n </div>\n\n <p v-if=\"entryStatus === 'loading'\" class=\"zip__msg\">读取中…</p>\n <div v-else-if=\"entryStatus === 'error'\" class=\"zip__msg zip__msg--error\">\n <p>{{ entryErr }}</p>\n </div>\n\n <!-- dicom series: a whole folder of .dcm slices browsed as one scrollable series (wheel/cine翻层) -->\n <DicomViewer\n v-else-if=\"entryStack\"\n :slices=\"stackSlices\"\n :start=\"stackStart\"\n :name=\"stackName\"\n :size=\"stackTotalSize\"\n />\n\n <!-- everything else: the shared body picks the right viewer from the extracted blob -->\n <FileViewerBody\n v-else-if=\"entryFile\"\n :file=\"entryFile\"\n :mode=\"entryMode\"\n :html-preview=\"entryHtmlPreview || undefined\"\n @loaded=\"entryText = $event\"\n />\n </template>\n\n <!-- list: the file tree -->\n <template v-else>\n <div class=\"zip__summary\">{{ fileCount }} 个文件 · {{ formatBytes(totalSize) }}</div>\n\n <!-- password bar: shown when the archive has encrypted entries (ZipCrypto / WinZip AES) -->\n <div v-if=\"anyEncrypted\" class=\"zip__pw\" :class=\"{ 'zip__pw--ok': unlocked }\">\n <Icon name=\"key\" :size=\"14\" />\n <input\n v-model=\"password\"\n type=\"password\"\n class=\"zip__pwinput\"\n :placeholder=\"unlocked ? '已解锁(如需可重新输入)' : '此压缩包已加密,输入密码解锁'\"\n @keyup.enter=\"applyPassword\"\n />\n <button type=\"button\" class=\"zip__pwbtn\" @click=\"applyPassword\">\n {{ unlocked ? '已解锁' : '解锁' }}\n </button>\n <span v-if=\"pwError\" class=\"zip__pwerr\">{{ pwError }}</span>\n </div>\n\n <div class=\"zip__scroll zip__tree\">\n <p v-if=\"rows.length === 0\" class=\"zip__msg\">空压缩包</p>\n <button\n v-for=\"row in rows\"\n :key=\"row.node.path\"\n type=\"button\"\n class=\"zip__row\"\n :class=\"{ 'zip__row--dir': row.node.isDir }\"\n :style=\"{ paddingLeft: `${12 + row.depth * 14}px` }\"\n @click=\"onRow(row.node)\"\n >\n <span class=\"zip__twisty\">\n <Icon\n v-if=\"row.node.isDir\"\n name=\"chevronRight\"\n :size=\"13\"\n :class=\"{ 'zip__twisty--open': expanded.has(row.node.path) }\"\n />\n </span>\n <span class=\"zip__rowicon\">\n <Icon :name=\"row.node.isDir ? 'folder' : 'fileOutline'\" :size=\"15\" />\n </span>\n <span class=\"zip__rowname\">{{ row.node.name }}</span>\n <Icon\n v-if=\"row.node.entry?.encrypted\"\n name=\"key\"\n :size=\"12\"\n class=\"zip__rowlock\"\n title=\"已加密\"\n />\n <span v-if=\"!row.node.isDir\" class=\"zip__rowsize\">{{ formatBytes(row.node.size) }}</span>\n </button>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.zip {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.zip__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.zip__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.zip__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.zip__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* List view ------------------------------------------------------------------------------------ */\n.zip__summary {\n flex-shrink: 0;\n padding: 9px 16px;\n border-bottom: 1px solid var(--border);\n font-size: 11.5px;\n letter-spacing: 0.02em;\n color: var(--faint);\n}\n/* Password bar (encrypted archives). */\n.zip__pw {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 8px 12px;\n border-bottom: 1px solid var(--border);\n background: var(--surface-2);\n color: var(--muted);\n}\n.zip__pw--ok {\n color: var(--accent);\n}\n.zip__pwinput {\n flex: 1 1 auto;\n min-width: 0;\n height: 28px;\n padding: 0 9px;\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: 7px;\n}\n.zip__pwinput:focus {\n outline: none;\n border-color: var(--accent);\n}\n.zip__pwbtn {\n flex-shrink: 0;\n height: 28px;\n padding: 0 12px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n}\n.zip__pwbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.zip__pwerr {\n flex-shrink: 0;\n font-size: 11.5px;\n color: var(--danger);\n}\n.zip__rowlock {\n flex-shrink: 0;\n color: var(--faint);\n}\n\n.zip__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.zip__tree {\n padding: 6px 0;\n}\n.zip__row {\n display: flex;\n align-items: center;\n gap: 7px;\n width: 100%;\n padding: 5px 12px 5px 12px;\n background: transparent;\n border: none;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n text-align: left;\n cursor: pointer;\n}\n.zip__row:hover {\n background: var(--surface-2);\n}\n.zip__twisty {\n flex-shrink: 0;\n display: inline-flex;\n width: 13px;\n color: var(--faint);\n}\n.zip__twisty--open {\n transform: rotate(90deg);\n transition: transform 0.12s;\n}\n.zip__rowicon {\n flex-shrink: 0;\n display: inline-flex;\n color: var(--faint);\n}\n.zip__row--dir .zip__rowicon {\n color: var(--accent);\n}\n.zip__rowname {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.zip__row--dir .zip__rowname {\n font-weight: 500;\n}\n.zip__rowsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n\n/* Detail bar ----------------------------------------------------------------------------------- */\n.zip__bar {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 7px 10px 7px 8px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.zip__back,\n.zip__act {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 30px;\n height: 28px;\n background: transparent;\n border: none;\n border-radius: 7px;\n color: var(--muted);\n cursor: pointer;\n transition:\n background 0.15s,\n color 0.15s;\n}\n.zip__back:hover,\n.zip__act:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.zip__path {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n text-align: left;\n font-size: 12.5px;\n font-weight: 500;\n color: var(--text-strong);\n}\n.zip__bsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n/* Render/source toggle (when the selected entry supports it) — compact segmented control. */\n.zip__view {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n gap: 2px;\n padding: 2px;\n border-radius: 7px;\n background: var(--surface-2);\n}\n.zip__vbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 24px;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 5px;\n color: var(--faint);\n cursor: pointer;\n}\n.zip__vbtn--on {\n background: var(--surface);\n color: var(--accent);\n box-shadow: var(--shadow);\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"ZipBrowser.js","names":[],"sources":["../../src/components/ZipBrowser.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport {\n computed,\n defineAsyncComponent,\n onBeforeUnmount,\n onMounted,\n reactive,\n ref,\n watch,\n} from 'vue'\nimport {\n extractZipEntry,\n readZip,\n zipSupported,\n ZipEncryptedError,\n ZipPasswordError,\n type ZipEntry,\n} from '@/lib/zip'\nimport type { DicomSlice } from '@/lib/dicom'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { useArchiveImageGallery } from '@/composables/useArchiveImageGallery'\nimport { fileExt, isDicomFile, isTextFile } from '@/lib/fileType'\nimport { canToggleView, classifyViewerFile, mediaMimeForName } from '@/lib/viewerKind'\nimport { buildArchiveHtmlPreview } from '@/lib/archiveHtml'\nimport { decodeTextSniff } from '@/lib/encoding'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n// A folder of sibling .dcm files browses as one scrollable series — FileViewerBody has no slice model, so the\n// DICOM stack stays a direct hand-off here (a lone .dcm goes through the body like any other entry).\nconst DicomViewer = defineAsyncComponent(() => import('@/components/DicomViewer.vue'))\n\n// The shared side-viewer body renders a selected entry with the SAME viewers as a top-level file. Lazy-imported\n// to break the module cycle (the body lazy-imports this browser back, for nested archives).\nconst FileViewerBody = defineAsyncComponent(() => import('@/components/FileViewerBody.vue'))\n\n/**\n * The side viewer's ZIP mode: fetch an archive's bytes, decompress them entirely in-browser (see lib/zip.ts),\n * and browse the contents. A master→detail within the narrow pane — a collapsible file tree, and selecting an\n * entry extracts just that one, wraps it in a blob URL, and hands it to {@link FileViewerBody} (so a file\n * inside the zip previews with every viewer a top-level file gets — code/image/pdf/cert/audio/nested-zip/…).\n * HTML is the one multi-entry case: its relative JS/CSS/image/font dependencies are resolved from the same ZIP\n * and rewritten before entering the existing sandboxed iframe. Other entry bytes are not retained; selecting\n * decodes on demand, and the download button re-extracts.\n */\nconst props = defineProps<{ url: string; name: string }>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nlet buffer: ArrayBuffer | null = null\nconst entries = ref<ZipEntry[]>([])\n\n// Encrypted archives: a password bar appears once any entry is encrypted. The password is held only in memory\n// and passed to each extraction (ZipCrypto / WinZip AES decrypt happens in lib/zip.ts).\nconst password = ref('')\nconst pwError = ref('')\nconst unlocked = ref(false)\nconst anyEncrypted = computed(() => entries.value.some((e) => e.encrypted))\n\ninterface TreeNode {\n name: string\n path: string\n isDir: boolean\n size: number\n entry?: ZipEntry\n children: TreeNode[]\n}\nconst root = ref<TreeNode>({ name: '', path: '', isDir: true, size: 0, children: [] })\nconst expanded = reactive(new Set<string>())\n\nconst fileCount = computed(() => entries.value.reduce((n, e) => n + (e.isDir ? 0 : 1), 0))\nconst totalSize = computed(() => entries.value.reduce((s, e) => s + (e.isDir ? 0 : e.size), 0))\n\n// Build a folder tree from the flat entry paths (directories may be implicit — derive them either way).\nfunction buildTree(list: ZipEntry[]): TreeNode {\n const rootNode: TreeNode = { name: '', path: '', isDir: true, size: 0, children: [] }\n const dirs = new Map<string, TreeNode>([['', rootNode]])\n const ensureDir = (segs: string[]): TreeNode => {\n let cur = rootNode\n let acc = ''\n for (const seg of segs) {\n acc = acc ? `${acc}/${seg}` : seg\n let node = dirs.get(acc)\n if (!node) {\n node = { name: seg, path: acc, isDir: true, size: 0, children: [] }\n dirs.set(acc, node)\n cur.children.push(node)\n }\n cur = node\n }\n return cur\n }\n for (const e of list) {\n const segs = e.path.split('/').filter(Boolean)\n if (e.isDir) {\n ensureDir(segs)\n continue\n }\n const fname = segs.pop()\n if (!fname) continue\n ensureDir(segs).children.push({\n name: fname,\n path: e.path,\n isDir: false,\n size: e.size,\n entry: e,\n children: [],\n })\n }\n sortNode(rootNode)\n return rootNode\n}\n// Folders first, then files, each alphabetical (case-insensitive).\nfunction sortNode(n: TreeNode): void {\n n.children.sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1))\n for (const c of n.children) if (c.isDir) sortNode(c)\n}\n\n// Flatten the tree to the rows currently visible (respecting which folders are expanded).\ninterface Row {\n node: TreeNode\n depth: number\n}\nconst rows = computed<Row[]>(() => {\n const out: Row[] = []\n const walk = (nodes: TreeNode[], depth: number): void => {\n for (const n of nodes) {\n out.push({ node: n, depth })\n if (n.isDir && expanded.has(n.path)) walk(n.children, depth + 1)\n }\n }\n walk(root.value.children, 0)\n return out\n})\n\nasync function load(): Promise<void> {\n selectionRun++\n imageGallery.clear()\n status.value = 'loading'\n errorMsg.value = ''\n selected.value = null\n entryFile.value = null\n entryHtmlPreview.value = ''\n password.value = ''\n pwError.value = ''\n unlocked.value = false\n revokeBlob()\n stackSlices.value = []\n if (!zipSupported()) {\n errorMsg.value = '当前浏览器不支持在前端解压 ZIP(缺少 DecompressionStream)'\n status.value = 'error'\n return\n }\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n buffer = await res.arrayBuffer()\n const list = readZip(buffer)\n entries.value = list\n root.value = buildTree(list)\n expanded.clear()\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\n// ── selected entry (detail view) ──────────────────────────────────────────────────────────────\nconst selected = ref<ZipEntry | null>(null)\nconst entryStatus = ref<Status>('ready') // covers extraction; the body owns loading once entryFile is set\nconst entryErr = ref('')\nconst entryText = ref('') // the body emits loaded text up so the copy button can use it\nconst entryFile = ref<ViewerFile | null>(null)\nconst entryHtmlPreview = ref('') // dependency-rewritten HTML; source/copy still use the original text\nconst entryStack = ref(false) // DICOM series (handled directly, not via the body)\nconst entryMode = ref<'render' | 'source'>('render')\nconst blobUrl = ref('') // entry blob URL fed to the body (revoked on change/unmount)\nlet selectionRun = 0 // stale guard for async HTML dependency extraction (including selecting the same row twice)\n\n// DICOM series stack: when the clicked .dcm has sibling .dcm files in the same folder, they are browsed as\n// one scrollable series. Slices extract lazily; the DICOM viewer caches/prefetches them.\nconst stackSlices = ref<DicomSlice[]>([])\nconst stackStart = ref(0)\nconst stackName = ref('')\nconst stackTotalSize = computed(() => stackSlices.value.reduce((s, sl) => s + (sl.size ?? 0), 0))\n\nconst canToggle = computed(() => canToggleView(entryFile.value, entryText.value))\n\nfunction revokeBlob(): void {\n if (blobUrl.value) {\n URL.revokeObjectURL(blobUrl.value)\n blobUrl.value = ''\n }\n}\n\nfunction dirOf(path: string): string {\n const i = path.lastIndexOf('/')\n return i < 0 ? '' : path.slice(0, i)\n}\n// Sibling .dcm files in the same folder, natural-sorted (numeric-aware) so e.g. img2 precedes img10.\nfunction dicomSiblings(e: ZipEntry): ZipEntry[] {\n const dir = dirOf(e.path)\n return entries.value\n .filter((x) => !x.isDir && isDicomFile(x.name) && dirOf(x.path) === dir)\n .sort((a, b) => a.path.localeCompare(b.path, undefined, { numeric: true, sensitivity: 'base' }))\n}\n\n// Treat as text if the name says so; for extensionless entries, sniff the first bytes for NULs. Feeds the\n// body's source fall-back so an extensionless text file still opens as source instead of a download card.\nfunction looksText(bytes: Uint8Array, name: string): boolean {\n if (isTextFile(name)) return true\n if (fileExt(name)) return false\n const n = Math.min(bytes.length, 4096)\n for (let i = 0; i < n; i++) if (bytes[i] === 0) return false\n return true\n}\n\nconst imageGallery = useArchiveImageGallery<ZipEntry>({\n entries: () => entries.value,\n archiveName: () => props.name,\n extract: async (entry) => {\n if (!buffer) throw new Error('ZIP 尚未加载')\n return extractZipEntry(buffer, entry, password.value)\n },\n})\n\nasync function selectEntry(e: ZipEntry): Promise<void> {\n const run = ++selectionRun\n let pendingUrl = ''\n selected.value = e\n entryStatus.value = 'loading'\n entryErr.value = ''\n entryText.value = ''\n entryFile.value = null\n entryHtmlPreview.value = ''\n entryStack.value = false\n entryMode.value = 'render'\n revokeBlob()\n stackSlices.value = []\n if (!buffer) return\n try {\n // DICOM series: a folder holding several .dcm → browse them as one scrollable series (slices on demand).\n if (isDicomFile(e.name)) {\n const siblings = dicomSiblings(e)\n if (siblings.length > 1) {\n stackSlices.value = siblings.map((se) => ({\n name: se.name,\n size: se.size,\n load: async () => {\n const u = await extractZipEntry(buffer as ArrayBuffer, se, password.value)\n const out = new ArrayBuffer(u.byteLength)\n new Uint8Array(out).set(u)\n return out\n },\n }))\n stackStart.value = Math.max(\n 0,\n siblings.findIndex((se) => se.path === e.path),\n )\n stackName.value = dirOf(e.path).split('/').pop() || e.name\n entryStack.value = true\n entryStatus.value = 'ready'\n return\n }\n }\n const archiveBuffer = buffer\n const bytes = await extractZipEntry(archiveBuffer, e, password.value)\n if (selectionRun !== run || selected.value !== e) return\n const url = URL.createObjectURL(new Blob([bytes], { type: mediaMimeForName(e.name) }))\n pendingUrl = url\n const file = classifyViewerFile({\n name: e.name,\n url,\n size: e.size,\n textHint: looksText(bytes, e.name),\n })\n let htmlPreview = ''\n if (file.html) {\n const source = decodeTextSniff(bytes)\n file.content = source\n const preview = await buildArchiveHtmlPreview({\n html: source,\n documentPath: e.path,\n entries: entries.value,\n loadEntry: (dependency) => extractZipEntry(archiveBuffer, dependency, password.value),\n })\n if (selectionRun !== run || selected.value !== e) {\n URL.revokeObjectURL(url)\n pendingUrl = ''\n return\n }\n htmlPreview = preview.html\n }\n if (selectionRun !== run || selected.value !== e) {\n URL.revokeObjectURL(url)\n pendingUrl = ''\n return\n }\n blobUrl.value = url\n pendingUrl = ''\n entryHtmlPreview.value = htmlPreview\n entryFile.value = file\n entryStatus.value = 'ready'\n if (file.image) void imageGallery.open(e, bytes)\n } catch (err) {\n if (pendingUrl) URL.revokeObjectURL(pendingUrl)\n if (err instanceof ZipEncryptedError)\n entryErr.value = '该文件已加密:请返回列表并在顶部输入密码'\n else if (err instanceof ZipPasswordError) {\n entryErr.value = '密码错误'\n unlocked.value = false\n } else entryErr.value = err instanceof Error ? err.message : String(err)\n entryStatus.value = 'error'\n }\n}\n\n// Validate the entered password against the first encrypted entry, then unlock (and re-open any selected file).\nasync function applyPassword(): Promise<void> {\n pwError.value = ''\n const enc = entries.value.find((e) => !e.isDir && e.encrypted && e.size > 0)\n if (!enc || !buffer) {\n unlocked.value = true\n return\n }\n try {\n await extractZipEntry(buffer, enc, password.value)\n unlocked.value = true\n if (selected.value) void selectEntry(selected.value)\n } catch (e) {\n unlocked.value = false\n pwError.value =\n e instanceof ZipPasswordError ? '密码错误' : e instanceof Error ? e.message : String(e)\n }\n}\n\nfunction onRow(node: TreeNode): void {\n if (node.isDir) {\n if (expanded.has(node.path)) expanded.delete(node.path)\n else expanded.add(node.path)\n } else if (node.entry) {\n void selectEntry(node.entry)\n }\n}\n\nfunction back(): void {\n selectionRun++\n selected.value = null\n entryText.value = ''\n entryFile.value = null\n entryHtmlPreview.value = ''\n entryStack.value = false\n revokeBlob()\n stackSlices.value = []\n}\n\n// Re-extract on demand so we don't retain decompressed bytes for the whole session.\nasync function downloadEntry(e: ZipEntry): Promise<void> {\n if (!buffer) return\n try {\n const url = URL.createObjectURL(new Blob([await extractZipEntry(buffer, e, password.value)]))\n const a = document.createElement('a')\n a.href = url\n a.download = e.name\n document.body.appendChild(a)\n a.click()\n a.remove()\n setTimeout(() => URL.revokeObjectURL(url), 1000)\n } catch {\n /* extraction failed — nothing to download */\n }\n}\n\nconst copied = ref(false)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyEntry(): Promise<void> {\n try {\n await navigator.clipboard?.writeText(entryText.value)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copied.value = true\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copied.value = false), 1400)\n}\n\nonMounted(load)\nwatch(() => props.url, load)\nonBeforeUnmount(() => {\n selectionRun++\n revokeBlob()\n clearTimeout(copyTimer)\n})\n</script>\n\n<template>\n <div class=\"zip\">\n <p v-if=\"status === 'loading'\" class=\"zip__msg\">正在解压…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"zip__msg zip__msg--error\">\n <p>{{ errorMsg }}</p>\n <button type=\"button\" class=\"zip__retry\" @click=\"load\">重试</button>\n </div>\n\n <!-- detail: one selected entry -->\n <template v-else-if=\"selected\">\n <div class=\"zip__bar\">\n <button type=\"button\" class=\"zip__back\" title=\"返回列表\" @click=\"back\">\n <Icon name=\"arrowLeft\" :size=\"16\" />\n </button>\n <span class=\"zip__path\" :title=\"selected.path\">{{ selected.path }}</span>\n <span class=\"zip__bsize\">{{ formatBytes(selected.size) }}</span>\n <div v-if=\"canToggle\" class=\"zip__view\" role=\"group\" aria-label=\"渲染或源码\">\n <button\n type=\"button\"\n class=\"zip__vbtn\"\n :class=\"{ 'zip__vbtn--on': entryMode === 'render' }\"\n title=\"渲染\"\n @click=\"entryMode = 'render'\"\n >\n <Icon name=\"eye\" :size=\"14\" />\n </button>\n <button\n type=\"button\"\n class=\"zip__vbtn\"\n :class=\"{ 'zip__vbtn--on': entryMode === 'source' }\"\n title=\"源码\"\n @click=\"entryMode = 'source'\"\n >\n <Icon name=\"code\" :size=\"14\" />\n </button>\n </div>\n <button\n v-if=\"entryText && entryStatus === 'ready'\"\n type=\"button\"\n class=\"zip__act\"\n :title=\"copied ? '已复制' : '复制'\"\n @click=\"copyEntry\"\n >\n <Icon name=\"copy\" :size=\"14\" />\n </button>\n <button type=\"button\" class=\"zip__act\" title=\"下载\" @click=\"downloadEntry(selected)\">\n <Icon name=\"download\" :size=\"15\" />\n </button>\n </div>\n\n <p v-if=\"entryStatus === 'loading'\" class=\"zip__msg\">读取中…</p>\n <div v-else-if=\"entryStatus === 'error'\" class=\"zip__msg zip__msg--error\">\n <p>{{ entryErr }}</p>\n </div>\n\n <!-- dicom series: a whole folder of .dcm slices browsed as one scrollable series (wheel/cine翻层) -->\n <DicomViewer\n v-else-if=\"entryStack\"\n :slices=\"stackSlices\"\n :start=\"stackStart\"\n :name=\"stackName\"\n :size=\"stackTotalSize\"\n />\n\n <!-- everything else: the shared body picks the right viewer from the extracted blob -->\n <FileViewerBody\n v-else-if=\"entryFile\"\n :file=\"entryFile\"\n :mode=\"entryMode\"\n :html-preview=\"entryHtmlPreview || undefined\"\n @loaded=\"entryText = $event\"\n />\n </template>\n\n <!-- list: the file tree -->\n <template v-else>\n <div class=\"zip__summary\">{{ fileCount }} 个文件 · {{ formatBytes(totalSize) }}</div>\n\n <!-- password bar: shown when the archive has encrypted entries (ZipCrypto / WinZip AES) -->\n <div v-if=\"anyEncrypted\" class=\"zip__pw\" :class=\"{ 'zip__pw--ok': unlocked }\">\n <Icon name=\"key\" :size=\"14\" />\n <input\n v-model=\"password\"\n type=\"password\"\n class=\"zip__pwinput\"\n :placeholder=\"unlocked ? '已解锁(如需可重新输入)' : '此压缩包已加密,输入密码解锁'\"\n @keyup.enter=\"applyPassword\"\n />\n <button type=\"button\" class=\"zip__pwbtn\" @click=\"applyPassword\">\n {{ unlocked ? '已解锁' : '解锁' }}\n </button>\n <span v-if=\"pwError\" class=\"zip__pwerr\">{{ pwError }}</span>\n </div>\n\n <div class=\"zip__scroll zip__tree\">\n <p v-if=\"rows.length === 0\" class=\"zip__msg\">空压缩包</p>\n <button\n v-for=\"row in rows\"\n :key=\"row.node.path\"\n type=\"button\"\n class=\"zip__row\"\n :class=\"{ 'zip__row--dir': row.node.isDir }\"\n :style=\"{ paddingLeft: `${12 + row.depth * 14}px` }\"\n @click=\"onRow(row.node)\"\n >\n <span class=\"zip__twisty\">\n <Icon\n v-if=\"row.node.isDir\"\n name=\"chevronRight\"\n :size=\"13\"\n :class=\"{ 'zip__twisty--open': expanded.has(row.node.path) }\"\n />\n </span>\n <span class=\"zip__rowicon\">\n <Icon :name=\"row.node.isDir ? 'folder' : 'fileOutline'\" :size=\"15\" />\n </span>\n <span class=\"zip__rowname\">{{ row.node.name }}</span>\n <Icon\n v-if=\"row.node.entry?.encrypted\"\n name=\"key\"\n :size=\"12\"\n class=\"zip__rowlock\"\n title=\"已加密\"\n />\n <span v-if=\"!row.node.isDir\" class=\"zip__rowsize\">{{ formatBytes(row.node.size) }}</span>\n </button>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.zip {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.zip__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.zip__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.zip__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.zip__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* List view ------------------------------------------------------------------------------------ */\n.zip__summary {\n flex-shrink: 0;\n padding: 9px 16px;\n border-bottom: 1px solid var(--border);\n font-size: 11.5px;\n letter-spacing: 0.02em;\n color: var(--faint);\n}\n/* Password bar (encrypted archives). */\n.zip__pw {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 8px 12px;\n border-bottom: 1px solid var(--border);\n background: var(--surface-2);\n color: var(--muted);\n}\n.zip__pw--ok {\n color: var(--accent);\n}\n.zip__pwinput {\n flex: 1 1 auto;\n min-width: 0;\n height: 28px;\n padding: 0 9px;\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: 7px;\n}\n.zip__pwinput:focus {\n outline: none;\n border-color: var(--accent);\n}\n.zip__pwbtn {\n flex-shrink: 0;\n height: 28px;\n padding: 0 12px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n}\n.zip__pwbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.zip__pwerr {\n flex-shrink: 0;\n font-size: 11.5px;\n color: var(--danger);\n}\n.zip__rowlock {\n flex-shrink: 0;\n color: var(--faint);\n}\n\n.zip__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.zip__tree {\n padding: 6px 0;\n}\n.zip__row {\n display: flex;\n align-items: center;\n gap: 7px;\n width: 100%;\n padding: 5px 12px 5px 12px;\n background: transparent;\n border: none;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n text-align: left;\n cursor: pointer;\n}\n.zip__row:hover {\n background: var(--surface-2);\n}\n.zip__twisty {\n flex-shrink: 0;\n display: inline-flex;\n width: 13px;\n color: var(--faint);\n}\n.zip__twisty--open {\n transform: rotate(90deg);\n transition: transform 0.12s;\n}\n.zip__rowicon {\n flex-shrink: 0;\n display: inline-flex;\n color: var(--faint);\n}\n.zip__row--dir .zip__rowicon {\n color: var(--accent);\n}\n.zip__rowname {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.zip__row--dir .zip__rowname {\n font-weight: 500;\n}\n.zip__rowsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n\n/* Detail bar ----------------------------------------------------------------------------------- */\n.zip__bar {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 7px 10px 7px 8px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.zip__back,\n.zip__act {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 30px;\n height: 28px;\n background: transparent;\n border: none;\n border-radius: 7px;\n color: var(--muted);\n cursor: pointer;\n transition:\n background 0.15s,\n color 0.15s;\n}\n.zip__back:hover,\n.zip__act:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.zip__path {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n text-align: left;\n font-size: 12.5px;\n font-weight: 500;\n color: var(--text-strong);\n}\n.zip__bsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n/* Render/source toggle (when the selected entry supports it) — compact segmented control. */\n.zip__view {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n gap: 2px;\n padding: 2px;\n border-radius: 7px;\n background: var(--surface-2);\n}\n.zip__vbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 24px;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 5px;\n color: var(--faint);\n cursor: pointer;\n}\n.zip__vbtn--on {\n background: var(--surface);\n color: var(--accent);\n box-shadow: var(--shadow);\n}\n</style>\n"],"mappings":""}