0ad9e1e21db431a38ad484a8140574f18f216067

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

Add watched videos page

Diff

This diff is truncated to protect this page.

  1diff --git a/db/videos.go b/db/videos.go
  2index 7cc3309c1eea425387856fbfa0f6fef85ae0abda..8dfad98b2defa98feed633c2d807003b3fd59977 100644
  3--- a/db/videos.go
  4+++ b/db/videos.go
  5@@ -72,7 +72,7 @@ func (d *postgresDB) GetWatchedVideos(ctx context.Context, sortDesc bool) ([]mod
  6 		FROM videos
  7 		LEFT JOIN channels ON videos.channel_id=channels.id
  8 		WHERE watch_timestamp IS NOT NULL
  9-		ORDER BY watch_timestamp DESC
 10+		ORDER BY watch_timestamp
 11 	`
 12 	if sortDesc {
 13 		query += " DESC"
 14diff --git a/httpserver/ytrssil/server.go b/httpserver/ytrssil/server.go
 15index 8c59f62b26792426b70e638d43bb0daff930bbc3..544c0b89bab920db8085a607e21e37322c3c45d6 100644
 16--- a/httpserver/ytrssil/server.go
 17+++ b/httpserver/ytrssil/server.go
 18@@ -74,9 +74,11 @@ func SetupGinRouter(
 19 	pages.Use(auth.PageAuthMiddleware(cfg.AuthToken))
 20 	{
 21 		pages.GET("/", srv.NewVideosPage)
 22+		pages.GET("/watched", srv.WatchedVideosPage)
 23 		pages.POST("/subscribe", srv.SubscribeToChannelPage)
 24 		pages.POST("/videos", srv.AddVideoPage)
 25 		pages.PATCH("/videos/:video_id/watch", srv.MarkVideoAsWatchedPage)
 26+		pages.PATCH("/videos/:video_id/unwatch", srv.MarkVideoAsUnwatchedPage)
 27 		pages.PATCH("/videos/:video_id/progress", srv.SetVideoProgressPage)
 28 	}
 29 
 30diff --git a/httpserver/ytrssil/videos_pages.go b/httpserver/ytrssil/videos_pages.go
 31index 8200b219b93d771aa4c96dfdfab6683b74fb6c41..cf81b9d7bd090d5970059a86b2798e5fa7929bee 100644
 32--- a/httpserver/ytrssil/videos_pages.go
 33+++ b/httpserver/ytrssil/videos_pages.go
 34@@ -21,6 +21,19 @@ func (srv server) NewVideosPage(c *gin.Context) {
 35 	})
 36 }
 37 
 38+func (srv server) WatchedVideosPage(c *gin.Context) {
 39+	videos, err := srv.handler.GetWatchedVideos(c.Request.Context(), true)
 40+	if err != nil {
 41+		returnErr(c, http.StatusInternalServerError, err)
 42+		return
 43+	}
 44+
 45+	c.Render(http.StatusOK, pages.TemplRenderer{
 46+		Ctx:       c.Request.Context(),
 47+		Component: pages.WatchedVideosPage(videos),
 48+	})
 49+}
 50+
 51 func (srv server) MarkVideoAsWatchedPage(c *gin.Context) {
 52 	err := srv.handler.MarkVideoAsWatched(c.Request.Context(), c.Param("video_id"))
 53 	if err != nil {
 54@@ -31,6 +44,16 @@ func (srv server) MarkVideoAsWatchedPage(c *gin.Context) {
 55 	returnMsg(c, http.StatusOK, "")
 56 }
 57 
 58+func (srv server) MarkVideoAsUnwatchedPage(c *gin.Context) {
 59+	err := srv.handler.MarkVideoAsUnwatched(c.Request.Context(), c.Param("video_id"))
 60+	if err != nil {
 61+		returnErr(c, http.StatusInternalServerError, err)
 62+		return
 63+	}
 64+
 65+	returnMsg(c, http.StatusOK, "")
 66+}
 67+
 68 func (srv server) SetVideoProgressPage(c *gin.Context) {
 69 	progress := c.PostForm("progress")
 70 	if progress == "" {
 71diff --git a/pages/auth.templ b/pages/auth.templ
 72index 7f9d27fb9ececa6cda0bc7824d5c7fdb51a41d65..daf515468f7b51ddc988c42716cd98af9c0f8de9 100644
 73--- a/pages/auth.templ
 74+++ b/pages/auth.templ
 75@@ -1,7 +1,7 @@
 76 package pages
 77 
 78 templ AuthPage(errorMsg string) {
 79-	@BaseLayout("ytrssil - Authentication") {
 80+	@BaseLayout("ytrssil - Authentication", false) {
 81 		<div class="row justify-content-center mt-5">
 82 			<div class="col-md-6 col-lg-4">
 83 				<div class="card">
 84diff --git a/pages/auth_templ.go b/pages/auth_templ.go
 85index c0ad6766c0943f40c920a670a2d416c007df1b92..c49afe51c2d07b4e3bac09addfbea1d211668f31 100644
 86--- a/pages/auth_templ.go
 87+++ b/pages/auth_templ.go
 88@@ -70,7 +70,7 @@ func AuthPage(errorMsg string) templ.Component {
 89 			}
 90 			return nil
 91 		})
 92-		templ_7745c5c3_Err = BaseLayout("ytrssil - Authentication").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
 93+		templ_7745c5c3_Err = BaseLayout("ytrssil - Authentication", false).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
 94 		if templ_7745c5c3_Err != nil {
 95 			return templ_7745c5c3_Err
 96 		}
 97diff --git a/pages/main.templ b/pages/main.templ
 98index 5e6066c1607c3cf9ebc17842664cad3bf5734fff..676e1fec14d1b3e37ccba59c3408027f67a24a43 100644
 99--- a/pages/main.templ
100+++ b/pages/main.templ
101@@ -1,6 +1,6 @@
102 package pages
103 
104-templ BaseLayout(title string) {
105+templ BaseLayout(title string, isWatchedPage bool) {
106 	<!DOCTYPE html>
107 	<html lang="en" data-bs-theme="dark">
108 		<head>
109@@ -28,7 +28,7 @@ templ BaseLayout(title string) {
110 		<body>
111 			@subscriptionModal()
112 			@addVideoModal()
113-			@navbar()
114+			@navbar(isWatchedPage)
115 			<div class="container-fluid">
116 				{ children... }
117 			</div>
118diff --git a/pages/main_templ.go b/pages/main_templ.go
119index 8b39fb0635b20110ef1d3891e48979d272f6f8c7..b9f17548503ee5fc741cae1cc0901ca5a6c67615 100644
120--- a/pages/main_templ.go
121+++ b/pages/main_templ.go
122@@ -8,7 +8,7 @@ package pages
123 import "github.com/a-h/templ"
124 import templruntime "github.com/a-h/templ/runtime"
125 
126-func BaseLayout(title string) templ.Component {
127+func BaseLayout(title string, isWatchedPage bool) templ.Component {
128 	return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
129 		templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
130 		if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
131@@ -54,7 +54,7 @@ func BaseLayout(title string) templ.Component {
132 		if templ_7745c5c3_Err != nil {
133 			return templ_7745c5c3_Err
134 		}
135-		templ_7745c5c3_Err = navbar().Render(ctx, templ_7745c5c3_Buffer)
136+		templ_7745c5c3_Err = navbar(isWatchedPage).Render(ctx, templ_7745c5c3_Buffer)
137 		if templ_7745c5c3_Err != nil {
138 			return templ_7745c5c3_Err
139 		}
140diff --git a/pages/navbar.templ b/pages/navbar.templ
141index 950e09bc795ed55887b76b507cf9d023d89f43bb..5828b0b16de926ba421b2df3640e47751fe5dbef 100644
142--- a/pages/navbar.templ
143+++ b/pages/navbar.templ
144@@ -1,6 +1,6 @@
145 package pages
146 
147-templ navbar() {
148+templ navbar(isWatchedPage bool) {
149 	<nav class="navbar navbar-expand-lg bg-body-tertiary">
150 		<div class="container-fluid">
151 			<a class="navbar-brand" href="/">
152@@ -32,6 +32,17 @@ templ navbar() {
153 							Add Video <i class="bi bi-camera-video"></i>
154 						</button>
155 					</li>
156+					<li class="nav-item me-md-2 mt-2 mt-md-0">
157+						if isWatchedPage {
158+							<a href="/" class="btn btn-outline-secondary">
159+								New Videos <i class="bi bi-collection-play"></i>
160+							</a>
161+						} else {
162+							<a href="/watched" class="btn btn-outline-secondary">
163+								Watched <i class="bi bi-check-circle"></i>
164+							</a>
165+						}
166+					</li>
167 				</ul>
168 				<div class="d-flex" role="search">
169 					<input id="video-search" class="form-control" type="search" placeholder="Search videos..."/>
170diff --git a/pages/navbar_templ.go b/pages/navbar_templ.go
171index 9f020fc85b1250d27ecd7e30f90982a5631ca897..133e9cff43c2cae5ad7100d7ab177c9f537b83f9 100644
172--- a/pages/navbar_templ.go
173+++ b/pages/navbar_templ.go
174@@ -8,7 +8,7 @@ package pages
175 import "github.com/a-h/templ"
176 import templruntime "github.com/a-h/templ/runtime"
177 
178-func navbar() templ.Component {
179+func navbar(isWatchedPage bool) templ.Component {
180 	return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
181 		templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
182 		if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
183@@ -29,7 +29,22 @@ func navbar() templ.Component {
184 			templ_7745c5c3_Var1 = templ.NopComponent
185 		}
186 		ctx = templ.ClearChildren(ctx)
187diff --git a/pages/new_videos.templ b/pages/new_videos.templ
188index 80b0651998b085238b76aecda0905f76c6999edb..036c9704137c4308bd2414a41d3372a2b02bce2b 100644
189--- a/pages/new_videos.templ
190+++ b/pages/new_videos.templ
191@@ -100,7 +100,7 @@ templ videoCard(video models.Video) {
192 }
193 
194 templ NewVideosPage(videos []models.Video) {
195-	@BaseLayout("ytrssil - New Videos") {
196+	@BaseLayout("ytrssil - New Videos", false) {
197 		<div class="row">
198 			for _, video := range videos {
199 				<div class="video-card col-md-3 p-2" data-title={ video.Title } data-channel-name={ video.ChannelName }>
200diff --git a/pages/new_videos_templ.go b/pages/new_videos_templ.go
201index 5a1701ede5fceb27054bcf260e26623856aa5403..4bb5e32fdc39129f11b9ea7a58c616caf6c069e8 100644
202--- a/pages/new_videos_templ.go
203+++ b/pages/new_videos_templ.go
204@@ -436,7 +436,7 @@ func NewVideosPage(videos []models.Video) templ.Component {
205 			}
206 			return nil
207 		})
208-		templ_7745c5c3_Err = BaseLayout("ytrssil - New Videos").Render(templ.WithChildren(ctx, templ_7745c5c3_Var21), templ_7745c5c3_Buffer)
209+		templ_7745c5c3_Err = BaseLayout("ytrssil - New Videos", false).Render(templ.WithChildren(ctx, templ_7745c5c3_Var21), templ_7745c5c3_Buffer)
210 		if templ_7745c5c3_Err != nil {
211 			return templ_7745c5c3_Err
212 		}
213diff --git a/pages/watched_videos.templ b/pages/watched_videos.templ
214new file mode 100644
215index 0000000000000000000000000000000000000000..27f8f6019febe01437d69e920923f3d59dfc25ba
216--- /dev/null
217+++ b/pages/watched_videos.templ
218@@ -0,0 +1,48 @@
219+package pages
220+
221+import (
222+	"fmt"
223+
224+	"github.com/TheEdgeOfRage/ytrssil-api/models"
225+)
226+
227+templ watchedVideoCard(video models.Video) {
228+	<div class="card">
229+		@thumbnail(video)
230+		<div class="card-body">
231+			<p class="card-title text-truncate mb-0">{ video.Title }</p>
232+			<p class="d-flex justify-content-between">
233+				<a
234+					target="blank"
235+					href={ fmt.Sprintf("https://www.youtube.com/channel/%s", video.ChannelID) }
236+					class="card-text fw-light link-light link-underline-opacity-0"
237+				>{ video.ChannelName }</a>
238+				<span>
239+					{ video.HumanizedPublishTime() }
240+				</span>
241+			</p>
242+			<div class="d-flex justify-content-end">
243+				<button
244+					hx-patch={ fmt.Sprintf("/videos/%s/unwatch", video.ID) }
245+					hx-target="closest .video-card"
246+					hx-swap="outerHTML"
247+					class="btn btn-secondary"
248+				>
249+					<i class="bi bi-arrow-counterclockwise"></i>
250+				</button>
251+			</div>
252+		</div>
253+	</div>
254+}
255+
256+templ WatchedVideosPage(videos []models.Video) {
257+	@BaseLayout("ytrssil - Watched Videos", true) {
258+		<div class="row">
259+			for _, video := range videos {
260+				<div class="video-card col-md-3 p-2" data-title={ video.Title } data-channel-name={ video.ChannelName }>
261+					@watchedVideoCard(video)
262+				</div>
263+			}
264+		</div>
265+	}
266+}
267diff --git a/pages/watched_videos_templ.go b/pages/watched_videos_templ.go
268new file mode 100644
269index 0000000000000000000000000000000000000000..02000188c8e55176cdf4438cbfae65e0ade657eb
270--- /dev/null
271+++ b/pages/watched_videos_templ.go
272@@ -0,0 +1,210 @@
273+// Code generated by templ - DO NOT EDIT.
274+
275+// templ: version: v0.3.960
276+package pages
277+
278+//lint:file-ignore SA4006 This context is only used if a nested component is present.
279+
280+import "github.com/a-h/templ"
281+import templruntime "github.com/a-h/templ/runtime"
282+
283+import (
284+	"fmt"
285+
286+	"github.com/TheEdgeOfRage/ytrssil-api/models"
287+)
288+
289+func watchedVideoCard(video models.Video) templ.Component {
290+	return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
291+		templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
292+		if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
293+			return templ_7745c5c3_CtxErr
294+		}
295+		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
296+		if !templ_7745c5c3_IsBuffer {
297+			defer func() {
298+				templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
299+				if templ_7745c5c3_Err == nil {
300+					templ_7745c5c3_Err = templ_7745c5c3_BufErr
301+				}
302+			}()
303+		}
304+		ctx = templ.InitializeContext(ctx)
305+		templ_7745c5c3_Var1 := templ.GetChildren(ctx)
306+		if templ_7745c5c3_Var1 == nil {
307+			templ_7745c5c3_Var1 = templ.NopComponent
308+		}
309+		ctx = templ.ClearChildren(ctx)
310+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"card\">")
311+		if templ_7745c5c3_Err != nil {
312+			return templ_7745c5c3_Err
313+		}
314+		templ_7745c5c3_Err = thumbnail(video).Render(ctx, templ_7745c5c3_Buffer)
315+		if templ_7745c5c3_Err != nil {
316+			return templ_7745c5c3_Err
317+		}
318+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"card-body\"><p class=\"card-title text-truncate mb-0\">")
319+		if templ_7745c5c3_Err != nil {
320+			return templ_7745c5c3_Err
321+		}
322+		var templ_7745c5c3_Var2 string
323+		templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(video.Title)
324+		if templ_7745c5c3_Err != nil {
325+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/watched_videos.templ`, Line: 13, Col: 57}
326+		}
327+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
328+		if templ_7745c5c3_Err != nil {
329+			return templ_7745c5c3_Err
330+		}
331+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</p><p class=\"d-flex justify-content-between\"><a target=\"blank\" href=\"")
332+		if templ_7745c5c3_Err != nil {
333+			return templ_7745c5c3_Err
334+		}
335+		var templ_7745c5c3_Var3 templ.SafeURL
336+		templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(fmt.Sprintf("https://www.youtube.com/channel/%s", video.ChannelID))
337+		if templ_7745c5c3_Err != nil {
338+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/watched_videos.templ`, Line: 17, Col: 78}
339+		}
340+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
341+		if templ_7745c5c3_Err != nil {
342+			return templ_7745c5c3_Err
343+		}
344+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" class=\"card-text fw-light link-light link-underline-opacity-0\">")
345+		if templ_7745c5c3_Err != nil {
346+			return templ_7745c5c3_Err
347+		}
348+		var templ_7745c5c3_Var4 string
349+		templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(video.ChannelName)
350+		if templ_7745c5c3_Err != nil {
351+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/watched_videos.templ`, Line: 19, Col: 24}
352+		}
353+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
354+		if templ_7745c5c3_Err != nil {
355+			return templ_7745c5c3_Err
356+		}
357+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</a> <span>")
358+		if templ_7745c5c3_Err != nil {
359+			return templ_7745c5c3_Err
360+		}
361+		var templ_7745c5c3_Var5 string
362+		templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(video.HumanizedPublishTime())
363+		if templ_7745c5c3_Err != nil {
364+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/watched_videos.templ`, Line: 21, Col: 35}
365+		}
366+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
367+		if templ_7745c5c3_Err != nil {
368+			return templ_7745c5c3_Err
369+		}
370+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</span></p><div class=\"d-flex justify-content-end\"><button hx-patch=\"")
371+		if templ_7745c5c3_Err != nil {