include-pagination.go (5168B)
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(*) 100 FROM ( 101 SELECT f.id 102 FROM files f 103 LEFT JOIN file_tags ft ON ft.file_id = f.id 104 LEFT JOIN tags t ON t.id = ft.tag_id 105 WHERE LOWER(f.filename) LIKE ? 106 OR LOWER(f.description) LIKE ? 107 OR LOWER(t.value) LIKE ? 108 GROUP BY f.id 109 ) matched 110 `, sqlPattern, sqlPattern, sqlPattern).Scan(&total) 111 if err != nil { 112 return nil, 0, err 113 } 114 115 offset := (page - 1) * perPage 116 rows, err := db.Query(` 117 SELECT 118 f.id, 119 f.filename, 120 f.path, 121 COALESCE(f.description, '') AS description, 122 c.name AS category, 123 t.value AS tag 124 FROM ( 125 SELECT f2.id 126 FROM files f2 127 LEFT JOIN file_tags ft2 ON ft2.file_id = f2.id 128 LEFT JOIN tags t2 ON t2.id = ft2.tag_id 129 WHERE LOWER(f2.filename) LIKE ? 130 OR LOWER(f2.description) LIKE ? 131 OR LOWER(t2.value) LIKE ? 132 GROUP BY f2.id 133 ORDER BY f2.id DESC 134 LIMIT ? OFFSET ? 135 ) matched 136 JOIN files f 137 ON f.id = matched.id 138 LEFT JOIN file_tags ft 139 ON ft.file_id = f.id 140 LEFT JOIN tags t 141 ON t.id = ft.tag_id 142 LEFT JOIN categories c 143 ON c.id = t.category_id 144 ORDER BY f.id DESC, c.name, t.value 145 `, sqlPattern, sqlPattern, sqlPattern, perPage, offset) 146 if err != nil { 147 return nil, 0, err 148 } 149 defer rows.Close() 150 151 fileMap := make(map[int]*File) 152 files := make([]File, 0, perPage) 153 154 for rows.Next() { 155 var ( 156 id int 157 filename, path, description sql.NullString 158 category, tag sql.NullString 159 ) 160 161 if err := rows.Scan(&id, &filename, &path, &description, &category, &tag); err != nil { 162 return nil, 0, err 163 } 164 f, exists := fileMap[id] 165 if !exists { 166 file := File{ 167 ID: id, 168 Filename: filename.String, 169 Path: path.String, 170 EscapedFilename: url.PathEscape(filename.String), 171 Description: description.String, 172 Tags: make(map[string][]string), 173 } 174 files = append(files, file) 175 f = &files[len(files)-1] 176 fileMap[id] = f 177 } 178 if category.Valid && tag.Valid && tag.String != "" { 179 f.Tags[category.String] = append(f.Tags[category.String], tag.String) 180 } 181 } 182 if err := rows.Err(); err != nil { 183 return nil, 0, err 184 } 185 186 return files, total, nil 187 } 188 189 func getTaggedFilesPaginated(page, perPage int) ([]File, int, error) { 190 // Get total count 191 var total int 192 err := db.QueryRow(`SELECT COUNT(DISTINCT f.id) FROM files f JOIN file_tags ft ON ft.file_id = f.id`).Scan(&total) 193 if err != nil { 194 return nil, 0, err 195 } 196 197 offset := (page - 1) * perPage 198 files, err := queryFilesWithTags(` 199 SELECT DISTINCT f.id, f.filename, f.path, COALESCE(f.description, '') as description 200 FROM files f 201 JOIN file_tags ft ON ft.file_id = f.id 202 ORDER BY f.id DESC 203 LIMIT ? OFFSET ? 204 `, perPage, offset) 205 206 return files, total, err 207 }