selfhostable, read-only reddit client

Feat: Consolidate/port Card and Compact media viewers

Changed files
+69 -94
src
mixins
public
+37 -56
src/mixins/post.pug
··· 28 28 |  ·  29 29 a(href=`/comments/${p.id}?from=${from}&sort=${sortQuery}&view=${viewQuery}`) #{fmtnum (p.num_comments)} ↩ 30 30 if (query.view == "card" && !isPostGallery(p) && !isPostImage(p) && !isPostVideo(p) && p.selftext_html) 31 - div.self-text-overflow(class='card') 32 - if query.view == "card" && (p.spoiler || p.over_18) 31 + div.self-text-overflow.card 32 + if p.spoiler || p.over_18 33 33 div.spoiler(id=`spoiler_${p.id}`, onclick=`javascript:document.getElementById('spoiler_${p.id}').style.display = 'none';`) 34 34 h2 35 35 != p.over_18 ? 'nsfw' : 'spoiler' 36 - div.self-text(class='card') 36 + div.self-text.card 37 37 != convertInlineImageLinks(p.selftext_html) 38 - div.media-preview(class=`${query.view}`) 39 - - var onclick = query.view != "card" ? `toggleDetails('${p.id}')` : `` 38 + if query.view != "card" 39 + div.media-preview 40 + - var onclick = `toggleDetails('${p.id}')` 41 + if isPostGallery(p) 42 + - var item = (p.over_18 ? `/nsfw.svg` : p.spoiler ? `/spoiler.svg` : postGalleryItems(p)[0].url) 43 + img(src=item onclick=onclick) 44 + else if isPostImage(p) 45 + - var url = postThumbnail(p) 46 + img(src=url onclick=onclick) 47 + else if isPostVideo(p) 48 + - var decodedVideos = decodePostVideoUrls(p) 49 + video(autoplay="" muted="" data-dashjs-player="" onclick=`toggleDetails('${p.id}')` poster=decodedVideos[4] width="100px" height="100px") 50 + // Scrubber 51 + source(src=decodedVideos[3]) 52 + else if isPostLink(p) 53 + a(href=p.url) 54 + | ↗ 55 + 56 + details(id=`${p.id}` open=(query.view == "card" && (isPostGallery(p) || isPostImage(p) || isPostVideo(p)))) 57 + summary.expand-post expand media 58 + div.image-viewer 40 59 if query.view == "card" && (p.spoiler || p.over_18) && (isPostGallery(p) || isPostImage(p) || isPostVideo(p)) 41 60 div.spoiler(id=`spoiler_${p.id}`, onclick=`javascript:document.getElementById('spoiler_${p.id}').style.display = 'none';`) 42 61 h2 43 62 != p.over_18 ? 'nsfw' : 'spoiler' 44 63 if isPostGallery(p) 45 - - var item = postGalleryItems(p)[0] 46 - if query.view == "card" 47 - div.gallery(class=`${query.view}`) 48 - each item in postGalleryItems(p) 49 - div.gallery-item(class=`${query.view}`) 50 - a(href=`/media/${item.url}`) 51 - img(src=item.url loading="lazy") 52 - div.gallery-item-idx(class=`${query.view}`) 53 - | #{`${item.idx}/${item.total}`} 54 - else 55 - img(src=item.url onclick=onclick) 64 + div.gallery 65 + each item in postGalleryItems(p) 66 + div.gallery-item 67 + a(href=`/media/${item.url}`) 68 + img(src=item.url loading="lazy") 69 + div.gallery-item-idx 70 + | #{`${item.idx}/${item.total}`} 56 71 else if isPostImage(p) 57 - - var url = query.view == "card" ? p.url : postThumbnail(p) 58 - #{query.view == "card" ? "a href=/media/" + url : span} 59 - img(src=url onclick=onclick) 72 + a(href=`/media/${p.url}`) 73 + img(src=p.url loading="lazy") 60 74 else if isPostVideo(p) 61 75 - var decodedVideos = decodePostVideoUrls(p) 62 - if query.view == "card" 63 - video(controls="" muted="" data-dashjs-player="" preload="metadata" poster=decodedVideos[4]) 76 + video(controls="" muted="" data-dashjs-player="" preload="metadata" playsinline="" poster=decodedVideos[4] objectfit="contain" loading="lazy") 64 77 // HLS 65 78 source(src=decodedVideos[0]) 66 79 // Dash 67 80 source(src=decodedVideos[1]) 68 81 // Fallback 69 82 source(src=decodedVideos[2]) 70 - else 71 - video(autoplay="" muted="" data-dashjs-player="" onclick=`toggleDetails('${p.id}')` width="100px" height="100px") 72 - // Scrubber 73 - source(src=decodedVideos[3]) 74 - else if isPostLink(p) 75 - a(href=p.url) 76 - if (query.view == 'card') 77 - | #{p.domain} 78 - | ↗ 79 - 80 - if query.view == "compact" && (isPostGallery(p) || isPostImage(p) || isPostVideo(p)) 81 - details(id=`${p.id}`) 82 - summary.expand-post expand media 83 - div.image-viewer 84 - if isPostGallery(p) 85 - div.gallery 86 - each item in postGalleryItems(p) 87 - div.gallery-item 88 - div.gallery-item-idx 89 - | #{`${item.idx}/${item.total}`} 90 - a(href=`/media/${item.url}`) 91 - img(src=item.url loading="lazy") 92 - else if isPostImage(p) 93 - a(href=`/media/${p.url}`) 94 - img(src=p.url loading="lazy").post-media 95 - else if isPostVideo(p) 96 - video(controls="" muted="" data-dashjs-player="" preload="metadata" playsinline="" poster=decodedVideos[4] objectfit="contain" loading="lazy").post-media 97 - //HLS 98 - source(src=decodedVideos[0]) 99 - // Dash 100 - source(src=decodedVideos[1]) 101 - // Fallback 102 - source(src=decodedVideos[2]) 103 - button(onclick=`toggleDetails('${p.id}')`) 104 - | close 83 + if (query.view == "compact") 84 + button(onclick=`toggleDetails('${p.id}')`) 85 + | close
+32 -38
src/public/styles.css
··· 11 11 --link-visited-color: #999; 12 12 --accent: var(--link-color); 13 13 --error-text-color: red; 14 + --border-radius-card: 8px; 15 + --border-radius-media: 6px; 16 + --border-radius-preview: 4px; 14 17 15 18 font-family: Inter, sans-serif; 16 19 font-feature-settings: 'ss01' 1, 'kern' 1, 'liga' 1, 'cv05' 1, 'dlig' 1, 'ss01' 1, 'ss07' 1, 'ss08' 1; ··· 45 48 overflow-x: hidden; 46 49 background-color: var(--bg-color); 47 50 color: var(--text-color); 48 - } 49 - 50 - body:has(details.card[open]) { 51 - overflow: hidden; 52 51 } 53 52 54 53 body.media-maximized { ··· 169 168 170 169 .post-container.card { 171 170 border: 1px solid var(--bg-color-muted); 172 - border-radius: 8px; 171 + border-radius: var(--border-radius-card); 173 172 display: block; 174 173 } 175 174 ··· 198 197 text-overflow: ellipsis; 199 198 } 200 199 201 - .media-preview.card { 200 + .image-viewer { 202 201 position: relative; 203 202 padding: 0.3rem; 204 203 padding-bottom: 0.3rem; 205 204 } 206 205 207 - .media-preview.card > img { 206 + .image-viewer > img { 208 207 cursor: pointer; 209 208 } 210 209 211 - .gallery.card { 212 - align-items: center; 213 - scroll-snap-type: both mandatory; 214 - } 215 - 216 - .gallery-item.card { 217 - max-width: 100%; 218 - width: 100%; 219 - scroll-snap-align: center; 220 - } 221 - 222 - .gallery-item-idx.card { 223 - text-align: center; 224 - } 225 - 226 210 .spoiler { 227 211 background-color: rbga(var(--bg-color-muted), 0.2); 228 212 /* Safari on iOS <= 17 */ 229 213 -webkit-backdrop-filter: blur(3rem); 230 214 backdrop-filter: blur(3rem); 231 - border-radius: 4px; 215 + border-radius: var(--border-radius-preview); 232 216 233 217 position: absolute; 234 218 top: 0; ··· 247 231 z-index: 10; 248 232 } 249 233 250 - .gallery-item-idx.card, 234 + .gallery-item-idx, 251 235 .spoiler > h2 { 252 236 text-shadow: 0.1rem 0.1rem 1rem var(--bg-color-muted); 253 237 } ··· 294 278 object-fit: cover; 295 279 width: 4rem; 296 280 height: 4rem; 281 + border-radius: var(--border-radius-preview); 297 282 } 298 283 299 - .media-preview.card { 284 + .image-viewer { 300 285 padding: unset; 301 286 } 302 287 303 - .media-preview.card img, 304 - .media-preview.card video { 305 - border-radius: 6px; 288 + .image-viewer img, 289 + .image-viewer video { 290 + border-radius: var(--border-radius-media); 306 291 307 292 max-height: 40vh; 308 293 max-width: 95%; ··· 317 302 object-fit: fill; 318 303 } 319 304 320 - .media-preview.card a { 305 + .image-viewer a { 321 306 font-size: 1.5rem; 322 307 padding: unset; 323 308 padding-left: 1rem; 324 309 } 325 310 326 - .media-preview.card a:has(img) { 311 + .image-viewer a:has(img) { 327 312 font-size: 0rem; 328 313 padding: unset; 329 314 } ··· 382 367 width: 5rem; 383 368 height: 5rem; 384 369 } 385 - .media-preview.card img, 386 - .media-preview.card video 370 + .image-viewer img, 371 + .image-viewer video 387 372 { 388 373 max-height: 50vh; 389 374 } 390 - .media-preview.card a { 375 + .image-viewer a { 391 376 font-size: 1rem; 392 377 margin: 0.7rem; 393 378 padding: initial; ··· 424 409 width: 5rem; 425 410 height: 5rem; 426 411 } 427 - .media-preview.card img, 428 - .media-preview.card video 412 + .image-viewer img, 413 + .image-viewer video 429 414 { 430 415 max-height: 30vh; 431 416 } ··· 433 418 font-size: 2rem; 434 419 padding: 2rem; 435 420 } 436 - .media-preview.card a { 421 + .image-viewer a { 437 422 font-size: 1rem; 438 423 margin: 0.5rem; 439 424 padding: initial; ··· 464 449 flex: 1 1 40%; 465 450 width: 40%; 466 451 } 467 - .media-preview.card img, 468 - .media-preview.card video 452 + .image-viewer img, 453 + .image-viewer video 469 454 { 470 455 max-height: 20vh; 471 456 } ··· 648 633 overflow-x: auto; 649 634 position: relative; 650 635 padding: 5px; 636 + align-items: center; 637 + scroll-snap-type: both mandatory; 651 638 } 652 639 653 640 .gallery-item { 654 641 flex: 0 0 auto; 655 642 margin-right: 10px; 643 + max-width: 100%; 644 + width: 100%; 645 + scroll-snap-align: center; 646 + } 647 + 648 + .gallery-item-idx { 649 + text-align: center; 656 650 } 657 651 658 652 .gallery img {