From c84d9acc9805339281e054c698a9c8a42131ea13 Mon Sep 17 00:00:00 2001 From: Mark Kittisopikul Date: Sat, 30 May 2026 12:35:00 -0400 Subject: [PATCH 1/2] feat: add per-file download button to EntryList Adds a HiArrowDownTray icon button on each file row. Clicking it fetches the entry bytes via zip.readBytes, wraps them in a Blob URL, and triggers a browser download using a temporary anchor element. A CSS spinner replaces the icon while the download is in flight, and stopPropagation prevents the row click from also opening the preview. Co-Authored-By: Claude Sonnet 4.6 --- src/App.tsx | 14 +++++++++- src/components/EntryList.tsx | 51 +++++++++++++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 3381712..da31bb1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -67,6 +67,18 @@ function Explorer({ url, initialEntry, initialMaximized, initialCollapsed }: Exp updateUrlState({ entry: null, maximized: false }); }; + const handleDownload = async (entry: { name: string }) => { + if (!archive.data) return; + const bytes = await archive.data.zip.readBytes(entry.name); + const blob = new Blob([bytes]); + const href = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = href; + a.download = entry.name.split('/').pop() ?? entry.name; + a.click(); + URL.revokeObjectURL(href); + }; + if (archive.isLoading) { return Reading archive…; } @@ -101,7 +113,7 @@ function Explorer({ url, initialEntry, initialMaximized, initialCollapsed }: Exp
{!maximized ? ( - + ) : null} {selected ? ( { type EntryListProps = { readonly entries: ZipEntry[]; readonly onSelect: (entry: ZipEntry) => void; + readonly onDownload?: (entry: ZipEntry) => Promise; }; -export default function EntryList({ entries, onSelect }: EntryListProps) { +export default function EntryList({ entries, onSelect, onDownload }: EntryListProps) { const tree = useMemo(() => buildTree(entries), [entries]); const [expanded, setExpanded] = useState>(() => initialExpanded(tree)); const [filter, setFilter] = useState(''); + const [downloading, setDownloading] = useState>(new Set()); + + const handleDownload = async (e: React.MouseEvent, entry: ZipEntry) => { + e.stopPropagation(); + if (!onDownload || downloading.has(entry.name)) return; + setDownloading(prev => new Set(prev).add(entry.name)); + try { + await onDownload(entry); + } finally { + setDownloading(prev => { + const next = new Set(prev); + next.delete(entry.name); + return next; + }); + } + }; const toggle = (path: string) => { setExpanded(prev => { @@ -83,6 +99,7 @@ export default function EntryList({ entries, onSelect }: EntryListProps) { Name Size Compressed + {onDownload ? : null} @@ -138,6 +155,32 @@ export default function EntryList({ entries, onSelect }: EntryListProps) { ? '—' : formatBytes(row.entry.compressedSize)} + {onDownload ? ( + + {!row.isDirectory && row.entry ? ( + + + + + + Download + + + + ) : null} + + ) : null} ); })} From d0b4e6766fbd55905a6c103008e0a4e99bea0257 Mon Sep 17 00:00:00 2001 From: Mark Kittisopikul Date: Sat, 30 May 2026 12:37:23 -0400 Subject: [PATCH 2/2] ci: switch to branch-based Pages deployment; add test subdirectory workflow Replaces the artifact-based deploy-pages approach with JamesIves/github-pages-deploy-action so that multiple branches can each own a subdirectory of the gh-pages branch without wiping each other. - deploy.yml (main): deploys to root of gh-pages, clean-exclude: test - deploy-test.yml (test): deploys to test/ subdirectory at /zipglancer/test/ Requires changing the GitHub Pages source to "Deploy from a branch: gh-pages" in the repository settings. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/deploy-test.yml | 38 +++++++++++++++++++++++++++++++ .github/workflows/deploy.yml | 29 ++++++++--------------- 2 files changed, 48 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/deploy-test.yml diff --git a/.github/workflows/deploy-test.yml b/.github/workflows/deploy-test.yml new file mode 100644 index 0000000..4fac88e --- /dev/null +++ b/.github/workflows/deploy-test.yml @@ -0,0 +1,38 @@ +name: Deploy test branch to GitHub Pages subdirectory + +on: + push: + branches: [test] + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: pages-test + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: '22' + cache: npm + + - run: npm ci + + - name: Build + run: npm run build + env: + ZIPGLANCER_BASE: /${{ github.event.repository.name }}/test/ + + - name: Deploy to gh-pages (test/) + uses: JamesIves/github-pages-deploy-action@v4 + with: + branch: gh-pages + folder: dist + target-folder: test diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f75f7d5..694c5e2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,16 +6,14 @@ on: workflow_dispatch: permissions: - contents: read - pages: write - id-token: write + contents: write concurrency: - group: pages + group: pages-main cancel-in-progress: false jobs: - build: + deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -30,20 +28,13 @@ jobs: - name: Build run: npm run build env: - # Base path matches the repo name so assets resolve correctly under - # https://JaneliaSciComp.github.io// ZIPGLANCER_BASE: /${{ github.event.repository.name }}/ - - uses: actions/upload-pages-artifact@v5 + - name: Deploy to gh-pages (root) + uses: JamesIves/github-pages-deploy-action@v4 with: - path: dist - - deploy: - needs: build - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - id: deployment - uses: actions/deploy-pages@v5 + branch: gh-pages + folder: dist + # Preserve subdirectory deployments (e.g. test/) from other branches. + clean-exclude: | + test