commit 8eece6d161f5c525c3f7340ce7e15da0d9f6ae99
parent 363e7c39b166cf9e9ad8aa13921948e4398534ee
Author: breadcat <breadcat@users.noreply.github.com>
Date: Thu, 25 Sep 2025 21:11:58 +0100
Replace inline javascript and CSS with external files
Also make description [timestamps] clickable
Diffstat:
9 files changed, 224 insertions(+), 184 deletions(-)
diff --git a/static/bulk-tag.js b/static/bulk-tag.js
@@ -0,0 +1,60 @@
+// Auto-focus the file range input
+document.getElementById('file_range').focus();
+
+// Update form behavior based on operation selection
+function updateValueField() {
+ const operation = document.querySelector('input[name="operation"]:checked').value;
+ const valueField = document.getElementById('value');
+ const valueLabel = document.querySelector('label[for="value"]');
+
+ if (operation === 'add') {
+ valueField.required = true;
+ valueLabel.innerHTML = 'Value: <span style="color: red;">*</span>';
+ } else {
+ valueField.required = false;
+ valueLabel.innerHTML = 'Value:';
+ }
+}
+
+// Set up event listeners for radio buttons
+document.querySelectorAll('input[name="operation"]').forEach(radio => {
+ radio.addEventListener('change', updateValueField);
+});
+
+// Initialize on page load
+updateValueField();
+
+// Add form validation
+document.querySelector('form').addEventListener('submit', function(e) {
+ const fileRange = document.getElementById('file_range').value.trim();
+ const category = document.getElementById('category').value.trim();
+ const value = document.getElementById('value').value.trim();
+ const operation = document.querySelector('input[name="operation"]:checked').value;
+
+ if (!fileRange) {
+ alert('Please enter a file ID range');
+ e.preventDefault();
+ return;
+ }
+
+ if (!category) {
+ alert('Please enter a category');
+ e.preventDefault();
+ return;
+ }
+
+ // Only require value for add operations
+ if (operation === 'add' && !value) {
+ alert('Please enter a tag value when adding tags');
+ e.preventDefault();
+ return;
+ }
+
+ // Basic validation of range format
+ const rangePattern = /^[\d\s,-]+$/;
+ if (!rangePattern.test(fileRange)) {
+ alert('File range should only contain numbers, commas, dashes, and spaces');
+ e.preventDefault();
+ return;
+ }
+});
+\ No newline at end of file
diff --git a/static/copy-link.js b/static/copy-link.js
@@ -0,0 +1,30 @@
+document.getElementById("copy-btn").addEventListener("click", function() {
+ const text = document.getElementById("raw-url").textContent.trim();
+ const status = document.getElementById("copy-status");
+
+ // Fallback approach using a temporary textarea
+ const textarea = document.createElement("textarea");
+ textarea.value = text;
+ textarea.style.position = "fixed"; // prevent scrolling
+ textarea.style.left = "-9999px"; // off-screen
+ document.body.appendChild(textarea);
+ textarea.focus();
+ textarea.select();
+
+ let successful = false;
+ try {
+ successful = document.execCommand("copy");
+ } catch (err) {
+ successful = false;
+ }
+
+ document.body.removeChild(textarea);
+
+ if (successful) {
+ status.textContent = "✓ Copied!";
+ status.style.color = "green";
+ } else {
+ status.textContent = "✗ Copy failed";
+ status.style.color = "red";
+ }
+});
+\ No newline at end of file
diff --git a/static/description.js b/static/description.js
@@ -0,0 +1,43 @@
+function toggleDescriptionEdit() {
+ const displayDiv = document.getElementById('description-display');
+ const editDiv = document.getElementById('description-edit');
+
+ displayDiv.style.display = 'none';
+ editDiv.style.display = 'block';
+
+ // Focus the textarea and update character count
+ const textarea = document.getElementById('description-textarea');
+ textarea.focus();
+
+ // Move cursor to end of text if there's existing content
+ if (textarea.value) {
+ textarea.setSelectionRange(textarea.value.length, textarea.value.length);
+ }
+}
+
+function cancelDescriptionEdit() {
+ const displayDiv = document.getElementById('description-display');
+ const editDiv = document.getElementById('description-edit');
+ const textarea = document.getElementById('description-textarea');
+
+ // Reset textarea to original value
+ const original = displayDiv.dataset.originalDescription || '';
+ textarea.value = original;
+
+ displayDiv.style.display = 'block';
+ editDiv.style.display = 'none';
+}
+
+// Auto-resize textarea as content changes
+document.addEventListener('DOMContentLoaded', function() {
+ const textarea = document.getElementById('description-textarea');
+ if (textarea) {
+ textarea.addEventListener('input', function() {
+ // Reset height to auto to get the correct scrollHeight
+ this.style.height = 'auto';
+ // Set the height to match the content, with a minimum of 6 rows
+ const minHeight = parseInt(getComputedStyle(this).lineHeight) * 6;
+ this.style.height = Math.max(minHeight, this.scrollHeight) + 'px';
+ });
+ }
+});
+\ No newline at end of file
diff --git a/static/style.css b/static/style.css
@@ -0,0 +1,31 @@
+/* main body styling */
+body, div#search-container form input {background: #1a1a1a; color: #cfcfcf; font-family: sans-serif; margin: 0}
+a {color: lightblue; text-decoration: none}
+a:hover {text-decoration: underline}
+
+/* nav menu */
+nav {display: flex; border-bottom: 1px solid gray; height: 50px}
+nav ul {flex: 1 1 auto; list-style: none; margin: 0; padding: 0}
+nav ul li {display: inline-block; margin: 0; padding: 0; border-right: 1px solid gray}
+nav ul li a, nav ul li strong {padding: 15px; display: block}
+
+/* search bar */
+div#search-container form input {border: 1px solid gray; padding: 8px; margin-top: 7px}
+span#searchToggle {cursor: pointer; color: lightblue; padding: 8px}
+div#searchToggleContainer {display: none; background-color: #2a2a2a}
+
+/* gallery styling */
+div.gallery {}
+div.gallery-item {display: inline-block; width: 250px}
+div.gallery-item a {display: inline-block; overflow: hidden; text-overflow:ellipsis; width: 250px}
+
+/* cascading menu */
+ul.tag-menu,ul.tag-menu ul{list-style:none;margin:0;padding:0}
+ul.tag-menu li{position:relative}
+ul.tag-menu>li{display:inline-block;margin-right:20px}
+ul.tag-menu li a{text-decoration:none;padding:5px 10px;display:block;background:#eee;color:#333}
+ul.tag-menu li ul{display:none;position:absolute;top:100%;left:0;min-width:150px;z-index:1000}
+ul.tag-menu li ul li,ul.tag-menu li:hover>ul{display:block}
+ul.tag-menu li ul li ul{left:100%;top:0}
+ul.tag-menu li ul li a{background:#f9f9f9}
+ul.tag-menu li ul li a:hover{background:#ddd}
+\ No newline at end of file
diff --git a/static/timestamps.js b/static/timestamps.js
@@ -0,0 +1,36 @@
+function parseTimestamp(ts) {
+ // Split by ":" and reverse to handle h:m:s flexibly
+ const parts = ts.split(":").map(Number).reverse();
+ let seconds = 0;
+ if (parts[0]) seconds += parts[0]; // seconds
+ if (parts[1]) seconds += parts[1] * 60; // minutes
+ if (parts[2]) seconds += parts[2] * 3600; // hours
+ return seconds;
+}
+
+function makeTimestampsClickable(containerId, videoId) {
+ const container = document.getElementById(containerId);
+ const video = document.getElementById(videoId);
+
+ // Regex: [h:mm:ss] or [mm:ss] or [ss]
+ const regex = /\[(\d{1,2}(?::\d{2}){0,2})\]/g;
+
+ container.innerHTML = container.innerHTML.replace(regex, (match, ts) => {
+ const seconds = parseTimestamp(ts);
+ return `<a href="#" class="timestamp" data-time="${seconds}">${match}</a>`;
+ });
+
+ container.addEventListener("click", e => {
+ if (e.target.classList.contains("timestamp")) {
+ e.preventDefault();
+ const time = Number(e.target.dataset.time);
+ video.currentTime = time;
+ video.play();
+ }
+ });
+}
+
+// Run it
+document.addEventListener("DOMContentLoaded", () => {
+ makeTimestampsClickable("current-description", "videoPlayer");
+});
+\ No newline at end of file
diff --git a/static/toggle-help.js b/static/toggle-help.js
@@ -0,0 +1,7 @@
+const button = document.getElementById("searchToggle");
+const div = document.getElementById("searchToggleContainer");
+button.addEventListener("click", () => {
+ div.style.display = (div.style.display === "none" || div.style.display === "")
+ ? "block"
+ : "none";
+});
+\ No newline at end of file
diff --git a/templates/_header.html b/templates/_header.html
@@ -4,39 +4,7 @@
<head>
<meta charset="utf-8">
<title>{{if .Title}}{{.Title}} - Taggart{{else}}Taggart{{end}}</title>
- <style>
-/* main body styling */
-body, div#search-container form input {background: #1a1a1a; color: #cfcfcf; font-family: sans-serif; margin: 0}
-a {color: lightblue; text-decoration: none}
-a:hover {text-decoration: underline}
-
-/* nav menu */
-nav {display: flex; border-bottom: 1px solid gray; height: 50px}
-nav ul {flex: 1 1 auto; list-style: none; margin: 0; padding: 0}
-nav ul li {display: inline-block; margin: 0; padding: 0; border-right: 1px solid gray}
-nav ul li a, nav ul li strong {padding: 15px; display: block}
-
-/* search bar */
-div#search-container form input {border: 1px solid gray; padding: 8px; margin-top: 7px}
-span#searchToggle {cursor: pointer; color: lightblue; padding: 8px}
-div#searchToggleContainer {display: none; background-color: #2a2a2a}
-
-/* gallery styling */
-div.gallery {}
-div.gallery-item {display: inline-block; width: 250px}
-div.gallery-item a {display: inline-block; overflow: hidden; text-overflow:ellipsis; width: 250px}
-
-/* cascading menu */
-ul.tag-menu,ul.tag-menu ul{list-style:none;margin:0;padding:0}
-ul.tag-menu li{position:relative}
-ul.tag-menu>li{display:inline-block;margin-right:20px}
-ul.tag-menu li a{text-decoration:none;padding:5px 10px;display:block;background:#eee;color:#333}
-ul.tag-menu li ul{display:none;position:absolute;top:100%;left:0;min-width:150px;z-index:1000}
-ul.tag-menu li ul li,ul.tag-menu li:hover>ul{display:block}
-ul.tag-menu li ul li ul{left:100%;top:0}
-ul.tag-menu li ul li a{background:#f9f9f9}
-ul.tag-menu li ul li a:hover{background:#ddd}
- </style>
+ <link href="/static/style.css" rel="stylesheet">
</head>
<body>
<nav>
@@ -65,14 +33,5 @@ ul.tag-menu li ul li a:hover{background:#ddd}
<li><code>*vacation*</code> - finds files containing "vacation" anywhere</li>
</ul>
</div>
- <script>
- const button = document.getElementById("searchToggle");
- const div = document.getElementById("searchToggleContainer");
- button.addEventListener("click", () => {
- div.style.display = (div.style.display === "none" || div.style.display === "")
- ? "block"
- : "none";
- });
- </script>
-
+ <script src="/static/toggle-help.js" defer></script>
{{end}}
\ No newline at end of file
diff --git a/templates/bulk-tag.html b/templates/bulk-tag.html
@@ -93,66 +93,7 @@
</div>
</div>
- <script>
- // Auto-focus the file range input
- document.getElementById('file_range').focus();
- // Update form behavior based on operation selection
- function updateValueField() {
- const operation = document.querySelector('input[name="operation"]:checked').value;
- const valueField = document.getElementById('value');
- const valueLabel = document.querySelector('label[for="value"]');
- if (operation === 'add') {
- valueField.required = true;
- valueLabel.innerHTML = 'Value: <span style="color: red;">*</span>';
- } else {
- valueField.required = false;
- valueLabel.innerHTML = 'Value:';
- }
- }
-
- // Set up event listeners for radio buttons
- document.querySelectorAll('input[name="operation"]').forEach(radio => {
- radio.addEventListener('change', updateValueField);
- });
-
- // Initialize on page load
- updateValueField();
-
- // Add form validation
- document.querySelector('form').addEventListener('submit', function(e) {
- const fileRange = document.getElementById('file_range').value.trim();
- const category = document.getElementById('category').value.trim();
- const value = document.getElementById('value').value.trim();
- const operation = document.querySelector('input[name="operation"]:checked').value;
-
- if (!fileRange) {
- alert('Please enter a file ID range');
- e.preventDefault();
- return;
- }
-
- if (!category) {
- alert('Please enter a category');
- e.preventDefault();
- return;
- }
-
- // Only require value for add operations
- if (operation === 'add' && !value) {
- alert('Please enter a tag value when adding tags');
- e.preventDefault();
- return;
- }
-
- // Basic validation of range format
- const rangePattern = /^[\d\s,-]+$/;
- if (!rangePattern.test(fileRange)) {
- alert('File range should only contain numbers, commas, dashes, and spaces');
- e.preventDefault();
- return;
- }
- });
- </script>
+ <script src="/static/bulk-tag.js" defer></script>
{{template "_footer"}}
diff --git a/templates/file.html b/templates/file.html
@@ -4,22 +4,24 @@
{{if hasAnySuffix .Data.File.Filename ".jpg" ".jpeg" ".png" ".gif" ".webp"}}
<a href="/uploads/{{.Data.EscapedFilename}}" target="_blank"><img src="/uploads/{{.Data.EscapedFilename}}" style="max-width:400px"></a><br>
{{else if hasAnySuffix .Data.File.Filename ".mp4" ".webm" ".mov"}}
- <video controls width="400" muted>
+ <video id="videoPlayer" controls width="600" muted>
<source src="/uploads/{{.Data.EscapedFilename}}">
</video><br>
{{else}}
<a href="/uploads/{{.Data.EscapedFilename}}">Download file</a><br>
{{end}}
+<script src="/static/timestamps.js" defer></script>
+
<div class="description-section" style="margin: 20px 0; padding: 15px;">
<h3 style="margin-top: 0;">Description</h3>
<!-- Display Mode -->
- <div id="description-display">
+ <div id="description-display" data-original-description="{{.Data.File.Description}}">
{{if .Data.File.Description}}
- <div class="current-description" style="background: #f9f9f9; padding: 10px; border-radius: 3px; margin-bottom: 10px; white-space: pre-wrap; min-height: 20px;">{{.Data.File.Description}}</div>
+ <div id="current-description" style="background: #f9f9f9; padding: 10px; border-radius: 3px; margin-bottom: 10px; white-space: pre-wrap; min-height: 20px;">{{.Data.File.Description}}</div>
{{else}}
- <div class="no-description" style="color: #666; font-style: italic; margin-bottom: 10px; padding: 10px;">No description set</div>
+ <div id="no-description" style="color: #666; font-style: italic; margin-bottom: 10px; padding: 10px;">No description set</div>
{{end}}
<button id="edit-description-btn" onclick="toggleDescriptionEdit()" style="background: #007cba; color: white; padding: 6px 12px; border: none; border-radius: 3px; cursor: pointer; font-size: 14px;">
{{if .Data.File.Description}}Edit Description{{else}}Add Description{{end}}
@@ -48,88 +50,13 @@
</div>
</div>
-<script>
-function toggleDescriptionEdit() {
- const displayDiv = document.getElementById('description-display');
- const editDiv = document.getElementById('description-edit');
-
- displayDiv.style.display = 'none';
- editDiv.style.display = 'block';
-
- // Focus the textarea and update character count
- const textarea = document.getElementById('description-textarea');
- textarea.focus();
-
- // Move cursor to end of text if there's existing content
- if (textarea.value) {
- textarea.setSelectionRange(textarea.value.length, textarea.value.length);
- }
-}
-
-function cancelDescriptionEdit() {
- const displayDiv = document.getElementById('description-display');
- const editDiv = document.getElementById('description-edit');
- const textarea = document.getElementById('description-textarea');
-
- // Reset textarea to original value
- textarea.value = {{if .Data.File.Description}}`{{.Data.File.Description}}`{{else}}``{{end}};
-
- displayDiv.style.display = 'block';
- editDiv.style.display = 'none';
-}
-
-// Auto-resize textarea as content changes
-document.addEventListener('DOMContentLoaded', function() {
- const textarea = document.getElementById('description-textarea');
- if (textarea) {
- textarea.addEventListener('input', function() {
- // Reset height to auto to get the correct scrollHeight
- this.style.height = 'auto';
- // Set the height to match the content, with a minimum of 6 rows
- const minHeight = parseInt(getComputedStyle(this).lineHeight) * 6;
- this.style.height = Math.max(minHeight, this.scrollHeight) + 'px';
- });
- }
-});
-</script>
+<script src="/static/description.js" defer></script>
<h3>Raw URL</h3>
<pre id="raw-url">http://{{.IP}}:{{.Port}}/uploads/{{.Data.EscapedFilename}}</pre>
<button id="copy-btn" style="margin-top: 5px;">Copy to Clipboard</button>
<span id="copy-status" style="margin-left: 10px;"></span>
-
-<script>
-document.getElementById("copy-btn").addEventListener("click", function() {
- const text = document.getElementById("raw-url").textContent.trim();
- const status = document.getElementById("copy-status");
-
- // Fallback approach using a temporary textarea
- const textarea = document.createElement("textarea");
- textarea.value = text;
- textarea.style.position = "fixed"; // prevent scrolling
- textarea.style.left = "-9999px"; // off-screen
- document.body.appendChild(textarea);
- textarea.focus();
- textarea.select();
-
- let successful = false;
- try {
- successful = document.execCommand("copy");
- } catch (err) {
- successful = false;
- }
-
- document.body.removeChild(textarea);
-
- if (successful) {
- status.textContent = "✓ Copied!";
- status.style.color = "green";
- } else {
- status.textContent = "✗ Copy failed";
- status.style.color = "red";
- }
-});
-</script>
+<script src="/static/copy-link.js" defer></script>