4159d32f858c18db2bd7b7d2cc2df6a801af1ddc
- Author
- TheEdgeOfClaw <openclaw@theedgeofrage.com>
- Committer
- TheEdgeOfRage <git@theedgeofrage.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/assets/index.js b/assets/index.js
2index aed890cdaf123fb44a475a9614133521bd10639e..2425c7604404a1fcdd58afa2556f157146d00ffb 100644
3--- a/assets/index.js
4+++ b/assets/index.js
5@@ -2,14 +2,6 @@ document.addEventListener("DOMContentLoaded", function() {
6 const searchInput = document.getElementById("video-search");
7 if (!searchInput) return;
8
9- document.querySelectorAll('button[data-url]').forEach(function(btn) {
10- btn.addEventListener('click', function() {
11- const url = this.dataset.url;
12- const filename = this.dataset.filename;
13- downloadVideo(url, filename);
14- });
15- });
16-
17 function performSearch() {
18 const videoCards = Array.from(document.querySelectorAll(".video-card"));
19 const query = searchInput.value.trim();
20@@ -46,6 +38,36 @@ document.addEventListener("DOMContentLoaded", function() {
21 }
22 });
23
24+document.addEventListener('click', function(e) {
25+ const btn = e.target.closest('button[data-url]');
26+ if (!btn) return;
27+ downloadVideo(btn.dataset.url, btn.dataset.filename);
28+});
29+
30+var currentVideoID = '';
31+
32+function openResolutionModal(button) {
33+ currentVideoID = button.dataset.videoId;
34+ document.getElementById('resolution-modal-title').textContent = button.dataset.videoTitle;
35+ new bootstrap.Modal(document.getElementById('resolution-modal')).show();
36+}
37+
38+document.addEventListener('click', function(e) {
39+ const btn = e.target.closest('.resolution-btn');
40+ if (!btn || !currentVideoID) return;
41+ const formData = new FormData();
42+ formData.append('format', btn.dataset.height);
43+ fetch('/videos/' + currentVideoID + '/download', {
44+ method: 'POST',
45+ body: formData,
46+ }).then(function(resp) {
47+ if (resp.ok) {
48+ bootstrap.Modal.getInstance(document.getElementById('resolution-modal')).hide();
49+ location.reload();
50+ }
51+ });
52+});
53+
54 function addVideoHandler(event) {
55 if (event.detail.successful) {
56 bootstrap.Modal.getInstance(
57diff --git a/go.mod b/go.mod
58index b3efde3cd256dac5e866f7b5832c6f69d1aaf994..c31dd57bdce235b2e3cfce9c752a744fd5c3cff7 100644
59--- a/go.mod
60+++ b/go.mod
61@@ -3,7 +3,7 @@ module github.com/TheEdgeOfRage/ytrssil-api
62 go 1.25
63
64 require (
65- github.com/a-h/templ v0.3.977
66+ github.com/a-h/templ v0.3.1001
67 github.com/dustin/go-humanize v1.0.1
68 github.com/gin-gonic/gin v1.11.0
69 github.com/jackc/pgx/v5 v5.8.0
70diff --git a/go.sum b/go.sum
71index a9cad87ad8beafede3c72d577107bed1bc87ee92..963daa9a46793316c2d32d7a6fb090a5d31f0e50 100644
72--- a/go.sum
73+++ b/go.sum
74@@ -1,5 +1,5 @@
75-github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg=
76-github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
77+github.com/a-h/templ v0.3.1001 h1:yHDTgexACdJttyiyamcTHXr2QkIeVF1MukLy44EAhMY=
78+github.com/a-h/templ v0.3.1001/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
79 github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
80 github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
81 github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
82diff --git a/handler/downloads.go b/handler/downloads.go
83index 411740972e9e8e1a328836e991884eba1fe2a419..7cf2cedc77c427b74f0ba21a752fdd0f97c2fb5b 100644
84--- a/handler/downloads.go
85+++ b/handler/downloads.go
86@@ -22,7 +22,7 @@ func sanitizeFilename(title string) string {
87 return title
88 }
89
90-func (h *handler) DownloadVideo(ctx context.Context, videoID string) error {
91+func (h *handler) DownloadVideo(ctx context.Context, videoID string, resolution string) error {
92 exists, err := h.db.HasVideo(ctx, videoID)
93 if err != nil {
94 return fmt.Errorf("failed to check video existence: %w", err)
95@@ -35,12 +35,12 @@ func (h *handler) DownloadVideo(ctx context.Context, videoID string) error {
96 return fmt.Errorf("failed to set download status: %w", err)
97 }
98
99- go h.performDownload(videoID)
100+ go h.performDownload(videoID, resolution)
101
102 return nil
103 }
104
105-func (h *handler) performDownload(videoID string) {
106+func (h *handler) performDownload(videoID string, resolution string) {
107 ctx := context.Background()
108
109 video, err := h.db.GetVideo(ctx, videoID)
110@@ -57,9 +57,9 @@ func (h *handler) performDownload(videoID string) {
111 return
112 }
113
114- h.log.Info("Starting video download", "video_id", videoID, "title", video.Title)
115+ h.log.Info("Starting video download", "video_id", videoID, "title", video.Title, "resolution", resolution)
116
117- filePath, err := h.downloader.Download(ctx, videoID, video.Title, h.config.DownloadsDir)
118+ filePath, err := h.downloader.Download(ctx, videoID, video.Title, h.config.DownloadsDir, resolution)
119 if err != nil {
120 h.log.Error("Video download failed", "video_id", videoID, "error", err)
121 if dbErr := h.db.SetVideoDownloadFailed(ctx, videoID, err.Error()); dbErr != nil {
122diff --git a/handler/handler.go b/handler/handler.go
123index 10fcef86e3011578b308a065fe36b23bcdea6248..3989f8ab85847a1f55c168810ef89c86076dd72f 100644
124--- a/handler/handler.go
125+++ b/handler/handler.go
126@@ -25,7 +25,7 @@ type Handler interface {
127 MarkVideoAsUnwatched(ctx context.Context, videoID string) error
128 SetVideoProgress(ctx context.Context, videoID string, progressTime string) (*models.Video, error)
129 AddCustomVideo(ctx context.Context, videoID string) error
130- DownloadVideo(ctx context.Context, videoID string) error
131+ DownloadVideo(ctx context.Context, videoID string, resolution string) error
132 ServeVideoFile(ctx context.Context, videoID string) (filePath string, filename string, err error)
133 CleanupRoutine(ctx context.Context)
134 }
135diff --git a/httpserver/ytrssil/server.go b/httpserver/ytrssil/server.go
136index 23bb7d705210b7da8ce50916c21dd1193937665f..b10bca04f900a1dee3dccdc7bd1e1ff8981ef7cb 100644
137--- a/httpserver/ytrssil/server.go
138+++ b/httpserver/ytrssil/server.go
139@@ -97,6 +97,7 @@ func SetupGinRouter(
140 api.GET("videos/watched", srv.GetWatchedVideosJSON)
141 api.POST("videos/:video_id/watch", srv.MarkVideoAsWatchedJSON)
142 api.POST("videos/:video_id/unwatch", srv.MarkVideoAsUnwatchedJSON)
143+ api.POST("videos/:video_id/download", srv.DownloadVideoJSON)
144 }
145
146 return engine, nil
147diff --git a/httpserver/ytrssil/videos.go b/httpserver/ytrssil/videos.go
148index c81f088c8ca7017f434c9f9abccf781a159a2036..17eae03180d365697b73e75a1dbeb461fd3ebb9d 100644
149--- a/httpserver/ytrssil/videos.go
150+++ b/httpserver/ytrssil/videos.go
151@@ -55,3 +55,16 @@ func (srv *server) MarkVideoAsUnwatchedJSON(c *gin.Context) {
152
153 c.JSON(http.StatusOK, gin.H{"msg": "cleared video from watch history"})
154 }
155+
156+func (srv *server) DownloadVideoJSON(c *gin.Context) {
157+ videoID := c.Param("video_id")
158+ resolution := c.PostForm("format")
159+
160+ err := srv.handler.DownloadVideo(c.Request.Context(), videoID, resolution)
161+ if err != nil {
162+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
163+ return
164+ }
165+
166+ c.JSON(http.StatusOK, gin.H{"msg": "download started"})
167+}
168diff --git a/httpserver/ytrssil/videos_pages.go b/httpserver/ytrssil/videos_pages.go
169index 03f3f6a6f7fef321860dfc295a5906fe082114a1..4926aa32540247bbc9fef93ef310480f782afe82 100644
170--- a/httpserver/ytrssil/videos_pages.go
171+++ b/httpserver/ytrssil/videos_pages.go
172@@ -101,8 +101,9 @@ func (srv server) AddVideoPage(c *gin.Context) {
173
174 func (srv server) DownloadVideoPage(c *gin.Context) {
175 videoID := c.Param("video_id")
176+ resolution := c.PostForm("format")
177
178- err := srv.handler.DownloadVideo(c.Request.Context(), videoID)
179+ err := srv.handler.DownloadVideo(c.Request.Context(), videoID, resolution)
180 if err != nil {
181 returnErr(c, http.StatusInternalServerError, err)
182 return
183diff --git a/lib/downloader/downloader.go b/lib/downloader/downloader.go
184index ef1f4e8f5e3d182fca8686078d8ab44027449d43..50be9a0e79056e9fc4aa9fcfac5921d69525f34c 100644
185--- a/lib/downloader/downloader.go
186+++ b/lib/downloader/downloader.go
187@@ -8,10 +8,10 @@ import (
188 "path/filepath"
189 )
190
191-const ytDlpFormat = "bestvideo[height<=1080][vcodec^=vp9]+bestaudio/bestvideo[height<=1080][vcodec^=av01]+bestaudio/bestvideo[height<=1080]+bestaudio/best[height<=1080]/best" // nolint:lll
192+const defaultResolution = "1080"
193
194 type Downloader interface {
195- Download(ctx context.Context, videoID string, title string, outputDir string) (string, error)
196+ Download(ctx context.Context, videoID string, title string, outputDir string, resolution string) (string, error)
197 ValidateInstallation() error
198 }
199
200@@ -31,17 +31,28 @@ func (d *ytdlpDownloader) ValidateInstallation() error {
201 return nil
202 }
203
204+func formatSelector(resolution string) string {
205+ return fmt.Sprintf(
206+ "bestvideo[height<=%s]+bestaudio/best[height<=%s]",
207+ resolution, resolution,
208+ )
209+}
210+
211 func (d *ytdlpDownloader) Download(
212 ctx context.Context,
213 videoID string,
214 title string,
215 outputDir string,
216+ resolution string,
217 ) (string, error) {
218+ if resolution == "" {
219+ resolution = defaultResolution
220+ }
221 outputTemplate := filepath.Join(outputDir, fmt.Sprintf("%s.%%(ext)s", videoID))
222
223 cmd := exec.CommandContext(ctx,
224 "yt-dlp",
225- "--format", ytDlpFormat,
226+ "--format", formatSelector(resolution),
227 "--output", outputTemplate,
228 "--no-playlist",
229 "--no-warnings",
230diff --git a/pages/main.templ b/pages/main.templ
231index 43d062efd53549bbd5e341ebeb4efcb9bef4fe43..2d676d1aed1c08ae2266073438c917d7bf7c3e89 100644
232--- a/pages/main.templ
233+++ b/pages/main.templ
234@@ -28,6 +28,7 @@ templ BaseLayout(title string, route string) {
235 <body>
236 @subscriptionModal()
237 @addVideoModal()
238+ @resolutionModal()
239 @navbar(route)
240 <div class="container-fluid">
241 { children... }
242diff --git a/pages/main_templ.go b/pages/main_templ.go
243index 7261b696e0843d0c37b8e59929ef89de592013d3..2443fcbb99560ed893f80092bbbbd9a2df64581b 100644
244--- a/pages/main_templ.go
245+++ b/pages/main_templ.go
246@@ -54,6 +54,10 @@ func BaseLayout(title string, route string) templ.Component {
247 if templ_7745c5c3_Err != nil {
248 return templ_7745c5c3_Err
249 }
250+ templ_7745c5c3_Err = resolutionModal().Render(ctx, templ_7745c5c3_Buffer)
251+ if templ_7745c5c3_Err != nil {
252+ return templ_7745c5c3_Err
253+ }
254 templ_7745c5c3_Err = navbar(route).Render(ctx, templ_7745c5c3_Buffer)
255 if templ_7745c5c3_Err != nil {
256 return templ_7745c5c3_Err
257diff --git a/pages/new_videos.templ b/pages/new_videos.templ
258index 438644480aba067f868b5fa721d7dd3a99816831..b15bc9194983eb53fd3fef6bf13620f1dae8b15b 100644
259--- a/pages/new_videos.templ
260+++ b/pages/new_videos.templ
261@@ -109,22 +109,24 @@ templ VideoCard(video models.Video) {
262 <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
263 </button>
264 } else if video.DownloadFailed() {
265- <button
266- class="btn btn-danger"
267- title={ fmt.Sprintf("Download failed: %s", *video.DownloadError) }
268- hx-post={ fmt.Sprintf("/videos/%s/download", video.ID) }
269- hx-target="closest .video-card"
270- hx-swap="outerHTML"
271- >
272+ <button
273+ type="button"
274+ class="btn btn-danger"
275+ title={ fmt.Sprintf("Download failed: %s", *video.DownloadError) }
276+ data-video-id={ video.ID }
277+ data-video-title={ video.Title }
278+ onclick="openResolutionModal(this)"
279+ >
280 <i class="bi bi-exclamation-triangle"></i>
281 </button>
282 } else {
283- <button
284- hx-post={ fmt.Sprintf("/videos/%s/download", video.ID) }
285- hx-target="closest .video-card"
286- hx-swap="outerHTML"
287- class="btn btn-outline-primary"
288- >
289+ <button
290+ type="button"
291+ class="btn btn-outline-primary"
292+ data-video-id={ video.ID }
293+ data-video-title={ video.Title }
294+ onclick="openResolutionModal(this)"
295+ >
296 <i class="bi bi-download"></i>
297 </button>
298 }
299diff --git a/pages/new_videos_templ.go b/pages/new_videos_templ.go
300index 9877f02faa5663c91cacb52285cf3e90e2347f9f..44c8563dece41368e291f71d6f930aa376a767ed 100644
301--- a/pages/new_videos_templ.go
302+++ b/pages/new_videos_templ.go
303@@ -412,69 +412,95 @@ func VideoCard(video models.Video) templ.Component {
304 return templ_7745c5c3_Err
305 }
306 } else if video.DownloadFailed() {
307- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<button class=\"btn btn-danger\" title=\"")
308+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<button type=\"button\" class=\"btn btn-danger\" title=\"")
309 if templ_7745c5c3_Err != nil {
310 return templ_7745c5c3_Err
311 }
312 var templ_7745c5c3_Var24 string
313 templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Download failed: %s", *video.DownloadError))
314 if templ_7745c5c3_Err != nil {
315- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/new_videos.templ`, Line: 114, Col: 72}
316+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/new_videos.templ`, Line: 115, Col: 71}
317 }
318 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
319 if templ_7745c5c3_Err != nil {
320 return templ_7745c5c3_Err
321 }
322- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\" hx-post=\"")
323+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\" data-video-id=\"")
324 if templ_7745c5c3_Err != nil {
325 return templ_7745c5c3_Err
326 }
327 var templ_7745c5c3_Var25 string
328- templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/videos/%s/download", video.ID))
329+ templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(video.ID)
330 if templ_7745c5c3_Err != nil {
331- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/new_videos.templ`, Line: 115, Col: 62}
332+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/new_videos.templ`, Line: 116, Col: 31}
333 }
334 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
335 if templ_7745c5c3_Err != nil {
336 return templ_7745c5c3_Err
337 }
338- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" hx-target=\"closest .video-card\" hx-swap=\"outerHTML\"><i class=\"bi bi-exclamation-triangle\"></i></button> ")
339+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" data-video-title=\"")
340+ if templ_7745c5c3_Err != nil {
341+ return templ_7745c5c3_Err
342+ }
343+ var templ_7745c5c3_Var26 string
344+ templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(video.Title)
345+ if templ_7745c5c3_Err != nil {
346+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/new_videos.templ`, Line: 117, Col: 37}
347+ }
348+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
349+ if templ_7745c5c3_Err != nil {
350+ return templ_7745c5c3_Err
351+ }
352+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\" onclick=\"openResolutionModal(this)\"><i class=\"bi bi-exclamation-triangle\"></i></button> ")
353 if templ_7745c5c3_Err != nil {
354 return templ_7745c5c3_Err
355 }
356 } else {
357- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<button hx-post=\"")
358+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<button type=\"button\" class=\"btn btn-outline-primary\" data-video-id=\"")
359 if templ_7745c5c3_Err != nil {
360 return templ_7745c5c3_Err
361 }
362- var templ_7745c5c3_Var26 string
363- templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/videos/%s/download", video.ID))
364+ var templ_7745c5c3_Var27 string
365+ templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(video.ID)
366 if templ_7745c5c3_Err != nil {
367- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/new_videos.templ`, Line: 123, Col: 62}
368+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/new_videos.templ`, Line: 126, Col: 31}
369 }
370- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
371+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
372+ if templ_7745c5c3_Err != nil {
373+ return templ_7745c5c3_Err
374+ }
375+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\" data-video-title=\"")
376+ if templ_7745c5c3_Err != nil {
377+ return templ_7745c5c3_Err
378+ }
379+ var templ_7745c5c3_Var28 string
380+ templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(video.Title)
381+ if templ_7745c5c3_Err != nil {
382+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/new_videos.templ`, Line: 127, Col: 37}
383+ }
384+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
385 if templ_7745c5c3_Err != nil {
386 return templ_7745c5c3_Err
387 }
388- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\" hx-target=\"closest .video-card\" hx-swap=\"outerHTML\" class=\"btn btn-outline-primary\"><i class=\"bi bi-download\"></i></button> ")
389+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\" onclick=\"openResolutionModal(this)\"><i class=\"bi bi-download\"></i></button> ")
390 if templ_7745c5c3_Err != nil {
391 return templ_7745c5c3_Err
392 }
393 }
394- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<button hx-patch=\"")
395+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<button hx-patch=\"")
396 if templ_7745c5c3_Err != nil {
397 return templ_7745c5c3_Err
398 }
399- var templ_7745c5c3_Var27 string
400- templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/videos/%s/watch", video.ID))
401+ var templ_7745c5c3_Var29 string
402+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/videos/%s/watch", video.ID))
403diff --git a/pages/resolution_modal.templ b/pages/resolution_modal.templ
404new file mode 100644
405index 0000000000000000000000000000000000000000..69e155d089660dd7f6655f91436c822407e11017
406--- /dev/null
407+++ b/pages/resolution_modal.templ
408@@ -0,0 +1,24 @@
409+package pages
410+
411+templ resolutionModal() {
412+ <div class="modal fade" id="resolution-modal" tabindex="-1">
413+ <div class="modal-dialog">
414+ <div class="modal-content">
415+ <div class="modal-header">
416+ <h1 class="modal-title fs-5">Select Resolution</h1>
417+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
418+ </div>
419+ <div class="modal-body">
420+ <p id="resolution-modal-title" class="mb-3"></p>
421+ <div class="d-flex flex-column gap-2">
422+ <button type="button" class="btn btn-outline-primary w-100 resolution-btn" data-height="2160">2160p</button>
423+ <button type="button" class="btn btn-outline-primary w-100 resolution-btn" data-height="1440">1440p</button>
424+ <button type="button" class="btn btn-outline-primary w-100 resolution-btn" data-height="1080">1080p</button>
425+ <button type="button" class="btn btn-outline-primary w-100 resolution-btn" data-height="720">720p</button>
426+ <button type="button" class="btn btn-outline-primary w-100 resolution-btn" data-height="480">480p</button>
427+ </div>
428+ </div>
429+ </div>
430+ </div>
431+ </div>
432+}
433diff --git a/pages/resolution_modal_templ.go b/pages/resolution_modal_templ.go
434new file mode 100644
435index 0000000000000000000000000000000000000000..de33504a0de42376c51695e125a13c55960cc16c
436--- /dev/null
437+++ b/pages/resolution_modal_templ.go
438@@ -0,0 +1,40 @@
439+// Code generated by templ - DO NOT EDIT.
440+
441+// templ: version: v0.3.1001
442+package pages
443+
444+//lint:file-ignore SA4006 This context is only used if a nested component is present.
445+
446+import "github.com/a-h/templ"
447+import templruntime "github.com/a-h/templ/runtime"
448+
449+func resolutionModal() templ.Component {
450+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
451+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
452+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
453+ return templ_7745c5c3_CtxErr
454+ }
455+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
456+ if !templ_7745c5c3_IsBuffer {
457+ defer func() {
458+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
459+ if templ_7745c5c3_Err == nil {
460+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
461+ }
462+ }()
463+ }
464+ ctx = templ.InitializeContext(ctx)
465+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
466+ if templ_7745c5c3_Var1 == nil {
467+ templ_7745c5c3_Var1 = templ.NopComponent
468+ }
469+ ctx = templ.ClearChildren(ctx)