+3
-2
cmd/handlers.go
+3
-2
cmd/handlers.go
···
14
14
"github.com/teal-fm/piper/session"
15
15
)
16
16
17
-
func home(database *db.DB) http.HandlerFunc {
17
+
func home(database *db.DB, pages *Pages) http.HandlerFunc {
18
18
return func(w http.ResponseWriter, r *http.Request) {
19
19
20
20
w.Header().Set("Content-Type", "text/html")
···
138
138
</body>
139
139
</html>
140
140
`
141
+
pages.execute("home", w, nil)
141
142
142
-
w.Write([]byte(html))
143
+
//w.Write([]byte(html))
143
144
}
144
145
}
145
146
+2
cmd/main.go
+2
cmd/main.go
···
31
31
mbService *musicbrainz.MusicBrainzService
32
32
atprotoService *atproto.ATprotoAuthService
33
33
playingNowService *playingnow.PlayingNowService
34
+
pages *Pages
34
35
}
35
36
36
37
// JSON API handlers
···
105
106
spotifyService: spotifyService,
106
107
atprotoService: atprotoService,
107
108
playingNowService: playingNowService,
109
+
pages: NewPages(true),
108
110
}
109
111
110
112
trackerInterval := time.Duration(viper.GetInt("tracker.interval")) * time.Second
+182
cmd/pages.go
+182
cmd/pages.go
···
1
+
package main
2
+
3
+
import (
4
+
"embed"
5
+
"html/template"
6
+
"io"
7
+
"io/fs"
8
+
"os"
9
+
"strings"
10
+
"sync"
11
+
"time"
12
+
)
13
+
14
+
//go:embed templates/*
15
+
var Files embed.FS
16
+
17
+
// inspired from tangled's implementation
18
+
//https://tangled.org/@tangled.org/core/blob/master/appview/pages/pages.go
19
+
20
+
type Pages struct {
21
+
cache *TmplCache[string, *template.Template]
22
+
dev bool
23
+
templateDir string // Path to templates on disk for dev mode
24
+
embedFS fs.FS
25
+
}
26
+
27
+
func NewPages(dev bool) *Pages {
28
+
pages := &Pages{
29
+
cache: NewTmplCache[string, *template.Template](),
30
+
dev: dev,
31
+
templateDir: "templates",
32
+
}
33
+
if pages.dev {
34
+
pages.embedFS = os.DirFS(pages.templateDir)
35
+
} else {
36
+
//pages.embedFS = Files
37
+
}
38
+
39
+
return pages
40
+
}
41
+
42
+
func (p *Pages) fragmentPaths() ([]string, error) {
43
+
var fragmentPaths []string
44
+
err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error {
45
+
if err != nil {
46
+
return err
47
+
}
48
+
if d.IsDir() {
49
+
return nil
50
+
}
51
+
if !strings.HasSuffix(path, ".gohtml") {
52
+
return nil
53
+
}
54
+
if !strings.Contains(path, "fragments/") {
55
+
return nil
56
+
}
57
+
fragmentPaths = append(fragmentPaths, path)
58
+
return nil
59
+
})
60
+
if err != nil {
61
+
return nil, err
62
+
}
63
+
64
+
return fragmentPaths, nil
65
+
}
66
+
67
+
func (p *Pages) pathToName(s string) string {
68
+
return strings.TrimSuffix(strings.TrimPrefix(s, "templates/"), ".html")
69
+
}
70
+
71
+
// reverse of pathToName
72
+
func (p *Pages) nameToPath(s string) string {
73
+
return "templates/" + s + ".html"
74
+
}
75
+
76
+
// parse without memoization
77
+
func (p *Pages) rawParse(stack ...string) (*template.Template, error) {
78
+
paths, err := p.fragmentPaths()
79
+
if err != nil {
80
+
return nil, err
81
+
}
82
+
for _, s := range stack {
83
+
paths = append(paths, p.nameToPath(s))
84
+
}
85
+
86
+
funcs := p.funcMap()
87
+
top := stack[len(stack)-1]
88
+
parsed, err := template.New(top).
89
+
Funcs(funcs).
90
+
ParseFS(p.embedFS, paths...)
91
+
if err != nil {
92
+
return nil, err
93
+
}
94
+
95
+
return parsed, nil
96
+
}
97
+
98
+
func (p *Pages) parse(stack ...string) (*template.Template, error) {
99
+
key := strings.Join(stack, "|")
100
+
101
+
// never cache in dev mode
102
+
if cached, exists := p.cache.Get(key); !p.dev && exists {
103
+
return cached, nil
104
+
}
105
+
106
+
result, err := p.rawParse(stack...)
107
+
if err != nil {
108
+
return nil, err
109
+
}
110
+
111
+
p.cache.Set(key, result)
112
+
return result, nil
113
+
}
114
+
115
+
func (p *Pages) funcMap() template.FuncMap {
116
+
return template.FuncMap{
117
+
"formatTime": func(t time.Time) string {
118
+
if t.IsZero() {
119
+
return "N/A"
120
+
}
121
+
return t.Format("Jan 02, 2006 15:04")
122
+
},
123
+
}
124
+
}
125
+
126
+
func (p *Pages) parseBase(top string) (*template.Template, error) {
127
+
stack := []string{
128
+
"layouts/base",
129
+
top,
130
+
}
131
+
return p.parse(stack...)
132
+
}
133
+
134
+
func (p *Pages) executePlain(name string, w io.Writer, params any) error {
135
+
tpl, err := p.parse(name)
136
+
if err != nil {
137
+
return err
138
+
}
139
+
140
+
return tpl.Execute(w, params)
141
+
}
142
+
143
+
func (p *Pages) execute(name string, w io.Writer, params any) error {
144
+
tpl, err := p.parseBase(name)
145
+
if err != nil {
146
+
return err
147
+
}
148
+
149
+
return tpl.ExecuteTemplate(w, "layouts/base", params)
150
+
}
151
+
152
+
/// Cache for pages
153
+
154
+
type TmplCache[K comparable, V any] struct {
155
+
data map[K]V
156
+
mutex sync.RWMutex
157
+
}
158
+
159
+
func NewTmplCache[K comparable, V any]() *TmplCache[K, V] {
160
+
return &TmplCache[K, V]{
161
+
data: make(map[K]V),
162
+
}
163
+
}
164
+
165
+
func (c *TmplCache[K, V]) Get(key K) (V, bool) {
166
+
c.mutex.RLock()
167
+
defer c.mutex.RUnlock()
168
+
val, exists := c.data[key]
169
+
return val, exists
170
+
}
171
+
172
+
func (c *TmplCache[K, V]) Set(key K, value V) {
173
+
c.mutex.Lock()
174
+
defer c.mutex.Unlock()
175
+
c.data[key] = value
176
+
}
177
+
178
+
func (c *TmplCache[K, V]) Size() int {
179
+
c.mutex.RLock()
180
+
defer c.mutex.RUnlock()
181
+
return len(c.data)
182
+
}
+1
-1
cmd/routes.go
+1
-1
cmd/routes.go
···
11
11
func (app *application) routes() http.Handler {
12
12
mux := http.NewServeMux()
13
13
14
-
mux.HandleFunc("/", session.WithPossibleAuth(home(app.database), app.sessionManager))
14
+
mux.HandleFunc("/", session.WithPossibleAuth(home(app.database, app.pages), app.sessionManager))
15
15
16
16
// OAuth Routes
17
17
mux.HandleFunc("/login/spotify", app.oauthManager.HandleLogin("spotify"))
+46
templates/layouts/base.gohtml
+46
templates/layouts/base.gohtml
···
1
+
{{ define "layouts/base" }}
2
+
3
+
<html lang="en">
4
+
<head>
5
+
<title>Piper - Spotify & Last.fm Tracker</title>
6
+
<style>
7
+
body {
8
+
font-family: Arial, sans-serif;
9
+
max-width: 800px;
10
+
margin: 0 auto;
11
+
padding: 20px;
12
+
line-height: 1.6;
13
+
}
14
+
h1 {
15
+
color: #1DB954; /* Spotify green */
16
+
}
17
+
.nav {
18
+
display: flex;
19
+
flex-wrap: wrap; /* Allow wrapping on smaller screens */
20
+
margin-bottom: 20px;
21
+
}
22
+
.nav a {
23
+
margin-right: 15px;
24
+
margin-bottom: 5px; /* Add spacing below links */
25
+
text-decoration: none;
26
+
color: #1DB954;
27
+
font-weight: bold;
28
+
}
29
+
.card {
30
+
border: 1px solid #ddd;
31
+
border-radius: 8px;
32
+
padding: 20px;
33
+
margin-bottom: 20px;
34
+
}
35
+
.service-status {
36
+
font-style: italic;
37
+
color: #555;
38
+
}
39
+
</style>
40
+
</head>
41
+
<body>
42
+
{{ block "content" . }}{{ end }}
43
+
44
+
</body>
45
+
</html>
46
+
{{ end }}