179ec8e108e39a3f6706caf0b22573625222c291
- Author
- TheEdgeOfRage <git@theedgeofrage.com>
- Committer
- TheEdgeOfRage <git@theedgeofrage.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/.air.toml b/.air.toml
2index 97c50c5d88668fe6ee05d242de2076d62233f38b..9f8c40e45aa3722347d1504ffe7101de03c32764 100644
3--- a/.air.toml
4+++ b/.air.toml
5@@ -5,6 +5,7 @@ tmp_dir = "/tmp"
6 args_bin = [
7 "--db-uri=postgres://ytrssil:ytrssil@localhost:5432/ytrssil?sslmode=disable",
8 "--auth-token=foo",
9+ "--dev",
10 ]
11 entrypoint = "./dist/ytrssil"
12 cmd = "make build"
13diff --git a/cmd/main.go b/cmd/main.go
14index c45e085be7f2abdb79de5275ac85108f81db4675..72c0896cd168778afbcc4c4d250d5bfb9ae9673a 100644
15--- a/cmd/main.go
16+++ b/cmd/main.go
17@@ -17,7 +17,6 @@ import (
18 "github.com/TheEdgeOfRage/ytrssil-api/db"
19 "github.com/TheEdgeOfRage/ytrssil-api/feedparser"
20 "github.com/TheEdgeOfRage/ytrssil-api/handler"
21- "github.com/TheEdgeOfRage/ytrssil-api/httpserver/auth"
22 "github.com/TheEdgeOfRage/ytrssil-api/httpserver/ytrssil"
23 )
24
25@@ -61,13 +60,12 @@ func main() {
26 }
27 parser := feedparser.NewParser(logger)
28 handler := handler.New(logger, cfg, db, parser)
29- gin.SetMode(gin.ReleaseMode)
30- router, err := ytrssil.SetupGinRouter(
31- logger,
32- handler,
33- auth.APIAuthMiddleware(cfg.AuthToken),
34- auth.PageAuthMiddleware(cfg.AuthToken),
35- )
36+ if cfg.Dev {
37+ gin.SetMode(gin.DebugMode)
38+ } else {
39+ gin.SetMode(gin.ReleaseMode)
40+ }
41+ router, err := ytrssil.SetupGinRouter(logger, cfg, handler)
42 if err != nil {
43 logger.Error(
44 "Failed to set up gin server",
45diff --git a/config/config.go b/config/config.go
46index 25c2dc4bab48f28e252921c2c8f39e652c1119e6..762ebf76bae42339e8767eabc123d620127be740 100644
47--- a/config/config.go
48+++ b/config/config.go
49@@ -9,6 +9,7 @@ import (
50 )
51
52 type Config struct {
53+ Dev bool `long:"dev" env:"DEV"`
54 Port int `long:"port" env:"PORT" default:"8080"`
55 DBURI string `long:"db-uri" env:"DB_URI"`
56 AuthToken string `long:"auth-token" env:"AUTH_TOKEN"`
57diff --git a/httpserver/auth/auth.go b/httpserver/auth/auth.go
58index 47b21aa8bcf60a70c67bcab6a82f6327b67797aa..aae426d79d6a5e982b7bfd9326e070a5dc7d585e 100644
59--- a/httpserver/auth/auth.go
60+++ b/httpserver/auth/auth.go
61@@ -11,15 +11,14 @@ func PageAuthMiddleware(authToken string) gin.HandlerFunc {
62 return func(c *gin.Context) {
63 tokenCookie, err := c.Request.Cookie("token")
64 if err != nil {
65- c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing auth token cookie"})
66+ c.Redirect(http.StatusFound, "/auth")
67 return
68 }
69 if tokenCookie.Value != authToken {
70- c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid auth token"})
71+ c.Redirect(http.StatusFound, "/auth")
72 return
73 }
74
75- // handle request
76 c.Next()
77 }
78 }
79@@ -37,7 +36,6 @@ func APIAuthMiddleware(authToken string) gin.HandlerFunc {
80 return
81 }
82
83- // handle request
84 c.Next()
85 }
86 }
87diff --git a/httpserver/ytrssil/auth_pages.go b/httpserver/ytrssil/auth_pages.go
88new file mode 100644
89index 0000000000000000000000000000000000000000..e3d2a07070ba070bdc1abded1a703f98f90fa856
90--- /dev/null
91+++ b/httpserver/ytrssil/auth_pages.go
92@@ -0,0 +1,41 @@
93+package ytrssil
94+
95+import (
96+ "net/http"
97+
98+ "github.com/gin-gonic/gin"
99+
100+ "github.com/TheEdgeOfRage/ytrssil-api/pages"
101+)
102+
103+func (srv server) AuthPage(c *gin.Context) {
104+ r := pages.TemplRenderer{
105+ Ctx: c.Request.Context(),
106+ Component: pages.AuthPage(""),
107+ }
108+ c.Render(http.StatusOK, r)
109+}
110+
111+func (srv server) HandleAuth(c *gin.Context) {
112+ token := c.PostForm("token")
113+
114+ if token != srv.cfg.AuthToken {
115+ r := pages.TemplRenderer{
116+ Ctx: c.Request.Context(),
117+ Component: pages.AuthPage("Invalid token"),
118+ }
119+ c.Render(http.StatusBadRequest, r)
120+ return
121+ }
122+
123+ c.SetCookie(
124+ "token",
125+ token,
126+ 0, // maxAge (0 = session cookie)
127+ "/", // path
128+ "", // domain (empty = current domain)
129+ !srv.cfg.Dev, // secure (false = works on HTTP)
130+ true, // httpOnly (true = not accessible via JavaScript)
131+ )
132+ c.Redirect(http.StatusFound, "/")
133+}
134diff --git a/httpserver/ytrssil/server.go b/httpserver/ytrssil/server.go
135index 17882aadef829c1d30e09689136edd49171d8b67..37afb434831bf74872ada122d5082d2a24bbf8f9 100644
136--- a/httpserver/ytrssil/server.go
137+++ b/httpserver/ytrssil/server.go
138@@ -8,16 +8,19 @@ import (
139
140 "github.com/gin-gonic/gin"
141
142+ "github.com/TheEdgeOfRage/ytrssil-api/config"
143 "github.com/TheEdgeOfRage/ytrssil-api/handler"
144+ "github.com/TheEdgeOfRage/ytrssil-api/httpserver/auth"
145 )
146
147 type server struct {
148 log *slog.Logger
149+ cfg config.Config
150 handler handler.Handler
151 }
152
153-func NewServer(log *slog.Logger, handler handler.Handler) (*server, error) {
154- return &server{log: log, handler: handler}, nil
155+func NewServer(log *slog.Logger, cfg config.Config, handler handler.Handler) (*server, error) {
156+ return &server{log: log, cfg: cfg, handler: handler}, nil
157 }
158
159 func ginLogFormatter(param gin.LogFormatterParams) string {
160@@ -39,9 +42,8 @@ func (srv *server) Healthz(c *gin.Context) {
161 // SetupGinRouter sets up routes for all APIs on a Gin server (aka router)
162 func SetupGinRouter(
163 l *slog.Logger,
164+ cfg config.Config,
165 handler handler.Handler,
166- apiAuthMiddleware func(c *gin.Context),
167- pageAuthMiddleware func(c *gin.Context),
168 ) (*gin.Engine, error) {
169 engine := gin.New()
170 // Middlewares are executed top to bottom in a stack-like manner
171@@ -57,24 +59,26 @@ func SetupGinRouter(
172 c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"error": "HTTP method not allowed"})
173 })
174
175- srv, err := NewServer(l, handler)
176+ srv, err := NewServer(l, cfg, handler)
177 if err != nil {
178 return nil, err
179 }
180 engine.GET("/healthz", srv.Healthz)
181 engine.POST("/fetch", srv.FetchVideosJSON)
182
183+ engine.GET("/auth", srv.AuthPage)
184+ engine.POST("/auth", srv.HandleAuth)
185+
186 pages := engine.Group("")
187- pages.Use(pageAuthMiddleware)
188+ pages.Use(auth.PageAuthMiddleware(cfg.AuthToken))
189 {
190 pages.GET("/", srv.NewVideosPage)
191 pages.PATCH("/videos/:video_id/watch", srv.MarkVideoAsWatchedPage)
192 pages.PATCH("/videos/:video_id/progress", srv.SetVideoProgressPage)
193 }
194
195- // all APIs go in this routing group and require authentication
196 api := engine.Group("/api")
197- api.Use(apiAuthMiddleware)
198+ api.Use(auth.APIAuthMiddleware(cfg.AuthToken))
199 {
200 api.POST("channels/:channel_id/subscribe", srv.SubscribeToChannelJSON)
201 api.POST("channels/:channel_id/unsubscribe", srv.UnsubscribeFromChannelJSON)
202diff --git a/httpserver/ytrssil/videos.go b/httpserver/ytrssil/videos.go
203index 053fd6a0bfb3f4fc18c2643a4a4fd1504e702364..663abadd7e853c07f1aca97911382cb4a610766b 100644
204--- a/httpserver/ytrssil/videos.go
205+++ b/httpserver/ytrssil/videos.go
206@@ -4,8 +4,6 @@ import (
207 "net/http"
208
209 "github.com/gin-gonic/gin"
210-
211- "github.com/TheEdgeOfRage/ytrssil-api/models"
212 )
213
214 func (srv *server) GetNewVideosJSON(c *gin.Context) {
215@@ -15,9 +13,7 @@ func (srv *server) GetNewVideosJSON(c *gin.Context) {
216 return
217 }
218
219- c.JSON(http.StatusOK, models.VideosResponse{
220- Videos: videos,
221- })
222+ c.JSON(http.StatusOK, gin.H{"videos": videos})
223 }
224
225 func (srv *server) GetWatchedVideosJSON(c *gin.Context) {
226@@ -27,9 +23,7 @@ func (srv *server) GetWatchedVideosJSON(c *gin.Context) {
227 return
228 }
229
230- c.JSON(http.StatusOK, models.VideosResponse{
231- Videos: videos,
232- })
233+ c.JSON(http.StatusOK, gin.H{"videos": videos})
234 }
235
236 func (srv *server) FetchVideosJSON(c *gin.Context) {
237@@ -43,14 +37,7 @@ func (srv *server) FetchVideosJSON(c *gin.Context) {
238 }
239
240 func (srv *server) MarkVideoAsWatchedJSON(c *gin.Context) {
241- var req models.VideoURIRequest
242- err := c.ShouldBindUri(&req)
243- if err != nil {
244- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
245- return
246- }
247-
248- err = srv.handler.MarkVideoAsWatched(c.Request.Context(), req.VideoID)
249+ err := srv.handler.MarkVideoAsWatched(c.Request.Context(), c.Param("video_id"))
250 if err != nil {
251 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
252 return
253@@ -60,14 +47,7 @@ func (srv *server) MarkVideoAsWatchedJSON(c *gin.Context) {
254 }
255
256 func (srv *server) MarkVideoAsUnwatchedJSON(c *gin.Context) {
257- var req models.VideoURIRequest
258- err := c.ShouldBindUri(&req)
259- if err != nil {
260- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
261- return
262- }
263-
264- err = srv.handler.MarkVideoAsUnwatched(c.Request.Context(), req.VideoID)
265+ err := srv.handler.MarkVideoAsUnwatched(c.Request.Context(), c.Param("video_id"))
266 if err != nil {
267 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
268 return
269diff --git a/httpserver/ytrssil/videos_pages.go b/httpserver/ytrssil/videos_pages.go
270index 0351c41b6d41cf33a8b7a0b65a6b46d3355e14bf..7e21e7a1645cdf0795300514f403c7d525af92ec 100644
271--- a/httpserver/ytrssil/videos_pages.go
272+++ b/httpserver/ytrssil/videos_pages.go
273@@ -5,7 +5,6 @@ import (
274
275 "github.com/gin-gonic/gin"
276
277- "github.com/TheEdgeOfRage/ytrssil-api/models"
278 "github.com/TheEdgeOfRage/ytrssil-api/pages"
279 )
280
281@@ -28,17 +27,9 @@ func (srv server) MarkVideoAsWatchedPage(c *gin.Context) {
282 Ctx: c.Request.Context(),
283 }
284
285- var req models.VideoURIRequest
286- err := c.ShouldBindUri(&req)
287+ err := srv.handler.MarkVideoAsWatched(c.Request.Context(), c.Param("video_id"))
288 if err != nil {
289- r.Component = pages.ErrorPage(err)
290- c.Render(http.StatusBadRequest, r)
291- return
292- }
293-
294- err = srv.handler.MarkVideoAsWatched(c.Request.Context(), req.VideoID)
295- if err != nil {
296- r.Component = pages.ErrorPage(err)
297+ r.Component = pages.ErrorPage(err.Error())
298 c.Render(http.StatusInternalServerError, r)
299 return
300 }
301@@ -50,26 +41,16 @@ func (srv server) SetVideoProgressPage(c *gin.Context) {
302 r := pages.TemplRenderer{
303 Ctx: c.Request.Context(),
304 }
305- var req struct {
306- models.VideoURIRequest
307- models.SetVideoProgressRequest
308- }
309- err := c.ShouldBindUri(&req.VideoURIRequest)
310- if err != nil {
311- r.Component = pages.ErrorPage(err)
312- c.Render(http.StatusBadRequest, r)
313- return
314- }
315- err = c.ShouldBind(&req.SetVideoProgressRequest)
316- if err != nil {
317- r.Component = pages.ErrorPage(err)
318+ progress := c.PostForm("progress")
319+ if progress == "" {
320+ r.Component = pages.ErrorPage("missing progress")
321 c.Render(http.StatusBadRequest, r)
322 return
323 }
324
325- video, err := srv.handler.SetVideoProgress(c.Request.Context(), req.VideoID, req.Progress)
326+ video, err := srv.handler.SetVideoProgress(c.Request.Context(), c.Param("video_id"), progress)
327 if err != nil {
328- r.Component = pages.ErrorPage(err)
329+ r.Component = pages.ErrorPage(err.Error())
330 c.Render(http.StatusInternalServerError, r)
331 return
332 }
333diff --git a/models/http.go b/models/http.go
334deleted file mode 100644
335index f65eb00e5c22fa1b049e1dd4f4c416e2fd40e48b..0000000000000000000000000000000000000000
336--- a/models/http.go
337+++ /dev/null
338@@ -1,13 +0,0 @@
339-package models
340-
341-type VideosResponse struct {
342- Videos []Video `json:"videos"`
343-}
344-
345-type VideoURIRequest struct {
346- VideoID string `uri:"video_id" binding:"required"`
347-}
348-
349-type SetVideoProgressRequest struct {
350- Progress string `form:"progress" binding:"required"`
351-}
352diff --git a/pages/auth.templ b/pages/auth.templ
353new file mode 100644
354index 0000000000000000000000000000000000000000..7f9d27fb9ececa6cda0bc7824d5c7fdb51a41d65
355--- /dev/null
356+++ b/pages/auth.templ
357@@ -0,0 +1,35 @@
358+package pages
359+
360+templ AuthPage(errorMsg string) {
361+ @BaseLayout("ytrssil - Authentication") {
362+ <div class="row justify-content-center mt-5">
363+ <div class="col-md-6 col-lg-4">
364+ <div class="card">
365+ <div class="card-body">
366+ <h5 class="card-title mb-4">Authentication Required</h5>
367+ if errorMsg != "" {
368+ <div class="alert alert-danger" role="alert">
369+ { errorMsg }
370+ </div>
371+ }
372+ <form method="POST" action="/auth">
373+ <div class="mb-3">
374+ <label for="token" class="form-label">Access Token</label>
375+ <input
376+ type="password"
377+ class="form-control"
378+ id="token"
379+ name="token"
380+ placeholder="Enter your token"
381+ required
382+ autofocus
383+ />
384+ </div>
385+ <button type="submit" class="btn btn-primary w-100">Submit</button>
386+ </form>
387+ </div>
388+ </div>
389+ </div>
390+ </div>
391+ }
392+}
393diff --git a/pages/auth_templ.go b/pages/auth_templ.go
394new file mode 100644
395index 0000000000000000000000000000000000000000..c0ad6766c0943f40c920a670a2d416c007df1b92
396--- /dev/null
397+++ b/pages/auth_templ.go
398@@ -0,0 +1,81 @@
399+// Code generated by templ - DO NOT EDIT.
400+
401+// templ: version: v0.3.960
402+package pages
403+
404+//lint:file-ignore SA4006 This context is only used if a nested component is present.
405+
406+import "github.com/a-h/templ"
407+import templruntime "github.com/a-h/templ/runtime"
408+
409+func AuthPage(errorMsg string) templ.Component {
410+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
411+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
412+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
413+ return templ_7745c5c3_CtxErr
414+ }
415+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
416+ if !templ_7745c5c3_IsBuffer {
417+ defer func() {
418+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
419+ if templ_7745c5c3_Err == nil {
420+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
421+ }
422+ }()
423+ }
424+ ctx = templ.InitializeContext(ctx)
425+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
426+ if templ_7745c5c3_Var1 == nil {
427+ templ_7745c5c3_Var1 = templ.NopComponent
428+ }
429+ ctx = templ.ClearChildren(ctx)
430+ templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
431+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
432+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
433+ if !templ_7745c5c3_IsBuffer {
434+ defer func() {
435+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
436+ if templ_7745c5c3_Err == nil {
437+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
438+ }
439+ }()
440+ }
441+ ctx = templ.InitializeContext(ctx)
442diff --git a/pages/error.templ b/pages/error.templ
443index 35b8b02c43f9362612896555244b3003fa9c6dad..23e489136b9f01f343307bc91b2b85a54b8c1515 100644
444--- a/pages/error.templ
445+++ b/pages/error.templ
446@@ -2,11 +2,11 @@ package pages
447
448 import "fmt"
449
450-templ ErrorPage(err error) {
451+templ ErrorPage(err string) {
452 <div class="toast align-items-center text-bg-danger border-0" role="alert">
453 <div class="d-flex">
454 <div class="toast-body">
455- { fmt.Sprintf("Error: %s", err.Error()) }
456+ { fmt.Sprintf("Error: %s", err) }
457 </div>
458 <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
459 </div>
460diff --git a/pages/error_templ.go b/pages/error_templ.go
461index 04e3ca881c58552b3b83f4e37bd3bd8e31719608..8661470749078b3e55083c47e1ef873071e81d90 100644
462--- a/pages/error_templ.go
463+++ b/pages/error_templ.go
464@@ -10,7 +10,7 @@ import templruntime "github.com/a-h/templ/runtime"
465
466 import "fmt"
467
468-func ErrorPage(err error) templ.Component {
469+func ErrorPage(err string) templ.Component {
470 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
471 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
472 if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
473@@ -36,9 +36,9 @@ func ErrorPage(err error) templ.Component {
474 return templ_7745c5c3_Err
475 }
476 var templ_7745c5c3_Var2 string
477- templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Error: %s", err.Error()))
478+ templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Error: %s", err))
479 if templ_7745c5c3_Err != nil {
480- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/error.templ`, Line: 9, Col: 43}
481+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/error.templ`, Line: 9, Col: 35}
482 }
483 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
484 if templ_7745c5c3_Err != nil {
485diff --git a/pages/main.templ b/pages/main.templ
486new file mode 100644
487index 0000000000000000000000000000000000000000..b23de3f4cf10d30565c6ccbd75c5b229255d63f3
488--- /dev/null
489+++ b/pages/main.templ
490@@ -0,0 +1,26 @@
491+package pages
492+
493+templ BaseLayout(title string) {
494+ <!DOCTYPE html>
495+ <html lang="en" data-bs-theme="dark">
496+ <head>
497+ <title>{ title }</title>
498+ <meta charset="utf-8"/>
499+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
500+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous"/>
501+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css"/>
502+ <style>
503+ .pillarbox { position: relative; }
504+ .pillarbox::after { right: 0; }
505+ .pillarbox::before,
506+ .pillarbox::after { content: ''; position: absolute; top: 0; bottom: 0%; width: 36%; background: black; z-index: 1; }
507+ </style>
508+ <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
509+ </head>
510+ <body>
511+ <div class="container-fluid">
512+ { children... }
513+ </div>
514+ </body>
515+ </html>
516+}
517diff --git a/pages/main_templ.go b/pages/main_templ.go
518new file mode 100644
519index 0000000000000000000000000000000000000000..280534d91bbb5bdd2f9aee6dd0b0f6f0bec857ba
520--- /dev/null
521+++ b/pages/main_templ.go
522@@ -0,0 +1,61 @@
523+// Code generated by templ - DO NOT EDIT.
524+
525+// templ: version: v0.3.960
526+package pages
527+
528+//lint:file-ignore SA4006 This context is only used if a nested component is present.
529+
530+import "github.com/a-h/templ"
531+import templruntime "github.com/a-h/templ/runtime"
532+
533+func BaseLayout(title string) templ.Component {
534+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
535+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
536+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
537+ return templ_7745c5c3_CtxErr
538+ }
539+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
540+ if !templ_7745c5c3_IsBuffer {
541+ defer func() {
542+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
543+ if templ_7745c5c3_Err == nil {
544+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
545+ }
546+ }()
547+ }
548+ ctx = templ.InitializeContext(ctx)
549+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
550+ if templ_7745c5c3_Var1 == nil {
551+ templ_7745c5c3_Var1 = templ.NopComponent
552+ }
553+ ctx = templ.ClearChildren(ctx)
554+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" data-bs-theme=\"dark\"><head><title>")
555+ if templ_7745c5c3_Err != nil {
556+ return templ_7745c5c3_Err
557+ }
558+ var templ_7745c5c3_Var2 string
559+ templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
560+ if templ_7745c5c3_Err != nil {
561+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/main.templ`, Line: 7, Col: 17}
562+ }
563+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
564+ if templ_7745c5c3_Err != nil {
565+ return templ_7745c5c3_Err
566+ }
567diff --git a/pages/videos.templ b/pages/new_videos.templ
568rename from pages/videos.templ
569rename to pages/new_videos.templ
570index 81aa9d0c1c04fcc21db6a4993f84af6c859cb244..1947ba9c165a9e77ffe3e749269687a518852ab6 100644
571--- a/pages/videos.templ
572+++ b/pages/new_videos.templ
573@@ -80,8 +80,8 @@ templ videoCard(video models.Video) {
574 hx-swap="outerHTML"
575 class="d-flex"
576 >
577- <input type="text" name="progress" placeholder="hh:mm:ss" class="form-control "/>
578- <button type="submit" class="btn btn-primary ms-3"><i class="bi bi-clock"></i> </button>
579+ <input type="text" name="progress" placeholder="1h2m3s" class="form-control" style="width: 50%"/>
580+ <button type="submit" class="btn btn-primary ms-3"><i class="bi bi-clock"></i></button>
581 </form>
582 <button
583 hx-patch={ fmt.Sprintf("/videos/%s/watch", video.ID) }
584@@ -97,32 +97,13 @@ templ videoCard(video models.Video) {
585 }
586
587 templ NewVideosPage(videos []models.Video) {
588- <!DOCTYPE html>
589- <html lang="en" data-bs-theme="dark">
590- <head>
591- <title>ytrssil</title>
592- <meta charset="utf-8"/>
593- <meta name="viewport" content="width=device-width, initial-scale=1"/>
594- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous"/>
595- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css"/>
596- <style>
597- .pillarbox { position: relative; }
598- .pillarbox::after { right: 0; }
599- .pillarbox::before,
600- .pillarbox::after { content: ''; position: absolute; top: 0; bottom: 0%; width: 36%; background: black; z-index: 1; }
601- </style>
602- <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
603- </head>
604- <body>
605- <div class="container-fluid">
606- <div class="row">
607- for _, video := range videos {
608- <div class="video-card col-md-3 p-2">
609- @videoCard(video)
610- </div>
611- }
612+ @BaseLayout("ytrssil - New Videos") {
613+ <div class="row">
614+ for _, video := range videos {
615+ <div class="video-card col-md-3 p-2">
616+ @videoCard(video)
617 </div>
618- </div>
619- </body>
620- </html>
621+ }
622+ </div>
623+ }
624 }
625diff --git a/pages/videos_templ.go b/pages/new_videos_templ.go
626rename from pages/videos_templ.go
627rename to pages/new_videos_templ.go
628index aabcfef72e4874ab0c22b7b8699fd608801a6eba..baf4eabd53deacdbbf77f7e239e74d1a1c8579f7 100644
629--- a/pages/videos_templ.go
630+++ b/pages/new_videos_templ.go
631@@ -43,7 +43,7 @@ func ProgressBar(video models.Video) templ.Component {
632 var templ_7745c5c3_Var2 string
633 templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("progress-%s", video.ID))
634 if templ_7745c5c3_Err != nil {
635- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/videos.templ`, Line: 11, Col: 48}
636+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/new_videos.templ`, Line: 11, Col: 48}
637 }
638 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
639 if templ_7745c5c3_Err != nil {
640@@ -56,7 +56,7 @@ func ProgressBar(video models.Video) templ.Component {
641 var templ_7745c5c3_Var3 string
642 templ_7745c5c3_Var3, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("background: #b11; width: %d%%", video.ProgressPercentage()))
643 if templ_7745c5c3_Err != nil {
644- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/videos.templ`, Line: 12, Col: 109}
645+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/new_videos.templ`, Line: 12, Col: 109}
646 }
647 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
648 if templ_7745c5c3_Err != nil {
649@@ -74,7 +74,7 @@ func ProgressBar(video models.Video) templ.Component {
650 var templ_7745c5c3_Var4 string
651 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("progress-%s", video.ID))
652 if templ_7745c5c3_Err != nil {
653- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/videos.templ`, Line: 15, Col: 48}
654+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/new_videos.templ`, Line: 15, Col: 48}
655 }
656 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
657 if templ_7745c5c3_Err != nil {
658@@ -117,7 +117,7 @@ func thumbnail(video models.Video) templ.Component {
659 var templ_7745c5c3_Var6 templ.SafeURL
660 templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("https://youtube.com/watch?v=%s", video.ID)))
661 if templ_7745c5c3_Err != nil {
662- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/videos.templ`, Line: 22, Col: 79}
663+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/new_videos.templ`, Line: 22, Col: 79}
664 }
665 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
666 if templ_7745c5c3_Err != nil {
667@@ -145,7 +145,7 @@ func thumbnail(video models.Video) templ.Component {
668 var templ_7745c5c3_Var7 string
669 templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("https://img.youtube.com/vi/%s/0.jpg", video.ID))
670 if templ_7745c5c3_Err != nil {
671- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/videos.templ`, Line: 30, Col: 70}
672+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/new_videos.templ`, Line: 30, Col: 70}
673 }
674 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
675 if templ_7745c5c3_Err != nil {
676@@ -158,7 +158,7 @@ func thumbnail(video models.Video) templ.Component {
677 var templ_7745c5c3_Var8 string
678 templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(video.Title)
679 if templ_7745c5c3_Err != nil {
680- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/videos.templ`, Line: 32, Col: 21}
681+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/new_videos.templ`, Line: 32, Col: 21}
682 }
683 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
684 if templ_7745c5c3_Err != nil {
685@@ -176,7 +176,7 @@ func thumbnail(video models.Video) templ.Component {
686 var templ_7745c5c3_Var9 string
687 templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("https://img.youtube.com/vi/%s/0.jpg", video.ID))
688 if templ_7745c5c3_Err != nil {
689- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/videos.templ`, Line: 37, Col: 70}
690+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/new_videos.templ`, Line: 37, Col: 70}
691 }
692 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
693 if templ_7745c5c3_Err != nil {
694@@ -189,7 +189,7 @@ func thumbnail(video models.Video) templ.Component {
695 var templ_7745c5c3_Var10 string
696 templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(video.Title)
697 if templ_7745c5c3_Err != nil {
698- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/videos.templ`, Line: 39, Col: 21}
699+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/new_videos.templ`, Line: 39, Col: 21}
700 }
701 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
702 if templ_7745c5c3_Err != nil {
703@@ -253,7 +253,7 @@ func videoCard(video models.Video) templ.Component {
704 var templ_7745c5c3_Var12 string
705 templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(video.Title)
706 if templ_7745c5c3_Err != nil {
707- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/videos.templ`, Line: 64, Col: 18}
708+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/new_videos.templ`, Line: 64, Col: 18}
709 }
710 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
711 if templ_7745c5c3_Err != nil {
712@@ -271,7 +271,7 @@ func videoCard(video models.Video) templ.Component {
713 var templ_7745c5c3_Var13 string
714 templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(video.Title)
715 if templ_7745c5c3_Err != nil {
716- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/videos.templ`, Line: 67, Col: 53}
717+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/new_videos.templ`, Line: 67, Col: 53}
718 }
719 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
720 if templ_7745c5c3_Err != nil {
721@@ -289,7 +289,7 @@ func videoCard(video models.Video) templ.Component {
722 var templ_7745c5c3_Var14 templ.SafeURL
723 templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinURLErrs(fmt.Sprintf("https://www.youtube.com/channel/%s", video.ChannelID))
724 if templ_7745c5c3_Err != nil {
725- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/videos.templ`, Line: 72, Col: 78}
726+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/new_videos.templ`, Line: 72, Col: 78}
727 }
728 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
729 if templ_7745c5c3_Err != nil {
730@@ -302,7 +302,7 @@ func videoCard(video models.Video) templ.Component {