tagliatelle

Simple self-hosted tagging filesystem webapp
Log | Files | Refs

commit e10930cb1e05750f70aa53c6d2ea0847fde85a7a
parent 4980f16e0dbf7c28c395f1a785f693bdf09d0f1b
Author: Peter <peter@minskio.co.uk>
Date:   Mon, 26 Oct 2026 13:18:52 +0000

Replace admin tabs with submenu

Christ the Admin page is mess

Diffstat:
Minternal/app/admin.go | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Minternal/app/admin_thumbnails.go | 10+++++++---
Minternal/app/general.go | 1+
Minternal/app/properties.go | 4++--
Minternal/app/routes.go | 3++-
Mmain.go | 2+-
Dstatic/admin-tabs.js | 34----------------------------------
Mstatic/style.css | 8--------
Mtemplates/_header.html | 12+++++++++++-
Mtemplates/admin.html | 273++++++++++++++++++++++++++++++++++++-------------------------------------------
Mtemplates/notes.html | 2+-
Mtemplates/properties.html | 2+-
12 files changed, 237 insertions(+), 244 deletions(-)

diff --git a/internal/app/admin.go b/internal/app/admin.go @@ -12,12 +12,30 @@ import ( "time" ) +const adminDefaultPath = "/admin/settings" +const adminThumbnailsPath = "/admin/thumbnails" +var adminPages = map[string]bool{ + "settings": true, + "database": true, + "aliases": true, + "sed-rules": true, + "magic-links": true, + "orphans": true, + "thumbnails": true, + "thumbnails/regenerate": true, + "duplicates": true, +} + +func adminTabFromPath(p string) string { + return strings.Trim(strings.TrimPrefix(p, "/admin"), "/") +} + type AdminPageData struct { Config Config Error string Success string OrphanData OrphanData - ActiveTab string + Tab string MissingThumbnails []VideoFile RecentFiles []File } @@ -33,11 +51,32 @@ type SedRule struct { Command string `json:"command"` } -func renderAdminPage(w http.ResponseWriter, r *http.Request, data AdminPageData) { - if data.ActiveTab == "" { - data.ActiveTab = r.FormValue("active_tab") +// Only load data for relevant admin page +func loadAdminTabData(data *AdminPageData) { + switch data.Tab { + case "orphans": + orphanData, err := getOrphanedFiles(Cfg.UploadDir) + if err != nil { + log.Printf("Warning: loadAdminTabData: failed to get orphaned files: %v", err) + } + data.OrphanData = orphanData + + case "thumbnails": + missing, err := getMissingThumbnailVideos() + if err != nil { + log.Printf("Warning: loadAdminTabData: failed to get missing thumbnails: %v", err) + } + data.MissingThumbnails = missing + + case "thumbnails/regenerate": + data.RecentFiles = getRecentFiles() } +} + +func renderAdminPage(w http.ResponseWriter, r *http.Request, data AdminPageData) { + loadAdminTabData(&data) pageData := buildPageData("Admin", data) + pageData.AdminTab = data.Tab renderTemplate(w, "admin.html", pageData) } @@ -57,13 +96,13 @@ func getRecentFiles() []File { return files } -func currentAdminState(r *http.Request, orphanData OrphanData, missingThumbnails []VideoFile) AdminPageData { +func currentAdminState(r *http.Request) AdminPageData { + q := r.URL.Query() return AdminPageData{ - Config: Cfg, - OrphanData: orphanData, - ActiveTab: r.FormValue("active_tab"), - MissingThumbnails: missingThumbnails, - RecentFiles: getRecentFiles(), + Config: Cfg, + Tab: adminTabFromPath(r.URL.Path), + Success: q.Get("success"), + Error: q.Get("error"), } } @@ -77,31 +116,38 @@ func validateConfig(newConfig Config) error { return nil } -func adminHandler(w http.ResponseWriter, r *http.Request) { - // Get orphaned files - orphanData, err := getOrphanedFiles(Cfg.UploadDir) - if err != nil { - log.Printf("Warning: adminHandler: failed to get orphaned files: %v", err) +func adminIndexHandler(w http.ResponseWriter, r *http.Request) { + target := adminDefaultPath + if r.URL.RawQuery != "" { + target += "?" + r.URL.RawQuery } + http.Redirect(w, r, target, http.StatusTemporaryRedirect) +} - // Get video files for thumbnails - missingThumbnails, err := getMissingThumbnailVideos() - if err != nil { - log.Printf("Warning: adminHandler: failed to get missing thumbnails: %v", err) +// serve /admin/slug +func adminHandler(w http.ResponseWriter, r *http.Request) { + slug := adminTabFromPath(r.URL.Path) + if slug == "" { + adminIndexHandler(w, r) + return + } + if !adminPages[slug] { + http.NotFound(w, r) + return } switch r.Method { case http.MethodPost: switch r.FormValue("action") { case "save", "": - handleSaveSettings(w, r, orphanData, missingThumbnails) + handleSaveSettings(w, r) case "backup": err := backupDatabase(Cfg.DatabasePath) if err != nil { log.Printf("Error: adminHandler: database backup failed: %v", err) } - data := currentAdminState(r, orphanData, missingThumbnails) + data := currentAdminState(r) data.Error = errorString(err) data.Success = successString(err, "Database backup created successfully!") renderAdminPage(w, r, data) @@ -111,26 +157,26 @@ func adminHandler(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("Error: adminHandler: database vacuum failed: %v", err) } - data := currentAdminState(r, orphanData, missingThumbnails) + data := currentAdminState(r) data.Error = errorString(err) data.Success = successString(err, "Database vacuum completed successfully!") renderAdminPage(w, r, data) case "save_aliases": - handleSaveAliases(w, r, orphanData, missingThumbnails) + handleSaveAliases(w, r) case "save_sed_rules": - handleSaveSedRules(w, r, orphanData, missingThumbnails) + handleSaveSedRules(w, r) case "save_magic_links": - handleSaveMagicLinks(w, r, orphanData, missingThumbnails) + handleSaveMagicLinks(w, r) case "compute_properties": - handleComputeProperties(w, r, orphanData, missingThumbnails) + handleComputeProperties(w, r) } default: - renderAdminPage(w, r, currentAdminState(r, orphanData, missingThumbnails)) + renderAdminPage(w, r, currentAdminState(r)) } } @@ -204,10 +250,10 @@ func parseMagicLinksFromForm(r *http.Request) ([]MagicLinkRule, error) { return rules, nil } -func handleSaveMagicLinks(w http.ResponseWriter, r *http.Request, orphanData OrphanData, missingThumbnails []VideoFile) { +func handleSaveMagicLinks(w http.ResponseWriter, r *http.Request) { rules, err := parseMagicLinksFromForm(r) if err != nil { - data := currentAdminState(r, orphanData, missingThumbnails) + data := currentAdminState(r) data.Error = err.Error() renderAdminPage(w, r, data) return @@ -217,40 +263,40 @@ func handleSaveMagicLinks(w http.ResponseWriter, r *http.Request, orphanData Orp if err := SaveConfig(DB, Cfg); err != nil { log.Printf("Error: handleSaveMagicLinks: failed to save configuration: %v", err) - data := currentAdminState(r, orphanData, missingThumbnails) + data := currentAdminState(r) data.Error = "Failed to save configuration: " + err.Error() renderAdminPage(w, r, data) return } - data := currentAdminState(r, orphanData, missingThumbnails) + data := currentAdminState(r) data.Success = "Magic link rules saved successfully!" renderAdminPage(w, r, data) } -func handleSaveAliases(w http.ResponseWriter, r *http.Request, orphanData OrphanData, missingThumbnails []VideoFile) { +func handleSaveAliases(w http.ResponseWriter, r *http.Request) { Cfg.TagAliases = parseAliasesFromForm(r) if err := SaveConfig(DB, Cfg); err != nil { log.Printf("Error: handleSaveAliases: failed to save configuration: %v", err) - data := currentAdminState(r, orphanData, missingThumbnails) + data := currentAdminState(r) data.Error = "Failed to save configuration: " + err.Error() renderAdminPage(w, r, data) return } - data := currentAdminState(r, orphanData, missingThumbnails) + data := currentAdminState(r) data.Success = "Tag aliases saved successfully!" renderAdminPage(w, r, data) } -func handleSaveSettings(w http.ResponseWriter, r *http.Request, orphanData OrphanData, missingThumbnails []VideoFile) { +func handleSaveSettings(w http.ResponseWriter, r *http.Request) { newConfig := Cfg // preserve runtime fields newConfig.GallerySize = strings.TrimSpace(r.FormValue("gallery_size")) newConfig.ItemsPerPage = strings.TrimSpace(r.FormValue("items_per_page")) if err := validateConfig(newConfig); err != nil { - data := currentAdminState(r, orphanData, missingThumbnails) + data := currentAdminState(r) data.Error = err.Error() renderAdminPage(w, r, data) return @@ -260,21 +306,21 @@ func handleSaveSettings(w http.ResponseWriter, r *http.Request, orphanData Orpha if err := SaveConfig(DB, Cfg); err != nil { log.Printf("Error: handleSaveSettings: failed to save configuration: %v", err) - data := currentAdminState(r, orphanData, missingThumbnails) + data := currentAdminState(r) data.Error = "Failed to save configuration: " + err.Error() renderAdminPage(w, r, data) return } - data := currentAdminState(r, orphanData, missingThumbnails) + data := currentAdminState(r) data.Success = "Settings saved successfully!" renderAdminPage(w, r, data) } -func handleSaveSedRules(w http.ResponseWriter, r *http.Request, orphanData OrphanData, missingThumbnails []VideoFile) { +func handleSaveSedRules(w http.ResponseWriter, r *http.Request) { rules, err := parseSedRulesFromForm(r) if err != nil { - data := currentAdminState(r, orphanData, missingThumbnails) + data := currentAdminState(r) data.Error = err.Error() renderAdminPage(w, r, data) return @@ -284,13 +330,13 @@ func handleSaveSedRules(w http.ResponseWriter, r *http.Request, orphanData Orpha if err := SaveConfig(DB, Cfg); err != nil { log.Printf("Error: handleSaveSedRules: failed to save configuration: %v", err) - data := currentAdminState(r, orphanData, missingThumbnails) + data := currentAdminState(r) data.Error = "Failed to save configuration: " + err.Error() renderAdminPage(w, r, data) return } - data := currentAdminState(r, orphanData, missingThumbnails) + data := currentAdminState(r) data.Success = "Sed rules saved successfully!" renderAdminPage(w, r, data) } diff --git a/internal/app/admin_thumbnails.go b/internal/app/admin_thumbnails.go @@ -117,7 +117,7 @@ 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, "/admin", http.StatusSeeOther) + http.Redirect(w, r, adminThumbnailsPath, http.StatusSeeOther) return } @@ -128,6 +128,10 @@ func generateThumbnailHandler(w http.ResponseWriter, r *http.Request) { } redirectBase := "/" + redirectTo + if redirectTo == "admin" { + redirectBase = adminThumbnailsPath + } + fromAdmin := strings.HasPrefix(redirectBase, "/admin/") switch action { case "generate_all": @@ -182,8 +186,8 @@ func generateThumbnailHandler(w http.ResponseWriter, r *http.Request) { return } - if redirectTo == "admin" { - http.Redirect(w, r, "/admin?success="+url.QueryEscape(fmt.Sprintf("Thumbnail generated for file %s at %s", fileID, timestamp)), http.StatusSeeOther) + if fromAdmin { + http.Redirect(w, r, redirectBase+"?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) } diff --git a/internal/app/general.go b/internal/app/general.go @@ -31,6 +31,7 @@ type PageData struct { Pagination *Pagination GallerySize string UntaggedCount int + AdminTab string // slug of the admin page being viewed, empty elsewhere } type File struct { diff --git a/internal/app/properties.go b/internal/app/properties.go @@ -293,9 +293,9 @@ func propertiesIndexHandler(w http.ResponseWriter, r *http.Request) { renderTemplate(w, "properties.html", pageData) } -func handleComputeProperties(w http.ResponseWriter, r *http.Request, orphanData OrphanData, missingThumbnails []VideoFile) { +func handleComputeProperties(w http.ResponseWriter, r *http.Request) { count, err := computeMissingProperties() - data := currentAdminState(r, orphanData, missingThumbnails) + data := currentAdminState(r) if err != nil { data.Error = "Property computation failed: " + err.Error() } else { diff --git a/internal/app/routes.go b/internal/app/routes.go @@ -11,7 +11,8 @@ func RegisterRoutes() { http.HandleFunc("/add", uploadHandler) http.HandleFunc("/add-yt", ytdlpHandler) http.HandleFunc("/add-local", localFileHandler) - http.HandleFunc("/admin", adminHandler) + http.HandleFunc("/admin", adminIndexHandler) + http.HandleFunc("/admin/", adminHandler) http.HandleFunc("/admin/duplicates/delete", duplicatesDeleteHandler) http.HandleFunc("/admin/duplicates/delete-group", duplicatesDeleteGroupHandler) http.HandleFunc("/admin/duplicates/groups", duplicatesGroupsHandler) diff --git a/main.go b/main.go @@ -15,7 +15,7 @@ func main() { // CLI flags dataDir := flag.String("d", ".", "Data directory (stores tagliatelle.db and uploads/ subfolder)") port := flag.String("p", "8080", "Port to listen on") - czkawka := flag.String("czkawka", "czkawka_cli", "Path to the czkawka_cli binary (used by the admin Duplicates tab)") + czkawka := flag.String("czkawka", "czkawka_cli", "Path to the czkawka_cli binary (used by the admin Duplicates page)") flag.Parse() app.CzkawkaBin = *czkawka diff --git a/static/admin-tabs.js b/static/admin-tabs.js @@ -1,33 +0,0 @@ -function activateTab(contentPrefix, btnSelector, tabPrefix, tabName) { - document.querySelectorAll(btnSelector).forEach(btn => { - btn.style.borderBottomColor = 'transparent'; - btn.style.fontWeight = 'normal'; - }); - - document.querySelectorAll('[id^="' + contentPrefix + '"]').forEach(el => { - el.style.display = 'none'; - }); - - const selectedContent = document.getElementById(contentPrefix + tabName); - if (selectedContent) selectedContent.style.display = 'block'; - - const selectedBtn = document.getElementById(tabPrefix + tabName); - if (selectedBtn) { - selectedBtn.style.borderBottomColor = '#007bff'; - selectedBtn.style.fontWeight = 'bold'; - } -} - -function showAdminTab(tabName) { - activateTab('admin-content-', '.admin-tab-btn', 'admin-tab-', tabName); -} - -function showThumbnailSubTab(subTabName) { - activateTab('thumb-content-', '.thumb-subtab-btn', 'thumb-subtab-', subTabName); -} - -document.addEventListener('DOMContentLoaded', function() { - showAdminTab(window.activeAdminTab || 'settings'); - showThumbnailSubTab('missing'); - autoHideSuccessMessages(); -}); -\ No newline at end of file diff --git a/static/style.css b/static/style.css @@ -238,14 +238,6 @@ div.config-split {flex: 1} .admin-btn--success {background:#28a745} .admin-btn--purple {background:#6f42c1} .admin-btn--teal {background:#17a2b8} -/* tabs */ -.admin-tab-nav {margin-bottom:20px;border-bottom:2px solid #ddd} -.admin-tab-btn {padding:10px 20px;border:none;background:none;cursor:pointer;border-bottom:3px solid transparent;color:#cfcfcf} -.admin-tab-btn--active {border-bottom-color:#007bff;font-weight:bold} -.admin-tab-content {display:none} -.admin-subtab-nav {margin-bottom:20px;border-bottom:1px solid #ddd} -.thumb-subtab-btn {padding:8px 16px;border:none;background:none;cursor:pointer;border-bottom:2px solid transparent;color:#cfcfcf} -.thumb-subtab-btn--active {border-bottom-color:#007bff;font-weight:bold} /* orphans */ .admin-orphan-heading {margin-top:24px} .admin-orphan-list {list-style-type:disc;padding-left:20px} diff --git a/templates/_header.html b/templates/_header.html @@ -46,7 +46,17 @@ </li>{{end}} </ul></li> <li><a href="/notes"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="000000" d="M7.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zM7 10.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m.5 2.5a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1zm-1-11a.5.5 0 0 0-.5.5V3h-.5A1.5 1.5 0 0 0 4 4.5v12A1.5 1.5 0 0 0 5.5 18h6a.5.5 0 0 0 .354-.146l4-4A.5.5 0 0 0 16 13.5v-9A1.5 1.5 0 0 0 14.5 3H14v-.5a.5.5 0 0 0-1 0V3h-2.5v-.5a.5.5 0 0 0-1 0V3H7v-.5a.5.5 0 0 0-.5-.5m8 2a.5.5 0 0 1 .5.5V13h-2.5a1.5 1.5 0 0 0-1.5 1.5V17H5.5a.5.5 0 0 1-.5-.5v-12a.5.5 0 0 1 .5-.5zm-.207 10L12 16.293V14.5a.5.5 0 0 1 .5-.5z"/></svg><span>Notes</span></a></li> -<li><a href="/admin"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="#000000" d="M9 6.5a4.5 4.5 0 0 1 6.352-4.102a.5.5 0 0 1 .148.809L13.207 5.5L14.5 6.793L16.793 4.5a.5.5 0 0 1 .809.147a4.5 4.5 0 0 1-5.207 6.216L6.03 17.311a2.357 2.357 0 0 1-3.374-3.293L9.082 7.36A4.52 4.52 0 0 1 9 6.5ZM13.5 3a3.5 3.5 0 0 0-3.387 4.386a.5.5 0 0 1-.125.473l-6.612 6.854a1.357 1.357 0 0 0 1.942 1.896l6.574-6.66a.5.5 0 0 1 .512-.124a3.5 3.5 0 0 0 4.521-4.044l-2.072 2.073a.5.5 0 0 1-.707 0l-2-2a.5.5 0 0 1 0-.708l2.073-2.072a3.518 3.518 0 0 0-.72-.074Z"/></svg><span>Admin</span></a></li> +<li><a href="/admin/settings"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="#000000" d="M9 6.5a4.5 4.5 0 0 1 6.352-4.102a.5.5 0 0 1 .148.809L13.207 5.5L14.5 6.793L16.793 4.5a.5.5 0 0 1 .809.147a4.5 4.5 0 0 1-5.207 6.216L6.03 17.311a2.357 2.357 0 0 1-3.374-3.293L9.082 7.36A4.52 4.52 0 0 1 9 6.5ZM13.5 3a3.5 3.5 0 0 0-3.387 4.386a.5.5 0 0 1-.125.473l-6.612 6.854a1.357 1.357 0 0 0 1.942 1.896l6.574-6.66a.5.5 0 0 1 .512-.124a3.5 3.5 0 0 0 4.521-4.044l-2.072 2.073a.5.5 0 0 1-.707 0l-2-2a.5.5 0 0 1 0-.708l2.073-2.072a3.518 3.518 0 0 0-.72-.074Z"/></svg><span>Admin</span></a> + <ul class="sub-menu"> + <li><a href="/admin/settings">Settings</a></li> + <li><a href="/admin/database">Database</a></li> + <li><a href="/admin/aliases">Aliases</a></li> + <li><a href="/admin/sed-rules">Sed Rules</a></li> + <li><a href="/admin/magic-links">Magic Links</a></li> + <li><a href="/admin/orphans">Orphans</a></li> + <li><a href="/admin/thumbnails">Thumbnails</a><ul><li><a href="/admin/thumbnails/regenerate">Regenerate</a></li></ul></li> + <li><a href="/admin/duplicates">Duplicates</a></li> + </ul></li> </ul> <div id="search-container"> diff --git a/templates/admin.html b/templates/admin.html @@ -13,42 +13,13 @@ </div> {{end}} -<!-- Tab Navigation --> -<div class="admin-tab-nav"> - <button onclick="showAdminTab('settings')" id="admin-tab-settings" class="admin-tab-btn admin-tab-btn--active"> - Settings - </button> - <button onclick="showAdminTab('database')" id="admin-tab-database" class="admin-tab-btn"> - Database - </button> - <button onclick="showAdminTab('aliases')" id="admin-tab-aliases" class="admin-tab-btn"> - Aliases - </button> - <button onclick="showAdminTab('sedrules')" id="admin-tab-sedrules" class="admin-tab-btn"> - Sed Rules - </button> - <button onclick="showAdminTab('magiclinks')" id="admin-tab-magiclinks" class="admin-tab-btn"> - Magic Links - </button> - <button onclick="showAdminTab('orphans')" id="admin-tab-orphans" class="admin-tab-btn"> - Orphans - </button> - <button onclick="showAdminTab('thumbnails')" id="admin-tab-thumbnails" class="admin-tab-btn"> - Thumbnails - </button> - <button onclick="showAdminTab('duplicates')" id="admin-tab-duplicates" class="admin-tab-btn"> - Duplicates - </button> -</div> - -<!-- Settings Tab --> -<div id="admin-content-settings"> +{{if eq .Data.Tab "settings"}} +<!-- Settings --> +<div> <div class="config-container"> <div class="config-split"> <h2>Settings</h2> <form method="post" class="admin-form"> - <input type="hidden" name="active_tab" value="settings"> - <div class="admin-form-group"> <label for="gallery_size" class="admin-form-label">Gallery Size:</label> <input type="text" id="gallery_size" name="gallery_size" value="{{.Data.Config.GallerySize}}" required @@ -85,13 +56,14 @@ </div> </div> </div> +{{end}} -<!-- Database Tab --> -<div id="admin-content-database" class="admin-tab-content"> +{{if eq .Data.Tab "database"}} +<!-- Database --> +<div> <h2>Database Maintenance</h2> <form method="post" class="admin-form-inline"> - <input type="hidden" name="active_tab" value="database"> <input type="hidden" name="action" value="backup"> <button type="submit" class="admin-btn admin-btn--success"> Backup Database @@ -100,7 +72,6 @@ </form> <form method="post"> - <input type="hidden" name="active_tab" value="database"> <input type="hidden" name="action" value="vacuum"> <button type="submit" class="admin-btn admin-btn--purple"> Vacuum Database @@ -114,7 +85,6 @@ Compute auto-detected properties (filetype, duration, orientation, resolution) for any files that do not yet have them. </p> <form method="post"> - <input type="hidden" name="active_tab" value="database"> <input type="hidden" name="action" value="compute_properties"> <button type="submit" class="admin-btn admin-btn--teal"> Compute Missing Properties @@ -122,9 +92,11 @@ <small class="admin-hint admin-hint--inline">Processes only files with no existing properties</small> </form> </div> +{{end}} -<!-- Aliases Tab --> -<div id="admin-content-aliases" class="admin-tab-content"> +{{if eq .Data.Tab "aliases"}} +<!-- Aliases --> +<div> <h2>Tag Aliases</h2> <p> Define tag aliases so that multiple tag values are treated as equivalent when searching or filtering. @@ -139,7 +111,6 @@ </button> <form method="post" id="aliases-form" class="admin-form-top"> - <input type="hidden" name="active_tab" value="aliases"> <input type="hidden" name="action" value="save_aliases"> <input type="hidden" name="aliases_json" id="aliases_json"> <button type="submit" class="admin-btn admin-btn--primary"> @@ -148,9 +119,11 @@ </form> </div> </div> +{{end}} -<!-- Sed Rules Tab --> -<div id="admin-content-sedrules" class="admin-tab-content"> +{{if eq .Data.Tab "sed-rules"}} +<!-- Sed Rules --> +<div> <div class="config-container"> <div class="config-split"> <h2>Sed Rules for Notes</h2> @@ -166,8 +139,7 @@ + Add Sed Rule </button> - <form method="post" action="/admin" id="sedrules-form" class="admin-form-top"> - <input type="hidden" name="active_tab" value="sedrules"> + <form method="post" id="sedrules-form" class="admin-form-top"> <input type="hidden" name="action" value="save_sed_rules"> <input type="hidden" name="sed_rules_json" id="sed_rules_json"> <button type="submit" class="admin-btn admin-btn--primary"> @@ -189,9 +161,11 @@ </div> </div> </div> +{{end}} -<!-- Magic Links Tab --> -<div id="admin-content-magiclinks" class="admin-tab-content"> +{{if eq .Data.Tab "magic-links"}} +<!-- Magic Links --> +<div> <div class="config-container"> <div class="config-split"> <h2>Magic Links</h2> @@ -208,8 +182,7 @@ + Add Magic Link Rule </button> - <form method="post" action="/admin" id="magiclinks-form" class="admin-form-top"> - <input type="hidden" name="active_tab" value="magiclinks"> + <form method="post" id="magiclinks-form" class="admin-form-top"> <input type="hidden" name="action" value="save_magic_links"> <button type="submit" class="admin-btn admin-btn--primary"> Save Magic Links @@ -231,9 +204,11 @@ </div> </div> </div> +{{end}} -<!-- Orphans Tab --> -<div id="admin-content-orphans" class="admin-tab-content"> +{{if eq .Data.Tab "orphans"}} +<!-- Orphans --> +<div> <h2>Orphaned Files</h2> @@ -263,116 +238,108 @@ </div> </div> </div> +{{end}} -<!-- Thumbnails Tab --> -<div id="admin-content-thumbnails" class="admin-tab-content"> - <h2>Thumbnail Management</h2> - - <!-- Sub-tab Navigation --> - <div class="admin-subtab-nav"> - <button onclick="showThumbnailSubTab('missing')" id="thumb-subtab-missing" class="thumb-subtab-btn thumb-subtab-btn--active"> - Missing ({{len .Data.MissingThumbnails}}) - </button> - <button onclick="showThumbnailSubTab('regenerate')" id="thumb-subtab-regenerate" class="thumb-subtab-btn"> - 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" class="admin-form-inline"> - <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.');" class="admin-btn admin-btn--success"> - Generate All Missing Thumbnails - </button> - <small class="admin-hint admin-hint--inline">Uses timestamp 00:00:05 for all videos</small> - </form> - - <div class="admin-thumb-grid"> - {{range .Data.MissingThumbnails}} - <div class="admin-thumb-card"> - <h4 class="admin-thumb-card__title"> - <a href="/file/{{.ID}}" target="_blank">{{.Filename}}</a> - </h4> - <p class="admin-thumb-card__id">ID: {{.ID}}</p> - - <video class="admin-thumb-card__video" title="Click to capture frame"> - <source src="/uploads/{{.EscapedFilename}}"> - </video> - - <form method="post" action="/thumbnails/generate" class="admin-thumb-card__form"> - <input type="hidden" name="action" value="generate_single"> - <input type="hidden" name="file_id" value="{{.ID}}"> - <input type="hidden" name="redirect" value="admin"> - - <div class="admin-thumb-card__timestamp-row"> - <label class="admin-thumb-card__timestamp-label">Timestamp:</label> - <input type="text" name="timestamp" value="00:00:05" placeholder="00:00:05" - class="admin-thumb-card__timestamp-input"> - </div> - - <button type="submit" class="admin-btn admin-btn--primary admin-btn--full"> - Generate Thumbnail - </button> - </form> - </div> - {{end}} - </div> - {{else}} - <div class="admin-alert admin-alert--success"> - <strong>✓ All videos have thumbnails!</strong> - </div> - {{end}} - </div> - - <!-- Regenerate Sub-tab --> - <div id="thumb-content-regenerate" class="admin-tab-content"> - <h3>Regenerate Thumbnail</h3> +{{if eq .Data.Tab "thumbnails"}} +<!-- Missing Thumbnails --> +<div> + <h2>Missing Thumbnails ({{len .Data.MissingThumbnails}})</h2> + + {{if .Data.MissingThumbnails}} + <form method="post" action="/thumbnails/generate" class="admin-form-inline"> + <input type="hidden" name="action" value="generate_all"> + <input type="hidden" name="redirect" value="admin/thumbnails"> + <button type="submit" onclick="return confirm('Generate thumbnails for all {{len .Data.MissingThumbnails}} videos? This may take a while.');" class="admin-btn admin-btn--success"> + Generate All Missing Thumbnails + </button> + <small class="admin-hint admin-hint--inline">Uses timestamp 00:00:05 for all videos</small> + </form> - <div class="admin-regen-layout"> + <div class="admin-thumb-grid"> + {{range .Data.MissingThumbnails}} + <div class="admin-thumb-card"> + <h4 class="admin-thumb-card__title"> + <a href="/file/{{.ID}}" target="_blank">{{.Filename}}</a> + </h4> + <p class="admin-thumb-card__id">ID: {{.ID}}</p> - <div class="admin-regen-form-col"> - <div class="admin-tip-box"> - <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> + <video class="admin-thumb-card__video" title="Click to capture frame"> + <source src="/uploads/{{.EscapedFilename}}"> + </video> - <form method="post" action="/thumbnails/generate" class="admin-regen-form"> + <form method="post" action="/thumbnails/generate" class="admin-thumb-card__form"> <input type="hidden" name="action" value="generate_single"> - <input type="hidden" name="redirect" value="admin"> - - <div class="admin-form-group"> - <label for="file_id" class="admin-form-label">File ID:</label> - <input type="text" id="file_id" name="file_id" required - class="admin-mono-input" - placeholder="e.g., 312"> - <small class="admin-hint">Enter the ID of the video file</small> - </div> + <input type="hidden" name="file_id" value="{{.ID}}"> + <input type="hidden" name="redirect" value="admin/thumbnails"> - <div class="admin-form-group"> - <label for="timestamp" class="admin-form-label">Timestamp:</label> - <input type="text" id="timestamp" name="timestamp" value="00:00:05" required - class="admin-mono-input" - placeholder="00:00:05"> - <small class="admin-hint">Format: HH:MM:SS (e.g., 00:00:05 for 5 seconds)</small> + <div class="admin-thumb-card__timestamp-row"> + <label class="admin-thumb-card__timestamp-label">Timestamp:</label> + <input type="text" name="timestamp" value="00:00:05" placeholder="00:00:05" + class="admin-thumb-card__timestamp-input"> </div> <button type="submit" class="admin-btn admin-btn--primary admin-btn--full"> - Generate/Regenerate Thumbnail + Generate Thumbnail </button> </form> </div> + {{end}} + </div> + {{else}} + <div class="admin-alert admin-alert--success"> + <strong>✓ All videos have thumbnails!</strong> + </div> + {{end}} +</div> +{{end}} + +{{if eq .Data.Tab "thumbnails/regenerate"}} +<!-- Regenerate Thumbnail --> +<div> + <h2>Regenerate Thumbnail</h2> + + <div class="admin-regen-layout"> + + <div class="admin-regen-form-col"> + <div class="admin-tip-box"> + <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> - {{template "_recent" .}} + <form method="post" action="/thumbnails/generate" class="admin-regen-form"> + <input type="hidden" name="action" value="generate_single"> + <input type="hidden" name="redirect" value="admin/thumbnails/regenerate"> + <div class="admin-form-group"> + <label for="file_id" class="admin-form-label">File ID:</label> + <input type="text" id="file_id" name="file_id" required + class="admin-mono-input" + placeholder="e.g., 312"> + <small class="admin-hint">Enter the ID of the video file</small> + </div> + + <div class="admin-form-group"> + <label for="timestamp" class="admin-form-label">Timestamp:</label> + <input type="text" id="timestamp" name="timestamp" value="00:00:05" required + class="admin-mono-input" + placeholder="00:00:05"> + <small class="admin-hint">Format: HH:MM:SS (e.g., 00:00:05 for 5 seconds)</small> + </div> + + <button type="submit" class="admin-btn admin-btn--primary admin-btn--full"> + Generate/Regenerate Thumbnail + </button> + </form> </div> + + {{template "_recent" .}} + </div> </div> +{{end}} -<!-- Duplicates Tab --> -<div id="admin-content-duplicates" class="admin-tab-content"> +{{if eq .Data.Tab "duplicates"}} +<!-- Duplicates --> +<div> <h2>Duplicates</h2> <p class="admin-hint"> Scan one or two directories with <code>czkawka_cli</code> for similar images, then compare, delete or replace them. @@ -404,17 +371,24 @@ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m18 15-6-6-6 6"/></svg> </a> </div> +{{end}} +<script src="/static/common.js" defer></script> +<script src="/static/hover-preview.js" defer></script> +{{if eq .Data.Tab "aliases"}} <script>window.initialAliasGroups = {{.Data.Config.TagAliases}};</script> -<script>window.initialSedRules = {{.Data.Config.SedRules}};</script> -<script>window.initialMagicLinks = {{.Data.Config.MagicLinks}};</script> -<script>window.activeAdminTab = "{{.Data.ActiveTab}}";</script> <script src="/static/admin-tag-alias.js" defer></script> +{{end}} +{{if eq .Data.Tab "sed-rules"}} +<script>window.initialSedRules = {{.Data.Config.SedRules}};</script> <script src="/static/admin-sed-rules.js" defer></script> +{{end}} +{{if eq .Data.Tab "magic-links"}} +<script>window.initialMagicLinks = {{.Data.Config.MagicLinks}};</script> <script src="/static/admin-magic-links.js" defer></script> +{{end}} +{{if eq .Data.Tab "duplicates"}} <script src="/static/admin-duplicates.js" defer></script> -<script src="/static/admin-tabs.js"></script> -<script src="/static/common.js" defer></script> -<script src="/static/hover-preview.js" defer></script> +{{end}} -{{template "_footer"}} -\ No newline at end of file +{{template "_footer"}} diff --git a/templates/notes.html b/templates/notes.html @@ -53,7 +53,7 @@ {{else}} <div class="dropdown-empty"> No sed rules configured.<br> - <a href="/admin">Admin → Sed Rules</a> + <a href="/admin/sed-rules">Admin → Sed Rules</a> </div> {{end}} </div> diff --git a/templates/properties.html b/templates/properties.html @@ -10,7 +10,7 @@ </ul> </details> {{else}} -<p>No properties have been computed yet. Visit the <a href="/admin">Admin</a> page to compute them.</p> +<p>No properties have been computed yet. Visit the <a href="/admin/database">Admin → Database</a> page to compute them.</p> {{end}} <script src="/static/list-filter.js" defer></script>