@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
|
@@ -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 {
|
|
6
|
-
import {
|
|
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
|
|
9
|
+
var pe = { class: "sz" }, me = {
|
|
9
10
|
key: 0,
|
|
10
11
|
class: "sz__msg"
|
|
11
|
-
},
|
|
12
|
+
}, he = {
|
|
12
13
|
key: 1,
|
|
13
14
|
class: "sz__msg sz__msg--error"
|
|
14
|
-
},
|
|
15
|
+
}, ge = {
|
|
15
16
|
key: 2,
|
|
16
17
|
class: "sz__locked"
|
|
17
|
-
},
|
|
18
|
+
}, _e = { class: "sz__lockform" }, ve = {
|
|
18
19
|
key: 0,
|
|
19
20
|
class: "sz__lockerr"
|
|
20
|
-
},
|
|
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
|
-
},
|
|
26
|
+
}, Ce = ["title"], we = {
|
|
26
27
|
key: 0,
|
|
27
28
|
class: "sz__msg"
|
|
28
|
-
},
|
|
29
|
+
}, Te = {
|
|
29
30
|
key: 1,
|
|
30
31
|
class: "sz__msg sz__msg--error"
|
|
31
|
-
},
|
|
32
|
+
}, Ee = { class: "sz__summary" }, De = ["placeholder"], Oe = {
|
|
32
33
|
key: 0,
|
|
33
34
|
class: "sz__pwerr"
|
|
34
|
-
},
|
|
35
|
+
}, ke = { class: "sz__scroll sz__tree" }, Ae = {
|
|
35
36
|
key: 0,
|
|
36
37
|
class: "sz__msg"
|
|
37
|
-
},
|
|
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
|
-
},
|
|
41
|
+
}, w = /*@__PURE__*/ h({
|
|
41
42
|
__name: "SevenZipBrowser",
|
|
42
43
|
props: {
|
|
43
44
|
url: {},
|
|
44
45
|
name: {}
|
|
45
46
|
},
|
|
46
|
-
setup(
|
|
47
|
-
let
|
|
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
|
-
}),
|
|
54
|
-
function
|
|
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
|
|
93
|
+
return Be(t), t;
|
|
93
94
|
}
|
|
94
|
-
function
|
|
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 &&
|
|
97
|
+
for (let t of e.children) t.isDir && Be(t);
|
|
97
98
|
}
|
|
98
|
-
let
|
|
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 &&
|
|
104
|
+
}), i.isDir && L.has(i.path) && t(i.children, r + 1);
|
|
104
105
|
};
|
|
105
|
-
return t(
|
|
106
|
+
return t(I.value.children, 0), e;
|
|
106
107
|
});
|
|
107
|
-
async function
|
|
108
|
-
|
|
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(
|
|
111
|
+
let e = await fetch(T.url);
|
|
111
112
|
if (!e.ok) throw Error(`HTTP ${e.status}`);
|
|
112
|
-
|
|
113
|
+
O = await e.arrayBuffer();
|
|
113
114
|
try {
|
|
114
|
-
|
|
115
|
+
k = ie(O);
|
|
115
116
|
} catch (e) {
|
|
116
|
-
if (e instanceof
|
|
117
|
-
|
|
117
|
+
if (e instanceof ne) {
|
|
118
|
+
N.value = !0, E.value = "ready";
|
|
118
119
|
return;
|
|
119
120
|
}
|
|
120
121
|
throw e;
|
|
121
122
|
}
|
|
122
|
-
|
|
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
|
-
|
|
125
|
+
D.value = e instanceof Error ? e.message : String(e), E.value = "error";
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
|
-
function
|
|
128
|
-
return e instanceof
|
|
128
|
+
function z(e) {
|
|
129
|
+
return e instanceof re ? "密码错误" : e instanceof ne ? "需要密码" : e instanceof Error ? e.message : String(e);
|
|
129
130
|
}
|
|
130
|
-
async function
|
|
131
|
-
if (!(!
|
|
132
|
-
if (
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
+
M.value = z(e);
|
|
138
139
|
} finally {
|
|
139
|
-
|
|
140
|
+
E.value = "ready";
|
|
140
141
|
}
|
|
141
142
|
return;
|
|
142
143
|
}
|
|
143
144
|
try {
|
|
144
|
-
let e =
|
|
145
|
-
e &&
|
|
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
|
-
|
|
148
|
+
P.value = !1, M.value = z(e);
|
|
148
149
|
}
|
|
149
150
|
}
|
|
150
151
|
}
|
|
151
|
-
let
|
|
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
|
-
|
|
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
|
|
160
|
+
function Ke(e) {
|
|
160
161
|
let n = Z(e.path);
|
|
161
|
-
return
|
|
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
|
|
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
|
-
|
|
174
|
-
|
|
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 =
|
|
185
|
+
let t = Ke(e);
|
|
177
186
|
if (t.length > 1) {
|
|
178
|
-
let n =
|
|
179
|
-
|
|
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,
|
|
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
|
-
})),
|
|
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(
|
|
191
|
-
|
|
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:
|
|
203
|
+
url: J.value,
|
|
194
204
|
size: e.size,
|
|
195
|
-
textHint:
|
|
196
|
-
})
|
|
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
|
-
|
|
209
|
+
U.value = z(e), H.value = "error";
|
|
199
210
|
}
|
|
200
211
|
}
|
|
201
|
-
function
|
|
202
|
-
e.isDir ?
|
|
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
|
|
205
|
-
|
|
215
|
+
function Ze() {
|
|
216
|
+
V.value = null, W.value = "", G.value = null, K.value = !1, X(), Y.value = [];
|
|
206
217
|
}
|
|
207
|
-
function
|
|
208
|
-
if (!(!
|
|
209
|
-
let t = URL.createObjectURL(new Blob([s(
|
|
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 =
|
|
214
|
-
async function
|
|
224
|
+
let Q = v(!1), $;
|
|
225
|
+
async function $e() {
|
|
215
226
|
try {
|
|
216
|
-
await navigator.clipboard?.writeText(
|
|
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
|
|
231
|
+
return le(R), fe(() => T.url, R), ce(() => {
|
|
221
232
|
X(), clearTimeout($);
|
|
222
|
-
}), (e, t) => (
|
|
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:
|
|
226
|
-
}, "重试")])) :
|
|
227
|
-
|
|
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] ||=
|
|
233
|
-
|
|
234
|
-
"onUpdate:modelValue": t[0] ||= (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:
|
|
239
|
-
}, null, 544), [[
|
|
249
|
+
onKeyup: C(B, ["enter"])
|
|
250
|
+
}, null, 544), [[x, j.value]]), p("button", {
|
|
240
251
|
type: "button",
|
|
241
252
|
class: "sz__pwbtn",
|
|
242
|
-
onClick:
|
|
253
|
+
onClick: B
|
|
243
254
|
}, "解锁")]),
|
|
244
|
-
|
|
245
|
-
])) :
|
|
246
|
-
|
|
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:
|
|
251
|
-
}, [
|
|
261
|
+
onClick: Ze
|
|
262
|
+
}, [m(o, {
|
|
252
263
|
name: "arrowLeft",
|
|
253
264
|
size: 16
|
|
254
265
|
})]),
|
|
255
|
-
|
|
266
|
+
p("span", {
|
|
256
267
|
class: "sz__path",
|
|
257
|
-
title:
|
|
258
|
-
},
|
|
259
|
-
|
|
260
|
-
|
|
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:
|
|
273
|
+
class: g(["sz__vbtn", { "sz__vbtn--on": q.value === "render" }]),
|
|
263
274
|
title: "渲染",
|
|
264
|
-
onClick: t[1] ||= (e) =>
|
|
265
|
-
}, [
|
|
275
|
+
onClick: t[1] ||= (e) => q.value = "render"
|
|
276
|
+
}, [m(o, {
|
|
266
277
|
name: "eye",
|
|
267
278
|
size: 14
|
|
268
|
-
})], 2),
|
|
279
|
+
})], 2), p("button", {
|
|
269
280
|
type: "button",
|
|
270
|
-
class:
|
|
281
|
+
class: g(["sz__vbtn", { "sz__vbtn--on": q.value === "source" }]),
|
|
271
282
|
title: "源码",
|
|
272
|
-
onClick: t[2] ||= (e) =>
|
|
273
|
-
}, [
|
|
283
|
+
onClick: t[2] ||= (e) => q.value = "source"
|
|
284
|
+
}, [m(o, {
|
|
274
285
|
name: "code",
|
|
275
286
|
size: 14
|
|
276
|
-
})], 2)])) :
|
|
277
|
-
|
|
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:
|
|
283
|
-
}, [
|
|
293
|
+
onClick: $e
|
|
294
|
+
}, [m(o, {
|
|
284
295
|
name: "copy",
|
|
285
296
|
size: 14
|
|
286
|
-
})], 8,
|
|
287
|
-
|
|
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) =>
|
|
292
|
-
}, [
|
|
302
|
+
onClick: t[3] ||= (e) => Qe(V.value)
|
|
303
|
+
}, [m(o, {
|
|
293
304
|
name: "download",
|
|
294
305
|
size: 15
|
|
295
306
|
})])
|
|
296
|
-
]),
|
|
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:
|
|
299
|
-
start:
|
|
300
|
-
name:
|
|
301
|
-
size:
|
|
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
|
-
])) :
|
|
318
|
+
])) : G.value ? (_(), u(b(Ie), {
|
|
308
319
|
key: 3,
|
|
309
|
-
file:
|
|
310
|
-
mode:
|
|
311
|
-
onLoaded: t[4] ||= (e) =>
|
|
312
|
-
}, null, 8, ["file", "mode"])) :
|
|
313
|
-
|
|
314
|
-
|
|
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:
|
|
327
|
+
class: g(["sz__pw", { "sz__pw--ok": P.value }])
|
|
317
328
|
}, [
|
|
318
|
-
|
|
329
|
+
m(o, {
|
|
319
330
|
name: "key",
|
|
320
331
|
size: 14
|
|
321
332
|
}),
|
|
322
|
-
|
|
323
|
-
"onUpdate:modelValue": t[5] ||= (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:
|
|
327
|
-
onKeyup:
|
|
328
|
-
}, null, 40,
|
|
329
|
-
|
|
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:
|
|
333
|
-
},
|
|
334
|
-
|
|
335
|
-
], 2)) :
|
|
336
|
-
|
|
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:
|
|
340
|
-
style:
|
|
341
|
-
onClick: (t) =>
|
|
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
|
-
|
|
354
|
+
p("span", Me, [e.node.isDir ? (_(), u(o, {
|
|
344
355
|
key: 0,
|
|
345
356
|
name: "chevronRight",
|
|
346
357
|
size: 13,
|
|
347
|
-
class:
|
|
348
|
-
}, null, 8, ["class"])) :
|
|
349
|
-
|
|
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
|
-
|
|
354
|
-
!e.node.isDir && e.node.entry?.encrypted ? (
|
|
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
|
-
})) :
|
|
360
|
-
e.node.isDir ?
|
|
361
|
-
], 14,
|
|
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 {
|
|
377
|
+
export { w as default };
|
|
367
378
|
|
|
368
379
|
//# sourceMappingURL=SevenZipBrowser.vue_vue_type_script_setup_true_lang.js.map
|