commit 424ff38442d36652e7ab62693a2256bc5c025ac1
parent b813d418c9e39593113ce882aaf709846b32fc63
Author: breadcat <breadcat@users.noreply.github.com>
Date: Fri, 28 Nov 2025 13:26:01 +0000
Merge orphans and thumbnails into admin page
First draft, requires heavy CSS work
Diffstat:
5 files changed, 464 insertions(+), 281 deletions(-)
diff --git a/main.go b/main.go
@@ -379,9 +379,6 @@ func main() {
http.HandleFunc("/search", searchHandler)
http.HandleFunc("/bulk-tag", bulkTagHandler)
http.HandleFunc("/admin", adminHandler)
- http.HandleFunc("/orphans", orphansHandler)
- http.HandleFunc("/thumbnails", thumbnailsHandler)
- http.HandleFunc("/thumbnails/generate", generateThumbnailHandler)
http.Handle("/uploads/", http.StripPrefix("/uploads/", http.FileServer(http.Dir(config.UploadDir))))
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
@@ -1185,25 +1182,35 @@ func validateConfig(newConfig Config) error {
}
func adminHandler(w http.ResponseWriter, r *http.Request) {
+ // Get orphaned files
+ orphans, _ := getOrphanedFiles(config.UploadDir)
+
+ // Get video files for thumbnails
+ missingThumbnails, _ := getMissingThumbnailVideos()
+
switch r.Method {
case http.MethodPost:
action := r.FormValue("action")
switch action {
case "save", "":
- handleSaveSettings(w, r)
+ handleSaveSettings(w, r, orphans, missingThumbnails)
return
case "backup":
err := backupDatabase(config.DatabasePath)
pageData := buildPageData("Admin", struct {
- Config Config
- Error string
- Success string
+ Config Config
+ Error string
+ Success string
+ Orphans []string
+ MissingThumbnails []VideoFile
}{
- Config: config,
- Error: errorString(err),
- Success: successString(err, "Database backup created successfully!"),
+ Config: config,
+ Error: errorString(err),
+ Success: successString(err, "Database backup created successfully!"),
+ Orphans: orphans,
+ MissingThumbnails: missingThumbnails,
})
renderTemplate(w, "admin.html", pageData)
return
@@ -1211,43 +1218,63 @@ func adminHandler(w http.ResponseWriter, r *http.Request) {
case "vacuum":
err := vacuumDatabase(config.DatabasePath)
pageData := buildPageData("Admin", struct {
- Config Config
- Error string
- Success string
+ Config Config
+ Error string
+ Success string
+ Orphans []string
+ MissingThumbnails []VideoFile
}{
- Config: config,
- Error: errorString(err),
- Success: successString(err, "Database vacuum completed successfully!"),
+ Config: config,
+ Error: errorString(err),
+ Success: successString(err, "Database vacuum completed successfully!"),
+ Orphans: orphans,
+ MissingThumbnails: missingThumbnails,
})
renderTemplate(w, "admin.html", pageData)
return
case "save_aliases":
- handleSaveAliases(w, r)
+ handleSaveAliases(w, r, orphans, missingThumbnails)
return
}
default:
pageData := buildPageData("Admin", struct {
- Config Config
- Error string
- Success string
- }{config, "", ""})
+ Config Config
+ Error string
+ Success string
+ Orphans []string
+ MissingThumbnails []VideoFile
+ }{
+ Config: config,
+ Error: "",
+ Success: "",
+ Orphans: orphans,
+ MissingThumbnails: missingThumbnails,
+ })
renderTemplate(w, "admin.html", pageData)
}
}
-func handleSaveAliases(w http.ResponseWriter, r *http.Request) {
+func handleSaveAliases(w http.ResponseWriter, r *http.Request, orphans []string, missingThumbnails []VideoFile) {
aliasesJSON := r.FormValue("aliases_json")
var aliases []TagAliasGroup
if aliasesJSON != "" {
if err := json.Unmarshal([]byte(aliasesJSON), &aliases); err != nil {
pageData := buildPageData("Admin", struct {
- Config Config
- Error string
- Success string
- }{config, "Invalid aliases JSON: " + err.Error(), ""})
+ Config Config
+ Error string
+ Success string
+ Orphans []string
+ MissingThumbnails []VideoFile
+ }{
+ Config: config,
+ Error: "Invalid aliases JSON: " + err.Error(),
+ Success: "",
+ Orphans: orphans,
+ MissingThumbnails: missingThumbnails,
+ })
renderTemplate(w, "admin.html", pageData)
return
}
@@ -1257,23 +1284,39 @@ func handleSaveAliases(w http.ResponseWriter, r *http.Request) {
if err := saveConfig(); err != nil {
pageData := buildPageData("Admin", struct {
- Config Config
- Error string
- Success string
- }{config, "Failed to save configuration: " + err.Error(), ""})
+ Config Config
+ Error string
+ Success string
+ Orphans []string
+ MissingThumbnails []VideoFile
+ }{
+ Config: config,
+ Error: "Failed to save configuration: " + err.Error(),
+ Success: "",
+ Orphans: orphans,
+ MissingThumbnails: missingThumbnails,
+ })
renderTemplate(w, "admin.html", pageData)
return
}
pageData := buildPageData("Admin", struct {
- Config Config
- Error string
- Success string
- }{config, "", "Tag aliases saved successfully!"})
+ Config Config
+ Error string
+ Success string
+ Orphans []string
+ MissingThumbnails []VideoFile
+ }{
+ Config: config,
+ Error: "",
+ Success: "Tag aliases saved successfully!",
+ Orphans: orphans,
+ MissingThumbnails: missingThumbnails,
+ })
renderTemplate(w, "admin.html", pageData)
}
-func handleSaveSettings(w http.ResponseWriter, r *http.Request) {
+func handleSaveSettings(w http.ResponseWriter, r *http.Request, orphans []string, missingThumbnails []VideoFile) {
newConfig := Config{
DatabasePath: strings.TrimSpace(r.FormValue("database_path")),
UploadDir: strings.TrimSpace(r.FormValue("upload_dir")),
@@ -1286,9 +1329,18 @@ func handleSaveSettings(w http.ResponseWriter, r *http.Request) {
if err := validateConfig(newConfig); err != nil {
pageData := buildPageData("Admin", struct {
- Config Config
- Error string
- }{config, err.Error()})
+ Config Config
+ Error string
+ Success string
+ Orphans []string
+ MissingThumbnails []VideoFile
+ }{
+ Config: config,
+ Error: err.Error(),
+ Success: "",
+ Orphans: orphans,
+ MissingThumbnails: missingThumbnails,
+ })
renderTemplate(w, "admin.html", pageData)
return
}
@@ -1299,9 +1351,18 @@ func handleSaveSettings(w http.ResponseWriter, r *http.Request) {
config = newConfig
if err := saveConfig(); err != nil {
pageData := buildPageData("Admin", struct {
- Config Config
- Error string
- }{config, "Failed to save configuration: " + err.Error()})
+ Config Config
+ Error string
+ Success string
+ Orphans []string
+ MissingThumbnails []VideoFile
+ }{
+ Config: config,
+ Error: "Failed to save configuration: " + err.Error(),
+ Success: "",
+ Orphans: orphans,
+ MissingThumbnails: missingThumbnails,
+ })
renderTemplate(w, "admin.html", pageData)
return
}
@@ -1314,13 +1375,22 @@ func handleSaveSettings(w http.ResponseWriter, r *http.Request) {
}
pageData := buildPageData("Admin", struct {
- Config Config
- Error string
- Success string
- }{config, "", message})
+ Config Config
+ Error string
+ Success string
+ Orphans []string
+ MissingThumbnails []VideoFile
+ }{
+ Config: config,
+ Error: "",
+ Success: message,
+ Orphans: orphans,
+ MissingThumbnails: missingThumbnails,
+ })
renderTemplate(w, "admin.html", pageData)
}
+
func errorString(err error) string {
if err != nil {
return err.Error()
@@ -1995,17 +2065,23 @@ func thumbnailsHandler(w http.ResponseWriter, r *http.Request) {
func generateThumbnailHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
- http.Redirect(w, r, "/thumbnails", http.StatusSeeOther)
+ http.Redirect(w, r, "/admin", http.StatusSeeOther)
return
}
action := r.FormValue("action")
+ redirectTo := r.FormValue("redirect")
+ if redirectTo == "" {
+ redirectTo = "thumbnails"
+ }
+
+ redirectBase := "/" + redirectTo
switch action {
case "generate_all":
missing, err := getMissingThumbnailVideos()
if err != nil {
- http.Redirect(w, r, "/thumbnails?error="+url.QueryEscape("Failed to get videos: "+err.Error()), http.StatusSeeOther)
+ http.Redirect(w, r, redirectBase+"?error="+url.QueryEscape("Failed to get videos: "+err.Error()), http.StatusSeeOther)
return
}
@@ -2022,9 +2098,9 @@ func generateThumbnailHandler(w http.ResponseWriter, r *http.Request) {
}
if len(errors) > 0 {
- http.Redirect(w, r, "/thumbnails?success="+url.QueryEscape(fmt.Sprintf("Generated %d thumbnails", successCount))+"&error="+url.QueryEscape(fmt.Sprintf("Failed: %s", strings.Join(errors, "; "))), http.StatusSeeOther)
+ http.Redirect(w, r, redirectBase+"?success="+url.QueryEscape(fmt.Sprintf("Generated %d thumbnails", successCount))+"&error="+url.QueryEscape(fmt.Sprintf("Failed: %s", strings.Join(errors, "; "))), http.StatusSeeOther)
} else {
- http.Redirect(w, r, "/thumbnails?success="+url.QueryEscape(fmt.Sprintf("Successfully generated %d thumbnails", successCount)), http.StatusSeeOther)
+ http.Redirect(w, r, redirectBase+"?success="+url.QueryEscape(fmt.Sprintf("Successfully generated %d thumbnails", successCount)), http.StatusSeeOther)
}
case "generate_single":
@@ -2038,20 +2114,24 @@ func generateThumbnailHandler(w http.ResponseWriter, r *http.Request) {
var filename, path string
err := db.QueryRow("SELECT filename, path FROM files WHERE id=?", fileID).Scan(&filename, &path)
if err != nil {
- http.Redirect(w, r, "/thumbnails?error="+url.QueryEscape("File not found"), http.StatusSeeOther)
+ http.Redirect(w, r, redirectBase+"?error="+url.QueryEscape("File not found"), http.StatusSeeOther)
return
}
err = generateThumbnailAtTime(path, config.UploadDir, filename, timestamp)
if err != nil {
- http.Redirect(w, r, "/thumbnails?error="+url.QueryEscape("Failed to generate thumbnail: "+err.Error()), http.StatusSeeOther)
+ http.Redirect(w, r, redirectBase+"?error="+url.QueryEscape("Failed to generate thumbnail: "+err.Error()), http.StatusSeeOther)
return
}
- http.Redirect(w, r, fmt.Sprintf("/file/%s?success=%s", fileID, url.QueryEscape(fmt.Sprintf("Thumbnail generated at %s", timestamp))), http.StatusSeeOther)
+ if redirectTo == "admin" {
+ http.Redirect(w, r, "/admin?success="+url.QueryEscape(fmt.Sprintf("Thumbnail generated for file %s at %s", fileID, timestamp)), http.StatusSeeOther)
+ } else {
+ http.Redirect(w, r, fmt.Sprintf("/file/%s?success=%s", fileID, url.QueryEscape(fmt.Sprintf("Thumbnail generated at %s", timestamp))), http.StatusSeeOther)
+ }
default:
- http.Redirect(w, r, "/thumbnails", http.StatusSeeOther)
+ http.Redirect(w, r, redirectBase, http.StatusSeeOther)
}
}
diff --git a/static/admin-tabs.js b/static/admin-tabs.js
@@ -0,0 +1,88 @@
+// Admin tab management
+function showAdminTab(tabName) {
+ // Hide all content sections
+ const contents = ['settings', 'database', 'aliases', 'orphans', 'thumbnails'];
+ contents.forEach(name => {
+ const content = document.getElementById(`admin-content-${name}`);
+ if (content) {
+ content.style.display = 'none';
+ }
+ });
+
+ // Remove active styling from all tabs
+ document.querySelectorAll('.admin-tab-btn').forEach(btn => {
+ btn.style.borderBottomColor = 'transparent';
+ btn.style.fontWeight = 'normal';
+ });
+
+ // Show selected content
+ const selectedContent = document.getElementById(`admin-content-${tabName}`);
+ if (selectedContent) {
+ selectedContent.style.display = 'block';
+ }
+
+ // Activate selected tab
+ const selectedTab = document.getElementById(`admin-tab-${tabName}`);
+ if (selectedTab) {
+ selectedTab.style.borderBottomColor = '#007bff';
+ selectedTab.style.fontWeight = 'bold';
+ }
+
+ // Store active tab in session storage
+ sessionStorage.setItem('activeAdminTab', tabName);
+}
+
+// Thumbnail sub-tab management
+function showThumbnailSubTab(subTabName) {
+ // Hide all sub-tab contents
+ const subContents = ['missing', 'regenerate'];
+ subContents.forEach(name => {
+ const content = document.getElementById(`thumb-content-${name}`);
+ if (content) {
+ content.style.display = 'none';
+ }
+ });
+
+ // Remove active styling from all sub-tabs
+ document.querySelectorAll('.thumb-subtab-btn').forEach(btn => {
+ btn.style.borderBottomColor = 'transparent';
+ btn.style.fontWeight = 'normal';
+ });
+
+ // Show selected sub-tab content
+ const selectedContent = document.getElementById(`thumb-content-${subTabName}`);
+ if (selectedContent) {
+ selectedContent.style.display = 'block';
+ }
+
+ // Activate selected sub-tab
+ const selectedTab = document.getElementById(`thumb-subtab-${subTabName}`);
+ if (selectedTab) {
+ selectedTab.style.borderBottomColor = '#007bff';
+ selectedTab.style.fontWeight = 'bold';
+ }
+
+ // Store active sub-tab in session storage
+ sessionStorage.setItem('activeThumbnailSubTab', subTabName);
+}
+
+// Initialize on page load
+document.addEventListener('DOMContentLoaded', function() {
+ // Restore previous tab selection or default to settings
+ const savedTab = sessionStorage.getItem('activeAdminTab') || 'settings';
+ showAdminTab(savedTab);
+
+ // Restore previous thumbnail sub-tab or default to missing
+ const savedSubTab = sessionStorage.getItem('activeThumbnailSubTab') || 'missing';
+ showThumbnailSubTab(savedSubTab);
+
+ // Auto-hide success messages after 5 seconds
+ const successDivs = document.querySelectorAll('.auto-hide-success');
+ successDivs.forEach(div => {
+ setTimeout(() => {
+ div.style.transition = 'opacity 0.5s';
+ div.style.opacity = '0';
+ setTimeout(() => div.remove(), 500);
+ }, 5000);
+ });
+});
+\ No newline at end of file
diff --git a/templates/admin.html b/templates/admin.html
@@ -1,5 +1,5 @@
{{template "_header" .}}
-<h1>Settings</h1>
+<h1>Admin</h1>
{{if .Data.Error}}
<div style="background-color: #f8d7da; color: #721c24; padding: 10px; border: 1px solid #f5c6cb; border-radius: 4px; margin-bottom: 20px;">
@@ -13,119 +13,265 @@
</div>
{{end}}
-<form method="post" style="max-width: 600px;">
- <div style="margin-bottom: 20px;">
- <label for="database_path" style="display: block; font-weight: bold; margin-bottom: 5px;">Database Path:</label>
- <input type="text" id="database_path" name="database_path" value="{{.Data.Config.DatabasePath}}" required
- style="width: 100%; padding: 8px; font-size: 14px;"
- placeholder="./database.db">
- <small style="color: #666;">Path to SQLite database file (requires restart if changed)</small>
- </div>
+<!-- Tab Navigation -->
+<div style="margin-bottom: 20px; border-bottom: 2px solid #ddd;">
+ <button onclick="showAdminTab('settings')" id="admin-tab-settings" class="admin-tab-btn" style="padding: 10px 20px; border: none; background: none; cursor: pointer; border-bottom: 3px solid #007bff; font-weight: bold;">
+ Settings
+ </button>
+ <button onclick="showAdminTab('database')" id="admin-tab-database" class="admin-tab-btn" style="padding: 10px 20px; border: none; background: none; cursor: pointer; border-bottom: 3px solid transparent;">
+ Database
+ </button>
+ <button onclick="showAdminTab('aliases')" id="admin-tab-aliases" class="admin-tab-btn" style="padding: 10px 20px; border: none; background: none; cursor: pointer; border-bottom: 3px solid transparent;">
+ Aliases
+ </button>
+ <button onclick="showAdminTab('orphans')" id="admin-tab-orphans" class="admin-tab-btn" style="padding: 10px 20px; border: none; background: none; cursor: pointer; border-bottom: 3px solid transparent;">
+ Orphans
+ </button>
+ <button onclick="showAdminTab('thumbnails')" id="admin-tab-thumbnails" class="admin-tab-btn" style="padding: 10px 20px; border: none; background: none; cursor: pointer; border-bottom: 3px solid transparent;">
+ Thumbnails
+ </button>
+</div>
- <div style="margin-bottom: 20px;">
- <label for="upload_dir" style="display: block; font-weight: bold; margin-bottom: 5px;">Upload Directory:</label>
- <input type="text" id="upload_dir" name="upload_dir" value="{{.Data.Config.UploadDir}}" required
- style="width: 100%; padding: 8px; font-size: 14px;"
- placeholder="uploads">
- <small style="color: #666;">Directory where uploaded files are stored</small>
- </div>
+<!-- Settings Tab -->
+<div id="admin-content-settings">
+ <h2>Settings</h2>
+ <form method="post" style="max-width: 600px;">
+ <div style="margin-bottom: 20px;">
+ <label for="database_path" style="display: block; font-weight: bold; margin-bottom: 5px;">Database Path:</label>
+ <input type="text" id="database_path" name="database_path" value="{{.Data.Config.DatabasePath}}" required
+ style="width: 100%; padding: 8px; font-size: 14px;"
+ placeholder="./database.db">
+ <small style="color: #666;">Path to SQLite database file (requires restart if changed)</small>
+ </div>
- <div style="margin-bottom: 20px;">
- <label for="server_port" style="display: block; font-weight: bold; margin-bottom: 5px;">Server Port:</label>
- <input type="text" id="server_port" name="server_port" value="{{.Data.Config.ServerPort}}" required
- style="width: 100%; padding: 8px; font-size: 14px;"
- placeholder=":8080">
- <small style="color: #666;">Port for web server (format: :8080, requires restart if changed)</small>
- </div>
+ <div style="margin-bottom: 20px;">
+ <label for="upload_dir" style="display: block; font-weight: bold; margin-bottom: 5px;">Upload Directory:</label>
+ <input type="text" id="upload_dir" name="upload_dir" value="{{.Data.Config.UploadDir}}" required
+ style="width: 100%; padding: 8px; font-size: 14px;"
+ placeholder="uploads">
+ <small style="color: #666;">Directory where uploaded files are stored</small>
+ </div>
- <div style="margin-bottom: 20px;">
- <label for="instance_name" style="display: block; font-weight: bold; margin-bottom: 5px;">Instance Name:</label>
- <input type="text" id="instance_name" name="instance_name" value="{{.Data.Config.InstanceName}}" required
- style="width: 100%; padding: 8px; font-size: 14px;"
- placeholder="Taggart">
- <small style="color: #666;">Instance Name, used in header and title bar</small>
- </div>
+ <div style="margin-bottom: 20px;">
+ <label for="server_port" style="display: block; font-weight: bold; margin-bottom: 5px;">Server Port:</label>
+ <input type="text" id="server_port" name="server_port" value="{{.Data.Config.ServerPort}}" required
+ style="width: 100%; padding: 8px; font-size: 14px;"
+ placeholder=":8080">
+ <small style="color: #666;">Port for web server (format: :8080, requires restart if changed)</small>
+ </div>
- <div style="margin-bottom: 20px;">
- <label for="gallery_size" style="display: block; font-weight: bold; margin-bottom: 5px;">Gallery Size:</label>
- <input type="text" id="gallery_size" name="gallery_size" value="{{.Data.Config.GallerySize}}" required
- style="width: 100%; padding: 8px; font-size: 14px;"
- placeholder="400px">
- <small style="color: #666;">Size of previews used in galleries</small>
- </div>
+ <div style="margin-bottom: 20px;">
+ <label for="instance_name" style="display: block; font-weight: bold; margin-bottom: 5px;">Instance Name:</label>
+ <input type="text" id="instance_name" name="instance_name" value="{{.Data.Config.InstanceName}}" required
+ style="width: 100%; padding: 8px; font-size: 14px;"
+ placeholder="Taggart">
+ <small style="color: #666;">Instance Name, used in header and title bar</small>
+ </div>
- <div style="margin-bottom: 20px;">
- <label for="items_per_page" style="display: block; font-weight: bold; margin-bottom: 5px;">Items per Page:</label>
- <input type="text" id="items_per_page" name="items_per_page" value="{{.Data.Config.ItemsPerPage}}" required
- style="width: 100%; padding: 8px; font-size: 14px;"
- placeholder="100">
- <small style="color: #666;">Items per page in galleries</small>
- </div>
+ <div style="margin-bottom: 20px;">
+ <label for="gallery_size" style="display: block; font-weight: bold; margin-bottom: 5px;">Gallery Size:</label>
+ <input type="text" id="gallery_size" name="gallery_size" value="{{.Data.Config.GallerySize}}" required
+ style="width: 100%; padding: 8px; font-size: 14px;"
+ placeholder="400px">
+ <small style="color: #666;">Size of previews used in galleries</small>
+ </div>
- <button type="submit" style="background-color: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; font-size: 16px; cursor: pointer;">
- Save Settings
- </button>
-</form>
+ <div style="margin-bottom: 20px;">
+ <label for="items_per_page" style="display: block; font-weight: bold; margin-bottom: 5px;">Items per Page:</label>
+ <input type="text" id="items_per_page" name="items_per_page" value="{{.Data.Config.ItemsPerPage}}" required
+ style="width: 100%; padding: 8px; font-size: 14px;"
+ placeholder="100">
+ <small style="color: #666;">Items per page in galleries</small>
+ </div>
-<hr style="margin: 40px 0;">
+ <button type="submit" style="background-color: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; font-size: 16px; cursor: pointer;">
+ Save Settings
+ </button>
+ </form>
-<h2>Tag Aliases</h2>
-<p style="color: #666; margin-bottom: 20px;">
- Define tag aliases so that multiple tag values are treated as equivalent when searching or filtering.
- For example, if you alias "colour/blue" with "colour/navy", searching for either will show files tagged with both.
-</p>
+ <div style="margin-top: 40px; padding: 20px; background-color: #f8f9fa; border-radius: 5px;">
+ <h3>Current Configuration:</h3>
+ <ul>
+ <li><strong>Database:</strong> {{.Data.Config.DatabasePath}}</li>
+ <li><strong>Upload Directory:</strong> {{.Data.Config.UploadDir}}</li>
+ <li><strong>Server Port:</strong> {{.Data.Config.ServerPort}}</li>
+ <li><strong>Instance Name:</strong> {{.Data.Config.InstanceName}}</li>
+ <li><strong>Gallery Size:</strong> {{.Data.Config.GallerySize}}</li>
+ <li><strong>Items per Page:</strong> {{.Data.Config.ItemsPerPage}}</li>
+ </ul>
-<div id="aliases-section" style="max-width: 800px;">
- <div id="alias-groups"></div>
+ <h4>Configuration File:</h4>
+ <p>Settings are stored in <code>config.json</code> in the application directory.</p>
+ </div>
+</div>
- <button onclick="addAliasGroup()" style="background-color: #28a745; color: white; padding: 8px 16px; border: none; border-radius: 4px; font-size: 14px; cursor: pointer; margin-top: 10px;">
- + Add Alias Group
- </button>
+<!-- Database Tab -->
+<div id="admin-content-database" style="display: none;">
+ <h2>Database Maintenance</h2>
- <form method="post" id="aliases-form" style="margin-top: 20px;">
- <input type="hidden" name="action" value="save_aliases">
- <input type="hidden" name="aliases_json" id="aliases_json">
- <button type="submit" style="background-color: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; font-size: 16px; cursor: pointer;">
- Save Aliases
+ <form method="post" style="margin-bottom: 20px;">
+ <input type="hidden" name="action" value="backup">
+ <button type="submit" style="background-color: #28a745; color: white; padding: 10px 20px; border: none; border-radius: 4px; font-size: 16px; cursor: pointer;">
+ Backup Database
</button>
+ <small style="color: #666; margin-left: 10px;">Creates a timestamped backup of the database file</small>
+ </form>
+
+ <form method="post">
+ <input type="hidden" name="action" value="vacuum">
+ <button type="submit" style="background-color: #6f42c1; color: white; padding: 10px 20px; border: none; border-radius: 4px; font-size: 16px; cursor: pointer;">
+ Vacuum Database
+ </button>
+ <small style="color: #666; margin-left: 10px;">Reclaims unused space and optimizes database performance</small>
</form>
</div>
-<script>window.initialAliasGroups = {{.Data.Config.TagAliases}};</script><script src="/static/tag-alias.js" defer></script>
-
-<div style="margin-top: 40px; padding: 20px; background-color: #f8f9fa; border-radius: 5px;">
- <h3>Current Configuration:</h3>
- <ul>
- <li><strong>Database:</strong> {{.Data.Config.DatabasePath}}</li>
- <li><strong>Upload Directory:</strong> {{.Data.Config.UploadDir}}</li>
- <li><strong>Server Port:</strong> {{.Data.Config.ServerPort}}</li>
- <li><strong>Instance Name:</strong> {{.Data.Config.InstanceName}}</li>
- <li><strong>Gallery Size:</strong> {{.Data.Config.GallerySize}}</li>
- <li><strong>Items per Page:</strong> {{.Data.Config.ItemsPerPage}}</li>
- </ul>
+<!-- Aliases Tab -->
+<div id="admin-content-aliases" style="display: none;">
+ <h2>Tag Aliases</h2>
+ <p style="color: #666; margin-bottom: 20px;">
+ Define tag aliases so that multiple tag values are treated as equivalent when searching or filtering.
+ For example, if you alias "colour/blue" with "colour/navy", searching for either will show files tagged with both.
+ </p>
+
+ <div id="aliases-section" style="max-width: 800px;">
+ <div id="alias-groups"></div>
+
+ <button onclick="addAliasGroup()" style="background-color: #28a745; color: white; padding: 8px 16px; border: none; border-radius: 4px; font-size: 14px; cursor: pointer; margin-top: 10px;">
+ + Add Alias Group
+ </button>
- <h4>Configuration File:</h4>
- <p>Settings are stored in <code>config.json</code> in the application directory.</p>
+ <form method="post" id="aliases-form" style="margin-top: 20px;">
+ <input type="hidden" name="action" value="save_aliases">
+ <input type="hidden" name="aliases_json" id="aliases_json">
+ <button type="submit" style="background-color: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; font-size: 16px; cursor: pointer;">
+ Save Aliases
+ </button>
+ </form>
+ </div>
</div>
-<hr style="margin: 40px 0;">
+<!-- Orphans Tab -->
+<div id="admin-content-orphans" style="display: none;">
+ <h2>Orphaned Files</h2>
+ <p style="color: #666; margin-bottom: 20px;">
+ These files exist in the upload directory but are not tracked in the database.
+ </p>
-<h2>Database Maintenance</h2>
+ {{if .Data.Orphans}}
+ <ul style="list-style-type: disc; padding-left: 20px;">
+ {{range .Data.Orphans}}
+ <li style="margin-bottom: 5px; font-family: monospace;">{{.}}</li>
+ {{end}}
+ </ul>
+ {{else}}
+ <div style="padding: 20px; background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; border-radius: 4px;">
+ <strong>✓ No orphaned files found!</strong>
+ </div>
+ {{end}}
+</div>
-<form method="post" style="margin-bottom: 20px;">
- <input type="hidden" name="action" value="backup">
- <button type="submit" style="background-color: #28a745; color: white; padding: 10px 20px; border: none; border-radius: 4px; font-size: 16px; cursor: pointer;">
- Backup Database
- </button>
- <small style="color: #666; margin-left: 10px;">Creates a timestamped backup of the database file</small>
-</form>
+<!-- Thumbnails Tab -->
+<div id="admin-content-thumbnails" style="display: none;">
+ <h2>Thumbnail Management</h2>
-<form method="post">
- <input type="hidden" name="action" value="vacuum">
- <button type="submit" style="background-color: #6f42c1; color: white; padding: 10px 20px; border: none; border-radius: 4px; font-size: 16px; cursor: pointer;">
- Vacuum Database
- </button>
- <small style="color: #666; margin-left: 10px;">Reclaims unused space and optimizes database performance</small>
-</form>
+ <!-- Sub-tab Navigation -->
+ <div style="margin-bottom: 20px; border-bottom: 1px solid #ddd;">
+ <button onclick="showThumbnailSubTab('missing')" id="thumb-subtab-missing" class="thumb-subtab-btn" style="padding: 8px 16px; border: none; background: none; cursor: pointer; border-bottom: 2px solid #007bff; font-weight: bold;">
+ Missing ({{len .Data.MissingThumbnails}})
+ </button>
+ <button onclick="showThumbnailSubTab('regenerate')" id="thumb-subtab-regenerate" class="thumb-subtab-btn" style="padding: 8px 16px; border: none; background: none; cursor: pointer; border-bottom: 2px solid transparent;">
+ Regenerate
+ </button>
+ </div>
+
+ <!-- Missing Thumbnails Sub-tab -->
+ <div id="thumb-content-missing">
+ <h3>Missing Thumbnails ({{len .Data.MissingThumbnails}})</h3>
+
+ {{if .Data.MissingThumbnails}}
+ <form method="post" action="/thumbnails/generate" style="margin-bottom: 20px;">
+ <input type="hidden" name="action" value="generate_all">
+ <button type="submit" onclick="return confirm('Generate thumbnails for all {{len .Data.MissingThumbnails}} videos? This may take a while.');" style="background-color: #28a745; color: white; padding: 10px 20px; border: none; border-radius: 4px; font-size: 16px; cursor: pointer;">
+ Generate All Missing Thumbnails
+ </button>
+ <small style="color: #666; margin-left: 10px;">Uses timestamp 00:00:05 for all videos</small>
+ </form>
+
+ <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px;">
+ {{range .Data.MissingThumbnails}}
+ <div style="border: 1px solid #ddd; padding: 15px; border-radius: 5px; background-color: #f8f9fa;">
+ <h4 style="margin-top: 0; font-size: 14px; word-break: break-word;">
+ <a href="/file/{{.ID}}" target="_blank">{{.Filename}}</a>
+ </h4>
+ <p style="color: #666; font-size: 12px; margin: 5px 0;">ID: {{.ID}}</p>
+
+ <video width="100%" style="max-height: 200px; background: #000; margin: 10px 0; cursor: pointer;" title="Click to capture frame">
+ <source src="/uploads/{{.EscapedFilename}}">
+ </video>
+
+ <form method="post" action="/thumbnails/generate" style="margin-top: 10px;">
+ <input type="hidden" name="action" value="generate_single">
+ <input type="hidden" name="file_id" value="{{.ID}}">
+ <input type="hidden" name="redirect" value="admin">
+
+ <div style="display: flex; gap: 5px; align-items: center; margin-bottom: 10px;">
+ <label style="font-size: 13px; white-space: nowrap;">Timestamp:</label>
+ <input type="text" name="timestamp" value="00:00:05" placeholder="00:00:05"
+ style="flex: 1; padding: 5px; font-size: 13px; font-family: monospace;">
+ </div>
+
+ <button type="submit" style="background-color: #007bff; color: white; padding: 6px 12px; border: none; border-radius: 3px; font-size: 13px; cursor: pointer; width: 100%;">
+ Generate Thumbnail
+ </button>
+ </form>
+ </div>
+ {{end}}
+ </div>
+ {{else}}
+ <div style="padding: 20px; background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; border-radius: 4px;">
+ <strong>✓ All videos have thumbnails!</strong>
+ </div>
+ {{end}}
+ </div>
+
+ <!-- Regenerate Sub-tab -->
+ <div id="thumb-content-regenerate" style="display: none;">
+ <h3>Regenerate Thumbnail</h3>
+
+ <div style="margin-bottom: 20px; padding: 10px; background-color: #e7f3ff; border: 1px solid #b3d9ff; border-radius: 4px;">
+ <strong>Tip:</strong> Enter a file ID to regenerate its thumbnail with a custom timestamp. You can find file IDs in the URL when viewing a file (e.g., /file/312).
+ </div>
+
+ <form method="post" action="/thumbnails/generate" style="max-width: 500px; padding: 20px; background-color: #f8f9fa; border: 1px solid #ddd; border-radius: 5px;">
+ <input type="hidden" name="action" value="generate_single">
+ <input type="hidden" name="redirect" value="admin">
+
+ <div style="margin-bottom: 20px;">
+ <label for="file_id" style="display: block; font-weight: bold; margin-bottom: 5px;">File ID:</label>
+ <input type="text" id="file_id" name="file_id" required
+ style="width: 100%; padding: 8px; font-size: 14px; font-family: monospace;"
+ placeholder="e.g., 312">
+ <small style="color: #666;">Enter the ID of the video file</small>
+ </div>
+
+ <div style="margin-bottom: 20px;">
+ <label for="timestamp" style="display: block; font-weight: bold; margin-bottom: 5px;">Timestamp:</label>
+ <input type="text" id="timestamp" name="timestamp" value="00:00:05" required
+ style="width: 100%; padding: 8px; font-size: 14px; font-family: monospace;"
+ placeholder="00:00:05">
+ <small style="color: #666;">Format: HH:MM:SS (e.g., 00:00:05 for 5 seconds)</small>
+ </div>
+
+ <button type="submit" style="background-color: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; font-size: 16px; cursor: pointer; width: 100%;">
+ Generate/Regenerate Thumbnail
+ </button>
+ </form>
+ </div>
+</div>
+
+<script>window.initialAliasGroups = {{.Data.Config.TagAliases}};</script>
+<script src="/static/tag-alias.js" defer></script>
+<script src="/static/admin-tabs.js" defer></script>
-{{template "_footer"}}
+{{template "_footer"}}
+\ No newline at end of file
diff --git a/templates/orphans.html b/templates/orphans.html
@@ -1,15 +0,0 @@
-{{template "_header" .}}
-
-<h1>Orphaned Files</h1>
-
-{{if .Data}}
-<ul>
- {{range .Data}}
- <li>{{.}}</li>
- {{end}}
-</ul>
-{{else}}
-<p>No orphaned files</p>
-{{end}}
-
-{{template "_footer"}}
-\ No newline at end of file
diff --git a/templates/thumbnails.html b/templates/thumbnails.html
@@ -1,116 +0,0 @@
-{{template "_header" .}}
-<h1>Thumbnail Management</h1>
-
-{{if .Data.Error}}
-<div style="background-color: #f8d7da; color: #721c24; padding: 10px; border: 1px solid #f5c6cb; border-radius: 4px; margin-bottom: 20px;">
- <strong>Error:</strong> {{.Data.Error}}
-</div>
-{{end}}
-
-{{if .Data.Success}}
-<div style="background-color: #d4edda; color: #155724; padding: 10px; border: 1px solid #c3e6cb; border-radius: 4px; margin-bottom: 20px;">
- <strong>Success:</strong> {{.Data.Success}}
-</div>
-{{end}}
-
-<!-- Tab Navigation -->
-<div style="margin-bottom: 20px; border-bottom: 2px solid #ddd;">
- <button onclick="showTab('missing')" id="tab-missing" style="padding: 10px 20px; border: none; background: none; cursor: pointer; border-bottom: 3px solid #007bff; font-weight: bold;">
- Missing ({{len .Data.MissingThumbnails}})
- </button>
- <button onclick="showTab('all')" id="tab-all" style="padding: 10px 20px; border: none; background: none; cursor: pointer; border-bottom: 3px solid transparent;">
- All Videos ({{len .Data.AllVideos}})
- </button>
-</div>
-
-<!-- Missing Thumbnails Tab -->
-<div id="content-missing" style="margin-bottom: 30px;">
- <h2>Missing Thumbnails ({{len .Data.MissingThumbnails}})</h2>
-
- {{if .Data.MissingThumbnails}}
- <form method="post" action="/thumbnails/generate" style="margin-bottom: 20px;">
- <input type="hidden" name="action" value="generate_all">
- <button type="submit" onclick="return confirm('Generate thumbnails for all {{len .Data.MissingThumbnails}} videos? This may take a while.');" style="background-color: #28a745; color: white; padding: 10px 20px; border: none; border-radius: 4px; font-size: 16px; cursor: pointer;">
- Generate All Missing Thumbnails
- </button>
- <small style="color: #666; margin-left: 10px;">Uses timestamp 00:00:05 for all videos</small>
- </form>
-
- <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px;">
- {{range .Data.MissingThumbnails}}
- <div style="border: 1px solid #ddd; padding: 15px; border-radius: 5px; background-color: #f8f9fa;">
- <h3 style="margin-top: 0; font-size: 14px; word-break: break-word;">
- <a href="/file/{{.ID}}" target="_blank">{{.Filename}}</a>
- </h3>
- <p style="color: #666; font-size: 12px; margin: 5px 0;">ID: {{.ID}}</p>
-
- <video width="100%" style="max-height: 200px; background: #000; margin: 10px 0; cursor: pointer;" title="Click to capture frame">
- <source src="/uploads/{{.EscapedFilename}}">
- </video>
-
- <form method="post" action="/thumbnails/generate" style="margin-top: 10px;">
- <input type="hidden" name="action" value="generate_single">
- <input type="hidden" name="file_id" value="{{.ID}}">
-
- <div style="display: flex; gap: 5px; align-items: center; margin-bottom: 10px;">
- <label style="font-size: 13px; white-space: nowrap;">Timestamp:</label>
- <input type="text" name="timestamp" value="00:00:05" placeholder="00:00:05"
- style="flex: 1; padding: 5px; font-size: 13px; font-family: monospace;">
- </div>
-
- <button type="submit" style="background-color: #007bff; color: white; padding: 6px 12px; border: none; border-radius: 3px; font-size: 13px; cursor: pointer; width: 100%;">
- Generate Thumbnail
- </button>
- </form>
- </div>
- {{end}}
- </div>
- {{else}}
- <div style="padding: 20px; background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; border-radius: 4px;">
- <strong>✓ All videos have thumbnails!</strong>
- </div>
- {{end}}
-</div>
-
-<!-- All Videos Tab -->
-<div id="content-all" style="margin-bottom: 30px; display: none;">
- <h2>Regenerate Thumbnail</h2>
-
- <div style="margin-bottom: 20px; padding: 10px; background-color: #e7f3ff; border: 1px solid #b3d9ff; border-radius: 4px;">
- <strong>Tip:</strong> Enter a file ID to regenerate its thumbnail with a custom timestamp. You can find file IDs in the URL when viewing a file (e.g., /file/312).
- </div>
-
- <form method="post" action="/thumbnails/generate" style="max-width: 500px; padding: 20px; background-color: #f8f9fa; border: 1px solid #ddd; border-radius: 5px;">
- <input type="hidden" name="action" value="generate_single">
-
- <div style="margin-bottom: 20px;">
- <label for="file_id" style="display: block; font-weight: bold; margin-bottom: 5px;">File ID:</label>
- <input type="text" id="file_id" name="file_id" required
- style="width: 100%; padding: 8px; font-size: 14px; font-family: monospace;"
- placeholder="e.g., 312">
- <small style="color: #666;">Enter the ID of the video file</small>
- </div>
-
- <div style="margin-bottom: 20px;">
- <label for="timestamp" style="display: block; font-weight: bold; margin-bottom: 5px;">Timestamp:</label>
- <input type="text" id="timestamp" name="timestamp" value="00:00:05" required
- style="width: 100%; padding: 8px; font-size: 14px; font-family: monospace;"
- placeholder="00:00:05">
- <small style="color: #666;">Format: HH:MM:SS (e.g., 00:00:05 for 5 seconds)</small>
- </div>
-
- <button type="submit" style="background-color: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; font-size: 16px; cursor: pointer; width: 100%;">
- Generate/Regenerate Thumbnail
- </button>
- </form>
-
- {{if .Data.Success}}
- <div class="auto-hide-success" style="margin-top: 20px; padding: 15px; background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; border-radius: 4px;">
- <strong>✓ Success!</strong> {{.Data.Success}}
- </div>
- {{end}}
-</div>
-
-<script src="/static/thumbnails.js" defer></script>
-
-{{template "_footer"}}
-\ No newline at end of file