@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.
- package/dist/components/ArchiveBrowser.js +1 -1
- package/dist/components/ArchiveBrowser.js.map +1 -1
- package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js +105 -94
- package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/FileViewerBody.js +1 -1
- package/dist/components/FileViewerBody.js.map +1 -1
- package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/ImageLightbox.js +1 -1
- package/dist/components/ImageLightbox.js.map +1 -1
- package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js +101 -55
- package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/JsonlViewer.js +1 -1
- package/dist/components/JsonlViewer.js.map +1 -1
- package/dist/components/JsonlViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/LightboxGalleryStrip.js +9 -0
- package/dist/components/LightboxGalleryStrip.js.map +1 -0
- package/dist/components/LightboxGalleryStrip.vue.d.ts +12 -0
- package/dist/components/LightboxGalleryStrip.vue_vue_type_script_setup_true_lang.js +60 -0
- package/dist/components/LightboxGalleryStrip.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/PdfNavigationPane.js +9 -0
- package/dist/components/PdfNavigationPane.js.map +1 -0
- package/dist/components/PdfNavigationPane.vue.d.ts +24 -0
- package/dist/components/PdfNavigationPane.vue_vue_type_script_setup_true_lang.js +149 -0
- package/dist/components/PdfNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/PdfViewer.js +1 -1
- package/dist/components/PdfViewer.js.map +1 -1
- package/dist/components/PdfViewer.vue.d.ts +0 -11
- package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js +131 -69
- package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/SevenZipBrowser.js +1 -1
- package/dist/components/SevenZipBrowser.js.map +1 -1
- package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js +157 -146
- package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/ZipBrowser.js +1 -1
- package/dist/components/ZipBrowser.js.map +1 -1
- package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js +70 -61
- package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/ZoomImage.js +1 -1
- package/dist/components/ZoomImage.js.map +1 -1
- package/dist/components/ZoomImage.vue.d.ts +2 -0
- package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js +104 -95
- package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/composables/useArchiveImageGallery.d.ts +15 -0
- package/dist/composables/useArchiveImageGallery.js +62 -0
- package/dist/composables/useArchiveImageGallery.js.map +1 -0
- package/dist/composables/useLightbox.d.ts +29 -21
- package/dist/composables/useLightbox.js +72 -12
- package/dist/composables/useLightbox.js.map +1 -1
- package/dist/lib/archiveImages.d.ts +14 -0
- package/dist/lib/archiveImages.js +15 -0
- package/dist/lib/archiveImages.js.map +1 -0
- package/dist/lib/pdfNavigation.d.ts +11 -0
- package/dist/lib/pdfNavigation.js +32 -0
- package/dist/lib/pdfNavigation.js.map +1 -0
- package/dist/style.css +1 -1
- 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-
|
|
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
|
|
3
|
-
import { formatBytes as
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
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" },
|
|
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
|
-
},
|
|
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
|
|
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
|
-
}),
|
|
45
|
-
function
|
|
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
|
|
84
|
+
return P(t), t;
|
|
84
85
|
}
|
|
85
|
-
function
|
|
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 &&
|
|
88
|
+
for (let t of e.children) t.isDir && P(t);
|
|
88
89
|
}
|
|
89
|
-
let
|
|
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 &&
|
|
95
|
+
}), i.isDir && N.has(i.path) && t(i.children, r + 1);
|
|
95
96
|
};
|
|
96
|
-
return t(
|
|
97
|
+
return t(M.value.children, 0), e;
|
|
97
98
|
});
|
|
98
|
-
async function
|
|
99
|
-
|
|
100
|
-
let e =
|
|
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 =
|
|
105
|
-
if (
|
|
106
|
-
|
|
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
|
-
|
|
109
|
+
k.value = e instanceof Error ? e.message : String(e), O.value = "error";
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
|
-
let
|
|
112
|
-
function
|
|
113
|
-
|
|
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
|
|
116
|
+
function Y(e) {
|
|
116
117
|
let t = e.lastIndexOf("/");
|
|
117
118
|
return t < 0 ? "" : e.slice(0, t);
|
|
118
119
|
}
|
|
119
|
-
function
|
|
120
|
-
let n =
|
|
121
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
|
138
|
-
if (
|
|
139
|
-
let n =
|
|
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 =
|
|
151
|
+
let t = De(e);
|
|
143
152
|
if (t.length > 1) {
|
|
144
|
-
|
|
153
|
+
G.value = t.map((e) => ({
|
|
145
154
|
name: e.name,
|
|
146
155
|
size: e.size,
|
|
147
|
-
load: async () =>
|
|
148
|
-
})),
|
|
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 (
|
|
154
|
-
|
|
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:
|
|
166
|
+
url: W.value,
|
|
157
167
|
size: e.size,
|
|
158
|
-
textHint:
|
|
159
|
-
})
|
|
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
|
-
|
|
172
|
+
z.value = e instanceof Error ? e.message : String(e), R.value = "error";
|
|
162
173
|
}
|
|
163
174
|
}
|
|
164
|
-
function
|
|
165
|
-
e.isDir ?
|
|
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
|
|
168
|
-
|
|
178
|
+
function je() {
|
|
179
|
+
L.value = null, B.value = "", V.value = null, H.value = !1, J(), G.value = [];
|
|
169
180
|
}
|
|
170
|
-
async function
|
|
171
|
-
if (
|
|
172
|
-
let t = URL.createObjectURL(new Blob([await
|
|
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 =
|
|
177
|
-
async function
|
|
187
|
+
let Q = S(!1), $;
|
|
188
|
+
async function Ne() {
|
|
178
189
|
try {
|
|
179
|
-
await navigator.clipboard?.writeText(
|
|
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
|
|
184
|
-
|
|
185
|
-
}), (e, t) => (
|
|
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:
|
|
189
|
-
}, "重试")])) :
|
|
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:
|
|
195
|
-
}, [m(
|
|
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:
|
|
202
|
-
},
|
|
203
|
-
p("span", ce,
|
|
204
|
-
|
|
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":
|
|
217
|
+
class: _(["ar__vbtn", { "ar__vbtn--on": U.value === "render" }]),
|
|
207
218
|
title: "渲染",
|
|
208
|
-
onClick: t[0] ||= (e) =>
|
|
209
|
-
}, [m(
|
|
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":
|
|
225
|
+
class: _(["ar__vbtn", { "ar__vbtn--on": U.value === "source" }]),
|
|
215
226
|
title: "源码",
|
|
216
|
-
onClick: t[1] ||= (e) =>
|
|
217
|
-
}, [m(
|
|
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
|
-
|
|
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:
|
|
227
|
-
}, [m(
|
|
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) =>
|
|
236
|
-
}, [m(
|
|
246
|
+
onClick: t[2] ||= (e) => Me(L.value)
|
|
247
|
+
}, [m(o, {
|
|
237
248
|
name: "download",
|
|
238
249
|
size: 15
|
|
239
250
|
})])
|
|
240
|
-
]),
|
|
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:
|
|
243
|
-
start:
|
|
244
|
-
name:
|
|
245
|
-
size:
|
|
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
|
-
])) :
|
|
262
|
+
])) : V.value ? (b(), u(T(xe), {
|
|
252
263
|
key: 3,
|
|
253
|
-
file:
|
|
254
|
-
mode:
|
|
255
|
-
onLoaded: t[3] ||= (e) =>
|
|
256
|
-
}, null, 8, ["file", "mode"])) : d("", !0)], 64)) : (
|
|
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:
|
|
261
|
-
onClick: (t) =>
|
|
271
|
+
style: v({ paddingLeft: `${12 + e.depth * 14}px` }),
|
|
272
|
+
onClick: (t) => Ae(e.node)
|
|
262
273
|
}, [
|
|
263
|
-
p("span", _e, [e.node.isDir ? (
|
|
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":
|
|
278
|
+
class: _({ "ar__twisty--open": N.has(e.node.path) })
|
|
268
279
|
}, null, 8, ["class"])) : d("", !0)]),
|
|
269
|
-
p("span",
|
|
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",
|
|
274
|
-
e.node.isDir ? d("", !0) : (
|
|
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 {
|
|
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-
|
|
5
|
+
var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-82d079e3"]]);
|
|
6
6
|
//#endregion
|
|
7
7
|
export { n as default };
|
|
8
8
|
|