Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/deploy-test.yml
Original file line number Diff line number Diff line change
@@ -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
29 changes: 10 additions & 19 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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/<repo-name>/
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
14 changes: 13 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Typography className="text-foreground">Reading archive…</Typography>;
}
Expand Down Expand Up @@ -101,7 +113,7 @@ function Explorer({ url, initialEntry, initialMaximized, initialCollapsed }: Exp

<div className={`grid gap-3 min-h-[40vh] lg:min-h-0 lg:flex-1 lg:overflow-hidden lg:grid-rows-1 ${maximized ? 'grid-cols-1' : 'grid-cols-1 lg:grid-cols-2'}`}>
{!maximized ? (
<EntryList entries={zip.entries} onSelect={handleSelect} />
<EntryList entries={zip.entries} onSelect={handleSelect} onDownload={handleDownload} />
) : null}
{selected ? (
<EntryPreview
Expand Down
51 changes: 47 additions & 4 deletions src/components/EntryList.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { useMemo, useState } from 'react';
import { Typography } from '@material-tailwind/react';
import { HiFolder, HiChevronRight, HiChevronDown } from 'react-icons/hi2';
import { Typography, Tooltip } from '@material-tailwind/react';
import { HiFolder, HiChevronRight, HiChevronDown, HiArrowDownTray, HiOutlineMagnifyingGlass } from 'react-icons/hi2';
import { TbFile } from 'react-icons/tb';
import { HiOutlineMagnifyingGlass } from 'react-icons/hi2';

import { formatBytes } from '@/lib/format';
import type { ZipEntry } from '@/lib/types';
Expand All @@ -22,12 +21,29 @@ function initialExpanded(_root: DirNode): Set<string> {
type EntryListProps = {
readonly entries: ZipEntry[];
readonly onSelect: (entry: ZipEntry) => void;
readonly onDownload?: (entry: ZipEntry) => Promise<void>;
};

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<Set<string>>(() => initialExpanded(tree));
const [filter, setFilter] = useState('');
const [downloading, setDownloading] = useState<Set<string>>(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 => {
Expand Down Expand Up @@ -83,6 +99,7 @@ export default function EntryList({ entries, onSelect }: EntryListProps) {
<th className="px-3 py-2 font-medium">Name</th>
<th className="px-3 py-2 text-right font-medium">Size</th>
<th className="px-3 py-2 text-right font-medium">Compressed</th>
{onDownload ? <th className="px-2 py-2" /> : null}
</tr>
</thead>
<tbody>
Expand Down Expand Up @@ -138,6 +155,32 @@ export default function EntryList({ entries, onSelect }: EntryListProps) {
? '—'
: formatBytes(row.entry.compressedSize)}
</td>
{onDownload ? (
<td className="px-2 py-1.5">
{!row.isDirectory && row.entry ? (
<Tooltip>
<Tooltip.Trigger as="div">
<button
className="flex items-center justify-center rounded p-1 text-foreground/60 hover:bg-surface hover:text-foreground disabled:opacity-40"
disabled={downloading.has(row.entry.name)}
onClick={e => handleDownload(e, row.entry!)}
aria-label="Download"
>
{downloading.has(row.entry.name) ? (
<span className="h-4 w-4 animate-spin rounded-full border-2 border-foreground/30 border-t-foreground" />
) : (
<HiArrowDownTray className="h-4 w-4" />
)}
</button>
</Tooltip.Trigger>
<Tooltip.Content>
Download
<Tooltip.Arrow />
</Tooltip.Content>
</Tooltip>
) : null}
</td>
) : null}
</tr>
);
})}
Expand Down