@l1yp/file-viewer 0.1.1 → 0.1.2

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 (431) hide show
  1. package/README.md +1 -0
  2. package/dist/FileViewer.js +2 -0
  3. package/dist/FileViewer.js.map +1 -0
  4. package/dist/FileViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  5. package/dist/FileViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  6. package/dist/components/AppxViewer.js +2 -0
  7. package/dist/components/AppxViewer.js.map +1 -0
  8. package/dist/components/AppxViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  9. package/dist/components/AppxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  10. package/dist/components/ArchiveBrowser.js +2 -0
  11. package/dist/components/ArchiveBrowser.js.map +1 -0
  12. package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
  13. package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
  14. package/dist/components/AudioViewer.js +2 -0
  15. package/dist/components/AudioViewer.js.map +1 -0
  16. package/dist/components/AudioViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  17. package/dist/components/AudioViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  18. package/dist/components/BinObjBrowser.js +2 -0
  19. package/dist/components/BinObjBrowser.js.map +1 -0
  20. package/dist/components/BinObjBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
  21. package/dist/components/BinObjBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
  22. package/dist/components/CertViewer.js +2 -0
  23. package/dist/components/CertViewer.js.map +1 -0
  24. package/dist/components/CertViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  25. package/dist/components/CertViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  26. package/dist/components/ClassViewer.js +2 -0
  27. package/dist/components/ClassViewer.js.map +1 -0
  28. package/dist/components/ClassViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  29. package/dist/components/ClassViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  30. package/dist/components/CodeBlock.js +2 -0
  31. package/dist/components/CodeBlock.js.map +1 -0
  32. package/dist/components/CodeBlock.vue_vue_type_script_setup_true_lang.js +2 -0
  33. package/dist/components/CodeBlock.vue_vue_type_script_setup_true_lang.js.map +1 -0
  34. package/dist/components/CodeView.js +2 -0
  35. package/dist/components/CodeView.js.map +1 -0
  36. package/dist/components/CodeView.vue_vue_type_script_setup_true_lang.js +2 -0
  37. package/dist/components/CodeView.vue_vue_type_script_setup_true_lang.js.map +1 -0
  38. package/dist/components/CsvViewer.js +2 -0
  39. package/dist/components/CsvViewer.js.map +1 -0
  40. package/dist/components/CsvViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  41. package/dist/components/CsvViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  42. package/dist/components/DicomViewer.js +2 -0
  43. package/dist/components/DicomViewer.js.map +1 -0
  44. package/dist/components/DicomViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  45. package/dist/components/DicomViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  46. package/dist/components/DocxNavigationPane.js +2 -0
  47. package/dist/components/DocxNavigationPane.js.map +1 -0
  48. package/dist/components/DocxNavigationPane.vue_vue_type_script_setup_true_lang.js +2 -0
  49. package/dist/components/DocxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -0
  50. package/dist/components/DocxViewer.js +2 -0
  51. package/dist/components/DocxViewer.js.map +1 -0
  52. package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  53. package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  54. package/dist/components/DrawioViewer.js +2 -0
  55. package/dist/components/DrawioViewer.js.map +1 -0
  56. package/dist/components/DrawioViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  57. package/dist/components/DrawioViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  58. package/dist/components/DxfViewer.js +2 -0
  59. package/dist/components/DxfViewer.js.map +1 -0
  60. package/dist/components/DxfViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  61. package/dist/components/DxfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  62. package/dist/components/EbookViewer.js +2 -0
  63. package/dist/components/EbookViewer.js.map +1 -0
  64. package/dist/components/EbookViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  65. package/dist/components/EbookViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  66. package/dist/components/EmlViewer.js +2 -0
  67. package/dist/components/EmlViewer.js.map +1 -0
  68. package/dist/components/EmlViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  69. package/dist/components/EmlViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  70. package/dist/components/EplViewer.js +2 -0
  71. package/dist/components/EplViewer.js.map +1 -0
  72. package/dist/components/EplViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  73. package/dist/components/EplViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  74. package/dist/components/FileViewerBody.js +2 -0
  75. package/dist/components/FileViewerBody.js.map +1 -0
  76. package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js +2 -0
  77. package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js.map +1 -0
  78. package/dist/components/FileViewerPanel.js +2 -0
  79. package/dist/components/FileViewerPanel.js.map +1 -0
  80. package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js +2 -0
  81. package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js.map +1 -0
  82. package/dist/components/FontViewer.js +2 -0
  83. package/dist/components/FontViewer.js.map +1 -0
  84. package/dist/components/FontViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  85. package/dist/components/FontViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  86. package/dist/components/FrontmatterCard.js +2 -0
  87. package/dist/components/FrontmatterCard.js.map +1 -0
  88. package/dist/components/FrontmatterCard.vue_vue_type_script_setup_true_lang.js +2 -0
  89. package/dist/components/FrontmatterCard.vue_vue_type_script_setup_true_lang.js.map +1 -0
  90. package/dist/components/GeoViewer.js +2 -0
  91. package/dist/components/GeoViewer.js.map +1 -0
  92. package/dist/components/GeoViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  93. package/dist/components/GeoViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  94. package/dist/components/HarViewer.js +2 -0
  95. package/dist/components/HarViewer.js.map +1 -0
  96. package/dist/components/HarViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  97. package/dist/components/HarViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  98. package/dist/components/HexViewer.js +2 -0
  99. package/dist/components/HexViewer.js.map +1 -0
  100. package/dist/components/HexViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  101. package/dist/components/HexViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  102. package/dist/components/HprofViewer.js +2 -0
  103. package/dist/components/HprofViewer.js.map +1 -0
  104. package/dist/components/HprofViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  105. package/dist/components/HprofViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  106. package/dist/components/ImageLightbox.js +2 -0
  107. package/dist/components/ImageLightbox.js.map +1 -0
  108. package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js +2 -0
  109. package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js.map +1 -0
  110. package/dist/components/InlineText.js +2 -0
  111. package/dist/components/InlineText.js.map +1 -0
  112. package/dist/components/JksViewer.js +2 -0
  113. package/dist/components/JksViewer.js.map +1 -0
  114. package/dist/components/JksViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  115. package/dist/components/JksViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  116. package/dist/components/JsonlViewer.js +2 -0
  117. package/dist/components/JsonlViewer.js.map +1 -0
  118. package/dist/components/JsonlViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  119. package/dist/components/JsonlViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  120. package/dist/components/LogViewer.js +2 -0
  121. package/dist/components/LogViewer.js.map +1 -0
  122. package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  123. package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  124. package/dist/components/LrcViewer.js +2 -0
  125. package/dist/components/LrcViewer.js.map +1 -0
  126. package/dist/components/LrcViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  127. package/dist/components/LrcViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  128. package/dist/components/MarkdownTable.js +2 -0
  129. package/dist/components/MarkdownTable.js.map +1 -0
  130. package/dist/components/MarkdownTable.vue_vue_type_script_setup_true_lang.js +2 -0
  131. package/dist/components/MarkdownTable.vue_vue_type_script_setup_true_lang.js.map +1 -0
  132. package/dist/components/MarkdownText.js +2 -0
  133. package/dist/components/MarkdownText.js.map +1 -0
  134. package/dist/components/MhtViewer.js +2 -0
  135. package/dist/components/MhtViewer.js.map +1 -0
  136. package/dist/components/MhtViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  137. package/dist/components/MhtViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  138. package/dist/components/MindMapView.js +2 -0
  139. package/dist/components/MindMapView.js.map +1 -0
  140. package/dist/components/MindMapView.vue_vue_type_script_setup_true_lang.js +2 -0
  141. package/dist/components/MindMapView.vue_vue_type_script_setup_true_lang.js.map +1 -0
  142. package/dist/components/MissingDependency.js +2 -0
  143. package/dist/components/MissingDependency.js.map +1 -0
  144. package/dist/components/MissingDependency.vue_vue_type_script_setup_true_lang.js +2 -0
  145. package/dist/components/MissingDependency.vue_vue_type_script_setup_true_lang.js.map +1 -0
  146. package/dist/components/ModelViewer.js +2 -0
  147. package/dist/components/ModelViewer.js.map +1 -0
  148. package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  149. package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  150. package/dist/components/MsiViewer.js +2 -0
  151. package/dist/components/MsiViewer.js.map +1 -0
  152. package/dist/components/MsiViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  153. package/dist/components/MsiViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  154. package/dist/components/NotebookViewer.js +2 -0
  155. package/dist/components/NotebookViewer.js.map +1 -0
  156. package/dist/components/NotebookViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  157. package/dist/components/NotebookViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  158. package/dist/components/PdfViewer.js +2 -0
  159. package/dist/components/PdfViewer.js.map +1 -0
  160. package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  161. package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  162. package/dist/components/PptxNavigationPane.js +2 -0
  163. package/dist/components/PptxNavigationPane.js.map +1 -0
  164. package/dist/components/PptxNavigationPane.vue_vue_type_script_setup_true_lang.js +2 -0
  165. package/dist/components/PptxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -0
  166. package/dist/components/PptxViewer.js +3 -1
  167. package/dist/components/PptxViewer.js.map +1 -0
  168. package/dist/components/PptxViewer.vue_vue_type_script_setup_true_lang.js +55 -49
  169. package/dist/components/PptxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  170. package/dist/components/PsdViewer.js +2 -0
  171. package/dist/components/PsdViewer.js.map +1 -0
  172. package/dist/components/PsdViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  173. package/dist/components/PsdViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  174. package/dist/components/QmcAudioViewer.js +2 -0
  175. package/dist/components/QmcAudioViewer.js.map +1 -0
  176. package/dist/components/QmcAudioViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  177. package/dist/components/QmcAudioViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  178. package/dist/components/RasterViewer.js +2 -0
  179. package/dist/components/RasterViewer.js.map +1 -0
  180. package/dist/components/RasterViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  181. package/dist/components/RasterViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  182. package/dist/components/RtfViewer.js +2 -0
  183. package/dist/components/RtfViewer.js.map +1 -0
  184. package/dist/components/RtfViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  185. package/dist/components/RtfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  186. package/dist/components/SevenZipBrowser.js +2 -0
  187. package/dist/components/SevenZipBrowser.js.map +1 -0
  188. package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
  189. package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
  190. package/dist/components/SqliteViewer.js +2 -0
  191. package/dist/components/SqliteViewer.js.map +1 -0
  192. package/dist/components/SqliteViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  193. package/dist/components/SqliteViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  194. package/dist/components/SubtitleViewer.js +2 -0
  195. package/dist/components/SubtitleViewer.js.map +1 -0
  196. package/dist/components/SubtitleViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  197. package/dist/components/SubtitleViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  198. package/dist/components/XlsxChart.js +2 -0
  199. package/dist/components/XlsxChart.js.map +1 -0
  200. package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js +2 -0
  201. package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js.map +1 -0
  202. package/dist/components/XlsxViewer.js +2 -0
  203. package/dist/components/XlsxViewer.js.map +1 -0
  204. package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  205. package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  206. package/dist/components/XmindViewer.js +2 -0
  207. package/dist/components/XmindViewer.js.map +1 -0
  208. package/dist/components/XmindViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  209. package/dist/components/XmindViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  210. package/dist/components/ZipBrowser.js +2 -0
  211. package/dist/components/ZipBrowser.js.map +1 -0
  212. package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
  213. package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
  214. package/dist/components/ZoomImage.js +2 -0
  215. package/dist/components/ZoomImage.js.map +1 -0
  216. package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js +2 -0
  217. package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js.map +1 -0
  218. package/dist/components/epl/EplCodeBlock.js +2 -0
  219. package/dist/components/epl/EplCodeBlock.js.map +1 -0
  220. package/dist/components/epl/EplCodeBlock.vue_vue_type_script_setup_true_lang.js +2 -0
  221. package/dist/components/epl/EplCodeBlock.vue_vue_type_script_setup_true_lang.js.map +1 -0
  222. package/dist/components/epl/EplDoc.js +2 -0
  223. package/dist/components/epl/EplDoc.js.map +1 -0
  224. package/dist/components/epl/EplDoc.vue_vue_type_script_setup_true_lang.js +2 -0
  225. package/dist/components/epl/EplDoc.vue_vue_type_script_setup_true_lang.js.map +1 -0
  226. package/dist/components/epl/EplMethod.js +2 -0
  227. package/dist/components/epl/EplMethod.js.map +1 -0
  228. package/dist/components/epl/EplMethod.vue_vue_type_script_setup_true_lang.js +2 -0
  229. package/dist/components/epl/EplMethod.vue_vue_type_script_setup_true_lang.js.map +1 -0
  230. package/dist/components/epl/EplResource.js +2 -0
  231. package/dist/components/epl/EplResource.js.map +1 -0
  232. package/dist/components/epl/EplResource.vue_vue_type_script_setup_true_lang.js +2 -0
  233. package/dist/components/epl/EplResource.vue_vue_type_script_setup_true_lang.js.map +1 -0
  234. package/dist/components/epl/EplTree.js +2 -0
  235. package/dist/components/epl/EplTree.js.map +1 -0
  236. package/dist/components/epl/EplTree.vue_vue_type_script_setup_true_lang.js +2 -0
  237. package/dist/components/epl/EplTree.vue_vue_type_script_setup_true_lang.js.map +1 -0
  238. package/dist/components/epl/EplVarTable.js +2 -0
  239. package/dist/components/epl/EplVarTable.js.map +1 -0
  240. package/dist/components/epl/EplVarTable.vue_vue_type_script_setup_true_lang.js +2 -0
  241. package/dist/components/epl/EplVarTable.vue_vue_type_script_setup_true_lang.js.map +1 -0
  242. package/dist/components/epl/numbering.js +2 -0
  243. package/dist/components/epl/numbering.js.map +1 -0
  244. package/dist/components/icons/Icon.js +2 -0
  245. package/dist/components/icons/Icon.js.map +1 -0
  246. package/dist/components/icons/Icon.vue_vue_type_script_setup_true_lang.js +2 -0
  247. package/dist/components/icons/Icon.vue_vue_type_script_setup_true_lang.js.map +1 -0
  248. package/dist/components/icons/iconPaths.js +2 -0
  249. package/dist/components/icons/iconPaths.js.map +1 -0
  250. package/dist/components/optionalViewer.js +2 -0
  251. package/dist/components/optionalViewer.js.map +1 -0
  252. package/dist/components/ui/ClampBlock.js +2 -0
  253. package/dist/components/ui/ClampBlock.js.map +1 -0
  254. package/dist/composables/useElementNavigation.js +2 -0
  255. package/dist/composables/useElementNavigation.js.map +1 -0
  256. package/dist/composables/useFileViewer.js +2 -0
  257. package/dist/composables/useFileViewer.js.map +1 -0
  258. package/dist/composables/useLightbox.js +2 -0
  259. package/dist/composables/useLightbox.js.map +1 -0
  260. package/dist/composables/useLogRules.js +2 -0
  261. package/dist/composables/useLogRules.js.map +1 -0
  262. package/dist/lib/aes.js +2 -0
  263. package/dist/lib/aes.js.map +1 -0
  264. package/dist/lib/appx.js +2 -0
  265. package/dist/lib/appx.js.map +1 -0
  266. package/dist/lib/archiveHtml.js +2 -0
  267. package/dist/lib/archiveHtml.js.map +1 -0
  268. package/dist/lib/asn1.js +2 -0
  269. package/dist/lib/asn1.js.map +1 -0
  270. package/dist/lib/binobj.js +2 -0
  271. package/dist/lib/binobj.js.map +1 -0
  272. package/dist/lib/brotli.js +2 -0
  273. package/dist/lib/brotli.js.map +1 -0
  274. package/dist/lib/bzip2.js +2 -0
  275. package/dist/lib/bzip2.js.map +1 -0
  276. package/dist/lib/classfile.js +2 -0
  277. package/dist/lib/classfile.js.map +1 -0
  278. package/dist/lib/codePreview.js +2 -0
  279. package/dist/lib/codePreview.js.map +1 -0
  280. package/dist/lib/codefold.js +2 -0
  281. package/dist/lib/codefold.js.map +1 -0
  282. package/dist/lib/codeformat.js +2 -0
  283. package/dist/lib/codeformat.js.map +1 -0
  284. package/dist/lib/csv.js +2 -0
  285. package/dist/lib/csv.js.map +1 -0
  286. package/dist/lib/des.js +2 -0
  287. package/dist/lib/des.js.map +1 -0
  288. package/dist/lib/diagram.js +2 -0
  289. package/dist/lib/diagram.js.map +1 -0
  290. package/dist/lib/dicom.js +2 -0
  291. package/dist/lib/dicom.js.map +1 -0
  292. package/dist/lib/dicomCodecs.js +2 -0
  293. package/dist/lib/dicomCodecs.js.map +1 -0
  294. package/dist/lib/dicomRender.js +2 -0
  295. package/dist/lib/dicomRender.js.map +1 -0
  296. package/dist/lib/docxNavigation.js +2 -0
  297. package/dist/lib/docxNavigation.js.map +1 -0
  298. package/dist/lib/dxf.js +2 -0
  299. package/dist/lib/dxf.js.map +1 -0
  300. package/dist/lib/eml.js +2 -0
  301. package/dist/lib/eml.js.map +1 -0
  302. package/dist/lib/encoding.js +2 -0
  303. package/dist/lib/encoding.js.map +1 -0
  304. package/dist/lib/epl/code/codeDataParser.js +2 -0
  305. package/dist/lib/epl/code/codeDataParser.js.map +1 -0
  306. package/dist/lib/epl/code/codeLines.js +2 -0
  307. package/dist/lib/epl/code/codeLines.js.map +1 -0
  308. package/dist/lib/epl/code/textCode.js +2 -0
  309. package/dist/lib/epl/code/textCode.js.map +1 -0
  310. package/dist/lib/epl/eplDocument.js +2 -0
  311. package/dist/lib/epl/eplDocument.js.map +1 -0
  312. package/dist/lib/epl/imageList.js +2 -0
  313. package/dist/lib/epl/imageList.js.map +1 -0
  314. package/dist/lib/epl/index.js +2 -0
  315. package/dist/lib/epl/index.js.map +1 -0
  316. package/dist/lib/epl/io/binaryReader.js +2 -0
  317. package/dist/lib/epl/io/binaryReader.js.map +1 -0
  318. package/dist/lib/epl/io/encoding.js +2 -0
  319. package/dist/lib/epl/io/encoding.js.map +1 -0
  320. package/dist/lib/epl/model/eplSystemId.js +2 -0
  321. package/dist/lib/epl/model/eplSystemId.js.map +1 -0
  322. package/dist/lib/epl/names/eplLibData.js +2 -0
  323. package/dist/lib/epl/names/eplLibData.js.map +1 -0
  324. package/dist/lib/epl/names/idToNameMap.js +2 -0
  325. package/dist/lib/epl/names/idToNameMap.js.map +1 -0
  326. package/dist/lib/epl/names/libNameProvider.js +2 -0
  327. package/dist/lib/epl/names/libNameProvider.js.map +1 -0
  328. package/dist/lib/epl/program.js +2 -0
  329. package/dist/lib/epl/program.js.map +1 -0
  330. package/dist/lib/epl/projectFileReader.js +2 -0
  331. package/dist/lib/epl/projectFileReader.js.map +1 -0
  332. package/dist/lib/fb2.js +2 -0
  333. package/dist/lib/fb2.js.map +1 -0
  334. package/dist/lib/fileType.js +2 -0
  335. package/dist/lib/fileType.js.map +1 -0
  336. package/dist/lib/fonts.js +2 -0
  337. package/dist/lib/fonts.js.map +1 -0
  338. package/dist/lib/format.js +2 -0
  339. package/dist/lib/format.js.map +1 -0
  340. package/dist/lib/frontmatter.js +2 -0
  341. package/dist/lib/frontmatter.js.map +1 -0
  342. package/dist/lib/geo.js +2 -0
  343. package/dist/lib/geo.js.map +1 -0
  344. package/dist/lib/har.js +2 -0
  345. package/dist/lib/har.js.map +1 -0
  346. package/dist/lib/highlight.js +2 -0
  347. package/dist/lib/highlight.js.map +1 -0
  348. package/dist/lib/highlightLangs.js +2 -0
  349. package/dist/lib/highlightLangs.js.map +1 -0
  350. package/dist/lib/hprof.js +2 -0
  351. package/dist/lib/hprof.js.map +1 -0
  352. package/dist/lib/ipynb.js +2 -0
  353. package/dist/lib/ipynb.js.map +1 -0
  354. package/dist/lib/jks.js +2 -0
  355. package/dist/lib/jks.js.map +1 -0
  356. package/dist/lib/jsonl.js +2 -0
  357. package/dist/lib/jsonl.js.map +1 -0
  358. package/dist/lib/lrc.js +2 -0
  359. package/dist/lib/lrc.js.map +1 -0
  360. package/dist/lib/lzma.js +2 -0
  361. package/dist/lib/lzma.js.map +1 -0
  362. package/dist/lib/markdown/blocks.js +2 -0
  363. package/dist/lib/markdown/blocks.js.map +1 -0
  364. package/dist/lib/markdown/inline.js +2 -0
  365. package/dist/lib/markdown/inline.js.map +1 -0
  366. package/dist/lib/markdown/math.js +2 -0
  367. package/dist/lib/markdown/math.js.map +1 -0
  368. package/dist/lib/markdown/segmentize.js +2 -0
  369. package/dist/lib/markdown/segmentize.js.map +1 -0
  370. package/dist/lib/markdown/styles.js +2 -0
  371. package/dist/lib/markdown/styles.js.map +1 -0
  372. package/dist/lib/mht.js +2 -0
  373. package/dist/lib/mht.js.map +1 -0
  374. package/dist/lib/midi.js +2 -0
  375. package/dist/lib/midi.js.map +1 -0
  376. package/dist/lib/mime.js +2 -0
  377. package/dist/lib/mime.js.map +1 -0
  378. package/dist/lib/mindmap.js +2 -0
  379. package/dist/lib/mindmap.js.map +1 -0
  380. package/dist/lib/mobi.js +2 -0
  381. package/dist/lib/mobi.js.map +1 -0
  382. package/dist/lib/msi.js +2 -0
  383. package/dist/lib/msi.js.map +1 -0
  384. package/dist/lib/optionalDep.js +2 -0
  385. package/dist/lib/optionalDep.js.map +1 -0
  386. package/dist/lib/pkcs12.js +2 -0
  387. package/dist/lib/pkcs12.js.map +1 -0
  388. package/dist/lib/pptxNavigation.js +2 -0
  389. package/dist/lib/pptxNavigation.js.map +1 -0
  390. package/dist/lib/qmc.js +2 -0
  391. package/dist/lib/qmc.js.map +1 -0
  392. package/dist/lib/rar.js +2 -0
  393. package/dist/lib/rar.js.map +1 -0
  394. package/dist/lib/rawPreview.js +2 -0
  395. package/dist/lib/rawPreview.js.map +1 -0
  396. package/dist/lib/rc2.js +2 -0
  397. package/dist/lib/rc2.js.map +1 -0
  398. package/dist/lib/resolveInput.js +2 -0
  399. package/dist/lib/resolveInput.js.map +1 -0
  400. package/dist/lib/rtf.js +2 -0
  401. package/dist/lib/rtf.js.map +1 -0
  402. package/dist/lib/sevenZip.js +2 -0
  403. package/dist/lib/sevenZip.js.map +1 -0
  404. package/dist/lib/sha1.js +2 -0
  405. package/dist/lib/sha1.js.map +1 -0
  406. package/dist/lib/sha256.js +2 -0
  407. package/dist/lib/sha256.js.map +1 -0
  408. package/dist/lib/speechAudio.js +2 -0
  409. package/dist/lib/speechAudio.js.map +1 -0
  410. package/dist/lib/sqlite.js +2 -0
  411. package/dist/lib/sqlite.js.map +1 -0
  412. package/dist/lib/subtitle.js +2 -0
  413. package/dist/lib/subtitle.js.map +1 -0
  414. package/dist/lib/tarball.js +2 -0
  415. package/dist/lib/tarball.js.map +1 -0
  416. package/dist/lib/viewerFormats.js +2 -0
  417. package/dist/lib/viewerFormats.js.map +1 -0
  418. package/dist/lib/viewerKind.js +2 -0
  419. package/dist/lib/viewerKind.js.map +1 -0
  420. package/dist/lib/x509.js +2 -0
  421. package/dist/lib/x509.js.map +1 -0
  422. package/dist/lib/xlsx.js +2 -0
  423. package/dist/lib/xlsx.js.map +1 -0
  424. package/dist/lib/xlsxCharts.js +2 -0
  425. package/dist/lib/xlsxCharts.js.map +1 -0
  426. package/dist/lib/xmind.js +2 -0
  427. package/dist/lib/xmind.js.map +1 -0
  428. package/dist/lib/zip.js +2 -0
  429. package/dist/lib/zip.js.map +1 -0
  430. package/dist/style.css +1 -1
  431. package/package.json +1 -1
@@ -1,17 +1,17 @@
1
1
  import e from "./PptxNavigationPane.js";
2
2
  import { buildPptxNavigation as t } from "../lib/pptxNavigation.js";
3
- import { createBlock as n, createCommentVNode as r, createElementBlock as i, createElementVNode as a, defineComponent as o, onBeforeUnmount as s, onMounted as c, openBlock as l, shallowRef as u, toDisplayString as d, watch as f } from "vue";
4
- import p from "pdfjs-dist/build/pdf.worker.min.mjs?url";
5
- import { PptxViewer as m, RECOMMENDED_ZIP_LIMITS as h } from "@aiden0z/pptx-renderer";
6
- import g from "pdfjs-dist/build/pdf.min.mjs?url";
3
+ import { createBlock as n, createCommentVNode as r, createElementBlock as i, createElementVNode as a, defineComponent as o, onBeforeUnmount as s, onMounted as c, openBlock as l, shallowRef as u, toDisplayString as d, useTemplateRef as f, watch as p } from "vue";
4
+ import m from "pdfjs-dist/build/pdf.worker.min.mjs?url";
5
+ import { PptxViewer as h, RECOMMENDED_ZIP_LIMITS as g } from "@aiden0z/pptx-renderer";
6
+ import _ from "pdfjs-dist/build/pdf.min.mjs?url";
7
7
  //#region src/components/PptxViewer.vue?vue&type=script&setup=true&lang.ts
8
- var _ = { class: "pv" }, v = { class: "pv__stage" }, y = {
8
+ var v = { class: "pv" }, y = { class: "pv__stage" }, b = {
9
9
  key: 0,
10
10
  class: "pv__overlay"
11
- }, b = {
11
+ }, x = {
12
12
  key: 1,
13
13
  class: "pv__overlay"
14
- }, x = { class: "pv__msg pv__msg--error" }, S = /*@__PURE__*/ o({
14
+ }, S = { class: "pv__msg pv__msg--error" }, C = /*@__PURE__*/ o({
15
15
  __name: "PptxViewer",
16
16
  props: {
17
17
  url: {},
@@ -19,81 +19,87 @@ var _ = { class: "pv" }, v = { class: "pv__stage" }, y = {
19
19
  size: {}
20
20
  },
21
21
  setup(o) {
22
- let S = o, C = u("loading"), w = u(""), T = u(null), E = u(null), D = u([]), O = u(0), k = u(!1), A = null, j = null, M;
23
- function N() {
24
- if (j = null, M && clearTimeout(M), M = void 0, E.value = null, D.value = [], A) {
22
+ let C = o, w = u("loading"), T = u(""), E = f("scrollEl"), D = f("docEl"), O = u(null), k = u([]), A = u(0), j = u(!1), M = null, N = null, P;
23
+ function F() {
24
+ if (N = null, P && clearTimeout(P), P = void 0, O.value = null, k.value = [], M) {
25
25
  try {
26
- A.destroy();
26
+ M.destroy();
27
27
  } catch {}
28
- A = null;
28
+ M = null;
29
29
  }
30
30
  }
31
- async function P() {
32
- let e = T.value;
33
- if (!S.url || !e) return;
34
- C.value = "loading", w.value = "", N(), e.innerHTML = "";
35
- let n = S.url;
31
+ async function I() {
32
+ let e = D.value, n = E.value;
33
+ if (!C.url || !e || !n) return;
34
+ w.value = "loading", T.value = "", F(), e.innerHTML = "";
35
+ let r = C.url;
36
36
  try {
37
- let r = await fetch(n);
38
- if (!r.ok) throw Error(`HTTP ${r.status}`);
39
- let i = await r.arrayBuffer();
40
- if (S.url !== n || T.value !== e) return;
41
- let a = await m.open(i, e, {
42
- zipLimits: h,
43
- scrollContainer: e,
37
+ let i = await fetch(r);
38
+ if (!i.ok) throw Error(`HTTP ${i.status}`);
39
+ let a = await i.arrayBuffer();
40
+ if (C.url !== r || D.value !== e || E.value !== n) return;
41
+ let o = await h.open(a, e, {
42
+ zipLimits: g,
43
+ scrollContainer: n,
44
44
  listOptions: { windowed: !0 },
45
45
  pdfjs: {
46
- moduleUrl: g,
47
- workerUrl: p
46
+ moduleUrl: _,
47
+ workerUrl: m
48
48
  },
49
49
  onSlideChange: (e) => {
50
- j !== null && e !== j || S.url === n && (O.value = e);
50
+ N !== null && e !== N || C.url === r && (A.value = e);
51
51
  }
52
52
  });
53
- if (S.url !== n || T.value !== e) {
53
+ if (C.url !== r || D.value !== e || E.value !== n) {
54
54
  try {
55
- a.destroy();
55
+ o.destroy();
56
56
  } catch {}
57
57
  return;
58
58
  }
59
- A = a, E.value = a, D.value = a.presentationData ? t(a.presentationData) : [], O.value = a.currentSlideIndex, C.value = "ready";
59
+ M = o, O.value = o, k.value = o.presentationData ? t(o.presentationData) : [], A.value = o.currentSlideIndex, w.value = "ready";
60
60
  } catch (e) {
61
- w.value = e instanceof Error ? e.message : String(e), C.value = "error";
61
+ T.value = e instanceof Error ? e.message : String(e), w.value = "error";
62
62
  }
63
63
  }
64
- function F(e) {
65
- j = e, O.value = e, M && clearTimeout(M), M = setTimeout(() => {
66
- j = null, M = void 0;
67
- }, 1e3), A?.goToSlide(e, {
64
+ function L(e) {
65
+ N = e, A.value = e, P && clearTimeout(P), P = setTimeout(() => {
66
+ N = null, P = void 0;
67
+ }, 1e3), M?.goToSlide(e, {
68
68
  behavior: "smooth",
69
69
  block: "start"
70
70
  });
71
71
  }
72
- return c(P), f(() => S.url, P), s(() => {
73
- N(), T.value && (T.value.innerHTML = "");
74
- }), (t, o) => (l(), i("div", _, [C.value === "ready" && E.value ? (l(), n(e, {
72
+ return c(I), p(() => C.url, I), s(() => {
73
+ F(), D.value && (D.value.innerHTML = "");
74
+ }), (t, o) => (l(), i("div", v, [w.value === "ready" && O.value ? (l(), n(e, {
75
75
  key: 0,
76
- collapsed: k.value,
77
- "onUpdate:collapsed": o[0] ||= (e) => k.value = e,
78
- viewer: E.value,
79
- items: D.value,
80
- "active-index": O.value,
81
- onNavigate: F
76
+ collapsed: j.value,
77
+ "onUpdate:collapsed": o[0] ||= (e) => j.value = e,
78
+ viewer: O.value,
79
+ items: k.value,
80
+ "active-index": A.value,
81
+ onNavigate: L
82
82
  }, null, 8, [
83
83
  "collapsed",
84
84
  "viewer",
85
85
  "items",
86
86
  "active-index"
87
- ])) : r("", !0), a("div", v, [a("div", {
87
+ ])) : r("", !0), a("div", y, [a("div", {
88
+ ref_key: "scrollEl",
89
+ ref: E,
90
+ class: "pv__scroll"
91
+ }, [a("div", {
88
92
  ref_key: "docEl",
89
- ref: T,
93
+ ref: D,
90
94
  class: "pv__doc"
91
- }, null, 512), C.value === "loading" ? (l(), i("div", y, [...o[1] ||= [a("p", { class: "pv__msg" }, "正在渲染 PPT…", -1)]])) : C.value === "error" ? (l(), i("div", b, [a("div", x, [a("p", null, "渲染失败:" + d(w.value), 1), a("button", {
95
+ }, null, 512)], 512), w.value === "loading" ? (l(), i("div", b, [...o[1] ||= [a("p", { class: "pv__msg" }, "正在渲染 PPT…", -1)]])) : w.value === "error" ? (l(), i("div", x, [a("div", S, [a("p", null, "渲染失败:" + d(T.value), 1), a("button", {
92
96
  type: "button",
93
97
  class: "pv__retry",
94
- onClick: P
98
+ onClick: I
95
99
  }, "重试")])])) : r("", !0)])]));
96
100
  }
97
101
  });
98
102
  //#endregion
99
- export { S as default };
103
+ export { C as default };
104
+
105
+ //# sourceMappingURL=PptxViewer.vue_vue_type_script_setup_true_lang.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PptxViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/PptxViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, shallowRef, useTemplateRef, watch } from 'vue'\nimport { PptxViewer, RECOMMENDED_ZIP_LIMITS } from '@aiden0z/pptx-renderer'\nimport PptxNavigationPane from '@/components/PptxNavigationPane.vue'\nimport { buildPptxNavigation, type PptxNavigationItem } from '@/lib/pptxNavigation'\n// pdfjs is only used as a fallback to rasterize SmartArt / EMF graphics the native renderer can't draw. The\n// library dynamic-imports these two ESM files inside its OWN web worker, so we hand it servable URLs (Vite's\n// `?url` emits the real assets and returns their URL — root-relative in the same-origin prod build, a dev\n// URL under `pnpm dev`). They're fetched lazily by that worker only when a slide actually needs the fallback.\nimport pdfModuleUrl from 'pdfjs-dist/build/pdf.min.mjs?url'\nimport pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url'\n\n/**\n * Side-viewer PowerPoint (`.pptx`/`.pptm`/`.ppsx`) renderer. Fetches the deck bytes from the download URL and\n * renders the OOXML PresentationML to HTML/SVG entirely in-browser via @aiden0z/pptx-renderer\n * (https://github.com/aiden0z/pptx-renderer) — a vertically scrollable list of slides, each scaled to fit the\n * pane (`fitMode: 'contain'`, the library default). No CDN / network beyond the file fetch (+ the lazy pdfjs\n * fallback above): the renderer bundles its own jszip + echarts (charts) and inlines media. The library writes\n * inline styles only, so nothing leaks onto the page.\n *\n * Two entry shapes mirror the other viewers (docx/xlsx/zip): a real download URL for a held attachment, or a\n * blob URL minted by the archive browsers from already-extracted bytes.\n */\nconst props = defineProps<{\n /** Download/blob URL of the .pptx bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = shallowRef<Status>('loading')\nconst errorMsg = shallowRef('')\n// Keep the scroll viewport separate from the renderer mount: `fitMode: 'contain'` measures the mount's\n// `clientWidth`, so putting visual padding on that same element makes the slide consume the padded width and\n// overflow horizontally. Both stay mounted while loading so `open()` always sees real dimensions.\nconst scrollEl = useTemplateRef<HTMLElement>('scrollEl')\nconst docEl = useTemplateRef<HTMLElement>('docEl')\nconst navViewer = shallowRef<PptxViewer | null>(null)\nconst navItems = shallowRef<PptxNavigationItem[]>([])\nconst activeSlide = shallowRef(0)\nconst navCollapsed = shallowRef(false)\n// Live viewer instance — destroyed on re-render / unmount (it holds echarts instances, observers, and its\n// pdfjs worker, none of which GC on their own).\nlet viewer: PptxViewer | null = null\nlet navigationLock: number | null = null\nlet navigationLockTimer: ReturnType<typeof setTimeout> | undefined\n\nfunction disposeViewer(): void {\n navigationLock = null\n if (navigationLockTimer) clearTimeout(navigationLockTimer)\n navigationLockTimer = undefined\n navViewer.value = null\n navItems.value = []\n if (viewer) {\n try {\n viewer.destroy()\n } catch {\n /* already torn down — ignore */\n }\n viewer = null\n }\n}\n\nasync function render(): Promise<void> {\n const el = docEl.value\n const scroll = scrollEl.value\n if (!props.url || !el || !scroll) return\n status.value = 'loading'\n errorMsg.value = ''\n disposeViewer()\n el.innerHTML = '' // drop any prior render (e.g. on retry / switching files)\n const url = props.url\n try {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n // Stale guard: a newer render may have started while we awaited the fetch.\n if (props.url !== url || docEl.value !== el || scrollEl.value !== scroll) return\n const v = await PptxViewer.open(buf, el, {\n zipLimits: RECOMMENDED_ZIP_LIMITS,\n scrollContainer: scroll, // the outer viewport drives which slides the windowed list mounts\n listOptions: { windowed: true }, // virtualize: only mount slides near the viewport (large decks stay fast)\n pdfjs: { moduleUrl: pdfModuleUrl, workerUrl: pdfWorkerUrl },\n onSlideChange: (index) => {\n if (navigationLock !== null && index !== navigationLock) return\n if (props.url === url) activeSlide.value = index\n },\n })\n // Stale guard again: the file may have changed while parse/render ran — tear the new viewer down.\n if (props.url !== url || docEl.value !== el || scrollEl.value !== scroll) {\n try {\n v.destroy()\n } catch {\n /* ignore */\n }\n return\n }\n viewer = v\n navViewer.value = v\n navItems.value = v.presentationData ? buildPptxNavigation(v.presentationData) : []\n activeSlide.value = v.currentSlideIndex\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 navigateToSlide(index: number): void {\n navigationLock = index\n activeSlide.value = index\n if (navigationLockTimer) clearTimeout(navigationLockTimer)\n navigationLockTimer = setTimeout(() => {\n navigationLock = null\n navigationLockTimer = undefined\n }, 1000)\n void viewer?.goToSlide(index, { behavior: 'smooth', block: 'start' })\n}\n\nonMounted(render)\nwatch(() => props.url, render)\nonBeforeUnmount(() => {\n disposeViewer()\n if (docEl.value) docEl.value.innerHTML = ''\n})\n</script>\n\n<template>\n <div class=\"pv\">\n <PptxNavigationPane\n v-if=\"status === 'ready' && navViewer\"\n v-model:collapsed=\"navCollapsed\"\n :viewer=\"navViewer\"\n :items=\"navItems\"\n :active-index=\"activeSlide\"\n @navigate=\"navigateToSlide\"\n />\n <div class=\"pv__stage\">\n <!-- The padded scroll viewport wraps a padding-free mount whose clientWidth is the real slide width. -->\n <div ref=\"scrollEl\" class=\"pv__scroll\">\n <div ref=\"docEl\" class=\"pv__doc\" />\n </div>\n <!-- Loading / error as an overlay so the container underneath keeps its dimensions. -->\n <div v-if=\"status === 'loading'\" class=\"pv__overlay\">\n <p class=\"pv__msg\">正在渲染 PPT…</p>\n </div>\n <div v-else-if=\"status === 'error'\" class=\"pv__overlay\">\n <div class=\"pv__msg pv__msg--error\">\n <p>渲染失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"pv__retry\" @click=\"render\">重试</button>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.pv {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: row;\n background: var(--surface);\n container-type: inline-size;\n}\n.pv__stage {\n position: relative;\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n/* Scroll region holding the rendered slides. The deck keeps its native appearance (the library scales each\n * slide to fit this width); we only tint the surrounding \"desk\" to the app surface so it sits naturally in\n * both light and dark themes. The renderer writes inline styles, so no :deep overrides are needed. */\n.pv__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface-2);\n padding: 16px 14px 40px;\n}\n.pv__doc {\n width: 100%;\n min-width: 0;\n}\n/* Overlay covers the (empty) container while loading or on error, without unmounting it. */\n.pv__overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: flex-start;\n justify-content: flex-start;\n background: var(--surface);\n}\n.pv__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.pv__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.pv__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.pv__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n</style>\n"],"mappings":"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"}
@@ -5,3 +5,5 @@ import t from "./PsdViewer.vue_vue_type_script_setup_true_lang.js";
5
5
  var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-5fbd3b9c"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
+
9
+ //# sourceMappingURL=PsdViewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PsdViewer.js","names":[],"sources":["../../src/components/PsdViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, ref, shallowRef, watch } from 'vue'\nimport type { Layer, Psd } from 'ag-psd'\nimport { formatBytes } from '@/lib/format'\nimport { isMissingModule } from '@/lib/optionalDep'\nimport Icon from '@/components/icons/Icon.vue'\nimport MissingDependency from '@/components/MissingDependency.vue'\n\n/**\n * Side-viewer for Photoshop documents (.psd/.psb). Fetches the bytes and parses them in-browser with ag-psd\n * (dynamically imported), then — unlike the generic RasterViewer which only shows the flattened composite —\n * exposes the LAYER TREE: a panel of every layer/group with a thumbnail and a visibility toggle, and it\n * RE-COMPOSITES live onto a canvas as you toggle layers (honouring per-layer opacity and blend mode; group\n * visibility cascades to its children). A flattened PSD (no layers) falls back to ag-psd's composite.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\n// Set when the PSD engine (ag-psd, an optional peer) isn't installed → show the install card, not a raw error.\nconst missingDeps = ref<string[] | null>(null)\nconst fit = ref(true)\n\n// Reading a PSD decodes EVERY layer's pixels (needed for the layer panel), so gate very large files behind a\n// confirm the way RasterViewer does — a huge multi-layer PSD would otherwise lock the tab while decoding.\nconst SIZE_LIMIT = 60 * 1024 * 1024\n\ninterface PsdNode {\n id: number\n name: string\n depth: number\n isGroup: boolean\n opacity: number\n blend: string\n left: number\n top: number\n canvas?: HTMLCanvasElement // leaf pixels\n thumb?: string // leaf thumbnail data URL\n parentId: number\n children: PsdNode[]\n}\n\nlet width = 0\nlet height = 0\nconst drawOrder = shallowRef<PsdNode[]>([]) // leaves, bottom → top (compositing order)\nconst panelRows = shallowRef<PsdNode[]>([]) // groups + leaves, top → bottom (Photoshop panel order)\nconst nodeById = new Map<number, PsdNode>()\nconst hidden = ref(new Set<number>()) // ids the user (or the file) has hidden\nlet fallbackCanvas: HTMLCanvasElement | undefined // flattened composite, when there are no layers\nconst stage = ref<HTMLCanvasElement>()\n\nconst layerCount = ref(0)\n\n// Photoshop blend mode → canvas globalCompositeOperation (unknown → normal).\nconst BLEND: Record<string, GlobalCompositeOperation> = {\n normal: 'source-over',\n multiply: 'multiply',\n screen: 'screen',\n overlay: 'overlay',\n darken: 'darken',\n lighten: 'lighten',\n 'color dodge': 'color-dodge',\n 'linear dodge': 'lighter',\n 'color burn': 'color-burn',\n 'hard light': 'hard-light',\n 'soft light': 'soft-light',\n difference: 'difference',\n exclusion: 'exclusion',\n hue: 'hue',\n saturation: 'saturation',\n color: 'color',\n luminosity: 'luminosity',\n}\n\nfunction makeThumb(src: HTMLCanvasElement): string | undefined {\n if (!src.width || !src.height) return undefined\n const S = 34\n const c = document.createElement('canvas')\n c.width = S\n c.height = S\n const ctx = c.getContext('2d')\n if (!ctx) return undefined\n const s = Math.min(S / src.width, S / src.height)\n const w = src.width * s\n const h = src.height * s\n ctx.drawImage(src, (S - w) / 2, (S - h) / 2, w, h)\n return c.toDataURL()\n}\n\nfunction build(psd: Psd): void {\n nodeById.clear()\n const hid = new Set<number>()\n let id = 0\n function rec(layers: Layer[] | undefined, depth: number, parentId: number): PsdNode[] {\n const out: PsdNode[] = []\n for (const l of layers ?? []) {\n const isGroup = Array.isArray(l.children)\n const n: PsdNode = {\n id: id++,\n name: l.name || (isGroup ? '组' : '图层'),\n depth,\n isGroup,\n opacity: typeof l.opacity === 'number' ? l.opacity : 1,\n blend: l.blendMode || 'normal',\n left: l.left ?? 0,\n top: l.top ?? 0,\n canvas: isGroup ? undefined : (l.canvas as HTMLCanvasElement | undefined),\n parentId,\n children: [],\n }\n nodeById.set(n.id, n)\n if (l.hidden) hid.add(n.id)\n if (n.canvas) n.thumb = makeThumb(n.canvas)\n if (isGroup) n.children = rec(l.children, depth + 1, n.id)\n out.push(n)\n }\n return out\n }\n const tree = rec(psd.children, 0, -1)\n\n // Compositing order: leaves in document (bottom → top) order.\n const leaves: PsdNode[] = []\n const collectDraw = (ns: PsdNode[]): void => {\n for (const n of ns) {\n if (n.isGroup) collectDraw(n.children)\n else if (n.canvas) leaves.push(n)\n }\n }\n collectDraw(tree)\n\n // Panel order: top → bottom (reverse each level), group header before its indented children.\n const rows: PsdNode[] = []\n const collectPanel = (ns: PsdNode[]): void => {\n for (const n of [...ns].reverse()) {\n rows.push(n)\n if (n.isGroup) collectPanel(n.children)\n }\n }\n collectPanel(tree)\n\n drawOrder.value = leaves\n panelRows.value = rows\n hidden.value = hid\n layerCount.value = leaves.length\n fallbackCanvas = leaves.length ? undefined : (psd.canvas as HTMLCanvasElement | undefined)\n}\n\n// A leaf is visible only if neither it nor any ancestor group is hidden.\nfunction visible(id: number): boolean {\n let n: PsdNode | undefined = nodeById.get(id)\n while (n) {\n if (hidden.value.has(n.id)) return false\n n = n.parentId >= 0 ? nodeById.get(n.parentId) : undefined\n }\n return true\n}\n\nfunction recomposite(): void {\n const cv = stage.value\n if (!cv) return\n cv.width = width || fallbackCanvas?.width || 1\n cv.height = height || fallbackCanvas?.height || 1\n const ctx = cv.getContext('2d')\n if (!ctx) return\n ctx.clearRect(0, 0, cv.width, cv.height)\n if (fallbackCanvas) {\n ctx.drawImage(fallbackCanvas, 0, 0)\n return\n }\n for (const leaf of drawOrder.value) {\n if (!leaf.canvas || !visible(leaf.id)) continue\n ctx.globalAlpha = Math.max(0, Math.min(1, leaf.opacity))\n ctx.globalCompositeOperation = BLEND[leaf.blend] ?? 'source-over'\n ctx.drawImage(leaf.canvas, leaf.left, leaf.top)\n }\n ctx.globalAlpha = 1\n ctx.globalCompositeOperation = 'source-over'\n}\n\nfunction toggle(n: PsdNode): void {\n const next = new Set(hidden.value)\n if (next.has(n.id)) next.delete(n.id)\n else next.add(n.id)\n hidden.value = next\n recomposite()\n}\n\nfunction load(): void {\n if (!props.url) return\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n missingDeps.value = null\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n const { readPsd } = await import('ag-psd')\n const psd = readPsd(buf, { skipThumbnail: true })\n width = psd.width\n height = psd.height\n build(psd)\n status.value = 'ready'\n // wait for the canvas element to mount, then paint\n requestAnimationFrame(recomposite)\n } catch (e) {\n if (isMissingModule(e)) missingDeps.value = ['ag-psd']\n else errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nwatch(() => props.url, load, { immediate: true })\nonBeforeUnmount(() => {\n drawOrder.value = []\n panelRows.value = []\n nodeById.clear()\n})\n</script>\n\n<template>\n <div class=\"psd\">\n <p v-if=\"status === 'loading'\" class=\"psd__msg\">正在解码 PSD…</p>\n <div v-else-if=\"status === 'gate'\" class=\"psd__msg psd__gate\">\n <p>该 PSD 较大({{ formatBytes(props.size) }}),将在浏览器内逐图层解码。是否继续?</p>\n <button type=\"button\" class=\"psd__btn\" @click=\"doLoad\">解码并预览</button>\n </div>\n <MissingDependency\n v-else-if=\"missingDeps\"\n :pkg=\"missingDeps\"\n label=\"PSD\"\n :download-url=\"props.url\"\n :download-name=\"props.name\"\n />\n <div v-else-if=\"status === 'error'\" class=\"psd__msg psd__msg--error\">\n <p>解码失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"psd__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <template v-else>\n <div class=\"psd__bar\">\n <span class=\"psd__badge\">PSD</span>\n <span class=\"psd__dim\">{{ width }}×{{ height }}</span>\n <span v-if=\"layerCount\" class=\"psd__dim\">{{ layerCount }} 图层</span>\n <span v-if=\"props.size\" class=\"psd__dim\">{{ formatBytes(props.size) }}</span>\n <span class=\"psd__spacer\" />\n <button\n type=\"button\"\n class=\"psd__toggle\"\n :class=\"{ 'psd__toggle--on': !fit }\"\n @click=\"fit = !fit\"\n >\n {{ fit ? '原始大小' : '适应' }}\n </button>\n </div>\n\n <div class=\"psd__stage\" :class=\"{ 'psd__stage--fit': fit }\">\n <canvas ref=\"stage\" class=\"psd__canvas\" :class=\"{ 'psd__canvas--fit': fit }\" />\n </div>\n\n <ul v-if=\"panelRows.length\" class=\"psd__layers\">\n <li\n v-for=\"n in panelRows\"\n :key=\"n.id\"\n class=\"psd__layer\"\n :class=\"{ 'psd__layer--group': n.isGroup, 'psd__layer--off': !visible(n.id) }\"\n :style=\"{ paddingLeft: 10 + n.depth * 14 + 'px' }\"\n >\n <button\n type=\"button\"\n class=\"psd__eye\"\n :class=\"{ 'psd__eye--off': hidden.has(n.id) }\"\n :title=\"hidden.has(n.id) ? '显示' : '隐藏'\"\n @click=\"toggle(n)\"\n >\n <Icon name=\"eye\" :size=\"14\" />\n </button>\n <span class=\"psd__thumb\">\n <img v-if=\"n.thumb\" :src=\"n.thumb\" alt=\"\" />\n <span v-else class=\"psd__thumb--group\" />\n </span>\n <span class=\"psd__name\">{{ n.name }}</span>\n <span v-if=\"n.blend !== 'normal'\" class=\"psd__blend\">{{ n.blend }}</span>\n <span v-if=\"n.opacity < 1\" class=\"psd__op\">{{ Math.round(n.opacity * 100) }}%</span>\n </li>\n </ul>\n </template>\n </div>\n</template>\n\n<style scoped>\n.psd {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n.psd__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.psd__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.psd__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.psd__btn {\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.psd__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.psd__bar {\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);\n font-size: 12px;\n color: var(--muted);\n}\n.psd__badge {\n padding: 2px 9px;\n border-radius: 999px;\n background: var(--surface-2);\n color: var(--accent);\n font-weight: 600;\n}\n.psd__dim {\n color: var(--faint);\n}\n.psd__spacer {\n flex: 1 1 auto;\n}\n.psd__toggle {\n min-width: 26px;\n height: 26px;\n padding: 0 10px;\n font-family: inherit;\n font-size: 12px;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 6px;\n cursor: pointer;\n}\n.psd__toggle:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.psd__toggle--on {\n color: var(--accent);\n border-color: var(--accent);\n background: var(--surface-2);\n}\n\n/* Checkerboard so layer transparency reads clearly. */\n.psd__stage {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n --sq: 10px;\n background-color: #fff;\n background-image:\n linear-gradient(45deg, #e6e6e6 25%, transparent 25%),\n linear-gradient(-45deg, #e6e6e6 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, #e6e6e6 75%),\n linear-gradient(-45deg, transparent 75%, #e6e6e6 75%);\n background-size: calc(var(--sq) * 2) calc(var(--sq) * 2);\n background-position:\n 0 0,\n 0 var(--sq),\n var(--sq) calc(var(--sq) * -1),\n calc(var(--sq) * -1) 0;\n}\n.psd__stage--fit {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n}\n.psd__canvas {\n display: block;\n image-rendering: auto;\n}\n.psd__canvas--fit {\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n}\n\n.psd__layers {\n list-style: none;\n margin: 0;\n padding: 4px 0;\n flex-shrink: 0;\n max-height: 40%;\n overflow: auto;\n overscroll-behavior: contain;\n border-top: 1px solid var(--border);\n background: var(--surface);\n}\n.psd__layer {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 4px 12px 4px 0;\n font-size: 12.5px;\n color: var(--text);\n}\n.psd__layer--group {\n font-weight: 600;\n color: var(--muted);\n}\n.psd__layer--off {\n opacity: 0.45;\n}\n.psd__eye {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 22px;\n flex-shrink: 0;\n padding: 0;\n color: var(--accent);\n background: transparent;\n border: none;\n cursor: pointer;\n}\n.psd__eye--off {\n color: var(--faint);\n}\n.psd__thumb {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n height: 30px;\n flex-shrink: 0;\n border: 1px solid var(--border);\n border-radius: 4px;\n background: repeating-conic-gradient(#eee 0% 25%, #fff 0% 50%) 0 / 10px 10px;\n overflow: hidden;\n}\n.psd__thumb img {\n max-width: 100%;\n max-height: 100%;\n display: block;\n}\n.psd__thumb--group {\n width: 14px;\n height: 11px;\n border: 1.4px solid var(--faint);\n border-radius: 2px;\n background: transparent;\n}\n.psd__name {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.psd__blend {\n flex-shrink: 0;\n font-size: 10.5px;\n color: var(--accent);\n}\n.psd__op {\n flex-shrink: 0;\n font-family: var(--font-mono, monospace);\n font-size: 11px;\n color: var(--faint);\n}\n</style>\n"],"mappings":""}
@@ -216,3 +216,5 @@ var x = { class: "psd" }, S = {
216
216
  });
217
217
  //#endregion
218
218
  export { F as default };
219
+
220
+ //# sourceMappingURL=PsdViewer.vue_vue_type_script_setup_true_lang.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PsdViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/PsdViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, ref, shallowRef, watch } from 'vue'\nimport type { Layer, Psd } from 'ag-psd'\nimport { formatBytes } from '@/lib/format'\nimport { isMissingModule } from '@/lib/optionalDep'\nimport Icon from '@/components/icons/Icon.vue'\nimport MissingDependency from '@/components/MissingDependency.vue'\n\n/**\n * Side-viewer for Photoshop documents (.psd/.psb). Fetches the bytes and parses them in-browser with ag-psd\n * (dynamically imported), then — unlike the generic RasterViewer which only shows the flattened composite —\n * exposes the LAYER TREE: a panel of every layer/group with a thumbnail and a visibility toggle, and it\n * RE-COMPOSITES live onto a canvas as you toggle layers (honouring per-layer opacity and blend mode; group\n * visibility cascades to its children). A flattened PSD (no layers) falls back to ag-psd's composite.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\n// Set when the PSD engine (ag-psd, an optional peer) isn't installed → show the install card, not a raw error.\nconst missingDeps = ref<string[] | null>(null)\nconst fit = ref(true)\n\n// Reading a PSD decodes EVERY layer's pixels (needed for the layer panel), so gate very large files behind a\n// confirm the way RasterViewer does — a huge multi-layer PSD would otherwise lock the tab while decoding.\nconst SIZE_LIMIT = 60 * 1024 * 1024\n\ninterface PsdNode {\n id: number\n name: string\n depth: number\n isGroup: boolean\n opacity: number\n blend: string\n left: number\n top: number\n canvas?: HTMLCanvasElement // leaf pixels\n thumb?: string // leaf thumbnail data URL\n parentId: number\n children: PsdNode[]\n}\n\nlet width = 0\nlet height = 0\nconst drawOrder = shallowRef<PsdNode[]>([]) // leaves, bottom → top (compositing order)\nconst panelRows = shallowRef<PsdNode[]>([]) // groups + leaves, top → bottom (Photoshop panel order)\nconst nodeById = new Map<number, PsdNode>()\nconst hidden = ref(new Set<number>()) // ids the user (or the file) has hidden\nlet fallbackCanvas: HTMLCanvasElement | undefined // flattened composite, when there are no layers\nconst stage = ref<HTMLCanvasElement>()\n\nconst layerCount = ref(0)\n\n// Photoshop blend mode → canvas globalCompositeOperation (unknown → normal).\nconst BLEND: Record<string, GlobalCompositeOperation> = {\n normal: 'source-over',\n multiply: 'multiply',\n screen: 'screen',\n overlay: 'overlay',\n darken: 'darken',\n lighten: 'lighten',\n 'color dodge': 'color-dodge',\n 'linear dodge': 'lighter',\n 'color burn': 'color-burn',\n 'hard light': 'hard-light',\n 'soft light': 'soft-light',\n difference: 'difference',\n exclusion: 'exclusion',\n hue: 'hue',\n saturation: 'saturation',\n color: 'color',\n luminosity: 'luminosity',\n}\n\nfunction makeThumb(src: HTMLCanvasElement): string | undefined {\n if (!src.width || !src.height) return undefined\n const S = 34\n const c = document.createElement('canvas')\n c.width = S\n c.height = S\n const ctx = c.getContext('2d')\n if (!ctx) return undefined\n const s = Math.min(S / src.width, S / src.height)\n const w = src.width * s\n const h = src.height * s\n ctx.drawImage(src, (S - w) / 2, (S - h) / 2, w, h)\n return c.toDataURL()\n}\n\nfunction build(psd: Psd): void {\n nodeById.clear()\n const hid = new Set<number>()\n let id = 0\n function rec(layers: Layer[] | undefined, depth: number, parentId: number): PsdNode[] {\n const out: PsdNode[] = []\n for (const l of layers ?? []) {\n const isGroup = Array.isArray(l.children)\n const n: PsdNode = {\n id: id++,\n name: l.name || (isGroup ? '组' : '图层'),\n depth,\n isGroup,\n opacity: typeof l.opacity === 'number' ? l.opacity : 1,\n blend: l.blendMode || 'normal',\n left: l.left ?? 0,\n top: l.top ?? 0,\n canvas: isGroup ? undefined : (l.canvas as HTMLCanvasElement | undefined),\n parentId,\n children: [],\n }\n nodeById.set(n.id, n)\n if (l.hidden) hid.add(n.id)\n if (n.canvas) n.thumb = makeThumb(n.canvas)\n if (isGroup) n.children = rec(l.children, depth + 1, n.id)\n out.push(n)\n }\n return out\n }\n const tree = rec(psd.children, 0, -1)\n\n // Compositing order: leaves in document (bottom → top) order.\n const leaves: PsdNode[] = []\n const collectDraw = (ns: PsdNode[]): void => {\n for (const n of ns) {\n if (n.isGroup) collectDraw(n.children)\n else if (n.canvas) leaves.push(n)\n }\n }\n collectDraw(tree)\n\n // Panel order: top → bottom (reverse each level), group header before its indented children.\n const rows: PsdNode[] = []\n const collectPanel = (ns: PsdNode[]): void => {\n for (const n of [...ns].reverse()) {\n rows.push(n)\n if (n.isGroup) collectPanel(n.children)\n }\n }\n collectPanel(tree)\n\n drawOrder.value = leaves\n panelRows.value = rows\n hidden.value = hid\n layerCount.value = leaves.length\n fallbackCanvas = leaves.length ? undefined : (psd.canvas as HTMLCanvasElement | undefined)\n}\n\n// A leaf is visible only if neither it nor any ancestor group is hidden.\nfunction visible(id: number): boolean {\n let n: PsdNode | undefined = nodeById.get(id)\n while (n) {\n if (hidden.value.has(n.id)) return false\n n = n.parentId >= 0 ? nodeById.get(n.parentId) : undefined\n }\n return true\n}\n\nfunction recomposite(): void {\n const cv = stage.value\n if (!cv) return\n cv.width = width || fallbackCanvas?.width || 1\n cv.height = height || fallbackCanvas?.height || 1\n const ctx = cv.getContext('2d')\n if (!ctx) return\n ctx.clearRect(0, 0, cv.width, cv.height)\n if (fallbackCanvas) {\n ctx.drawImage(fallbackCanvas, 0, 0)\n return\n }\n for (const leaf of drawOrder.value) {\n if (!leaf.canvas || !visible(leaf.id)) continue\n ctx.globalAlpha = Math.max(0, Math.min(1, leaf.opacity))\n ctx.globalCompositeOperation = BLEND[leaf.blend] ?? 'source-over'\n ctx.drawImage(leaf.canvas, leaf.left, leaf.top)\n }\n ctx.globalAlpha = 1\n ctx.globalCompositeOperation = 'source-over'\n}\n\nfunction toggle(n: PsdNode): void {\n const next = new Set(hidden.value)\n if (next.has(n.id)) next.delete(n.id)\n else next.add(n.id)\n hidden.value = next\n recomposite()\n}\n\nfunction load(): void {\n if (!props.url) return\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n missingDeps.value = null\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n const { readPsd } = await import('ag-psd')\n const psd = readPsd(buf, { skipThumbnail: true })\n width = psd.width\n height = psd.height\n build(psd)\n status.value = 'ready'\n // wait for the canvas element to mount, then paint\n requestAnimationFrame(recomposite)\n } catch (e) {\n if (isMissingModule(e)) missingDeps.value = ['ag-psd']\n else errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nwatch(() => props.url, load, { immediate: true })\nonBeforeUnmount(() => {\n drawOrder.value = []\n panelRows.value = []\n nodeById.clear()\n})\n</script>\n\n<template>\n <div class=\"psd\">\n <p v-if=\"status === 'loading'\" class=\"psd__msg\">正在解码 PSD…</p>\n <div v-else-if=\"status === 'gate'\" class=\"psd__msg psd__gate\">\n <p>该 PSD 较大({{ formatBytes(props.size) }}),将在浏览器内逐图层解码。是否继续?</p>\n <button type=\"button\" class=\"psd__btn\" @click=\"doLoad\">解码并预览</button>\n </div>\n <MissingDependency\n v-else-if=\"missingDeps\"\n :pkg=\"missingDeps\"\n label=\"PSD\"\n :download-url=\"props.url\"\n :download-name=\"props.name\"\n />\n <div v-else-if=\"status === 'error'\" class=\"psd__msg psd__msg--error\">\n <p>解码失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"psd__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <template v-else>\n <div class=\"psd__bar\">\n <span class=\"psd__badge\">PSD</span>\n <span class=\"psd__dim\">{{ width }}×{{ height }}</span>\n <span v-if=\"layerCount\" class=\"psd__dim\">{{ layerCount }} 图层</span>\n <span v-if=\"props.size\" class=\"psd__dim\">{{ formatBytes(props.size) }}</span>\n <span class=\"psd__spacer\" />\n <button\n type=\"button\"\n class=\"psd__toggle\"\n :class=\"{ 'psd__toggle--on': !fit }\"\n @click=\"fit = !fit\"\n >\n {{ fit ? '原始大小' : '适应' }}\n </button>\n </div>\n\n <div class=\"psd__stage\" :class=\"{ 'psd__stage--fit': fit }\">\n <canvas ref=\"stage\" class=\"psd__canvas\" :class=\"{ 'psd__canvas--fit': fit }\" />\n </div>\n\n <ul v-if=\"panelRows.length\" class=\"psd__layers\">\n <li\n v-for=\"n in panelRows\"\n :key=\"n.id\"\n class=\"psd__layer\"\n :class=\"{ 'psd__layer--group': n.isGroup, 'psd__layer--off': !visible(n.id) }\"\n :style=\"{ paddingLeft: 10 + n.depth * 14 + 'px' }\"\n >\n <button\n type=\"button\"\n class=\"psd__eye\"\n :class=\"{ 'psd__eye--off': hidden.has(n.id) }\"\n :title=\"hidden.has(n.id) ? '显示' : '隐藏'\"\n @click=\"toggle(n)\"\n >\n <Icon name=\"eye\" :size=\"14\" />\n </button>\n <span class=\"psd__thumb\">\n <img v-if=\"n.thumb\" :src=\"n.thumb\" alt=\"\" />\n <span v-else class=\"psd__thumb--group\" />\n </span>\n <span class=\"psd__name\">{{ n.name }}</span>\n <span v-if=\"n.blend !== 'normal'\" class=\"psd__blend\">{{ n.blend }}</span>\n <span v-if=\"n.opacity < 1\" class=\"psd__op\">{{ Math.round(n.opacity * 100) }}%</span>\n </li>\n </ul>\n </template>\n </div>\n</template>\n\n<style scoped>\n.psd {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n.psd__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.psd__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.psd__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.psd__btn {\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.psd__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.psd__bar {\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);\n font-size: 12px;\n color: var(--muted);\n}\n.psd__badge {\n padding: 2px 9px;\n border-radius: 999px;\n background: var(--surface-2);\n color: var(--accent);\n font-weight: 600;\n}\n.psd__dim {\n color: var(--faint);\n}\n.psd__spacer {\n flex: 1 1 auto;\n}\n.psd__toggle {\n min-width: 26px;\n height: 26px;\n padding: 0 10px;\n font-family: inherit;\n font-size: 12px;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 6px;\n cursor: pointer;\n}\n.psd__toggle:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.psd__toggle--on {\n color: var(--accent);\n border-color: var(--accent);\n background: var(--surface-2);\n}\n\n/* Checkerboard so layer transparency reads clearly. */\n.psd__stage {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n --sq: 10px;\n background-color: #fff;\n background-image:\n linear-gradient(45deg, #e6e6e6 25%, transparent 25%),\n linear-gradient(-45deg, #e6e6e6 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, #e6e6e6 75%),\n linear-gradient(-45deg, transparent 75%, #e6e6e6 75%);\n background-size: calc(var(--sq) * 2) calc(var(--sq) * 2);\n background-position:\n 0 0,\n 0 var(--sq),\n var(--sq) calc(var(--sq) * -1),\n calc(var(--sq) * -1) 0;\n}\n.psd__stage--fit {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n}\n.psd__canvas {\n display: block;\n image-rendering: auto;\n}\n.psd__canvas--fit {\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n}\n\n.psd__layers {\n list-style: none;\n margin: 0;\n padding: 4px 0;\n flex-shrink: 0;\n max-height: 40%;\n overflow: auto;\n overscroll-behavior: contain;\n border-top: 1px solid var(--border);\n background: var(--surface);\n}\n.psd__layer {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 4px 12px 4px 0;\n font-size: 12.5px;\n color: var(--text);\n}\n.psd__layer--group {\n font-weight: 600;\n color: var(--muted);\n}\n.psd__layer--off {\n opacity: 0.45;\n}\n.psd__eye {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 22px;\n flex-shrink: 0;\n padding: 0;\n color: var(--accent);\n background: transparent;\n border: none;\n cursor: pointer;\n}\n.psd__eye--off {\n color: var(--faint);\n}\n.psd__thumb {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n height: 30px;\n flex-shrink: 0;\n border: 1px solid var(--border);\n border-radius: 4px;\n background: repeating-conic-gradient(#eee 0% 25%, #fff 0% 50%) 0 / 10px 10px;\n overflow: hidden;\n}\n.psd__thumb img {\n max-width: 100%;\n max-height: 100%;\n display: block;\n}\n.psd__thumb--group {\n width: 14px;\n height: 11px;\n border: 1.4px solid var(--faint);\n border-radius: 2px;\n background: transparent;\n}\n.psd__name {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.psd__blend {\n flex-shrink: 0;\n font-size: 10.5px;\n color: var(--accent);\n}\n.psd__op {\n flex-shrink: 0;\n font-family: var(--font-mono, monospace);\n font-size: 11px;\n color: var(--faint);\n}\n</style>\n"],"mappings":"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"}
@@ -5,3 +5,5 @@ import t from "./QmcAudioViewer.vue_vue_type_script_setup_true_lang.js";
5
5
  var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-b95a2049"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
+
9
+ //# sourceMappingURL=QmcAudioViewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"QmcAudioViewer.js","names":[],"sources":["../../src/components/QmcAudioViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { decryptQmc, QmcError } from '@/lib/qmc'\nimport { fileExt } from '@/lib/fileType'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * Side-viewer player for QQ Music encrypted audio (`.mflac` / `.mgg` / …). Fetches the encrypted bytes from\n * the download URL, DECRYPTS them in-browser (see lib/qmc.ts) — recovering the embedded key and undoing the\n * QMCv2 stream cipher — then plays the resulting FLAC/OGG/MP3 through a native <audio> from a typed Blob URL.\n *\n * The panel's own download button would only fetch the still-encrypted file, so this viewer surfaces its own\n * \"download decrypted\" action (the playable audio with the right extension). Decryption is pure + fast\n * (tens of ms even for ~60MB), but it loads the whole file into memory, so very large files are gated behind\n * a confirm like the plain AudioViewer.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Status = 'loading' | 'gate' | 'decrypting' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst audioUrl = ref('')\nconst format = ref('')\nconst decryptedSize = ref(0)\n\n// Whole file is read + decrypted in memory; gate very large files behind a confirm.\nconst SIZE_LIMIT = 100 * 1024 * 1024\n\nconst baseName = computed(() => {\n const n = props.name || 'audio'\n const e = fileExt(n)\n return e ? n.slice(0, n.length - e.length - 1) : n\n})\nconst downloadName = computed(() => `${baseName.value}.${format.value || 'audio'}`)\n\nfunction revoke(): void {\n if (audioUrl.value) {\n URL.revokeObjectURL(audioUrl.value)\n audioUrl.value = ''\n }\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n revoke()\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n // Yield a frame so the \"解密中…\" state paints before the (synchronous) decrypt runs.\n status.value = 'decrypting'\n await new Promise((r) => setTimeout(r, 0))\n const out = decryptQmc(buf)\n format.value = out.format\n decryptedSize.value = out.data.length\n audioUrl.value = URL.createObjectURL(new Blob([out.data], { type: out.mime }))\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof QmcError ? e.message : e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nwatch(() => props.url, load, { immediate: true })\nonBeforeUnmount(revoke)\n</script>\n\n<template>\n <div class=\"qmc\">\n <p v-if=\"status === 'loading'\" class=\"qmc__msg\">正在加载…</p>\n <p v-else-if=\"status === 'decrypting'\" class=\"qmc__msg\">正在解密…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"qmc__msg qmc__gate\">\n <p>该文件较大({{ formatBytes(props.size) }}),将在浏览器内加载并解密。是否继续?</p>\n <button type=\"button\" class=\"qmc__btn\" @click=\"doLoad\">加载并解密</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"qmc__msg qmc__msg--error\">\n <p>无法解密:{{ errorMsg }}</p>\n <button type=\"button\" class=\"qmc__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <div v-else class=\"qmc__player\">\n <div class=\"qmc__icon\"><Icon name=\"music\" :size=\"40\" /></div>\n <div class=\"qmc__name\">{{ props.name }}</div>\n <div class=\"qmc__meta\">\n <span class=\"qmc__badge\">{{ format.toUpperCase() }} · 已解密</span>\n <span v-if=\"decryptedSize\">{{ formatBytes(decryptedSize) }}</span>\n </div>\n <audio class=\"qmc__audio\" :src=\"audioUrl\" controls preload=\"metadata\" />\n <a class=\"qmc__dl\" :href=\"audioUrl\" :download=\"downloadName\">\n <Icon name=\"download\" :size=\"14\" /><span>下载解密后的音频</span>\n </a>\n </div>\n </div>\n</template>\n\n<style scoped>\n.qmc {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.qmc__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.qmc__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.qmc__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.qmc__btn {\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.qmc__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Centered card: glyph, filename, a format/size line, the native controls, and a download-decrypted button. */\n.qmc__player {\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: 14px;\n padding: 24px;\n}\n.qmc__icon {\n width: 84px;\n height: 84px;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--accent);\n background: var(--surface-2);\n border-radius: 50%;\n}\n.qmc__name {\n max-width: 100%;\n font-size: 13.5px;\n color: var(--text-strong);\n word-break: break-word;\n text-align: center;\n}\n.qmc__meta {\n display: flex;\n align-items: center;\n gap: 10px;\n font-size: 12px;\n color: var(--faint);\n}\n.qmc__badge {\n padding: 2px 9px;\n border-radius: 999px;\n background: var(--surface-2);\n color: var(--accent);\n font-weight: 600;\n letter-spacing: 0.02em;\n}\n.qmc__audio {\n width: min(420px, 100%);\n}\n.qmc__dl {\n display: inline-flex;\n align-items: center;\n gap: 6px;\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: 6px 14px;\n text-decoration: none;\n cursor: pointer;\n}\n.qmc__dl:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n</style>\n"],"mappings":""}
@@ -90,3 +90,5 @@ var _ = { class: "qmc" }, v = {
90
90
  });
91
91
  //#endregion
92
92
  export { j as default };
93
+
94
+ //# sourceMappingURL=QmcAudioViewer.vue_vue_type_script_setup_true_lang.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"QmcAudioViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/QmcAudioViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { decryptQmc, QmcError } from '@/lib/qmc'\nimport { fileExt } from '@/lib/fileType'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * Side-viewer player for QQ Music encrypted audio (`.mflac` / `.mgg` / …). Fetches the encrypted bytes from\n * the download URL, DECRYPTS them in-browser (see lib/qmc.ts) — recovering the embedded key and undoing the\n * QMCv2 stream cipher — then plays the resulting FLAC/OGG/MP3 through a native <audio> from a typed Blob URL.\n *\n * The panel's own download button would only fetch the still-encrypted file, so this viewer surfaces its own\n * \"download decrypted\" action (the playable audio with the right extension). Decryption is pure + fast\n * (tens of ms even for ~60MB), but it loads the whole file into memory, so very large files are gated behind\n * a confirm like the plain AudioViewer.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Status = 'loading' | 'gate' | 'decrypting' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst audioUrl = ref('')\nconst format = ref('')\nconst decryptedSize = ref(0)\n\n// Whole file is read + decrypted in memory; gate very large files behind a confirm.\nconst SIZE_LIMIT = 100 * 1024 * 1024\n\nconst baseName = computed(() => {\n const n = props.name || 'audio'\n const e = fileExt(n)\n return e ? n.slice(0, n.length - e.length - 1) : n\n})\nconst downloadName = computed(() => `${baseName.value}.${format.value || 'audio'}`)\n\nfunction revoke(): void {\n if (audioUrl.value) {\n URL.revokeObjectURL(audioUrl.value)\n audioUrl.value = ''\n }\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n revoke()\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n // Yield a frame so the \"解密中…\" state paints before the (synchronous) decrypt runs.\n status.value = 'decrypting'\n await new Promise((r) => setTimeout(r, 0))\n const out = decryptQmc(buf)\n format.value = out.format\n decryptedSize.value = out.data.length\n audioUrl.value = URL.createObjectURL(new Blob([out.data], { type: out.mime }))\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof QmcError ? e.message : e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nwatch(() => props.url, load, { immediate: true })\nonBeforeUnmount(revoke)\n</script>\n\n<template>\n <div class=\"qmc\">\n <p v-if=\"status === 'loading'\" class=\"qmc__msg\">正在加载…</p>\n <p v-else-if=\"status === 'decrypting'\" class=\"qmc__msg\">正在解密…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"qmc__msg qmc__gate\">\n <p>该文件较大({{ formatBytes(props.size) }}),将在浏览器内加载并解密。是否继续?</p>\n <button type=\"button\" class=\"qmc__btn\" @click=\"doLoad\">加载并解密</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"qmc__msg qmc__msg--error\">\n <p>无法解密:{{ errorMsg }}</p>\n <button type=\"button\" class=\"qmc__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <div v-else class=\"qmc__player\">\n <div class=\"qmc__icon\"><Icon name=\"music\" :size=\"40\" /></div>\n <div class=\"qmc__name\">{{ props.name }}</div>\n <div class=\"qmc__meta\">\n <span class=\"qmc__badge\">{{ format.toUpperCase() }} · 已解密</span>\n <span v-if=\"decryptedSize\">{{ formatBytes(decryptedSize) }}</span>\n </div>\n <audio class=\"qmc__audio\" :src=\"audioUrl\" controls preload=\"metadata\" />\n <a class=\"qmc__dl\" :href=\"audioUrl\" :download=\"downloadName\">\n <Icon name=\"download\" :size=\"14\" /><span>下载解密后的音频</span>\n </a>\n </div>\n </div>\n</template>\n\n<style scoped>\n.qmc {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.qmc__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.qmc__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.qmc__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.qmc__btn {\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.qmc__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Centered card: glyph, filename, a format/size line, the native controls, and a download-decrypted button. */\n.qmc__player {\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: 14px;\n padding: 24px;\n}\n.qmc__icon {\n width: 84px;\n height: 84px;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--accent);\n background: var(--surface-2);\n border-radius: 50%;\n}\n.qmc__name {\n max-width: 100%;\n font-size: 13.5px;\n color: var(--text-strong);\n word-break: break-word;\n text-align: center;\n}\n.qmc__meta {\n display: flex;\n align-items: center;\n gap: 10px;\n font-size: 12px;\n color: var(--faint);\n}\n.qmc__badge {\n padding: 2px 9px;\n border-radius: 999px;\n background: var(--surface-2);\n color: var(--accent);\n font-weight: 600;\n letter-spacing: 0.02em;\n}\n.qmc__audio {\n width: min(420px, 100%);\n}\n.qmc__dl {\n display: inline-flex;\n align-items: center;\n gap: 6px;\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: 6px 14px;\n text-decoration: none;\n cursor: pointer;\n}\n.qmc__dl:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n</style>\n"],"mappings":"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"}
@@ -5,3 +5,5 @@ import t from "./RasterViewer.vue_vue_type_script_setup_true_lang.js";
5
5
  var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-76bced18"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
+
9
+ //# sourceMappingURL=RasterViewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RasterViewer.js","names":[],"sources":["../../src/components/RasterViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { formatBytes } from '@/lib/format'\nimport { isMissingModule } from '@/lib/optionalDep'\nimport MissingDependency from '@/components/MissingDependency.vue'\n\n/**\n * Side-viewer for raster image formats the browser can't decode in a plain <img>. Fetches the bytes and\n * decodes them to a viewable raster in-browser, then shows the result (fit-to-pane, with an actual-size\n * toggle). Each decoder is DYNAMICALLY imported in its branch, so opening a TIFF never pulls the PSD/HEIC\n * code (and vice-versa).\n *\n * - tiff: UTIF (utif2) → RGBA → canvas; multi-page TIFFs get a page switcher.\n * - psd: ag-psd readPsd() → the composited canvas.\n * - heic/heif: heic2any (libheif) → a PNG blob.\n * - raw (cr2/nef/arw/dng/…): BEST EFFORT — no in-browser demosaic, so we extract the embedded JPEG preview\n * (see lib/rawPreview.ts). Usually the camera's full-size preview; a file without one can't be shown here.\n */\nconst props = defineProps<{\n url?: string\n name: string\n size?: number\n kind: 'tiff' | 'psd' | 'heic' | 'raw'\n}>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\n// Set when this kind's decoder (an optional peer) isn't installed → show the install card, not a raw error.\nconst missingDeps = ref<string[] | null>(null)\nconst pages = ref<string[]>([])\nconst pageIdx = ref(0)\nconst fit = ref(true)\nlet objectUrls: string[] = []\n\nconst SIZE_LIMIT = 80 * 1024 * 1024\n\nconst KIND_LABEL: Record<string, string> = { tiff: 'TIFF', psd: 'PSD', heic: 'HEIC', raw: 'RAW' }\n// Optional peer each kind's decoder needs (raw uses the internal lib/rawPreview — no external dep).\nconst DEP_BY_KIND: Record<string, string[]> = {\n tiff: ['utif2'],\n psd: ['ag-psd'],\n heic: ['heic2any'],\n}\n\nfunction revoke(): void {\n for (const u of objectUrls) URL.revokeObjectURL(u)\n objectUrls = []\n pages.value = []\n pageIdx.value = 0\n}\n\nfunction canvasToUrl(canvas: HTMLCanvasElement): Promise<string> {\n return new Promise((resolve, reject) => {\n canvas.toBlob((blob) => {\n if (!blob) return reject(new Error('canvas 编码失败'))\n const u = URL.createObjectURL(blob)\n objectUrls.push(u)\n resolve(u)\n }, 'image/png')\n })\n}\n\nasync function decodeTiff(buf: ArrayBuffer): Promise<string[]> {\n const UTIF = await import('utif2')\n const ifds = UTIF.decode(buf)\n const urls: string[] = []\n for (const ifd of ifds) {\n try {\n UTIF.decodeImage(buf, ifd)\n } catch {\n continue\n }\n if (!ifd.width || !ifd.height) continue\n const rgba = UTIF.toRGBA8(ifd)\n const canvas = document.createElement('canvas')\n canvas.width = ifd.width\n canvas.height = ifd.height\n const ctx = canvas.getContext('2d')\n if (!ctx) continue\n ctx.putImageData(new ImageData(new Uint8ClampedArray(rgba), ifd.width, ifd.height), 0, 0)\n urls.push(await canvasToUrl(canvas))\n }\n if (!urls.length) throw new Error('未能解码该 TIFF')\n return urls\n}\n\nasync function decodePsd(buf: ArrayBuffer): Promise<string[]> {\n const { readPsd } = await import('ag-psd')\n const psd = readPsd(buf, { skipLayerImageData: true, skipThumbnail: true })\n if (!psd.canvas) throw new Error('该 PSD 没有可显示的合成图像')\n return [await canvasToUrl(psd.canvas)]\n}\n\nasync function decodeHeic(buf: ArrayBuffer): Promise<string[]> {\n const heic2any = (await import('heic2any')).default\n const out = await heic2any({ blob: new Blob([buf]), toType: 'image/png' })\n const blob = Array.isArray(out) ? out[0] : out\n const u = URL.createObjectURL(blob)\n objectUrls.push(u)\n return [u]\n}\n\nasync function decodeRaw(buf: ArrayBuffer): Promise<string[]> {\n const { extractLargestJpeg } = await import('@/lib/rawPreview')\n const jpeg = extractLargestJpeg(buf)\n if (!jpeg) throw new Error('未能从该 RAW 文件提取内嵌预览(可下载后用专业工具打开)')\n const u = URL.createObjectURL(new Blob([jpeg], { type: 'image/jpeg' }))\n objectUrls.push(u)\n return [u]\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n missingDeps.value = null\n revoke()\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n let urls: string[]\n if (props.kind === 'tiff') urls = await decodeTiff(buf)\n else if (props.kind === 'psd') urls = await decodePsd(buf)\n else if (props.kind === 'heic') urls = await decodeHeic(buf)\n else urls = await decodeRaw(buf)\n pages.value = urls\n pageIdx.value = 0\n status.value = 'ready'\n } catch (e) {\n const dep = DEP_BY_KIND[props.kind]\n if (dep && isMissingModule(e)) missingDeps.value = dep\n else errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nconst multi = computed(() => pages.value.length > 1)\n\nwatch(() => [props.url, props.kind], load, { immediate: true })\nonBeforeUnmount(revoke)\n</script>\n\n<template>\n <div class=\"ras\">\n <p v-if=\"status === 'loading'\" class=\"ras__msg\">正在解码 {{ KIND_LABEL[props.kind] }}…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"ras__msg ras__gate\">\n <p>该图像较大({{ formatBytes(props.size) }}),将在浏览器内解码。是否继续?</p>\n <button type=\"button\" class=\"ras__btn\" @click=\"doLoad\">解码并预览</button>\n </div>\n\n <MissingDependency\n v-else-if=\"missingDeps\"\n :pkg=\"missingDeps\"\n :label=\"KIND_LABEL[props.kind]\"\n :download-url=\"props.url\"\n :download-name=\"props.name\"\n />\n\n <div v-else-if=\"status === 'error'\" class=\"ras__msg ras__msg--error\">\n <p>解码失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"ras__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <template v-else>\n <div class=\"ras__bar\">\n <span class=\"ras__badge\">{{ KIND_LABEL[props.kind] }}</span>\n <span v-if=\"props.kind === 'raw'\" class=\"ras__hint\">内嵌预览(非完整 RAW 解码)</span>\n <template v-if=\"multi\">\n <button type=\"button\" class=\"ras__nav\" :disabled=\"pageIdx === 0\" @click=\"pageIdx--\">\n ‹\n </button>\n <span class=\"ras__page\">{{ pageIdx + 1 }}/{{ pages.length }}</span>\n <button\n type=\"button\"\n class=\"ras__nav\"\n :disabled=\"pageIdx === pages.length - 1\"\n @click=\"pageIdx++\"\n >\n ›\n </button>\n </template>\n <span class=\"ras__spacer\" />\n <button\n type=\"button\"\n class=\"ras__nav ras__nav--text\"\n :class=\"{ 'ras__nav--on': !fit }\"\n @click=\"fit = !fit\"\n >\n {{ fit ? '原始大小' : '适应' }}\n </button>\n </div>\n <div class=\"ras__stage\" :class=\"{ 'ras__stage--fit': fit }\">\n <img\n class=\"ras__img\"\n :class=\"{ 'ras__img--fit': fit }\"\n :src=\"pages[pageIdx]\"\n :alt=\"props.name\"\n />\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.ras {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.ras__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.ras__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.ras__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.ras__btn {\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.ras__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.ras__bar {\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);\n font-size: 12px;\n color: var(--muted);\n}\n.ras__badge {\n padding: 2px 9px;\n border-radius: 999px;\n background: var(--surface-2);\n color: var(--accent);\n font-weight: 600;\n}\n.ras__hint {\n color: var(--faint);\n}\n.ras__spacer {\n flex: 1 1 auto;\n}\n.ras__nav {\n min-width: 26px;\n height: 26px;\n padding: 0 8px;\n font-family: inherit;\n font-size: 13px;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 6px;\n cursor: pointer;\n}\n.ras__nav:hover:not(:disabled) {\n border-color: var(--accent);\n color: var(--accent);\n}\n.ras__nav:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.ras__nav--on {\n color: var(--accent);\n border-color: var(--accent);\n background: var(--surface-2);\n}\n.ras__page {\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--faint);\n}\n\n.ras__stage {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface-2);\n}\n.ras__stage--fit {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n}\n.ras__img {\n display: block;\n}\n.ras__img--fit {\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n}\n</style>\n"],"mappings":""}
@@ -159,3 +159,5 @@ var _ = { class: "ras" }, v = {
159
159
  });
160
160
  //#endregion
161
161
  export { k as default };
162
+
163
+ //# sourceMappingURL=RasterViewer.vue_vue_type_script_setup_true_lang.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RasterViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/RasterViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { formatBytes } from '@/lib/format'\nimport { isMissingModule } from '@/lib/optionalDep'\nimport MissingDependency from '@/components/MissingDependency.vue'\n\n/**\n * Side-viewer for raster image formats the browser can't decode in a plain <img>. Fetches the bytes and\n * decodes them to a viewable raster in-browser, then shows the result (fit-to-pane, with an actual-size\n * toggle). Each decoder is DYNAMICALLY imported in its branch, so opening a TIFF never pulls the PSD/HEIC\n * code (and vice-versa).\n *\n * - tiff: UTIF (utif2) → RGBA → canvas; multi-page TIFFs get a page switcher.\n * - psd: ag-psd readPsd() → the composited canvas.\n * - heic/heif: heic2any (libheif) → a PNG blob.\n * - raw (cr2/nef/arw/dng/…): BEST EFFORT — no in-browser demosaic, so we extract the embedded JPEG preview\n * (see lib/rawPreview.ts). Usually the camera's full-size preview; a file without one can't be shown here.\n */\nconst props = defineProps<{\n url?: string\n name: string\n size?: number\n kind: 'tiff' | 'psd' | 'heic' | 'raw'\n}>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\n// Set when this kind's decoder (an optional peer) isn't installed → show the install card, not a raw error.\nconst missingDeps = ref<string[] | null>(null)\nconst pages = ref<string[]>([])\nconst pageIdx = ref(0)\nconst fit = ref(true)\nlet objectUrls: string[] = []\n\nconst SIZE_LIMIT = 80 * 1024 * 1024\n\nconst KIND_LABEL: Record<string, string> = { tiff: 'TIFF', psd: 'PSD', heic: 'HEIC', raw: 'RAW' }\n// Optional peer each kind's decoder needs (raw uses the internal lib/rawPreview — no external dep).\nconst DEP_BY_KIND: Record<string, string[]> = {\n tiff: ['utif2'],\n psd: ['ag-psd'],\n heic: ['heic2any'],\n}\n\nfunction revoke(): void {\n for (const u of objectUrls) URL.revokeObjectURL(u)\n objectUrls = []\n pages.value = []\n pageIdx.value = 0\n}\n\nfunction canvasToUrl(canvas: HTMLCanvasElement): Promise<string> {\n return new Promise((resolve, reject) => {\n canvas.toBlob((blob) => {\n if (!blob) return reject(new Error('canvas 编码失败'))\n const u = URL.createObjectURL(blob)\n objectUrls.push(u)\n resolve(u)\n }, 'image/png')\n })\n}\n\nasync function decodeTiff(buf: ArrayBuffer): Promise<string[]> {\n const UTIF = await import('utif2')\n const ifds = UTIF.decode(buf)\n const urls: string[] = []\n for (const ifd of ifds) {\n try {\n UTIF.decodeImage(buf, ifd)\n } catch {\n continue\n }\n if (!ifd.width || !ifd.height) continue\n const rgba = UTIF.toRGBA8(ifd)\n const canvas = document.createElement('canvas')\n canvas.width = ifd.width\n canvas.height = ifd.height\n const ctx = canvas.getContext('2d')\n if (!ctx) continue\n ctx.putImageData(new ImageData(new Uint8ClampedArray(rgba), ifd.width, ifd.height), 0, 0)\n urls.push(await canvasToUrl(canvas))\n }\n if (!urls.length) throw new Error('未能解码该 TIFF')\n return urls\n}\n\nasync function decodePsd(buf: ArrayBuffer): Promise<string[]> {\n const { readPsd } = await import('ag-psd')\n const psd = readPsd(buf, { skipLayerImageData: true, skipThumbnail: true })\n if (!psd.canvas) throw new Error('该 PSD 没有可显示的合成图像')\n return [await canvasToUrl(psd.canvas)]\n}\n\nasync function decodeHeic(buf: ArrayBuffer): Promise<string[]> {\n const heic2any = (await import('heic2any')).default\n const out = await heic2any({ blob: new Blob([buf]), toType: 'image/png' })\n const blob = Array.isArray(out) ? out[0] : out\n const u = URL.createObjectURL(blob)\n objectUrls.push(u)\n return [u]\n}\n\nasync function decodeRaw(buf: ArrayBuffer): Promise<string[]> {\n const { extractLargestJpeg } = await import('@/lib/rawPreview')\n const jpeg = extractLargestJpeg(buf)\n if (!jpeg) throw new Error('未能从该 RAW 文件提取内嵌预览(可下载后用专业工具打开)')\n const u = URL.createObjectURL(new Blob([jpeg], { type: 'image/jpeg' }))\n objectUrls.push(u)\n return [u]\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n missingDeps.value = null\n revoke()\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n let urls: string[]\n if (props.kind === 'tiff') urls = await decodeTiff(buf)\n else if (props.kind === 'psd') urls = await decodePsd(buf)\n else if (props.kind === 'heic') urls = await decodeHeic(buf)\n else urls = await decodeRaw(buf)\n pages.value = urls\n pageIdx.value = 0\n status.value = 'ready'\n } catch (e) {\n const dep = DEP_BY_KIND[props.kind]\n if (dep && isMissingModule(e)) missingDeps.value = dep\n else errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nconst multi = computed(() => pages.value.length > 1)\n\nwatch(() => [props.url, props.kind], load, { immediate: true })\nonBeforeUnmount(revoke)\n</script>\n\n<template>\n <div class=\"ras\">\n <p v-if=\"status === 'loading'\" class=\"ras__msg\">正在解码 {{ KIND_LABEL[props.kind] }}…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"ras__msg ras__gate\">\n <p>该图像较大({{ formatBytes(props.size) }}),将在浏览器内解码。是否继续?</p>\n <button type=\"button\" class=\"ras__btn\" @click=\"doLoad\">解码并预览</button>\n </div>\n\n <MissingDependency\n v-else-if=\"missingDeps\"\n :pkg=\"missingDeps\"\n :label=\"KIND_LABEL[props.kind]\"\n :download-url=\"props.url\"\n :download-name=\"props.name\"\n />\n\n <div v-else-if=\"status === 'error'\" class=\"ras__msg ras__msg--error\">\n <p>解码失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"ras__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <template v-else>\n <div class=\"ras__bar\">\n <span class=\"ras__badge\">{{ KIND_LABEL[props.kind] }}</span>\n <span v-if=\"props.kind === 'raw'\" class=\"ras__hint\">内嵌预览(非完整 RAW 解码)</span>\n <template v-if=\"multi\">\n <button type=\"button\" class=\"ras__nav\" :disabled=\"pageIdx === 0\" @click=\"pageIdx--\">\n ‹\n </button>\n <span class=\"ras__page\">{{ pageIdx + 1 }}/{{ pages.length }}</span>\n <button\n type=\"button\"\n class=\"ras__nav\"\n :disabled=\"pageIdx === pages.length - 1\"\n @click=\"pageIdx++\"\n >\n ›\n </button>\n </template>\n <span class=\"ras__spacer\" />\n <button\n type=\"button\"\n class=\"ras__nav ras__nav--text\"\n :class=\"{ 'ras__nav--on': !fit }\"\n @click=\"fit = !fit\"\n >\n {{ fit ? '原始大小' : '适应' }}\n </button>\n </div>\n <div class=\"ras__stage\" :class=\"{ 'ras__stage--fit': fit }\">\n <img\n class=\"ras__img\"\n :class=\"{ 'ras__img--fit': fit }\"\n :src=\"pages[pageIdx]\"\n :alt=\"props.name\"\n />\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.ras {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.ras__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.ras__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.ras__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.ras__btn {\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.ras__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.ras__bar {\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);\n font-size: 12px;\n color: var(--muted);\n}\n.ras__badge {\n padding: 2px 9px;\n border-radius: 999px;\n background: var(--surface-2);\n color: var(--accent);\n font-weight: 600;\n}\n.ras__hint {\n color: var(--faint);\n}\n.ras__spacer {\n flex: 1 1 auto;\n}\n.ras__nav {\n min-width: 26px;\n height: 26px;\n padding: 0 8px;\n font-family: inherit;\n font-size: 13px;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 6px;\n cursor: pointer;\n}\n.ras__nav:hover:not(:disabled) {\n border-color: var(--accent);\n color: var(--accent);\n}\n.ras__nav:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.ras__nav--on {\n color: var(--accent);\n border-color: var(--accent);\n background: var(--surface-2);\n}\n.ras__page {\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--faint);\n}\n\n.ras__stage {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface-2);\n}\n.ras__stage--fit {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n}\n.ras__img {\n display: block;\n}\n.ras__img--fit {\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n}\n</style>\n"],"mappings":"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"}
@@ -5,3 +5,5 @@ import t from "./RtfViewer.vue_vue_type_script_setup_true_lang.js";
5
5
  var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-be38fd1e"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
+
9
+ //# sourceMappingURL=RtfViewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RtfViewer.js","names":[],"sources":["../../src/components/RtfViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { parseRtf, rtfToHtml } from '@/lib/rtf'\n\n/**\n * Side-viewer Rich Text (`.rtf`) renderer. Fetches the document BYTES (needed for `\\'hh` charset decoding) and\n * interprets the RTF entirely in-browser via the hand-rolled {@link parseRtf} → {@link rtfToHtml} (zero deps,\n * like the eml / mht / har viewers). The produced HTML is OUR markup — every text byte escaped, only numeric\n * sizes / `#rrggbb` colors / sanitized font names emitted — so it's safe to inline with `v-html` (same posture\n * as DrawioViewer/WidgetBlock). It renders onto a white \"paper\" sheet (like DocxViewer) so the document's own\n * colors read correctly in both light and dark themes.\n *\n * Entry shape mirrors the other self-fetching viewers: a real download URL for a held attachment, or a blob URL\n * minted by an archive browser from already-extracted bytes. Large files are gated behind a confirm (all parsing\n * is in-memory) like EmlViewer.\n */\nconst props = defineProps<{\n /** Download / blob URL of the .rtf bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst html = ref('')\n\n// RTF embeds pictures as hex, so a file can balloon; gate very large ones behind a confirm (parsing is in-memory).\nconst SIZE_LIMIT = 16 * 1024 * 1024\n\nconst empty = computed(\n () => status.value === 'ready' && html.value.replace(/<[^>]+>/g, '').trim() === '',\n)\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n html.value = ''\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n html.value = rtfToHtml(parseRtf(new Uint8Array(buf)))\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 load(): void {\n // Gate oversized files behind a confirm; otherwise fetch + parse straight away.\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nwatch(() => props.url, load, { immediate: true })\nonBeforeUnmount(() => {\n html.value = ''\n})\n</script>\n\n<template>\n <div class=\"rtf\">\n <p v-if=\"status === 'loading'\" class=\"rtf__msg\">正在渲染 RTF 文档…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"rtf__msg rtf__gate\">\n <p>文件较大({{ ((props.size ?? 0) / 1024 / 1024).toFixed(1) }} MB),是否继续解析预览?</p>\n <button type=\"button\" class=\"rtf__btn\" @click=\"doLoad\">继续预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"rtf__msg rtf__msg--error\">\n <p>渲染失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"rtf__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <div v-else class=\"rtf__scroll\">\n <!-- eslint-disable-next-line vue/no-v-html — html is our own generated markup: every text byte is escaped\n and only numeric sizes / #rrggbb colors / sanitized font names are emitted (see lib/rtf.ts header) -->\n <article v-if=\"!empty\" class=\"rtf__paper\" v-html=\"html\" />\n <p v-else class=\"rtf__msg\">(此文档没有可显示的文本内容)</p>\n </div>\n </div>\n</template>\n\n<style scoped>\n.rtf {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n.rtf__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.rtf__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.rtf__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n}\n.rtf__btn {\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.rtf__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Scroll region holding the rendered document, on a neutral \"desk\" (like DocxViewer). */\n.rtf__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface-2);\n padding: 22px 16px 40px;\n}\n/* A white paper sheet so the document's own colors read correctly in both themes (fidelity over theming). */\n.rtf__paper {\n max-width: 820px;\n margin: 0 auto;\n padding: 48px 56px;\n background: #fff;\n color: #1a1a1a;\n border-radius: 2px;\n box-shadow: 0 2px 14px rgba(0, 0, 0, 0.18);\n font-family: 'Times New Roman', Times, serif;\n font-size: 15px;\n line-height: 1.5;\n}\n/* Each paragraph: preserve the document's tabs / soft line-breaks / runs of spaces; allow long words to wrap. */\n.rtf__paper :deep(p) {\n margin: 0 0 0.55em;\n white-space: pre-wrap;\n overflow-wrap: break-word;\n}\n.rtf__paper :deep(p:last-child) {\n margin-bottom: 0;\n}\n\n@media (max-width: 720px) {\n .rtf__scroll {\n padding: 12px 8px 28px;\n }\n .rtf__paper {\n padding: 28px 22px;\n }\n}\n</style>\n"],"mappings":""}
@@ -64,3 +64,5 @@ var d = { class: "rtf" }, f = {
64
64
  });
65
65
  //#endregion
66
66
  export { y as default };
67
+
68
+ //# sourceMappingURL=RtfViewer.vue_vue_type_script_setup_true_lang.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RtfViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/RtfViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { parseRtf, rtfToHtml } from '@/lib/rtf'\n\n/**\n * Side-viewer Rich Text (`.rtf`) renderer. Fetches the document BYTES (needed for `\\'hh` charset decoding) and\n * interprets the RTF entirely in-browser via the hand-rolled {@link parseRtf} → {@link rtfToHtml} (zero deps,\n * like the eml / mht / har viewers). The produced HTML is OUR markup — every text byte escaped, only numeric\n * sizes / `#rrggbb` colors / sanitized font names emitted — so it's safe to inline with `v-html` (same posture\n * as DrawioViewer/WidgetBlock). It renders onto a white \"paper\" sheet (like DocxViewer) so the document's own\n * colors read correctly in both light and dark themes.\n *\n * Entry shape mirrors the other self-fetching viewers: a real download URL for a held attachment, or a blob URL\n * minted by an archive browser from already-extracted bytes. Large files are gated behind a confirm (all parsing\n * is in-memory) like EmlViewer.\n */\nconst props = defineProps<{\n /** Download / blob URL of the .rtf bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst html = ref('')\n\n// RTF embeds pictures as hex, so a file can balloon; gate very large ones behind a confirm (parsing is in-memory).\nconst SIZE_LIMIT = 16 * 1024 * 1024\n\nconst empty = computed(\n () => status.value === 'ready' && html.value.replace(/<[^>]+>/g, '').trim() === '',\n)\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n html.value = ''\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n html.value = rtfToHtml(parseRtf(new Uint8Array(buf)))\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 load(): void {\n // Gate oversized files behind a confirm; otherwise fetch + parse straight away.\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nwatch(() => props.url, load, { immediate: true })\nonBeforeUnmount(() => {\n html.value = ''\n})\n</script>\n\n<template>\n <div class=\"rtf\">\n <p v-if=\"status === 'loading'\" class=\"rtf__msg\">正在渲染 RTF 文档…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"rtf__msg rtf__gate\">\n <p>文件较大({{ ((props.size ?? 0) / 1024 / 1024).toFixed(1) }} MB),是否继续解析预览?</p>\n <button type=\"button\" class=\"rtf__btn\" @click=\"doLoad\">继续预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"rtf__msg rtf__msg--error\">\n <p>渲染失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"rtf__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <div v-else class=\"rtf__scroll\">\n <!-- eslint-disable-next-line vue/no-v-html — html is our own generated markup: every text byte is escaped\n and only numeric sizes / #rrggbb colors / sanitized font names are emitted (see lib/rtf.ts header) -->\n <article v-if=\"!empty\" class=\"rtf__paper\" v-html=\"html\" />\n <p v-else class=\"rtf__msg\">(此文档没有可显示的文本内容)</p>\n </div>\n </div>\n</template>\n\n<style scoped>\n.rtf {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n.rtf__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.rtf__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.rtf__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n}\n.rtf__btn {\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.rtf__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Scroll region holding the rendered document, on a neutral \"desk\" (like DocxViewer). */\n.rtf__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface-2);\n padding: 22px 16px 40px;\n}\n/* A white paper sheet so the document's own colors read correctly in both themes (fidelity over theming). */\n.rtf__paper {\n max-width: 820px;\n margin: 0 auto;\n padding: 48px 56px;\n background: #fff;\n color: #1a1a1a;\n border-radius: 2px;\n box-shadow: 0 2px 14px rgba(0, 0, 0, 0.18);\n font-family: 'Times New Roman', Times, serif;\n font-size: 15px;\n line-height: 1.5;\n}\n/* Each paragraph: preserve the document's tabs / soft line-breaks / runs of spaces; allow long words to wrap. */\n.rtf__paper :deep(p) {\n margin: 0 0 0.55em;\n white-space: pre-wrap;\n overflow-wrap: break-word;\n}\n.rtf__paper :deep(p:last-child) {\n margin-bottom: 0;\n}\n\n@media (max-width: 720px) {\n .rtf__scroll {\n padding: 12px 8px 28px;\n }\n .rtf__paper {\n padding: 28px 22px;\n }\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;GA6BM,IAAa,KAAK,OAAO;;;;;;;;EAb/B,IAAM,IAAQ,GAQR,IAAS,EAAY,SAAS,GAC9B,IAAW,EAAI,EAAE,GACjB,IAAO,EAAI,EAAE,GAKb,IAAQ,QACN,EAAO,UAAU,WAAW,EAAK,MAAM,QAAQ,YAAY,EAAE,CAAC,CAAC,KAAK,MAAM,EAClF;EAEA,eAAe,IAAwB;GAChC,MAAM,KAGX;IAFA,EAAO,QAAQ,WACf,EAAS,QAAQ,IACjB,EAAK,QAAQ;IACb,IAAI;KACF,IAAM,IAAM,MAAM,MAAM,EAAM,GAAG;KACjC,IAAI,CAAC,EAAI,IAAI,MAAU,MAAM,QAAQ,EAAI,QAAQ;KACjD,IAAM,IAAM,MAAM,EAAI,YAAY;KAElC,AADA,EAAK,QAAQ,EAAU,EAAS,IAAI,WAAW,CAAG,CAAC,CAAC,GACpD,EAAO,QAAQ;IACjB,SAAS,GAAG;KAEV,AADA,EAAS,QAAQ,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,GAC1D,EAAO,QAAQ;IACjB;GAVa;EAWf;EAEA,SAAS,IAAa;GAEpB,IAAI,EAAM,QAAQ,EAAM,OAAO,GAAY;IACzC,EAAO,QAAQ;IACf;GACF;GACA,EAAY;EACd;SAEA,QAAY,EAAM,KAAK,GAAM,EAAE,WAAW,GAAK,CAAC,GAChD,QAAsB;GACpB,EAAK,QAAQ;EACf,CAAC,mBAIC,EAmBM,OAnBN,GAmBM,CAlBK,EAAA,UAAM,aAAA,EAAA,GAAf,EAAgE,KAAhE,GAAgD,cAAY,KAE5C,EAAA,UAAM,UAAA,EAAA,GAAtB,EAGM,OAHN,GAGM,CAFJ,EAA4E,KAAA,MAAzE,UAAK,IAAK,EAAM,QAAI,KAAA,OAAA,KAAA,CAAsB,QAAO,CAAA,CAAA,IAAM,kBAAc,CAAA,GACxE,EAAoE,UAAA;GAA5D,MAAK;GAAS,OAAM;GAAY,SAAO;KAAQ,MAAI,CAAA,CAAA,KAG7C,EAAA,UAAM,WAAA,EAAA,GAAtB,EAGM,OAHN,GAGM,CAFJ,EAA0B,KAAA,MAAvB,UAAK,EAAG,EAAA,KAAQ,GAAA,CAAA,GACnB,EAAkE,UAAA;GAA1D,MAAK;GAAS,OAAM;GAAY,SAAO;KAAQ,IAAE,CAAA,CAAA,MAAA,EAAA,GAG3D,EAKM,OALN,GAKM,CAFY,EAAA,cAChB,EAA8C,KAA9C,GAA2B,iBAAe,MAD1B,EAAA,GAAhB,EAA0D,WAAA;;GAAnC,OAAM;GAAa,WAAQ,EAAA;iBACR,CAAA,EAAA,CAAA"}
@@ -5,3 +5,5 @@ import t from "./SevenZipBrowser.vue_vue_type_script_setup_true_lang.js";
5
5
  var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-c8a21efa"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
+
9
+ //# sourceMappingURL=SevenZipBrowser.js.map
@@ -0,0 +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":""}
@@ -364,3 +364,5 @@ var me = { class: "sz" }, he = {
364
364
  });
365
365
  //#endregion
366
366
  export { S as default };
367
+
368
+ //# sourceMappingURL=SevenZipBrowser.vue_vue_type_script_setup_true_lang.js.map