@l1yp/file-viewer 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/FileViewer.js +2 -0
- package/dist/FileViewer.js.map +1 -0
- package/dist/FileViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/FileViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/AppxViewer.js +2 -0
- package/dist/components/AppxViewer.js.map +1 -0
- package/dist/components/AppxViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/AppxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/ArchiveBrowser.js +2 -0
- package/dist/components/ArchiveBrowser.js.map +1 -0
- package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/AudioViewer.js +2 -0
- package/dist/components/AudioViewer.js.map +1 -0
- package/dist/components/AudioViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/AudioViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/BinObjBrowser.js +2 -0
- package/dist/components/BinObjBrowser.js.map +1 -0
- package/dist/components/BinObjBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/BinObjBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/CertViewer.js +2 -0
- package/dist/components/CertViewer.js.map +1 -0
- package/dist/components/CertViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/CertViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/ClassViewer.js +2 -0
- package/dist/components/ClassViewer.js.map +1 -0
- package/dist/components/ClassViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/ClassViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/CodeBlock.js +2 -0
- package/dist/components/CodeBlock.js.map +1 -0
- package/dist/components/CodeBlock.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/CodeBlock.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/CodeView.js +2 -0
- package/dist/components/CodeView.js.map +1 -0
- package/dist/components/CodeView.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/CodeView.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/CsvViewer.js +2 -0
- package/dist/components/CsvViewer.js.map +1 -0
- package/dist/components/CsvViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/CsvViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/DicomViewer.js +2 -0
- package/dist/components/DicomViewer.js.map +1 -0
- package/dist/components/DicomViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/DicomViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/DocxNavigationPane.js +2 -0
- package/dist/components/DocxNavigationPane.js.map +1 -0
- package/dist/components/DocxNavigationPane.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/DocxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/DocxViewer.js +2 -0
- package/dist/components/DocxViewer.js.map +1 -0
- package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/DrawioViewer.js +2 -0
- package/dist/components/DrawioViewer.js.map +1 -0
- package/dist/components/DrawioViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/DrawioViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/DxfViewer.js +2 -0
- package/dist/components/DxfViewer.js.map +1 -0
- package/dist/components/DxfViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/DxfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/EbookViewer.js +2 -0
- package/dist/components/EbookViewer.js.map +1 -0
- package/dist/components/EbookViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/EbookViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/EmlViewer.js +2 -0
- package/dist/components/EmlViewer.js.map +1 -0
- package/dist/components/EmlViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/EmlViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/EplViewer.js +2 -0
- package/dist/components/EplViewer.js.map +1 -0
- package/dist/components/EplViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/EplViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/FileViewerBody.js +2 -0
- package/dist/components/FileViewerBody.js.map +1 -0
- package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/FileViewerPanel.js +2 -0
- package/dist/components/FileViewerPanel.js.map +1 -0
- package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/FontViewer.js +2 -0
- package/dist/components/FontViewer.js.map +1 -0
- package/dist/components/FontViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/FontViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/FrontmatterCard.js +2 -0
- package/dist/components/FrontmatterCard.js.map +1 -0
- package/dist/components/FrontmatterCard.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/FrontmatterCard.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/GeoViewer.js +2 -0
- package/dist/components/GeoViewer.js.map +1 -0
- package/dist/components/GeoViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/GeoViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/HarViewer.js +2 -0
- package/dist/components/HarViewer.js.map +1 -0
- package/dist/components/HarViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/HarViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/HexViewer.js +2 -0
- package/dist/components/HexViewer.js.map +1 -0
- package/dist/components/HexViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/HexViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/HprofViewer.js +2 -0
- package/dist/components/HprofViewer.js.map +1 -0
- package/dist/components/HprofViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/HprofViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/ImageLightbox.js +2 -0
- package/dist/components/ImageLightbox.js.map +1 -0
- package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/InlineText.js +2 -0
- package/dist/components/InlineText.js.map +1 -0
- package/dist/components/JksViewer.js +2 -0
- package/dist/components/JksViewer.js.map +1 -0
- package/dist/components/JksViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/JksViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/JsonlViewer.js +2 -0
- package/dist/components/JsonlViewer.js.map +1 -0
- package/dist/components/JsonlViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/JsonlViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/LogViewer.js +2 -0
- package/dist/components/LogViewer.js.map +1 -0
- package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/LrcViewer.js +2 -0
- package/dist/components/LrcViewer.js.map +1 -0
- package/dist/components/LrcViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/LrcViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/MarkdownTable.js +2 -0
- package/dist/components/MarkdownTable.js.map +1 -0
- package/dist/components/MarkdownTable.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/MarkdownTable.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/MarkdownText.js +2 -0
- package/dist/components/MarkdownText.js.map +1 -0
- package/dist/components/MhtViewer.js +2 -0
- package/dist/components/MhtViewer.js.map +1 -0
- package/dist/components/MhtViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/MhtViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/MindMapView.js +2 -0
- package/dist/components/MindMapView.js.map +1 -0
- package/dist/components/MindMapView.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/MindMapView.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/MissingDependency.js +2 -0
- package/dist/components/MissingDependency.js.map +1 -0
- package/dist/components/MissingDependency.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/MissingDependency.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/ModelViewer.js +2 -0
- package/dist/components/ModelViewer.js.map +1 -0
- package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/MsiViewer.js +2 -0
- package/dist/components/MsiViewer.js.map +1 -0
- package/dist/components/MsiViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/MsiViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/NotebookViewer.js +2 -0
- package/dist/components/NotebookViewer.js.map +1 -0
- package/dist/components/NotebookViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/NotebookViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/PdfViewer.js +2 -0
- package/dist/components/PdfViewer.js.map +1 -0
- package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/PptxNavigationPane.js +2 -0
- package/dist/components/PptxNavigationPane.js.map +1 -0
- package/dist/components/PptxNavigationPane.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/PptxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/PptxViewer.js +3 -1
- package/dist/components/PptxViewer.js.map +1 -0
- package/dist/components/PptxViewer.vue_vue_type_script_setup_true_lang.js +55 -49
- package/dist/components/PptxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/PsdViewer.js +2 -0
- package/dist/components/PsdViewer.js.map +1 -0
- package/dist/components/PsdViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/PsdViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/QmcAudioViewer.js +2 -0
- package/dist/components/QmcAudioViewer.js.map +1 -0
- package/dist/components/QmcAudioViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/QmcAudioViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/RasterViewer.js +2 -0
- package/dist/components/RasterViewer.js.map +1 -0
- package/dist/components/RasterViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/RasterViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/RtfViewer.js +2 -0
- package/dist/components/RtfViewer.js.map +1 -0
- package/dist/components/RtfViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/RtfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/SevenZipBrowser.js +2 -0
- package/dist/components/SevenZipBrowser.js.map +1 -0
- package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/SqliteViewer.js +2 -0
- package/dist/components/SqliteViewer.js.map +1 -0
- package/dist/components/SqliteViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/SqliteViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/SubtitleViewer.js +2 -0
- package/dist/components/SubtitleViewer.js.map +1 -0
- package/dist/components/SubtitleViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/SubtitleViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/XlsxChart.js +2 -0
- package/dist/components/XlsxChart.js.map +1 -0
- package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/XlsxViewer.js +2 -0
- package/dist/components/XlsxViewer.js.map +1 -0
- package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/XmindViewer.js +2 -0
- package/dist/components/XmindViewer.js.map +1 -0
- package/dist/components/XmindViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/XmindViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/ZipBrowser.js +2 -0
- package/dist/components/ZipBrowser.js.map +1 -0
- package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/ZoomImage.js +2 -0
- package/dist/components/ZoomImage.js.map +1 -0
- package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/epl/EplCodeBlock.js +2 -0
- package/dist/components/epl/EplCodeBlock.js.map +1 -0
- package/dist/components/epl/EplCodeBlock.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/epl/EplCodeBlock.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/epl/EplDoc.js +2 -0
- package/dist/components/epl/EplDoc.js.map +1 -0
- package/dist/components/epl/EplDoc.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/epl/EplDoc.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/epl/EplMethod.js +2 -0
- package/dist/components/epl/EplMethod.js.map +1 -0
- package/dist/components/epl/EplMethod.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/epl/EplMethod.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/epl/EplResource.js +2 -0
- package/dist/components/epl/EplResource.js.map +1 -0
- package/dist/components/epl/EplResource.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/epl/EplResource.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/epl/EplTree.js +2 -0
- package/dist/components/epl/EplTree.js.map +1 -0
- package/dist/components/epl/EplTree.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/epl/EplTree.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/epl/EplVarTable.js +2 -0
- package/dist/components/epl/EplVarTable.js.map +1 -0
- package/dist/components/epl/EplVarTable.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/epl/EplVarTable.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/epl/numbering.js +2 -0
- package/dist/components/epl/numbering.js.map +1 -0
- package/dist/components/icons/Icon.js +2 -0
- package/dist/components/icons/Icon.js.map +1 -0
- package/dist/components/icons/Icon.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/icons/Icon.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/icons/iconPaths.js +2 -0
- package/dist/components/icons/iconPaths.js.map +1 -0
- package/dist/components/optionalViewer.js +2 -0
- package/dist/components/optionalViewer.js.map +1 -0
- package/dist/components/ui/ClampBlock.js +2 -0
- package/dist/components/ui/ClampBlock.js.map +1 -0
- package/dist/composables/useElementNavigation.js +2 -0
- package/dist/composables/useElementNavigation.js.map +1 -0
- package/dist/composables/useFileViewer.js +2 -0
- package/dist/composables/useFileViewer.js.map +1 -0
- package/dist/composables/useLightbox.js +2 -0
- package/dist/composables/useLightbox.js.map +1 -0
- package/dist/composables/useLogRules.js +2 -0
- package/dist/composables/useLogRules.js.map +1 -0
- package/dist/lib/aes.js +2 -0
- package/dist/lib/aes.js.map +1 -0
- package/dist/lib/appx.js +2 -0
- package/dist/lib/appx.js.map +1 -0
- package/dist/lib/archiveHtml.js +2 -0
- package/dist/lib/archiveHtml.js.map +1 -0
- package/dist/lib/asn1.js +2 -0
- package/dist/lib/asn1.js.map +1 -0
- package/dist/lib/binobj.js +2 -0
- package/dist/lib/binobj.js.map +1 -0
- package/dist/lib/brotli.js +2 -0
- package/dist/lib/brotli.js.map +1 -0
- package/dist/lib/bzip2.js +2 -0
- package/dist/lib/bzip2.js.map +1 -0
- package/dist/lib/classfile.js +2 -0
- package/dist/lib/classfile.js.map +1 -0
- package/dist/lib/codePreview.js +2 -0
- package/dist/lib/codePreview.js.map +1 -0
- package/dist/lib/codefold.js +2 -0
- package/dist/lib/codefold.js.map +1 -0
- package/dist/lib/codeformat.js +2 -0
- package/dist/lib/codeformat.js.map +1 -0
- package/dist/lib/csv.js +2 -0
- package/dist/lib/csv.js.map +1 -0
- package/dist/lib/des.js +2 -0
- package/dist/lib/des.js.map +1 -0
- package/dist/lib/diagram.js +2 -0
- package/dist/lib/diagram.js.map +1 -0
- package/dist/lib/dicom.js +2 -0
- package/dist/lib/dicom.js.map +1 -0
- package/dist/lib/dicomCodecs.js +2 -0
- package/dist/lib/dicomCodecs.js.map +1 -0
- package/dist/lib/dicomRender.js +2 -0
- package/dist/lib/dicomRender.js.map +1 -0
- package/dist/lib/docxNavigation.js +2 -0
- package/dist/lib/docxNavigation.js.map +1 -0
- package/dist/lib/dxf.js +2 -0
- package/dist/lib/dxf.js.map +1 -0
- package/dist/lib/eml.js +2 -0
- package/dist/lib/eml.js.map +1 -0
- package/dist/lib/encoding.js +2 -0
- package/dist/lib/encoding.js.map +1 -0
- package/dist/lib/epl/code/codeDataParser.js +2 -0
- package/dist/lib/epl/code/codeDataParser.js.map +1 -0
- package/dist/lib/epl/code/codeLines.js +2 -0
- package/dist/lib/epl/code/codeLines.js.map +1 -0
- package/dist/lib/epl/code/textCode.js +2 -0
- package/dist/lib/epl/code/textCode.js.map +1 -0
- package/dist/lib/epl/eplDocument.js +2 -0
- package/dist/lib/epl/eplDocument.js.map +1 -0
- package/dist/lib/epl/imageList.js +2 -0
- package/dist/lib/epl/imageList.js.map +1 -0
- package/dist/lib/epl/index.js +2 -0
- package/dist/lib/epl/index.js.map +1 -0
- package/dist/lib/epl/io/binaryReader.js +2 -0
- package/dist/lib/epl/io/binaryReader.js.map +1 -0
- package/dist/lib/epl/io/encoding.js +2 -0
- package/dist/lib/epl/io/encoding.js.map +1 -0
- package/dist/lib/epl/model/eplSystemId.js +2 -0
- package/dist/lib/epl/model/eplSystemId.js.map +1 -0
- package/dist/lib/epl/names/eplLibData.js +2 -0
- package/dist/lib/epl/names/eplLibData.js.map +1 -0
- package/dist/lib/epl/names/idToNameMap.js +2 -0
- package/dist/lib/epl/names/idToNameMap.js.map +1 -0
- package/dist/lib/epl/names/libNameProvider.js +2 -0
- package/dist/lib/epl/names/libNameProvider.js.map +1 -0
- package/dist/lib/epl/program.js +2 -0
- package/dist/lib/epl/program.js.map +1 -0
- package/dist/lib/epl/projectFileReader.js +2 -0
- package/dist/lib/epl/projectFileReader.js.map +1 -0
- package/dist/lib/fb2.js +2 -0
- package/dist/lib/fb2.js.map +1 -0
- package/dist/lib/fileType.js +2 -0
- package/dist/lib/fileType.js.map +1 -0
- package/dist/lib/fonts.js +2 -0
- package/dist/lib/fonts.js.map +1 -0
- package/dist/lib/format.js +2 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/frontmatter.js +2 -0
- package/dist/lib/frontmatter.js.map +1 -0
- package/dist/lib/geo.js +2 -0
- package/dist/lib/geo.js.map +1 -0
- package/dist/lib/har.js +2 -0
- package/dist/lib/har.js.map +1 -0
- package/dist/lib/highlight.js +2 -0
- package/dist/lib/highlight.js.map +1 -0
- package/dist/lib/highlightLangs.js +2 -0
- package/dist/lib/highlightLangs.js.map +1 -0
- package/dist/lib/hprof.js +2 -0
- package/dist/lib/hprof.js.map +1 -0
- package/dist/lib/ipynb.js +2 -0
- package/dist/lib/ipynb.js.map +1 -0
- package/dist/lib/jks.js +2 -0
- package/dist/lib/jks.js.map +1 -0
- package/dist/lib/jsonl.js +2 -0
- package/dist/lib/jsonl.js.map +1 -0
- package/dist/lib/lrc.js +2 -0
- package/dist/lib/lrc.js.map +1 -0
- package/dist/lib/lzma.js +2 -0
- package/dist/lib/lzma.js.map +1 -0
- package/dist/lib/markdown/blocks.js +2 -0
- package/dist/lib/markdown/blocks.js.map +1 -0
- package/dist/lib/markdown/inline.js +2 -0
- package/dist/lib/markdown/inline.js.map +1 -0
- package/dist/lib/markdown/math.js +2 -0
- package/dist/lib/markdown/math.js.map +1 -0
- package/dist/lib/markdown/segmentize.js +2 -0
- package/dist/lib/markdown/segmentize.js.map +1 -0
- package/dist/lib/markdown/styles.js +2 -0
- package/dist/lib/markdown/styles.js.map +1 -0
- package/dist/lib/mht.js +2 -0
- package/dist/lib/mht.js.map +1 -0
- package/dist/lib/midi.js +2 -0
- package/dist/lib/midi.js.map +1 -0
- package/dist/lib/mime.js +2 -0
- package/dist/lib/mime.js.map +1 -0
- package/dist/lib/mindmap.js +2 -0
- package/dist/lib/mindmap.js.map +1 -0
- package/dist/lib/mobi.js +2 -0
- package/dist/lib/mobi.js.map +1 -0
- package/dist/lib/msi.js +2 -0
- package/dist/lib/msi.js.map +1 -0
- package/dist/lib/optionalDep.js +2 -0
- package/dist/lib/optionalDep.js.map +1 -0
- package/dist/lib/pkcs12.js +2 -0
- package/dist/lib/pkcs12.js.map +1 -0
- package/dist/lib/pptxNavigation.js +2 -0
- package/dist/lib/pptxNavigation.js.map +1 -0
- package/dist/lib/qmc.js +2 -0
- package/dist/lib/qmc.js.map +1 -0
- package/dist/lib/rar.js +2 -0
- package/dist/lib/rar.js.map +1 -0
- package/dist/lib/rawPreview.js +2 -0
- package/dist/lib/rawPreview.js.map +1 -0
- package/dist/lib/rc2.js +2 -0
- package/dist/lib/rc2.js.map +1 -0
- package/dist/lib/resolveInput.js +2 -0
- package/dist/lib/resolveInput.js.map +1 -0
- package/dist/lib/rtf.js +2 -0
- package/dist/lib/rtf.js.map +1 -0
- package/dist/lib/sevenZip.js +2 -0
- package/dist/lib/sevenZip.js.map +1 -0
- package/dist/lib/sha1.js +2 -0
- package/dist/lib/sha1.js.map +1 -0
- package/dist/lib/sha256.js +2 -0
- package/dist/lib/sha256.js.map +1 -0
- package/dist/lib/speechAudio.js +2 -0
- package/dist/lib/speechAudio.js.map +1 -0
- package/dist/lib/sqlite.js +2 -0
- package/dist/lib/sqlite.js.map +1 -0
- package/dist/lib/subtitle.js +2 -0
- package/dist/lib/subtitle.js.map +1 -0
- package/dist/lib/tarball.js +2 -0
- package/dist/lib/tarball.js.map +1 -0
- package/dist/lib/viewerFormats.js +2 -0
- package/dist/lib/viewerFormats.js.map +1 -0
- package/dist/lib/viewerKind.js +2 -0
- package/dist/lib/viewerKind.js.map +1 -0
- package/dist/lib/x509.js +2 -0
- package/dist/lib/x509.js.map +1 -0
- package/dist/lib/xlsx.js +2 -0
- package/dist/lib/xlsx.js.map +1 -0
- package/dist/lib/xlsxCharts.js +2 -0
- package/dist/lib/xlsxCharts.js.map +1 -0
- package/dist/lib/xmind.js +2 -0
- package/dist/lib/xmind.js.map +1 -0
- package/dist/lib/zip.js +2 -0
- package/dist/lib/zip.js.map +1 -0
- package/dist/style.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MindMapView.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/MindMapView.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'\nimport { fileExt } from '@/lib/fileType'\nimport { parseMindMap, type MindMap, type MindNode } from '@/lib/mindmap'\n\n/**\n * The side viewer's mind-map mode: render a parsed OPML / FreeMind tree (see lib/mindmap.ts) as an interactive,\n * hand-rolled SVG mind map — no third-party graph library (consistent with the codebase's hand-rolled renderers).\n *\n * Layout is a tidy left-to-right tree (root at the left, branches growing right) — the clearest fit for a narrow\n * side panel, and what web mind-map viewers (e.g. markmap) do. FreeMind's left/right `POSITION` split is folded\n * into a single right-growing tree on purpose. Nodes wrap to a max width and pack vertically (leaves take the next\n * slot; a parent centers on its children). Click a node with children to collapse/expand; drag the background to\n * pan, wheel to zoom (around the cursor), and the toolbar zooms / fits. The XML source stays one toggle away in\n * the panel header.\n *\n * Text is sized deterministically (a CJK-aware per-char width estimate, no DOM measurement) so layout matches the\n * rendered `<tspan>`s without a measure pass — SSR-safe and reflow-free.\n */\n// `text` drives the OPML/FreeMind path (parsed here); `map` lets a caller (e.g. XmindViewer) hand in an\n// already-parsed tree so the same renderer draws XMind too.\nconst props = defineProps<{ text?: string; name: string; map?: MindMap }>()\n\n// Geometry constants (px). Exposed to the template via <script setup> top-level bindings.\nconst FONT = 13\nconst LINE_H = 19\nconst PAD_X = 11\nconst PAD_Y = 7\nconst MAX_W = 240 // max inner (text) width before wrapping\nconst MIN_W = 22\nconst H_GAP = 46 // horizontal gap between depth columns\nconst V_GAP = 14 // vertical gap between sibling subtrees\n\n// Depth-cycling palette for edges + branch accents (used when a node has no explicit color).\nconst PALETTE = [\n '#6ea8fe',\n '#63e6be',\n '#ffd43b',\n '#ff8787',\n '#da77f2',\n '#4dabf7',\n '#ffa94d',\n '#9775fa',\n]\n\nconst baseName = computed(() => {\n const n = props.name || ''\n const e = fileExt(n)\n return e ? n.slice(0, n.length - e.length - 1) : n\n})\n\n// Parse is memoized by `props.text`; while the text is unchanged the same tree (and thus node ids) is returned,\n// so the collapse Set stays valid. A parse failure is surfaced as `error`.\nconst result = computed<{ map?: MindMap; error?: string }>(() => {\n if (props.map) return { map: props.map }\n if (!props.text) return {}\n try {\n const ext = fileExt(props.name).toLowerCase()\n return { map: parseMindMap(props.text, ext === 'opml' ? 'opml' : 'mm', baseName.value) }\n } catch (e) {\n return { error: e instanceof Error ? e.message : String(e) }\n }\n})\n\n// Collapsed node ids (reactive Set — Vue tracks has/add/delete, so the layout recomputes on toggle).\nconst collapsed = reactive(new Set<number>())\n\nfunction seedFolded(n: MindNode): void {\n if (n.foldedInit && n.children.length) collapsed.add(n.id)\n for (const c of n.children) seedFolded(c)\n}\n\n// New file (or first parse): reset collapse to the authored fold state and fit the view once laid out.\nwatch(\n () => result.value.map,\n (map) => {\n collapsed.clear()\n if (map) seedFolded(map.root)\n void nextTick(fit)\n },\n { immediate: true },\n)\n\n// ---- Layout --------------------------------------------------------------------------------------------------\ninterface LaidNode {\n node: MindNode\n x: number\n y: number // vertical center\n w: number\n h: number\n depth: number\n lines: string[]\n hasChildren: boolean\n collapsed: boolean\n}\ninterface LaidEdge {\n x1: number\n y1: number\n x2: number\n y2: number\n color: string\n}\n\n// CJK / fullwidth / emoji read as ~1em; Latin as ~0.56em. Good enough to size boxes without measuring the DOM.\nfunction isWide(ch: string): boolean {\n const c = ch.codePointAt(0) ?? 0\n return (\n (c >= 0x1100 && c <= 0x115f) ||\n (c >= 0x2e80 && c <= 0xa4cf) ||\n (c >= 0xac00 && c <= 0xd7a3) ||\n (c >= 0xf900 && c <= 0xfaff) ||\n (c >= 0xfe30 && c <= 0xfe4f) ||\n (c >= 0xff00 && c <= 0xff60) ||\n (c >= 0xffe0 && c <= 0xffe6) ||\n (c >= 0x1f300 && c <= 0x1faff)\n )\n}\nfunction strW(s: string): number {\n let w = 0\n for (const ch of s) w += isWide(ch) ? FONT * 1.02 : FONT * 0.56\n return w\n}\n// Break a line into wrap units: each wide char is its own unit; Latin runs break after spaces / - / _ / /.\nfunction units(line: string): string[] {\n const out: string[] = []\n let buf = ''\n for (const ch of line) {\n if (isWide(ch)) {\n if (buf) {\n out.push(buf)\n buf = ''\n }\n out.push(ch)\n } else {\n buf += ch\n if (ch === ' ' || ch === '-' || ch === '_' || ch === '/') {\n out.push(buf)\n buf = ''\n }\n }\n }\n if (buf) out.push(buf)\n return out\n}\nfunction wrapText(text: string): { lines: string[]; width: number } {\n const src = (text || '').replace(/\\s+/g, ' ').trim() || '(无标题)'\n const lines: string[] = []\n let cur = ''\n let curW = 0\n for (const u of units(src)) {\n const uw = strW(u)\n if (uw > MAX_W) {\n // A single unit wider than the box (e.g. a long URL) — hard-break by characters.\n if (cur) {\n lines.push(cur.trimEnd())\n cur = ''\n curW = 0\n }\n let part = ''\n for (const ch of u) {\n if (strW(part + ch) > MAX_W) {\n lines.push(part)\n part = ch\n } else part += ch\n }\n cur = part\n curW = strW(part)\n continue\n }\n if (cur && curW + uw > MAX_W) {\n lines.push(cur.trimEnd())\n cur = u.trimStart()\n curW = strW(cur)\n } else {\n cur += u\n curW += uw\n }\n }\n if (cur.trim()) lines.push(cur.trimEnd())\n if (!lines.length) lines.push('(无标题)')\n let width = 0\n for (const ln of lines) width = Math.max(width, strW(ln))\n return { lines, width: Math.max(MIN_W, Math.min(MAX_W, Math.ceil(width))) }\n}\n\nconst layout = computed(() => {\n const map = result.value.map\n if (!map) return { nodes: [] as LaidNode[], edges: [] as LaidEdge[], width: 0, height: 0 }\n\n // Measure pass: text → wrapped lines + box size, pruning collapsed subtrees.\n interface M {\n node: MindNode\n lines: string[]\n w: number\n h: number\n depth: number\n children: M[]\n hasChildren: boolean\n }\n function build(node: MindNode, depth: number): M {\n const { lines, width } = wrapText(node.text)\n const isCollapsed = collapsed.has(node.id)\n const children =\n !isCollapsed && node.children.length ? node.children.map((c) => build(c, depth + 1)) : []\n return {\n node,\n lines,\n w: width + PAD_X * 2,\n h: lines.length * LINE_H + PAD_Y * 2,\n depth,\n children,\n hasChildren: node.children.length > 0,\n }\n }\n const rootM = build(map.root, 0)\n\n // Column x: each depth starts after the previous column's widest box.\n const colW: number[] = []\n ;(function scan(m: M) {\n colW[m.depth] = Math.max(colW[m.depth] ?? 0, m.w)\n m.children.forEach(scan)\n })(rootM)\n const colX: number[] = [0]\n for (let d = 1; d < colW.length; d++) colX[d] = colX[d - 1] + (colW[d - 1] ?? 0) + H_GAP\n\n // Vertical packing: leaves take the running cursor; parents center on their children.\n const nodes: LaidNode[] = []\n const edges: LaidEdge[] = []\n let cursor = 0\n function place(m: M): number {\n const x = colX[m.depth]\n let y: number\n if (m.children.length === 0) {\n y = cursor + m.h / 2\n cursor += m.h + V_GAP\n } else {\n const ys = m.children.map(place)\n y = (ys[0] + ys[ys.length - 1]) / 2\n for (let i = 0; i < m.children.length; i++) {\n const cm = m.children[i]\n edges.push({\n x1: x + m.w,\n y1: y,\n x2: colX[cm.depth],\n y2: ys[i],\n color: cm.node.color || cm.node.bg || PALETTE[cm.depth % PALETTE.length],\n })\n }\n }\n nodes.push({\n node: m.node,\n x,\n y,\n w: m.w,\n h: m.h,\n depth: m.depth,\n lines: m.lines,\n hasChildren: m.hasChildren,\n collapsed: collapsed.has(m.node.id),\n })\n return y\n }\n place(rootM)\n\n let width = 0\n let height = 0\n for (const n of nodes) {\n width = Math.max(width, n.x + n.w)\n height = Math.max(height, n.y + n.h / 2)\n }\n return { nodes, edges, width, height }\n})\n\nfunction edgePath(e: LaidEdge): string {\n const dx = Math.max(18, (e.x2 - e.x1) * 0.5)\n return `M${e.x1},${e.y1} C${e.x1 + dx},${e.y1} ${e.x2 - dx},${e.y2} ${e.x2},${e.y2}`\n}\nfunction boxStyle(n: LaidNode): Record<string, string> {\n const s: Record<string, string> = {}\n if (n.node.bg) s.fill = n.node.bg\n if (n.node.color) s.stroke = n.node.color\n return s\n}\nfunction textStyle(n: LaidNode): Record<string, string> {\n return n.node.color ? { fill: n.node.color } : {}\n}\n\n// ---- Pan / zoom ----------------------------------------------------------------------------------------------\nconst viewport = ref<HTMLElement>()\nconst tx = ref(0)\nconst ty = ref(0)\nconst k = ref(1)\n\nfunction clamp(v: number, lo: number, hi: number): number {\n return Math.min(hi, Math.max(lo, v))\n}\n\n// Fit the laid-out content into the viewport (centered, a touch of padding), capped so tiny maps aren't huge.\nfunction fit(): void {\n const el = viewport.value\n const L = layout.value\n if (!el || !L.nodes.length) return\n const pad = 24\n const cw = el.clientWidth || 1\n const ch = el.clientHeight || 1\n const scale = clamp(\n Math.min((cw - pad * 2) / Math.max(L.width, 1), (ch - pad * 2) / Math.max(L.height, 1)),\n 0.2,\n 1.25,\n )\n k.value = scale\n tx.value = (cw - L.width * scale) / 2\n ty.value = (ch - L.height * scale) / 2\n}\n\nfunction zoomAround(cx: number, cy: number, factor: number): void {\n const nk = clamp(k.value * factor, 0.1, 4)\n const wx = (cx - tx.value) / k.value\n const wy = (cy - ty.value) / k.value\n tx.value = cx - wx * nk\n ty.value = cy - wy * nk\n k.value = nk\n}\nfunction zoomBy(factor: number): void {\n const el = viewport.value\n if (!el) return\n zoomAround(el.clientWidth / 2, el.clientHeight / 2, factor)\n}\nfunction onWheel(e: WheelEvent): void {\n e.preventDefault()\n const el = viewport.value\n if (!el) return\n const r = el.getBoundingClientRect()\n zoomAround(e.clientX - r.left, e.clientY - r.top, Math.exp(-e.deltaY * 0.0015))\n}\n\n// Pan only when the drag starts on empty canvas, so a click that lands on a node toggles it instead of panning.\nlet panning = false\nlet sx = 0\nlet sy = 0\nlet stx = 0\nlet sty = 0\nfunction onBgPointerDown(e: PointerEvent): void {\n if (e.button !== 0) return\n if ((e.target as Element)?.closest('[data-node]')) return\n panning = true\n sx = e.clientX\n sy = e.clientY\n stx = tx.value\n sty = ty.value\n ;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)\n}\nfunction onPointerMove(e: PointerEvent): void {\n if (!panning) return\n tx.value = stx + (e.clientX - sx)\n ty.value = sty + (e.clientY - sy)\n}\nfunction onPointerUp(e: PointerEvent): void {\n if (!panning) return\n panning = false\n try {\n ;(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId)\n } catch {\n /* capture already released */\n }\n}\n\nfunction toggle(n: LaidNode): void {\n if (!n.hasChildren) return\n if (collapsed.has(n.node.id)) collapsed.delete(n.node.id)\n else collapsed.add(n.node.id)\n}\n\nconst summary = computed(() => {\n const map = result.value.map\n if (!map) return ''\n const fmt = map.format === 'opml' ? 'OPML' : map.format === 'xmind' ? 'XMind' : 'FreeMind'\n return `${fmt} · ${map.count} 个节点`\n})\n\n// Refit when the panel is resized (the splitter drag changes our width).\nlet ro: ResizeObserver | undefined\nonMounted(() => {\n fit()\n if (typeof ResizeObserver !== 'undefined' && viewport.value) {\n ro = new ResizeObserver(() => fit())\n ro.observe(viewport.value)\n }\n})\nonBeforeUnmount(() => ro?.disconnect())\n</script>\n\n<template>\n <div class=\"mm\">\n <p v-if=\"result.error\" class=\"mm__msg mm__msg--error\">{{ result.error }}</p>\n <p v-else-if=\"!layout.nodes.length\" class=\"mm__msg\">空的思维导图</p>\n\n <template v-else>\n <div\n ref=\"viewport\"\n class=\"mm__canvas\"\n :class=\"{ 'mm__canvas--panning': panning }\"\n @wheel=\"onWheel\"\n @pointerdown=\"onBgPointerDown\"\n @pointermove=\"onPointerMove\"\n @pointerup=\"onPointerUp\"\n @pointerleave=\"onPointerUp\"\n >\n <svg class=\"mm__svg\" width=\"100%\" height=\"100%\">\n <g :transform=\"`translate(${tx},${ty}) scale(${k})`\">\n <path\n v-for=\"(e, i) in layout.edges\"\n :key=\"'e' + i\"\n class=\"mm__edge\"\n :d=\"edgePath(e)\"\n :stroke=\"e.color\"\n />\n <g\n v-for=\"n in layout.nodes\"\n :key=\"n.node.id\"\n data-node\n :transform=\"`translate(${n.x},${n.y - n.h / 2})`\"\n :class=\"{ 'mm__node--clickable': n.hasChildren }\"\n >\n <rect\n class=\"mm__box\"\n :class=\"{\n 'mm__box--root': n.depth === 0,\n 'mm__box--branch': n.depth > 0 && n.hasChildren,\n }\"\n :width=\"n.w\"\n :height=\"n.h\"\n rx=\"8\"\n :style=\"boxStyle(n)\"\n @click=\"toggle(n)\"\n />\n <!-- each line is centered in its LINE_H band (dominant-baseline:central) → the whole block,\n box height = lines*LINE_H + 2*PAD_Y, sits vertically centered without a measure pass -->\n <text class=\"mm__text\" :style=\"textStyle(n)\" @click=\"toggle(n)\">\n <tspan\n v-for=\"(ln, li) in n.lines\"\n :key=\"li\"\n :x=\"PAD_X\"\n :y=\"PAD_Y + li * LINE_H + LINE_H / 2\"\n >\n {{ ln }}\n </tspan>\n </text>\n <title v-if=\"n.node.note\">{{ n.node.note }}</title>\n\n <!-- collapse/expand pip on the right edge (only when the node has children).\n The +/− are drawn as stroked lines (not glyphs) so they stay perfectly centered in the\n circle regardless of font metrics. -->\n <g v-if=\"n.hasChildren\" class=\"mm__pip\" @click.stop=\"toggle(n)\">\n <circle :cx=\"n.w\" :cy=\"n.h / 2\" r=\"7.5\" />\n <line class=\"mm__pipsign\" :x1=\"n.w - 4\" :y1=\"n.h / 2\" :x2=\"n.w + 4\" :y2=\"n.h / 2\" />\n <line\n v-if=\"n.collapsed\"\n class=\"mm__pipsign\"\n :x1=\"n.w\"\n :y1=\"n.h / 2 - 4\"\n :x2=\"n.w\"\n :y2=\"n.h / 2 + 4\"\n />\n <text\n v-if=\"n.collapsed\"\n class=\"mm__pipcount\"\n :x=\"n.w + 12\"\n :y=\"n.h / 2\"\n dominant-baseline=\"central\"\n >\n {{ n.node.children.length }}\n </text>\n </g>\n\n <!-- external link glyph (top-right), opens in a new tab without triggering the toggle -->\n <a\n v-if=\"n.node.link\"\n :href=\"n.node.link\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n @click.stop\n @pointerdown.stop\n >\n <text class=\"mm__link\" :x=\"n.w - 5\" y=\"-3\" text-anchor=\"end\">↗</text>\n </a>\n </g>\n </g>\n </svg>\n </div>\n\n <div class=\"mm__toolbar\">\n <span class=\"mm__summary\">{{ summary }}</span>\n <span class=\"mm__zoom\">\n <button type=\"button\" class=\"mm__tbtn\" title=\"放大\" @click=\"zoomBy(1.2)\">+</button>\n <button type=\"button\" class=\"mm__tbtn\" title=\"缩小\" @click=\"zoomBy(1 / 1.2)\">−</button>\n <button type=\"button\" class=\"mm__tbtn mm__tbtn--text\" title=\"适应窗口\" @click=\"fit\">\n 适应\n </button>\n </span>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.mm {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface-2);\n}\n.mm__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.mm__msg--error {\n color: var(--danger);\n}\n\n.mm__canvas {\n flex: 1 1 auto;\n min-height: 0;\n overflow: hidden;\n cursor: grab;\n touch-action: none;\n}\n.mm__canvas--panning {\n cursor: grabbing;\n}\n.mm__svg {\n display: block;\n width: 100%;\n height: 100%;\n}\n\n/* Edges: stroke color comes from the node/depth palette (inline `stroke`); width/fill fixed here. */\n.mm__edge {\n fill: none;\n stroke-width: 2;\n stroke-opacity: 0.55;\n}\n\n.mm__node--clickable .mm__box,\n.mm__node--clickable .mm__text {\n cursor: pointer;\n}\n.mm__box {\n fill: var(--surface);\n stroke: var(--border-2);\n stroke-width: 1.2;\n transition: filter 0.12s;\n}\n.mm__node--clickable:hover .mm__box {\n filter: brightness(1.04);\n stroke: var(--accent);\n}\n.mm__box--root {\n fill: var(--accent);\n stroke: var(--accent);\n}\n.mm__box--branch {\n stroke-width: 1.5;\n}\n.mm__text {\n font-family: var(--font-sans, inherit);\n font-size: 13px;\n fill: var(--text);\n dominant-baseline: central;\n}\n/* Root box is filled with the accent → its label must read on that fill. */\n.mm__box--root + .mm__text {\n fill: #fff;\n font-weight: 600;\n}\n\n.mm__pip {\n cursor: pointer;\n}\n.mm__pip circle {\n fill: var(--surface);\n stroke: var(--border-2);\n stroke-width: 1.2;\n}\n.mm__pip:hover circle {\n stroke: var(--accent);\n}\n.mm__pipsign {\n stroke: var(--muted);\n stroke-width: 1.6;\n stroke-linecap: round;\n}\n.mm__pip:hover .mm__pipsign {\n stroke: var(--accent);\n}\n.mm__pipcount {\n font-family: var(--font-mono, monospace);\n font-size: 10.5px;\n fill: var(--faint);\n user-select: none;\n}\n\n.mm__link {\n font-size: 12px;\n fill: var(--accent);\n cursor: pointer;\n}\n\n/* Floating toolbar, bottom-right of the canvas. */\n.mm__toolbar {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n padding: 7px 12px;\n background: color-mix(in srgb, var(--surface) 86%, transparent);\n border-top: 1px solid var(--border);\n backdrop-filter: blur(4px);\n}\n.mm__summary {\n font-size: 11.5px;\n letter-spacing: 0.02em;\n color: var(--faint);\n}\n.mm__zoom {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n}\n.mm__tbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 28px;\n height: 26px;\n padding: 0 6px;\n font-family: inherit;\n font-size: 14px;\n line-height: 1;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 7px;\n cursor: pointer;\n transition:\n background 0.15s,\n color 0.15s,\n border-color 0.15s;\n}\n.mm__tbtn:hover {\n color: var(--accent);\n border-color: var(--accent);\n}\n.mm__tbtn--text {\n font-size: 12px;\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MissingDependency.js","names":[],"sources":["../../src/components/MissingDependency.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\n\n/**\n * Shown when an OPTIONAL peer dependency for a format isn't installed in the consumer's app (see\n * docs/file-viewer-package-design.md §4). Tells the user which package to install to enable this viewer,\n * offers a one-click copy of the `npm i …` command, and keeps the file downloadable in the meantime.\n */\nconst props = defineProps<{\n /** The missing npm package name(s). */\n pkg: string | string[]\n /** Human label for the format (e.g. \"PDF\", \"Word 文档\"). */\n label?: string\n /** Download URL for the raw file, so it stays reachable even without a viewer. */\n downloadUrl?: string\n downloadName?: string\n}>()\n\nconst pkgs = computed(() => (Array.isArray(props.pkg) ? props.pkg : [props.pkg]))\nconst installCmd = computed(() => `npm i ${pkgs.value.join(' ')}`)\n\nconst copied = ref(false)\nlet timer: ReturnType<typeof setTimeout> | undefined\nasync function copyCmd(): Promise<void> {\n try {\n await navigator.clipboard?.writeText(installCmd.value)\n copied.value = true\n clearTimeout(timer)\n timer = setTimeout(() => (copied.value = false), 1400)\n } catch {\n /* clipboard unavailable — ignore */\n }\n}\n</script>\n\n<template>\n <div class=\"missdep\">\n <div class=\"missdep__icon\" aria-hidden=\"true\">\n <svg\n viewBox=\"0 0 24 24\"\n width=\"34\"\n height=\"34\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.6\"\n >\n <path\n d=\"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z\"\n />\n <path d=\"M12 8v4\" stroke-linecap=\"round\" />\n <path d=\"M12 16h.01\" stroke-linecap=\"round\" />\n </svg>\n </div>\n <p class=\"missdep__title\">预览{{ label ? ` ${label} ` : '此格式' }}需要额外依赖</p>\n <p class=\"missdep__sub\">该格式查看器依赖以下未安装的 npm 包,请在你的项目中安装后重试:</p>\n <div class=\"missdep__cmd\">\n <code>{{ installCmd }}</code>\n <button type=\"button\" class=\"missdep__copy\" @click=\"copyCmd\">\n {{ copied ? '已复制' : '复制' }}\n </button>\n </div>\n <a v-if=\"downloadUrl\" class=\"missdep__dl\" :href=\"downloadUrl\" :download=\"downloadName\"\n >下载原文件</a\n >\n </div>\n</template>\n\n<style scoped>\n.missdep {\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: 8px;\n padding: 32px;\n text-align: center;\n color: var(--faint);\n}\n.missdep__icon {\n color: var(--faint);\n}\n.missdep__title {\n margin: 4px 0 0;\n font-size: 14px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.missdep__sub {\n margin: 0;\n max-width: 340px;\n font-size: 12.5px;\n line-height: 1.6;\n color: var(--muted);\n}\n.missdep__cmd {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-top: 6px;\n padding: 7px 8px 7px 12px;\n background: var(--code-bg);\n border: 1px solid var(--code-border);\n border-radius: 8px;\n}\n.missdep__cmd code {\n font-family: var(--font-mono);\n font-size: 12.5px;\n color: var(--text);\n}\n.missdep__copy {\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: 6px;\n padding: 4px 10px;\n cursor: pointer;\n}\n.missdep__copy:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.missdep__dl {\n margin-top: 10px;\n font-size: 12.5px;\n color: var(--muted);\n text-decoration: underline;\n text-underline-offset: 3px;\n}\n.missdep__dl:hover {\n color: var(--accent);\n}\n</style>\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MissingDependency.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/MissingDependency.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\n\n/**\n * Shown when an OPTIONAL peer dependency for a format isn't installed in the consumer's app (see\n * docs/file-viewer-package-design.md §4). Tells the user which package to install to enable this viewer,\n * offers a one-click copy of the `npm i …` command, and keeps the file downloadable in the meantime.\n */\nconst props = defineProps<{\n /** The missing npm package name(s). */\n pkg: string | string[]\n /** Human label for the format (e.g. \"PDF\", \"Word 文档\"). */\n label?: string\n /** Download URL for the raw file, so it stays reachable even without a viewer. */\n downloadUrl?: string\n downloadName?: string\n}>()\n\nconst pkgs = computed(() => (Array.isArray(props.pkg) ? props.pkg : [props.pkg]))\nconst installCmd = computed(() => `npm i ${pkgs.value.join(' ')}`)\n\nconst copied = ref(false)\nlet timer: ReturnType<typeof setTimeout> | undefined\nasync function copyCmd(): Promise<void> {\n try {\n await navigator.clipboard?.writeText(installCmd.value)\n copied.value = true\n clearTimeout(timer)\n timer = setTimeout(() => (copied.value = false), 1400)\n } catch {\n /* clipboard unavailable — ignore */\n }\n}\n</script>\n\n<template>\n <div class=\"missdep\">\n <div class=\"missdep__icon\" aria-hidden=\"true\">\n <svg\n viewBox=\"0 0 24 24\"\n width=\"34\"\n height=\"34\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.6\"\n >\n <path\n d=\"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z\"\n />\n <path d=\"M12 8v4\" stroke-linecap=\"round\" />\n <path d=\"M12 16h.01\" stroke-linecap=\"round\" />\n </svg>\n </div>\n <p class=\"missdep__title\">预览{{ label ? ` ${label} ` : '此格式' }}需要额外依赖</p>\n <p class=\"missdep__sub\">该格式查看器依赖以下未安装的 npm 包,请在你的项目中安装后重试:</p>\n <div class=\"missdep__cmd\">\n <code>{{ installCmd }}</code>\n <button type=\"button\" class=\"missdep__copy\" @click=\"copyCmd\">\n {{ copied ? '已复制' : '复制' }}\n </button>\n </div>\n <a v-if=\"downloadUrl\" class=\"missdep__dl\" :href=\"downloadUrl\" :download=\"downloadName\"\n >下载原文件</a\n >\n </div>\n</template>\n\n<style scoped>\n.missdep {\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: 8px;\n padding: 32px;\n text-align: center;\n color: var(--faint);\n}\n.missdep__icon {\n color: var(--faint);\n}\n.missdep__title {\n margin: 4px 0 0;\n font-size: 14px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.missdep__sub {\n margin: 0;\n max-width: 340px;\n font-size: 12.5px;\n line-height: 1.6;\n color: var(--muted);\n}\n.missdep__cmd {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-top: 6px;\n padding: 7px 8px 7px 12px;\n background: var(--code-bg);\n border: 1px solid var(--code-border);\n border-radius: 8px;\n}\n.missdep__cmd code {\n font-family: var(--font-mono);\n font-size: 12.5px;\n color: var(--text);\n}\n.missdep__copy {\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: 6px;\n padding: 4px 10px;\n cursor: pointer;\n}\n.missdep__copy:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.missdep__dl {\n margin-top: 10px;\n font-size: 12.5px;\n color: var(--muted);\n text-decoration: underline;\n text-underline-offset: 3px;\n}\n.missdep__dl:hover {\n color: var(--accent);\n}\n</style>\n"],"mappings":";;;;;;;;;;;EAQA,IAAM,IAAQ,GAUR,IAAO,QAAgB,MAAM,QAAQ,EAAM,GAAG,IAAI,EAAM,MAAM,CAAC,EAAM,GAAG,CAAE,GAC1E,IAAa,QAAe,SAAS,EAAK,MAAM,KAAK,GAAG,GAAG,GAE3D,IAAS,EAAI,EAAK,GACpB;EACJ,eAAe,IAAyB;GACtC,IAAI;IAIF,AAHA,MAAM,UAAU,WAAW,UAAU,EAAW,KAAK,GACrD,EAAO,QAAQ,IACf,aAAa,CAAK,GAClB,IAAQ,iBAAkB,EAAO,QAAQ,IAAQ,IAAI;GACvD,QAAQ,CAER;EACF;yBAIE,EA4BM,OA5BN,GA4BM;;GAXJ,EAAwE,KAAxE,GAA0B,OAAE,EAAG,EAAA,QAAK,IAAO,EAAA,MAAK,KAAA,KAAA,IAAc,UAAM,CAAA;YACpE,EAA8D,KAAA,EAA3D,OAAM,eAAc,GAAC,sCAAkC,EAAA;GAC1D,EAKM,OALN,GAKM,CAJJ,EAA6B,QAAA,MAAA,EAApB,EAAA,KAAU,GAAA,CAAA,GACnB,EAES,UAAA;IAFD,MAAK;IAAS,OAAM;IAAiB,SAAO;QAC/C,EAAA,QAAM,QAAA,IAAA,GAAA,CAAA,CAAA,CAAA;GAGJ,EAAA,eAAA,EAAA,GAAT,EAEC,KAAA;;IAFqB,OAAM;IAAe,MAAM,EAAA;IAAc,UAAU,EAAA;MACtE,SAAK,GAAA,CAAA,KAAA,EAAA,IAAA,EAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModelViewer.js","names":[],"sources":["../../src/components/ModelViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport * as THREE from 'three'\nimport { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'\nimport { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js'\nimport { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'\nimport { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader.js'\nimport { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'\nimport { fileExt } from '@/lib/fileType'\nimport { formatBytes } from '@/lib/format'\n\n/**\n * Side-viewer 3D model renderer. Fetches the model bytes and renders them with three.js (async-loaded so the\n * ~1MB library only ships when a model is opened): STL / PLY / OBJ / glTF / GLB. Auto-centers and frames the\n * model, lights it, and gives OrbitControls (drag to rotate, wheel to zoom, right-drag to pan).\n *\n * Best-effort by format: STL/PLY/OBJ are self-contained. A `.glb` is fully embedded and renders cleanly; a\n * `.gltf` that references external `.bin`/textures can't resolve them here (we only hold the one file), so it\n * may render untextured or fail — download it with its assets to view fully. No materials library for OBJ\n * (.mtl isn't fetched), so OBJ shows in a neutral material.\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('')\nconst host = ref<HTMLElement | null>(null)\n\n// Models can be large (dense meshes); gate very large files behind a confirm (everything decodes in-memory).\nconst SIZE_LIMIT = 96 * 1024 * 1024\n\n// three.js handles — kept out of Vue reactivity on purpose (mutated every animation frame).\nlet renderer: THREE.WebGLRenderer | null = null\nlet scene: THREE.Scene | null = null\nlet camera: THREE.PerspectiveCamera | null = null\nlet controls: OrbitControls | null = null\nlet current: THREE.Object3D | null = null\nlet raf = 0\nlet ro: ResizeObserver | null = null\n\nfunction disposeObject(obj: THREE.Object3D): void {\n obj.traverse((child) => {\n const mesh = child as THREE.Mesh\n if (mesh.geometry) mesh.geometry.dispose()\n const mat = mesh.material\n if (Array.isArray(mat)) mat.forEach((m) => m.dispose())\n else if (mat) mat.dispose()\n })\n}\n\nfunction clearModel(): void {\n if (current && scene) {\n scene.remove(current)\n disposeObject(current)\n current = null\n }\n}\n\nfunction initScene(): void {\n const el = host.value\n if (!el || renderer) return\n const w = el.clientWidth || 1\n const h = el.clientHeight || 1\n\n scene = new THREE.Scene()\n camera = new THREE.PerspectiveCamera(50, w / h, 0.1, 2000)\n camera.position.set(2, 1.5, 3)\n\n renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })\n renderer.setPixelRatio(Math.min(2, window.devicePixelRatio || 1))\n renderer.setSize(w, h)\n el.appendChild(renderer.domElement)\n\n scene.add(new THREE.HemisphereLight(0xffffff, 0x444455, 1.1))\n const key = new THREE.DirectionalLight(0xffffff, 1.4)\n key.position.set(4, 6, 5)\n scene.add(key)\n const fill = new THREE.DirectionalLight(0xffffff, 0.5)\n fill.position.set(-5, -2, -4)\n scene.add(fill)\n\n controls = new OrbitControls(camera, renderer.domElement)\n controls.enableDamping = true\n controls.dampingFactor = 0.08\n\n ro = new ResizeObserver(onResize)\n ro.observe(el)\n\n const tick = (): void => {\n raf = requestAnimationFrame(tick)\n controls?.update()\n if (renderer && scene && camera) renderer.render(scene, camera)\n }\n tick()\n}\n\nfunction onResize(): void {\n const el = host.value\n if (!el || !renderer || !camera) return\n const w = el.clientWidth || 1\n const h = el.clientHeight || 1\n renderer.setSize(w, h)\n camera.aspect = w / h\n camera.updateProjectionMatrix()\n}\n\n// Center the model at the origin and pull the camera back to frame its bounding sphere.\nfunction frame(obj: THREE.Object3D): void {\n if (!camera || !controls) return\n const box = new THREE.Box3().setFromObject(obj)\n const size = box.getSize(new THREE.Vector3())\n const center = box.getCenter(new THREE.Vector3())\n obj.position.sub(center)\n const maxDim = Math.max(size.x, size.y, size.z) || 1\n const fov = (camera.fov * Math.PI) / 180\n const dist = (maxDim / (2 * Math.tan(fov / 2))) * 1.6\n camera.position.set(dist * 0.6, dist * 0.45, dist)\n camera.near = Math.max(dist / 1000, 0.001)\n camera.far = dist * 1000\n camera.updateProjectionMatrix()\n controls.target.set(0, 0, 0)\n controls.update()\n}\n\nconst NEUTRAL = (): THREE.Material =>\n new THREE.MeshStandardMaterial({\n color: 0x9aa7b4,\n metalness: 0.1,\n roughness: 0.7,\n side: THREE.DoubleSide,\n })\n\nasync function buildObject(buf: ArrayBuffer, ext: string): Promise<THREE.Object3D> {\n if (ext === 'stl') {\n const geom = new STLLoader().parse(buf)\n geom.computeVertexNormals()\n return new THREE.Mesh(geom, NEUTRAL())\n }\n if (ext === 'ply') {\n const geom = new PLYLoader().parse(buf)\n geom.computeVertexNormals()\n const mat = geom.hasAttribute('color')\n ? new THREE.MeshStandardMaterial({\n vertexColors: true,\n side: THREE.DoubleSide,\n roughness: 0.7,\n })\n : NEUTRAL()\n return new THREE.Mesh(geom, mat)\n }\n if (ext === 'obj') {\n const obj = new OBJLoader().parse(new TextDecoder().decode(buf))\n // OBJ without an .mtl comes in with no material — give meshes the neutral one.\n obj.traverse((child) => {\n const mesh = child as THREE.Mesh\n if (mesh.isMesh) mesh.material = NEUTRAL()\n })\n return obj\n }\n // glTF / GLB\n const data: ArrayBuffer | string = ext === 'gltf' ? new TextDecoder().decode(buf) : buf\n const gltf = await new Promise<{ scene: THREE.Group }>((resolve, reject) => {\n new GLTFLoader().parse(data, '', (g) => resolve(g as unknown as { scene: THREE.Group }), reject)\n })\n return gltf.scene\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.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 initScene()\n if (!scene) throw new Error('WebGL 初始化失败')\n clearModel()\n const obj = await buildObject(buf, fileExt(props.name))\n current = obj\n scene.add(obj)\n frame(obj)\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 if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nonMounted(() => {\n watch(() => props.url, load, { immediate: true })\n})\n\nonBeforeUnmount(() => {\n cancelAnimationFrame(raf)\n ro?.disconnect()\n clearModel()\n controls?.dispose()\n if (renderer) {\n renderer.dispose()\n renderer.domElement.remove()\n }\n renderer = null\n scene = null\n camera = null\n controls = null\n})\n</script>\n\n<template>\n <div class=\"mdl\">\n <p v-if=\"status === 'loading'\" class=\"mdl__msg\">正在加载模型…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"mdl__msg mdl__gate\">\n <p>该模型较大({{ formatBytes(props.size) }}),将在浏览器内解析。是否继续?</p>\n <button type=\"button\" class=\"mdl__btn\" @click=\"doLoad\">加载并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"mdl__msg mdl__msg--error\">\n <p>加载失败:{{ errorMsg }}</p>\n <p class=\"mdl__hint\">\n 外部引用的 .gltf(需配套 .bin/贴图)或压缩网格可能无法在此渲染,可下载后用本地工具打开。\n </p>\n <button type=\"button\" class=\"mdl__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <!-- the canvas host is always present so three.js can mount into it; overlaid messages sit above it -->\n <div ref=\"host\" class=\"mdl__host\" :class=\"{ 'mdl__host--hidden': status !== 'ready' }\" />\n <div v-if=\"status === 'ready'\" class=\"mdl__hint mdl__hint--float\">\n 拖动旋转 · 滚轮缩放 · 右键平移\n </div>\n </div>\n</template>\n\n<style scoped>\n.mdl {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: radial-gradient(circle at 50% 35%, var(--surface-2), var(--code-bg));\n}\n\n.mdl__msg {\n position: relative;\n z-index: 2;\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.mdl__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n color: var(--danger);\n}\n.mdl__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.mdl__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.mdl__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.mdl__host {\n flex: 1 1 auto;\n min-height: 0;\n touch-action: none;\n}\n.mdl__host--hidden {\n visibility: hidden;\n}\n.mdl__host :deep(canvas) {\n display: block;\n}\n\n.mdl__hint {\n color: var(--faint);\n font-size: 12px;\n line-height: 1.5;\n}\n.mdl__hint--float {\n position: absolute;\n left: 12px;\n bottom: 10px;\n z-index: 2;\n padding: 4px 10px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--surface) 70%, transparent);\n pointer-events: none;\n}\n</style>\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModelViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/ModelViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport * as THREE from 'three'\nimport { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'\nimport { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js'\nimport { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'\nimport { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader.js'\nimport { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'\nimport { fileExt } from '@/lib/fileType'\nimport { formatBytes } from '@/lib/format'\n\n/**\n * Side-viewer 3D model renderer. Fetches the model bytes and renders them with three.js (async-loaded so the\n * ~1MB library only ships when a model is opened): STL / PLY / OBJ / glTF / GLB. Auto-centers and frames the\n * model, lights it, and gives OrbitControls (drag to rotate, wheel to zoom, right-drag to pan).\n *\n * Best-effort by format: STL/PLY/OBJ are self-contained. A `.glb` is fully embedded and renders cleanly; a\n * `.gltf` that references external `.bin`/textures can't resolve them here (we only hold the one file), so it\n * may render untextured or fail — download it with its assets to view fully. No materials library for OBJ\n * (.mtl isn't fetched), so OBJ shows in a neutral material.\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('')\nconst host = ref<HTMLElement | null>(null)\n\n// Models can be large (dense meshes); gate very large files behind a confirm (everything decodes in-memory).\nconst SIZE_LIMIT = 96 * 1024 * 1024\n\n// three.js handles — kept out of Vue reactivity on purpose (mutated every animation frame).\nlet renderer: THREE.WebGLRenderer | null = null\nlet scene: THREE.Scene | null = null\nlet camera: THREE.PerspectiveCamera | null = null\nlet controls: OrbitControls | null = null\nlet current: THREE.Object3D | null = null\nlet raf = 0\nlet ro: ResizeObserver | null = null\n\nfunction disposeObject(obj: THREE.Object3D): void {\n obj.traverse((child) => {\n const mesh = child as THREE.Mesh\n if (mesh.geometry) mesh.geometry.dispose()\n const mat = mesh.material\n if (Array.isArray(mat)) mat.forEach((m) => m.dispose())\n else if (mat) mat.dispose()\n })\n}\n\nfunction clearModel(): void {\n if (current && scene) {\n scene.remove(current)\n disposeObject(current)\n current = null\n }\n}\n\nfunction initScene(): void {\n const el = host.value\n if (!el || renderer) return\n const w = el.clientWidth || 1\n const h = el.clientHeight || 1\n\n scene = new THREE.Scene()\n camera = new THREE.PerspectiveCamera(50, w / h, 0.1, 2000)\n camera.position.set(2, 1.5, 3)\n\n renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })\n renderer.setPixelRatio(Math.min(2, window.devicePixelRatio || 1))\n renderer.setSize(w, h)\n el.appendChild(renderer.domElement)\n\n scene.add(new THREE.HemisphereLight(0xffffff, 0x444455, 1.1))\n const key = new THREE.DirectionalLight(0xffffff, 1.4)\n key.position.set(4, 6, 5)\n scene.add(key)\n const fill = new THREE.DirectionalLight(0xffffff, 0.5)\n fill.position.set(-5, -2, -4)\n scene.add(fill)\n\n controls = new OrbitControls(camera, renderer.domElement)\n controls.enableDamping = true\n controls.dampingFactor = 0.08\n\n ro = new ResizeObserver(onResize)\n ro.observe(el)\n\n const tick = (): void => {\n raf = requestAnimationFrame(tick)\n controls?.update()\n if (renderer && scene && camera) renderer.render(scene, camera)\n }\n tick()\n}\n\nfunction onResize(): void {\n const el = host.value\n if (!el || !renderer || !camera) return\n const w = el.clientWidth || 1\n const h = el.clientHeight || 1\n renderer.setSize(w, h)\n camera.aspect = w / h\n camera.updateProjectionMatrix()\n}\n\n// Center the model at the origin and pull the camera back to frame its bounding sphere.\nfunction frame(obj: THREE.Object3D): void {\n if (!camera || !controls) return\n const box = new THREE.Box3().setFromObject(obj)\n const size = box.getSize(new THREE.Vector3())\n const center = box.getCenter(new THREE.Vector3())\n obj.position.sub(center)\n const maxDim = Math.max(size.x, size.y, size.z) || 1\n const fov = (camera.fov * Math.PI) / 180\n const dist = (maxDim / (2 * Math.tan(fov / 2))) * 1.6\n camera.position.set(dist * 0.6, dist * 0.45, dist)\n camera.near = Math.max(dist / 1000, 0.001)\n camera.far = dist * 1000\n camera.updateProjectionMatrix()\n controls.target.set(0, 0, 0)\n controls.update()\n}\n\nconst NEUTRAL = (): THREE.Material =>\n new THREE.MeshStandardMaterial({\n color: 0x9aa7b4,\n metalness: 0.1,\n roughness: 0.7,\n side: THREE.DoubleSide,\n })\n\nasync function buildObject(buf: ArrayBuffer, ext: string): Promise<THREE.Object3D> {\n if (ext === 'stl') {\n const geom = new STLLoader().parse(buf)\n geom.computeVertexNormals()\n return new THREE.Mesh(geom, NEUTRAL())\n }\n if (ext === 'ply') {\n const geom = new PLYLoader().parse(buf)\n geom.computeVertexNormals()\n const mat = geom.hasAttribute('color')\n ? new THREE.MeshStandardMaterial({\n vertexColors: true,\n side: THREE.DoubleSide,\n roughness: 0.7,\n })\n : NEUTRAL()\n return new THREE.Mesh(geom, mat)\n }\n if (ext === 'obj') {\n const obj = new OBJLoader().parse(new TextDecoder().decode(buf))\n // OBJ without an .mtl comes in with no material — give meshes the neutral one.\n obj.traverse((child) => {\n const mesh = child as THREE.Mesh\n if (mesh.isMesh) mesh.material = NEUTRAL()\n })\n return obj\n }\n // glTF / GLB\n const data: ArrayBuffer | string = ext === 'gltf' ? new TextDecoder().decode(buf) : buf\n const gltf = await new Promise<{ scene: THREE.Group }>((resolve, reject) => {\n new GLTFLoader().parse(data, '', (g) => resolve(g as unknown as { scene: THREE.Group }), reject)\n })\n return gltf.scene\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.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 initScene()\n if (!scene) throw new Error('WebGL 初始化失败')\n clearModel()\n const obj = await buildObject(buf, fileExt(props.name))\n current = obj\n scene.add(obj)\n frame(obj)\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 if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nonMounted(() => {\n watch(() => props.url, load, { immediate: true })\n})\n\nonBeforeUnmount(() => {\n cancelAnimationFrame(raf)\n ro?.disconnect()\n clearModel()\n controls?.dispose()\n if (renderer) {\n renderer.dispose()\n renderer.domElement.remove()\n }\n renderer = null\n scene = null\n camera = null\n controls = null\n})\n</script>\n\n<template>\n <div class=\"mdl\">\n <p v-if=\"status === 'loading'\" class=\"mdl__msg\">正在加载模型…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"mdl__msg mdl__gate\">\n <p>该模型较大({{ formatBytes(props.size) }}),将在浏览器内解析。是否继续?</p>\n <button type=\"button\" class=\"mdl__btn\" @click=\"doLoad\">加载并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"mdl__msg mdl__msg--error\">\n <p>加载失败:{{ errorMsg }}</p>\n <p class=\"mdl__hint\">\n 外部引用的 .gltf(需配套 .bin/贴图)或压缩网格可能无法在此渲染,可下载后用本地工具打开。\n </p>\n <button type=\"button\" class=\"mdl__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <!-- the canvas host is always present so three.js can mount into it; overlaid messages sit above it -->\n <div ref=\"host\" class=\"mdl__host\" :class=\"{ 'mdl__host--hidden': status !== 'ready' }\" />\n <div v-if=\"status === 'ready'\" class=\"mdl__hint mdl__hint--float\">\n 拖动旋转 · 滚轮缩放 · 右键平移\n </div>\n </div>\n</template>\n\n<style scoped>\n.mdl {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: radial-gradient(circle at 50% 35%, var(--surface-2), var(--code-bg));\n}\n\n.mdl__msg {\n position: relative;\n z-index: 2;\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.mdl__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n color: var(--danger);\n}\n.mdl__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.mdl__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.mdl__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.mdl__host {\n flex: 1 1 auto;\n min-height: 0;\n touch-action: none;\n}\n.mdl__host--hidden {\n visibility: hidden;\n}\n.mdl__host :deep(canvas) {\n display: block;\n}\n\n.mdl__hint {\n color: var(--faint);\n font-size: 12px;\n line-height: 1.5;\n}\n.mdl__hint--float {\n position: absolute;\n left: 12px;\n bottom: 10px;\n z-index: 2;\n padding: 4px 10px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--surface) 70%, transparent);\n pointer-events: none;\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MsiViewer.js","names":[],"sources":["../../src/components/MsiViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport { parseMsi, NotMsiError, type MsiInfo } from '@/lib/msi'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * The side viewer's MSI mode: fetch a Windows Installer `.msi`, parse its OLE compound-file structure\n * entirely in-browser (see lib/msi.ts), and surface the install metadata — the product identity (name /\n * version / codes / publisher) from the Property table, plus the SummaryInformation (platform, package\n * code, authoring tool, dates). Mirrors BinObjBrowser: the bytes come from the same download endpoint as\n * any other file and never leave the browser. An `.msi` is NOT a PE (that's BinObjBrowser) — it's the\n * same OLE2 container legacy Office docs use, which is why this is a separate viewer.\n *\n * Large-file gate: an `.msi` can be tens of MB and parsing needs the whole file in memory, so unless the\n * manifest size is known to be small (≤ LARGE_THRESHOLD) we show a confirm card and only fetch on click.\n */\nconst props = defineProps<{ url: string; name: string; size?: number }>()\n\nconst LARGE_THRESHOLD = 8 * 1024 * 1024 // 8 MB\n\ntype Status = 'confirm' | 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst info = ref<MsiInfo | null>(null)\nconst showProps = ref(false)\nconst showTables = ref(false)\n\nfunction decideInitial(): void {\n info.value = null\n showProps.value = false\n showTables.value = false\n errorMsg.value = ''\n if (props.size != null && props.size <= LARGE_THRESHOLD) void load()\n else status.value = 'confirm'\n}\n\nasync function load(): Promise<void> {\n status.value = 'loading'\n errorMsg.value = ''\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n info.value = parseMsi(await res.arrayBuffer())\n status.value = 'ready'\n } catch (e) {\n errorMsg.value =\n e instanceof NotMsiError ? e.message : e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\n// The resolved product-identity rows (only the fields actually present).\nconst identity = computed<{ label: string; value: string }[]>(() => {\n const c = info.value\n if (!c) return []\n const rows: { label: string; value: string }[] = []\n const add = (label: string, v?: string): void => {\n if (v) rows.push({ label, value: v })\n }\n add('产品', c.productName)\n add('版本', c.productVersion)\n add('厂商', c.manufacturer)\n add('语言', c.language)\n add('产品码', c.productCode)\n add('升级码', c.upgradeCode)\n return rows\n})\n\n// Click a property/identity value → copy it; the row briefly shows \"已复制\".\nconst copied = ref('')\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyValue(v: string): Promise<void> {\n try {\n await navigator.clipboard?.writeText(v)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copied.value = v\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copied.value = ''), 1200)\n}\n\nonMounted(decideInitial)\nwatch(() => props.url, decideInitial)\nonBeforeUnmount(() => clearTimeout(copyTimer))\n</script>\n\n<template>\n <div class=\"msi\">\n <!-- large-file gate -->\n <div v-if=\"status === 'confirm'\" class=\"msi__confirm\">\n <Icon name=\"archive\" :size=\"32\" />\n <p class=\"msi__confirm-title\" :title=\"name\">{{ name }}</p>\n <p class=\"msi__confirm-sub\">\n {{\n size != null ? `文件较大(${formatBytes(size)})` : '文件大小未知'\n }},解析需将整个文件下载到浏览器。\n </p>\n <button type=\"button\" class=\"msi__btn\" @click=\"load\">加载并解析安装包</button>\n </div>\n\n <p v-else-if=\"status === 'loading'\" class=\"msi__msg\">正在解析安装包…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"msi__msg msi__msg--error\">\n <p>{{ errorMsg }}</p>\n <button type=\"button\" class=\"msi__retry\" @click=\"load\">重试</button>\n </div>\n\n <template v-else-if=\"status === 'ready' && info\">\n <div class=\"msi__summary\">\n <span class=\"msi__fmt\">MSI</span>\n <span v-if=\"info.productVersion\">v{{ info.productVersion }}</span>\n <span v-if=\"info.language\">{{ info.language }}</span>\n <span>{{ info.properties.length }} 个属性</span>\n <span>{{ info.tables.length }} 张表</span>\n </div>\n\n <!-- product identity (the headline) -->\n <div v-if=\"identity.length\" class=\"msi__card\">\n <h3 class=\"msi__h\">产品信息</h3>\n <dl class=\"msi__dl\">\n <template v-for=\"(r, i) in identity\" :key=\"i\">\n <dt class=\"msi__dt\">{{ r.label }}</dt>\n <dd class=\"msi__dd\">\n <button\n type=\"button\"\n class=\"msi__copyable\"\n :title=\"`点击复制:${r.value}`\"\n @click=\"copyValue(r.value)\"\n >\n <span>{{ r.value }}</span>\n <span v-if=\"copied === r.value\" class=\"msi__copied\">已复制</span>\n </button>\n </dd>\n </template>\n </dl>\n </div>\n\n <!-- summary information -->\n <div v-if=\"info.summary.length\" class=\"msi__card\">\n <h3 class=\"msi__h\">摘要信息</h3>\n <dl class=\"msi__dl\">\n <template v-for=\"(m, i) in info.summary\" :key=\"i\">\n <dt class=\"msi__dt\">{{ m.label }}</dt>\n <dd class=\"msi__dd msi__dd--plain\">{{ m.value }}</dd>\n </template>\n </dl>\n </div>\n\n <!-- full property table (collapsible — there can be many) -->\n <div v-if=\"info.properties.length\" class=\"msi__section\">\n <button type=\"button\" class=\"msi__toggle\" @click=\"showProps = !showProps\">\n <span class=\"msi__caret\" :class=\"{ 'msi__caret--open': showProps }\">▶</span>\n 全部属性({{ info.properties.length }})\n </button>\n <div v-if=\"showProps\" class=\"msi__props\">\n <button\n v-for=\"(p, i) in info.properties\"\n :key=\"i\"\n type=\"button\"\n class=\"msi__prow\"\n :title=\"`点击复制值:${p.value}`\"\n @click=\"copyValue(p.value)\"\n >\n <span class=\"msi__pname\">{{ p.name }}</span>\n <span class=\"msi__pval\">{{ copied === p.value ? '已复制' : p.value }}</span>\n </button>\n </div>\n </div>\n\n <!-- database tables present (collapsible) -->\n <div v-if=\"info.tables.length\" class=\"msi__section\">\n <button type=\"button\" class=\"msi__toggle\" @click=\"showTables = !showTables\">\n <span class=\"msi__caret\" :class=\"{ 'msi__caret--open': showTables }\">▶</span>\n 数据库表({{ info.tables.length }})\n </button>\n <div v-if=\"showTables\" class=\"msi__tables\">\n <span v-for=\"t in info.tables\" :key=\"t\" class=\"msi__chip\">{{ t }}</span>\n </div>\n </div>\n\n <p v-for=\"(n, i) in info.notes\" :key=\"i\" class=\"msi__note\">{{ n }}</p>\n </template>\n </div>\n</template>\n\n<style scoped>\n.msi {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface);\n}\n\n.msi__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.msi__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.msi__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.msi__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Large-file confirm card. */\n.msi__confirm {\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: 32px 28px;\n text-align: center;\n color: var(--muted);\n}\n.msi__confirm-title {\n max-width: 100%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13.5px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.msi__confirm-sub {\n font-size: 12.5px;\n line-height: 1.6;\n color: var(--muted);\n}\n.msi__btn {\n margin-top: 4px;\n font-family: inherit;\n font-size: 13px;\n color: #fff;\n background: var(--accent);\n border: none;\n border-radius: 8px;\n padding: 8px 18px;\n cursor: pointer;\n}\n.msi__btn:hover {\n filter: brightness(1.06);\n}\n\n/* Summary bar. */\n.msi__summary {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px 12px;\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.msi__fmt {\n font-weight: 700;\n color: var(--accent);\n}\n\n/* Identity / summary cards. */\n.msi__card {\n flex-shrink: 0;\n padding: 12px 16px;\n border-bottom: 1px solid var(--border);\n}\n.msi__h {\n margin: 0 0 8px;\n font-size: 10.5px;\n font-weight: 700;\n letter-spacing: 0.08em;\n color: var(--faint);\n text-transform: uppercase;\n}\n.msi__dl {\n margin: 0;\n display: grid;\n grid-template-columns: auto minmax(0, 1fr);\n gap: 4px 12px;\n align-content: start;\n}\n.msi__dt {\n color: var(--faint);\n font-size: 12px;\n white-space: nowrap;\n}\n.msi__dd {\n margin: 0;\n min-width: 0;\n font-size: 12.5px;\n color: var(--text);\n}\n.msi__dd--plain {\n overflow-wrap: anywhere;\n}\n.msi__copyable {\n display: inline-flex;\n align-items: baseline;\n gap: 8px;\n max-width: 100%;\n padding: 0;\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 overflow-wrap: anywhere;\n}\n.msi__copyable:hover {\n color: var(--accent);\n}\n.msi__copied {\n flex-shrink: 0;\n font-size: 10.5px;\n color: var(--accent);\n}\n\n/* Collapsible sections (properties / tables). */\n.msi__section {\n flex-shrink: 0;\n border-bottom: 1px solid var(--border);\n}\n.msi__toggle {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 10px 16px;\n background: transparent;\n border: none;\n font-family: inherit;\n font-size: 12px;\n font-weight: 600;\n color: var(--muted);\n text-align: left;\n cursor: pointer;\n}\n.msi__toggle:hover {\n background: var(--surface-2);\n}\n.msi__caret {\n flex-shrink: 0;\n width: 9px;\n font-size: 9px;\n color: var(--faint);\n transition: transform 0.12s;\n}\n.msi__caret--open {\n transform: rotate(90deg);\n}\n\n.msi__props {\n padding: 2px 0 8px;\n}\n.msi__prow {\n display: flex;\n align-items: baseline;\n gap: 12px;\n width: 100%;\n padding: 4px 16px;\n background: transparent;\n border: none;\n font-family: var(--font-mono);\n font-size: 12px;\n text-align: left;\n cursor: pointer;\n}\n.msi__prow:hover {\n background: var(--surface-2);\n}\n.msi__pname {\n flex-shrink: 0;\n min-width: 38%;\n color: var(--accent);\n overflow-wrap: anywhere;\n}\n.msi__pval {\n flex: 1 1 auto;\n min-width: 0;\n color: var(--text);\n overflow-wrap: anywhere;\n}\n\n.msi__tables {\n display: flex;\n flex-wrap: wrap;\n gap: 5px;\n padding: 2px 16px 12px;\n}\n.msi__chip {\n padding: 2px 9px;\n border-radius: 999px;\n background: var(--surface-2);\n border: 1px solid var(--border);\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--muted);\n}\n\n.msi__note {\n flex-shrink: 0;\n margin: 0;\n padding: 10px 16px;\n font-size: 12px;\n color: var(--muted);\n background: var(--surface-2);\n}\n</style>\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MsiViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/MsiViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport { parseMsi, NotMsiError, type MsiInfo } from '@/lib/msi'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * The side viewer's MSI mode: fetch a Windows Installer `.msi`, parse its OLE compound-file structure\n * entirely in-browser (see lib/msi.ts), and surface the install metadata — the product identity (name /\n * version / codes / publisher) from the Property table, plus the SummaryInformation (platform, package\n * code, authoring tool, dates). Mirrors BinObjBrowser: the bytes come from the same download endpoint as\n * any other file and never leave the browser. An `.msi` is NOT a PE (that's BinObjBrowser) — it's the\n * same OLE2 container legacy Office docs use, which is why this is a separate viewer.\n *\n * Large-file gate: an `.msi` can be tens of MB and parsing needs the whole file in memory, so unless the\n * manifest size is known to be small (≤ LARGE_THRESHOLD) we show a confirm card and only fetch on click.\n */\nconst props = defineProps<{ url: string; name: string; size?: number }>()\n\nconst LARGE_THRESHOLD = 8 * 1024 * 1024 // 8 MB\n\ntype Status = 'confirm' | 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst info = ref<MsiInfo | null>(null)\nconst showProps = ref(false)\nconst showTables = ref(false)\n\nfunction decideInitial(): void {\n info.value = null\n showProps.value = false\n showTables.value = false\n errorMsg.value = ''\n if (props.size != null && props.size <= LARGE_THRESHOLD) void load()\n else status.value = 'confirm'\n}\n\nasync function load(): Promise<void> {\n status.value = 'loading'\n errorMsg.value = ''\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n info.value = parseMsi(await res.arrayBuffer())\n status.value = 'ready'\n } catch (e) {\n errorMsg.value =\n e instanceof NotMsiError ? e.message : e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\n// The resolved product-identity rows (only the fields actually present).\nconst identity = computed<{ label: string; value: string }[]>(() => {\n const c = info.value\n if (!c) return []\n const rows: { label: string; value: string }[] = []\n const add = (label: string, v?: string): void => {\n if (v) rows.push({ label, value: v })\n }\n add('产品', c.productName)\n add('版本', c.productVersion)\n add('厂商', c.manufacturer)\n add('语言', c.language)\n add('产品码', c.productCode)\n add('升级码', c.upgradeCode)\n return rows\n})\n\n// Click a property/identity value → copy it; the row briefly shows \"已复制\".\nconst copied = ref('')\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyValue(v: string): Promise<void> {\n try {\n await navigator.clipboard?.writeText(v)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copied.value = v\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copied.value = ''), 1200)\n}\n\nonMounted(decideInitial)\nwatch(() => props.url, decideInitial)\nonBeforeUnmount(() => clearTimeout(copyTimer))\n</script>\n\n<template>\n <div class=\"msi\">\n <!-- large-file gate -->\n <div v-if=\"status === 'confirm'\" class=\"msi__confirm\">\n <Icon name=\"archive\" :size=\"32\" />\n <p class=\"msi__confirm-title\" :title=\"name\">{{ name }}</p>\n <p class=\"msi__confirm-sub\">\n {{\n size != null ? `文件较大(${formatBytes(size)})` : '文件大小未知'\n }},解析需将整个文件下载到浏览器。\n </p>\n <button type=\"button\" class=\"msi__btn\" @click=\"load\">加载并解析安装包</button>\n </div>\n\n <p v-else-if=\"status === 'loading'\" class=\"msi__msg\">正在解析安装包…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"msi__msg msi__msg--error\">\n <p>{{ errorMsg }}</p>\n <button type=\"button\" class=\"msi__retry\" @click=\"load\">重试</button>\n </div>\n\n <template v-else-if=\"status === 'ready' && info\">\n <div class=\"msi__summary\">\n <span class=\"msi__fmt\">MSI</span>\n <span v-if=\"info.productVersion\">v{{ info.productVersion }}</span>\n <span v-if=\"info.language\">{{ info.language }}</span>\n <span>{{ info.properties.length }} 个属性</span>\n <span>{{ info.tables.length }} 张表</span>\n </div>\n\n <!-- product identity (the headline) -->\n <div v-if=\"identity.length\" class=\"msi__card\">\n <h3 class=\"msi__h\">产品信息</h3>\n <dl class=\"msi__dl\">\n <template v-for=\"(r, i) in identity\" :key=\"i\">\n <dt class=\"msi__dt\">{{ r.label }}</dt>\n <dd class=\"msi__dd\">\n <button\n type=\"button\"\n class=\"msi__copyable\"\n :title=\"`点击复制:${r.value}`\"\n @click=\"copyValue(r.value)\"\n >\n <span>{{ r.value }}</span>\n <span v-if=\"copied === r.value\" class=\"msi__copied\">已复制</span>\n </button>\n </dd>\n </template>\n </dl>\n </div>\n\n <!-- summary information -->\n <div v-if=\"info.summary.length\" class=\"msi__card\">\n <h3 class=\"msi__h\">摘要信息</h3>\n <dl class=\"msi__dl\">\n <template v-for=\"(m, i) in info.summary\" :key=\"i\">\n <dt class=\"msi__dt\">{{ m.label }}</dt>\n <dd class=\"msi__dd msi__dd--plain\">{{ m.value }}</dd>\n </template>\n </dl>\n </div>\n\n <!-- full property table (collapsible — there can be many) -->\n <div v-if=\"info.properties.length\" class=\"msi__section\">\n <button type=\"button\" class=\"msi__toggle\" @click=\"showProps = !showProps\">\n <span class=\"msi__caret\" :class=\"{ 'msi__caret--open': showProps }\">▶</span>\n 全部属性({{ info.properties.length }})\n </button>\n <div v-if=\"showProps\" class=\"msi__props\">\n <button\n v-for=\"(p, i) in info.properties\"\n :key=\"i\"\n type=\"button\"\n class=\"msi__prow\"\n :title=\"`点击复制值:${p.value}`\"\n @click=\"copyValue(p.value)\"\n >\n <span class=\"msi__pname\">{{ p.name }}</span>\n <span class=\"msi__pval\">{{ copied === p.value ? '已复制' : p.value }}</span>\n </button>\n </div>\n </div>\n\n <!-- database tables present (collapsible) -->\n <div v-if=\"info.tables.length\" class=\"msi__section\">\n <button type=\"button\" class=\"msi__toggle\" @click=\"showTables = !showTables\">\n <span class=\"msi__caret\" :class=\"{ 'msi__caret--open': showTables }\">▶</span>\n 数据库表({{ info.tables.length }})\n </button>\n <div v-if=\"showTables\" class=\"msi__tables\">\n <span v-for=\"t in info.tables\" :key=\"t\" class=\"msi__chip\">{{ t }}</span>\n </div>\n </div>\n\n <p v-for=\"(n, i) in info.notes\" :key=\"i\" class=\"msi__note\">{{ n }}</p>\n </template>\n </div>\n</template>\n\n<style scoped>\n.msi {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface);\n}\n\n.msi__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.msi__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.msi__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.msi__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Large-file confirm card. */\n.msi__confirm {\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: 32px 28px;\n text-align: center;\n color: var(--muted);\n}\n.msi__confirm-title {\n max-width: 100%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13.5px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.msi__confirm-sub {\n font-size: 12.5px;\n line-height: 1.6;\n color: var(--muted);\n}\n.msi__btn {\n margin-top: 4px;\n font-family: inherit;\n font-size: 13px;\n color: #fff;\n background: var(--accent);\n border: none;\n border-radius: 8px;\n padding: 8px 18px;\n cursor: pointer;\n}\n.msi__btn:hover {\n filter: brightness(1.06);\n}\n\n/* Summary bar. */\n.msi__summary {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px 12px;\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.msi__fmt {\n font-weight: 700;\n color: var(--accent);\n}\n\n/* Identity / summary cards. */\n.msi__card {\n flex-shrink: 0;\n padding: 12px 16px;\n border-bottom: 1px solid var(--border);\n}\n.msi__h {\n margin: 0 0 8px;\n font-size: 10.5px;\n font-weight: 700;\n letter-spacing: 0.08em;\n color: var(--faint);\n text-transform: uppercase;\n}\n.msi__dl {\n margin: 0;\n display: grid;\n grid-template-columns: auto minmax(0, 1fr);\n gap: 4px 12px;\n align-content: start;\n}\n.msi__dt {\n color: var(--faint);\n font-size: 12px;\n white-space: nowrap;\n}\n.msi__dd {\n margin: 0;\n min-width: 0;\n font-size: 12.5px;\n color: var(--text);\n}\n.msi__dd--plain {\n overflow-wrap: anywhere;\n}\n.msi__copyable {\n display: inline-flex;\n align-items: baseline;\n gap: 8px;\n max-width: 100%;\n padding: 0;\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 overflow-wrap: anywhere;\n}\n.msi__copyable:hover {\n color: var(--accent);\n}\n.msi__copied {\n flex-shrink: 0;\n font-size: 10.5px;\n color: var(--accent);\n}\n\n/* Collapsible sections (properties / tables). */\n.msi__section {\n flex-shrink: 0;\n border-bottom: 1px solid var(--border);\n}\n.msi__toggle {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 10px 16px;\n background: transparent;\n border: none;\n font-family: inherit;\n font-size: 12px;\n font-weight: 600;\n color: var(--muted);\n text-align: left;\n cursor: pointer;\n}\n.msi__toggle:hover {\n background: var(--surface-2);\n}\n.msi__caret {\n flex-shrink: 0;\n width: 9px;\n font-size: 9px;\n color: var(--faint);\n transition: transform 0.12s;\n}\n.msi__caret--open {\n transform: rotate(90deg);\n}\n\n.msi__props {\n padding: 2px 0 8px;\n}\n.msi__prow {\n display: flex;\n align-items: baseline;\n gap: 12px;\n width: 100%;\n padding: 4px 16px;\n background: transparent;\n border: none;\n font-family: var(--font-mono);\n font-size: 12px;\n text-align: left;\n cursor: pointer;\n}\n.msi__prow:hover {\n background: var(--surface-2);\n}\n.msi__pname {\n flex-shrink: 0;\n min-width: 38%;\n color: var(--accent);\n overflow-wrap: anywhere;\n}\n.msi__pval {\n flex: 1 1 auto;\n min-width: 0;\n color: var(--text);\n overflow-wrap: anywhere;\n}\n\n.msi__tables {\n display: flex;\n flex-wrap: wrap;\n gap: 5px;\n padding: 2px 16px 12px;\n}\n.msi__chip {\n padding: 2px 9px;\n border-radius: 999px;\n background: var(--surface-2);\n border: 1px solid var(--border);\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--muted);\n}\n\n.msi__note {\n flex-shrink: 0;\n margin: 0;\n padding: 10px 16px;\n font-size: 12px;\n color: var(--muted);\n background: var(--surface-2);\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NotebookViewer.js","names":[],"sources":["../../src/components/NotebookViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ref, watch } from 'vue'\nimport { parseNotebook, type NbCell } from '@/lib/ipynb'\nimport { highlightCode } from '@/lib/highlight'\nimport { formatBytes } from '@/lib/format'\nimport MarkdownText from '@/components/MarkdownText'\n\n/**\n * Side-viewer Jupyter Notebook (.ipynb) renderer. Fetches the notebook bytes from the download URL and parses\n * the JSON in-browser (see lib/ipynb.ts), then walks the cells: markdown cells reuse the app's markdown engine,\n * code cells reuse highlight.js, and each output is shown by its best-fit type (stream / text / image / rich\n * HTML / error). Mirrors the other byte-fetching viewers (DICOM/DOCX/…): a real download URL in, a self-owned\n * fetch + render.\n *\n * Safety: a cell's rich `text/html` output is hostile markup (it comes from arbitrary kernels), so it renders\n * the SAME way the panel's HTML preview does — in a fully sandboxed iframe (opaque origin, no `allow-scripts`)\n * whose markup carries an injected `script-src 'none'` CSP, so NO upstream script runs. (Interactive widgets\n * like Plotly therefore won't execute — download the notebook to run it; static tables/plots render fine.)\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('')\nconst cells = ref<NbCell[]>([])\nconst language = ref('python')\n\n// Notebooks with many embedded plot images can balloon; gate very large ones behind a confirm (in-memory parse).\nconst SIZE_LIMIT = 20 * 1024 * 1024\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\n}\n\n// Same hardening as FileViewerPanel's HTML preview — kill all script/plugin/base/form vectors. Multiple CSPs\n// intersect, so a fragment's own markup can't loosen this.\nconst NO_SCRIPT_CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'\">`\n\nfunction safeHtml(html: string): string {\n return (\n `<!doctype html><html><head><meta charset=\"utf-8\">${NO_SCRIPT_CSP}` +\n `<style>body{margin:0;padding:8px;font:13px system-ui,-apple-system,sans-serif;color:#222}` +\n `table{border-collapse:collapse}th,td{border:1px solid #ddd;padding:4px 8px}</style></head>` +\n `<body>${html}</body></html>`\n )\n}\n\nfunction highlight(source: string): string {\n try {\n return highlightCode(source, language.value)\n } catch {\n return escapeHtml(source)\n }\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const parsed = parseNotebook(await res.text())\n cells.value = parsed.cells\n language.value = parsed.language\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 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 })\n</script>\n\n<template>\n <div class=\"nb\">\n <p v-if=\"status === 'loading'\" class=\"nb__msg\">正在解析 Notebook…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"nb__msg nb__gate\">\n <p>该 Notebook 较大({{ formatBytes(props.size) }}),将在浏览器内解析。是否继续?</p>\n <button type=\"button\" class=\"nb__btn\" @click=\"doLoad\">解析并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"nb__msg nb__msg--error\">\n <p>解析失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"nb__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <div v-else class=\"nb__scroll\">\n <div v-for=\"(cell, i) in cells\" :key=\"i\" class=\"nb__cell\">\n <!-- markdown cell: the app's markdown engine -->\n <div v-if=\"cell.type === 'markdown'\" class=\"nb__md\">\n <MarkdownText :text=\"cell.source\" kp=\"nb\" />\n </div>\n\n <!-- raw cell: verbatim -->\n <pre v-else-if=\"cell.type === 'raw'\" class=\"nb__raw\">{{ cell.source }}</pre>\n\n <!-- code cell: prompt + highlighted source + outputs -->\n <div v-else class=\"nb__code\">\n <div class=\"nb__in\">\n <span class=\"nb__prompt\">[{{ cell.executionCount ?? ' ' }}]</span>\n <pre class=\"nb__src\"><code class=\"hljs\" v-html=\"highlight(cell.source)\" /></pre>\n </div>\n\n <div\n v-for=\"(out, j) in cell.outputs\"\n :key=\"j\"\n class=\"nb__out\"\n :class=\"{\n 'nb__out--err': out.kind === 'error' || (out.kind === 'stream' && out.isError),\n }\"\n >\n <pre v-if=\"out.kind === 'stream'\" class=\"nb__stream\">{{ out.text }}</pre>\n <pre v-else-if=\"out.kind === 'text' || out.kind === 'json'\" class=\"nb__text\">{{\n out.text\n }}</pre>\n <pre v-else-if=\"out.kind === 'error'\" class=\"nb__errtext\">{{ out.text }}</pre>\n <div v-else-if=\"out.kind === 'markdown'\" class=\"nb__md nb__md--out\">\n <MarkdownText :text=\"out.text\" :kp=\"`nbo-${i}-${j}`\" />\n </div>\n <img v-else-if=\"out.kind === 'image'\" class=\"nb__img\" :src=\"out.src\" alt=\"输出图像\" />\n <iframe\n v-else-if=\"out.kind === 'html'\"\n class=\"nb__html\"\n :srcdoc=\"safeHtml(out.html)\"\n title=\"Notebook 富文本输出\"\n sandbox=\"\"\n loading=\"lazy\"\n referrerpolicy=\"no-referrer\"\n />\n </div>\n </div>\n </div>\n\n <p v-if=\"!cells.length\" class=\"nb__msg\">(空 Notebook)</p>\n </div>\n </div>\n</template>\n\n<style scoped>\n.nb {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.nb__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.nb__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.nb__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.nb__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.nb__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.nb__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: 14px 16px 70px;\n}\n.nb__cell {\n margin-bottom: 14px;\n}\n\n/* markdown cell — lean on the shared markdown typography; just constrain width. */\n.nb__md {\n font-size: 13.5px;\n line-height: 1.6;\n color: var(--text);\n word-break: break-word;\n}\n.nb__md--out {\n padding: 6px 0 0;\n}\n.nb__raw {\n margin: 0;\n padding: 10px 12px;\n font-family: var(--font-mono);\n font-size: 12.5px;\n line-height: 1.5;\n color: var(--muted);\n background: var(--code-bg);\n border-radius: 8px;\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n/* code cell — claude.ai artifact look: a prompt gutter + highlighted source, outputs stacked beneath. */\n.nb__code {\n border: 1px solid var(--border);\n border-radius: 10px;\n overflow: hidden;\n background: var(--code-bg);\n}\n.nb__in {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 8px 10px;\n}\n.nb__prompt {\n flex-shrink: 0;\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--accent);\n user-select: none;\n padding-top: 1px;\n}\n.nb__src {\n flex: 1 1 auto;\n min-width: 0;\n margin: 0;\n overflow-x: auto;\n font-family: var(--font-mono);\n font-size: 12.5px;\n line-height: 1.55;\n color: var(--text);\n white-space: pre;\n tab-size: 4;\n}\n.nb__src code {\n font-family: inherit;\n background: transparent;\n border: none;\n padding: 0;\n}\n\n.nb__out {\n border-top: 1px solid var(--border);\n padding: 8px 10px 8px 38px;\n background: var(--surface);\n}\n.nb__out--err {\n background: color-mix(in srgb, var(--danger) 7%, var(--surface));\n}\n.nb__stream,\n.nb__text,\n.nb__errtext {\n margin: 0;\n font-family: var(--font-mono);\n font-size: 12px;\n line-height: 1.5;\n white-space: pre-wrap;\n word-break: break-word;\n color: var(--text);\n}\n.nb__errtext {\n color: var(--danger);\n}\n.nb__img {\n display: block;\n max-width: 100%;\n height: auto;\n border-radius: 4px;\n background: #fff;\n}\n.nb__html {\n display: block;\n width: 100%;\n height: 300px;\n border: 1px solid var(--border);\n border-radius: 6px;\n background: #fff;\n}\n</style>\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NotebookViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/NotebookViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ref, watch } from 'vue'\nimport { parseNotebook, type NbCell } from '@/lib/ipynb'\nimport { highlightCode } from '@/lib/highlight'\nimport { formatBytes } from '@/lib/format'\nimport MarkdownText from '@/components/MarkdownText'\n\n/**\n * Side-viewer Jupyter Notebook (.ipynb) renderer. Fetches the notebook bytes from the download URL and parses\n * the JSON in-browser (see lib/ipynb.ts), then walks the cells: markdown cells reuse the app's markdown engine,\n * code cells reuse highlight.js, and each output is shown by its best-fit type (stream / text / image / rich\n * HTML / error). Mirrors the other byte-fetching viewers (DICOM/DOCX/…): a real download URL in, a self-owned\n * fetch + render.\n *\n * Safety: a cell's rich `text/html` output is hostile markup (it comes from arbitrary kernels), so it renders\n * the SAME way the panel's HTML preview does — in a fully sandboxed iframe (opaque origin, no `allow-scripts`)\n * whose markup carries an injected `script-src 'none'` CSP, so NO upstream script runs. (Interactive widgets\n * like Plotly therefore won't execute — download the notebook to run it; static tables/plots render fine.)\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('')\nconst cells = ref<NbCell[]>([])\nconst language = ref('python')\n\n// Notebooks with many embedded plot images can balloon; gate very large ones behind a confirm (in-memory parse).\nconst SIZE_LIMIT = 20 * 1024 * 1024\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\n}\n\n// Same hardening as FileViewerPanel's HTML preview — kill all script/plugin/base/form vectors. Multiple CSPs\n// intersect, so a fragment's own markup can't loosen this.\nconst NO_SCRIPT_CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'\">`\n\nfunction safeHtml(html: string): string {\n return (\n `<!doctype html><html><head><meta charset=\"utf-8\">${NO_SCRIPT_CSP}` +\n `<style>body{margin:0;padding:8px;font:13px system-ui,-apple-system,sans-serif;color:#222}` +\n `table{border-collapse:collapse}th,td{border:1px solid #ddd;padding:4px 8px}</style></head>` +\n `<body>${html}</body></html>`\n )\n}\n\nfunction highlight(source: string): string {\n try {\n return highlightCode(source, language.value)\n } catch {\n return escapeHtml(source)\n }\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const parsed = parseNotebook(await res.text())\n cells.value = parsed.cells\n language.value = parsed.language\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 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 })\n</script>\n\n<template>\n <div class=\"nb\">\n <p v-if=\"status === 'loading'\" class=\"nb__msg\">正在解析 Notebook…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"nb__msg nb__gate\">\n <p>该 Notebook 较大({{ formatBytes(props.size) }}),将在浏览器内解析。是否继续?</p>\n <button type=\"button\" class=\"nb__btn\" @click=\"doLoad\">解析并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"nb__msg nb__msg--error\">\n <p>解析失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"nb__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <div v-else class=\"nb__scroll\">\n <div v-for=\"(cell, i) in cells\" :key=\"i\" class=\"nb__cell\">\n <!-- markdown cell: the app's markdown engine -->\n <div v-if=\"cell.type === 'markdown'\" class=\"nb__md\">\n <MarkdownText :text=\"cell.source\" kp=\"nb\" />\n </div>\n\n <!-- raw cell: verbatim -->\n <pre v-else-if=\"cell.type === 'raw'\" class=\"nb__raw\">{{ cell.source }}</pre>\n\n <!-- code cell: prompt + highlighted source + outputs -->\n <div v-else class=\"nb__code\">\n <div class=\"nb__in\">\n <span class=\"nb__prompt\">[{{ cell.executionCount ?? ' ' }}]</span>\n <pre class=\"nb__src\"><code class=\"hljs\" v-html=\"highlight(cell.source)\" /></pre>\n </div>\n\n <div\n v-for=\"(out, j) in cell.outputs\"\n :key=\"j\"\n class=\"nb__out\"\n :class=\"{\n 'nb__out--err': out.kind === 'error' || (out.kind === 'stream' && out.isError),\n }\"\n >\n <pre v-if=\"out.kind === 'stream'\" class=\"nb__stream\">{{ out.text }}</pre>\n <pre v-else-if=\"out.kind === 'text' || out.kind === 'json'\" class=\"nb__text\">{{\n out.text\n }}</pre>\n <pre v-else-if=\"out.kind === 'error'\" class=\"nb__errtext\">{{ out.text }}</pre>\n <div v-else-if=\"out.kind === 'markdown'\" class=\"nb__md nb__md--out\">\n <MarkdownText :text=\"out.text\" :kp=\"`nbo-${i}-${j}`\" />\n </div>\n <img v-else-if=\"out.kind === 'image'\" class=\"nb__img\" :src=\"out.src\" alt=\"输出图像\" />\n <iframe\n v-else-if=\"out.kind === 'html'\"\n class=\"nb__html\"\n :srcdoc=\"safeHtml(out.html)\"\n title=\"Notebook 富文本输出\"\n sandbox=\"\"\n loading=\"lazy\"\n referrerpolicy=\"no-referrer\"\n />\n </div>\n </div>\n </div>\n\n <p v-if=\"!cells.length\" class=\"nb__msg\">(空 Notebook)</p>\n </div>\n </div>\n</template>\n\n<style scoped>\n.nb {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.nb__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.nb__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.nb__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.nb__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.nb__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.nb__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: 14px 16px 70px;\n}\n.nb__cell {\n margin-bottom: 14px;\n}\n\n/* markdown cell — lean on the shared markdown typography; just constrain width. */\n.nb__md {\n font-size: 13.5px;\n line-height: 1.6;\n color: var(--text);\n word-break: break-word;\n}\n.nb__md--out {\n padding: 6px 0 0;\n}\n.nb__raw {\n margin: 0;\n padding: 10px 12px;\n font-family: var(--font-mono);\n font-size: 12.5px;\n line-height: 1.5;\n color: var(--muted);\n background: var(--code-bg);\n border-radius: 8px;\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n/* code cell — claude.ai artifact look: a prompt gutter + highlighted source, outputs stacked beneath. */\n.nb__code {\n border: 1px solid var(--border);\n border-radius: 10px;\n overflow: hidden;\n background: var(--code-bg);\n}\n.nb__in {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 8px 10px;\n}\n.nb__prompt {\n flex-shrink: 0;\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--accent);\n user-select: none;\n padding-top: 1px;\n}\n.nb__src {\n flex: 1 1 auto;\n min-width: 0;\n margin: 0;\n overflow-x: auto;\n font-family: var(--font-mono);\n font-size: 12.5px;\n line-height: 1.55;\n color: var(--text);\n white-space: pre;\n tab-size: 4;\n}\n.nb__src code {\n font-family: inherit;\n background: transparent;\n border: none;\n padding: 0;\n}\n\n.nb__out {\n border-top: 1px solid var(--border);\n padding: 8px 10px 8px 38px;\n background: var(--surface);\n}\n.nb__out--err {\n background: color-mix(in srgb, var(--danger) 7%, var(--surface));\n}\n.nb__stream,\n.nb__text,\n.nb__errtext {\n margin: 0;\n font-family: var(--font-mono);\n font-size: 12px;\n line-height: 1.5;\n white-space: pre-wrap;\n word-break: break-word;\n color: var(--text);\n}\n.nb__errtext {\n color: var(--danger);\n}\n.nb__img {\n display: block;\n max-width: 100%;\n height: auto;\n border-radius: 4px;\n background: #fff;\n}\n.nb__html {\n display: block;\n width: 100%;\n height: 300px;\n border: 1px solid var(--border);\n border-radius: 6px;\n background: #fff;\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PdfViewer.js","names":[],"sources":["../../src/components/PdfViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport * as pdfjsLib from 'pdfjs-dist'\nimport type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist'\n// pdfjs runs its parser/rasterizer in a web worker. Vite's `?url` emits the real worker asset and returns a\n// servable URL (root-relative in the same-origin prod build, a dev URL under `pnpm dev`); we point pdfjs at\n// it. The `.mjs` is served as text/javascript by Ktor (see AGENTS.md) so the module worker loads in prod.\nimport pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url'\n\npdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorkerUrl\n\n/**\n * Side-viewer PDF (`.pdf`) renderer. Fetches the document bytes from the download URL and rasterizes each\n * page to a <canvas> entirely in-browser via pdfjs-dist (https://github.com/mozilla/pdf.js) — a vertically\n * scrollable list of pages (white sheets on a neutral \"desk\", like the docx/pptx viewers). No CDN / network\n * beyond the file fetch: pdfjs bundles its own worker, fetched lazily as a same-origin asset (see above).\n *\n * Two entry shapes mirror the other viewers (docx/xlsx/pptx/zip): a real download URL for a held attachment,\n * or a blob URL minted by the archive browsers from already-extracted bytes. Pages re-rasterize when the pane\n * is resized (debounced) so text stays crisp at the new width.\n */\nconst props = defineProps<{\n /** Download/blob URL of the .pdf bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst pageCount = ref(0)\n// The scroll container the page canvases render into. Always mounted (the loading/error states overlay it).\nconst docEl = ref<HTMLElement | null>(null)\n\n// Live document proxy + in-flight render tasks — torn down on re-render / unmount (the worker and page\n// rasterizers don't GC on their own).\nlet pdfDoc: PDFDocumentProxy | null = null\nlet renderTasks: RenderTask[] = []\n// Bumped on every (re)render so a stale async pass (older file, or a superseded resize) can bail out.\nlet renderToken = 0\n// Cap the backing-store resolution: device pixels × this, clamped, keeps big/high-DPI pages from minting\n// multi-hundred-MB canvases while staying crisp.\nconst MAX_SCALE = 2\n\nfunction cancelRenders(): void {\n for (const t of renderTasks) {\n try {\n t.cancel()\n } catch {\n /* already settled — ignore */\n }\n }\n renderTasks = []\n}\n\nfunction disposeDoc(): void {\n cancelRenders()\n if (pdfDoc) {\n try {\n pdfDoc.destroy()\n } catch {\n /* already torn down — ignore */\n }\n pdfDoc = null\n }\n}\n\n// Rasterize every page of the live document into the container at its current width. Called on load and on\n// resize; guarded by `renderToken` so an outdated pass (file switched, or a newer resize started) stops early.\nasync function renderPages(): Promise<void> {\n const el = docEl.value\n const doc = pdfDoc\n if (!el || !doc) return\n const token = ++renderToken\n cancelRenders()\n el.innerHTML = ''\n // Available CSS width for a page (minus the container's horizontal padding). Floor at a sane minimum so a\n // momentarily-zero clientWidth (not yet laid out) doesn't render a 0-px canvas.\n const style = getComputedStyle(el)\n const padX = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight)\n const cssWidth = Math.max(120, el.clientWidth - padX)\n const dpr = Math.min(typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1, MAX_SCALE)\n for (let n = 1; n <= doc.numPages; n++) {\n let page\n try {\n page = await doc.getPage(n)\n } catch {\n if (token !== renderToken) return\n continue // skip an unreadable page rather than failing the whole doc\n }\n if (token !== renderToken) return\n const base = page.getViewport({ scale: 1 })\n const cssScale = cssWidth / base.width\n const viewport = page.getViewport({ scale: cssScale * dpr })\n const canvas = document.createElement('canvas')\n canvas.className = 'pf__page'\n canvas.width = Math.ceil(viewport.width)\n canvas.height = Math.ceil(viewport.height)\n // Display at the CSS width; the extra backing-store pixels (× dpr) make text sharp on HiDPI screens.\n canvas.style.width = `${Math.round(cssWidth)}px`\n canvas.style.height = `${Math.round(cssWidth * (base.height / base.width))}px`\n if (!canvas.getContext('2d')) continue // no 2D context available — skip rather than crash the render\n el.appendChild(canvas)\n // Pass `canvas` (the preferred param in pdfjs v5); it derives the 2D context itself.\n const task = page.render({ canvas, viewport })\n renderTasks.push(task)\n try {\n await task.promise\n } catch {\n // RenderingCancelledException on supersede/unmount is expected — only a live token cares about failures.\n if (token !== renderToken) return\n }\n if (token !== renderToken) return\n }\n}\n\nasync function load(): Promise<void> {\n const el = docEl.value\n if (!props.url || !el) return\n status.value = 'loading'\n errorMsg.value = ''\n disposeDoc()\n el.innerHTML = ''\n const url = props.url\n try {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n // Stale guard: a newer load may have started while we awaited the fetch.\n if (props.url !== url || docEl.value !== el) return\n const doc = await pdfjsLib.getDocument({ data: new Uint8Array(buf) }).promise\n if (props.url !== url || docEl.value !== el) {\n try {\n doc.destroy()\n } catch {\n /* ignore */\n }\n return\n }\n pdfDoc = doc\n pageCount.value = doc.numPages\n await renderPages()\n if (props.url !== url || docEl.value !== el) return\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\n// Re-rasterize on width changes (debounced) so pages stay crisp as the pane is dragged wider/narrower. Only\n// the width matters; height changes don't affect page scale.\nlet resizeTimer: ReturnType<typeof setTimeout> | undefined\nlet lastWidth = 0\nlet ro: ResizeObserver | null = null\nfunction onResize(): void {\n const el = docEl.value\n if (!el || !pdfDoc) return\n const w = el.clientWidth\n if (Math.abs(w - lastWidth) < 8) return // ignore sub-pixel / negligible jitter\n lastWidth = w\n clearTimeout(resizeTimer)\n resizeTimer = setTimeout(() => {\n if (pdfDoc) void renderPages()\n }, 180)\n}\n\nonMounted(() => {\n load()\n if (typeof ResizeObserver !== 'undefined' && docEl.value) {\n lastWidth = docEl.value.clientWidth\n ro = new ResizeObserver(onResize)\n ro.observe(docEl.value)\n }\n})\nwatch(() => props.url, load)\nonBeforeUnmount(() => {\n clearTimeout(resizeTimer)\n ro?.disconnect()\n renderToken++ // invalidate any in-flight render pass\n disposeDoc()\n if (docEl.value) docEl.value.innerHTML = ''\n})\n</script>\n\n<template>\n <div class=\"pf\">\n <!-- Always mounted so the renderer has a sized, scrollable container; states overlay it. -->\n <div ref=\"docEl\" class=\"pf__doc\" />\n <div v-if=\"status === 'loading'\" class=\"pf__overlay\">\n <p class=\"pf__msg\">正在渲染 PDF…</p>\n </div>\n <div v-else-if=\"status === 'error'\" class=\"pf__overlay\">\n <div class=\"pf__msg pf__msg--error\">\n <p>渲染失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"pf__retry\" @click=\"load\">重试</button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.pf {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n/* Scroll region holding the rendered pages — white sheets on a neutral \"desk\", like the docx/pptx viewers. */\n.pf__doc {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface-2);\n padding: 18px 16px 40px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n}\n/* Each page canvas: a white sheet with a soft shadow. Width is set inline (the rasterized CSS width); the\n * canvas paints its own white, so no background needed here. */\n.pf__doc :deep(.pf__page) {\n display: block;\n max-width: 100%;\n box-shadow: 0 2px 14px rgba(0, 0, 0, 0.18);\n border-radius: 2px;\n background: #fff;\n}\n/* Overlay covers the (empty) container while loading or on error, without unmounting it. */\n.pf__overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: flex-start;\n justify-content: flex-start;\n background: var(--surface);\n}\n.pf__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.pf__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.pf__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.pf__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n</style>\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PdfViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/PdfViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport * as pdfjsLib from 'pdfjs-dist'\nimport type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist'\n// pdfjs runs its parser/rasterizer in a web worker. Vite's `?url` emits the real worker asset and returns a\n// servable URL (root-relative in the same-origin prod build, a dev URL under `pnpm dev`); we point pdfjs at\n// it. The `.mjs` is served as text/javascript by Ktor (see AGENTS.md) so the module worker loads in prod.\nimport pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url'\n\npdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorkerUrl\n\n/**\n * Side-viewer PDF (`.pdf`) renderer. Fetches the document bytes from the download URL and rasterizes each\n * page to a <canvas> entirely in-browser via pdfjs-dist (https://github.com/mozilla/pdf.js) — a vertically\n * scrollable list of pages (white sheets on a neutral \"desk\", like the docx/pptx viewers). No CDN / network\n * beyond the file fetch: pdfjs bundles its own worker, fetched lazily as a same-origin asset (see above).\n *\n * Two entry shapes mirror the other viewers (docx/xlsx/pptx/zip): a real download URL for a held attachment,\n * or a blob URL minted by the archive browsers from already-extracted bytes. Pages re-rasterize when the pane\n * is resized (debounced) so text stays crisp at the new width.\n */\nconst props = defineProps<{\n /** Download/blob URL of the .pdf bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst pageCount = ref(0)\n// The scroll container the page canvases render into. Always mounted (the loading/error states overlay it).\nconst docEl = ref<HTMLElement | null>(null)\n\n// Live document proxy + in-flight render tasks — torn down on re-render / unmount (the worker and page\n// rasterizers don't GC on their own).\nlet pdfDoc: PDFDocumentProxy | null = null\nlet renderTasks: RenderTask[] = []\n// Bumped on every (re)render so a stale async pass (older file, or a superseded resize) can bail out.\nlet renderToken = 0\n// Cap the backing-store resolution: device pixels × this, clamped, keeps big/high-DPI pages from minting\n// multi-hundred-MB canvases while staying crisp.\nconst MAX_SCALE = 2\n\nfunction cancelRenders(): void {\n for (const t of renderTasks) {\n try {\n t.cancel()\n } catch {\n /* already settled — ignore */\n }\n }\n renderTasks = []\n}\n\nfunction disposeDoc(): void {\n cancelRenders()\n if (pdfDoc) {\n try {\n pdfDoc.destroy()\n } catch {\n /* already torn down — ignore */\n }\n pdfDoc = null\n }\n}\n\n// Rasterize every page of the live document into the container at its current width. Called on load and on\n// resize; guarded by `renderToken` so an outdated pass (file switched, or a newer resize started) stops early.\nasync function renderPages(): Promise<void> {\n const el = docEl.value\n const doc = pdfDoc\n if (!el || !doc) return\n const token = ++renderToken\n cancelRenders()\n el.innerHTML = ''\n // Available CSS width for a page (minus the container's horizontal padding). Floor at a sane minimum so a\n // momentarily-zero clientWidth (not yet laid out) doesn't render a 0-px canvas.\n const style = getComputedStyle(el)\n const padX = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight)\n const cssWidth = Math.max(120, el.clientWidth - padX)\n const dpr = Math.min(typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1, MAX_SCALE)\n for (let n = 1; n <= doc.numPages; n++) {\n let page\n try {\n page = await doc.getPage(n)\n } catch {\n if (token !== renderToken) return\n continue // skip an unreadable page rather than failing the whole doc\n }\n if (token !== renderToken) return\n const base = page.getViewport({ scale: 1 })\n const cssScale = cssWidth / base.width\n const viewport = page.getViewport({ scale: cssScale * dpr })\n const canvas = document.createElement('canvas')\n canvas.className = 'pf__page'\n canvas.width = Math.ceil(viewport.width)\n canvas.height = Math.ceil(viewport.height)\n // Display at the CSS width; the extra backing-store pixels (× dpr) make text sharp on HiDPI screens.\n canvas.style.width = `${Math.round(cssWidth)}px`\n canvas.style.height = `${Math.round(cssWidth * (base.height / base.width))}px`\n if (!canvas.getContext('2d')) continue // no 2D context available — skip rather than crash the render\n el.appendChild(canvas)\n // Pass `canvas` (the preferred param in pdfjs v5); it derives the 2D context itself.\n const task = page.render({ canvas, viewport })\n renderTasks.push(task)\n try {\n await task.promise\n } catch {\n // RenderingCancelledException on supersede/unmount is expected — only a live token cares about failures.\n if (token !== renderToken) return\n }\n if (token !== renderToken) return\n }\n}\n\nasync function load(): Promise<void> {\n const el = docEl.value\n if (!props.url || !el) return\n status.value = 'loading'\n errorMsg.value = ''\n disposeDoc()\n el.innerHTML = ''\n const url = props.url\n try {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n // Stale guard: a newer load may have started while we awaited the fetch.\n if (props.url !== url || docEl.value !== el) return\n const doc = await pdfjsLib.getDocument({ data: new Uint8Array(buf) }).promise\n if (props.url !== url || docEl.value !== el) {\n try {\n doc.destroy()\n } catch {\n /* ignore */\n }\n return\n }\n pdfDoc = doc\n pageCount.value = doc.numPages\n await renderPages()\n if (props.url !== url || docEl.value !== el) return\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\n// Re-rasterize on width changes (debounced) so pages stay crisp as the pane is dragged wider/narrower. Only\n// the width matters; height changes don't affect page scale.\nlet resizeTimer: ReturnType<typeof setTimeout> | undefined\nlet lastWidth = 0\nlet ro: ResizeObserver | null = null\nfunction onResize(): void {\n const el = docEl.value\n if (!el || !pdfDoc) return\n const w = el.clientWidth\n if (Math.abs(w - lastWidth) < 8) return // ignore sub-pixel / negligible jitter\n lastWidth = w\n clearTimeout(resizeTimer)\n resizeTimer = setTimeout(() => {\n if (pdfDoc) void renderPages()\n }, 180)\n}\n\nonMounted(() => {\n load()\n if (typeof ResizeObserver !== 'undefined' && docEl.value) {\n lastWidth = docEl.value.clientWidth\n ro = new ResizeObserver(onResize)\n ro.observe(docEl.value)\n }\n})\nwatch(() => props.url, load)\nonBeforeUnmount(() => {\n clearTimeout(resizeTimer)\n ro?.disconnect()\n renderToken++ // invalidate any in-flight render pass\n disposeDoc()\n if (docEl.value) docEl.value.innerHTML = ''\n})\n</script>\n\n<template>\n <div class=\"pf\">\n <!-- Always mounted so the renderer has a sized, scrollable container; states overlay it. -->\n <div ref=\"docEl\" class=\"pf__doc\" />\n <div v-if=\"status === 'loading'\" class=\"pf__overlay\">\n <p class=\"pf__msg\">正在渲染 PDF…</p>\n </div>\n <div v-else-if=\"status === 'error'\" class=\"pf__overlay\">\n <div class=\"pf__msg pf__msg--error\">\n <p>渲染失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"pf__retry\" @click=\"load\">重试</button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.pf {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n/* Scroll region holding the rendered pages — white sheets on a neutral \"desk\", like the docx/pptx viewers. */\n.pf__doc {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface-2);\n padding: 18px 16px 40px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n}\n/* Each page canvas: a white sheet with a soft shadow. Width is set inline (the rasterized CSS width); the\n * canvas paints its own white, so no background needed here. */\n.pf__doc :deep(.pf__page) {\n display: block;\n max-width: 100%;\n box-shadow: 0 2px 14px rgba(0, 0, 0, 0.18);\n border-radius: 2px;\n background: #fff;\n}\n/* Overlay covers the (empty) container while loading or on error, without unmounting it. */\n.pf__overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: flex-start;\n justify-content: flex-start;\n background: var(--surface);\n}\n.pf__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.pf__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.pf__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.pf__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PptxNavigationPane.js","names":[],"sources":["../../src/components/PptxNavigationPane.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport {\n nextTick,\n onBeforeUnmount,\n onMounted,\n useTemplateRef,\n watch,\n type ComponentPublicInstance,\n} from 'vue'\nimport type { PptxViewer, SlideHandle } from '@aiden0z/pptx-renderer'\nimport Icon from '@/components/icons/Icon.vue'\nimport type { PptxNavigationItem } from '@/lib/pptxNavigation'\n\nconst props = defineProps<{\n viewer: PptxViewer\n items: readonly PptxNavigationItem[]\n activeIndex: number\n}>()\n\nconst emit = defineEmits<{ navigate: [index: number] }>()\nconst collapsed = defineModel<boolean>('collapsed', { default: false })\nconst listEl = useTemplateRef<HTMLElement>('list')\n\nconst thumbElements = new Map<number, HTMLElement>()\nconst thumbHandles = new Map<number, SlideHandle>()\nlet observer: IntersectionObserver | null = null\n\nfunction disposeThumbnails(): void {\n observer?.disconnect()\n observer = null\n for (const handle of thumbHandles.values()) handle.dispose()\n thumbHandles.clear()\n for (const element of thumbElements.values()) element.innerHTML = ''\n}\n\nfunction mountThumbnail(index: number, element: HTMLElement): void {\n if (thumbHandles.has(index)) return\n const handle = props.viewer.renderThumbnailToContainer(index, element, { width: 104 })\n if (!handle) return\n thumbHandles.set(index, handle)\n void handle.ready.catch(() => {\n if (thumbHandles.get(index) !== handle) return\n handle.dispose()\n thumbHandles.delete(index)\n element.innerHTML = ''\n })\n}\n\nasync function setupThumbnails(): Promise<void> {\n disposeThumbnails()\n if (collapsed.value) return\n await nextTick()\n const root = listEl.value\n if (!root) return\n\n if (typeof IntersectionObserver === 'undefined') {\n for (const [index, element] of thumbElements) mountThumbnail(index, element)\n return\n }\n\n observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue\n const element = entry.target as HTMLElement\n const index = Number(element.dataset.slideIndex)\n if (Number.isInteger(index)) mountThumbnail(index, element)\n observer?.unobserve(element)\n }\n },\n { root, rootMargin: '140px 0px' },\n )\n for (const element of thumbElements.values()) observer.observe(element)\n}\n\nfunction setThumbElement(index: number, value: Element | ComponentPublicInstance | null): void {\n const previous = thumbElements.get(index)\n if (previous && previous !== value) observer?.unobserve(previous)\n if (!(value instanceof HTMLElement)) {\n thumbElements.delete(index)\n return\n }\n thumbElements.set(index, value)\n observer?.observe(value)\n}\n\nwatch(() => props.viewer, setupThumbnails, { flush: 'post' })\nwatch(() => props.items, setupThumbnails, { flush: 'post' })\nwatch(collapsed, setupThumbnails, { flush: 'post' })\nwatch(\n () => props.activeIndex,\n async (index) => {\n await nextTick()\n listEl.value\n ?.querySelector<HTMLElement>(`[data-nav-slide-index=\"${index}\"]`)\n ?.scrollIntoView({ block: 'nearest' })\n },\n)\n\nonMounted(setupThumbnails)\nonBeforeUnmount(disposeThumbnails)\n</script>\n\n<template>\n <aside class=\"pn\" :class=\"{ 'pn--collapsed': collapsed }\" aria-label=\"幻灯片导航\">\n <button\n v-if=\"collapsed\"\n type=\"button\"\n class=\"pn__reopen\"\n aria-label=\"展开幻灯片导航\"\n title=\"展开幻灯片导航\"\n @click=\"collapsed = false\"\n >\n <Icon name=\"chevronRight\" :size=\"15\" />\n </button>\n\n <template v-else>\n <header class=\"pn__header\">\n <div>\n <strong class=\"pn__title\">幻灯片</strong>\n <span class=\"pn__count\">{{ items.length }} 张</span>\n </div>\n <button\n type=\"button\"\n class=\"pn__collapse\"\n aria-label=\"收起幻灯片导航\"\n title=\"收起幻灯片导航\"\n @click=\"collapsed = true\"\n >\n <Icon name=\"chevronLeft\" :size=\"15\" />\n </button>\n </header>\n\n <nav ref=\"list\" class=\"pn__list\" aria-label=\"幻灯片列表\">\n <button\n v-for=\"item in items\"\n :key=\"item.index\"\n type=\"button\"\n class=\"pn__item\"\n :class=\"{ 'pn__item--active': item.index === activeIndex }\"\n :data-nav-slide-index=\"item.index\"\n :aria-current=\"item.index === activeIndex ? 'page' : undefined\"\n :title=\"`${item.index + 1}. ${item.title}`\"\n @click=\"emit('navigate', item.index)\"\n >\n <span class=\"pn__number\">{{ item.index + 1 }}</span>\n <span class=\"pn__preview-wrap\">\n <span\n :ref=\"(value) => setThumbElement(item.index, value)\"\n class=\"pn__preview\"\n :data-slide-index=\"item.index\"\n aria-hidden=\"true\"\n />\n </span>\n <span class=\"pn__label\">{{ item.title }}</span>\n </button>\n </nav>\n </template>\n </aside>\n</template>\n\n<style scoped>\n.pn {\n position: relative;\n flex: 0 0 142px;\n width: 142px;\n min-width: 0;\n display: flex;\n flex-direction: column;\n border-right: 1px solid var(--border);\n background: color-mix(in srgb, var(--surface) 94%, var(--surface-2));\n transition: flex-basis 160ms ease;\n}\n.pn--collapsed {\n flex-basis: 0;\n width: 0;\n border-right: 0;\n}\n.pn__header {\n min-height: 45px;\n padding: 9px 8px 8px 12px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 6px;\n border-bottom: 1px solid var(--border);\n}\n.pn__title {\n display: block;\n color: var(--text-strong);\n font-size: 12.5px;\n font-weight: 650;\n}\n.pn__count {\n display: block;\n margin-top: 1px;\n color: var(--faint);\n font-size: 10.5px;\n}\n.pn__collapse,\n.pn__reopen {\n border: 0;\n color: var(--muted);\n background: transparent;\n cursor: pointer;\n}\n.pn__collapse {\n width: 27px;\n height: 27px;\n display: grid;\n place-items: center;\n border-radius: 7px;\n}\n.pn__collapse:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n.pn__reopen {\n position: absolute;\n z-index: 3;\n top: 12px;\n right: -28px;\n width: 28px;\n height: 36px;\n padding: 0;\n display: grid;\n place-items: center;\n border: 1px solid var(--border-2);\n border-radius: 0 9px 9px 0;\n background: var(--surface);\n box-shadow: 2px 1px 6px rgba(0, 0, 0, 0.12);\n}\n.pn__reopen:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n.pn__list {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n padding: 8px 7px 22px;\n scrollbar-width: thin;\n}\n.pn__item {\n position: relative;\n width: 100%;\n min-width: 0;\n margin: 0 0 8px;\n padding: 5px;\n display: block;\n border: 1px solid transparent;\n border-radius: 8px;\n color: var(--muted);\n background: transparent;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n.pn__item:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n.pn__item--active {\n border-color: color-mix(in srgb, var(--accent) 70%, transparent);\n color: var(--text-strong);\n background: var(--accent-soft);\n}\n.pn__number {\n position: absolute;\n z-index: 2;\n top: 9px;\n left: 9px;\n min-width: 20px;\n height: 20px;\n padding: 0 5px;\n display: grid;\n place-items: center;\n border-radius: 5px;\n color: #fff;\n background: rgba(20, 20, 20, 0.68);\n font-size: 9.5px;\n font-weight: 650;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);\n backdrop-filter: blur(4px);\n}\n.pn__item--active .pn__number {\n color: #fff;\n background: var(--accent);\n}\n.pn__preview-wrap {\n height: 76px;\n min-width: 0;\n overflow: hidden;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 1px solid var(--border-2);\n border-radius: 4px;\n background: #fff;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);\n}\n.pn__item--active .pn__preview-wrap {\n border-color: var(--accent);\n box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent);\n}\n.pn__preview {\n width: 104px;\n min-height: 1px;\n display: block;\n overflow: hidden;\n pointer-events: none;\n}\n.pn__label {\n min-width: 0;\n margin-top: 5px;\n overflow: hidden;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n font-size: 10.5px;\n line-height: 1.35;\n}\n\n@container (max-width: 500px) {\n .pn {\n flex-basis: 124px;\n width: 124px;\n }\n .pn--collapsed {\n flex-basis: 0;\n width: 0;\n }\n .pn__preview-wrap {\n height: 66px;\n }\n}\n</style>\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PptxNavigationPane.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/PptxNavigationPane.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport {\n nextTick,\n onBeforeUnmount,\n onMounted,\n useTemplateRef,\n watch,\n type ComponentPublicInstance,\n} from 'vue'\nimport type { PptxViewer, SlideHandle } from '@aiden0z/pptx-renderer'\nimport Icon from '@/components/icons/Icon.vue'\nimport type { PptxNavigationItem } from '@/lib/pptxNavigation'\n\nconst props = defineProps<{\n viewer: PptxViewer\n items: readonly PptxNavigationItem[]\n activeIndex: number\n}>()\n\nconst emit = defineEmits<{ navigate: [index: number] }>()\nconst collapsed = defineModel<boolean>('collapsed', { default: false })\nconst listEl = useTemplateRef<HTMLElement>('list')\n\nconst thumbElements = new Map<number, HTMLElement>()\nconst thumbHandles = new Map<number, SlideHandle>()\nlet observer: IntersectionObserver | null = null\n\nfunction disposeThumbnails(): void {\n observer?.disconnect()\n observer = null\n for (const handle of thumbHandles.values()) handle.dispose()\n thumbHandles.clear()\n for (const element of thumbElements.values()) element.innerHTML = ''\n}\n\nfunction mountThumbnail(index: number, element: HTMLElement): void {\n if (thumbHandles.has(index)) return\n const handle = props.viewer.renderThumbnailToContainer(index, element, { width: 104 })\n if (!handle) return\n thumbHandles.set(index, handle)\n void handle.ready.catch(() => {\n if (thumbHandles.get(index) !== handle) return\n handle.dispose()\n thumbHandles.delete(index)\n element.innerHTML = ''\n })\n}\n\nasync function setupThumbnails(): Promise<void> {\n disposeThumbnails()\n if (collapsed.value) return\n await nextTick()\n const root = listEl.value\n if (!root) return\n\n if (typeof IntersectionObserver === 'undefined') {\n for (const [index, element] of thumbElements) mountThumbnail(index, element)\n return\n }\n\n observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue\n const element = entry.target as HTMLElement\n const index = Number(element.dataset.slideIndex)\n if (Number.isInteger(index)) mountThumbnail(index, element)\n observer?.unobserve(element)\n }\n },\n { root, rootMargin: '140px 0px' },\n )\n for (const element of thumbElements.values()) observer.observe(element)\n}\n\nfunction setThumbElement(index: number, value: Element | ComponentPublicInstance | null): void {\n const previous = thumbElements.get(index)\n if (previous && previous !== value) observer?.unobserve(previous)\n if (!(value instanceof HTMLElement)) {\n thumbElements.delete(index)\n return\n }\n thumbElements.set(index, value)\n observer?.observe(value)\n}\n\nwatch(() => props.viewer, setupThumbnails, { flush: 'post' })\nwatch(() => props.items, setupThumbnails, { flush: 'post' })\nwatch(collapsed, setupThumbnails, { flush: 'post' })\nwatch(\n () => props.activeIndex,\n async (index) => {\n await nextTick()\n listEl.value\n ?.querySelector<HTMLElement>(`[data-nav-slide-index=\"${index}\"]`)\n ?.scrollIntoView({ block: 'nearest' })\n },\n)\n\nonMounted(setupThumbnails)\nonBeforeUnmount(disposeThumbnails)\n</script>\n\n<template>\n <aside class=\"pn\" :class=\"{ 'pn--collapsed': collapsed }\" aria-label=\"幻灯片导航\">\n <button\n v-if=\"collapsed\"\n type=\"button\"\n class=\"pn__reopen\"\n aria-label=\"展开幻灯片导航\"\n title=\"展开幻灯片导航\"\n @click=\"collapsed = false\"\n >\n <Icon name=\"chevronRight\" :size=\"15\" />\n </button>\n\n <template v-else>\n <header class=\"pn__header\">\n <div>\n <strong class=\"pn__title\">幻灯片</strong>\n <span class=\"pn__count\">{{ items.length }} 张</span>\n </div>\n <button\n type=\"button\"\n class=\"pn__collapse\"\n aria-label=\"收起幻灯片导航\"\n title=\"收起幻灯片导航\"\n @click=\"collapsed = true\"\n >\n <Icon name=\"chevronLeft\" :size=\"15\" />\n </button>\n </header>\n\n <nav ref=\"list\" class=\"pn__list\" aria-label=\"幻灯片列表\">\n <button\n v-for=\"item in items\"\n :key=\"item.index\"\n type=\"button\"\n class=\"pn__item\"\n :class=\"{ 'pn__item--active': item.index === activeIndex }\"\n :data-nav-slide-index=\"item.index\"\n :aria-current=\"item.index === activeIndex ? 'page' : undefined\"\n :title=\"`${item.index + 1}. ${item.title}`\"\n @click=\"emit('navigate', item.index)\"\n >\n <span class=\"pn__number\">{{ item.index + 1 }}</span>\n <span class=\"pn__preview-wrap\">\n <span\n :ref=\"(value) => setThumbElement(item.index, value)\"\n class=\"pn__preview\"\n :data-slide-index=\"item.index\"\n aria-hidden=\"true\"\n />\n </span>\n <span class=\"pn__label\">{{ item.title }}</span>\n </button>\n </nav>\n </template>\n </aside>\n</template>\n\n<style scoped>\n.pn {\n position: relative;\n flex: 0 0 142px;\n width: 142px;\n min-width: 0;\n display: flex;\n flex-direction: column;\n border-right: 1px solid var(--border);\n background: color-mix(in srgb, var(--surface) 94%, var(--surface-2));\n transition: flex-basis 160ms ease;\n}\n.pn--collapsed {\n flex-basis: 0;\n width: 0;\n border-right: 0;\n}\n.pn__header {\n min-height: 45px;\n padding: 9px 8px 8px 12px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 6px;\n border-bottom: 1px solid var(--border);\n}\n.pn__title {\n display: block;\n color: var(--text-strong);\n font-size: 12.5px;\n font-weight: 650;\n}\n.pn__count {\n display: block;\n margin-top: 1px;\n color: var(--faint);\n font-size: 10.5px;\n}\n.pn__collapse,\n.pn__reopen {\n border: 0;\n color: var(--muted);\n background: transparent;\n cursor: pointer;\n}\n.pn__collapse {\n width: 27px;\n height: 27px;\n display: grid;\n place-items: center;\n border-radius: 7px;\n}\n.pn__collapse:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n.pn__reopen {\n position: absolute;\n z-index: 3;\n top: 12px;\n right: -28px;\n width: 28px;\n height: 36px;\n padding: 0;\n display: grid;\n place-items: center;\n border: 1px solid var(--border-2);\n border-radius: 0 9px 9px 0;\n background: var(--surface);\n box-shadow: 2px 1px 6px rgba(0, 0, 0, 0.12);\n}\n.pn__reopen:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n.pn__list {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n padding: 8px 7px 22px;\n scrollbar-width: thin;\n}\n.pn__item {\n position: relative;\n width: 100%;\n min-width: 0;\n margin: 0 0 8px;\n padding: 5px;\n display: block;\n border: 1px solid transparent;\n border-radius: 8px;\n color: var(--muted);\n background: transparent;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n.pn__item:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n.pn__item--active {\n border-color: color-mix(in srgb, var(--accent) 70%, transparent);\n color: var(--text-strong);\n background: var(--accent-soft);\n}\n.pn__number {\n position: absolute;\n z-index: 2;\n top: 9px;\n left: 9px;\n min-width: 20px;\n height: 20px;\n padding: 0 5px;\n display: grid;\n place-items: center;\n border-radius: 5px;\n color: #fff;\n background: rgba(20, 20, 20, 0.68);\n font-size: 9.5px;\n font-weight: 650;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);\n backdrop-filter: blur(4px);\n}\n.pn__item--active .pn__number {\n color: #fff;\n background: var(--accent);\n}\n.pn__preview-wrap {\n height: 76px;\n min-width: 0;\n overflow: hidden;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 1px solid var(--border-2);\n border-radius: 4px;\n background: #fff;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);\n}\n.pn__item--active .pn__preview-wrap {\n border-color: var(--accent);\n box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent);\n}\n.pn__preview {\n width: 104px;\n min-height: 1px;\n display: block;\n overflow: hidden;\n pointer-events: none;\n}\n.pn__label {\n min-width: 0;\n margin-top: 5px;\n overflow: hidden;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n font-size: 10.5px;\n line-height: 1.35;\n}\n\n@container (max-width: 500px) {\n .pn {\n flex-basis: 124px;\n width: 124px;\n }\n .pn--collapsed {\n flex-basis: 0;\n width: 0;\n }\n .pn__preview-wrap {\n height: 66px;\n }\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -2,6 +2,8 @@ import e from "../_virtual/_plugin-vue_export-helper.js";
|
|
|
2
2
|
import t from "./PptxViewer.vue_vue_type_script_setup_true_lang.js";
|
|
3
3
|
/* empty css */
|
|
4
4
|
//#region src/components/PptxViewer.vue
|
|
5
|
-
var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-
|
|
5
|
+
var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-72d11ba8"]]);
|
|
6
6
|
//#endregion
|
|
7
7
|
export { n as default };
|
|
8
|
+
|
|
9
|
+
//# sourceMappingURL=PptxViewer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PptxViewer.js","names":[],"sources":["../../src/components/PptxViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, shallowRef, useTemplateRef, watch } from 'vue'\nimport { PptxViewer, RECOMMENDED_ZIP_LIMITS } from '@aiden0z/pptx-renderer'\nimport PptxNavigationPane from '@/components/PptxNavigationPane.vue'\nimport { buildPptxNavigation, type PptxNavigationItem } from '@/lib/pptxNavigation'\n// pdfjs is only used as a fallback to rasterize SmartArt / EMF graphics the native renderer can't draw. The\n// library dynamic-imports these two ESM files inside its OWN web worker, so we hand it servable URLs (Vite's\n// `?url` emits the real assets and returns their URL — root-relative in the same-origin prod build, a dev\n// URL under `pnpm dev`). They're fetched lazily by that worker only when a slide actually needs the fallback.\nimport pdfModuleUrl from 'pdfjs-dist/build/pdf.min.mjs?url'\nimport pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url'\n\n/**\n * Side-viewer PowerPoint (`.pptx`/`.pptm`/`.ppsx`) renderer. Fetches the deck bytes from the download URL and\n * renders the OOXML PresentationML to HTML/SVG entirely in-browser via @aiden0z/pptx-renderer\n * (https://github.com/aiden0z/pptx-renderer) — a vertically scrollable list of slides, each scaled to fit the\n * pane (`fitMode: 'contain'`, the library default). No CDN / network beyond the file fetch (+ the lazy pdfjs\n * fallback above): the renderer bundles its own jszip + echarts (charts) and inlines media. The library writes\n * inline styles only, so nothing leaks onto the page.\n *\n * Two entry shapes mirror the other viewers (docx/xlsx/zip): a real download URL for a held attachment, or a\n * blob URL minted by the archive browsers from already-extracted bytes.\n */\nconst props = defineProps<{\n /** Download/blob URL of the .pptx bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = shallowRef<Status>('loading')\nconst errorMsg = shallowRef('')\n// Keep the scroll viewport separate from the renderer mount: `fitMode: 'contain'` measures the mount's\n// `clientWidth`, so putting visual padding on that same element makes the slide consume the padded width and\n// overflow horizontally. Both stay mounted while loading so `open()` always sees real dimensions.\nconst scrollEl = useTemplateRef<HTMLElement>('scrollEl')\nconst docEl = useTemplateRef<HTMLElement>('docEl')\nconst navViewer = shallowRef<PptxViewer | null>(null)\nconst navItems = shallowRef<PptxNavigationItem[]>([])\nconst activeSlide = shallowRef(0)\nconst navCollapsed = shallowRef(false)\n// Live viewer instance — destroyed on re-render / unmount (it holds echarts instances, observers, and its\n// pdfjs worker, none of which GC on their own).\nlet viewer: PptxViewer | null = null\nlet navigationLock: number | null = null\nlet navigationLockTimer: ReturnType<typeof setTimeout> | undefined\n\nfunction disposeViewer(): void {\n navigationLock = null\n if (navigationLockTimer) clearTimeout(navigationLockTimer)\n navigationLockTimer = undefined\n navViewer.value = null\n navItems.value = []\n if (viewer) {\n try {\n viewer.destroy()\n } catch {\n /* already torn down — ignore */\n }\n viewer = null\n }\n}\n\nasync function render(): Promise<void> {\n const el = docEl.value\n const scroll = scrollEl.value\n if (!props.url || !el || !scroll) return\n status.value = 'loading'\n errorMsg.value = ''\n disposeViewer()\n el.innerHTML = '' // drop any prior render (e.g. on retry / switching files)\n const url = props.url\n try {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n // Stale guard: a newer render may have started while we awaited the fetch.\n if (props.url !== url || docEl.value !== el || scrollEl.value !== scroll) return\n const v = await PptxViewer.open(buf, el, {\n zipLimits: RECOMMENDED_ZIP_LIMITS,\n scrollContainer: scroll, // the outer viewport drives which slides the windowed list mounts\n listOptions: { windowed: true }, // virtualize: only mount slides near the viewport (large decks stay fast)\n pdfjs: { moduleUrl: pdfModuleUrl, workerUrl: pdfWorkerUrl },\n onSlideChange: (index) => {\n if (navigationLock !== null && index !== navigationLock) return\n if (props.url === url) activeSlide.value = index\n },\n })\n // Stale guard again: the file may have changed while parse/render ran — tear the new viewer down.\n if (props.url !== url || docEl.value !== el || scrollEl.value !== scroll) {\n try {\n v.destroy()\n } catch {\n /* ignore */\n }\n return\n }\n viewer = v\n navViewer.value = v\n navItems.value = v.presentationData ? buildPptxNavigation(v.presentationData) : []\n activeSlide.value = v.currentSlideIndex\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction navigateToSlide(index: number): void {\n navigationLock = index\n activeSlide.value = index\n if (navigationLockTimer) clearTimeout(navigationLockTimer)\n navigationLockTimer = setTimeout(() => {\n navigationLock = null\n navigationLockTimer = undefined\n }, 1000)\n void viewer?.goToSlide(index, { behavior: 'smooth', block: 'start' })\n}\n\nonMounted(render)\nwatch(() => props.url, render)\nonBeforeUnmount(() => {\n disposeViewer()\n if (docEl.value) docEl.value.innerHTML = ''\n})\n</script>\n\n<template>\n <div class=\"pv\">\n <PptxNavigationPane\n v-if=\"status === 'ready' && navViewer\"\n v-model:collapsed=\"navCollapsed\"\n :viewer=\"navViewer\"\n :items=\"navItems\"\n :active-index=\"activeSlide\"\n @navigate=\"navigateToSlide\"\n />\n <div class=\"pv__stage\">\n <!-- The padded scroll viewport wraps a padding-free mount whose clientWidth is the real slide width. -->\n <div ref=\"scrollEl\" class=\"pv__scroll\">\n <div ref=\"docEl\" class=\"pv__doc\" />\n </div>\n <!-- Loading / error as an overlay so the container underneath keeps its dimensions. -->\n <div v-if=\"status === 'loading'\" class=\"pv__overlay\">\n <p class=\"pv__msg\">正在渲染 PPT…</p>\n </div>\n <div v-else-if=\"status === 'error'\" class=\"pv__overlay\">\n <div class=\"pv__msg pv__msg--error\">\n <p>渲染失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"pv__retry\" @click=\"render\">重试</button>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.pv {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: row;\n background: var(--surface);\n container-type: inline-size;\n}\n.pv__stage {\n position: relative;\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n/* Scroll region holding the rendered slides. The deck keeps its native appearance (the library scales each\n * slide to fit this width); we only tint the surrounding \"desk\" to the app surface so it sits naturally in\n * both light and dark themes. The renderer writes inline styles, so no :deep overrides are needed. */\n.pv__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface-2);\n padding: 16px 14px 40px;\n}\n.pv__doc {\n width: 100%;\n min-width: 0;\n}\n/* Overlay covers the (empty) container while loading or on error, without unmounting it. */\n.pv__overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: flex-start;\n justify-content: flex-start;\n background: var(--surface);\n}\n.pv__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.pv__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.pv__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.pv__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n</style>\n"],"mappings":""}
|