@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
@@ -2,7 +2,7 @@ import e from "../_virtual/_plugin-vue_export-helper.js";
2
2
  import t from "./ArchiveBrowser.vue_vue_type_script_setup_true_lang.js";
3
3
  /* empty css */
4
4
  //#region src/components/ArchiveBrowser.vue
5
- var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-9693d0e6"]]);
5
+ var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-f359b121"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
8
 
@@ -1 +1 @@
1
- {"version":3,"file":"ArchiveBrowser.js","names":[],"sources":["../../src/components/ArchiveBrowser.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 type { ArchiveEntry, OpenedArchive } from '@/lib/archive'\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 generic archive mode — the multi-file browser shared by the tar family (tar/gz/xz/bz2) and\n * rar. Same master→detail UX as {@link ZipBrowser}: a collapsible file tree; selecting an entry extracts just\n * that one, wraps it in a blob URL, and hands it to {@link FileViewerBody} (so a file inside the archive\n * previews with every viewer a top-level file gets).\n *\n * The format-specific work lives behind {@link OpenedArchive}: this component fetches the bytes, lazy-imports\n * the right opener by `kind` (so neither the tar codecs nor the rar WASM ship in the main bundle), and drives\n * everything through `entries` + `extract()`. Extraction may be sync (tar) or async (rar) — every call is awaited.\n */\nconst props = defineProps<{ url: string; name: string; kind: 'tar' | 'rar' }>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nlet archive: OpenedArchive | null = null\nconst entries = ref<ArchiveEntry[]>([])\n\ninterface TreeNode {\n name: string\n path: string\n isDir: boolean\n size: number\n entry?: ArchiveEntry\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: ArchiveEntry[]): 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 archive = null\n revokeBlob()\n stackSlices.value = []\n const url = props.url\n try {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buffer = await res.arrayBuffer()\n // Lazy-import the opener so the tar codecs / rar WASM stay out of the main bundle until needed.\n const opened =\n props.kind === 'rar'\n ? await (await import('@/lib/rar')).openRar(buffer)\n : await (await import('@/lib/tarball')).openTarball(buffer, props.name)\n if (props.url !== url) return // a newer file superseded us while decoding\n archive = opened\n entries.value = opened.entries\n root.value = buildTree(opened.entries)\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<ArchiveEntry | 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).\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: ArchiveEntry): ArchiveEntry[] {\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\n// Copy out a freshly-allocated ArrayBuffer (a nested viewer/blob must own its bytes, not alias the archive).\nfunction toArrayBuffer(u: Uint8Array): ArrayBuffer {\n const out = new ArrayBuffer(u.byteLength)\n new Uint8Array(out).set(u)\n return out\n}\n\nasync function selectEntry(e: ArchiveEntry): Promise<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 (!archive) return\n const arc = archive\n try {\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 () => toArrayBuffer(await arc.extract(se)),\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 = await arc.extract(e)\n if (selected.value !== e) return // a newer selection superseded us mid-extract\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 = err instanceof Error ? err.message : String(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 void 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\nasync function downloadEntry(e: ArchiveEntry): Promise<void> {\n if (!archive) return\n try {\n const url = URL.createObjectURL(new Blob([await archive.extract(e)]))\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=\"ar\">\n <p v-if=\"status === 'loading'\" class=\"ar__msg\">正在解压…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"ar__msg ar__msg--error\">\n <p>{{ errorMsg }}</p>\n <button type=\"button\" class=\"ar__retry\" @click=\"load\">重试</button>\n </div>\n\n <!-- detail: one selected entry -->\n <template v-else-if=\"selected\">\n <div class=\"ar__bar\">\n <button type=\"button\" class=\"ar__back\" title=\"返回列表\" @click=\"back\">\n <Icon name=\"arrowLeft\" :size=\"16\" />\n </button>\n <span class=\"ar__path\" :title=\"selected.path\">{{ selected.path }}</span>\n <span class=\"ar__bsize\">{{ formatBytes(selected.size) }}</span>\n <div v-if=\"canToggle\" class=\"ar__view\" role=\"group\" aria-label=\"渲染或源码\">\n <button\n type=\"button\"\n class=\"ar__vbtn\"\n :class=\"{ 'ar__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=\"ar__vbtn\"\n :class=\"{ 'ar__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=\"ar__act\"\n :title=\"copied ? '已复制' : '复制'\"\n @click=\"copyEntry\"\n >\n <Icon name=\"copy\" :size=\"14\" />\n </button>\n <button type=\"button\" class=\"ar__act\" title=\"下载\" @click=\"downloadEntry(selected)\">\n <Icon name=\"download\" :size=\"15\" />\n </button>\n </div>\n\n <p v-if=\"entryStatus === 'loading'\" class=\"ar__msg\">读取中…</p>\n <div v-else-if=\"entryStatus === 'error'\" class=\"ar__msg ar__msg--error\">\n <p>{{ entryErr }}</p>\n </div>\n\n <!-- dicom series: a whole folder of .dcm slices browsed as one scrollable series -->\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=\"ar__summary\">{{ fileCount }} 个文件 · {{ formatBytes(totalSize) }}</div>\n <div class=\"ar__scroll ar__tree\">\n <p v-if=\"rows.length === 0\" class=\"ar__msg\">空压缩包</p>\n <button\n v-for=\"row in rows\"\n :key=\"row.node.path\"\n type=\"button\"\n class=\"ar__row\"\n :class=\"{ 'ar__row--dir': row.node.isDir }\"\n :style=\"{ paddingLeft: `${12 + row.depth * 14}px` }\"\n @click=\"onRow(row.node)\"\n >\n <span class=\"ar__twisty\">\n <Icon\n v-if=\"row.node.isDir\"\n name=\"chevronRight\"\n :size=\"13\"\n :class=\"{ 'ar__twisty--open': expanded.has(row.node.path) }\"\n />\n </span>\n <span class=\"ar__rowicon\">\n <Icon :name=\"row.node.isDir ? 'folder' : 'fileOutline'\" :size=\"15\" />\n </span>\n <span class=\"ar__rowname\">{{ row.node.name }}</span>\n <span v-if=\"!row.node.isDir\" class=\"ar__rowsize\">{{ formatBytes(row.node.size) }}</span>\n </button>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.ar {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.ar__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.ar__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.ar__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.ar__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* List view ------------------------------------------------------------------------------------ */\n.ar__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.ar__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.ar__tree {\n padding: 6px 0;\n}\n.ar__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.ar__row:hover {\n background: var(--surface-2);\n}\n.ar__twisty {\n flex-shrink: 0;\n display: inline-flex;\n width: 13px;\n color: var(--faint);\n}\n.ar__twisty--open {\n transform: rotate(90deg);\n transition: transform 0.12s;\n}\n.ar__rowicon {\n flex-shrink: 0;\n display: inline-flex;\n color: var(--faint);\n}\n.ar__row--dir .ar__rowicon {\n color: var(--accent);\n}\n.ar__rowname {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.ar__row--dir .ar__rowname {\n font-weight: 500;\n}\n.ar__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.ar__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.ar__back,\n.ar__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.ar__back:hover,\n.ar__act:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.ar__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.ar__bsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n.ar__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.ar__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.ar__vbtn--on {\n background: var(--surface);\n color: var(--accent);\n box-shadow: var(--shadow);\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"ArchiveBrowser.js","names":[],"sources":["../../src/components/ArchiveBrowser.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 type { ArchiveEntry, OpenedArchive } from '@/lib/archive'\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 generic archive mode — the multi-file browser shared by the tar family (tar/gz/xz/bz2) and\n * rar. Same master→detail UX as {@link ZipBrowser}: a collapsible file tree; selecting an entry extracts just\n * that one, wraps it in a blob URL, and hands it to {@link FileViewerBody} (so a file inside the archive\n * previews with every viewer a top-level file gets).\n *\n * The format-specific work lives behind {@link OpenedArchive}: this component fetches the bytes, lazy-imports\n * the right opener by `kind` (so neither the tar codecs nor the rar WASM ship in the main bundle), and drives\n * everything through `entries` + `extract()`. Extraction may be sync (tar) or async (rar) — every call is awaited.\n */\nconst props = defineProps<{ url: string; name: string; kind: 'tar' | 'rar' }>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nlet archive: OpenedArchive | null = null\nconst entries = ref<ArchiveEntry[]>([])\n\ninterface TreeNode {\n name: string\n path: string\n isDir: boolean\n size: number\n entry?: ArchiveEntry\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: ArchiveEntry[]): 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 archive = null\n revokeBlob()\n stackSlices.value = []\n const url = props.url\n try {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buffer = await res.arrayBuffer()\n // Lazy-import the opener so the tar codecs / rar WASM stay out of the main bundle until needed.\n const opened =\n props.kind === 'rar'\n ? await (await import('@/lib/rar')).openRar(buffer)\n : await (await import('@/lib/tarball')).openTarball(buffer, props.name)\n if (props.url !== url) return // a newer file superseded us while decoding\n archive = opened\n entries.value = opened.entries\n root.value = buildTree(opened.entries)\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<ArchiveEntry | 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).\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: ArchiveEntry): ArchiveEntry[] {\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<ArchiveEntry>({\n entries: () => entries.value,\n archiveName: () => props.name,\n extract: async (entry) => {\n if (!archive) throw new Error('压缩包尚未加载')\n return archive.extract(entry)\n },\n})\n\n// Copy out a freshly-allocated ArrayBuffer (a nested viewer/blob must own its bytes, not alias the archive).\nfunction toArrayBuffer(u: Uint8Array): ArrayBuffer {\n const out = new ArrayBuffer(u.byteLength)\n new Uint8Array(out).set(u)\n return out\n}\n\nasync function selectEntry(e: ArchiveEntry): Promise<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 (!archive) return\n const arc = archive\n try {\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 () => toArrayBuffer(await arc.extract(se)),\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 = await arc.extract(e)\n if (selected.value !== e) return // a newer selection superseded us mid-extract\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 = err instanceof Error ? err.message : String(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 void 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\nasync function downloadEntry(e: ArchiveEntry): Promise<void> {\n if (!archive) return\n try {\n const url = URL.createObjectURL(new Blob([await archive.extract(e)]))\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=\"ar\">\n <p v-if=\"status === 'loading'\" class=\"ar__msg\">正在解压…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"ar__msg ar__msg--error\">\n <p>{{ errorMsg }}</p>\n <button type=\"button\" class=\"ar__retry\" @click=\"load\">重试</button>\n </div>\n\n <!-- detail: one selected entry -->\n <template v-else-if=\"selected\">\n <div class=\"ar__bar\">\n <button type=\"button\" class=\"ar__back\" title=\"返回列表\" @click=\"back\">\n <Icon name=\"arrowLeft\" :size=\"16\" />\n </button>\n <span class=\"ar__path\" :title=\"selected.path\">{{ selected.path }}</span>\n <span class=\"ar__bsize\">{{ formatBytes(selected.size) }}</span>\n <div v-if=\"canToggle\" class=\"ar__view\" role=\"group\" aria-label=\"渲染或源码\">\n <button\n type=\"button\"\n class=\"ar__vbtn\"\n :class=\"{ 'ar__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=\"ar__vbtn\"\n :class=\"{ 'ar__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=\"ar__act\"\n :title=\"copied ? '已复制' : '复制'\"\n @click=\"copyEntry\"\n >\n <Icon name=\"copy\" :size=\"14\" />\n </button>\n <button type=\"button\" class=\"ar__act\" title=\"下载\" @click=\"downloadEntry(selected)\">\n <Icon name=\"download\" :size=\"15\" />\n </button>\n </div>\n\n <p v-if=\"entryStatus === 'loading'\" class=\"ar__msg\">读取中…</p>\n <div v-else-if=\"entryStatus === 'error'\" class=\"ar__msg ar__msg--error\">\n <p>{{ entryErr }}</p>\n </div>\n\n <!-- dicom series: a whole folder of .dcm slices browsed as one scrollable series -->\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=\"ar__summary\">{{ fileCount }} 个文件 · {{ formatBytes(totalSize) }}</div>\n <div class=\"ar__scroll ar__tree\">\n <p v-if=\"rows.length === 0\" class=\"ar__msg\">空压缩包</p>\n <button\n v-for=\"row in rows\"\n :key=\"row.node.path\"\n type=\"button\"\n class=\"ar__row\"\n :class=\"{ 'ar__row--dir': row.node.isDir }\"\n :style=\"{ paddingLeft: `${12 + row.depth * 14}px` }\"\n @click=\"onRow(row.node)\"\n >\n <span class=\"ar__twisty\">\n <Icon\n v-if=\"row.node.isDir\"\n name=\"chevronRight\"\n :size=\"13\"\n :class=\"{ 'ar__twisty--open': expanded.has(row.node.path) }\"\n />\n </span>\n <span class=\"ar__rowicon\">\n <Icon :name=\"row.node.isDir ? 'folder' : 'fileOutline'\" :size=\"15\" />\n </span>\n <span class=\"ar__rowname\">{{ row.node.name }}</span>\n <span v-if=\"!row.node.isDir\" class=\"ar__rowsize\">{{ formatBytes(row.node.size) }}</span>\n </button>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.ar {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.ar__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.ar__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.ar__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.ar__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* List view ------------------------------------------------------------------------------------ */\n.ar__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.ar__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.ar__tree {\n padding: 6px 0;\n}\n.ar__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.ar__row:hover {\n background: var(--surface-2);\n}\n.ar__twisty {\n flex-shrink: 0;\n display: inline-flex;\n width: 13px;\n color: var(--faint);\n}\n.ar__twisty--open {\n transform: rotate(90deg);\n transition: transform 0.12s;\n}\n.ar__rowicon {\n flex-shrink: 0;\n display: inline-flex;\n color: var(--faint);\n}\n.ar__row--dir .ar__rowicon {\n color: var(--accent);\n}\n.ar__rowname {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.ar__row--dir .ar__rowname {\n font-weight: 500;\n}\n.ar__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.ar__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.ar__back,\n.ar__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.ar__back:hover,\n.ar__act:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.ar__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.ar__bsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n.ar__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.ar__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.ar__vbtn--on {\n background: var(--surface);\n color: var(--accent);\n box-shadow: var(--shadow);\n}\n</style>\n"],"mappings":""}
@@ -1,8 +1,9 @@
1
1
  import { fileExt as e, isDicomFile as t, isTextFile as n } from "../lib/fileType.js";
2
- import { canToggleView as r, classifyViewerFile as i, mediaMimeForName as a } from "../lib/viewerKind.js";
3
- import { formatBytes as o } from "../lib/format.js";
4
- import s from "./icons/Icon.js";
5
- import { Fragment as c, computed as l, createBlock as u, createCommentVNode as d, createElementBlock as f, createElementVNode as p, createVNode as m, defineAsyncComponent as h, defineComponent as g, normalizeClass as _, normalizeStyle as ee, onBeforeUnmount as te, onMounted as v, openBlock as y, reactive as b, ref as x, renderList as S, toDisplayString as C, unref as w, watch as ne } from "vue";
2
+ import { canToggleView as r, classifyViewerFile as i, mediaMimeForName as ee } from "../lib/viewerKind.js";
3
+ import { formatBytes as a } from "../lib/format.js";
4
+ import o from "./icons/Icon.js";
5
+ import { useArchiveImageGallery as s } from "../composables/useArchiveImageGallery.js";
6
+ import { Fragment as c, computed as l, createBlock as u, createCommentVNode as d, createElementBlock as f, createElementVNode as p, createVNode as m, defineAsyncComponent as h, defineComponent as g, normalizeClass as _, normalizeStyle as v, onBeforeUnmount as y, onMounted as te, openBlock as b, reactive as x, ref as S, renderList as C, toDisplayString as w, unref as T, watch as ne } from "vue";
6
7
  //#region src/components/ArchiveBrowser.vue?vue&type=script&setup=true&lang.ts
7
8
  var re = { class: "ar" }, ie = {
8
9
  key: 0,
@@ -24,10 +25,10 @@ var re = { class: "ar" }, ie = {
24
25
  }, pe = { class: "ar__summary" }, me = { class: "ar__scroll ar__tree" }, he = {
25
26
  key: 0,
26
27
  class: "ar__msg"
27
- }, ge = ["onClick"], _e = { class: "ar__twisty" }, T = { class: "ar__rowicon" }, E = { class: "ar__rowname" }, D = {
28
+ }, ge = ["onClick"], _e = { class: "ar__twisty" }, ve = { class: "ar__rowicon" }, ye = { class: "ar__rowname" }, be = {
28
29
  key: 0,
29
30
  class: "ar__rowsize"
30
- }, O = /*@__PURE__*/ g({
31
+ }, E = /*@__PURE__*/ g({
31
32
  __name: "ArchiveBrowser",
32
33
  props: {
33
34
  url: {},
@@ -35,14 +36,14 @@ var re = { class: "ar" }, ie = {
35
36
  kind: {}
36
37
  },
37
38
  setup(g) {
38
- let O = h(() => import("./DicomViewer.js")), ve = h(() => import("./FileViewerBody.js")), k = g, A = x("loading"), j = x(""), M = null, N = x([]), P = x({
39
+ let E = h(() => import("./DicomViewer.js")), xe = h(() => import("./FileViewerBody.js")), D = g, O = S("loading"), k = S(""), A = null, j = S([]), M = S({
39
40
  name: "",
40
41
  path: "",
41
42
  isDir: !0,
42
43
  size: 0,
43
44
  children: []
44
- }), F = b(/* @__PURE__ */ new Set()), ye = l(() => N.value.reduce((e, t) => e + +!t.isDir, 0)), be = l(() => N.value.reduce((e, t) => e + (t.isDir ? 0 : t.size), 0));
45
- function xe(e) {
45
+ }), N = x(/* @__PURE__ */ new Set()), Se = l(() => j.value.reduce((e, t) => e + +!t.isDir, 0)), Ce = l(() => j.value.reduce((e, t) => e + (t.isDir ? 0 : t.size), 0));
46
+ function we(e) {
46
47
  let t = {
47
48
  name: "",
48
49
  path: "",
@@ -80,151 +81,161 @@ var re = { class: "ar" }, ie = {
80
81
  children: []
81
82
  });
82
83
  }
83
- return I(t), t;
84
+ return P(t), t;
84
85
  }
85
- function I(e) {
86
+ function P(e) {
86
87
  e.children.sort((e, t) => e.isDir === t.isDir ? e.name.localeCompare(t.name) : e.isDir ? -1 : 1);
87
- for (let t of e.children) t.isDir && I(t);
88
+ for (let t of e.children) t.isDir && P(t);
88
89
  }
89
- let L = l(() => {
90
+ let F = l(() => {
90
91
  let e = [], t = (n, r) => {
91
92
  for (let i of n) e.push({
92
93
  node: i,
93
94
  depth: r
94
- }), i.isDir && F.has(i.path) && t(i.children, r + 1);
95
+ }), i.isDir && N.has(i.path) && t(i.children, r + 1);
95
96
  };
96
- return t(P.value.children, 0), e;
97
+ return t(M.value.children, 0), e;
97
98
  });
98
- async function R() {
99
- A.value = "loading", j.value = "", z.value = null, M = null, X(), q.value = [];
100
- let e = k.url;
99
+ async function I() {
100
+ X.clear(), O.value = "loading", k.value = "", L.value = null, A = null, J(), G.value = [];
101
+ let e = D.url;
101
102
  try {
102
103
  let t = await fetch(e);
103
104
  if (!t.ok) throw Error(`HTTP ${t.status}`);
104
- let n = await t.arrayBuffer(), r = k.kind === "rar" ? await (await import("../lib/rar.js")).openRar(n) : await (await import("../lib/tarball.js")).openTarball(n, k.name);
105
- if (k.url !== e) return;
106
- M = r, N.value = r.entries, P.value = xe(r.entries), F.clear(), A.value = "ready";
105
+ let n = await t.arrayBuffer(), r = D.kind === "rar" ? await (await import("../lib/rar.js")).openRar(n) : await (await import("../lib/tarball.js")).openTarball(n, D.name);
106
+ if (D.url !== e) return;
107
+ A = r, j.value = r.entries, M.value = we(r.entries), N.clear(), O.value = "ready";
107
108
  } catch (e) {
108
- j.value = e instanceof Error ? e.message : String(e), A.value = "error";
109
+ k.value = e instanceof Error ? e.message : String(e), O.value = "error";
109
110
  }
110
111
  }
111
- let z = x(null), B = x("ready"), V = x(""), H = x(""), U = x(null), W = x(!1), G = x("render"), K = x(""), q = x([]), J = x(0), Y = x(""), Se = l(() => q.value.reduce((e, t) => e + (t.size ?? 0), 0)), Ce = l(() => r(U.value, H.value));
112
- function X() {
113
- K.value &&= (URL.revokeObjectURL(K.value), "");
112
+ let L = S(null), R = S("ready"), z = S(""), B = S(""), V = S(null), H = S(!1), U = S("render"), W = S(""), G = S([]), K = S(0), q = S(""), Te = l(() => G.value.reduce((e, t) => e + (t.size ?? 0), 0)), Ee = l(() => r(V.value, B.value));
113
+ function J() {
114
+ W.value &&= (URL.revokeObjectURL(W.value), "");
114
115
  }
115
- function Z(e) {
116
+ function Y(e) {
116
117
  let t = e.lastIndexOf("/");
117
118
  return t < 0 ? "" : e.slice(0, t);
118
119
  }
119
- function we(e) {
120
- let n = Z(e.path);
121
- return N.value.filter((e) => !e.isDir && t(e.name) && Z(e.path) === n).sort((e, t) => e.path.localeCompare(t.path, void 0, {
120
+ function De(e) {
121
+ let n = Y(e.path);
122
+ return j.value.filter((e) => !e.isDir && t(e.name) && Y(e.path) === n).sort((e, t) => e.path.localeCompare(t.path, void 0, {
122
123
  numeric: !0,
123
124
  sensitivity: "base"
124
125
  }));
125
126
  }
126
- function Te(t, r) {
127
+ function Oe(t, r) {
127
128
  if (n(r)) return !0;
128
129
  if (e(r)) return !1;
129
130
  let i = Math.min(t.length, 4096);
130
131
  for (let e = 0; e < i; e++) if (t[e] === 0) return !1;
131
132
  return !0;
132
133
  }
133
- function Ee(e) {
134
+ let X = s({
135
+ entries: () => j.value,
136
+ archiveName: () => D.name,
137
+ extract: async (e) => {
138
+ if (!A) throw Error("压缩包尚未加载");
139
+ return A.extract(e);
140
+ }
141
+ });
142
+ function ke(e) {
134
143
  let t = new ArrayBuffer(e.byteLength);
135
144
  return new Uint8Array(t).set(e), t;
136
145
  }
137
- async function De(e) {
138
- if (z.value = e, B.value = "loading", V.value = "", H.value = "", U.value = null, W.value = !1, G.value = "render", X(), q.value = [], !M) return;
139
- let n = M;
146
+ async function Z(e) {
147
+ if (L.value = e, R.value = "loading", z.value = "", B.value = "", V.value = null, H.value = !1, U.value = "render", J(), G.value = [], !A) return;
148
+ let n = A;
140
149
  try {
141
150
  if (t(e.name)) {
142
- let t = we(e);
151
+ let t = De(e);
143
152
  if (t.length > 1) {
144
- q.value = t.map((e) => ({
153
+ G.value = t.map((e) => ({
145
154
  name: e.name,
146
155
  size: e.size,
147
- load: async () => Ee(await n.extract(e))
148
- })), J.value = Math.max(0, t.findIndex((t) => t.path === e.path)), Y.value = Z(e.path).split("/").pop() || e.name, W.value = !0, B.value = "ready";
156
+ load: async () => ke(await n.extract(e))
157
+ })), K.value = Math.max(0, t.findIndex((t) => t.path === e.path)), q.value = Y(e.path).split("/").pop() || e.name, H.value = !0, R.value = "ready";
149
158
  return;
150
159
  }
151
160
  }
152
161
  let r = await n.extract(e);
153
- if (z.value !== e) return;
154
- K.value = URL.createObjectURL(new Blob([r], { type: a(e.name) })), U.value = i({
162
+ if (L.value !== e) return;
163
+ W.value = URL.createObjectURL(new Blob([r], { type: ee(e.name) }));
164
+ let a = i({
155
165
  name: e.name,
156
- url: K.value,
166
+ url: W.value,
157
167
  size: e.size,
158
- textHint: Te(r, e.name)
159
- }), B.value = "ready";
168
+ textHint: Oe(r, e.name)
169
+ });
170
+ V.value = a, R.value = "ready", a.image && X.open(e, r);
160
171
  } catch (e) {
161
- V.value = e instanceof Error ? e.message : String(e), B.value = "error";
172
+ z.value = e instanceof Error ? e.message : String(e), R.value = "error";
162
173
  }
163
174
  }
164
- function Oe(e) {
165
- e.isDir ? F.has(e.path) ? F.delete(e.path) : F.add(e.path) : e.entry && De(e.entry);
175
+ function Ae(e) {
176
+ e.isDir ? N.has(e.path) ? N.delete(e.path) : N.add(e.path) : e.entry && Z(e.entry);
166
177
  }
167
- function ke() {
168
- z.value = null, H.value = "", U.value = null, W.value = !1, X(), q.value = [];
178
+ function je() {
179
+ L.value = null, B.value = "", V.value = null, H.value = !1, J(), G.value = [];
169
180
  }
170
- async function Ae(e) {
171
- if (M) try {
172
- let t = URL.createObjectURL(new Blob([await M.extract(e)])), n = document.createElement("a");
181
+ async function Me(e) {
182
+ if (A) try {
183
+ let t = URL.createObjectURL(new Blob([await A.extract(e)])), n = document.createElement("a");
173
184
  n.href = t, n.download = e.name, document.body.appendChild(n), n.click(), n.remove(), setTimeout(() => URL.revokeObjectURL(t), 1e3);
174
185
  } catch {}
175
186
  }
176
- let Q = x(!1), $;
177
- async function je() {
187
+ let Q = S(!1), $;
188
+ async function Ne() {
178
189
  try {
179
- await navigator.clipboard?.writeText(H.value);
190
+ await navigator.clipboard?.writeText(B.value);
180
191
  } catch {}
181
192
  Q.value = !0, clearTimeout($), $ = setTimeout(() => Q.value = !1, 1400);
182
193
  }
183
- return v(R), ne(() => k.url, R), te(() => {
184
- X(), clearTimeout($);
185
- }), (e, t) => (y(), f("div", re, [A.value === "loading" ? (y(), f("p", ie, "正在解压…")) : A.value === "error" ? (y(), f("div", ae, [p("p", null, C(j.value), 1), p("button", {
194
+ return te(I), ne(() => D.url, I), y(() => {
195
+ J(), clearTimeout($);
196
+ }), (e, t) => (b(), f("div", re, [O.value === "loading" ? (b(), f("p", ie, "正在解压…")) : O.value === "error" ? (b(), f("div", ae, [p("p", null, w(k.value), 1), p("button", {
186
197
  type: "button",
187
198
  class: "ar__retry",
188
- onClick: R
189
- }, "重试")])) : z.value ? (y(), f(c, { key: 2 }, [p("div", oe, [
199
+ onClick: I
200
+ }, "重试")])) : L.value ? (b(), f(c, { key: 2 }, [p("div", oe, [
190
201
  p("button", {
191
202
  type: "button",
192
203
  class: "ar__back",
193
204
  title: "返回列表",
194
- onClick: ke
195
- }, [m(s, {
205
+ onClick: je
206
+ }, [m(o, {
196
207
  name: "arrowLeft",
197
208
  size: 16
198
209
  })]),
199
210
  p("span", {
200
211
  class: "ar__path",
201
- title: z.value.path
202
- }, C(z.value.path), 9, se),
203
- p("span", ce, C(w(o)(z.value.size)), 1),
204
- Ce.value ? (y(), f("div", le, [p("button", {
212
+ title: L.value.path
213
+ }, w(L.value.path), 9, se),
214
+ p("span", ce, w(T(a)(L.value.size)), 1),
215
+ Ee.value ? (b(), f("div", le, [p("button", {
205
216
  type: "button",
206
- class: _(["ar__vbtn", { "ar__vbtn--on": G.value === "render" }]),
217
+ class: _(["ar__vbtn", { "ar__vbtn--on": U.value === "render" }]),
207
218
  title: "渲染",
208
- onClick: t[0] ||= (e) => G.value = "render"
209
- }, [m(s, {
219
+ onClick: t[0] ||= (e) => U.value = "render"
220
+ }, [m(o, {
210
221
  name: "eye",
211
222
  size: 14
212
223
  })], 2), p("button", {
213
224
  type: "button",
214
- class: _(["ar__vbtn", { "ar__vbtn--on": G.value === "source" }]),
225
+ class: _(["ar__vbtn", { "ar__vbtn--on": U.value === "source" }]),
215
226
  title: "源码",
216
- onClick: t[1] ||= (e) => G.value = "source"
217
- }, [m(s, {
227
+ onClick: t[1] ||= (e) => U.value = "source"
228
+ }, [m(o, {
218
229
  name: "code",
219
230
  size: 14
220
231
  })], 2)])) : d("", !0),
221
- H.value && B.value === "ready" ? (y(), f("button", {
232
+ B.value && R.value === "ready" ? (b(), f("button", {
222
233
  key: 1,
223
234
  type: "button",
224
235
  class: "ar__act",
225
236
  title: Q.value ? "已复制" : "复制",
226
- onClick: je
227
- }, [m(s, {
237
+ onClick: Ne
238
+ }, [m(o, {
228
239
  name: "copy",
229
240
  size: 14
230
241
  })], 8, ue)) : d("", !0),
@@ -232,50 +243,50 @@ var re = { class: "ar" }, ie = {
232
243
  type: "button",
233
244
  class: "ar__act",
234
245
  title: "下载",
235
- onClick: t[2] ||= (e) => Ae(z.value)
236
- }, [m(s, {
246
+ onClick: t[2] ||= (e) => Me(L.value)
247
+ }, [m(o, {
237
248
  name: "download",
238
249
  size: 15
239
250
  })])
240
- ]), B.value === "loading" ? (y(), f("p", de, "读取中…")) : B.value === "error" ? (y(), f("div", fe, [p("p", null, C(V.value), 1)])) : W.value ? (y(), u(w(O), {
251
+ ]), R.value === "loading" ? (b(), f("p", de, "读取中…")) : R.value === "error" ? (b(), f("div", fe, [p("p", null, w(z.value), 1)])) : H.value ? (b(), u(T(E), {
241
252
  key: 2,
242
- slices: q.value,
243
- start: J.value,
244
- name: Y.value,
245
- size: Se.value
253
+ slices: G.value,
254
+ start: K.value,
255
+ name: q.value,
256
+ size: Te.value
246
257
  }, null, 8, [
247
258
  "slices",
248
259
  "start",
249
260
  "name",
250
261
  "size"
251
- ])) : U.value ? (y(), u(w(ve), {
262
+ ])) : V.value ? (b(), u(T(xe), {
252
263
  key: 3,
253
- file: U.value,
254
- mode: G.value,
255
- onLoaded: t[3] ||= (e) => H.value = e
256
- }, null, 8, ["file", "mode"])) : d("", !0)], 64)) : (y(), f(c, { key: 3 }, [p("div", pe, C(ye.value) + " 个文件 · " + C(w(o)(be.value)), 1), p("div", me, [L.value.length === 0 ? (y(), f("p", he, "空压缩包")) : d("", !0), (y(!0), f(c, null, S(L.value, (e) => (y(), f("button", {
264
+ file: V.value,
265
+ mode: U.value,
266
+ onLoaded: t[3] ||= (e) => B.value = e
267
+ }, null, 8, ["file", "mode"])) : d("", !0)], 64)) : (b(), f(c, { key: 3 }, [p("div", pe, w(Se.value) + " 个文件 · " + w(T(a)(Ce.value)), 1), p("div", me, [F.value.length === 0 ? (b(), f("p", he, "空压缩包")) : d("", !0), (b(!0), f(c, null, C(F.value, (e) => (b(), f("button", {
257
268
  key: e.node.path,
258
269
  type: "button",
259
270
  class: _(["ar__row", { "ar__row--dir": e.node.isDir }]),
260
- style: ee({ paddingLeft: `${12 + e.depth * 14}px` }),
261
- onClick: (t) => Oe(e.node)
271
+ style: v({ paddingLeft: `${12 + e.depth * 14}px` }),
272
+ onClick: (t) => Ae(e.node)
262
273
  }, [
263
- p("span", _e, [e.node.isDir ? (y(), u(s, {
274
+ p("span", _e, [e.node.isDir ? (b(), u(o, {
264
275
  key: 0,
265
276
  name: "chevronRight",
266
277
  size: 13,
267
- class: _({ "ar__twisty--open": F.has(e.node.path) })
278
+ class: _({ "ar__twisty--open": N.has(e.node.path) })
268
279
  }, null, 8, ["class"])) : d("", !0)]),
269
- p("span", T, [m(s, {
280
+ p("span", ve, [m(o, {
270
281
  name: e.node.isDir ? "folder" : "fileOutline",
271
282
  size: 15
272
283
  }, null, 8, ["name"])]),
273
- p("span", E, C(e.node.name), 1),
274
- e.node.isDir ? d("", !0) : (y(), f("span", D, C(w(o)(e.node.size)), 1))
284
+ p("span", ye, w(e.node.name), 1),
285
+ e.node.isDir ? d("", !0) : (b(), f("span", be, w(T(a)(e.node.size)), 1))
275
286
  ], 14, ge))), 128))])], 64))]));
276
287
  }
277
288
  });
278
289
  //#endregion
279
- export { O as default };
290
+ export { E as default };
280
291
 
281
292
  //# sourceMappingURL=ArchiveBrowser.vue_vue_type_script_setup_true_lang.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ArchiveBrowser.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/ArchiveBrowser.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 type { ArchiveEntry, OpenedArchive } from '@/lib/archive'\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 generic archive mode — the multi-file browser shared by the tar family (tar/gz/xz/bz2) and\n * rar. Same master→detail UX as {@link ZipBrowser}: a collapsible file tree; selecting an entry extracts just\n * that one, wraps it in a blob URL, and hands it to {@link FileViewerBody} (so a file inside the archive\n * previews with every viewer a top-level file gets).\n *\n * The format-specific work lives behind {@link OpenedArchive}: this component fetches the bytes, lazy-imports\n * the right opener by `kind` (so neither the tar codecs nor the rar WASM ship in the main bundle), and drives\n * everything through `entries` + `extract()`. Extraction may be sync (tar) or async (rar) — every call is awaited.\n */\nconst props = defineProps<{ url: string; name: string; kind: 'tar' | 'rar' }>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nlet archive: OpenedArchive | null = null\nconst entries = ref<ArchiveEntry[]>([])\n\ninterface TreeNode {\n name: string\n path: string\n isDir: boolean\n size: number\n entry?: ArchiveEntry\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: ArchiveEntry[]): 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 archive = null\n revokeBlob()\n stackSlices.value = []\n const url = props.url\n try {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buffer = await res.arrayBuffer()\n // Lazy-import the opener so the tar codecs / rar WASM stay out of the main bundle until needed.\n const opened =\n props.kind === 'rar'\n ? await (await import('@/lib/rar')).openRar(buffer)\n : await (await import('@/lib/tarball')).openTarball(buffer, props.name)\n if (props.url !== url) return // a newer file superseded us while decoding\n archive = opened\n entries.value = opened.entries\n root.value = buildTree(opened.entries)\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<ArchiveEntry | 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).\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: ArchiveEntry): ArchiveEntry[] {\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\n// Copy out a freshly-allocated ArrayBuffer (a nested viewer/blob must own its bytes, not alias the archive).\nfunction toArrayBuffer(u: Uint8Array): ArrayBuffer {\n const out = new ArrayBuffer(u.byteLength)\n new Uint8Array(out).set(u)\n return out\n}\n\nasync function selectEntry(e: ArchiveEntry): Promise<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 (!archive) return\n const arc = archive\n try {\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 () => toArrayBuffer(await arc.extract(se)),\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 = await arc.extract(e)\n if (selected.value !== e) return // a newer selection superseded us mid-extract\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 = err instanceof Error ? err.message : String(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 void 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\nasync function downloadEntry(e: ArchiveEntry): Promise<void> {\n if (!archive) return\n try {\n const url = URL.createObjectURL(new Blob([await archive.extract(e)]))\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=\"ar\">\n <p v-if=\"status === 'loading'\" class=\"ar__msg\">正在解压…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"ar__msg ar__msg--error\">\n <p>{{ errorMsg }}</p>\n <button type=\"button\" class=\"ar__retry\" @click=\"load\">重试</button>\n </div>\n\n <!-- detail: one selected entry -->\n <template v-else-if=\"selected\">\n <div class=\"ar__bar\">\n <button type=\"button\" class=\"ar__back\" title=\"返回列表\" @click=\"back\">\n <Icon name=\"arrowLeft\" :size=\"16\" />\n </button>\n <span class=\"ar__path\" :title=\"selected.path\">{{ selected.path }}</span>\n <span class=\"ar__bsize\">{{ formatBytes(selected.size) }}</span>\n <div v-if=\"canToggle\" class=\"ar__view\" role=\"group\" aria-label=\"渲染或源码\">\n <button\n type=\"button\"\n class=\"ar__vbtn\"\n :class=\"{ 'ar__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=\"ar__vbtn\"\n :class=\"{ 'ar__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=\"ar__act\"\n :title=\"copied ? '已复制' : '复制'\"\n @click=\"copyEntry\"\n >\n <Icon name=\"copy\" :size=\"14\" />\n </button>\n <button type=\"button\" class=\"ar__act\" title=\"下载\" @click=\"downloadEntry(selected)\">\n <Icon name=\"download\" :size=\"15\" />\n </button>\n </div>\n\n <p v-if=\"entryStatus === 'loading'\" class=\"ar__msg\">读取中…</p>\n <div v-else-if=\"entryStatus === 'error'\" class=\"ar__msg ar__msg--error\">\n <p>{{ entryErr }}</p>\n </div>\n\n <!-- dicom series: a whole folder of .dcm slices browsed as one scrollable series -->\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=\"ar__summary\">{{ fileCount }} 个文件 · {{ formatBytes(totalSize) }}</div>\n <div class=\"ar__scroll ar__tree\">\n <p v-if=\"rows.length === 0\" class=\"ar__msg\">空压缩包</p>\n <button\n v-for=\"row in rows\"\n :key=\"row.node.path\"\n type=\"button\"\n class=\"ar__row\"\n :class=\"{ 'ar__row--dir': row.node.isDir }\"\n :style=\"{ paddingLeft: `${12 + row.depth * 14}px` }\"\n @click=\"onRow(row.node)\"\n >\n <span class=\"ar__twisty\">\n <Icon\n v-if=\"row.node.isDir\"\n name=\"chevronRight\"\n :size=\"13\"\n :class=\"{ 'ar__twisty--open': expanded.has(row.node.path) }\"\n />\n </span>\n <span class=\"ar__rowicon\">\n <Icon :name=\"row.node.isDir ? 'folder' : 'fileOutline'\" :size=\"15\" />\n </span>\n <span class=\"ar__rowname\">{{ row.node.name }}</span>\n <span v-if=\"!row.node.isDir\" class=\"ar__rowsize\">{{ formatBytes(row.node.size) }}</span>\n </button>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.ar {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.ar__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.ar__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.ar__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.ar__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* List view ------------------------------------------------------------------------------------ */\n.ar__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.ar__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.ar__tree {\n padding: 6px 0;\n}\n.ar__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.ar__row:hover {\n background: var(--surface-2);\n}\n.ar__twisty {\n flex-shrink: 0;\n display: inline-flex;\n width: 13px;\n color: var(--faint);\n}\n.ar__twisty--open {\n transform: rotate(90deg);\n transition: transform 0.12s;\n}\n.ar__rowicon {\n flex-shrink: 0;\n display: inline-flex;\n color: var(--faint);\n}\n.ar__row--dir .ar__rowicon {\n color: var(--accent);\n}\n.ar__rowname {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.ar__row--dir .ar__rowname {\n font-weight: 500;\n}\n.ar__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.ar__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.ar__back,\n.ar__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.ar__back:hover,\n.ar__act:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.ar__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.ar__bsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n.ar__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.ar__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.ar__vbtn--on {\n background: var(--surface);\n color: var(--accent);\n box-shadow: var(--shadow);\n}\n</style>\n"],"mappings":"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"}
1
+ {"version":3,"file":"ArchiveBrowser.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/ArchiveBrowser.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 type { ArchiveEntry, OpenedArchive } from '@/lib/archive'\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 generic archive mode — the multi-file browser shared by the tar family (tar/gz/xz/bz2) and\n * rar. Same master→detail UX as {@link ZipBrowser}: a collapsible file tree; selecting an entry extracts just\n * that one, wraps it in a blob URL, and hands it to {@link FileViewerBody} (so a file inside the archive\n * previews with every viewer a top-level file gets).\n *\n * The format-specific work lives behind {@link OpenedArchive}: this component fetches the bytes, lazy-imports\n * the right opener by `kind` (so neither the tar codecs nor the rar WASM ship in the main bundle), and drives\n * everything through `entries` + `extract()`. Extraction may be sync (tar) or async (rar) — every call is awaited.\n */\nconst props = defineProps<{ url: string; name: string; kind: 'tar' | 'rar' }>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nlet archive: OpenedArchive | null = null\nconst entries = ref<ArchiveEntry[]>([])\n\ninterface TreeNode {\n name: string\n path: string\n isDir: boolean\n size: number\n entry?: ArchiveEntry\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: ArchiveEntry[]): 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 archive = null\n revokeBlob()\n stackSlices.value = []\n const url = props.url\n try {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buffer = await res.arrayBuffer()\n // Lazy-import the opener so the tar codecs / rar WASM stay out of the main bundle until needed.\n const opened =\n props.kind === 'rar'\n ? await (await import('@/lib/rar')).openRar(buffer)\n : await (await import('@/lib/tarball')).openTarball(buffer, props.name)\n if (props.url !== url) return // a newer file superseded us while decoding\n archive = opened\n entries.value = opened.entries\n root.value = buildTree(opened.entries)\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<ArchiveEntry | 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).\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: ArchiveEntry): ArchiveEntry[] {\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<ArchiveEntry>({\n entries: () => entries.value,\n archiveName: () => props.name,\n extract: async (entry) => {\n if (!archive) throw new Error('压缩包尚未加载')\n return archive.extract(entry)\n },\n})\n\n// Copy out a freshly-allocated ArrayBuffer (a nested viewer/blob must own its bytes, not alias the archive).\nfunction toArrayBuffer(u: Uint8Array): ArrayBuffer {\n const out = new ArrayBuffer(u.byteLength)\n new Uint8Array(out).set(u)\n return out\n}\n\nasync function selectEntry(e: ArchiveEntry): Promise<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 (!archive) return\n const arc = archive\n try {\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 () => toArrayBuffer(await arc.extract(se)),\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 = await arc.extract(e)\n if (selected.value !== e) return // a newer selection superseded us mid-extract\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 = err instanceof Error ? err.message : String(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 void 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\nasync function downloadEntry(e: ArchiveEntry): Promise<void> {\n if (!archive) return\n try {\n const url = URL.createObjectURL(new Blob([await archive.extract(e)]))\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=\"ar\">\n <p v-if=\"status === 'loading'\" class=\"ar__msg\">正在解压…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"ar__msg ar__msg--error\">\n <p>{{ errorMsg }}</p>\n <button type=\"button\" class=\"ar__retry\" @click=\"load\">重试</button>\n </div>\n\n <!-- detail: one selected entry -->\n <template v-else-if=\"selected\">\n <div class=\"ar__bar\">\n <button type=\"button\" class=\"ar__back\" title=\"返回列表\" @click=\"back\">\n <Icon name=\"arrowLeft\" :size=\"16\" />\n </button>\n <span class=\"ar__path\" :title=\"selected.path\">{{ selected.path }}</span>\n <span class=\"ar__bsize\">{{ formatBytes(selected.size) }}</span>\n <div v-if=\"canToggle\" class=\"ar__view\" role=\"group\" aria-label=\"渲染或源码\">\n <button\n type=\"button\"\n class=\"ar__vbtn\"\n :class=\"{ 'ar__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=\"ar__vbtn\"\n :class=\"{ 'ar__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=\"ar__act\"\n :title=\"copied ? '已复制' : '复制'\"\n @click=\"copyEntry\"\n >\n <Icon name=\"copy\" :size=\"14\" />\n </button>\n <button type=\"button\" class=\"ar__act\" title=\"下载\" @click=\"downloadEntry(selected)\">\n <Icon name=\"download\" :size=\"15\" />\n </button>\n </div>\n\n <p v-if=\"entryStatus === 'loading'\" class=\"ar__msg\">读取中…</p>\n <div v-else-if=\"entryStatus === 'error'\" class=\"ar__msg ar__msg--error\">\n <p>{{ entryErr }}</p>\n </div>\n\n <!-- dicom series: a whole folder of .dcm slices browsed as one scrollable series -->\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=\"ar__summary\">{{ fileCount }} 个文件 · {{ formatBytes(totalSize) }}</div>\n <div class=\"ar__scroll ar__tree\">\n <p v-if=\"rows.length === 0\" class=\"ar__msg\">空压缩包</p>\n <button\n v-for=\"row in rows\"\n :key=\"row.node.path\"\n type=\"button\"\n class=\"ar__row\"\n :class=\"{ 'ar__row--dir': row.node.isDir }\"\n :style=\"{ paddingLeft: `${12 + row.depth * 14}px` }\"\n @click=\"onRow(row.node)\"\n >\n <span class=\"ar__twisty\">\n <Icon\n v-if=\"row.node.isDir\"\n name=\"chevronRight\"\n :size=\"13\"\n :class=\"{ 'ar__twisty--open': expanded.has(row.node.path) }\"\n />\n </span>\n <span class=\"ar__rowicon\">\n <Icon :name=\"row.node.isDir ? 'folder' : 'fileOutline'\" :size=\"15\" />\n </span>\n <span class=\"ar__rowname\">{{ row.node.name }}</span>\n <span v-if=\"!row.node.isDir\" class=\"ar__rowsize\">{{ formatBytes(row.node.size) }}</span>\n </button>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.ar {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.ar__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.ar__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.ar__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.ar__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* List view ------------------------------------------------------------------------------------ */\n.ar__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.ar__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.ar__tree {\n padding: 6px 0;\n}\n.ar__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.ar__row:hover {\n background: var(--surface-2);\n}\n.ar__twisty {\n flex-shrink: 0;\n display: inline-flex;\n width: 13px;\n color: var(--faint);\n}\n.ar__twisty--open {\n transform: rotate(90deg);\n transition: transform 0.12s;\n}\n.ar__rowicon {\n flex-shrink: 0;\n display: inline-flex;\n color: var(--faint);\n}\n.ar__row--dir .ar__rowicon {\n color: var(--accent);\n}\n.ar__rowname {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.ar__row--dir .ar__rowname {\n font-weight: 500;\n}\n.ar__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.ar__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.ar__back,\n.ar__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.ar__back:hover,\n.ar__act:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.ar__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.ar__bsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n.ar__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.ar__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.ar__vbtn--on {\n background: var(--surface);\n color: var(--accent);\n box-shadow: var(--shadow);\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-af870145"]]);
5
+ var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-82d079e3"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
8