diff --git a/astro.config.mjs b/astro.config.mjs index 2528d402..028fbc54 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,8 +1,31 @@ import { defineConfig } from "astro/config"; +// Prevent duplicate customElements.define() errors in dev mode. +// The myst-awesome library imports Web Awesome components from many +// separate + + + + + + + diff --git a/src/pages/browse/publication/[id].astro b/src/pages/browse/publication/[id].astro index 36c8a4b4..f61233f2 100644 --- a/src/pages/browse/publication/[id].astro +++ b/src/pages/browse/publication/[id].astro @@ -27,8 +27,8 @@ export async function getStaticPaths() { const paths = pages.map((page, index) => { const id = page.id; - console.log(`Processing page ${index + 1}: "${page.id}"`); - console.log('Page contents:', page); + // console.log(`Processing page ${index + 1}: "${page.id}"`); + // console.log('Page contents:', page); return { params: { id }, @@ -36,10 +36,10 @@ export async function getStaticPaths() { }; }); - console.log(`Generated ${paths.length} paths for [id].astro`); - paths.forEach((path, i) => { - console.log(` Path ${i + 1}: id="${path.params.id}"`); - }); + // console.log(`Generated ${paths.length} paths for [id].astro`); + // paths.forEach((path, i) => { + // console.log(` Path ${i + 1}: id="${path.params.id}"`); + // }); return paths; } catch (error) { @@ -101,74 +101,6 @@ interface TreeNode { children?: TreeNode[]; } -function buildFileTree(files: any[]): TreeNode[] { - const root: TreeNode[] = []; - const folderMap = new Map(); - - files.forEach((file: any) => { - // Remove 'root/code/' prefix - const relativePath = file.title.replace('root/code/', ''); - const parts = relativePath.split('/'); - const fileName = parts[parts.length - 1]; - - // Build folder structure - let currentPath = ''; - for (let i = 0; i < parts.length - 1; i++) { - const folderName = parts[i]; - const parentPath = currentPath; - currentPath = currentPath ? `${currentPath}/${folderName}` : folderName; - - if (!folderMap.has(currentPath)) { - const folderNode: TreeNode = { - name: folderName, - path: currentPath, - type: 'folder', - children: [] - }; - folderMap.set(currentPath, folderNode); - - // Add to parent or root - if (parentPath) { - const parent = folderMap.get(parentPath); - parent?.children?.push(folderNode); - } else { - root.push(folderNode); - } - } - } - - // Add file - const fileNode: TreeNode = { - name: fileName, - path: relativePath, - type: 'file', - url: file.url, - size: file.extra?.size_bytes ? formatBytes(file.extra.size_bytes) : undefined, - id: file.id // CID from download object - }; - - if (parts.length === 1) { - root.push(fileNode); - } else { - const parentPath = parts.slice(0, -1).join('/'); - const parent = folderMap.get(parentPath); - parent?.children?.push(fileNode); - } - }); - - return root; -} - -function formatBytes(bytes: number): string { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${Math.round(bytes / Math.pow(k, i) * 10) / 10} ${sizes[i]}`; -} - -const fileTree = buildFileTree(codeFiles); - // Determine medical imaging file extensions const meshExtensions = ['.stl', '.ply', '.mz3', '.off', '.obj', '.fsa', '.fsb', '.byu', '.x3d', '.gii', '.vtp']; const volumeExtensions = ['.nii', '.nii.gz', '.dcm', '.nrrd', '.mha', '.mhd', '.vtk', '.vti', '.aim', '.isq', '.gipl', '.gipl.gz', '.hdf5', '.iwi', '.iwi.cbor', '.iwi.cbor.zst', '.lsm', '.mnc', '.mnc.gz', '.mgh', '.mgz', '.mgh.gz', '.mrc', '.pic', '.fdf', '.swc', '.fz', '.gqi', '.qsdr', '.mih', '.mif', '.v', '.v16', '.vmr', '.head', '.tract', '.tt', '.trx', '.trk', '.tck', '.annot', '.hdr', '.img']; @@ -262,7 +194,8 @@ function findDefaultFile(nodes: TreeNode[]): TreeNode | null { return null; } -defaultFile = findDefaultFile(fileTree); +// defaultFile is now determined client-side +// defaultFile = findDefaultFile(fileTree); // Function to replace email addresses with icon links function replaceEmailsWithIcons(html: string): string { @@ -334,14 +267,14 @@ function getFileIcon(filename: string): string { } --- -{/* Store tree rendering function in template section */} - - -{fileTree.length > 0 && ( - )} @@ -538,53 +471,13 @@ function getFileIcon(filename: string): string {
- {fileTree.map((node) => { - // Recursive rendering function defined inline - const renderNode = (n: TreeNode): any => { - if (n.type === 'folder') { - return ( - - - {n.name} - {n.children?.map((child) => renderNode(child))} - - ); - } else { - return ( - - - {n.name} - {n.size && ( - - {n.size} - - - - )} - - ); - } - }; - return renderNode(node); - })} + + +
+ +

Loading file tree...

+
@@ -706,6 +599,9 @@ function getFileIcon(filename: string): string { import { dicomLoader } from '@niivue/dicom-loader'; import { downloadZip } from 'client-zip'; + // File tree builder (lazy-loading) + import { FileTreeManager } from '../../../lib/fileTreeBuilder'; + // Import highlight.js core and individual languages import hljs from 'highlight.js/lib/core'; import python from 'highlight.js/lib/languages/python'; @@ -840,7 +736,7 @@ function getFileIcon(filename: string): string { }; // Helper function to find companion file for MHD/RAW pairs - const findCompanionFile = (filename: string, fileTreeData: any[]): string | null => { + const findCompanionFile = (filename: string): string | null => { const baseNameMatch = filename.match(/^(.+)\.(mhd|raw)$/i); if (!baseNameMatch) return null; @@ -849,25 +745,20 @@ function getFileIcon(filename: string): string { const companionExt = currentExt === 'mhd' ? 'raw' : 'mhd'; const companionFilename = `${baseName}.${companionExt}`; - // Search recursively in the file tree - const searchTree = (nodes: any[]): string | null => { - for (const node of nodes) { - if (node.type === 'file' && node.name.toLowerCase() === companionFilename.toLowerCase()) { - return node.url; - } - if (node.children) { - const found = searchTree(node.children); - if (found) return found; - } + // Search in flat codeFiles array + const codeFiles = (window as any).codeFilesData || []; + for (const file of codeFiles) { + const fileTitle = file.title || ''; + const fileName = fileTitle.split('/').pop() || ''; + if (fileName.toLowerCase() === companionFilename.toLowerCase()) { + return file.url; } - return null; - }; - - return searchTree(fileTreeData); + } + return null; }; // Helper function to find companion file for HDR/IMG pairs - const findHdrImgCompanion = (filename: string, fileTreeData: any[]): string | null => { + const findHdrImgCompanion = (filename: string): string | null => { const baseNameMatch = filename.match(/^(.+)\.(hdr|img)$/i); if (!baseNameMatch) return null; @@ -876,21 +767,16 @@ function getFileIcon(filename: string): string { const companionExt = currentExt === 'hdr' ? 'img' : 'hdr'; const companionFilename = `${baseName}.${companionExt}`; - // Search recursively in the file tree - const searchTree = (nodes: any[]): string | null => { - for (const node of nodes) { - if (node.type === 'file' && node.name.toLowerCase() === companionFilename.toLowerCase()) { - return node.url; - } - if (node.children) { - const found = searchTree(node.children); - if (found) return found; - } + // Search in flat codeFiles array + const codeFiles = (window as any).codeFilesData || []; + for (const file of codeFiles) { + const fileTitle = file.title || ''; + const fileName = fileTitle.split('/').pop() || ''; + if (fileName.toLowerCase() === companionFilename.toLowerCase()) { + return file.url; } - return null; - }; - - return searchTree(fileTreeData); + } + return null; }; // Helper function to combine MHD and RAW files into MHA format @@ -1006,7 +892,7 @@ function getFileIcon(filename: string): string { await nv.loadDicoms([{ url, name: filename }]); } else if (ext.endsWith('.mhd') || ext.endsWith('.raw')) { // Check for MHD/RAW companion files and combine them - const companionUrl = findCompanionFile(filename, (window as any).fileTreeData); + const companionUrl = findCompanionFile(filename); if (companionUrl) { // Determine which is MHD and which is RAW const isMhd = ext.endsWith('.mhd'); @@ -1032,7 +918,7 @@ function getFileIcon(filename: string): string { } } else if (ext.endsWith('.hdr') || ext.endsWith('.img')) { // Check for HDR/IMG companion files - const companionUrl = findHdrImgCompanion(filename, (window as any).fileTreeData); + const companionUrl = findHdrImgCompanion(filename); if (companionUrl) { // Determine which is HDR and which is IMG const isHdr = ext.endsWith('.hdr'); @@ -1201,6 +1087,15 @@ function getFileIcon(filename: string): string { }); } + // File tree manager instance (lazy-loading tree builder) + const fileTreeManager = new FileTreeManager(); + + // Build file tree dynamically (only top-level items, children loaded on expand) + const buildFileTree = () => { + const codeFiles = (window as any).codeFilesData; + fileTreeManager.initialize(codeFiles); + }; + // Handle download all files as ZIP const downloadAllButton = document.getElementById('download-all-button') as HTMLButtonElement; if (downloadAllButton) { @@ -1295,14 +1190,30 @@ function getFileIcon(filename: string): string { } if (tabName === 'files') { - // Load default file if available - const defaultFileEl = document.querySelector('.selected-file') as HTMLElement; - if (defaultFileEl && currentViewerType === 'none') { - const url = defaultFileEl.dataset.url; - const path = defaultFileEl.dataset.path; - if (url && path) { - const filename = path.split('/').pop() || 'file'; - await loadFile(url, filename); + // Build file tree on first visit + buildFileTree(); + + // Load first available file if nothing loaded yet + // Priority order matches original findDefaultFile: volume -> mesh -> image -> any + if (currentViewerType === 'none') { + const codeFiles = (window as any).codeFilesData || []; + if (codeFiles.length > 0) { + const volumeExts = (window as any).volumeExtensions || []; + const meshExts = (window as any).meshExtensions || []; + const imageExts = (window as any).imageExtensions || []; + + const findByExtensions = (exts: string[]) => + codeFiles.find((f: any) => exts.some((ext: string) => f.title?.toLowerCase().endsWith(ext))); + + let defaultFile = findByExtensions(volumeExts); + if (!defaultFile) defaultFile = findByExtensions(meshExts); + if (!defaultFile) defaultFile = findByExtensions(imageExts); + if (!defaultFile) defaultFile = codeFiles[0]; + + if (defaultFile?.url && defaultFile?.title) { + const filename = defaultFile.title.split('/').pop() || 'file'; + await loadFile(defaultFile.url, filename); + } } } } @@ -1721,6 +1632,31 @@ function getFileIcon(filename: string): string { --indent-guide-width: 1px; } + /* File tree loading placeholder */ + .file-tree-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--wa-space-2xl); + color: var(--wa-color-text-quiet); + } + + .file-tree-loading p { + margin-top: var(--wa-space-m); + font-size: var(--wa-font-size-m); + } + + .loading-spinner { + font-size: 2rem; + animation: spin 1s linear infinite; + } + + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + .file-size { font-size: var(--wa-font-size-xs); color: var(--wa-color-text-quiet); diff --git a/src/pages/index.astro b/src/pages/index.astro index a863d915..f809d34d 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -61,10 +61,10 @@ const featuredArticle = { Learn more about our vision →

- +