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:
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>