taggart

Simple golang tagging filesystem webapp
Log | Files | Refs

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:
Mstatic/style.css | 7+++++++
Astatic/text-viewer.js | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtemplates/_gallery.html | 1+
Mtemplates/file.html | 9+++++++++
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}}