View switching feature
This commit is contained in:
@@ -4,10 +4,12 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
@@ -31,6 +33,13 @@ func initMarkdown(root string) {
|
||||
type entry struct {
|
||||
Icon template.HTML
|
||||
Name, URL, Meta string
|
||||
// ThumbURL is set for thumbnailable files; the thumbnail view renders an
|
||||
// <img> when it is non-empty and falls back to Icon otherwise.
|
||||
ThumbURL string
|
||||
// modTime/size carry the raw sort keys; the template only reads the
|
||||
// formatted Meta string.
|
||||
modTime time.Time
|
||||
size int64
|
||||
}
|
||||
|
||||
type pageData struct {
|
||||
@@ -45,15 +54,68 @@ type pageData struct {
|
||||
RawContent string
|
||||
Content template.HTML
|
||||
Entries []entry
|
||||
View string // listing view style: "list" or "thumbnail"
|
||||
Sort string // listing sort key: "name" / "modified" / "size"
|
||||
Order string // listing sort order: "asc" / "desc"
|
||||
SpecialContent template.HTML
|
||||
SidebarWidget template.HTML
|
||||
SuppressTOC bool
|
||||
RenderMS int64
|
||||
}
|
||||
|
||||
// pageSettings holds the parsed contents of a .page-settings file.
|
||||
// Allowed values for the listing view settings. Unknown values in the file or
|
||||
// a POST body fall back to the first (default) value of each set.
|
||||
const (
|
||||
viewList = "list"
|
||||
viewThumbnail = "thumbnail"
|
||||
|
||||
sortName = "name"
|
||||
sortModified = "modified"
|
||||
sortSize = "size"
|
||||
|
||||
orderAsc = "asc"
|
||||
orderDesc = "desc"
|
||||
)
|
||||
|
||||
// pageSettings holds the parsed contents of a .page-settings file. View, Sort,
|
||||
// and Order are always valid once parsed (defaults applied on read).
|
||||
type pageSettings struct {
|
||||
Type string
|
||||
Type string
|
||||
View string
|
||||
Sort string
|
||||
Order string
|
||||
}
|
||||
|
||||
// viewSettings returns the listing view/sort/order, applying defaults when the
|
||||
// receiver is nil (no .page-settings file).
|
||||
func (s *pageSettings) viewSettings() (view, sortKey, order string) {
|
||||
if s == nil {
|
||||
return viewList, sortName, orderAsc
|
||||
}
|
||||
return s.View, s.Sort, s.Order
|
||||
}
|
||||
|
||||
func validateView(v string) string {
|
||||
if v == viewThumbnail {
|
||||
return viewThumbnail
|
||||
}
|
||||
return viewList
|
||||
}
|
||||
|
||||
func validateSort(v string) string {
|
||||
switch v {
|
||||
case sortModified, sortSize:
|
||||
return v
|
||||
default:
|
||||
return sortName
|
||||
}
|
||||
}
|
||||
|
||||
func validateOrder(v string) string {
|
||||
if v == orderDesc {
|
||||
return orderDesc
|
||||
}
|
||||
return orderAsc
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -117,7 +179,7 @@ func parentURL(urlPath string) string {
|
||||
return parent + "/"
|
||||
}
|
||||
|
||||
func listEntries(fsPath, urlPath string) []entry {
|
||||
func listEntries(fsPath, urlPath, sortKey, order string) []entry {
|
||||
entries, err := os.ReadDir(fsPath)
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -136,30 +198,36 @@ func listEntries(fsPath, urlPath string) []entry {
|
||||
entryURL := path.Join(urlPath, name)
|
||||
if e.IsDir() {
|
||||
folders = append(folders, entry{
|
||||
Icon: iconFolder,
|
||||
Name: name,
|
||||
URL: entryURL + "/",
|
||||
Meta: info.ModTime().Format("2006-01-02"),
|
||||
Icon: iconFolder,
|
||||
Name: name,
|
||||
URL: entryURL + "/",
|
||||
Meta: info.ModTime().Format("2006-01-02"),
|
||||
modTime: info.ModTime(),
|
||||
})
|
||||
} else {
|
||||
if name == "index.md" {
|
||||
continue // rendered above, don't list it
|
||||
}
|
||||
files = append(files, entry{
|
||||
Icon: fileIcon(name),
|
||||
Name: name,
|
||||
URL: entryURL,
|
||||
Meta: formatSize(info.Size()) + " · " + info.ModTime().Format("2006-01-02"),
|
||||
})
|
||||
f := entry{
|
||||
Icon: fileIcon(name),
|
||||
Name: name,
|
||||
URL: entryURL,
|
||||
Meta: formatSize(info.Size()) + " · " + info.ModTime().Format("2006-01-02"),
|
||||
modTime: info.ModTime(),
|
||||
size: info.Size(),
|
||||
}
|
||||
if hasThumbnail(name) {
|
||||
f.ThumbURL = thumbURL(path.Join(urlPath, url.PathEscape(name)), 300)
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(folders, func(i, j int) bool {
|
||||
return strings.ToLower(folders[i].Name) < strings.ToLower(folders[j].Name)
|
||||
})
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
return strings.ToLower(files[i].Name) < strings.ToLower(files[j].Name)
|
||||
})
|
||||
// Folders always sort by name regardless of the chosen key (they have no
|
||||
// meaningful byte size); files honor the chosen key. The chosen order
|
||||
// applies to both groups.
|
||||
sortEntries(folders, sortName, order)
|
||||
sortEntries(files, sortKey, order)
|
||||
|
||||
// `..` row mirrors the header Up button so the listing itself is
|
||||
// navigable without reaching for the header on mobile. Prepended after
|
||||
@@ -177,6 +245,42 @@ func listEntries(fsPath, urlPath string) []entry {
|
||||
return out
|
||||
}
|
||||
|
||||
// sortEntries sorts a single group (folders or files) in place by the given
|
||||
// key, breaking ties on case-insensitive name, then reverses for descending
|
||||
// order. The stable sort keeps the name tiebreak meaningful.
|
||||
func sortEntries(group []entry, sortKey, order string) {
|
||||
sort.SliceStable(group, func(i, j int) bool {
|
||||
a, b := group[i], group[j]
|
||||
cmp := 0
|
||||
switch sortKey {
|
||||
case sortModified:
|
||||
if a.modTime.Before(b.modTime) {
|
||||
cmp = -1
|
||||
} else if a.modTime.After(b.modTime) {
|
||||
cmp = 1
|
||||
}
|
||||
case sortSize:
|
||||
if a.size < b.size {
|
||||
cmp = -1
|
||||
} else if a.size > b.size {
|
||||
cmp = 1
|
||||
}
|
||||
}
|
||||
if cmp == 0 {
|
||||
an, bn := strings.ToLower(a.Name), strings.ToLower(b.Name)
|
||||
if an < bn {
|
||||
cmp = -1
|
||||
} else if an > bn {
|
||||
cmp = 1
|
||||
}
|
||||
}
|
||||
if order == orderDesc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
}
|
||||
|
||||
func readIcon(name string) template.HTML {
|
||||
b, _ := assets.ReadFile("assets/icons/" + name + ".svg")
|
||||
return template.HTML(strings.TrimSpace(string(b)))
|
||||
|
||||
Reference in New Issue
Block a user