Make the timeline the default view, paginated and configurable
The day-grouped timeline is now the site root (/, /page/N/); the flat square grid moves to /grid/ (/grid/page/N/). Both are linked from the profile header, separated by a divider. Timeline pages pack whole day-groups until they reach the page size, never splitting a day across a page boundary, so appended pages never repeat a date heading — the infinite-scroll handler relies on this. Page size is configurable via -page-size (default 60), threaded through the Generator and exposed as services.photogallery.pageSize in the NixOS module.
This commit is contained in:
+108
-38
@@ -22,9 +22,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
thumbSize = 600
|
||||
mediumMaxSize = 1600
|
||||
pageSize = 60
|
||||
thumbSize = 600
|
||||
mediumMaxSize = 1600
|
||||
defaultPageSize = 60
|
||||
)
|
||||
|
||||
// PhotoMeta is the structure of a .toml sidecar file.
|
||||
@@ -62,12 +62,16 @@ type Generator struct {
|
||||
fontsURL string
|
||||
serifFamily string
|
||||
mlFamily string
|
||||
pageSize int
|
||||
mu sync.Mutex
|
||||
tmpl *template.Template
|
||||
}
|
||||
|
||||
func NewGenerator(contentDir, outputDir, baseURL, author, handle, bio, bioAlt, fontsURL, serifFamily, mlFamily string) *Generator {
|
||||
func NewGenerator(contentDir, outputDir, baseURL, author, handle, bio, bioAlt, fontsURL, serifFamily, mlFamily string, pageSize int) *Generator {
|
||||
tmpl := template.Must(template.ParseFS(assets, "templates/*.html"))
|
||||
if pageSize < 1 {
|
||||
pageSize = defaultPageSize
|
||||
}
|
||||
return &Generator{
|
||||
contentDir: contentDir,
|
||||
outputDir: outputDir,
|
||||
@@ -79,6 +83,7 @@ func NewGenerator(contentDir, outputDir, baseURL, author, handle, bio, bioAlt, f
|
||||
fontsURL: fontsURL,
|
||||
serifFamily: serifFamily,
|
||||
mlFamily: mlFamily,
|
||||
pageSize: pageSize,
|
||||
tmpl: tmpl,
|
||||
}
|
||||
}
|
||||
@@ -101,7 +106,7 @@ func (g *Generator) Build() error {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, d := range []string{"images", "thumbs", "medium", "photo", "static"} {
|
||||
for _, d := range []string{"images", "thumbs", "medium", "photo", "grid", "static"} {
|
||||
if err := os.MkdirAll(filepath.Join(g.outputDir, d), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -113,7 +118,10 @@ func (g *Generator) Build() error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := g.renderIndexPages(photos); err != nil {
|
||||
if err := g.renderTimelinePages(photos); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := g.renderGridPages(photos); err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range photos {
|
||||
@@ -128,9 +136,6 @@ func (g *Generator) Build() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := g.renderTimeline(photos); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := g.renderFeed(photos); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -310,26 +315,29 @@ func generateMedium(src, dst string, maxDim int) error {
|
||||
return jpeg.Encode(out, resized, &jpeg.Options{Quality: 88})
|
||||
}
|
||||
|
||||
// renderIndexPages writes public/index.html and public/page/N/index.html
|
||||
// files, chunking photos into pageSize slices for progressive pagination.
|
||||
func (g *Generator) renderIndexPages(photos []Photo) error {
|
||||
// renderGridPages writes the paginated square-grid view under public/grid/:
|
||||
// grid/index.html and grid/page/N/index.html, chunking photos into pageSize
|
||||
// slices for progressive pagination. The grid is the secondary view; the
|
||||
// timeline is the site root.
|
||||
func (g *Generator) renderGridPages(photos []Photo) error {
|
||||
total := len(photos)
|
||||
totalPages := (total + pageSize - 1) / pageSize
|
||||
totalPages := (total + g.pageSize - 1) / g.pageSize
|
||||
if totalPages == 0 {
|
||||
totalPages = 1
|
||||
}
|
||||
gridDir := filepath.Join(g.outputDir, "grid")
|
||||
|
||||
for i := 0; i < totalPages; i++ {
|
||||
start := i * pageSize
|
||||
end := start + pageSize
|
||||
start := i * g.pageSize
|
||||
end := start + g.pageSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
slice := photos[start:end]
|
||||
|
||||
dst := filepath.Join(g.outputDir, "index.html")
|
||||
dst := filepath.Join(gridDir, "index.html")
|
||||
if i > 0 {
|
||||
dir := filepath.Join(g.outputDir, "page", strconv.Itoa(i+1))
|
||||
dir := filepath.Join(gridDir, "page", strconv.Itoa(i+1))
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -342,12 +350,12 @@ func (g *Generator) renderIndexPages(photos []Photo) error {
|
||||
case 0:
|
||||
// no prev
|
||||
case 1:
|
||||
prev = "/"
|
||||
prev = "/grid/"
|
||||
default:
|
||||
prev = "/page/" + strconv.Itoa(i) + "/"
|
||||
prev = "/grid/page/" + strconv.Itoa(i) + "/"
|
||||
}
|
||||
if i+1 < totalPages {
|
||||
next = "/page/" + strconv.Itoa(i+2) + "/"
|
||||
next = "/grid/page/" + strconv.Itoa(i+2) + "/"
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
@@ -368,7 +376,7 @@ func (g *Generator) renderIndexPages(photos []Photo) error {
|
||||
"SerifFamily": g.serifFamily,
|
||||
"MlFamily": g.mlFamily,
|
||||
}
|
||||
if err := g.render("index.html", dst, data); err != nil {
|
||||
if err := g.render("grid.html", dst, data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -418,24 +426,86 @@ func groupByDay(photos []Photo) []DayGroup {
|
||||
return groups
|
||||
}
|
||||
|
||||
// renderTimeline writes public/timeline/index.html, photos grouped by day.
|
||||
func (g *Generator) renderTimeline(photos []Photo) error {
|
||||
dir := filepath.Join(g.outputDir, "timeline")
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
// chunkDayGroups packs day groups into pages, filling each page until it holds
|
||||
// at least target photos, then starting a new one. A single day is never split
|
||||
// across a page boundary, so page sizes vary slightly around target. Always
|
||||
// returns at least one page (empty when there are no photos) so the site root
|
||||
// renders.
|
||||
func chunkDayGroups(groups []DayGroup, target int) [][]DayGroup {
|
||||
var pages [][]DayGroup
|
||||
var cur []DayGroup
|
||||
count := 0
|
||||
for _, grp := range groups {
|
||||
cur = append(cur, grp)
|
||||
count += grp.Count
|
||||
if count >= target {
|
||||
pages = append(pages, cur)
|
||||
cur, count = nil, 0
|
||||
}
|
||||
}
|
||||
return g.render("timeline.html", filepath.Join(dir, "index.html"), map[string]any{
|
||||
"Groups": groupByDay(photos),
|
||||
"Total": len(photos),
|
||||
"BaseURL": g.baseURL,
|
||||
"Year": time.Now().Year(),
|
||||
"Author": g.author,
|
||||
"Handle": g.handle,
|
||||
"Initial": g.initial(),
|
||||
"FontsURL": g.fontsURL,
|
||||
"SerifFamily": g.serifFamily,
|
||||
"MlFamily": g.mlFamily,
|
||||
})
|
||||
if len(cur) > 0 {
|
||||
pages = append(pages, cur)
|
||||
}
|
||||
if len(pages) == 0 {
|
||||
pages = [][]DayGroup{nil}
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
// renderTimelinePages writes the day-grouped timeline as the site root:
|
||||
// index.html and page/N/index.html. Photos are grouped by day, then packed
|
||||
// into pages of roughly pageSize without splitting a day across pages.
|
||||
func (g *Generator) renderTimelinePages(photos []Photo) error {
|
||||
pages := chunkDayGroups(groupByDay(photos), g.pageSize)
|
||||
totalPages := len(pages)
|
||||
|
||||
for i, groups := range pages {
|
||||
dst := filepath.Join(g.outputDir, "index.html")
|
||||
if i > 0 {
|
||||
dir := filepath.Join(g.outputDir, "page", strconv.Itoa(i+1))
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
dst = filepath.Join(dir, "index.html")
|
||||
}
|
||||
|
||||
prev := ""
|
||||
next := ""
|
||||
switch i {
|
||||
case 0:
|
||||
// no prev
|
||||
case 1:
|
||||
prev = "/"
|
||||
default:
|
||||
prev = "/page/" + strconv.Itoa(i) + "/"
|
||||
}
|
||||
if i+1 < totalPages {
|
||||
next = "/page/" + strconv.Itoa(i+2) + "/"
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"Groups": groups,
|
||||
"Total": len(photos),
|
||||
"Page": i + 1,
|
||||
"TotalPages": totalPages,
|
||||
"PrevURL": prev,
|
||||
"NextURL": next,
|
||||
"BaseURL": g.baseURL,
|
||||
"Year": time.Now().Year(),
|
||||
"Author": g.author,
|
||||
"Handle": g.handle,
|
||||
"Bio": g.bio,
|
||||
"BioAlt": g.bioAlt,
|
||||
"Initial": g.initial(),
|
||||
"FontsURL": g.fontsURL,
|
||||
"SerifFamily": g.serifFamily,
|
||||
"MlFamily": g.mlFamily,
|
||||
}
|
||||
if err := g.render("timeline.html", dst, data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Generator) render(tmpl, dst string, data any) error {
|
||||
|
||||
Reference in New Issue
Block a user