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:
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">«« First</a>
- <a href="?page={{.Pagination.PrevPage}}">« Previous</a>
+ <a href="{{.Pagination.PageBaseURL}}page=1">«« First</a>
+ <a href="{{.Pagination.PageBaseURL}}page={{.Pagination.PrevPage}}">« Previous</a>
{{else}}
<span class="disabled">«« First</span>
<span class="disabled">« 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 »</a>
- <a href="?page={{.Pagination.TotalPages}}">Last »»</a>
+ <a href="{{.Pagination.PageBaseURL}}page={{.Pagination.NextPage}}">Next »</a>
+ <a href="{{.Pagination.PageBaseURL}}page={{.Pagination.TotalPages}}">Last »»</a>
{{else}}
<span class="disabled">Next »</span>
<span class="disabled">Last »»</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