taggart

Simple golang tagging filesystem webapp
Log | Files | Refs

commit eaec34402ea1f954bfda4df67166ba67f40eb6ca
parent 551a2945612cfa201d7815fb6c89941d6c54438a
Author: breadcat <breadcat@users.noreply.github.com>
Date:   Tue,  7 Oct 2025 16:24:14 +0100

Start of pagination work

Diffstat:
Mmain.go | 223+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mstatic/style.css | 8++++++++
Mtemplates/list.html | 23++++++++++++++++++++---
Mtemplates/untagged.html | 23++++++++++++++++++++++-
4 files changed, 229 insertions(+), 48 deletions(-)

diff --git a/main.go b/main.go @@ -50,15 +50,28 @@ type TagDisplay struct { } type PageData struct { - Title string - Data interface{} - Query string - IP string - Port string - Files []File - Tags map[string][]TagDisplay + Title string + Data interface{} + Query string + IP string + Port string + Files []File + Tags map[string][]TagDisplay + Pagination *Pagination } +type Pagination struct { + CurrentPage int + TotalPages int + HasPrev bool + HasNext bool + PrevPage int + NextPage int + PerPage int +} + +package main + func getOrCreateCategoryAndTag(category, value string) (int, int, error) { var catID int err := db.QueryRow("SELECT id FROM categories WHERE name=?", category).Scan(&catID) @@ -119,6 +132,26 @@ func getTaggedFiles() ([]File, error) { `) } +func getTaggedFilesPaginated(page, perPage int) ([]File, int, error) { + // Get total count + var total int + err := db.QueryRow(`SELECT COUNT(DISTINCT f.id) FROM files f JOIN file_tags ft ON ft.file_id = f.id`).Scan(&total) + if err != nil { + return nil, 0, err + } + + offset := (page - 1) * perPage + files, err := queryFilesWithTags(` + SELECT DISTINCT f.id, f.filename, f.path, COALESCE(f.description, '') as description + FROM files f + JOIN file_tags ft ON ft.file_id = f.id + ORDER BY f.id DESC + LIMIT ? OFFSET ? + `, perPage, offset) + + return files, total, err +} + func getUntaggedFiles() ([]File, error) { return queryFilesWithTags(` SELECT f.id, f.filename, f.path, COALESCE(f.description, '') as description @@ -129,11 +162,55 @@ func getUntaggedFiles() ([]File, error) { `) } +func getUntaggedFilesPaginated(page, perPage int) ([]File, int, error) { + // Get total count + var total int + err := db.QueryRow(`SELECT COUNT(*) FROM files f LEFT JOIN file_tags ft ON ft.file_id = f.id WHERE ft.file_id IS NULL`).Scan(&total) + if err != nil { + return nil, 0, err + } + + offset := (page - 1) * perPage + files, err := queryFilesWithTags(` + SELECT f.id, f.filename, f.path, COALESCE(f.description, '') as description + FROM files f + LEFT JOIN file_tags ft ON ft.file_id = f.id + WHERE ft.file_id IS NULL + ORDER BY f.id DESC + LIMIT ? OFFSET ? + `, perPage, offset) + + return files, total, err +} + func buildPageData(title string, data interface{}) PageData { tagMap, _ := getTagData() return PageData{Title: title, Data: data, Tags: tagMap} } +func buildPageDataWithPagination(title string, data interface{}, page, total, perPage int) PageData { + pd := buildPageData(title, data) + pd.Pagination = calculatePagination(page, total, perPage) + return pd +} + +func calculatePagination(page, total, perPage int) *Pagination { + totalPages := (total + perPage - 1) / perPage + if totalPages < 1 { + totalPages = 1 + } + + return &Pagination{ + CurrentPage: page, + TotalPages: totalPages, + HasPrev: page > 1, + HasNext: page < totalPages, + PrevPage: page - 1, + NextPage: page + 1, + PerPage: perPage, + } +} + func buildPageDataWithIP(title string, data interface{}) PageData { pageData := buildPageData(title, data) ip, _ := getLocalIP() @@ -320,39 +397,57 @@ func searchHandler(w http.ResponseWriter, r *http.Request) { } func processUpload(src io.Reader, filename string) (int64, string, error) { - finalFilename, finalPath, err := checkFileConflictStrict(filename) - if err != nil { - return 0, "", err - } - - tempPath := finalPath + ".tmp" - tempFile, err := os.Create(tempPath) - if err != nil { - return 0, "", fmt.Errorf("failed to create temp file: %v", err) - } - - _, err = io.Copy(tempFile, src) - tempFile.Close() - if err != nil { - os.Remove(tempPath) - return 0, "", fmt.Errorf("failed to copy file data: %v", err) - } - - processedPath, warningMsg, err := processVideoFile(tempPath, finalPath) - if err != nil { - os.Remove(tempPath) - return 0, "", err - } - - id, err := saveFileToDatabase(finalFilename, processedPath) - if err != nil { - os.Remove(processedPath) - return 0, "", err - } - - return id, warningMsg, nil + finalFilename, finalPath, err := checkFileConflictStrict(filename) + if err != nil { + return 0, "", err + } + + tempPath := finalPath + ".tmp" + tempFile, err := os.Create(tempPath) + if err != nil { + return 0, "", fmt.Errorf("failed to create temp file: %v", err) + } + + _, err = io.Copy(tempFile, src) + tempFile.Close() + if err != nil { + os.Remove(tempPath) + return 0, "", fmt.Errorf("failed to copy file data: %v", err) + } + + ext := strings.ToLower(filepath.Ext(filename)) + videoExts := map[string]bool{ + ".mp4": true, ".mov": true, ".avi": true, + ".mkv": true, ".webm": true, ".m4v": true, + } + + var processedPath string + var warningMsg string + + if videoExts[ext] { + processedPath, warningMsg, err = processVideoFile(tempPath, finalPath) + if err != nil { + os.Remove(tempPath) + return 0, "", err + } + } else { + // Non-video → just rename temp file to final + if err := os.Rename(tempPath, finalPath); err != nil { + return 0, "", fmt.Errorf("failed to move file: %v", err) + } + processedPath = finalPath + } + + id, err := saveFileToDatabase(finalFilename, processedPath) + if err != nil { + os.Remove(processedPath) + return 0, "", err + } + + return id, warningMsg, nil } + func uploadFromURLHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Redirect(w, r, "/upload", http.StatusSeeOther) @@ -405,20 +500,60 @@ func uploadFromURLHandler(w http.ResponseWriter, r *http.Request) { } func listFilesHandler(w http.ResponseWriter, r *http.Request) { - tagged, _ := getTaggedFiles() - untagged, _ := getUntaggedFiles() + // Get page number from query params + pageStr := r.URL.Query().Get("page") + page := 1 + if pageStr != "" { + if p, err := strconv.Atoi(pageStr); err == nil && p > 0 { + page = p + } + } + + // Get per page from config + perPage := 50 + if config.ItemsPerPage != "" { + if pp, err := strconv.Atoi(config.ItemsPerPage); err == nil && pp > 0 { + perPage = pp + } + } + + tagged, taggedTotal, _ := getTaggedFilesPaginated(page, perPage) + untagged, untaggedTotal, _ := getUntaggedFilesPaginated(page, perPage) - pageData := buildPageData("Home", struct { + // Use the larger total for pagination + total := taggedTotal + if untaggedTotal > total { + total = untaggedTotal + } + + pageData := buildPageDataWithPagination("Home", struct { Tagged []File Untagged []File - }{tagged, untagged}) + }{tagged, untagged}, page, total, perPage) renderTemplate(w, "list.html", pageData) } func untaggedFilesHandler(w http.ResponseWriter, r *http.Request) { - files, _ := getUntaggedFiles() - pageData := buildPageData("Untagged Files", files) + // Get page number from query params + pageStr := r.URL.Query().Get("page") + page := 1 + if pageStr != "" { + if p, err := strconv.Atoi(pageStr); err == nil && p > 0 { + page = p + } + } + + // Get per page from config + perPage := 50 + if config.ItemsPerPage != "" { + if pp, err := strconv.Atoi(config.ItemsPerPage); err == nil && pp > 0 { + perPage = pp + } + } + + files, total, _ := getUntaggedFilesPaginated(page, perPage) + pageData := buildPageDataWithPagination("Untagged Files", files, page, total, perPage) renderTemplate(w, "untagged.html", pageData) } diff --git a/static/style.css b/static/style.css @@ -57,3 +57,11 @@ pre#text-viewer{font-family:serif;font-size:25px;line-height:1.8} #text-viewer-container:fullscreen{margin:0;max-width:75%;margin:auto;height:100vh;padding:1em;background:#000;display:flex;flex-direction:column} #text-viewer-container:fullscreen #text-viewer{flex:1;max-height:none!important;margin:0;height:100%} #text-viewer-container:fullscreen>div{flex-shrink:0} + +/* pagination */ +.pagination .disabled,.pagination a{border-radius:4px;padding:.5rem 1rem} +.pagination{display:flex;justify-content:center;align-items:center;gap:1rem;margin:2rem 0;padding:1rem} +.pagination a{background:#007bff;color:#fff;text-decoration:none;transition:background .2s} +.pagination a:hover{background:#0056b3} +.pagination .disabled{background:#ccc;color:#666;cursor:not-allowed} +.pagination .page-info{font-weight:700;padding:.5rem 1rem} diff --git a/templates/list.html b/templates/list.html @@ -3,12 +3,9 @@ {{if .Data.Tagged}} <h2>Tagged Files</h2> - <div class="gallery"> {{range .Data.Tagged}} - {{template "_gallery" .}} - {{else}} <p>No tagged files yet.</p> {{end}} @@ -26,4 +23,24 @@ </div> {{end}} +{{if .Pagination}} +{{if gt .Pagination.TotalPages 1}} +<div class="pagination"> + {{if .Pagination.HasPrev}} + <a href="?page={{.Pagination.PrevPage}}">&laquo; Previous</a> + {{else}} + <span class="disabled">&laquo; Previous</span> + {{end}} + + <span class="page-info">Page {{.Pagination.CurrentPage}} of {{.Pagination.TotalPages}}</span> + + {{if .Pagination.HasNext}} + <a href="?page={{.Pagination.NextPage}}">Next &raquo;</a> + {{else}} + <span class="disabled">Next &raquo;</span> + {{end}} +</div> +{{end}} +{{end}} + {{template "_footer"}} diff --git a/templates/untagged.html b/templates/untagged.html @@ -5,7 +5,28 @@ {{range .Data}} {{template "_gallery" .}} {{else}} -<p>No untagged files.</p> + <p>No untagged files.</p> {{end}} </div> + +{{if .Pagination}} +{{if gt .Pagination.TotalPages 1}} +<div class="pagination"> + {{if .Pagination.HasPrev}} + <a href="?page={{.Pagination.PrevPage}}">&laquo; Previous</a> + {{else}} + <span class="disabled">&laquo; Previous</span> + {{end}} + + <span class="page-info">Page {{.Pagination.CurrentPage}} of {{.Pagination.TotalPages}}</span> + + {{if .Pagination.HasNext}} + <a href="?page={{.Pagination.NextPage}}">Next &raquo;</a> + {{else}} + <span class="disabled">Next &raquo;</span> + {{end}} +</div> +{{end}} +{{end}} + {{template "_footer"}}