@l1yp/file-viewer 0.1.0 → 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 (439) hide show
  1. package/README.md +2 -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 +9 -0
  47. package/dist/components/DocxNavigationPane.js.map +1 -0
  48. package/dist/components/DocxNavigationPane.vue.d.ts +23 -0
  49. package/dist/components/DocxNavigationPane.vue_vue_type_script_setup_true_lang.js +98 -0
  50. package/dist/components/DocxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -0
  51. package/dist/components/DocxViewer.js +3 -1
  52. package/dist/components/DocxViewer.js.map +1 -0
  53. package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js +72 -37
  54. package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  55. package/dist/components/DrawioViewer.js +2 -0
  56. package/dist/components/DrawioViewer.js.map +1 -0
  57. package/dist/components/DrawioViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  58. package/dist/components/DrawioViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  59. package/dist/components/DxfViewer.js +2 -0
  60. package/dist/components/DxfViewer.js.map +1 -0
  61. package/dist/components/DxfViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  62. package/dist/components/DxfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  63. package/dist/components/EbookViewer.js +2 -0
  64. package/dist/components/EbookViewer.js.map +1 -0
  65. package/dist/components/EbookViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  66. package/dist/components/EbookViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  67. package/dist/components/EmlViewer.js +2 -0
  68. package/dist/components/EmlViewer.js.map +1 -0
  69. package/dist/components/EmlViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  70. package/dist/components/EmlViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  71. package/dist/components/EplViewer.js +2 -0
  72. package/dist/components/EplViewer.js.map +1 -0
  73. package/dist/components/EplViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  74. package/dist/components/EplViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  75. package/dist/components/FileViewerBody.js +3 -1
  76. package/dist/components/FileViewerBody.js.map +1 -0
  77. package/dist/components/FileViewerBody.vue.d.ts +5 -0
  78. package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js +250 -247
  79. package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js.map +1 -0
  80. package/dist/components/FileViewerPanel.js +2 -0
  81. package/dist/components/FileViewerPanel.js.map +1 -0
  82. package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js +2 -0
  83. package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js.map +1 -0
  84. package/dist/components/FontViewer.js +2 -0
  85. package/dist/components/FontViewer.js.map +1 -0
  86. package/dist/components/FontViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  87. package/dist/components/FontViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  88. package/dist/components/FrontmatterCard.js +2 -0
  89. package/dist/components/FrontmatterCard.js.map +1 -0
  90. package/dist/components/FrontmatterCard.vue_vue_type_script_setup_true_lang.js +2 -0
  91. package/dist/components/FrontmatterCard.vue_vue_type_script_setup_true_lang.js.map +1 -0
  92. package/dist/components/GeoViewer.js +2 -0
  93. package/dist/components/GeoViewer.js.map +1 -0
  94. package/dist/components/GeoViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  95. package/dist/components/GeoViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  96. package/dist/components/HarViewer.js +2 -0
  97. package/dist/components/HarViewer.js.map +1 -0
  98. package/dist/components/HarViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  99. package/dist/components/HarViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  100. package/dist/components/HexViewer.js +2 -0
  101. package/dist/components/HexViewer.js.map +1 -0
  102. package/dist/components/HexViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  103. package/dist/components/HexViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  104. package/dist/components/HprofViewer.js +2 -0
  105. package/dist/components/HprofViewer.js.map +1 -0
  106. package/dist/components/HprofViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  107. package/dist/components/HprofViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  108. package/dist/components/ImageLightbox.js +2 -0
  109. package/dist/components/ImageLightbox.js.map +1 -0
  110. package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js +2 -0
  111. package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js.map +1 -0
  112. package/dist/components/InlineText.js +2 -0
  113. package/dist/components/InlineText.js.map +1 -0
  114. package/dist/components/JksViewer.js +2 -0
  115. package/dist/components/JksViewer.js.map +1 -0
  116. package/dist/components/JksViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  117. package/dist/components/JksViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  118. package/dist/components/JsonlViewer.js +2 -0
  119. package/dist/components/JsonlViewer.js.map +1 -0
  120. package/dist/components/JsonlViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  121. package/dist/components/JsonlViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  122. package/dist/components/LogViewer.js +2 -0
  123. package/dist/components/LogViewer.js.map +1 -0
  124. package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  125. package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  126. package/dist/components/LrcViewer.js +2 -0
  127. package/dist/components/LrcViewer.js.map +1 -0
  128. package/dist/components/LrcViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  129. package/dist/components/LrcViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  130. package/dist/components/MarkdownTable.js +2 -0
  131. package/dist/components/MarkdownTable.js.map +1 -0
  132. package/dist/components/MarkdownTable.vue_vue_type_script_setup_true_lang.js +2 -0
  133. package/dist/components/MarkdownTable.vue_vue_type_script_setup_true_lang.js.map +1 -0
  134. package/dist/components/MarkdownText.js +2 -0
  135. package/dist/components/MarkdownText.js.map +1 -0
  136. package/dist/components/MhtViewer.js +2 -0
  137. package/dist/components/MhtViewer.js.map +1 -0
  138. package/dist/components/MhtViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  139. package/dist/components/MhtViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  140. package/dist/components/MindMapView.js +2 -0
  141. package/dist/components/MindMapView.js.map +1 -0
  142. package/dist/components/MindMapView.vue_vue_type_script_setup_true_lang.js +2 -0
  143. package/dist/components/MindMapView.vue_vue_type_script_setup_true_lang.js.map +1 -0
  144. package/dist/components/MissingDependency.js +2 -0
  145. package/dist/components/MissingDependency.js.map +1 -0
  146. package/dist/components/MissingDependency.vue_vue_type_script_setup_true_lang.js +2 -0
  147. package/dist/components/MissingDependency.vue_vue_type_script_setup_true_lang.js.map +1 -0
  148. package/dist/components/ModelViewer.js +2 -0
  149. package/dist/components/ModelViewer.js.map +1 -0
  150. package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  151. package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  152. package/dist/components/MsiViewer.js +2 -0
  153. package/dist/components/MsiViewer.js.map +1 -0
  154. package/dist/components/MsiViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  155. package/dist/components/MsiViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  156. package/dist/components/NotebookViewer.js +2 -0
  157. package/dist/components/NotebookViewer.js.map +1 -0
  158. package/dist/components/NotebookViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  159. package/dist/components/NotebookViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  160. package/dist/components/PdfViewer.js +2 -0
  161. package/dist/components/PdfViewer.js.map +1 -0
  162. package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  163. package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  164. package/dist/components/PptxNavigationPane.js +9 -0
  165. package/dist/components/PptxNavigationPane.js.map +1 -0
  166. package/dist/components/PptxNavigationPane.vue.d.ts +20 -0
  167. package/dist/components/PptxNavigationPane.vue_vue_type_script_setup_true_lang.js +120 -0
  168. package/dist/components/PptxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -0
  169. package/dist/components/PptxViewer.js +3 -1
  170. package/dist/components/PptxViewer.js.map +1 -0
  171. package/dist/components/PptxViewer.vue_vue_type_script_setup_true_lang.js +71 -39
  172. package/dist/components/PptxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  173. package/dist/components/PsdViewer.js +2 -0
  174. package/dist/components/PsdViewer.js.map +1 -0
  175. package/dist/components/PsdViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  176. package/dist/components/PsdViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  177. package/dist/components/QmcAudioViewer.js +2 -0
  178. package/dist/components/QmcAudioViewer.js.map +1 -0
  179. package/dist/components/QmcAudioViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  180. package/dist/components/QmcAudioViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  181. package/dist/components/RasterViewer.js +2 -0
  182. package/dist/components/RasterViewer.js.map +1 -0
  183. package/dist/components/RasterViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  184. package/dist/components/RasterViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  185. package/dist/components/RtfViewer.js +2 -0
  186. package/dist/components/RtfViewer.js.map +1 -0
  187. package/dist/components/RtfViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  188. package/dist/components/RtfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  189. package/dist/components/SevenZipBrowser.js +2 -0
  190. package/dist/components/SevenZipBrowser.js.map +1 -0
  191. package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
  192. package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
  193. package/dist/components/SqliteViewer.js +2 -0
  194. package/dist/components/SqliteViewer.js.map +1 -0
  195. package/dist/components/SqliteViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  196. package/dist/components/SqliteViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  197. package/dist/components/SubtitleViewer.js +2 -0
  198. package/dist/components/SubtitleViewer.js.map +1 -0
  199. package/dist/components/SubtitleViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  200. package/dist/components/SubtitleViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  201. package/dist/components/XlsxChart.js +2 -0
  202. package/dist/components/XlsxChart.js.map +1 -0
  203. package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js +2 -0
  204. package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js.map +1 -0
  205. package/dist/components/XlsxViewer.js +3 -1
  206. package/dist/components/XlsxViewer.js.map +1 -0
  207. package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js +301 -304
  208. package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  209. package/dist/components/XmindViewer.js +2 -0
  210. package/dist/components/XmindViewer.js.map +1 -0
  211. package/dist/components/XmindViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  212. package/dist/components/XmindViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  213. package/dist/components/ZipBrowser.js +3 -1
  214. package/dist/components/ZipBrowser.js.map +1 -0
  215. package/dist/components/ZipBrowser.vue.d.ts +3 -1
  216. package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js +154 -122
  217. package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
  218. package/dist/components/ZoomImage.js +2 -0
  219. package/dist/components/ZoomImage.js.map +1 -0
  220. package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js +2 -0
  221. package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js.map +1 -0
  222. package/dist/components/epl/EplCodeBlock.js +2 -0
  223. package/dist/components/epl/EplCodeBlock.js.map +1 -0
  224. package/dist/components/epl/EplCodeBlock.vue_vue_type_script_setup_true_lang.js +2 -0
  225. package/dist/components/epl/EplCodeBlock.vue_vue_type_script_setup_true_lang.js.map +1 -0
  226. package/dist/components/epl/EplDoc.js +2 -0
  227. package/dist/components/epl/EplDoc.js.map +1 -0
  228. package/dist/components/epl/EplDoc.vue_vue_type_script_setup_true_lang.js +2 -0
  229. package/dist/components/epl/EplDoc.vue_vue_type_script_setup_true_lang.js.map +1 -0
  230. package/dist/components/epl/EplMethod.js +2 -0
  231. package/dist/components/epl/EplMethod.js.map +1 -0
  232. package/dist/components/epl/EplMethod.vue_vue_type_script_setup_true_lang.js +2 -0
  233. package/dist/components/epl/EplMethod.vue_vue_type_script_setup_true_lang.js.map +1 -0
  234. package/dist/components/epl/EplResource.js +2 -0
  235. package/dist/components/epl/EplResource.js.map +1 -0
  236. package/dist/components/epl/EplResource.vue_vue_type_script_setup_true_lang.js +2 -0
  237. package/dist/components/epl/EplResource.vue_vue_type_script_setup_true_lang.js.map +1 -0
  238. package/dist/components/epl/EplTree.js +2 -0
  239. package/dist/components/epl/EplTree.js.map +1 -0
  240. package/dist/components/epl/EplTree.vue_vue_type_script_setup_true_lang.js +2 -0
  241. package/dist/components/epl/EplTree.vue_vue_type_script_setup_true_lang.js.map +1 -0
  242. package/dist/components/epl/EplVarTable.js +2 -0
  243. package/dist/components/epl/EplVarTable.js.map +1 -0
  244. package/dist/components/epl/EplVarTable.vue_vue_type_script_setup_true_lang.js +2 -0
  245. package/dist/components/epl/EplVarTable.vue_vue_type_script_setup_true_lang.js.map +1 -0
  246. package/dist/components/epl/numbering.js +2 -0
  247. package/dist/components/epl/numbering.js.map +1 -0
  248. package/dist/components/icons/Icon.js +2 -0
  249. package/dist/components/icons/Icon.js.map +1 -0
  250. package/dist/components/icons/Icon.vue_vue_type_script_setup_true_lang.js +2 -0
  251. package/dist/components/icons/Icon.vue_vue_type_script_setup_true_lang.js.map +1 -0
  252. package/dist/components/icons/iconPaths.js +2 -0
  253. package/dist/components/icons/iconPaths.js.map +1 -0
  254. package/dist/components/optionalViewer.js +2 -0
  255. package/dist/components/optionalViewer.js.map +1 -0
  256. package/dist/components/ui/ClampBlock.js +2 -0
  257. package/dist/components/ui/ClampBlock.js.map +1 -0
  258. package/dist/composables/useElementNavigation.d.ts +21 -0
  259. package/dist/composables/useElementNavigation.js +63 -0
  260. package/dist/composables/useElementNavigation.js.map +1 -0
  261. package/dist/composables/useFileViewer.js +2 -0
  262. package/dist/composables/useFileViewer.js.map +1 -0
  263. package/dist/composables/useLightbox.js +2 -0
  264. package/dist/composables/useLightbox.js.map +1 -0
  265. package/dist/composables/useLogRules.js +2 -0
  266. package/dist/composables/useLogRules.js.map +1 -0
  267. package/dist/lib/aes.js +2 -0
  268. package/dist/lib/aes.js.map +1 -0
  269. package/dist/lib/appx.js +2 -0
  270. package/dist/lib/appx.js.map +1 -0
  271. package/dist/lib/archiveHtml.d.ts +49 -0
  272. package/dist/lib/archiveHtml.js +377 -0
  273. package/dist/lib/archiveHtml.js.map +1 -0
  274. package/dist/lib/asn1.js +2 -0
  275. package/dist/lib/asn1.js.map +1 -0
  276. package/dist/lib/binobj.js +2 -0
  277. package/dist/lib/binobj.js.map +1 -0
  278. package/dist/lib/brotli.js +2 -0
  279. package/dist/lib/brotli.js.map +1 -0
  280. package/dist/lib/bzip2.js +2 -0
  281. package/dist/lib/bzip2.js.map +1 -0
  282. package/dist/lib/classfile.js +2 -0
  283. package/dist/lib/classfile.js.map +1 -0
  284. package/dist/lib/codePreview.js +2 -0
  285. package/dist/lib/codePreview.js.map +1 -0
  286. package/dist/lib/codefold.js +2 -0
  287. package/dist/lib/codefold.js.map +1 -0
  288. package/dist/lib/codeformat.js +2 -0
  289. package/dist/lib/codeformat.js.map +1 -0
  290. package/dist/lib/csv.js +2 -0
  291. package/dist/lib/csv.js.map +1 -0
  292. package/dist/lib/des.js +2 -0
  293. package/dist/lib/des.js.map +1 -0
  294. package/dist/lib/diagram.js +2 -0
  295. package/dist/lib/diagram.js.map +1 -0
  296. package/dist/lib/dicom.js +2 -0
  297. package/dist/lib/dicom.js.map +1 -0
  298. package/dist/lib/dicomCodecs.js +2 -0
  299. package/dist/lib/dicomCodecs.js.map +1 -0
  300. package/dist/lib/dicomRender.js +2 -0
  301. package/dist/lib/dicomRender.js.map +1 -0
  302. package/dist/lib/docxNavigation.d.ts +31 -0
  303. package/dist/lib/docxNavigation.js +65 -0
  304. package/dist/lib/docxNavigation.js.map +1 -0
  305. package/dist/lib/dxf.js +2 -0
  306. package/dist/lib/dxf.js.map +1 -0
  307. package/dist/lib/eml.js +2 -0
  308. package/dist/lib/eml.js.map +1 -0
  309. package/dist/lib/encoding.js +2 -0
  310. package/dist/lib/encoding.js.map +1 -0
  311. package/dist/lib/epl/code/codeDataParser.js +2 -0
  312. package/dist/lib/epl/code/codeDataParser.js.map +1 -0
  313. package/dist/lib/epl/code/codeLines.js +2 -0
  314. package/dist/lib/epl/code/codeLines.js.map +1 -0
  315. package/dist/lib/epl/code/textCode.js +2 -0
  316. package/dist/lib/epl/code/textCode.js.map +1 -0
  317. package/dist/lib/epl/eplDocument.js +2 -0
  318. package/dist/lib/epl/eplDocument.js.map +1 -0
  319. package/dist/lib/epl/imageList.js +2 -0
  320. package/dist/lib/epl/imageList.js.map +1 -0
  321. package/dist/lib/epl/index.js +2 -0
  322. package/dist/lib/epl/index.js.map +1 -0
  323. package/dist/lib/epl/io/binaryReader.js +2 -0
  324. package/dist/lib/epl/io/binaryReader.js.map +1 -0
  325. package/dist/lib/epl/io/encoding.js +2 -0
  326. package/dist/lib/epl/io/encoding.js.map +1 -0
  327. package/dist/lib/epl/model/eplSystemId.js +2 -0
  328. package/dist/lib/epl/model/eplSystemId.js.map +1 -0
  329. package/dist/lib/epl/names/eplLibData.js +2 -0
  330. package/dist/lib/epl/names/eplLibData.js.map +1 -0
  331. package/dist/lib/epl/names/idToNameMap.js +2 -0
  332. package/dist/lib/epl/names/idToNameMap.js.map +1 -0
  333. package/dist/lib/epl/names/libNameProvider.js +2 -0
  334. package/dist/lib/epl/names/libNameProvider.js.map +1 -0
  335. package/dist/lib/epl/program.js +2 -0
  336. package/dist/lib/epl/program.js.map +1 -0
  337. package/dist/lib/epl/projectFileReader.js +2 -0
  338. package/dist/lib/epl/projectFileReader.js.map +1 -0
  339. package/dist/lib/fb2.js +2 -0
  340. package/dist/lib/fb2.js.map +1 -0
  341. package/dist/lib/fileType.js +2 -0
  342. package/dist/lib/fileType.js.map +1 -0
  343. package/dist/lib/fonts.js +2 -0
  344. package/dist/lib/fonts.js.map +1 -0
  345. package/dist/lib/format.js +2 -0
  346. package/dist/lib/format.js.map +1 -0
  347. package/dist/lib/frontmatter.js +2 -0
  348. package/dist/lib/frontmatter.js.map +1 -0
  349. package/dist/lib/geo.js +2 -0
  350. package/dist/lib/geo.js.map +1 -0
  351. package/dist/lib/har.js +2 -0
  352. package/dist/lib/har.js.map +1 -0
  353. package/dist/lib/highlight.js +2 -0
  354. package/dist/lib/highlight.js.map +1 -0
  355. package/dist/lib/highlightLangs.js +2 -0
  356. package/dist/lib/highlightLangs.js.map +1 -0
  357. package/dist/lib/hprof.js +2 -0
  358. package/dist/lib/hprof.js.map +1 -0
  359. package/dist/lib/ipynb.js +2 -0
  360. package/dist/lib/ipynb.js.map +1 -0
  361. package/dist/lib/jks.js +2 -0
  362. package/dist/lib/jks.js.map +1 -0
  363. package/dist/lib/jsonl.js +2 -0
  364. package/dist/lib/jsonl.js.map +1 -0
  365. package/dist/lib/lrc.js +2 -0
  366. package/dist/lib/lrc.js.map +1 -0
  367. package/dist/lib/lzma.js +2 -0
  368. package/dist/lib/lzma.js.map +1 -0
  369. package/dist/lib/markdown/blocks.js +2 -0
  370. package/dist/lib/markdown/blocks.js.map +1 -0
  371. package/dist/lib/markdown/inline.js +2 -0
  372. package/dist/lib/markdown/inline.js.map +1 -0
  373. package/dist/lib/markdown/math.js +2 -0
  374. package/dist/lib/markdown/math.js.map +1 -0
  375. package/dist/lib/markdown/segmentize.js +2 -0
  376. package/dist/lib/markdown/segmentize.js.map +1 -0
  377. package/dist/lib/markdown/styles.js +2 -0
  378. package/dist/lib/markdown/styles.js.map +1 -0
  379. package/dist/lib/mht.js +2 -0
  380. package/dist/lib/mht.js.map +1 -0
  381. package/dist/lib/midi.js +2 -0
  382. package/dist/lib/midi.js.map +1 -0
  383. package/dist/lib/mime.js +2 -0
  384. package/dist/lib/mime.js.map +1 -0
  385. package/dist/lib/mindmap.js +2 -0
  386. package/dist/lib/mindmap.js.map +1 -0
  387. package/dist/lib/mobi.js +2 -0
  388. package/dist/lib/mobi.js.map +1 -0
  389. package/dist/lib/msi.js +2 -0
  390. package/dist/lib/msi.js.map +1 -0
  391. package/dist/lib/optionalDep.js +2 -0
  392. package/dist/lib/optionalDep.js.map +1 -0
  393. package/dist/lib/pkcs12.js +2 -0
  394. package/dist/lib/pkcs12.js.map +1 -0
  395. package/dist/lib/pptxNavigation.d.ts +7 -0
  396. package/dist/lib/pptxNavigation.js +29 -0
  397. package/dist/lib/pptxNavigation.js.map +1 -0
  398. package/dist/lib/qmc.js +2 -0
  399. package/dist/lib/qmc.js.map +1 -0
  400. package/dist/lib/rar.js +2 -0
  401. package/dist/lib/rar.js.map +1 -0
  402. package/dist/lib/rawPreview.js +2 -0
  403. package/dist/lib/rawPreview.js.map +1 -0
  404. package/dist/lib/rc2.js +2 -0
  405. package/dist/lib/rc2.js.map +1 -0
  406. package/dist/lib/resolveInput.js +2 -0
  407. package/dist/lib/resolveInput.js.map +1 -0
  408. package/dist/lib/rtf.js +2 -0
  409. package/dist/lib/rtf.js.map +1 -0
  410. package/dist/lib/sevenZip.js +2 -0
  411. package/dist/lib/sevenZip.js.map +1 -0
  412. package/dist/lib/sha1.js +2 -0
  413. package/dist/lib/sha1.js.map +1 -0
  414. package/dist/lib/sha256.js +2 -0
  415. package/dist/lib/sha256.js.map +1 -0
  416. package/dist/lib/speechAudio.js +2 -0
  417. package/dist/lib/speechAudio.js.map +1 -0
  418. package/dist/lib/sqlite.js +2 -0
  419. package/dist/lib/sqlite.js.map +1 -0
  420. package/dist/lib/subtitle.js +2 -0
  421. package/dist/lib/subtitle.js.map +1 -0
  422. package/dist/lib/tarball.js +2 -0
  423. package/dist/lib/tarball.js.map +1 -0
  424. package/dist/lib/viewerFormats.js +2 -0
  425. package/dist/lib/viewerFormats.js.map +1 -0
  426. package/dist/lib/viewerKind.js +2 -0
  427. package/dist/lib/viewerKind.js.map +1 -0
  428. package/dist/lib/x509.js +2 -0
  429. package/dist/lib/x509.js.map +1 -0
  430. package/dist/lib/xlsx.js +2 -0
  431. package/dist/lib/xlsx.js.map +1 -0
  432. package/dist/lib/xlsxCharts.js +2 -0
  433. package/dist/lib/xlsxCharts.js.map +1 -0
  434. package/dist/lib/xmind.js +2 -0
  435. package/dist/lib/xmind.js.map +1 -0
  436. package/dist/lib/zip.js +2 -0
  437. package/dist/lib/zip.js.map +1 -0
  438. package/dist/style.css +1 -1
  439. package/package.json +1 -1
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SevenZipBrowser.vue_vue_type_script_setup_true_lang.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":"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"}
@@ -5,3 +5,5 @@ import t from "./SqliteViewer.vue_vue_type_script_setup_true_lang.js";
5
5
  var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-b4d65354"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
+
9
+ //# sourceMappingURL=SqliteViewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SqliteViewer.js","names":[],"sources":["../../src/components/SqliteViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport type { Database } from 'sql.js'\nimport {\n type CellValue,\n type Column,\n type QueryResult,\n type TableInfo,\n execSql,\n initSqlEngine,\n listTables,\n loadDatabase,\n readPage,\n} from '@/lib/sqlite'\nimport { formatBytes } from '@/lib/format'\n\n/**\n * Side-viewer SQLite (`.sqlite`/`.db`/`.sqlite3`) reader. Fetches the database bytes and opens them with sql.js\n * (WASM, lazily loaded — see lib/sqlite.ts), then browses it read-only: a table/view picker, a paginated data\n * grid (with PK/FK/IDX column badges), and a small SQL console for ad-hoc queries.\n *\n * Everything runs in-browser; sql.js holds the whole file in WASM memory, so very large databases are gated\n * behind a confirm before loading. The DB handle is closed on reload/unmount to free that memory. BLOB cells\n * render as a `[BLOB nB]` placeholder (never decoded into the grid).\n */\nconst props = defineProps<{\n /** Download URL of the database 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('')\n\nlet db: Database | null = null\nconst tables = ref<TableInfo[]>([])\nconst selectedName = ref('')\n\ntype Mode = 'table' | 'sql'\nconst mode = ref<Mode>('table')\n\nconst PAGE_SIZE = 100\nconst page = ref(0)\nconst pageRows = ref<CellValue[][]>([])\n\nconst sqlText = ref('')\nconst queryResult = ref<QueryResult | null>(null)\n\n// sql.js loads the entire file into WASM memory; gate big databases behind a confirm.\nconst SIZE_LIMIT = 60 * 1024 * 1024\n\nconst selectedTable = computed(\n () => tables.value.find((t) => t.name === selectedName.value) ?? null,\n)\nconst totalPages = computed(() => {\n const t = selectedTable.value\n if (!t || t.rowCount <= 0) return 1\n return Math.max(1, Math.ceil(t.rowCount / PAGE_SIZE))\n})\n\nfunction closeDb(): void {\n try {\n db?.close()\n } catch {\n /* already closed — ignore */\n }\n db = null\n}\n\n/** Reload the current page of the selected table from the DB. */\nfunction loadPage(): void {\n if (!db || !selectedTable.value) {\n pageRows.value = []\n return\n }\n try {\n pageRows.value = readPage(db, selectedName.value, PAGE_SIZE, page.value * PAGE_SIZE)\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n pageRows.value = []\n }\n}\n\nfunction selectTable(name: string): void {\n selectedName.value = name\n page.value = 0\n mode.value = 'table'\n loadPage()\n}\n\nfunction prevPage(): void {\n if (page.value > 0) {\n page.value--\n loadPage()\n }\n}\nfunction nextPage(): void {\n if (page.value < totalPages.value - 1) {\n page.value++\n loadPage()\n }\n}\n\nfunction runSql(): void {\n if (!db) return\n const sql = sqlText.value.trim()\n if (!sql) return\n queryResult.value = execSql(db, sql)\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n closeDb()\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 SQL = await initSqlEngine()\n db = loadDatabase(SQL, new Uint8Array(buf))\n tables.value = listTables(db)\n if (tables.value.length) {\n selectedName.value = tables.value[0].name\n page.value = 0\n loadPage()\n } else {\n selectedName.value = ''\n }\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n closeDb()\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(closeDb)\n\n/* ===== grid rendering: shared between table view and query results ===== */\n\n// Column descriptors for the active grid. Table mode uses the rich Column (with badges); SQL mode\n// only knows column names, so it maps them to bare descriptors.\nconst gridColumns = computed<Column[]>(() => {\n if (mode.value === 'sql') {\n return (queryResult.value?.columns ?? []).map((name) => ({ name, type: '' }))\n }\n return selectedTable.value?.columns ?? []\n})\nconst gridRows = computed<CellValue[][]>(() =>\n mode.value === 'sql' ? (queryResult.value?.rows ?? []) : pageRows.value,\n)\n\nfunction colBadge(c: Column): string {\n if (c.pk) return 'PK'\n if (c.fk) return 'FK'\n if (c.idx) return 'IDX'\n return ''\n}\nfunction cellText(v: CellValue): string {\n return v == null ? '' : String(v)\n}\n</script>\n\n<template>\n <div class=\"sq\">\n <p v-if=\"status === 'loading'\" class=\"sq__msg\">正在加载数据库…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"sq__msg sq__gate\">\n <p>该数据库较大({{ formatBytes(props.size) }}),将整体载入浏览器内存。是否继续?</p>\n <button type=\"button\" class=\"sq__btn\" @click=\"doLoad\">加载并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"sq__msg sq__msg--error\">\n <p>加载失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"sq__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <template v-else-if=\"status === 'ready'\">\n <!-- toolbar: table picker + mode toggle -->\n <div class=\"sq__bar\">\n <select\n v-if=\"tables.length\"\n class=\"sq__select\"\n :value=\"selectedName\"\n @change=\"selectTable(($event.target as HTMLSelectElement).value)\"\n >\n <option v-for=\"t in tables\" :key=\"t.name\" :value=\"t.name\">\n {{ t.kind === 'view' ? '⊞ ' : '' }}{{ t.name }} ({{ t.rowCount }})\n </option>\n </select>\n <span v-else class=\"sq__notables\">(无用户表)</span>\n <div class=\"sq__modes\">\n <button\n type=\"button\"\n class=\"sq__mode\"\n :class=\"{ 'sq__mode--on': mode === 'table' }\"\n :disabled=\"!tables.length\"\n @click=\"mode = 'table'\"\n >\n 数据\n </button>\n <button\n type=\"button\"\n class=\"sq__mode\"\n :class=\"{ 'sq__mode--on': mode === 'sql' }\"\n @click=\"mode = 'sql'\"\n >\n SQL\n </button>\n </div>\n </div>\n\n <!-- SQL console -->\n <div v-if=\"mode === 'sql'\" class=\"sq__sqlbar\">\n <textarea\n v-model=\"sqlText\"\n class=\"sq__sqlinput\"\n rows=\"3\"\n placeholder=\"输入 SQL(如 SELECT * FROM … LIMIT 100),Ctrl+Enter 执行\"\n @keydown.ctrl.enter.prevent=\"runSql\"\n @keydown.meta.enter.prevent=\"runSql\"\n />\n <div class=\"sq__sqlrun\">\n <button type=\"button\" class=\"sq__btn\" @click=\"runSql\">执行</button>\n <span v-if=\"queryResult && !queryResult.error\" class=\"sq__stat\">\n {{ queryResult.rowCount }} 行 · {{ queryResult.elapsedMs.toFixed(1) }} ms\n </span>\n <span v-else-if=\"queryResult?.error\" class=\"sq__err\">{{ queryResult.error }}</span>\n </div>\n </div>\n\n <!-- data grid (table rows or query results) -->\n <div class=\"sq__gridwrap\">\n <table v-if=\"gridColumns.length\" class=\"sq__grid\">\n <thead>\n <tr>\n <th class=\"sq__rownum\">#</th>\n <th v-for=\"(c, ci) in gridColumns\" :key=\"ci\" class=\"sq__th\">\n <span class=\"sq__colname\">{{ c.name }}</span>\n <span\n v-if=\"colBadge(c)\"\n class=\"sq__badge\"\n :class=\"`sq__badge--${colBadge(c).toLowerCase()}`\"\n >{{ colBadge(c) }}</span\n >\n <span v-if=\"c.type\" class=\"sq__coltype\">{{ c.type }}</span>\n </th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"(row, ri) in gridRows\" :key=\"ri\">\n <td class=\"sq__rownum\">\n {{ mode === 'table' ? page * PAGE_SIZE + ri + 1 : ri + 1 }}\n </td>\n <td\n v-for=\"(v, ci) in row\"\n :key=\"ci\"\n class=\"sq__td\"\n :class=\"{ 'sq__td--null': v == null }\"\n >\n {{ v == null ? 'NULL' : cellText(v) }}\n </td>\n </tr>\n <tr v-if=\"!gridRows.length\">\n <td class=\"sq__emptycell\" :colspan=\"gridColumns.length + 1\">\n {{ mode === 'sql' ? '(无结果)' : '(空表)' }}\n </td>\n </tr>\n </tbody>\n </table>\n <p v-else class=\"sq__msg\">\n {{ mode === 'sql' ? '执行一条 SQL 查看结果。' : '该表无列。' }}\n </p>\n </div>\n\n <!-- pagination (table mode only) -->\n <div v-if=\"mode === 'table' && selectedTable\" class=\"sq__pager\">\n <button type=\"button\" class=\"sq__pgbtn\" :disabled=\"page === 0\" @click=\"prevPage\">‹</button>\n <span class=\"sq__pginfo\">\n 第 {{ page + 1 }} / {{ totalPages }} 页 · 共 {{ selectedTable.rowCount }} 行\n </span>\n <button\n type=\"button\"\n class=\"sq__pgbtn\"\n :disabled=\"page >= totalPages - 1\"\n @click=\"nextPage\"\n >\n ›\n </button>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.sq {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.sq__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.sq__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.sq__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.sq__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.sq__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* toolbar */\n.sq__bar {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 12px;\n border-bottom: 1px solid var(--border);\n}\n.sq__select {\n flex: 1 1 auto;\n min-width: 0;\n padding: 6px 10px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 7px;\n}\n.sq__select:focus {\n outline: none;\n border-color: var(--accent);\n}\n.sq__notables {\n flex: 1 1 auto;\n font-size: 12.5px;\n color: var(--faint);\n}\n.sq__modes {\n display: inline-flex;\n flex-shrink: 0;\n gap: 2px;\n padding: 2px;\n border-radius: 8px;\n background: var(--surface-2);\n}\n.sq__mode {\n font-family: inherit;\n font-size: 12px;\n color: var(--muted);\n background: transparent;\n border: none;\n border-radius: 6px;\n padding: 4px 12px;\n cursor: pointer;\n}\n.sq__mode:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n.sq__mode--on {\n color: var(--accent);\n background: var(--surface);\n box-shadow: var(--shadow);\n}\n\n/* SQL console */\n.sq__sqlbar {\n flex-shrink: 0;\n padding: 10px 12px;\n border-bottom: 1px solid var(--border);\n}\n.sq__sqlinput {\n width: 100%;\n resize: vertical;\n padding: 8px 10px;\n font-family: var(--font-mono);\n font-size: 12px;\n line-height: 1.5;\n color: var(--text);\n background: var(--code-bg);\n border: 1px solid var(--border);\n border-radius: 7px;\n}\n.sq__sqlinput:focus {\n outline: none;\n border-color: var(--accent);\n}\n.sq__sqlrun {\n display: flex;\n align-items: center;\n gap: 12px;\n margin-top: 8px;\n}\n.sq__stat {\n font-size: 11.5px;\n color: var(--faint);\n}\n.sq__err {\n font-size: 11.5px;\n color: var(--danger);\n word-break: break-word;\n}\n\n/* data grid */\n.sq__gridwrap {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.sq__grid {\n border-collapse: collapse;\n width: max-content;\n min-width: 100%;\n font-size: 12px;\n}\n.sq__grid th,\n.sq__grid td {\n border-right: 1px solid var(--border);\n border-bottom: 1px solid var(--border);\n padding: 5px 10px;\n text-align: left;\n vertical-align: top;\n white-space: nowrap;\n max-width: 360px;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.sq__th {\n position: sticky;\n top: 0;\n z-index: 1;\n background: var(--surface-2);\n white-space: nowrap;\n}\n.sq__colname {\n font-weight: 600;\n color: var(--text-strong);\n}\n.sq__badge {\n margin-left: 5px;\n padding: 0 4px;\n border-radius: 3px;\n font-size: 9.5px;\n font-weight: 700;\n letter-spacing: 0.03em;\n vertical-align: middle;\n}\n.sq__badge--pk {\n color: #b08900;\n background: rgba(240, 200, 0, 0.16);\n}\n.sq__badge--fk {\n color: #1c7ed6;\n background: rgba(28, 126, 214, 0.14);\n}\n.sq__badge--idx {\n color: #2f9e44;\n background: rgba(47, 158, 68, 0.14);\n}\n.sq__coltype {\n margin-left: 5px;\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--faint);\n}\n.sq__rownum {\n position: sticky;\n left: 0;\n z-index: 1;\n width: 1%;\n text-align: right;\n color: var(--faint);\n background: var(--surface-2);\n font-family: var(--font-mono);\n font-size: 11px;\n}\n.sq__td {\n color: var(--text);\n font-family: var(--font-mono);\n font-size: 11.5px;\n}\n.sq__td--null {\n color: var(--faint);\n font-style: italic;\n}\n.sq__emptycell {\n padding: 16px;\n text-align: center;\n color: var(--faint);\n font-size: 12px;\n}\n\n/* pagination */\n.sq__pager {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 14px;\n padding: 8px 12px;\n border-top: 1px solid var(--border);\n}\n.sq__pgbtn {\n width: 30px;\n height: 26px;\n font-size: 16px;\n line-height: 1;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n}\n.sq__pgbtn:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n.sq__pgbtn:not(:disabled):hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.sq__pginfo {\n font-size: 11.5px;\n color: var(--muted);\n}\n</style>\n"],"mappings":""}
@@ -184,3 +184,5 @@ var _ = { class: "sq" }, v = {
184
184
  });
185
185
  //#endregion
186
186
  export { P as default };
187
+
188
+ //# sourceMappingURL=SqliteViewer.vue_vue_type_script_setup_true_lang.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SqliteViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/SqliteViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport type { Database } from 'sql.js'\nimport {\n type CellValue,\n type Column,\n type QueryResult,\n type TableInfo,\n execSql,\n initSqlEngine,\n listTables,\n loadDatabase,\n readPage,\n} from '@/lib/sqlite'\nimport { formatBytes } from '@/lib/format'\n\n/**\n * Side-viewer SQLite (`.sqlite`/`.db`/`.sqlite3`) reader. Fetches the database bytes and opens them with sql.js\n * (WASM, lazily loaded — see lib/sqlite.ts), then browses it read-only: a table/view picker, a paginated data\n * grid (with PK/FK/IDX column badges), and a small SQL console for ad-hoc queries.\n *\n * Everything runs in-browser; sql.js holds the whole file in WASM memory, so very large databases are gated\n * behind a confirm before loading. The DB handle is closed on reload/unmount to free that memory. BLOB cells\n * render as a `[BLOB nB]` placeholder (never decoded into the grid).\n */\nconst props = defineProps<{\n /** Download URL of the database 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('')\n\nlet db: Database | null = null\nconst tables = ref<TableInfo[]>([])\nconst selectedName = ref('')\n\ntype Mode = 'table' | 'sql'\nconst mode = ref<Mode>('table')\n\nconst PAGE_SIZE = 100\nconst page = ref(0)\nconst pageRows = ref<CellValue[][]>([])\n\nconst sqlText = ref('')\nconst queryResult = ref<QueryResult | null>(null)\n\n// sql.js loads the entire file into WASM memory; gate big databases behind a confirm.\nconst SIZE_LIMIT = 60 * 1024 * 1024\n\nconst selectedTable = computed(\n () => tables.value.find((t) => t.name === selectedName.value) ?? null,\n)\nconst totalPages = computed(() => {\n const t = selectedTable.value\n if (!t || t.rowCount <= 0) return 1\n return Math.max(1, Math.ceil(t.rowCount / PAGE_SIZE))\n})\n\nfunction closeDb(): void {\n try {\n db?.close()\n } catch {\n /* already closed — ignore */\n }\n db = null\n}\n\n/** Reload the current page of the selected table from the DB. */\nfunction loadPage(): void {\n if (!db || !selectedTable.value) {\n pageRows.value = []\n return\n }\n try {\n pageRows.value = readPage(db, selectedName.value, PAGE_SIZE, page.value * PAGE_SIZE)\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n pageRows.value = []\n }\n}\n\nfunction selectTable(name: string): void {\n selectedName.value = name\n page.value = 0\n mode.value = 'table'\n loadPage()\n}\n\nfunction prevPage(): void {\n if (page.value > 0) {\n page.value--\n loadPage()\n }\n}\nfunction nextPage(): void {\n if (page.value < totalPages.value - 1) {\n page.value++\n loadPage()\n }\n}\n\nfunction runSql(): void {\n if (!db) return\n const sql = sqlText.value.trim()\n if (!sql) return\n queryResult.value = execSql(db, sql)\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n closeDb()\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 SQL = await initSqlEngine()\n db = loadDatabase(SQL, new Uint8Array(buf))\n tables.value = listTables(db)\n if (tables.value.length) {\n selectedName.value = tables.value[0].name\n page.value = 0\n loadPage()\n } else {\n selectedName.value = ''\n }\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n closeDb()\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(closeDb)\n\n/* ===== grid rendering: shared between table view and query results ===== */\n\n// Column descriptors for the active grid. Table mode uses the rich Column (with badges); SQL mode\n// only knows column names, so it maps them to bare descriptors.\nconst gridColumns = computed<Column[]>(() => {\n if (mode.value === 'sql') {\n return (queryResult.value?.columns ?? []).map((name) => ({ name, type: '' }))\n }\n return selectedTable.value?.columns ?? []\n})\nconst gridRows = computed<CellValue[][]>(() =>\n mode.value === 'sql' ? (queryResult.value?.rows ?? []) : pageRows.value,\n)\n\nfunction colBadge(c: Column): string {\n if (c.pk) return 'PK'\n if (c.fk) return 'FK'\n if (c.idx) return 'IDX'\n return ''\n}\nfunction cellText(v: CellValue): string {\n return v == null ? '' : String(v)\n}\n</script>\n\n<template>\n <div class=\"sq\">\n <p v-if=\"status === 'loading'\" class=\"sq__msg\">正在加载数据库…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"sq__msg sq__gate\">\n <p>该数据库较大({{ formatBytes(props.size) }}),将整体载入浏览器内存。是否继续?</p>\n <button type=\"button\" class=\"sq__btn\" @click=\"doLoad\">加载并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"sq__msg sq__msg--error\">\n <p>加载失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"sq__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <template v-else-if=\"status === 'ready'\">\n <!-- toolbar: table picker + mode toggle -->\n <div class=\"sq__bar\">\n <select\n v-if=\"tables.length\"\n class=\"sq__select\"\n :value=\"selectedName\"\n @change=\"selectTable(($event.target as HTMLSelectElement).value)\"\n >\n <option v-for=\"t in tables\" :key=\"t.name\" :value=\"t.name\">\n {{ t.kind === 'view' ? '⊞ ' : '' }}{{ t.name }} ({{ t.rowCount }})\n </option>\n </select>\n <span v-else class=\"sq__notables\">(无用户表)</span>\n <div class=\"sq__modes\">\n <button\n type=\"button\"\n class=\"sq__mode\"\n :class=\"{ 'sq__mode--on': mode === 'table' }\"\n :disabled=\"!tables.length\"\n @click=\"mode = 'table'\"\n >\n 数据\n </button>\n <button\n type=\"button\"\n class=\"sq__mode\"\n :class=\"{ 'sq__mode--on': mode === 'sql' }\"\n @click=\"mode = 'sql'\"\n >\n SQL\n </button>\n </div>\n </div>\n\n <!-- SQL console -->\n <div v-if=\"mode === 'sql'\" class=\"sq__sqlbar\">\n <textarea\n v-model=\"sqlText\"\n class=\"sq__sqlinput\"\n rows=\"3\"\n placeholder=\"输入 SQL(如 SELECT * FROM … LIMIT 100),Ctrl+Enter 执行\"\n @keydown.ctrl.enter.prevent=\"runSql\"\n @keydown.meta.enter.prevent=\"runSql\"\n />\n <div class=\"sq__sqlrun\">\n <button type=\"button\" class=\"sq__btn\" @click=\"runSql\">执行</button>\n <span v-if=\"queryResult && !queryResult.error\" class=\"sq__stat\">\n {{ queryResult.rowCount }} 行 · {{ queryResult.elapsedMs.toFixed(1) }} ms\n </span>\n <span v-else-if=\"queryResult?.error\" class=\"sq__err\">{{ queryResult.error }}</span>\n </div>\n </div>\n\n <!-- data grid (table rows or query results) -->\n <div class=\"sq__gridwrap\">\n <table v-if=\"gridColumns.length\" class=\"sq__grid\">\n <thead>\n <tr>\n <th class=\"sq__rownum\">#</th>\n <th v-for=\"(c, ci) in gridColumns\" :key=\"ci\" class=\"sq__th\">\n <span class=\"sq__colname\">{{ c.name }}</span>\n <span\n v-if=\"colBadge(c)\"\n class=\"sq__badge\"\n :class=\"`sq__badge--${colBadge(c).toLowerCase()}`\"\n >{{ colBadge(c) }}</span\n >\n <span v-if=\"c.type\" class=\"sq__coltype\">{{ c.type }}</span>\n </th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"(row, ri) in gridRows\" :key=\"ri\">\n <td class=\"sq__rownum\">\n {{ mode === 'table' ? page * PAGE_SIZE + ri + 1 : ri + 1 }}\n </td>\n <td\n v-for=\"(v, ci) in row\"\n :key=\"ci\"\n class=\"sq__td\"\n :class=\"{ 'sq__td--null': v == null }\"\n >\n {{ v == null ? 'NULL' : cellText(v) }}\n </td>\n </tr>\n <tr v-if=\"!gridRows.length\">\n <td class=\"sq__emptycell\" :colspan=\"gridColumns.length + 1\">\n {{ mode === 'sql' ? '(无结果)' : '(空表)' }}\n </td>\n </tr>\n </tbody>\n </table>\n <p v-else class=\"sq__msg\">\n {{ mode === 'sql' ? '执行一条 SQL 查看结果。' : '该表无列。' }}\n </p>\n </div>\n\n <!-- pagination (table mode only) -->\n <div v-if=\"mode === 'table' && selectedTable\" class=\"sq__pager\">\n <button type=\"button\" class=\"sq__pgbtn\" :disabled=\"page === 0\" @click=\"prevPage\">‹</button>\n <span class=\"sq__pginfo\">\n 第 {{ page + 1 }} / {{ totalPages }} 页 · 共 {{ selectedTable.rowCount }} 行\n </span>\n <button\n type=\"button\"\n class=\"sq__pgbtn\"\n :disabled=\"page >= totalPages - 1\"\n @click=\"nextPage\"\n >\n ›\n </button>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.sq {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.sq__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.sq__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.sq__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.sq__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.sq__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* toolbar */\n.sq__bar {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 12px;\n border-bottom: 1px solid var(--border);\n}\n.sq__select {\n flex: 1 1 auto;\n min-width: 0;\n padding: 6px 10px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 7px;\n}\n.sq__select:focus {\n outline: none;\n border-color: var(--accent);\n}\n.sq__notables {\n flex: 1 1 auto;\n font-size: 12.5px;\n color: var(--faint);\n}\n.sq__modes {\n display: inline-flex;\n flex-shrink: 0;\n gap: 2px;\n padding: 2px;\n border-radius: 8px;\n background: var(--surface-2);\n}\n.sq__mode {\n font-family: inherit;\n font-size: 12px;\n color: var(--muted);\n background: transparent;\n border: none;\n border-radius: 6px;\n padding: 4px 12px;\n cursor: pointer;\n}\n.sq__mode:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n.sq__mode--on {\n color: var(--accent);\n background: var(--surface);\n box-shadow: var(--shadow);\n}\n\n/* SQL console */\n.sq__sqlbar {\n flex-shrink: 0;\n padding: 10px 12px;\n border-bottom: 1px solid var(--border);\n}\n.sq__sqlinput {\n width: 100%;\n resize: vertical;\n padding: 8px 10px;\n font-family: var(--font-mono);\n font-size: 12px;\n line-height: 1.5;\n color: var(--text);\n background: var(--code-bg);\n border: 1px solid var(--border);\n border-radius: 7px;\n}\n.sq__sqlinput:focus {\n outline: none;\n border-color: var(--accent);\n}\n.sq__sqlrun {\n display: flex;\n align-items: center;\n gap: 12px;\n margin-top: 8px;\n}\n.sq__stat {\n font-size: 11.5px;\n color: var(--faint);\n}\n.sq__err {\n font-size: 11.5px;\n color: var(--danger);\n word-break: break-word;\n}\n\n/* data grid */\n.sq__gridwrap {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.sq__grid {\n border-collapse: collapse;\n width: max-content;\n min-width: 100%;\n font-size: 12px;\n}\n.sq__grid th,\n.sq__grid td {\n border-right: 1px solid var(--border);\n border-bottom: 1px solid var(--border);\n padding: 5px 10px;\n text-align: left;\n vertical-align: top;\n white-space: nowrap;\n max-width: 360px;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.sq__th {\n position: sticky;\n top: 0;\n z-index: 1;\n background: var(--surface-2);\n white-space: nowrap;\n}\n.sq__colname {\n font-weight: 600;\n color: var(--text-strong);\n}\n.sq__badge {\n margin-left: 5px;\n padding: 0 4px;\n border-radius: 3px;\n font-size: 9.5px;\n font-weight: 700;\n letter-spacing: 0.03em;\n vertical-align: middle;\n}\n.sq__badge--pk {\n color: #b08900;\n background: rgba(240, 200, 0, 0.16);\n}\n.sq__badge--fk {\n color: #1c7ed6;\n background: rgba(28, 126, 214, 0.14);\n}\n.sq__badge--idx {\n color: #2f9e44;\n background: rgba(47, 158, 68, 0.14);\n}\n.sq__coltype {\n margin-left: 5px;\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--faint);\n}\n.sq__rownum {\n position: sticky;\n left: 0;\n z-index: 1;\n width: 1%;\n text-align: right;\n color: var(--faint);\n background: var(--surface-2);\n font-family: var(--font-mono);\n font-size: 11px;\n}\n.sq__td {\n color: var(--text);\n font-family: var(--font-mono);\n font-size: 11.5px;\n}\n.sq__td--null {\n color: var(--faint);\n font-style: italic;\n}\n.sq__emptycell {\n padding: 16px;\n text-align: center;\n color: var(--faint);\n font-size: 12px;\n}\n\n/* pagination */\n.sq__pager {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 14px;\n padding: 8px 12px;\n border-top: 1px solid var(--border);\n}\n.sq__pgbtn {\n width: 30px;\n height: 26px;\n font-size: 16px;\n line-height: 1;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n}\n.sq__pgbtn:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n.sq__pgbtn:not(:disabled):hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.sq__pginfo {\n font-size: 11.5px;\n color: var(--muted);\n}\n</style>\n"],"mappings":"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"}
@@ -5,3 +5,5 @@ import t from "./SubtitleViewer.vue_vue_type_script_setup_true_lang.js";
5
5
  var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-405d1c5a"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
+
9
+ //# sourceMappingURL=SubtitleViewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SubtitleViewer.js","names":[],"sources":["../../src/components/SubtitleViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { parseSubtitle } from '@/lib/subtitle'\nimport { fileExt } from '@/lib/fileType'\n\n/**\n * Side-viewer subtitle renderer. Consumes the already-fetched text the panel loaded (like MindMapView —\n * no second download) and parses it in-browser (see lib/subtitle.ts) into a uniform cue list, shown as a\n * readable transcript: each row is its time range + the caption text. SRT / WebVTT / SubStation Alpha.\n *\n * Rendering only — styling/positioning/karaoke from the source are intentionally dropped (the panel's\n * source toggle shows the raw file for anyone who needs the original markup).\n */\nconst props = defineProps<{ text: string; name: string }>()\n\nconst parsed = computed(() => parseSubtitle(props.text, fileExt(props.name)))\nconst cues = computed(() => parsed.value.cues)\nconst totalMs = computed(() => cues.value.reduce((m, c) => Math.max(m, c.end), 0))\n\nconst FORMAT_LABEL: Record<string, string> = {\n srt: 'SubRip',\n vtt: 'WebVTT',\n ass: 'SubStation Alpha',\n}\n\n// ms → \"MM:SS.mmm\" (or \"HH:MM:SS.mmm\" past an hour).\nfunction fmtTime(ms: number): string {\n const clamped = Math.max(0, ms)\n const totalSec = Math.floor(clamped / 1000)\n const h = Math.floor(totalSec / 3600)\n const m = Math.floor((totalSec % 3600) / 60)\n const s = totalSec % 60\n const mmm = Math.floor(clamped % 1000)\n const pad = (n: number, w = 2) => String(n).padStart(w, '0')\n const base = `${pad(m)}:${pad(s)}.${pad(mmm, 3)}`\n return h > 0 ? `${pad(h)}:${base}` : base\n}\n\n// Coarse \"MM:SS\" total duration for the header summary.\nfunction fmtDuration(ms: number): string {\n const totalSec = Math.round(ms / 1000)\n const h = Math.floor(totalSec / 3600)\n const m = Math.floor((totalSec % 3600) / 60)\n const s = totalSec % 60\n const pad = (n: number) => String(n).padStart(2, '0')\n return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${m}:${pad(s)}`\n}\n</script>\n\n<template>\n <div class=\"sub\">\n <div class=\"sub__head\">\n <span class=\"sub__badge\">{{ FORMAT_LABEL[parsed.format] ?? parsed.format }}</span>\n <span class=\"sub__count\">{{ cues.length }} 条字幕</span>\n <span v-if=\"totalMs > 0\" class=\"sub__dur\">时长 {{ fmtDuration(totalMs) }}</span>\n </div>\n\n <div v-if=\"cues.length\" class=\"sub__list\">\n <div v-for=\"(cue, i) in cues\" :key=\"i\" class=\"sub__cue\">\n <div class=\"sub__time\">\n <span class=\"sub__idx\">{{ i + 1 }}</span>\n <span class=\"sub__range\">{{ fmtTime(cue.start) }} → {{ fmtTime(cue.end) }}</span>\n </div>\n <div class=\"sub__body\">\n <span v-if=\"cue.speaker\" class=\"sub__speaker\">{{ cue.speaker }}</span>\n <p class=\"sub__text\">{{ cue.text }}</p>\n </div>\n </div>\n </div>\n <p v-else class=\"sub__empty\">未解析到字幕条目(可切换到「源码」查看原始内容)。</p>\n </div>\n</template>\n\n<style scoped>\n.sub {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--code-bg);\n}\n\n.sub__head {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 12px;\n flex-shrink: 0;\n padding: 10px 16px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n font-size: 12px;\n color: var(--muted);\n}\n.sub__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.sub__dur {\n color: var(--faint);\n}\n\n.sub__list {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: 8px 0 60px;\n}\n.sub__cue {\n display: flex;\n gap: 14px;\n padding: 9px 16px;\n border-bottom: 1px solid var(--border);\n}\n.sub__cue:hover {\n background: var(--surface);\n}\n.sub__time {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n gap: 3px;\n width: 132px;\n}\n.sub__idx {\n font-size: 11px;\n color: var(--faint);\n}\n.sub__range {\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--accent);\n white-space: nowrap;\n}\n.sub__body {\n flex: 1 1 auto;\n min-width: 0;\n}\n.sub__speaker {\n display: inline-block;\n margin-bottom: 2px;\n font-size: 11.5px;\n font-weight: 600;\n color: var(--muted);\n}\n.sub__text {\n margin: 0;\n font-size: 13.5px;\n line-height: 1.55;\n color: var(--text);\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.sub__empty {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n</style>\n"],"mappings":""}
@@ -46,3 +46,5 @@ var d = { class: "sub" }, f = { class: "sub__head" }, p = { class: "sub__badge"
46
46
  });
47
47
  //#endregion
48
48
  export { w as default };
49
+
50
+ //# sourceMappingURL=SubtitleViewer.vue_vue_type_script_setup_true_lang.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SubtitleViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/SubtitleViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { parseSubtitle } from '@/lib/subtitle'\nimport { fileExt } from '@/lib/fileType'\n\n/**\n * Side-viewer subtitle renderer. Consumes the already-fetched text the panel loaded (like MindMapView —\n * no second download) and parses it in-browser (see lib/subtitle.ts) into a uniform cue list, shown as a\n * readable transcript: each row is its time range + the caption text. SRT / WebVTT / SubStation Alpha.\n *\n * Rendering only — styling/positioning/karaoke from the source are intentionally dropped (the panel's\n * source toggle shows the raw file for anyone who needs the original markup).\n */\nconst props = defineProps<{ text: string; name: string }>()\n\nconst parsed = computed(() => parseSubtitle(props.text, fileExt(props.name)))\nconst cues = computed(() => parsed.value.cues)\nconst totalMs = computed(() => cues.value.reduce((m, c) => Math.max(m, c.end), 0))\n\nconst FORMAT_LABEL: Record<string, string> = {\n srt: 'SubRip',\n vtt: 'WebVTT',\n ass: 'SubStation Alpha',\n}\n\n// ms → \"MM:SS.mmm\" (or \"HH:MM:SS.mmm\" past an hour).\nfunction fmtTime(ms: number): string {\n const clamped = Math.max(0, ms)\n const totalSec = Math.floor(clamped / 1000)\n const h = Math.floor(totalSec / 3600)\n const m = Math.floor((totalSec % 3600) / 60)\n const s = totalSec % 60\n const mmm = Math.floor(clamped % 1000)\n const pad = (n: number, w = 2) => String(n).padStart(w, '0')\n const base = `${pad(m)}:${pad(s)}.${pad(mmm, 3)}`\n return h > 0 ? `${pad(h)}:${base}` : base\n}\n\n// Coarse \"MM:SS\" total duration for the header summary.\nfunction fmtDuration(ms: number): string {\n const totalSec = Math.round(ms / 1000)\n const h = Math.floor(totalSec / 3600)\n const m = Math.floor((totalSec % 3600) / 60)\n const s = totalSec % 60\n const pad = (n: number) => String(n).padStart(2, '0')\n return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${m}:${pad(s)}`\n}\n</script>\n\n<template>\n <div class=\"sub\">\n <div class=\"sub__head\">\n <span class=\"sub__badge\">{{ FORMAT_LABEL[parsed.format] ?? parsed.format }}</span>\n <span class=\"sub__count\">{{ cues.length }} 条字幕</span>\n <span v-if=\"totalMs > 0\" class=\"sub__dur\">时长 {{ fmtDuration(totalMs) }}</span>\n </div>\n\n <div v-if=\"cues.length\" class=\"sub__list\">\n <div v-for=\"(cue, i) in cues\" :key=\"i\" class=\"sub__cue\">\n <div class=\"sub__time\">\n <span class=\"sub__idx\">{{ i + 1 }}</span>\n <span class=\"sub__range\">{{ fmtTime(cue.start) }} → {{ fmtTime(cue.end) }}</span>\n </div>\n <div class=\"sub__body\">\n <span v-if=\"cue.speaker\" class=\"sub__speaker\">{{ cue.speaker }}</span>\n <p class=\"sub__text\">{{ cue.text }}</p>\n </div>\n </div>\n </div>\n <p v-else class=\"sub__empty\">未解析到字幕条目(可切换到「源码」查看原始内容)。</p>\n </div>\n</template>\n\n<style scoped>\n.sub {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--code-bg);\n}\n\n.sub__head {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 12px;\n flex-shrink: 0;\n padding: 10px 16px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n font-size: 12px;\n color: var(--muted);\n}\n.sub__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.sub__dur {\n color: var(--faint);\n}\n\n.sub__list {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: 8px 0 60px;\n}\n.sub__cue {\n display: flex;\n gap: 14px;\n padding: 9px 16px;\n border-bottom: 1px solid var(--border);\n}\n.sub__cue:hover {\n background: var(--surface);\n}\n.sub__time {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n gap: 3px;\n width: 132px;\n}\n.sub__idx {\n font-size: 11px;\n color: var(--faint);\n}\n.sub__range {\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--accent);\n white-space: nowrap;\n}\n.sub__body {\n flex: 1 1 auto;\n min-width: 0;\n}\n.sub__speaker {\n display: inline-block;\n margin-bottom: 2px;\n font-size: 11.5px;\n font-weight: 600;\n color: var(--muted);\n}\n.sub__text {\n margin: 0;\n font-size: 13.5px;\n line-height: 1.55;\n color: var(--text);\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.sub__empty {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;EAaA,IAAM,IAAQ,GAER,IAAS,QAAe,EAAc,EAAM,MAAM,EAAQ,EAAM,IAAI,CAAC,CAAC,GACtE,IAAO,QAAe,EAAO,MAAM,IAAI,GACvC,IAAU,QAAe,EAAK,MAAM,QAAQ,GAAG,MAAM,KAAK,IAAI,GAAG,EAAE,GAAG,GAAG,CAAC,CAAC,GAE3E,IAAuC;GAC3C,KAAK;GACL,KAAK;GACL,KAAK;EACP;EAGA,SAAS,EAAQ,GAAoB;GACnC,IAAM,IAAU,KAAK,IAAI,GAAG,CAAE,GACxB,IAAW,KAAK,MAAM,IAAU,GAAI,GACpC,IAAI,KAAK,MAAM,IAAW,IAAI,GAC9B,IAAI,KAAK,MAAO,IAAW,OAAQ,EAAE,GACrC,IAAI,IAAW,IACf,IAAM,KAAK,MAAM,IAAU,GAAI,GAC/B,KAAO,GAAW,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG,GACrD,IAAO,GAAG,EAAI,CAAC,EAAE,GAAG,EAAI,CAAC,EAAE,GAAG,EAAI,GAAK,CAAC;GAC9C,OAAO,IAAI,IAAI,GAAG,EAAI,CAAC,EAAE,GAAG,MAAS;EACvC;EAGA,SAAS,EAAY,GAAoB;GACvC,IAAM,IAAW,KAAK,MAAM,IAAK,GAAI,GAC/B,IAAI,KAAK,MAAM,IAAW,IAAI,GAC9B,IAAI,KAAK,MAAO,IAAW,OAAQ,EAAE,GACrC,IAAI,IAAW,IACf,KAAO,MAAc,OAAO,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG;GACpD,OAAO,IAAI,IAAI,GAAG,EAAE,GAAG,EAAI,CAAC,EAAE,GAAG,EAAI,CAAC,MAAM,GAAG,EAAE,GAAG,EAAI,CAAC;EAC3D;yBAIE,EAoBM,OApBN,GAoBM,CAnBJ,EAIM,OAJN,GAIM;GAHJ,EAAkF,QAAlF,GAAkF,EAAtD,EAAa,EAAA,MAAO,WAAW,EAAA,MAAO,MAAM,GAAA,CAAA;GACxE,EAAqD,QAArD,GAAqD,EAAzB,EAAA,MAAK,MAAM,IAAG,QAAI,CAAA;GAClC,EAAA,QAAO,KAAA,EAAA,GAAnB,EAA8E,QAA9E,GAA0C,QAAG,EAAG,EAAY,EAAA,KAAO,CAAA,GAAA,CAAA,KAAA,EAAA,IAAA,EAAA;MAG1D,EAAA,MAAK,UAAA,EAAA,GAAhB,EAWM,OAXN,GAWM,EAAA,EAAA,EAAA,GAVJ,EASM,GAAA,MAAA,EATkB,EAAA,QAAX,GAAK,YAAlB,EASM,OAAA;GATyB,KAAK;GAAG,OAAM;MAC3C,EAGM,OAHN,GAGM,CAFJ,EAAyC,QAAzC,GAAyC,EAAf,IAAC,CAAA,GAAA,CAAA,GAC3B,EAAiF,QAAjF,GAAiF,EAArD,EAAQ,EAAI,KAAK,CAAA,IAAI,QAAG,EAAG,EAAQ,EAAI,GAAG,CAAA,GAAA,CAAA,CAAA,CAAA,GAExE,EAGM,OAHN,GAGM,CAFQ,EAAI,WAAA,EAAA,GAAhB,EAAsE,QAAtE,GAAsE,EAArB,EAAI,OAAO,GAAA,CAAA,KAAA,EAAA,IAAA,EAAA,GAC5D,EAAuC,KAAvC,GAAuC,EAAf,EAAI,IAAI,GAAA,CAAA,CAAA,CAAA,CAAA,CAAA,sBAItC,EAA0D,KAA1D,GAA6B,2BAAyB,EAAA,CAAA"}