tagliatelle

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

commit 284478598065f625e8adcc5217417d9292a4b8a3
parent 7ff9cf61738ba748258003ca08ed52c12326e496
Author: breadcat <breadcat@users.noreply.github.com>
Date:   Tue, 17 Feb 2026 15:18:28 +0000

Enable pagination on search results

Diffstat:
Minclude-filters.go | 6+++---
Minclude-pagination.go | 98++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Minclude-search.go | 71+++++++++++++++++++----------------------------------------------------
Minclude-types.go | 1+
Minclude-viewer.go | 2+-
Mtemplates/_pagination.html | 10+++++-----
Mtemplates/search.html | 2++
7 files changed, 128 insertions(+), 62 deletions(-)

diff --git a/include-filters.go b/include-filters.go @@ -46,7 +46,7 @@ func untaggedFilesHandler(w http.ResponseWriter, r *http.Request) { } files, total, _ := getUntaggedFilesPaginated(page, perPage) - pageData := buildPageDataWithPagination("Untagged Files", files, page, total, perPage) + pageData := buildPageDataWithPagination("Untagged Files", files, page, total, perPage, r) renderTemplate(w, "untagged.html", pageData) } @@ -153,7 +153,7 @@ func tagFilterHandler(w http.ResponseWriter, r *http.Request) { Tagged: files, Untagged: nil, Breadcrumbs: []Breadcrumb{}, - }, 1, len(files), len(files)) + }, 1, len(files), len(files), r) pageData.Breadcrumbs = breadcrumbs renderTemplate(w, "list.html", pageData) @@ -263,7 +263,7 @@ func tagFilterHandler(w http.ResponseWriter, r *http.Request) { Tagged: files, Untagged: nil, Breadcrumbs: []Breadcrumb{}, - }, page, total, perPage) + }, page, total, perPage, r) pageData.Breadcrumbs = breadcrumbs renderTemplate(w, "list.html", pageData) diff --git a/include-pagination.go b/include-pagination.go @@ -1,5 +1,12 @@ package main +import ( + "database/sql" + "net/http" + "net/url" + "strings" +) + func getUntaggedFilesPaginated(page, perPage int) ([]File, int, error) { // Get total count var total int @@ -21,12 +28,26 @@ func getUntaggedFilesPaginated(page, perPage int) ([]File, int, error) { return files, total, err } -func buildPageDataWithPagination(title string, data interface{}, page, total, perPage int) PageData { +func buildPageDataWithPagination(title string, data interface{}, page, total, perPage int, r *http.Request) PageData { pd := buildPageData(title, data) pd.Pagination = calculatePagination(page, total, perPage) + pd.Pagination.PageBaseURL = pageBaseURL(r) return pd } +// pageBaseURL returns a URL base suitable for appending page=N. +// It preserves all existing query parameters except 'page'. +// e.g. /search?q=cats → "?q=cats&" +// /browse → "?" +func pageBaseURL(r *http.Request) string { + params := r.URL.Query() + params.Del("page") + if encoded := params.Encode(); encoded != "" { + return "?" + encoded + "&" + } + return "?" +} + func calculatePagination(page, total, perPage int) *Pagination { totalPages := (total + perPage - 1) / perPage if totalPages < 1 { @@ -44,6 +65,81 @@ func calculatePagination(page, total, perPage int) *Pagination { } } +func getSearchResultsPaginated(query string, page, perPage int) ([]File, int, error) { + sqlPattern := "%" + strings.ReplaceAll(strings.ReplaceAll(strings.ToLower(query), "*", "%"), "?", "_") + "%" + + var total int + err := db.QueryRow(` + SELECT COUNT(DISTINCT f.id) + FROM files f + LEFT JOIN file_tags ft ON ft.file_id = f.id + LEFT JOIN tags t ON t.id = ft.tag_id + WHERE LOWER(f.filename) LIKE ? OR LOWER(f.description) LIKE ? OR LOWER(t.value) LIKE ? + `, sqlPattern, sqlPattern, sqlPattern).Scan(&total) + if err != nil { + return nil, 0, err + } + + offset := (page - 1) * perPage + rows, err := db.Query(` + SELECT f.id, f.filename, f.path, COALESCE(f.description, '') AS description, + c.name AS category, t.value AS tag + FROM files f + LEFT JOIN file_tags ft ON ft.file_id = f.id + LEFT JOIN tags t ON t.id = ft.tag_id + LEFT JOIN categories c ON c.id = t.category_id + WHERE f.id IN ( + SELECT DISTINCT f2.id + FROM files f2 + LEFT JOIN file_tags ft2 ON ft2.file_id = f2.id + LEFT JOIN tags t2 ON t2.id = ft2.tag_id + WHERE LOWER(f2.filename) LIKE ? OR LOWER(f2.description) LIKE ? OR LOWER(t2.value) LIKE ? + ORDER BY f2.filename + LIMIT ? OFFSET ? + ) + ORDER BY f.filename + `, sqlPattern, sqlPattern, sqlPattern, perPage, offset) + if err != nil { + return nil, 0, err + } + defer rows.Close() + + fileMap := make(map[int]*File) + var orderedIDs []int + for rows.Next() { + var id int + var filename, path, description, category, tag sql.NullString + if err := rows.Scan(&id, &filename, &path, &description, &category, &tag); err != nil { + return nil, 0, err + } + f, exists := fileMap[id] + if !exists { + f = &File{ + ID: id, + Filename: filename.String, + Path: path.String, + EscapedFilename: url.PathEscape(filename.String), + Description: description.String, + Tags: make(map[string][]string), + } + fileMap[id] = f + orderedIDs = append(orderedIDs, id) + } + if category.Valid && tag.Valid && tag.String != "" { + f.Tags[category.String] = append(f.Tags[category.String], tag.String) + } + } + if err := rows.Err(); err != nil { + return nil, 0, err + } + + files := make([]File, 0, len(orderedIDs)) + for _, id := range orderedIDs { + files = append(files, *fileMap[id]) + } + return files, total, nil +} + func getTaggedFilesPaginated(page, perPage int) ([]File, int, error) { // Get total count var total int diff --git a/include-search.go b/include-search.go @@ -1,76 +1,43 @@ package main import ( - "database/sql" - "fmt" - "net/http" - "net/url" - "strings" + "fmt" + "net/http" + "strconv" + "strings" ) func searchHandler(w http.ResponseWriter, r *http.Request) { query := strings.TrimSpace(r.URL.Query().Get("q")) + page := 1 + if p, err := strconv.Atoi(r.URL.Query().Get("page")); err == nil && p > 0 { + page = p + } + perPage := 100 + if config.ItemsPerPage != "" { + if n, err := strconv.Atoi(config.ItemsPerPage); err == nil && n > 0 { + perPage = n + } + } + var files []File + var total int var searchTitle string if query != "" { - sqlPattern := "%" + strings.ReplaceAll(strings.ReplaceAll(strings.ToLower(query), "*", "%"), "?", "_") + "%" - - rows, err := db.Query(` - SELECT f.id, f.filename, f.path, COALESCE(f.description, '') AS description, - c.name AS category, t.value AS tag - FROM files f - LEFT JOIN file_tags ft ON ft.file_id = f.id - LEFT JOIN tags t ON t.id = ft.tag_id - LEFT JOIN categories c ON c.id = t.category_id - WHERE LOWER(f.filename) LIKE ? OR LOWER(f.description) LIKE ? OR LOWER(t.value) LIKE ? - ORDER BY f.filename - `, sqlPattern, sqlPattern, sqlPattern) + var err error + files, total, err = getSearchResultsPaginated(query, page, perPage) if err != nil { renderError(w, "Search failed: "+err.Error(), http.StatusInternalServerError) return } - defer rows.Close() - - fileMap := make(map[int]*File) - for rows.Next() { - var id int - var filename, path, description, category, tag sql.NullString - - if err := rows.Scan(&id, &filename, &path, &description, &category, &tag); err != nil { - renderError(w, "Failed to read search results: "+err.Error(), http.StatusInternalServerError) - return - } - - f, exists := fileMap[id] - if !exists { - f = &File{ - ID: id, - Filename: filename.String, - Path: path.String, - EscapedFilename: url.PathEscape(filename.String), - Description: description.String, - Tags: make(map[string][]string), - } - fileMap[id] = f - } - - if category.Valid && tag.Valid && tag.String != "" { - f.Tags[category.String] = append(f.Tags[category.String], tag.String) - } - } - - for _, f := range fileMap { - files = append(files, *f) - } - searchTitle = fmt.Sprintf("Search Results for: %s", query) } else { searchTitle = "Search Files" } - pageData := buildPageData(searchTitle, files) + pageData := buildPageDataWithPagination(searchTitle, files, page, total, perPage, r) pageData.Query = query pageData.Files = files renderTemplate(w, "search.html", pageData) diff --git a/include-types.go b/include-types.go @@ -62,6 +62,7 @@ type Pagination struct { PrevPage int NextPage int PerPage int + PageBaseURL string } type VideoFile struct { diff --git a/include-viewer.go b/include-viewer.go @@ -237,7 +237,7 @@ func listFilesHandler(w http.ResponseWriter, r *http.Request) { Tagged: tagged, Untagged: untagged, Breadcrumbs: []Breadcrumb{}, - }, page, total, perPage) + }, page, total, perPage, r) renderTemplate(w, "list.html", pageData) } \ No newline at end of file diff --git a/templates/_pagination.html b/templates/_pagination.html @@ -4,8 +4,8 @@ {{if gt .Pagination.TotalPages 1}} <div class="pagination"> {{if .Pagination.HasPrev}} - <a href="?page=1">&laquo;&laquo; First</a> - <a href="?page={{.Pagination.PrevPage}}">&laquo; Previous</a> + <a href="{{.Pagination.PageBaseURL}}page=1">&laquo;&laquo; First</a> + <a href="{{.Pagination.PageBaseURL}}page={{.Pagination.PrevPage}}">&laquo; Previous</a> {{else}} <span class="disabled">&laquo;&laquo; First</span> <span class="disabled">&laquo; Previous</span> @@ -18,13 +18,13 @@ value="{{.Pagination.CurrentPage}}" min="1" max="{{.Pagination.TotalPages}}" - onkeypress="if(event.key === 'Enter') { var page = parseInt(this.value); if(page >= 1 && page <= {{.Pagination.TotalPages}}) { window.location.href = '?page=' + page; } }"> + onkeypress="if(event.key === 'Enter') { var page = parseInt(this.value); if(page >= 1 && page <= {{.Pagination.TotalPages}}) { window.location.href = '{{.Pagination.PageBaseURL}}page=' + page; } }"> of {{.Pagination.TotalPages}} </span> {{if .Pagination.HasNext}} - <a href="?page={{.Pagination.NextPage}}">Next &raquo;</a> - <a href="?page={{.Pagination.TotalPages}}">Last &raquo;&raquo;</a> + <a href="{{.Pagination.PageBaseURL}}page={{.Pagination.NextPage}}">Next &raquo;</a> + <a href="{{.Pagination.PageBaseURL}}page={{.Pagination.TotalPages}}">Last &raquo;&raquo;</a> {{else}} <span class="disabled">Next &raquo;</span> <span class="disabled">Last &raquo;&raquo;</span> diff --git a/templates/search.html b/templates/search.html @@ -15,4 +15,6 @@ <p>Try using wildcards like <code>*{{.Query}}*</code> for broader results.</p> {{end}} +{{template "_pagination" .}} + {{template "_footer"}} \ No newline at end of file