- 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>
167 lines
5.7 KiB
Templ
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 & 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 & 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>
|
|
}
|
|
|