@l1yp/file-viewer 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/components/ArchiveBrowser.js +1 -1
  2. package/dist/components/ArchiveBrowser.js.map +1 -1
  3. package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js +105 -94
  4. package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -1
  5. package/dist/components/FileViewerBody.js +1 -1
  6. package/dist/components/FileViewerBody.js.map +1 -1
  7. package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js.map +1 -1
  8. package/dist/components/ImageLightbox.js +1 -1
  9. package/dist/components/ImageLightbox.js.map +1 -1
  10. package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js +101 -55
  11. package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js.map +1 -1
  12. package/dist/components/JsonlViewer.js +1 -1
  13. package/dist/components/JsonlViewer.js.map +1 -1
  14. package/dist/components/JsonlViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  15. package/dist/components/LightboxGalleryStrip.js +9 -0
  16. package/dist/components/LightboxGalleryStrip.js.map +1 -0
  17. package/dist/components/LightboxGalleryStrip.vue.d.ts +12 -0
  18. package/dist/components/LightboxGalleryStrip.vue_vue_type_script_setup_true_lang.js +60 -0
  19. package/dist/components/LightboxGalleryStrip.vue_vue_type_script_setup_true_lang.js.map +1 -0
  20. package/dist/components/PdfNavigationPane.js +9 -0
  21. package/dist/components/PdfNavigationPane.js.map +1 -0
  22. package/dist/components/PdfNavigationPane.vue.d.ts +24 -0
  23. package/dist/components/PdfNavigationPane.vue_vue_type_script_setup_true_lang.js +149 -0
  24. package/dist/components/PdfNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -0
  25. package/dist/components/PdfViewer.js +1 -1
  26. package/dist/components/PdfViewer.js.map +1 -1
  27. package/dist/components/PdfViewer.vue.d.ts +0 -11
  28. package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js +131 -69
  29. package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  30. package/dist/components/SevenZipBrowser.js +1 -1
  31. package/dist/components/SevenZipBrowser.js.map +1 -1
  32. package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js +157 -146
  33. package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -1
  34. package/dist/components/ZipBrowser.js +1 -1
  35. package/dist/components/ZipBrowser.js.map +1 -1
  36. package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js +70 -61
  37. package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -1
  38. package/dist/components/ZoomImage.js +1 -1
  39. package/dist/components/ZoomImage.js.map +1 -1
  40. package/dist/components/ZoomImage.vue.d.ts +2 -0
  41. package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js +104 -95
  42. package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js.map +1 -1
  43. package/dist/composables/useArchiveImageGallery.d.ts +15 -0
  44. package/dist/composables/useArchiveImageGallery.js +62 -0
  45. package/dist/composables/useArchiveImageGallery.js.map +1 -0
  46. package/dist/composables/useLightbox.d.ts +29 -21
  47. package/dist/composables/useLightbox.js +72 -12
  48. package/dist/composables/useLightbox.js.map +1 -1
  49. package/dist/lib/archiveImages.d.ts +14 -0
  50. package/dist/lib/archiveImages.js +15 -0
  51. package/dist/lib/archiveImages.js.map +1 -0
  52. package/dist/lib/pdfNavigation.d.ts +11 -0
  53. package/dist/lib/pdfNavigation.js +32 -0
  54. package/dist/lib/pdfNavigation.js.map +1 -0
  55. package/dist/style.css +1 -1
  56. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"SevenZipBrowser.js","names":[],"sources":["../../src/components/SevenZipBrowser.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport {\n computed,\n defineAsyncComponent,\n nextTick,\n onBeforeUnmount,\n onMounted,\n reactive,\n ref,\n watch,\n} from 'vue'\nimport {\n extractSevenZipEntry,\n readSevenZip,\n SevenZipEncryptedError,\n SevenZipPasswordError,\n type SevenZipArchive,\n type SevenZipEntry,\n} from '@/lib/sevenZip'\nimport type { DicomSlice } from '@/lib/dicom'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { fileExt, isDicomFile, isTextFile } from '@/lib/fileType'\nimport { canToggleView, classifyViewerFile, mediaMimeForName } from '@/lib/viewerKind'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n// A folder of sibling .dcm files browses as one scrollable series — handled directly (the body has no slice model).\nconst DicomViewer = defineAsyncComponent(() => import('@/components/DicomViewer.vue'))\n// The shared side-viewer body renders a selected entry with the SAME viewers as a top-level file. Lazy to break\n// the module cycle (the body lazy-imports this browser back, for nested archives).\nconst FileViewerBody = defineAsyncComponent(() => import('@/components/FileViewerBody.vue'))\n\n/**\n * The side viewer's 7z mode — the LZMA counterpart to {@link ZipBrowser}. Fetches an archive's bytes, parses +\n * decompresses them entirely in-browser (see lib/sevenZip.ts, which carries its own LZMA decoder since browsers\n * ship none), and browses the contents. Selecting an entry extracts just that one, wraps it in a blob URL, and\n * hands it to {@link FileViewerBody} (so a file inside the 7z previews with every viewer a top-level file gets).\n */\nconst props = defineProps<{ url: string; name: string }>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nlet buffer: ArrayBuffer | null = null\nlet archive: SevenZipArchive | null = null\nconst entries = ref<SevenZipEntry[]>([])\n\n// Password state for AES-encrypted 7z. `locked` = the archive header itself is encrypted (encrypted file\n// names), so it can't even be listed until unlocked; `contentEncrypted` = the listing worked but file bytes\n// need a password; `unlocked` = a password has been accepted.\nconst password = ref('')\nconst pwError = ref('')\nconst locked = ref(false)\nconst unlocked = ref(false)\nconst contentEncrypted = ref(false)\n\ninterface TreeNode {\n name: string\n path: string\n isDir: boolean\n size: number\n entry?: SevenZipEntry\n children: TreeNode[]\n}\nconst root = ref<TreeNode>({ name: '', path: '', isDir: true, size: 0, children: [] })\nconst expanded = reactive(new Set<string>())\n\nconst fileCount = computed(() => entries.value.reduce((n, e) => n + (e.isDir ? 0 : 1), 0))\nconst totalSize = computed(() => entries.value.reduce((s, e) => s + (e.isDir ? 0 : e.size), 0))\n\n// Build a folder tree from the flat entry paths (directories may be implicit — derive them either way).\nfunction buildTree(list: SevenZipEntry[]): TreeNode {\n const rootNode: TreeNode = { name: '', path: '', isDir: true, size: 0, children: [] }\n const dirs = new Map<string, TreeNode>([['', rootNode]])\n const ensureDir = (segs: string[]): TreeNode => {\n let cur = rootNode\n let acc = ''\n for (const seg of segs) {\n acc = acc ? `${acc}/${seg}` : seg\n let node = dirs.get(acc)\n if (!node) {\n node = { name: seg, path: acc, isDir: true, size: 0, children: [] }\n dirs.set(acc, node)\n cur.children.push(node)\n }\n cur = node\n }\n return cur\n }\n for (const e of list) {\n const segs = e.path.split('/').filter(Boolean)\n if (e.isDir) {\n ensureDir(segs)\n continue\n }\n const fname = segs.pop()\n if (!fname) continue\n ensureDir(segs).children.push({\n name: fname,\n path: e.path,\n isDir: false,\n size: e.size,\n entry: e,\n children: [],\n })\n }\n sortNode(rootNode)\n return rootNode\n}\n// Folders first, then files, each alphabetical (case-insensitive).\nfunction sortNode(n: TreeNode): void {\n n.children.sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1))\n for (const c of n.children) if (c.isDir) sortNode(c)\n}\n\n// Flatten the tree to the rows currently visible (respecting which folders are expanded).\ninterface Row {\n node: TreeNode\n depth: number\n}\nconst rows = computed<Row[]>(() => {\n const out: Row[] = []\n const walk = (nodes: TreeNode[], depth: number): void => {\n for (const n of nodes) {\n out.push({ node: n, depth })\n if (n.isDir && expanded.has(n.path)) walk(n.children, depth + 1)\n }\n }\n walk(root.value.children, 0)\n return out\n})\n\nasync function load(): Promise<void> {\n status.value = 'loading'\n errorMsg.value = ''\n selected.value = null\n password.value = ''\n pwError.value = ''\n locked.value = false\n unlocked.value = false\n contentEncrypted.value = false\n revokeBlob()\n stackSlices.value = []\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n buffer = await res.arrayBuffer()\n try {\n archive = readSevenZip(buffer)\n } catch (e) {\n if (e instanceof SevenZipEncryptedError) {\n // Encrypted file names: nothing can be listed until the password unlocks the header.\n locked.value = true\n status.value = 'ready'\n return\n }\n throw e\n }\n entries.value = archive.entries\n root.value = buildTree(archive.entries)\n expanded.clear()\n contentEncrypted.value = archive.entries.some((e) => e.encrypted)\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction pwMessage(e: unknown): string {\n if (e instanceof SevenZipPasswordError) return '密码错误'\n if (e instanceof SevenZipEncryptedError) return '需要密码'\n return e instanceof Error ? e.message : String(e)\n}\n\n// Unlock an encrypted archive. An encrypted header re-lists with the password (the KDF runs here, ~100ms);\n// content-only encryption verifies the password by decoding the first encrypted entry's folder.\nasync function submitPassword(): Promise<void> {\n if (!buffer || !password.value) return\n pwError.value = ''\n if (locked.value) {\n status.value = 'loading'\n await nextTick() // let the spinner paint before the synchronous key derivation blocks\n try {\n archive = readSevenZip(buffer, password.value)\n entries.value = archive.entries\n root.value = buildTree(archive.entries)\n expanded.clear()\n contentEncrypted.value = archive.entries.some((e) => e.encrypted)\n locked.value = false\n unlocked.value = true\n } catch (e) {\n pwError.value = pwMessage(e)\n } finally {\n status.value = 'ready'\n }\n return\n }\n try {\n const target = entries.value.find((e) => !e.isDir && e.encrypted)\n if (target && archive) extractSevenZipEntry(buffer, archive, target, password.value)\n unlocked.value = true\n } catch (e) {\n unlocked.value = false\n pwError.value = pwMessage(e)\n }\n}\n\n// ── selected entry (detail view) ──────────────────────────────────────────────────────────────\nconst selected = ref<SevenZipEntry | null>(null)\nconst entryStatus = ref<Status>('ready')\nconst entryErr = ref('')\nconst entryText = ref('')\nconst entryFile = ref<ViewerFile | null>(null)\nconst entryStack = ref(false)\nconst entryMode = ref<'render' | 'source'>('render')\nconst blobUrl = ref('')\n\n// DICOM series stack: a folder of .dcm slices browsed as one scrollable series (see ZipBrowser). 7z decode is\n// synchronous, but slice.load() stays async to match DicomSlice (the viewer awaits it either way).\nconst stackSlices = ref<DicomSlice[]>([])\nconst stackStart = ref(0)\nconst stackName = ref('')\nconst stackTotalSize = computed(() => stackSlices.value.reduce((s, sl) => s + (sl.size ?? 0), 0))\n\nconst canToggle = computed(() => canToggleView(entryFile.value, entryText.value))\n\nfunction revokeBlob(): void {\n if (blobUrl.value) {\n URL.revokeObjectURL(blobUrl.value)\n blobUrl.value = ''\n }\n}\n\nfunction dirOf(path: string): string {\n const i = path.lastIndexOf('/')\n return i < 0 ? '' : path.slice(0, i)\n}\n// Sibling .dcm files in the same folder, natural-sorted (numeric-aware) so e.g. img2 precedes img10.\nfunction dicomSiblings(e: SevenZipEntry): SevenZipEntry[] {\n const dir = dirOf(e.path)\n return entries.value\n .filter((x) => !x.isDir && isDicomFile(x.name) && dirOf(x.path) === dir)\n .sort((a, b) => a.path.localeCompare(b.path, undefined, { numeric: true, sensitivity: 'base' }))\n}\n\n// Treat as text if the name says so; for extensionless entries, sniff the first bytes for NULs.\nfunction looksText(bytes: Uint8Array, name: string): boolean {\n if (isTextFile(name)) return true\n if (fileExt(name)) return false\n const n = Math.min(bytes.length, 4096)\n for (let i = 0; i < n; i++) if (bytes[i] === 0) return false\n return true\n}\n\nfunction selectEntry(e: SevenZipEntry): void {\n selected.value = e\n entryStatus.value = 'loading'\n entryErr.value = ''\n entryText.value = ''\n entryFile.value = null\n entryStack.value = false\n entryMode.value = 'render'\n revokeBlob()\n stackSlices.value = []\n if (!buffer || !archive) return\n try {\n if (isDicomFile(e.name)) {\n const siblings = dicomSiblings(e)\n if (siblings.length > 1) {\n const buf = buffer\n const arc = archive\n stackSlices.value = siblings.map((se) => ({\n name: se.name,\n size: se.size,\n load: async () => {\n const u = extractSevenZipEntry(buf, arc, se, password.value)\n const out = new ArrayBuffer(u.byteLength)\n new Uint8Array(out).set(u)\n return out\n },\n }))\n stackStart.value = Math.max(\n 0,\n siblings.findIndex((se) => se.path === e.path),\n )\n stackName.value = dirOf(e.path).split('/').pop() || e.name\n entryStack.value = true\n entryStatus.value = 'ready'\n return\n }\n }\n const bytes = extractSevenZipEntry(buffer, archive, e, password.value)\n blobUrl.value = URL.createObjectURL(new Blob([bytes], { type: mediaMimeForName(e.name) }))\n entryFile.value = classifyViewerFile({\n name: e.name,\n url: blobUrl.value,\n size: e.size,\n textHint: looksText(bytes, e.name),\n })\n entryStatus.value = 'ready'\n } catch (err) {\n entryErr.value = pwMessage(err)\n entryStatus.value = 'error'\n }\n}\n\nfunction onRow(node: TreeNode): void {\n if (node.isDir) {\n if (expanded.has(node.path)) expanded.delete(node.path)\n else expanded.add(node.path)\n } else if (node.entry) {\n selectEntry(node.entry)\n }\n}\n\nfunction back(): void {\n selected.value = null\n entryText.value = ''\n entryFile.value = null\n entryStack.value = false\n revokeBlob()\n stackSlices.value = []\n}\n\n// Re-extract on demand (the decoded folder is cached in lib/sevenZip, so this stays cheap).\nfunction downloadEntry(e: SevenZipEntry): void {\n if (!buffer || !archive) return\n try {\n const url = URL.createObjectURL(\n new Blob([extractSevenZipEntry(buffer, archive, e, password.value)]),\n )\n const a = document.createElement('a')\n a.href = url\n a.download = e.name\n document.body.appendChild(a)\n a.click()\n a.remove()\n setTimeout(() => URL.revokeObjectURL(url), 1000)\n } catch {\n /* extraction failed — nothing to download */\n }\n}\n\nconst copied = ref(false)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyEntry(): Promise<void> {\n try {\n await navigator.clipboard?.writeText(entryText.value)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copied.value = true\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copied.value = false), 1400)\n}\n\nonMounted(load)\nwatch(() => props.url, load)\nonBeforeUnmount(() => {\n revokeBlob()\n clearTimeout(copyTimer)\n})\n</script>\n\n<template>\n <div class=\"sz\">\n <p v-if=\"status === 'loading'\" class=\"sz__msg\">正在解压…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"sz__msg sz__msg--error\">\n <p>{{ errorMsg }}</p>\n <button type=\"button\" class=\"sz__retry\" @click=\"load\">重试</button>\n </div>\n\n <!-- locked: the header (file names) is encrypted — must unlock before anything can be listed -->\n <div v-else-if=\"locked\" class=\"sz__locked\">\n <Icon name=\"key\" :size=\"26\" class=\"sz__lockicon\" />\n <p class=\"sz__lockmsg\">该 7z 已加密(含文件名),输入密码后查看</p>\n <div class=\"sz__lockform\">\n <input\n v-model=\"password\"\n type=\"password\"\n class=\"sz__pwinput\"\n placeholder=\"输入密码\"\n @keyup.enter=\"submitPassword\"\n />\n <button type=\"button\" class=\"sz__pwbtn\" @click=\"submitPassword\">解锁</button>\n </div>\n <p v-if=\"pwError\" class=\"sz__lockerr\">{{ pwError }}</p>\n </div>\n\n <!-- detail: one selected entry -->\n <template v-else-if=\"selected\">\n <div class=\"sz__bar\">\n <button type=\"button\" class=\"sz__back\" title=\"返回列表\" @click=\"back\">\n <Icon name=\"arrowLeft\" :size=\"16\" />\n </button>\n <span class=\"sz__path\" :title=\"selected.path\">{{ selected.path }}</span>\n <span class=\"sz__bsize\">{{ formatBytes(selected.size) }}</span>\n <div v-if=\"canToggle\" class=\"sz__view\" role=\"group\" aria-label=\"渲染或源码\">\n <button\n type=\"button\"\n class=\"sz__vbtn\"\n :class=\"{ 'sz__vbtn--on': entryMode === 'render' }\"\n title=\"渲染\"\n @click=\"entryMode = 'render'\"\n >\n <Icon name=\"eye\" :size=\"14\" />\n </button>\n <button\n type=\"button\"\n class=\"sz__vbtn\"\n :class=\"{ 'sz__vbtn--on': entryMode === 'source' }\"\n title=\"源码\"\n @click=\"entryMode = 'source'\"\n >\n <Icon name=\"code\" :size=\"14\" />\n </button>\n </div>\n <button\n v-if=\"entryText && entryStatus === 'ready'\"\n type=\"button\"\n class=\"sz__act\"\n :title=\"copied ? '已复制' : '复制'\"\n @click=\"copyEntry\"\n >\n <Icon name=\"copy\" :size=\"14\" />\n </button>\n <button type=\"button\" class=\"sz__act\" title=\"下载\" @click=\"downloadEntry(selected)\">\n <Icon name=\"download\" :size=\"15\" />\n </button>\n </div>\n\n <p v-if=\"entryStatus === 'loading'\" class=\"sz__msg\">读取中…</p>\n <div v-else-if=\"entryStatus === 'error'\" class=\"sz__msg sz__msg--error\">\n <p>{{ entryErr }}</p>\n </div>\n\n <!-- dicom series: a whole folder of .dcm slices browsed as one scrollable series (wheel/cine翻层) -->\n <DicomViewer\n v-else-if=\"entryStack\"\n :slices=\"stackSlices\"\n :start=\"stackStart\"\n :name=\"stackName\"\n :size=\"stackTotalSize\"\n />\n\n <!-- everything else: the shared body picks the right viewer from the extracted blob -->\n <FileViewerBody\n v-else-if=\"entryFile\"\n :file=\"entryFile\"\n :mode=\"entryMode\"\n @loaded=\"entryText = $event\"\n />\n </template>\n\n <!-- list: the file tree -->\n <template v-else>\n <div class=\"sz__summary\">{{ fileCount }} 个文件 · {{ formatBytes(totalSize) }}</div>\n\n <!-- password bar: shown when file bytes are encrypted (but the file names were readable) -->\n <div v-if=\"contentEncrypted\" class=\"sz__pw\" :class=\"{ 'sz__pw--ok': unlocked }\">\n <Icon name=\"key\" :size=\"14\" />\n <input\n v-model=\"password\"\n type=\"password\"\n class=\"sz__pwinput\"\n :placeholder=\"unlocked ? '已解锁(如需可重新输入)' : '此压缩包内容已加密,输入密码解锁'\"\n @keyup.enter=\"submitPassword\"\n />\n <button type=\"button\" class=\"sz__pwbtn\" @click=\"submitPassword\">\n {{ unlocked ? '已解锁' : '解锁' }}\n </button>\n <span v-if=\"pwError\" class=\"sz__pwerr\">{{ pwError }}</span>\n </div>\n\n <div class=\"sz__scroll sz__tree\">\n <p v-if=\"rows.length === 0\" class=\"sz__msg\">空压缩包</p>\n <button\n v-for=\"row in rows\"\n :key=\"row.node.path\"\n type=\"button\"\n class=\"sz__row\"\n :class=\"{ 'sz__row--dir': row.node.isDir }\"\n :style=\"{ paddingLeft: `${12 + row.depth * 14}px` }\"\n @click=\"onRow(row.node)\"\n >\n <span class=\"sz__twisty\">\n <Icon\n v-if=\"row.node.isDir\"\n name=\"chevronRight\"\n :size=\"13\"\n :class=\"{ 'sz__twisty--open': expanded.has(row.node.path) }\"\n />\n </span>\n <span class=\"sz__rowicon\">\n <Icon :name=\"row.node.isDir ? 'folder' : 'fileOutline'\" :size=\"15\" />\n </span>\n <span class=\"sz__rowname\">{{ row.node.name }}</span>\n <Icon\n v-if=\"!row.node.isDir && row.node.entry?.encrypted\"\n name=\"key\"\n :size=\"12\"\n class=\"sz__rowlock\"\n />\n <span v-if=\"!row.node.isDir\" class=\"sz__rowsize\">{{ formatBytes(row.node.size) }}</span>\n </button>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.sz {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.sz__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.sz__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.sz__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.sz__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* List view ------------------------------------------------------------------------------------ */\n.sz__summary {\n flex-shrink: 0;\n padding: 9px 16px;\n border-bottom: 1px solid var(--border);\n font-size: 11.5px;\n letter-spacing: 0.02em;\n color: var(--faint);\n}\n.sz__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.sz__tree {\n padding: 6px 0;\n}\n.sz__row {\n display: flex;\n align-items: center;\n gap: 7px;\n width: 100%;\n padding: 5px 12px 5px 12px;\n background: transparent;\n border: none;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n text-align: left;\n cursor: pointer;\n}\n.sz__row:hover {\n background: var(--surface-2);\n}\n.sz__twisty {\n flex-shrink: 0;\n display: inline-flex;\n width: 13px;\n color: var(--faint);\n}\n.sz__twisty--open {\n transform: rotate(90deg);\n transition: transform 0.12s;\n}\n.sz__rowicon {\n flex-shrink: 0;\n display: inline-flex;\n color: var(--faint);\n}\n.sz__row--dir .sz__rowicon {\n color: var(--accent);\n}\n.sz__rowname {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.sz__row--dir .sz__rowname {\n font-weight: 500;\n}\n.sz__rowsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n\n/* Detail bar ----------------------------------------------------------------------------------- */\n.sz__bar {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 7px 10px 7px 8px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.sz__back,\n.sz__act {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 30px;\n height: 28px;\n background: transparent;\n border: none;\n border-radius: 7px;\n color: var(--muted);\n cursor: pointer;\n transition:\n background 0.15s,\n color 0.15s;\n}\n.sz__back:hover,\n.sz__act:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.sz__path {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n text-align: left;\n font-size: 12.5px;\n font-weight: 500;\n color: var(--text-strong);\n}\n.sz__bsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n.sz__view {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n gap: 2px;\n padding: 2px;\n border-radius: 7px;\n background: var(--surface-2);\n}\n.sz__vbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 24px;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 5px;\n color: var(--faint);\n cursor: pointer;\n}\n.sz__vbtn--on {\n background: var(--surface);\n color: var(--accent);\n box-shadow: var(--shadow);\n}\n\n/* Password UI (encrypted 7z) --------------------------------------------------------------------- */\n.sz__locked {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n padding: 24px;\n text-align: center;\n}\n.sz__lockicon {\n color: var(--faint);\n}\n.sz__lockmsg {\n margin: 0;\n font-size: 13px;\n color: var(--muted);\n}\n.sz__lockform {\n display: flex;\n gap: 8px;\n width: 100%;\n max-width: 320px;\n}\n.sz__lockerr {\n margin: 0;\n font-size: 12px;\n color: var(--danger);\n}\n/* Inline password bar (content-encrypted, names readable) + shared input/button. */\n.sz__pw {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 8px 12px;\n border-bottom: 1px solid var(--border);\n background: var(--surface-2);\n color: var(--muted);\n}\n.sz__pw--ok {\n color: var(--accent);\n}\n.sz__pwinput {\n flex: 1 1 auto;\n min-width: 0;\n height: 28px;\n padding: 0 9px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n}\n.sz__pwinput:focus {\n outline: none;\n border-color: var(--accent);\n}\n.sz__pwbtn {\n flex-shrink: 0;\n height: 28px;\n padding: 0 12px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n}\n.sz__pwbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.sz__pwerr {\n flex-shrink: 0;\n font-size: 11.5px;\n color: var(--danger);\n}\n.sz__rowlock {\n flex-shrink: 0;\n color: var(--faint);\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"SevenZipBrowser.js","names":[],"sources":["../../src/components/SevenZipBrowser.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport {\n computed,\n defineAsyncComponent,\n nextTick,\n onBeforeUnmount,\n onMounted,\n reactive,\n ref,\n watch,\n} from 'vue'\nimport {\n extractSevenZipEntry,\n readSevenZip,\n SevenZipEncryptedError,\n SevenZipPasswordError,\n type SevenZipArchive,\n type SevenZipEntry,\n} from '@/lib/sevenZip'\nimport type { DicomSlice } from '@/lib/dicom'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { useArchiveImageGallery } from '@/composables/useArchiveImageGallery'\nimport { fileExt, isDicomFile, isTextFile } from '@/lib/fileType'\nimport { canToggleView, classifyViewerFile, mediaMimeForName } from '@/lib/viewerKind'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n// A folder of sibling .dcm files browses as one scrollable series — handled directly (the body has no slice model).\nconst DicomViewer = defineAsyncComponent(() => import('@/components/DicomViewer.vue'))\n// The shared side-viewer body renders a selected entry with the SAME viewers as a top-level file. Lazy to break\n// the module cycle (the body lazy-imports this browser back, for nested archives).\nconst FileViewerBody = defineAsyncComponent(() => import('@/components/FileViewerBody.vue'))\n\n/**\n * The side viewer's 7z mode — the LZMA counterpart to {@link ZipBrowser}. Fetches an archive's bytes, parses +\n * decompresses them entirely in-browser (see lib/sevenZip.ts, which carries its own LZMA decoder since browsers\n * ship none), and browses the contents. Selecting an entry extracts just that one, wraps it in a blob URL, and\n * hands it to {@link FileViewerBody} (so a file inside the 7z previews with every viewer a top-level file gets).\n */\nconst props = defineProps<{ url: string; name: string }>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nlet buffer: ArrayBuffer | null = null\nlet archive: SevenZipArchive | null = null\nconst entries = ref<SevenZipEntry[]>([])\n\n// Password state for AES-encrypted 7z. `locked` = the archive header itself is encrypted (encrypted file\n// names), so it can't even be listed until unlocked; `contentEncrypted` = the listing worked but file bytes\n// need a password; `unlocked` = a password has been accepted.\nconst password = ref('')\nconst pwError = ref('')\nconst locked = ref(false)\nconst unlocked = ref(false)\nconst contentEncrypted = ref(false)\n\ninterface TreeNode {\n name: string\n path: string\n isDir: boolean\n size: number\n entry?: SevenZipEntry\n children: TreeNode[]\n}\nconst root = ref<TreeNode>({ name: '', path: '', isDir: true, size: 0, children: [] })\nconst expanded = reactive(new Set<string>())\n\nconst fileCount = computed(() => entries.value.reduce((n, e) => n + (e.isDir ? 0 : 1), 0))\nconst totalSize = computed(() => entries.value.reduce((s, e) => s + (e.isDir ? 0 : e.size), 0))\n\n// Build a folder tree from the flat entry paths (directories may be implicit — derive them either way).\nfunction buildTree(list: SevenZipEntry[]): TreeNode {\n const rootNode: TreeNode = { name: '', path: '', isDir: true, size: 0, children: [] }\n const dirs = new Map<string, TreeNode>([['', rootNode]])\n const ensureDir = (segs: string[]): TreeNode => {\n let cur = rootNode\n let acc = ''\n for (const seg of segs) {\n acc = acc ? `${acc}/${seg}` : seg\n let node = dirs.get(acc)\n if (!node) {\n node = { name: seg, path: acc, isDir: true, size: 0, children: [] }\n dirs.set(acc, node)\n cur.children.push(node)\n }\n cur = node\n }\n return cur\n }\n for (const e of list) {\n const segs = e.path.split('/').filter(Boolean)\n if (e.isDir) {\n ensureDir(segs)\n continue\n }\n const fname = segs.pop()\n if (!fname) continue\n ensureDir(segs).children.push({\n name: fname,\n path: e.path,\n isDir: false,\n size: e.size,\n entry: e,\n children: [],\n })\n }\n sortNode(rootNode)\n return rootNode\n}\n// Folders first, then files, each alphabetical (case-insensitive).\nfunction sortNode(n: TreeNode): void {\n n.children.sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1))\n for (const c of n.children) if (c.isDir) sortNode(c)\n}\n\n// Flatten the tree to the rows currently visible (respecting which folders are expanded).\ninterface Row {\n node: TreeNode\n depth: number\n}\nconst rows = computed<Row[]>(() => {\n const out: Row[] = []\n const walk = (nodes: TreeNode[], depth: number): void => {\n for (const n of nodes) {\n out.push({ node: n, depth })\n if (n.isDir && expanded.has(n.path)) walk(n.children, depth + 1)\n }\n }\n walk(root.value.children, 0)\n return out\n})\n\nasync function load(): Promise<void> {\n imageGallery.clear()\n status.value = 'loading'\n errorMsg.value = ''\n selected.value = null\n password.value = ''\n pwError.value = ''\n locked.value = false\n unlocked.value = false\n contentEncrypted.value = false\n revokeBlob()\n stackSlices.value = []\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n buffer = await res.arrayBuffer()\n try {\n archive = readSevenZip(buffer)\n } catch (e) {\n if (e instanceof SevenZipEncryptedError) {\n // Encrypted file names: nothing can be listed until the password unlocks the header.\n locked.value = true\n status.value = 'ready'\n return\n }\n throw e\n }\n entries.value = archive.entries\n root.value = buildTree(archive.entries)\n expanded.clear()\n contentEncrypted.value = archive.entries.some((e) => e.encrypted)\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction pwMessage(e: unknown): string {\n if (e instanceof SevenZipPasswordError) return '密码错误'\n if (e instanceof SevenZipEncryptedError) return '需要密码'\n return e instanceof Error ? e.message : String(e)\n}\n\n// Unlock an encrypted archive. An encrypted header re-lists with the password (the KDF runs here, ~100ms);\n// content-only encryption verifies the password by decoding the first encrypted entry's folder.\nasync function submitPassword(): Promise<void> {\n if (!buffer || !password.value) return\n pwError.value = ''\n if (locked.value) {\n status.value = 'loading'\n await nextTick() // let the spinner paint before the synchronous key derivation blocks\n try {\n archive = readSevenZip(buffer, password.value)\n entries.value = archive.entries\n root.value = buildTree(archive.entries)\n expanded.clear()\n contentEncrypted.value = archive.entries.some((e) => e.encrypted)\n locked.value = false\n unlocked.value = true\n } catch (e) {\n pwError.value = pwMessage(e)\n } finally {\n status.value = 'ready'\n }\n return\n }\n try {\n const target = entries.value.find((e) => !e.isDir && e.encrypted)\n if (target && archive) extractSevenZipEntry(buffer, archive, target, password.value)\n unlocked.value = true\n } catch (e) {\n unlocked.value = false\n pwError.value = pwMessage(e)\n }\n}\n\n// ── selected entry (detail view) ──────────────────────────────────────────────────────────────\nconst selected = ref<SevenZipEntry | null>(null)\nconst entryStatus = ref<Status>('ready')\nconst entryErr = ref('')\nconst entryText = ref('')\nconst entryFile = ref<ViewerFile | null>(null)\nconst entryStack = ref(false)\nconst entryMode = ref<'render' | 'source'>('render')\nconst blobUrl = ref('')\n\n// DICOM series stack: a folder of .dcm slices browsed as one scrollable series (see ZipBrowser). 7z decode is\n// synchronous, but slice.load() stays async to match DicomSlice (the viewer awaits it either way).\nconst stackSlices = ref<DicomSlice[]>([])\nconst stackStart = ref(0)\nconst stackName = ref('')\nconst stackTotalSize = computed(() => stackSlices.value.reduce((s, sl) => s + (sl.size ?? 0), 0))\n\nconst canToggle = computed(() => canToggleView(entryFile.value, entryText.value))\n\nfunction revokeBlob(): void {\n if (blobUrl.value) {\n URL.revokeObjectURL(blobUrl.value)\n blobUrl.value = ''\n }\n}\n\nfunction dirOf(path: string): string {\n const i = path.lastIndexOf('/')\n return i < 0 ? '' : path.slice(0, i)\n}\n// Sibling .dcm files in the same folder, natural-sorted (numeric-aware) so e.g. img2 precedes img10.\nfunction dicomSiblings(e: SevenZipEntry): SevenZipEntry[] {\n const dir = dirOf(e.path)\n return entries.value\n .filter((x) => !x.isDir && isDicomFile(x.name) && dirOf(x.path) === dir)\n .sort((a, b) => a.path.localeCompare(b.path, undefined, { numeric: true, sensitivity: 'base' }))\n}\n\n// Treat as text if the name says so; for extensionless entries, sniff the first bytes for NULs.\nfunction looksText(bytes: Uint8Array, name: string): boolean {\n if (isTextFile(name)) return true\n if (fileExt(name)) return false\n const n = Math.min(bytes.length, 4096)\n for (let i = 0; i < n; i++) if (bytes[i] === 0) return false\n return true\n}\n\nconst imageGallery = useArchiveImageGallery<SevenZipEntry>({\n entries: () => entries.value,\n archiveName: () => props.name,\n extract: (entry) => {\n if (!buffer || !archive) throw new Error('7z 尚未加载')\n return extractSevenZipEntry(buffer, archive, entry, password.value)\n },\n})\n\nfunction selectEntry(e: SevenZipEntry): void {\n selected.value = e\n entryStatus.value = 'loading'\n entryErr.value = ''\n entryText.value = ''\n entryFile.value = null\n entryStack.value = false\n entryMode.value = 'render'\n revokeBlob()\n stackSlices.value = []\n if (!buffer || !archive) return\n try {\n if (isDicomFile(e.name)) {\n const siblings = dicomSiblings(e)\n if (siblings.length > 1) {\n const buf = buffer\n const arc = archive\n stackSlices.value = siblings.map((se) => ({\n name: se.name,\n size: se.size,\n load: async () => {\n const u = extractSevenZipEntry(buf, arc, se, password.value)\n const out = new ArrayBuffer(u.byteLength)\n new Uint8Array(out).set(u)\n return out\n },\n }))\n stackStart.value = Math.max(\n 0,\n siblings.findIndex((se) => se.path === e.path),\n )\n stackName.value = dirOf(e.path).split('/').pop() || e.name\n entryStack.value = true\n entryStatus.value = 'ready'\n return\n }\n }\n const bytes = extractSevenZipEntry(buffer, archive, e, password.value)\n blobUrl.value = URL.createObjectURL(new Blob([bytes], { type: mediaMimeForName(e.name) }))\n const file = classifyViewerFile({\n name: e.name,\n url: blobUrl.value,\n size: e.size,\n textHint: looksText(bytes, e.name),\n })\n entryFile.value = file\n entryStatus.value = 'ready'\n if (file.image) void imageGallery.open(e, bytes)\n } catch (err) {\n entryErr.value = pwMessage(err)\n entryStatus.value = 'error'\n }\n}\n\nfunction onRow(node: TreeNode): void {\n if (node.isDir) {\n if (expanded.has(node.path)) expanded.delete(node.path)\n else expanded.add(node.path)\n } else if (node.entry) {\n selectEntry(node.entry)\n }\n}\n\nfunction back(): void {\n selected.value = null\n entryText.value = ''\n entryFile.value = null\n entryStack.value = false\n revokeBlob()\n stackSlices.value = []\n}\n\n// Re-extract on demand (the decoded folder is cached in lib/sevenZip, so this stays cheap).\nfunction downloadEntry(e: SevenZipEntry): void {\n if (!buffer || !archive) return\n try {\n const url = URL.createObjectURL(\n new Blob([extractSevenZipEntry(buffer, archive, e, password.value)]),\n )\n const a = document.createElement('a')\n a.href = url\n a.download = e.name\n document.body.appendChild(a)\n a.click()\n a.remove()\n setTimeout(() => URL.revokeObjectURL(url), 1000)\n } catch {\n /* extraction failed — nothing to download */\n }\n}\n\nconst copied = ref(false)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyEntry(): Promise<void> {\n try {\n await navigator.clipboard?.writeText(entryText.value)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copied.value = true\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copied.value = false), 1400)\n}\n\nonMounted(load)\nwatch(() => props.url, load)\nonBeforeUnmount(() => {\n revokeBlob()\n clearTimeout(copyTimer)\n})\n</script>\n\n<template>\n <div class=\"sz\">\n <p v-if=\"status === 'loading'\" class=\"sz__msg\">正在解压…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"sz__msg sz__msg--error\">\n <p>{{ errorMsg }}</p>\n <button type=\"button\" class=\"sz__retry\" @click=\"load\">重试</button>\n </div>\n\n <!-- locked: the header (file names) is encrypted — must unlock before anything can be listed -->\n <div v-else-if=\"locked\" class=\"sz__locked\">\n <Icon name=\"key\" :size=\"26\" class=\"sz__lockicon\" />\n <p class=\"sz__lockmsg\">该 7z 已加密(含文件名),输入密码后查看</p>\n <div class=\"sz__lockform\">\n <input\n v-model=\"password\"\n type=\"password\"\n class=\"sz__pwinput\"\n placeholder=\"输入密码\"\n @keyup.enter=\"submitPassword\"\n />\n <button type=\"button\" class=\"sz__pwbtn\" @click=\"submitPassword\">解锁</button>\n </div>\n <p v-if=\"pwError\" class=\"sz__lockerr\">{{ pwError }}</p>\n </div>\n\n <!-- detail: one selected entry -->\n <template v-else-if=\"selected\">\n <div class=\"sz__bar\">\n <button type=\"button\" class=\"sz__back\" title=\"返回列表\" @click=\"back\">\n <Icon name=\"arrowLeft\" :size=\"16\" />\n </button>\n <span class=\"sz__path\" :title=\"selected.path\">{{ selected.path }}</span>\n <span class=\"sz__bsize\">{{ formatBytes(selected.size) }}</span>\n <div v-if=\"canToggle\" class=\"sz__view\" role=\"group\" aria-label=\"渲染或源码\">\n <button\n type=\"button\"\n class=\"sz__vbtn\"\n :class=\"{ 'sz__vbtn--on': entryMode === 'render' }\"\n title=\"渲染\"\n @click=\"entryMode = 'render'\"\n >\n <Icon name=\"eye\" :size=\"14\" />\n </button>\n <button\n type=\"button\"\n class=\"sz__vbtn\"\n :class=\"{ 'sz__vbtn--on': entryMode === 'source' }\"\n title=\"源码\"\n @click=\"entryMode = 'source'\"\n >\n <Icon name=\"code\" :size=\"14\" />\n </button>\n </div>\n <button\n v-if=\"entryText && entryStatus === 'ready'\"\n type=\"button\"\n class=\"sz__act\"\n :title=\"copied ? '已复制' : '复制'\"\n @click=\"copyEntry\"\n >\n <Icon name=\"copy\" :size=\"14\" />\n </button>\n <button type=\"button\" class=\"sz__act\" title=\"下载\" @click=\"downloadEntry(selected)\">\n <Icon name=\"download\" :size=\"15\" />\n </button>\n </div>\n\n <p v-if=\"entryStatus === 'loading'\" class=\"sz__msg\">读取中…</p>\n <div v-else-if=\"entryStatus === 'error'\" class=\"sz__msg sz__msg--error\">\n <p>{{ entryErr }}</p>\n </div>\n\n <!-- dicom series: a whole folder of .dcm slices browsed as one scrollable series (wheel/cine翻层) -->\n <DicomViewer\n v-else-if=\"entryStack\"\n :slices=\"stackSlices\"\n :start=\"stackStart\"\n :name=\"stackName\"\n :size=\"stackTotalSize\"\n />\n\n <!-- everything else: the shared body picks the right viewer from the extracted blob -->\n <FileViewerBody\n v-else-if=\"entryFile\"\n :file=\"entryFile\"\n :mode=\"entryMode\"\n @loaded=\"entryText = $event\"\n />\n </template>\n\n <!-- list: the file tree -->\n <template v-else>\n <div class=\"sz__summary\">{{ fileCount }} 个文件 · {{ formatBytes(totalSize) }}</div>\n\n <!-- password bar: shown when file bytes are encrypted (but the file names were readable) -->\n <div v-if=\"contentEncrypted\" class=\"sz__pw\" :class=\"{ 'sz__pw--ok': unlocked }\">\n <Icon name=\"key\" :size=\"14\" />\n <input\n v-model=\"password\"\n type=\"password\"\n class=\"sz__pwinput\"\n :placeholder=\"unlocked ? '已解锁(如需可重新输入)' : '此压缩包内容已加密,输入密码解锁'\"\n @keyup.enter=\"submitPassword\"\n />\n <button type=\"button\" class=\"sz__pwbtn\" @click=\"submitPassword\">\n {{ unlocked ? '已解锁' : '解锁' }}\n </button>\n <span v-if=\"pwError\" class=\"sz__pwerr\">{{ pwError }}</span>\n </div>\n\n <div class=\"sz__scroll sz__tree\">\n <p v-if=\"rows.length === 0\" class=\"sz__msg\">空压缩包</p>\n <button\n v-for=\"row in rows\"\n :key=\"row.node.path\"\n type=\"button\"\n class=\"sz__row\"\n :class=\"{ 'sz__row--dir': row.node.isDir }\"\n :style=\"{ paddingLeft: `${12 + row.depth * 14}px` }\"\n @click=\"onRow(row.node)\"\n >\n <span class=\"sz__twisty\">\n <Icon\n v-if=\"row.node.isDir\"\n name=\"chevronRight\"\n :size=\"13\"\n :class=\"{ 'sz__twisty--open': expanded.has(row.node.path) }\"\n />\n </span>\n <span class=\"sz__rowicon\">\n <Icon :name=\"row.node.isDir ? 'folder' : 'fileOutline'\" :size=\"15\" />\n </span>\n <span class=\"sz__rowname\">{{ row.node.name }}</span>\n <Icon\n v-if=\"!row.node.isDir && row.node.entry?.encrypted\"\n name=\"key\"\n :size=\"12\"\n class=\"sz__rowlock\"\n />\n <span v-if=\"!row.node.isDir\" class=\"sz__rowsize\">{{ formatBytes(row.node.size) }}</span>\n </button>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.sz {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.sz__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.sz__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.sz__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.sz__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* List view ------------------------------------------------------------------------------------ */\n.sz__summary {\n flex-shrink: 0;\n padding: 9px 16px;\n border-bottom: 1px solid var(--border);\n font-size: 11.5px;\n letter-spacing: 0.02em;\n color: var(--faint);\n}\n.sz__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.sz__tree {\n padding: 6px 0;\n}\n.sz__row {\n display: flex;\n align-items: center;\n gap: 7px;\n width: 100%;\n padding: 5px 12px 5px 12px;\n background: transparent;\n border: none;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n text-align: left;\n cursor: pointer;\n}\n.sz__row:hover {\n background: var(--surface-2);\n}\n.sz__twisty {\n flex-shrink: 0;\n display: inline-flex;\n width: 13px;\n color: var(--faint);\n}\n.sz__twisty--open {\n transform: rotate(90deg);\n transition: transform 0.12s;\n}\n.sz__rowicon {\n flex-shrink: 0;\n display: inline-flex;\n color: var(--faint);\n}\n.sz__row--dir .sz__rowicon {\n color: var(--accent);\n}\n.sz__rowname {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.sz__row--dir .sz__rowname {\n font-weight: 500;\n}\n.sz__rowsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n\n/* Detail bar ----------------------------------------------------------------------------------- */\n.sz__bar {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 7px 10px 7px 8px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.sz__back,\n.sz__act {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 30px;\n height: 28px;\n background: transparent;\n border: none;\n border-radius: 7px;\n color: var(--muted);\n cursor: pointer;\n transition:\n background 0.15s,\n color 0.15s;\n}\n.sz__back:hover,\n.sz__act:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.sz__path {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n text-align: left;\n font-size: 12.5px;\n font-weight: 500;\n color: var(--text-strong);\n}\n.sz__bsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n.sz__view {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n gap: 2px;\n padding: 2px;\n border-radius: 7px;\n background: var(--surface-2);\n}\n.sz__vbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 24px;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 5px;\n color: var(--faint);\n cursor: pointer;\n}\n.sz__vbtn--on {\n background: var(--surface);\n color: var(--accent);\n box-shadow: var(--shadow);\n}\n\n/* Password UI (encrypted 7z) --------------------------------------------------------------------- */\n.sz__locked {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n padding: 24px;\n text-align: center;\n}\n.sz__lockicon {\n color: var(--faint);\n}\n.sz__lockmsg {\n margin: 0;\n font-size: 13px;\n color: var(--muted);\n}\n.sz__lockform {\n display: flex;\n gap: 8px;\n width: 100%;\n max-width: 320px;\n}\n.sz__lockerr {\n margin: 0;\n font-size: 12px;\n color: var(--danger);\n}\n/* Inline password bar (content-encrypted, names readable) + shared input/button. */\n.sz__pw {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 8px 12px;\n border-bottom: 1px solid var(--border);\n background: var(--surface-2);\n color: var(--muted);\n}\n.sz__pw--ok {\n color: var(--accent);\n}\n.sz__pwinput {\n flex: 1 1 auto;\n min-width: 0;\n height: 28px;\n padding: 0 9px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n}\n.sz__pwinput:focus {\n outline: none;\n border-color: var(--accent);\n}\n.sz__pwbtn {\n flex-shrink: 0;\n height: 28px;\n padding: 0 12px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n}\n.sz__pwbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.sz__pwerr {\n flex-shrink: 0;\n font-size: 11.5px;\n color: var(--danger);\n}\n.sz__rowlock {\n flex-shrink: 0;\n color: var(--faint);\n}\n</style>\n"],"mappings":""}
@@ -2,56 +2,57 @@ import { fileExt as e, isDicomFile as t, isTextFile as n } from "../lib/fileType
2
2
  import { canToggleView as r, classifyViewerFile as i, mediaMimeForName as ee } from "../lib/viewerKind.js";
3
3
  import { formatBytes as a } from "../lib/format.js";
4
4
  import o from "./icons/Icon.js";
5
- import { SevenZipEncryptedError as te, SevenZipPasswordError as ne, extractSevenZipEntry as s, readSevenZip as c } from "../lib/sevenZip.js";
6
- import { Fragment as l, computed as u, createBlock as d, createCommentVNode as f, createElementBlock as p, createElementVNode as m, createVNode as h, defineAsyncComponent as re, defineComponent as g, nextTick as ie, normalizeClass as _, normalizeStyle as ae, onBeforeUnmount as oe, onMounted as se, openBlock as v, reactive as ce, ref as y, renderList as le, toDisplayString as b, unref as x, vModelText as ue, watch as de, withDirectives as fe, withKeys as pe } from "vue";
5
+ import { useArchiveImageGallery as te } from "../composables/useArchiveImageGallery.js";
6
+ import { SevenZipEncryptedError as ne, SevenZipPasswordError as re, extractSevenZipEntry as s, readSevenZip as ie } from "../lib/sevenZip.js";
7
+ import { Fragment as c, computed as l, createBlock as u, createCommentVNode as d, createElementBlock as f, createElementVNode as p, createVNode as m, defineAsyncComponent as ae, defineComponent as h, nextTick as oe, normalizeClass as g, normalizeStyle as se, onBeforeUnmount as ce, onMounted as le, openBlock as _, reactive as ue, ref as v, renderList as de, toDisplayString as y, unref as b, vModelText as x, watch as fe, withDirectives as S, withKeys as C } from "vue";
7
8
  //#region src/components/SevenZipBrowser.vue?vue&type=script&setup=true&lang.ts
8
- var me = { class: "sz" }, he = {
9
+ var pe = { class: "sz" }, me = {
9
10
  key: 0,
10
11
  class: "sz__msg"
11
- }, ge = {
12
+ }, he = {
12
13
  key: 1,
13
14
  class: "sz__msg sz__msg--error"
14
- }, _e = {
15
+ }, ge = {
15
16
  key: 2,
16
17
  class: "sz__locked"
17
- }, ve = { class: "sz__lockform" }, ye = {
18
+ }, _e = { class: "sz__lockform" }, ve = {
18
19
  key: 0,
19
20
  class: "sz__lockerr"
20
- }, be = { class: "sz__bar" }, xe = ["title"], Se = { class: "sz__bsize" }, Ce = {
21
+ }, ye = { class: "sz__bar" }, be = ["title"], xe = { class: "sz__bsize" }, Se = {
21
22
  key: 0,
22
23
  class: "sz__view",
23
24
  role: "group",
24
25
  "aria-label": "渲染或源码"
25
- }, we = ["title"], Te = {
26
+ }, Ce = ["title"], we = {
26
27
  key: 0,
27
28
  class: "sz__msg"
28
- }, Ee = {
29
+ }, Te = {
29
30
  key: 1,
30
31
  class: "sz__msg sz__msg--error"
31
- }, De = { class: "sz__summary" }, Oe = ["placeholder"], ke = {
32
+ }, Ee = { class: "sz__summary" }, De = ["placeholder"], Oe = {
32
33
  key: 0,
33
34
  class: "sz__pwerr"
34
- }, Ae = { class: "sz__scroll sz__tree" }, je = {
35
+ }, ke = { class: "sz__scroll sz__tree" }, Ae = {
35
36
  key: 0,
36
37
  class: "sz__msg"
37
- }, Me = ["onClick"], Ne = { class: "sz__twisty" }, Pe = { class: "sz__rowicon" }, Fe = { class: "sz__rowname" }, Ie = {
38
+ }, je = ["onClick"], Me = { class: "sz__twisty" }, Ne = { class: "sz__rowicon" }, Pe = { class: "sz__rowname" }, Fe = {
38
39
  key: 1,
39
40
  class: "sz__rowsize"
40
- }, S = /*@__PURE__*/ g({
41
+ }, w = /*@__PURE__*/ h({
41
42
  __name: "SevenZipBrowser",
42
43
  props: {
43
44
  url: {},
44
45
  name: {}
45
46
  },
46
- setup(g) {
47
- let S = re(() => import("./DicomViewer.js")), Le = re(() => import("./FileViewerBody.js")), C = g, w = y("loading"), T = y(""), E = null, D = null, O = y([]), k = y(""), A = y(""), j = y(!1), M = y(!1), N = y(!1), P = y({
47
+ setup(h) {
48
+ let w = ae(() => import("./DicomViewer.js")), Ie = ae(() => import("./FileViewerBody.js")), T = h, E = v("loading"), D = v(""), O = null, k = null, A = v([]), j = v(""), M = v(""), N = v(!1), P = v(!1), F = v(!1), I = v({
48
49
  name: "",
49
50
  path: "",
50
51
  isDir: !0,
51
52
  size: 0,
52
53
  children: []
53
- }), F = ce(/* @__PURE__ */ new Set()), Re = u(() => O.value.reduce((e, t) => e + +!t.isDir, 0)), ze = u(() => O.value.reduce((e, t) => e + (t.isDir ? 0 : t.size), 0));
54
- function Be(e) {
54
+ }), L = ue(/* @__PURE__ */ new Set()), Le = l(() => A.value.reduce((e, t) => e + +!t.isDir, 0)), Re = l(() => A.value.reduce((e, t) => e + (t.isDir ? 0 : t.size), 0));
55
+ function ze(e) {
55
56
  let t = {
56
57
  name: "",
57
58
  path: "",
@@ -89,280 +90,290 @@ var me = { class: "sz" }, he = {
89
90
  children: []
90
91
  });
91
92
  }
92
- return Ve(t), t;
93
+ return Be(t), t;
93
94
  }
94
- function Ve(e) {
95
+ function Be(e) {
95
96
  e.children.sort((e, t) => e.isDir === t.isDir ? e.name.localeCompare(t.name) : e.isDir ? -1 : 1);
96
- for (let t of e.children) t.isDir && Ve(t);
97
+ for (let t of e.children) t.isDir && Be(t);
97
98
  }
98
- let He = u(() => {
99
+ let Ve = l(() => {
99
100
  let e = [], t = (n, r) => {
100
101
  for (let i of n) e.push({
101
102
  node: i,
102
103
  depth: r
103
- }), i.isDir && F.has(i.path) && t(i.children, r + 1);
104
+ }), i.isDir && L.has(i.path) && t(i.children, r + 1);
104
105
  };
105
- return t(P.value.children, 0), e;
106
+ return t(I.value.children, 0), e;
106
107
  });
107
- async function I() {
108
- w.value = "loading", T.value = "", z.value = null, k.value = "", A.value = "", j.value = !1, M.value = !1, N.value = !1, X(), q.value = [];
108
+ async function R() {
109
+ Je.clear(), E.value = "loading", D.value = "", V.value = null, j.value = "", M.value = "", N.value = !1, P.value = !1, F.value = !1, X(), Y.value = [];
109
110
  try {
110
- let e = await fetch(C.url);
111
+ let e = await fetch(T.url);
111
112
  if (!e.ok) throw Error(`HTTP ${e.status}`);
112
- E = await e.arrayBuffer();
113
+ O = await e.arrayBuffer();
113
114
  try {
114
- D = c(E);
115
+ k = ie(O);
115
116
  } catch (e) {
116
- if (e instanceof te) {
117
- j.value = !0, w.value = "ready";
117
+ if (e instanceof ne) {
118
+ N.value = !0, E.value = "ready";
118
119
  return;
119
120
  }
120
121
  throw e;
121
122
  }
122
- O.value = D.entries, P.value = Be(D.entries), F.clear(), N.value = D.entries.some((e) => e.encrypted), w.value = "ready";
123
+ A.value = k.entries, I.value = ze(k.entries), L.clear(), F.value = k.entries.some((e) => e.encrypted), E.value = "ready";
123
124
  } catch (e) {
124
- T.value = e instanceof Error ? e.message : String(e), w.value = "error";
125
+ D.value = e instanceof Error ? e.message : String(e), E.value = "error";
125
126
  }
126
127
  }
127
- function L(e) {
128
- return e instanceof ne ? "密码错误" : e instanceof te ? "需要密码" : e instanceof Error ? e.message : String(e);
128
+ function z(e) {
129
+ return e instanceof re ? "密码错误" : e instanceof ne ? "需要密码" : e instanceof Error ? e.message : String(e);
129
130
  }
130
- async function R() {
131
- if (!(!E || !k.value)) {
132
- if (A.value = "", j.value) {
133
- w.value = "loading", await ie();
131
+ async function B() {
132
+ if (!(!O || !j.value)) {
133
+ if (M.value = "", N.value) {
134
+ E.value = "loading", await oe();
134
135
  try {
135
- D = c(E, k.value), O.value = D.entries, P.value = Be(D.entries), F.clear(), N.value = D.entries.some((e) => e.encrypted), j.value = !1, M.value = !0;
136
+ k = ie(O, j.value), A.value = k.entries, I.value = ze(k.entries), L.clear(), F.value = k.entries.some((e) => e.encrypted), N.value = !1, P.value = !0;
136
137
  } catch (e) {
137
- A.value = L(e);
138
+ M.value = z(e);
138
139
  } finally {
139
- w.value = "ready";
140
+ E.value = "ready";
140
141
  }
141
142
  return;
142
143
  }
143
144
  try {
144
- let e = O.value.find((e) => !e.isDir && e.encrypted);
145
- e && D && s(E, D, e, k.value), M.value = !0;
145
+ let e = A.value.find((e) => !e.isDir && e.encrypted);
146
+ e && k && s(O, k, e, j.value), P.value = !0;
146
147
  } catch (e) {
147
- M.value = !1, A.value = L(e);
148
+ P.value = !1, M.value = z(e);
148
149
  }
149
150
  }
150
151
  }
151
- let z = y(null), B = y("ready"), V = y(""), H = y(""), U = y(null), W = y(!1), G = y("render"), K = y(""), q = y([]), J = y(0), Y = y(""), Ue = u(() => q.value.reduce((e, t) => e + (t.size ?? 0), 0)), We = u(() => r(U.value, H.value));
152
+ let V = v(null), H = v("ready"), U = v(""), W = v(""), G = v(null), K = v(!1), q = v("render"), J = v(""), Y = v([]), He = v(0), Ue = v(""), We = l(() => Y.value.reduce((e, t) => e + (t.size ?? 0), 0)), Ge = l(() => r(G.value, W.value));
152
153
  function X() {
153
- K.value &&= (URL.revokeObjectURL(K.value), "");
154
+ J.value &&= (URL.revokeObjectURL(J.value), "");
154
155
  }
155
156
  function Z(e) {
156
157
  let t = e.lastIndexOf("/");
157
158
  return t < 0 ? "" : e.slice(0, t);
158
159
  }
159
- function Ge(e) {
160
+ function Ke(e) {
160
161
  let n = Z(e.path);
161
- return O.value.filter((e) => !e.isDir && t(e.name) && Z(e.path) === n).sort((e, t) => e.path.localeCompare(t.path, void 0, {
162
+ return A.value.filter((e) => !e.isDir && t(e.name) && Z(e.path) === n).sort((e, t) => e.path.localeCompare(t.path, void 0, {
162
163
  numeric: !0,
163
164
  sensitivity: "base"
164
165
  }));
165
166
  }
166
- function Ke(t, r) {
167
+ function qe(t, r) {
167
168
  if (n(r)) return !0;
168
169
  if (e(r)) return !1;
169
170
  let i = Math.min(t.length, 4096);
170
171
  for (let e = 0; e < i; e++) if (t[e] === 0) return !1;
171
172
  return !0;
172
173
  }
173
- function qe(e) {
174
- if (z.value = e, B.value = "loading", V.value = "", H.value = "", U.value = null, W.value = !1, G.value = "render", X(), q.value = [], !(!E || !D)) try {
174
+ let Je = te({
175
+ entries: () => A.value,
176
+ archiveName: () => T.name,
177
+ extract: (e) => {
178
+ if (!O || !k) throw Error("7z 尚未加载");
179
+ return s(O, k, e, j.value);
180
+ }
181
+ });
182
+ function Ye(e) {
183
+ if (V.value = e, H.value = "loading", U.value = "", W.value = "", G.value = null, K.value = !1, q.value = "render", X(), Y.value = [], !(!O || !k)) try {
175
184
  if (t(e.name)) {
176
- let t = Ge(e);
185
+ let t = Ke(e);
177
186
  if (t.length > 1) {
178
- let n = E, r = D;
179
- q.value = t.map((e) => ({
187
+ let n = O, r = k;
188
+ Y.value = t.map((e) => ({
180
189
  name: e.name,
181
190
  size: e.size,
182
191
  load: async () => {
183
- let t = s(n, r, e, k.value), i = new ArrayBuffer(t.byteLength);
192
+ let t = s(n, r, e, j.value), i = new ArrayBuffer(t.byteLength);
184
193
  return new Uint8Array(i).set(t), i;
185
194
  }
186
- })), 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";
195
+ })), He.value = Math.max(0, t.findIndex((t) => t.path === e.path)), Ue.value = Z(e.path).split("/").pop() || e.name, K.value = !0, H.value = "ready";
187
196
  return;
188
197
  }
189
198
  }
190
- let n = s(E, D, e, k.value);
191
- K.value = URL.createObjectURL(new Blob([n], { type: ee(e.name) })), U.value = i({
199
+ let n = s(O, k, e, j.value);
200
+ J.value = URL.createObjectURL(new Blob([n], { type: ee(e.name) }));
201
+ let r = i({
192
202
  name: e.name,
193
- url: K.value,
203
+ url: J.value,
194
204
  size: e.size,
195
- textHint: Ke(n, e.name)
196
- }), B.value = "ready";
205
+ textHint: qe(n, e.name)
206
+ });
207
+ G.value = r, H.value = "ready", r.image && Je.open(e, n);
197
208
  } catch (e) {
198
- V.value = L(e), B.value = "error";
209
+ U.value = z(e), H.value = "error";
199
210
  }
200
211
  }
201
- function Je(e) {
202
- e.isDir ? F.has(e.path) ? F.delete(e.path) : F.add(e.path) : e.entry && qe(e.entry);
212
+ function Xe(e) {
213
+ e.isDir ? L.has(e.path) ? L.delete(e.path) : L.add(e.path) : e.entry && Ye(e.entry);
203
214
  }
204
- function Ye() {
205
- z.value = null, H.value = "", U.value = null, W.value = !1, X(), q.value = [];
215
+ function Ze() {
216
+ V.value = null, W.value = "", G.value = null, K.value = !1, X(), Y.value = [];
206
217
  }
207
- function Xe(e) {
208
- if (!(!E || !D)) try {
209
- let t = URL.createObjectURL(new Blob([s(E, D, e, k.value)])), n = document.createElement("a");
218
+ function Qe(e) {
219
+ if (!(!O || !k)) try {
220
+ let t = URL.createObjectURL(new Blob([s(O, k, e, j.value)])), n = document.createElement("a");
210
221
  n.href = t, n.download = e.name, document.body.appendChild(n), n.click(), n.remove(), setTimeout(() => URL.revokeObjectURL(t), 1e3);
211
222
  } catch {}
212
223
  }
213
- let Q = y(!1), $;
214
- async function Ze() {
224
+ let Q = v(!1), $;
225
+ async function $e() {
215
226
  try {
216
- await navigator.clipboard?.writeText(H.value);
227
+ await navigator.clipboard?.writeText(W.value);
217
228
  } catch {}
218
229
  Q.value = !0, clearTimeout($), $ = setTimeout(() => Q.value = !1, 1400);
219
230
  }
220
- return se(I), de(() => C.url, I), oe(() => {
231
+ return le(R), fe(() => T.url, R), ce(() => {
221
232
  X(), clearTimeout($);
222
- }), (e, t) => (v(), p("div", me, [w.value === "loading" ? (v(), p("p", he, "正在解压…")) : w.value === "error" ? (v(), p("div", ge, [m("p", null, b(T.value), 1), m("button", {
233
+ }), (e, t) => (_(), f("div", pe, [E.value === "loading" ? (_(), f("p", me, "正在解压…")) : E.value === "error" ? (_(), f("div", he, [p("p", null, y(D.value), 1), p("button", {
223
234
  type: "button",
224
235
  class: "sz__retry",
225
- onClick: I
226
- }, "重试")])) : j.value ? (v(), p("div", _e, [
227
- h(o, {
236
+ onClick: R
237
+ }, "重试")])) : N.value ? (_(), f("div", ge, [
238
+ m(o, {
228
239
  name: "key",
229
240
  size: 26,
230
241
  class: "sz__lockicon"
231
242
  }),
232
- t[6] ||= m("p", { class: "sz__lockmsg" }, "该 7z 已加密(含文件名),输入密码后查看", -1),
233
- m("div", ve, [fe(m("input", {
234
- "onUpdate:modelValue": t[0] ||= (e) => k.value = e,
243
+ t[6] ||= p("p", { class: "sz__lockmsg" }, "该 7z 已加密(含文件名),输入密码后查看", -1),
244
+ p("div", _e, [S(p("input", {
245
+ "onUpdate:modelValue": t[0] ||= (e) => j.value = e,
235
246
  type: "password",
236
247
  class: "sz__pwinput",
237
248
  placeholder: "输入密码",
238
- onKeyup: pe(R, ["enter"])
239
- }, null, 544), [[ue, k.value]]), m("button", {
249
+ onKeyup: C(B, ["enter"])
250
+ }, null, 544), [[x, j.value]]), p("button", {
240
251
  type: "button",
241
252
  class: "sz__pwbtn",
242
- onClick: R
253
+ onClick: B
243
254
  }, "解锁")]),
244
- A.value ? (v(), p("p", ye, b(A.value), 1)) : f("", !0)
245
- ])) : z.value ? (v(), p(l, { key: 3 }, [m("div", be, [
246
- m("button", {
255
+ M.value ? (_(), f("p", ve, y(M.value), 1)) : d("", !0)
256
+ ])) : V.value ? (_(), f(c, { key: 3 }, [p("div", ye, [
257
+ p("button", {
247
258
  type: "button",
248
259
  class: "sz__back",
249
260
  title: "返回列表",
250
- onClick: Ye
251
- }, [h(o, {
261
+ onClick: Ze
262
+ }, [m(o, {
252
263
  name: "arrowLeft",
253
264
  size: 16
254
265
  })]),
255
- m("span", {
266
+ p("span", {
256
267
  class: "sz__path",
257
- title: z.value.path
258
- }, b(z.value.path), 9, xe),
259
- m("span", Se, b(x(a)(z.value.size)), 1),
260
- We.value ? (v(), p("div", Ce, [m("button", {
268
+ title: V.value.path
269
+ }, y(V.value.path), 9, be),
270
+ p("span", xe, y(b(a)(V.value.size)), 1),
271
+ Ge.value ? (_(), f("div", Se, [p("button", {
261
272
  type: "button",
262
- class: _(["sz__vbtn", { "sz__vbtn--on": G.value === "render" }]),
273
+ class: g(["sz__vbtn", { "sz__vbtn--on": q.value === "render" }]),
263
274
  title: "渲染",
264
- onClick: t[1] ||= (e) => G.value = "render"
265
- }, [h(o, {
275
+ onClick: t[1] ||= (e) => q.value = "render"
276
+ }, [m(o, {
266
277
  name: "eye",
267
278
  size: 14
268
- })], 2), m("button", {
279
+ })], 2), p("button", {
269
280
  type: "button",
270
- class: _(["sz__vbtn", { "sz__vbtn--on": G.value === "source" }]),
281
+ class: g(["sz__vbtn", { "sz__vbtn--on": q.value === "source" }]),
271
282
  title: "源码",
272
- onClick: t[2] ||= (e) => G.value = "source"
273
- }, [h(o, {
283
+ onClick: t[2] ||= (e) => q.value = "source"
284
+ }, [m(o, {
274
285
  name: "code",
275
286
  size: 14
276
- })], 2)])) : f("", !0),
277
- H.value && B.value === "ready" ? (v(), p("button", {
287
+ })], 2)])) : d("", !0),
288
+ W.value && H.value === "ready" ? (_(), f("button", {
278
289
  key: 1,
279
290
  type: "button",
280
291
  class: "sz__act",
281
292
  title: Q.value ? "已复制" : "复制",
282
- onClick: Ze
283
- }, [h(o, {
293
+ onClick: $e
294
+ }, [m(o, {
284
295
  name: "copy",
285
296
  size: 14
286
- })], 8, we)) : f("", !0),
287
- m("button", {
297
+ })], 8, Ce)) : d("", !0),
298
+ p("button", {
288
299
  type: "button",
289
300
  class: "sz__act",
290
301
  title: "下载",
291
- onClick: t[3] ||= (e) => Xe(z.value)
292
- }, [h(o, {
302
+ onClick: t[3] ||= (e) => Qe(V.value)
303
+ }, [m(o, {
293
304
  name: "download",
294
305
  size: 15
295
306
  })])
296
- ]), B.value === "loading" ? (v(), p("p", Te, "读取中…")) : B.value === "error" ? (v(), p("div", Ee, [m("p", null, b(V.value), 1)])) : W.value ? (v(), d(x(S), {
307
+ ]), H.value === "loading" ? (_(), f("p", we, "读取中…")) : H.value === "error" ? (_(), f("div", Te, [p("p", null, y(U.value), 1)])) : K.value ? (_(), u(b(w), {
297
308
  key: 2,
298
- slices: q.value,
299
- start: J.value,
300
- name: Y.value,
301
- size: Ue.value
309
+ slices: Y.value,
310
+ start: He.value,
311
+ name: Ue.value,
312
+ size: We.value
302
313
  }, null, 8, [
303
314
  "slices",
304
315
  "start",
305
316
  "name",
306
317
  "size"
307
- ])) : U.value ? (v(), d(x(Le), {
318
+ ])) : G.value ? (_(), u(b(Ie), {
308
319
  key: 3,
309
- file: U.value,
310
- mode: G.value,
311
- onLoaded: t[4] ||= (e) => H.value = e
312
- }, null, 8, ["file", "mode"])) : f("", !0)], 64)) : (v(), p(l, { key: 4 }, [
313
- m("div", De, b(Re.value) + " 个文件 · " + b(x(a)(ze.value)), 1),
314
- N.value ? (v(), p("div", {
320
+ file: G.value,
321
+ mode: q.value,
322
+ onLoaded: t[4] ||= (e) => W.value = e
323
+ }, null, 8, ["file", "mode"])) : d("", !0)], 64)) : (_(), f(c, { key: 4 }, [
324
+ p("div", Ee, y(Le.value) + " 个文件 · " + y(b(a)(Re.value)), 1),
325
+ F.value ? (_(), f("div", {
315
326
  key: 0,
316
- class: _(["sz__pw", { "sz__pw--ok": M.value }])
327
+ class: g(["sz__pw", { "sz__pw--ok": P.value }])
317
328
  }, [
318
- h(o, {
329
+ m(o, {
319
330
  name: "key",
320
331
  size: 14
321
332
  }),
322
- fe(m("input", {
323
- "onUpdate:modelValue": t[5] ||= (e) => k.value = e,
333
+ S(p("input", {
334
+ "onUpdate:modelValue": t[5] ||= (e) => j.value = e,
324
335
  type: "password",
325
336
  class: "sz__pwinput",
326
- placeholder: M.value ? "已解锁(如需可重新输入)" : "此压缩包内容已加密,输入密码解锁",
327
- onKeyup: pe(R, ["enter"])
328
- }, null, 40, Oe), [[ue, k.value]]),
329
- m("button", {
337
+ placeholder: P.value ? "已解锁(如需可重新输入)" : "此压缩包内容已加密,输入密码解锁",
338
+ onKeyup: C(B, ["enter"])
339
+ }, null, 40, De), [[x, j.value]]),
340
+ p("button", {
330
341
  type: "button",
331
342
  class: "sz__pwbtn",
332
- onClick: R
333
- }, b(M.value ? "已解锁" : "解锁"), 1),
334
- A.value ? (v(), p("span", ke, b(A.value), 1)) : f("", !0)
335
- ], 2)) : f("", !0),
336
- m("div", Ae, [He.value.length === 0 ? (v(), p("p", je, "空压缩包")) : f("", !0), (v(!0), p(l, null, le(He.value, (e) => (v(), p("button", {
343
+ onClick: B
344
+ }, y(P.value ? "已解锁" : "解锁"), 1),
345
+ M.value ? (_(), f("span", Oe, y(M.value), 1)) : d("", !0)
346
+ ], 2)) : d("", !0),
347
+ p("div", ke, [Ve.value.length === 0 ? (_(), f("p", Ae, "空压缩包")) : d("", !0), (_(!0), f(c, null, de(Ve.value, (e) => (_(), f("button", {
337
348
  key: e.node.path,
338
349
  type: "button",
339
- class: _(["sz__row", { "sz__row--dir": e.node.isDir }]),
340
- style: ae({ paddingLeft: `${12 + e.depth * 14}px` }),
341
- onClick: (t) => Je(e.node)
350
+ class: g(["sz__row", { "sz__row--dir": e.node.isDir }]),
351
+ style: se({ paddingLeft: `${12 + e.depth * 14}px` }),
352
+ onClick: (t) => Xe(e.node)
342
353
  }, [
343
- m("span", Ne, [e.node.isDir ? (v(), d(o, {
354
+ p("span", Me, [e.node.isDir ? (_(), u(o, {
344
355
  key: 0,
345
356
  name: "chevronRight",
346
357
  size: 13,
347
- class: _({ "sz__twisty--open": F.has(e.node.path) })
348
- }, null, 8, ["class"])) : f("", !0)]),
349
- m("span", Pe, [h(o, {
358
+ class: g({ "sz__twisty--open": L.has(e.node.path) })
359
+ }, null, 8, ["class"])) : d("", !0)]),
360
+ p("span", Ne, [m(o, {
350
361
  name: e.node.isDir ? "folder" : "fileOutline",
351
362
  size: 15
352
363
  }, null, 8, ["name"])]),
353
- m("span", Fe, b(e.node.name), 1),
354
- !e.node.isDir && e.node.entry?.encrypted ? (v(), d(o, {
364
+ p("span", Pe, y(e.node.name), 1),
365
+ !e.node.isDir && e.node.entry?.encrypted ? (_(), u(o, {
355
366
  key: 0,
356
367
  name: "key",
357
368
  size: 12,
358
369
  class: "sz__rowlock"
359
- })) : f("", !0),
360
- e.node.isDir ? f("", !0) : (v(), p("span", Ie, b(x(a)(e.node.size)), 1))
361
- ], 14, Me))), 128))])
370
+ })) : d("", !0),
371
+ e.node.isDir ? d("", !0) : (_(), f("span", Fe, y(b(a)(e.node.size)), 1))
372
+ ], 14, je))), 128))])
362
373
  ], 64))]));
363
374
  }
364
375
  });
365
376
  //#endregion
366
- export { S as default };
377
+ export { w as default };
367
378
 
368
379
  //# sourceMappingURL=SevenZipBrowser.vue_vue_type_script_setup_true_lang.js.map