ci / docker (push) Successful in 13m28s
- shift-click selects ranges in the gallery; selection bar gains
'Delete N' with a single confirm (POST /api/photos/delete)
- PATCH /api/shares/{id}: allow_download and expiry editable in place,
token and client feedback preserved
- Gallery: re-attach ResizeObserver via callback ref — after a filter
with zero matches the gallery stayed blank at width 0
59 lines
1.4 KiB
React
59 lines
1.4 KiB
React
export function fmtBytes(bytes) {
|
|
if (!bytes) return '0 B'
|
|
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
const i = Math.min(Math.floor(Math.log2(bytes) / 10), units.length - 1)
|
|
const value = bytes / 2 ** (10 * i)
|
|
return `${value >= 100 || i === 0 ? Math.round(value) : value.toFixed(1)} ${units[i]}`
|
|
}
|
|
|
|
export default function SelectionBar({
|
|
count,
|
|
total,
|
|
selectedBytes,
|
|
totalBytes,
|
|
onSelectAll,
|
|
onClear,
|
|
onDownload,
|
|
onDownloadAll,
|
|
onDelete,
|
|
}) {
|
|
if (total === 0) return null
|
|
|
|
if (count === 0) {
|
|
return (
|
|
<div className="select-bar">
|
|
<span className="select-count">
|
|
{total} photo{total === 1 ? '' : 's'} · {fmtBytes(totalBytes)}
|
|
</span>
|
|
<button className="btn btn-primary" onClick={onDownloadAll}>
|
|
Download all as ZIP
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="select-bar">
|
|
<span className="select-count">
|
|
{count} of {total} selected
|
|
</span>
|
|
{count < total && (
|
|
<button className="btn" onClick={onSelectAll}>
|
|
Select all
|
|
</button>
|
|
)}
|
|
<button className="btn" onClick={onClear}>
|
|
Clear
|
|
</button>
|
|
<button className="btn btn-primary" onClick={onDownload}>
|
|
Download {count} as ZIP ({fmtBytes(selectedBytes)})
|
|
</button>
|
|
{onDelete && (
|
|
<button className="btn btn-danger" onClick={onDelete}>
|
|
Delete {count}
|
|
</button>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|