ee34e4dec92a91d64ab587dfae9f9ae8aab57c51

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

Message

Add pagination to watched videos page

Diff

  1diff --git a/db/db.go b/db/db.go
  2index c125121e19e6a3a41894f5c6d3b5652dd83f3096..8c1595eb9a42dce6544524e5c9ea326049ece9cd 100644
  3--- a/db/db.go
  4+++ b/db/db.go
  5@@ -27,7 +27,7 @@ type DB interface {
  6 	// GetNewVideos returns a list of unwatched videos from all subscribed channels
  7 	GetNewVideos(ctx context.Context, sortDesc bool) ([]models.Video, error)
  8 	// GetWatchedVideos returns a list of all watched videos
  9-	GetWatchedVideos(ctx context.Context, sortDesc bool) ([]models.Video, error)
 10+	GetWatchedVideos(ctx context.Context, sortDesc bool, limit int, offset int) ([]models.Video, error)
 11 	// HasVideo returns true if the video with the given ID exists in the DB
 12 	HasVideo(ctx context.Context, videoID string) (bool, error)
 13 	// AddVideo adds a newly published video to the database
 14diff --git a/db/videos.go b/db/videos.go
 15index 8dfad98b2defa98feed633c2d807003b3fd59977..34b27f9416eaf5d3fc8f44ada4d945aeb9cf446a 100644
 16--- a/db/videos.go
 17+++ b/db/videos.go
 18@@ -57,7 +57,9 @@ func (d *postgresDB) GetNewVideos(ctx context.Context, sortDesc bool) ([]models.
 19 	return videos, nil
 20 }
 21 
 22-func (d *postgresDB) GetWatchedVideos(ctx context.Context, sortDesc bool) ([]models.Video, error) {
 23+func (d *postgresDB) GetWatchedVideos(
 24+	ctx context.Context, sortDesc bool, limit int, offset int,
 25+) ([]models.Video, error) {
 26 	query := `
 27 		SELECT
 28 			videos.id
 29@@ -77,8 +79,9 @@ func (d *postgresDB) GetWatchedVideos(ctx context.Context, sortDesc bool) ([]mod
 30 	if sortDesc {
 31 		query += " DESC"
 32 	}
 33+	query += " LIMIT $1 OFFSET $2"
 34 
 35-	rows, err := d.db.QueryContext(ctx, query)
 36+	rows, err := d.db.QueryContext(ctx, query, limit, offset)
 37 	if err != nil {
 38 		d.l.Error("Failed to query for watched videos", "call", "sql.QueryContext", "error", err)
 39 		return nil, err
 40diff --git a/handler/handler.go b/handler/handler.go
 41index ed66479b9fb36156f39c38fb2c1b8deac656fec0..0557c14080f60cb742810b5c4b38494a1dd04405 100644
 42--- a/handler/handler.go
 43+++ b/handler/handler.go
 44@@ -15,7 +15,7 @@ type Handler interface {
 45 	UnsubscribeFromChannel(ctx context.Context, channelID string) error
 46 	ListChannels(ctx context.Context) ([]models.Channel, error)
 47 	GetNewVideos(ctx context.Context, sortDesc bool) ([]models.Video, error)
 48-	GetWatchedVideos(ctx context.Context, sortDesc bool) ([]models.Video, error)
 49+	GetWatchedVideos(ctx context.Context, sortDesc bool, page int) ([]models.Video, error)
 50 	FetchVideos(ctx context.Context) error
 51 	MarkVideoAsWatched(ctx context.Context, videoID string) error
 52 	MarkVideoAsUnwatched(ctx context.Context, videoID string) error
 53diff --git a/handler/videos.go b/handler/videos.go
 54index 108e1bd29fd81675d33815da724238baef4c8485..856e994e3ed40a51bf03fd6ff339a1029e4eaf40 100644
 55--- a/handler/videos.go
 56+++ b/handler/videos.go
 57@@ -20,8 +20,14 @@ func (h *handler) GetNewVideos(ctx context.Context, sortDesc bool) ([]models.Vid
 58 	return h.db.GetNewVideos(ctx, sortDesc)
 59 }
 60 
 61-func (h *handler) GetWatchedVideos(ctx context.Context, sortDesc bool) ([]models.Video, error) {
 62-	return h.db.GetWatchedVideos(ctx, sortDesc)
 63+const WatchedVideosPageSize = 100
 64+
 65+func (h *handler) GetWatchedVideos(ctx context.Context, sortDesc bool, page int) ([]models.Video, error) {
 66+	if page < 1 {
 67+		page = 1
 68+	}
 69+	offset := (page - 1) * WatchedVideosPageSize
 70+	return h.db.GetWatchedVideos(ctx, sortDesc, WatchedVideosPageSize, offset)
 71 }
 72 
 73 func (h *handler) addVideosForChannel(ctx context.Context, parsedChannel *feedparser.Channel) {
 74diff --git a/httpserver/ytrssil/videos.go b/httpserver/ytrssil/videos.go
 75index 663abadd7e853c07f1aca97911382cb4a610766b..c81f088c8ca7017f434c9f9abccf781a159a2036 100644
 76--- a/httpserver/ytrssil/videos.go
 77+++ b/httpserver/ytrssil/videos.go
 78@@ -17,7 +17,7 @@ func (srv *server) GetNewVideosJSON(c *gin.Context) {
 79 }
 80 
 81 func (srv *server) GetWatchedVideosJSON(c *gin.Context) {
 82-	videos, err := srv.handler.GetWatchedVideos(c.Request.Context(), false)
 83+	videos, err := srv.handler.GetWatchedVideos(c.Request.Context(), false, 1)
 84 	if err != nil {
 85 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
 86 		return
 87diff --git a/httpserver/ytrssil/videos_pages.go b/httpserver/ytrssil/videos_pages.go
 88index cf81b9d7bd090d5970059a86b2798e5fa7929bee..f4044398c5cec2aef7b232b107724d65b7f05fae 100644
 89--- a/httpserver/ytrssil/videos_pages.go
 90+++ b/httpserver/ytrssil/videos_pages.go
 91@@ -2,6 +2,7 @@ package ytrssil
 92 
 93 import (
 94 	"net/http"
 95+	"strconv"
 96 
 97 	"github.com/gin-gonic/gin"
 98 
 99@@ -22,7 +23,14 @@ func (srv server) NewVideosPage(c *gin.Context) {
100 }
101 
102 func (srv server) WatchedVideosPage(c *gin.Context) {
103-	videos, err := srv.handler.GetWatchedVideos(c.Request.Context(), true)
104+	page := 1
105+	if pageParam := c.Query("page"); pageParam != "" {
106+		if parsedPage, err := strconv.Atoi(pageParam); err == nil && parsedPage > 0 {
107+			page = parsedPage
108+		}
109+	}
110+
111+	videos, err := srv.handler.GetWatchedVideos(c.Request.Context(), true, page)
112 	if err != nil {
113 		returnErr(c, http.StatusInternalServerError, err)
114 		return
115@@ -30,7 +38,7 @@ func (srv server) WatchedVideosPage(c *gin.Context) {
116 
117 	c.Render(http.StatusOK, pages.TemplRenderer{
118 		Ctx:       c.Request.Context(),
119-		Component: pages.WatchedVideosPage(videos),
120+		Component: pages.WatchedVideosPage(videos, page),
121 	})
122 }
123 
124diff --git a/mocks/db/db.go b/mocks/db/db.go
125index 324291c1beaaa65ba316db256d5a0935b14870f7..3be205326db4a789e1dbf4df1e2fdb7ce507e574 100644
126--- a/mocks/db/db.go
127+++ b/mocks/db/db.go
128@@ -27,7 +27,7 @@ var _ db.DB = &DBMock{}
129 //			GetNewVideosFunc: func(ctx context.Context, sortDesc bool) ([]models.Video, error) {
130 //				panic("mock out the GetNewVideos method")
131 //			},
132-//			GetWatchedVideosFunc: func(ctx context.Context, sortDesc bool) ([]models.Video, error) {
133+//			GetWatchedVideosFunc: func(ctx context.Context, sortDesc bool, limit int, offset int) ([]models.Video, error) {
134 //				panic("mock out the GetWatchedVideos method")
135 //			},
136 //			HasVideoFunc: func(ctx context.Context, videoID string) (bool, error) {
137@@ -62,7 +62,7 @@ type DBMock struct {
138 	GetNewVideosFunc func(ctx context.Context, sortDesc bool) ([]models.Video, error)
139 
140 	// GetWatchedVideosFunc mocks the GetWatchedVideos method.
141-	GetWatchedVideosFunc func(ctx context.Context, sortDesc bool) ([]models.Video, error)
142+	GetWatchedVideosFunc func(ctx context.Context, sortDesc bool, limit int, offset int) ([]models.Video, error)
143 
144 	// HasVideoFunc mocks the HasVideo method.
145 	HasVideoFunc func(ctx context.Context, videoID string) (bool, error)
146@@ -106,6 +106,10 @@ type DBMock struct {
147 			Ctx context.Context
148 			// SortDesc is the sortDesc argument value.
149 			SortDesc bool
150+			// Limit is the limit argument value.
151+			Limit int
152+			// Offset is the offset argument value.
153+			Offset int
154 		}
155 		// HasVideo holds details about calls to the HasVideo method.
156 		HasVideo []struct {
157@@ -240,21 +244,25 @@ func (mock *DBMock) GetNewVideosCalls() []struct {
158 }
159 
160 // GetWatchedVideos calls GetWatchedVideosFunc.
161-func (mock *DBMock) GetWatchedVideos(ctx context.Context, sortDesc bool) ([]models.Video, error) {
162+func (mock *DBMock) GetWatchedVideos(ctx context.Context, sortDesc bool, limit int, offset int) ([]models.Video, error) {
163 	if mock.GetWatchedVideosFunc == nil {
164 		panic("DBMock.GetWatchedVideosFunc: method is nil but DB.GetWatchedVideos was just called")
165 	}
166 	callInfo := struct {
167 		Ctx      context.Context
168 		SortDesc bool
169+		Limit    int
170+		Offset   int
171 	}{
172 		Ctx:      ctx,
173 		SortDesc: sortDesc,
174+		Limit:    limit,
175+		Offset:   offset,
176 	}
177 	mock.lockGetWatchedVideos.Lock()
178 	mock.calls.GetWatchedVideos = append(mock.calls.GetWatchedVideos, callInfo)
179 	mock.lockGetWatchedVideos.Unlock()
180-	return mock.GetWatchedVideosFunc(ctx, sortDesc)
181+	return mock.GetWatchedVideosFunc(ctx, sortDesc, limit, offset)
182 }
183 
184 // GetWatchedVideosCalls gets all the calls that were made to GetWatchedVideos.
185@@ -264,10 +272,14 @@ func (mock *DBMock) GetWatchedVideos(ctx context.Context, sortDesc bool) ([]mode
186 func (mock *DBMock) GetWatchedVideosCalls() []struct {
187 	Ctx      context.Context
188 	SortDesc bool
189+	Limit    int
190+	Offset   int
191 } {
192 	var calls []struct {
193 		Ctx      context.Context
194 		SortDesc bool
195+		Limit    int
196+		Offset   int
197 	}
198 	mock.lockGetWatchedVideos.RLock()
199 	calls = mock.calls.GetWatchedVideos
200diff --git a/pages/watched_videos.templ b/pages/watched_videos.templ
201index 60e667580608834f8437cc67a3788b5afd255ea9..963c4b29f8107b0785e7087b5d2383d6bf53ecc4 100644
202--- a/pages/watched_videos.templ
203+++ b/pages/watched_videos.templ
204@@ -35,7 +35,7 @@ templ watchedVideoCard(video models.Video) {
205 	</div>
206 }
207 
208-templ WatchedVideosPage(videos []models.Video) {
209+templ WatchedVideosPage(videos []models.Video, currentPage int) {
210 	@BaseLayout("ytrssil - Watched Videos", "watched") {
211 		<div class="row">
212 			for _, video := range videos {
213@@ -44,5 +44,28 @@ templ WatchedVideosPage(videos []models.Video) {
214 				</div>
215 			}
216 		</div>
217+		<div class="row mt-4 mb-4">
218+			<div class="col-12 d-flex justify-content-center align-items-center gap-3">
219+				if currentPage > 1 {
220+					<a href={ templ.SafeURL(fmt.Sprintf("/watched?page=%d", currentPage-1)) } class="btn btn-primary">
221+						<i class="bi bi-arrow-left"></i> Previous
222+					</a>
223+				} else {
224+					<button class="btn btn-primary" disabled>
225+						<i class="bi bi-arrow-left"></i> Previous
226+					</button>
227+				}
228+				<span class="text-light">Page { fmt.Sprintf("%d", currentPage) }</span>
229+				if len(videos) == 100 {
230+					<a href={ templ.SafeURL(fmt.Sprintf("/watched?page=%d", currentPage+1)) } class="btn btn-primary">
231+						Next <i class="bi bi-arrow-right"></i>
232+					</a>
233+				} else {
234+					<button class="btn btn-primary" disabled>
235+						Next <i class="bi bi-arrow-right"></i>
236+					</button>
237+				}
238+			</div>
239+		</div>
240 	}
241 }
242diff --git a/pages/watched_videos_templ.go b/pages/watched_videos_templ.go
243index a5c5c218b81f9e5a5551d5436778b19ae1a28b24..322d8f923f7f9fdc3f23c392e650e47bb2148a6e 100644
244--- a/pages/watched_videos_templ.go
245+++ b/pages/watched_videos_templ.go
246@@ -116,7 +116,7 @@ func watchedVideoCard(video models.Video) templ.Component {
247 	})
248 }
249 
250-func WatchedVideosPage(videos []models.Video) templ.Component {
251+func WatchedVideosPage(videos []models.Video, currentPage int) templ.Component {
252 	return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
253 		templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
254 		if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
255@@ -193,7 +193,76 @@ func WatchedVideosPage(videos []models.Video) templ.Component {
256 					return templ_7745c5c3_Err
257 				}
258 			}
259-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div>")
260+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div><div class=\"row mt-4 mb-4\"><div class=\"col-12 d-flex justify-content-center align-items-center gap-3\">")
261+			if templ_7745c5c3_Err != nil {
262+				return templ_7745c5c3_Err
263+			}
264+			if currentPage > 1 {
265+				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<a href=\"")
266+				if templ_7745c5c3_Err != nil {
267+					return templ_7745c5c3_Err
268+				}
269+				var templ_7745c5c3_Var11 templ.SafeURL
270+				templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/watched?page=%d", currentPage-1)))
271+				if templ_7745c5c3_Err != nil {
272+					return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/watched_videos.templ`, Line: 50, Col: 76}
273+				}
274+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
275+				if templ_7745c5c3_Err != nil {
276+					return templ_7745c5c3_Err
277+				}
278+				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" class=\"btn btn-primary\"><i class=\"bi bi-arrow-left\"></i> Previous</a> ")
279+				if templ_7745c5c3_Err != nil {
280+					return templ_7745c5c3_Err
281+				}
282+			} else {
283+				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<button class=\"btn btn-primary\" disabled><i class=\"bi bi-arrow-left\"></i> Previous</button> ")
284+				if templ_7745c5c3_Err != nil {
285+					return templ_7745c5c3_Err
286+				}
287+			}
288+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<span class=\"text-light\">Page ")
289+			if templ_7745c5c3_Err != nil {
290+				return templ_7745c5c3_Err
291+			}
292+			var templ_7745c5c3_Var12 string
293+			templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", currentPage))
294+			if templ_7745c5c3_Err != nil {
295+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/watched_videos.templ`, Line: 58, Col: 66}
296+			}
297+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
298+			if templ_7745c5c3_Err != nil {
299+				return templ_7745c5c3_Err
300+			}
301+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</span> ")
302+			if templ_7745c5c3_Err != nil {
303+				return templ_7745c5c3_Err
304+			}
305+			if len(videos) == 100 {
306+				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<a href=\"")
307+				if templ_7745c5c3_Err != nil {
308+					return templ_7745c5c3_Err
309+				}
310+				var templ_7745c5c3_Var13 templ.SafeURL
311+				templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/watched?page=%d", currentPage+1)))
312+				if templ_7745c5c3_Err != nil {
313+					return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/watched_videos.templ`, Line: 60, Col: 76}
314+				}
315+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
316+				if templ_7745c5c3_Err != nil {
317+					return templ_7745c5c3_Err
318+				}
319+				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" class=\"btn btn-primary\">Next <i class=\"bi bi-arrow-right\"></i></a>")
320+				if templ_7745c5c3_Err != nil {
321+					return templ_7745c5c3_Err
322+				}
323+			} else {
324+				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<button class=\"btn btn-primary\" disabled>Next <i class=\"bi bi-arrow-right\"></i></button>")
325+				if templ_7745c5c3_Err != nil {
326+					return templ_7745c5c3_Err
327+				}
328+			}
329+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div></div>")
330 			if templ_7745c5c3_Err != nil {
331 				return templ_7745c5c3_Err
332 			}