feat(preview): add file preview page with metadata and styling
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m48s

Implement a rich file preview interface to allow users to view file
contents directly in the browser.

Changes include:
- Exposing raw file size (`SizeBytes`) in the download handler's file view.
- Adding comprehensive CSS styling for the preview layout and cards.
- Integrating Prism.js for syntax highlighting of code files.
- Updating Content Security Policy (CSP) headers to permit inline styles and frame sources required by the preview components.
- Adding unit tests to ensure preview metadata attributes are correctly rendered on the download page.
This commit is contained in:
2026-06-03 14:28:50 +03:00
parent e17c5e92a7
commit 3a0dd04e61
12 changed files with 1893 additions and 36 deletions

View File

@@ -60,6 +60,7 @@
<script defer src="/static/js/30-token-copy.js?version={{.AppVersion}}"></script>
<script defer src="/static/js/35-pagination.js?version={{.AppVersion}}"></script>
<script defer src="/static/js/40-upload.js?version={{.AppVersion}}"></script>
<script defer src="/static/js/45-preview.js?version={{.AppVersion}}"></script>
</head>
<body class="dark">
<a class="skip-link" href="#main">Skip to content</a>

View File

@@ -1,8 +1,8 @@
{{define "preview.html"}}{{template "base" .}}{{end}}
{{define "content"}}
<section class="download-view" aria-labelledby="preview-title">
<div class="card download-card">
<section class="download-view preview-view" aria-labelledby="preview-title">
<div class="card download-card preview-card">
<div class="card-content">
{{if .Data.Locked}}
<div class="file-emblem" aria-hidden="true">
@@ -12,23 +12,65 @@
<p class="download-subtitle">Unlock the box before viewing this file.</p>
<a class="button button-primary button-wide" href="/d/{{.Data.Box.ID}}">Unlock box</a>
{{else}}
<div class="preview-stage">
{{if eq .Data.File.PreviewKind "image"}}
<img src="{{.Data.DownloadURL}}?inline=1" alt="{{.Data.File.Name}}">
{{else if eq .Data.File.PreviewKind "video"}}
<video src="{{.Data.DownloadURL}}?inline=1" poster="{{.Data.File.ThumbnailURL}}" controls preload="metadata"></video>
{{else if eq .Data.File.PreviewKind "audio"}}
<audio src="{{.Data.DownloadURL}}?inline=1" controls preload="metadata"></audio>
{{else}}
<img src="{{.Data.File.ThumbnailURL}}" alt="">
{{end}}
<header class="preview-header">
<div class="preview-title-group">
<h1 id="preview-title" class="file-name" title="{{.Data.File.Name}}">{{.Data.File.Name}}</h1>
<p class="download-subtitle">{{.Data.File.Size}} · {{.Data.File.ContentType}}</p>
</div>
<a class="button button-primary" href="{{.Data.DownloadURL}}">
<svg viewBox="0 0 24 24" role="img" focusable="false" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14" /></svg>
Download
</a>
</header>
<div class="preview-window" data-preview-kind="{{.Data.File.PreviewKind}}" data-file-name="{{.Data.File.Name}}" data-content-type="{{.Data.File.ContentType}}" data-size-bytes="{{.Data.File.SizeBytes}}" data-source-url="{{.Data.DownloadURL}}?inline=1" data-download-url="{{.Data.DownloadURL}}" data-icon-url="{{.Data.File.IconURL}}" data-file-size="{{.Data.File.Size}}">
<div class="preview-window-titlebar">
<div>
<strong data-preview-mode-label>Preview</strong>
<span>{{.Data.File.ContentType}}</span>
</div>
<div class="preview-window-tools">
<button class="preview-fullscreen-button" type="button" data-render-fullscreen hidden>Full Screen</button>
<div class="preview-window-actions" aria-hidden="true"><span></span><span></span><span></span></div>
</div>
</div>
<div class="preview-tabs" data-preview-tabs></div>
<div class="preview-stage">
<div class="default-preview" data-default-preview hidden>
<img src="{{.Data.File.IconURL}}" alt="" loading="lazy">
<div>
<strong title="{{.Data.File.Name}}">{{.Data.File.Name}}</strong>
<span>{{.Data.File.Size}} · {{.Data.File.ContentType}}</span>
</div>
<a class="button button-primary" href="{{.Data.DownloadURL}}">
<svg viewBox="0 0 24 24" role="img" focusable="false" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14" /></svg>
Download
</a>
</div>
<img class="native-preview native-image-preview" data-image-preview src="{{.Data.DownloadURL}}?inline=1" alt="{{.Data.File.Name}}" hidden>
<video class="native-preview native-video-preview" data-video-preview src="{{.Data.DownloadURL}}?inline=1" poster="{{.Data.File.ThumbnailURL}}" controls preload="metadata" hidden></video>
<audio class="native-preview native-audio-preview" data-browser-audio-preview src="{{.Data.DownloadURL}}?inline=1" controls preload="metadata" hidden></audio>
<div class="code-preview raw-code-preview" data-raw-preview hidden>
<pre><code data-raw-output></code></pre>
</div>
<div class="code-preview prism-code-preview" data-code-preview hidden>
<pre class="line-numbers"><code data-code-output></code></pre>
</div>
<iframe class="render-preview" data-render-preview title="Rendered preview of {{.Data.File.Name}}" sandbox hidden></iframe>
<div class="large-preview-gate" data-large-preview-gate hidden>
<strong>Large preview</strong>
<p>This file is larger than 500 KB. Loading this preview may be slow on some devices.</p>
<div>
<button class="button button-primary" type="button" data-large-preview-confirm>Load anyway</button>
<button class="button button-outline" type="button" data-large-preview-cancel>Cancel</button>
</div>
</div>
<div class="preview-placeholder" data-preview-placeholder hidden>
<img src="{{.Data.File.IconURL}}" alt="">
<p>Preparing preview...</p>
</div>
</div>
</div>
<h1 id="preview-title" class="file-name" title="{{.Data.File.Name}}">{{.Data.File.Name}}</h1>
<p class="download-subtitle">{{.Data.File.Size}} · {{.Data.File.ContentType}}</p>
<a class="button button-primary button-wide" href="{{.Data.DownloadURL}}">
<svg viewBox="0 0 24 24" role="img" focusable="false" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14" /></svg>
Download file
</a>
{{end}}
</div>
</div>