commit 28eed2d7b9828b93374896794b11f676404f53a3
parent 0282b8f82de7a121605ac37a19a4bb5f25d4b4fc
Author: breadcat <breadcat@users.noreply.github.com>
Date: Thu, 2 Oct 2025 17:14:51 +0100
Add text viewer with line numbers and description shortcuts
Diffstat:
4 files changed, 112 insertions(+), 0 deletions(-)
diff --git a/static/style.css b/static/style.css
@@ -29,6 +29,7 @@ div#search-container form input:focus{border:none;background-color:#3a3a3a}
/* gallery styling */
div.gallery-item a{overflow:hidden;text-overflow:ellipsis}
+div.gallery-item a svg{filter: invert(100%);}
div.gallery-item,div.gallery-item a,nav ul li,nav>ul>li{display:inline-block}
div.gallery-item,div.gallery-item a{width:250px;display:inline-block}
div.play-button {position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 0; height: 0; border-left: 15px solid white; border-top: 10px solid transparent; border-bottom: 10px solid transparent; opacity: 0.8;}
@@ -50,3 +51,9 @@ span.file-tag-category,div.file-sidebar details ul li a {text-transform:capitali
details > summary {list-style: none; cursor: pointer; }
details > summary::before {content: "[+]"; display: inline-block; width: 2ch; margin-right: 0.5ch;}
details[open] > summary::before {content: "[-]";}
+
+/* text viewer */
+pre#text-viewer{font-family:serif;font-size:25px}
+#text-viewer-container:fullscreen{margin:0;max-width:75%;margin:auto;height:100vh;padding:1em;background:#000;display:flex;flex-direction:column}
+#text-viewer-container:fullscreen #text-viewer{flex:1;max-height:none!important;max-width:75%;margin:0;height:100%}
+#text-viewer-container:fullscreen>div{flex-shrink:0}
diff --git a/static/text-viewer.js b/static/text-viewer.js
@@ -0,0 +1,95 @@
+let originalText = '';
+
+async function loadTextFile() {
+ const viewer = document.getElementById("text-viewer");
+ const filename = viewer.dataset.filename;
+ const response = await fetch(`/uploads/${filename}`);
+ const text = await response.text();
+ originalText = text;
+ viewer.textContent = text;
+}
+
+function toggleLineNumbers() {
+ const viewer = document.getElementById("text-viewer");
+ if (viewer.classList.contains("with-lines")) {
+ viewer.classList.remove("with-lines");
+ viewer.textContent = originalText; // Use stored original text
+ } else {
+ const lines = originalText.split("\n"); // Use stored original text
+ viewer.innerHTML = lines.map((line, i) =>
+ `<span style="display:block;"><span style="color:#888; user-select:none; width:3em; display:inline-block;">${i+1}</span> ${escapeHtml(line)}</span>`
+ ).join("");
+ viewer.classList.add("with-lines");
+ }
+}
+
+function escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
+
+function toggleFullscreen() {
+ const container = document.getElementById("text-viewer-container");
+ if (!document.fullscreenElement) {
+ container.requestFullscreen();
+ } else {
+ document.exitFullscreen();
+ }
+}
+
+function parseLineNumber(str) {
+ // Match [l123] or [L123]
+ const match = str.match(/^[lL](\d+)$/);
+ return match ? Number(match[1]) : null;
+}
+
+function makeLineNumbersClickable(containerId, viewerId) {
+ const container = document.getElementById(containerId);
+ const viewer = document.getElementById(viewerId);
+
+ // Regex: [l123] or [L123]
+ const regex = /\[([lL]\d+)\]/g;
+
+ container.innerHTML = container.innerHTML.replace(regex, (match, lineRef) => {
+ const lineNum = parseLineNumber(lineRef);
+ return `<a href="#" class="line-link" data-line="${lineNum}">${match}</a>`;
+ });
+
+ container.addEventListener("click", e => {
+ if (e.target.classList.contains("line-link")) {
+ e.preventDefault();
+ const lineNum = Number(e.target.dataset.line);
+ scrollToLine(lineNum);
+ }
+ });
+}
+
+function scrollToLine(lineNum) {
+ const viewer = document.getElementById("text-viewer");
+
+ // If line numbers are visible, find the specific line span
+ if (viewer.classList.contains("with-lines")) {
+ const lines = viewer.querySelectorAll("span[style*='display:block']");
+ if (lines[lineNum - 1]) {
+ lines[lineNum - 1].scrollIntoView({ behavior: "smooth", block: "center" });
+ // Optional: highlight the line briefly
+ lines[lineNum - 1].style.background = "#ff06";
+ setTimeout(() => lines[lineNum - 1].style.background = "", 2000);
+ }
+ } else {
+ // If no line numbers shown, calculate approximate position
+ const lines = originalText.split("\n");
+ const totalLines = lines.length;
+ const percentage = (lineNum - 1) / totalLines;
+ viewer.scrollTop = viewer.scrollHeight * percentage;
+ }
+}
+
+// Run it after the page loads
+document.addEventListener("DOMContentLoaded", () => {
+ // Replace "description-container" with the ID of your description element
+ makeLineNumbersClickable("current-description", "text-viewer");
+});
+
+ loadTextFile();
diff --git a/templates/_gallery.html b/templates/_gallery.html
@@ -3,6 +3,7 @@
<a href="/file/{{.ID}}" title="{{.Filename}}">
{{if hasAnySuffix .Filename ".jpg" ".jpeg" ".png" ".gif" ".webp"}}<img src="/uploads/{{.EscapedFilename}}" style="max-width:150px">
{{else if hasAnySuffix .Filename ".mp4" ".webm" ".mov" ".m4v"}}<div class="gallery-video"><img src="/uploads/thumbnails/{{.EscapedFilename}}.jpg" style="width: 100%; display: block;" /><div class="play-button"></div></div>
+ {{else if hasAnySuffix .Filename ".txt" ".md"}}<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" fill="#f5f5f5" rx="8"/><rect x="4" y="4" width="56" height="56" fill="none" stroke="#666" stroke-width="2" rx="6"/><text x="32" y="42" font-family="sans-serif" font-size="26" font-weight="600" fill="#333" text-anchor="middle">Aa</text></svg>
{{end}}
<br>{{.Filename}}</a>
</div>
diff --git a/templates/file.html b/templates/file.html
@@ -73,6 +73,15 @@
<source src="/uploads/{{.Data.EscapedFilename}}">
</video><br>
<script src="/static/timestamps.js" defer></script>
+ {{else if hasAnySuffix .Data.File.Filename ".txt" ".md"}}
+ <div id="text-viewer-container" style="max-width:800px; margin:1em 0;">
+ <div style="display:flex; justify-content:space-between; margin-bottom:5px;">
+ <button onclick="toggleLineNumbers()" class="text-button">[ Line Numbers ]</button>
+ <button onclick="toggleFullscreen()" class="text-button">[ Fullscreen ]</button>
+ </div>
+ <pre id="text-viewer" data-filename="{{.Data.EscapedFilename}}" style="white-space:pre-wrap; overflow:auto; background:#111; color:#eee; padding:10px; border-radius:8px; max-height:500px;">Loading...</pre>
+ </div>
+ <script src="/static/text-viewer.js"></script>
{{else}}
<a href="/uploads/{{.Data.EscapedFilename}}">Download file</a><br>
{{end}}