include-pagination.go (5024B)
1 package main 2 3 import ( 4 "database/sql" 5 "net/http" 6 "net/url" 7 "strconv" 8 "strings" 9 ) 10 11 type Pagination struct { 12 CurrentPage int 13 TotalPages int 14 HasPrev bool 15 HasNext bool 16 PrevPage int 17 NextPage int 18 PerPage int 19 PageBaseURL string 20 } 21 22 func pageFromRequest(r *http.Request) int { 23 if p, err := strconv.Atoi(r.URL.Query().Get("page")); err == nil && p > 0 { 24 return p 25 } 26 return 1 27 } 28 29 func perPageFromConfig(fallback int) int { 30 if n, err := strconv.Atoi(config.ItemsPerPage); err == nil && n > 0 { 31 return n 32 } 33 return fallback 34 } 35 36 func getUntaggedFilesPaginated(page, perPage int) ([]File, int, error) { 37 // Get total count 38 var total int 39 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) 40 if err != nil { 41 return nil, 0, err 42 } 43 44 offset := (page - 1) * perPage 45 files, err := queryFilesWithTags(` 46 SELECT f.id, f.filename, f.path, COALESCE(f.description, '') as description 47 FROM files f 48 LEFT JOIN file_tags ft ON ft.file_id = f.id 49 WHERE ft.file_id IS NULL 50 ORDER BY f.id DESC 51 LIMIT ? OFFSET ? 52 `, perPage, offset) 53 54 return files, total, err 55 } 56 57 func buildPageDataWithPagination(title string, data interface{}, page, total, perPage int, r *http.Request) PageData { 58 pd := buildPageData(title, data) 59 pd.Pagination = calculatePagination(page, total, perPage) 60 pd.Pagination.PageBaseURL = pageBaseURL(r) 61 return pd 62 } 63 64 // pageBaseURL returns a URL base suitable for appending page=N. 65 // It preserves all existing query parameters except 'page'. 66 // e.g. /search?q=cats → "?q=cats&" 67 // /browse → "?" 68 func pageBaseURL(r *http.Request) string { 69 params := r.URL.Query() 70 params.Del("page") 71 if encoded := params.Encode(); encoded != "" { 72 return "?" + encoded + "&" 73 } 74 return "?" 75 } 76 77 func calculatePagination(page, total, perPage int) *Pagination { 78 totalPages := (total + perPage - 1) / perPage 79 if totalPages < 1 { 80 totalPages = 1 81 } 82 83 return &Pagination{ 84 CurrentPage: page, 85 TotalPages: totalPages, 86 HasPrev: page > 1, 87 HasNext: page < totalPages, 88 PrevPage: page - 1, 89 NextPage: page + 1, 90 PerPage: perPage, 91 } 92 } 93 94 func getSearchResultsPaginated(query string, page, perPage int) ([]File, int, error) { 95 sqlPattern := "%" + strings.ReplaceAll(strings.ReplaceAll(strings.ToLower(query), "*", "%"), "?", "_") + "%" 96 97 var total int 98 err := db.QueryRow(` 99 SELECT COUNT(DISTINCT f.id) 100 FROM files f 101 LEFT JOIN file_tags ft ON ft.file_id = f.id 102 LEFT JOIN tags t ON t.id = ft.tag_id 103 WHERE LOWER(f.filename) LIKE ? OR LOWER(f.description) LIKE ? OR LOWER(t.value) LIKE ? 104 `, sqlPattern, sqlPattern, sqlPattern).Scan(&total) 105 if err != nil { 106 return nil, 0, err 107 } 108 109 offset := (page - 1) * perPage 110 rows, err := db.Query(` 111 SELECT f.id, f.filename, f.path, COALESCE(f.description, '') AS description, 112 c.name AS category, t.value AS tag 113 FROM files f 114 LEFT JOIN file_tags ft ON ft.file_id = f.id 115 LEFT JOIN tags t ON t.id = ft.tag_id 116 LEFT JOIN categories c ON c.id = t.category_id 117 WHERE f.id IN ( 118 SELECT DISTINCT f2.id 119 FROM files f2 120 LEFT JOIN file_tags ft2 ON ft2.file_id = f2.id 121 LEFT JOIN tags t2 ON t2.id = ft2.tag_id 122 WHERE LOWER(f2.filename) LIKE ? OR LOWER(f2.description) LIKE ? OR LOWER(t2.value) LIKE ? 123 ORDER BY f2.filename 124 LIMIT ? OFFSET ? 125 ) 126 ORDER BY f.filename 127 `, sqlPattern, sqlPattern, sqlPattern, perPage, offset) 128 if err != nil { 129 return nil, 0, err 130 } 131 defer rows.Close() 132 133 fileMap := make(map[int]*File) 134 var orderedIDs []int 135 for rows.Next() { 136 var id int 137 var filename, path, description, category, tag sql.NullString 138 if err := rows.Scan(&id, &filename, &path, &description, &category, &tag); err != nil { 139 return nil, 0, err 140 } 141 f, exists := fileMap[id] 142 if !exists { 143 f = &File{ 144 ID: id, 145 Filename: filename.String, 146 Path: path.String, 147 EscapedFilename: url.PathEscape(filename.String), 148 Description: description.String, 149 Tags: make(map[string][]string), 150 } 151 fileMap[id] = f 152 orderedIDs = append(orderedIDs, id) 153 } 154 if category.Valid && tag.Valid && tag.String != "" { 155 f.Tags[category.String] = append(f.Tags[category.String], tag.String) 156 } 157 } 158 if err := rows.Err(); err != nil { 159 return nil, 0, err 160 } 161 162 files := make([]File, 0, len(orderedIDs)) 163 for _, id := range orderedIDs { 164 files = append(files, *fileMap[id]) 165 } 166 return files, total, nil 167 } 168 169 func getTaggedFilesPaginated(page, perPage int) ([]File, int, error) { 170 // Get total count 171 var total int 172 err := db.QueryRow(`SELECT COUNT(DISTINCT f.id) FROM files f JOIN file_tags ft ON ft.file_id = f.id`).Scan(&total) 173 if err != nil { 174 return nil, 0, err 175 } 176 177 offset := (page - 1) * perPage 178 files, err := queryFilesWithTags(` 179 SELECT DISTINCT f.id, f.filename, f.path, COALESCE(f.description, '') as description 180 FROM files f 181 JOIN file_tags ft ON ft.file_id = f.id 182 ORDER BY f.id DESC 183 LIMIT ? OFFSET ? 184 `, perPage, offset) 185 186 return files, total, err 187 }