tagliatelle

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

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 }