Files
nidus/internal/web/templates/files.templ
T
arnefandCopilot b2f39bd1f6 Add delete, inline preview, and mobile list layout to the file browser
- DELETE /files/{path} recursively removes a file/folder (root itself
  can't be deleted, 404 on missing paths); wired to a new "Delete"
  action in the web UI with a confirmation prompt.
- Files now open inline (Content-Disposition: inline) so browsers can
  play/preview natively-supported types (video, audio, images, PDF)
  directly instead of always forcing a download. A separate
  "Download" link (?download=1) still forces a save-as.
- Narrow screens get a stacked card list (name, size, modified date,
  download/delete actions) instead of a squeezed table, so nothing is
  hidden or requires horizontal scrolling; the table layout is kept
  unchanged for sm+ screens.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-21 09:15:40 +02:00

167 lines
5.7 KiB
Templ

package templates
// Breadcrumb is one segment of the current directory path shown above the
// file listing, e.g. "docs" linking to "/web/files/docs/".
type Breadcrumb struct {
Name string
Path string // relative path (no leading slash) used to build the link
}
// FileEntry is a single row (directory or file) in the file browser.
type FileEntry struct {
Name string
IsDir bool
Size string // human-readable, empty for directories
ModTime string
RelPath string // relative path (no leading slash) used to build the link
}
templ FilesPage(username string, breadcrumbs []Breadcrumb, entries []FileEntry, currentPath string) {
@Layout("Files", username) {
<div class="flex items-center justify-between mb-6 gap-4 flex-wrap">
<h1 class="text-2xl font-semibold">Files</h1>
<div class="flex gap-2 flex-wrap">
<button id="new-folder-button" type="button"
class="bg-white border border-gray-300 rounded-md px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50">
New folder
</button>
<label class="cursor-pointer bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700">
Upload files
<input id="upload-files-input" type="file" multiple class="hidden"/>
</label>
<label class="cursor-pointer bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700">
Upload folder
<input id="upload-folder-input" type="file" webkitdirectory multiple class="hidden"/>
</label>
</div>
</div>
<nav class="text-sm text-gray-500 mb-4 flex flex-wrap items-center gap-1">
<a href="/web/files/" class="hover:underline text-indigo-600">home</a>
for _, bc := range breadcrumbs {
<span>/</span>
<a href={ templ.URL("/web/files/" + bc.Path + "/") } class="hover:underline text-indigo-600">{ bc.Name }</a>
}
</nav>
<div
id="file-drop-zone"
data-current-path={ currentPath }
data-upload-url={ "/web/files/" + currentPath }
class="bg-white rounded-lg border-2 border-dashed border-gray-200 p-2 transition-colors"
>
<!-- Table layout for wider screens (sm and up). -->
<table class="hidden sm:table w-full text-sm table-fixed">
<colgroup>
<col class="w-auto"/>
<col class="w-20"/>
<col class="w-36"/>
<col class="w-16"/>
</colgroup>
<thead>
<tr class="text-left text-gray-400 border-b border-gray-100">
<th class="py-2 px-3 font-medium">Name</th>
<th class="py-2 px-3 font-medium">Size</th>
<th class="py-2 px-3 font-medium">Modified</th>
<th class="py-2 px-3 font-medium"></th>
</tr>
</thead>
<tbody>
for _, e := range entries {
<tr class="border-b border-gray-50 hover:bg-gray-50">
<td class="py-2 px-3 break-all">
@fileEntryLink(e)
</td>
<td class="py-2 px-3 text-gray-500 whitespace-nowrap">{ e.Size }</td>
<td class="py-2 px-3 text-gray-500 whitespace-nowrap">{ e.ModTime }</td>
<td class="py-2 px-3 text-right whitespace-nowrap">
<div class="flex items-center justify-end gap-3">
@fileActions(e)
</div>
</td>
</tr>
}
if len(entries) == 0 {
<tr>
<td colspan="4" class="py-6 px-3 text-center text-gray-400">
This folder is empty drag &amp; drop files or folders here, or use the upload buttons above.
</td>
</tr>
}
</tbody>
</table>
<!-- Stacked card layout for narrow screens: same information as
the table above (name, size, modified, delete), just
wrapped onto its own lines instead of squeezed into
columns, so nothing needs to be hidden or scrolled to. -->
<ul class="sm:hidden divide-y divide-gray-100">
for _, e := range entries {
<li class="py-2.5 px-1">
<div class="break-all">
@fileEntryLink(e)
</div>
<div class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-gray-500 pl-6">
if e.Size != "" {
<span>{ e.Size }</span>
}
<span>{ e.ModTime }</span>
<span class="ml-auto flex items-center gap-3">
@fileActions(e)
</span>
</div>
</li>
}
if len(entries) == 0 {
<li class="py-6 px-3 text-center text-gray-400 text-sm">
This folder is empty drag &amp; drop files or folders here, or use the upload buttons above.
</li>
}
</ul>
</div>
<p id="upload-status" class="mt-3 text-sm text-gray-500"></p>
<script type="module" src="/web/static/files.js"></script>
}
}
templ fileEntryLink(e FileEntry) {
if e.IsDir {
<a href={ templ.URL("/web/files/" + e.RelPath + "/") } class="flex items-center gap-2 text-indigo-600 hover:underline">
<span aria-hidden="true">📁</span>{ e.Name }
</a>
} else {
<a href={ templ.URL("/web/files/" + e.RelPath) } target="_blank" rel="noopener" class="flex items-center gap-2 text-gray-700 hover:underline">
<span aria-hidden="true">📄</span>{ e.Name }
</a>
}
}
// fileActions renders the per-entry action buttons: a Download link
// (forces a save-as via ?download=1, since the name link above now opens
// files inline so the browser can play/preview natively-supported types
// like video, audio, images, or PDFs) plus the Delete button. Folders have
// no download link since they aren't served as a single file.
templ fileActions(e FileEntry) {
if !e.IsDir {
<a
href={ templ.URL("/web/files/" + e.RelPath + "?download=1") }
class="text-gray-500 hover:text-indigo-600 hover:underline text-xs font-medium"
>
Download
</a>
}
@fileDeleteButton(e)
}
templ fileDeleteButton(e FileEntry) {
<button
type="button"
class="delete-entry-button text-red-500 hover:text-red-700 text-xs font-medium"
data-path={ e.RelPath }
data-name={ e.Name }
>
Delete
</button>
}