commit eaec34402ea1f954bfda4df67166ba67f40eb6ca
parent 551a2945612cfa201d7815fb6c89941d6c54438a
Author: breadcat <breadcat@users.noreply.github.com>
Date: Tue, 7 Oct 2025 16:24:14 +0100
Start of pagination work
Diffstat:
4 files changed, 229 insertions(+), 48 deletions(-)
diff --git a/main.go b/main.go
@@ -50,15 +50,28 @@ type TagDisplay struct {
}
type PageData struct {
- Title string
- Data interface{}
- Query string
- IP string
- Port string
- Files []File
- Tags map[string][]TagDisplay
+ Title string
+ Data interface{}
+ Query string
+ IP string
+ Port string
+ Files []File
+ Tags map[string][]TagDisplay
+ Pagination *Pagination
}
+type Pagination struct {
+ CurrentPage int
+ TotalPages int
+ HasPrev bool
+ HasNext bool
+ PrevPage int
+ NextPage int
+ PerPage int
+}
+
+package main
+
func getOrCreateCategoryAndTag(category, value string) (int, int, error) {
var catID int
err := db.QueryRow("SELECT id FROM categories WHERE name=?", category).Scan(&catID)
@@ -119,6 +132,26 @@ func getTaggedFiles() ([]File, error) {
`)
}
+func getTaggedFilesPaginated(page, perPage int) ([]File, int, error) {
+ // Get total count
+ var total int
+ err := db.QueryRow(`SELECT COUNT(DISTINCT f.id) FROM files f JOIN file_tags ft ON ft.file_id = f.id`).Scan(&total)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ offset := (page - 1) * perPage
+ files, err := queryFilesWithTags(`
+ SELECT DISTINCT f.id, f.filename, f.path, COALESCE(f.description, '') as description
+ FROM files f
+ JOIN file_tags ft ON ft.file_id = f.id
+ ORDER BY f.id DESC
+ LIMIT ? OFFSET ?
+ `, perPage, offset)
+
+ return files, total, err
+}
+
func getUntaggedFiles() ([]File, error) {
return queryFilesWithTags(`
SELECT f.id, f.filename, f.path, COALESCE(f.description, '') as description
@@ -129,11 +162,55 @@ func getUntaggedFiles() ([]File, error) {
`)
}
+func getUntaggedFilesPaginated(page, perPage int) ([]File, int, error) {
+ // Get total count
+ var total int
+ 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)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ offset := (page - 1) * perPage
+ files, err := queryFilesWithTags(`
+ SELECT f.id, f.filename, f.path, COALESCE(f.description, '') as description
+ FROM files f
+ LEFT JOIN file_tags ft ON ft.file_id = f.id
+ WHERE ft.file_id IS NULL
+ ORDER BY f.id DESC
+ LIMIT ? OFFSET ?
+ `, perPage, offset)
+
+ return files, total, err
+}
+
func buildPageData(title string, data interface{}) PageData {
tagMap, _ := getTagData()
return PageData{Title: title, Data: data, Tags: tagMap}
}
+func buildPageDataWithPagination(title string, data interface{}, page, total, perPage int) PageData {
+ pd := buildPageData(title, data)
+ pd.Pagination = calculatePagination(page, total, perPage)
+ return pd
+}
+
+func calculatePagination(page, total, perPage int) *Pagination {
+ totalPages := (total + perPage - 1) / perPage
+ if totalPages < 1 {
+ totalPages = 1
+ }
+
+ return &Pagination{
+ CurrentPage: page,
+ TotalPages: totalPages,
+ HasPrev: page > 1,
+ HasNext: page < totalPages,
+ PrevPage: page - 1,
+ NextPage: page + 1,
+ PerPage: perPage,
+ }
+}
+
func buildPageDataWithIP(title string, data interface{}) PageData {
pageData := buildPageData(title, data)
ip, _ := getLocalIP()
@@ -320,39 +397,57 @@ func searchHandler(w http.ResponseWriter, r *http.Request) {
}
func processUpload(src io.Reader, filename string) (int64, string, error) {
- finalFilename, finalPath, err := checkFileConflictStrict(filename)
- if err != nil {
- return 0, "", err
- }
-
- tempPath := finalPath + ".tmp"
- tempFile, err := os.Create(tempPath)
- if err != nil {
- return 0, "", fmt.Errorf("failed to create temp file: %v", err)
- }
-
- _, err = io.Copy(tempFile, src)
- tempFile.Close()
- if err != nil {
- os.Remove(tempPath)
- return 0, "", fmt.Errorf("failed to copy file data: %v", err)
- }
-
- processedPath, warningMsg, err := processVideoFile(tempPath, finalPath)
- if err != nil {
- os.Remove(tempPath)
- return 0, "", err
- }
-
- id, err := saveFileToDatabase(finalFilename, processedPath)
- if err != nil {
- os.Remove(processedPath)
- return 0, "", err
- }
-
- return id, warningMsg, nil
+ finalFilename, finalPath, err := checkFileConflictStrict(filename)
+ if err != nil {
+ return 0, "", err
+ }
+
+ tempPath := finalPath + ".tmp"
+ tempFile, err := os.Create(tempPath)
+ if err != nil {
+ return 0, "", fmt.Errorf("failed to create temp file: %v", err)
+ }
+
+ _, err = io.Copy(tempFile, src)
+ tempFile.Close()
+ if err != nil {
+ os.Remove(tempPath)
+ return 0, "", fmt.Errorf("failed to copy file data: %v", err)
+ }
+
+ ext := strings.ToLower(filepath.Ext(filename))
+ videoExts := map[string]bool{
+ ".mp4": true, ".mov": true, ".avi": true,
+ ".mkv": true, ".webm": true, ".m4v": true,
+ }
+
+ var processedPath string
+ var warningMsg string
+
+ if videoExts[ext] {
+ processedPath, warningMsg, err = processVideoFile(tempPath, finalPath)
+ if err != nil {
+ os.Remove(tempPath)
+ return 0, "", err
+ }
+ } else {
+ // Non-video → just rename temp file to final
+ if err := os.Rename(tempPath, finalPath); err != nil {
+ return 0, "", fmt.Errorf("failed to move file: %v", err)
+ }
+ processedPath = finalPath
+ }
+
+ id, err := saveFileToDatabase(finalFilename, processedPath)
+ if err != nil {
+ os.Remove(processedPath)
+ return 0, "", err
+ }
+
+ return id, warningMsg, nil
}
+
func uploadFromURLHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Redirect(w, r, "/upload", http.StatusSeeOther)
@@ -405,20 +500,60 @@ func uploadFromURLHandler(w http.ResponseWriter, r *http.Request) {
}
func listFilesHandler(w http.ResponseWriter, r *http.Request) {
- tagged, _ := getTaggedFiles()
- untagged, _ := getUntaggedFiles()
+ // Get page number from query params
+ pageStr := r.URL.Query().Get("page")
+ page := 1
+ if pageStr != "" {
+ if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
+ page = p
+ }
+ }
+
+ // Get per page from config
+ perPage := 50
+ if config.ItemsPerPage != "" {
+ if pp, err := strconv.Atoi(config.ItemsPerPage); err == nil && pp > 0 {
+ perPage = pp
+ }
+ }
+
+ tagged, taggedTotal, _ := getTaggedFilesPaginated(page, perPage)
+ untagged, untaggedTotal, _ := getUntaggedFilesPaginated(page, perPage)
- pageData := buildPageData("Home", struct {
+ // Use the larger total for pagination
+ total := taggedTotal
+ if untaggedTotal > total {
+ total = untaggedTotal
+ }
+
+ pageData := buildPageDataWithPagination("Home", struct {
Tagged []File
Untagged []File
- }{tagged, untagged})
+ }{tagged, untagged}, page, total, perPage)
renderTemplate(w, "list.html", pageData)
}
func untaggedFilesHandler(w http.ResponseWriter, r *http.Request) {
- files, _ := getUntaggedFiles()
- pageData := buildPageData("Untagged Files", files)
+ // Get page number from query params
+ pageStr := r.URL.Query().Get("page")
+ page := 1
+ if pageStr != "" {
+ if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
+ page = p
+ }
+ }
+
+ // Get per page from config
+ perPage := 50
+ if config.ItemsPerPage != "" {
+ if pp, err := strconv.Atoi(config.ItemsPerPage); err == nil && pp > 0 {
+ perPage = pp
+ }
+ }
+
+ files, total, _ := getUntaggedFilesPaginated(page, perPage)
+ pageData := buildPageDataWithPagination("Untagged Files", files, page, total, perPage)
renderTemplate(w, "untagged.html", pageData)
}
diff --git a/static/style.css b/static/style.css
@@ -57,3 +57,11 @@ pre#text-viewer{font-family:serif;font-size:25px;line-height:1.8}
#text-viewer-container:fullscreen{margin:0;max-width:75%;margin:auto;height:100vh;padding:1em;background:#000;display:flex;flex-direction:column}
#text-viewer-container:fullscreen #text-viewer{flex:1;max-height:none!important;margin:0;height:100%}
#text-viewer-container:fullscreen>div{flex-shrink:0}
+
+/* pagination */
+.pagination .disabled,.pagination a{border-radius:4px;padding:.5rem 1rem}
+.pagination{display:flex;justify-content:center;align-items:center;gap:1rem;margin:2rem 0;padding:1rem}
+.pagination a{background:#007bff;color:#fff;text-decoration:none;transition:background .2s}
+.pagination a:hover{background:#0056b3}
+.pagination .disabled{background:#ccc;color:#666;cursor:not-allowed}
+.pagination .page-info{font-weight:700;padding:.5rem 1rem}
diff --git a/templates/list.html b/templates/list.html
@@ -3,12 +3,9 @@
{{if .Data.Tagged}}
<h2>Tagged Files</h2>
-
<div class="gallery">
{{range .Data.Tagged}}
-
{{template "_gallery" .}}
-
{{else}}
<p>No tagged files yet.</p>
{{end}}
@@ -26,4 +23,24 @@
</div>
{{end}}
+{{if .Pagination}}
+{{if gt .Pagination.TotalPages 1}}
+<div class="pagination">
+ {{if .Pagination.HasPrev}}
+ <a href="?page={{.Pagination.PrevPage}}">« Previous</a>
+ {{else}}
+ <span class="disabled">« Previous</span>
+ {{end}}
+
+ <span class="page-info">Page {{.Pagination.CurrentPage}} of {{.Pagination.TotalPages}}</span>
+
+ {{if .Pagination.HasNext}}
+ <a href="?page={{.Pagination.NextPage}}">Next »</a>
+ {{else}}
+ <span class="disabled">Next »</span>
+ {{end}}
+</div>
+{{end}}
+{{end}}
+
{{template "_footer"}}
diff --git a/templates/untagged.html b/templates/untagged.html
@@ -5,7 +5,28 @@
{{range .Data}}
{{template "_gallery" .}}
{{else}}
-<p>No untagged files.</p>
+ <p>No untagged files.</p>
{{end}}
</div>
+
+{{if .Pagination}}
+{{if gt .Pagination.TotalPages 1}}
+<div class="pagination">
+ {{if .Pagination.HasPrev}}
+ <a href="?page={{.Pagination.PrevPage}}">« Previous</a>
+ {{else}}
+ <span class="disabled">« Previous</span>
+ {{end}}
+
+ <span class="page-info">Page {{.Pagination.CurrentPage}} of {{.Pagination.TotalPages}}</span>
+
+ {{if .Pagination.HasNext}}
+ <a href="?page={{.Pagination.NextPage}}">Next »</a>
+ {{else}}
+ <span class="disabled">Next »</span>
+ {{end}}
+</div>
+{{end}}
+{{end}}
+
{{template "_footer"}}