selfhostable, read-only reddit client

Merge pull request #18 from PortableProgrammer/feat-card-view

[Feat] Card View

authored by oppi.li and committed by GitHub 21eba43c 13951896

+2 -1
src/mixins/comment.pug
··· 1 1 include ../utils 2 + include postUtils 2 3 3 4 mixin infoContainer(data, next_id, prev_id) 4 5 - var hats = (data.is_submitter?['op']:[]).concat(data.distinguished=="moderator"?['mod']:[]) ··· 51 52 summary.expand-comments 52 53 +infoContainer(data, next_id, prev_id) 53 54 div.comment-body 54 - != data.body_html 55 + != convertInlineImageLinks(data.body_html) 55 56 if hasReplyData 56 57 div.replies 57 58 - var total = data.replies.data.children.length
+7 -5
src/mixins/header.pug
··· 1 1 mixin header(user) 2 + - var viewQuery = 'view=' + (query && query.view ? query.view : 'compact') 3 + - var sortQuery = 'sort=' + (query ? (query.sort ? query.sort + (query.t ? '&t=' + query.t : '') : 'hot') : 'hot') 2 4 div.header 3 5 div.header-item 4 - a(href=`/`) home 6 + a(href=`/?${sortQuery}&${viewQuery}`) home 5 7 div.header-item 6 - a(href=`/r/all`) all 8 + a(href=`/r/all?${sortQuery}&${viewQuery}`) all 7 9 div.header-item 8 - a(href=`/search`) search 10 + a(href=`/search?${sortQuery}&${viewQuery}`) search 9 11 div.header-item 10 - a(href=`/subs`) subs 12 + a(href=`/subs?${sortQuery}&${viewQuery}`) subs 11 13 if user 12 14 div.header-item 13 - a(href='/dashboard') #{user.username} 15 + a(href=`/dashboard?${sortQuery}&${viewQuery}`) #{user.username} 14 16 |  15 17 a(href='/logout') (logout) 16 18 else
+75 -42
src/mixins/post.pug
··· 2 2 include postUtils 3 3 mixin post(p, currentUrl) 4 4 - var from = encodeURIComponent(currentUrl) 5 - article(class=`post ${p.stickied?"sticky":""}`) 6 - div.post-container 7 - div.post-text 8 - div.title-container 9 - a(href=`/comments/${p.id}?from=${from}`) 5 + - var viewQuery = query && query.view ? query.view : 'compact' 6 + - var sortQuery = query && query.sort ? query.sort + (query.t ? '&t=' + query.t : '') : 'hot' 7 + article(class=`post`) 8 + div.post-container(class=`${query.view} ${p.stickied?"sticky":""}`) 9 + div.post-text(class=`${query.view}`) 10 + div.title-container(class=`${query.view}`) 11 + a(class=`${query.view}`, href=`/comments/${p.id}?from=${from}&sort=${sortQuery}&view=${viewQuery}`) 10 12 != p.title 11 13 span.domain (#{p.domain}) 12 14 div.info-container ··· 22 24 |  ·  23 25 | #{timeDifference(Date.now(), p.created * 1000)} 24 26 |  ·  25 - a(href=`/r/${p.subreddit}`) r/#{p.subreddit} 27 + a(href=`/r/${p.subreddit}?sort=${sortQuery}&view=${viewQuery}`) r/#{p.subreddit} 26 28 |  ·  27 - a(href=`/comments/${p.id}?from=${from}`) #{fmtnum (p.num_comments)} ↩ 28 - div.media-preview 29 + a(href=`/comments/${p.id}?from=${from}&sort=${sortQuery}&view=${viewQuery}`) #{fmtnum (p.num_comments)} ↩ 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) 33 + div.spoiler(id=`spoiler_${p.id}`, onclick=`javascript:document.getElementById('spoiler_${p.id}').style.display = 'none';`) 34 + h2 35 + != p.over_18 ? 'nsfw' : 'spoiler' 36 + div.self-text(class='card') 37 + != convertInlineImageLinks(p.selftext_html) 38 + div.media-preview(class=`${query.view}`) 39 + - var onclick = query.view != "card" ? `toggleDetails('${p.id}')` : `` 40 + if query.view == "card" && (p.spoiler || p.over_18) && (isPostGallery(p) || isPostImage(p) || isPostVideo(p)) 41 + div.spoiler(id=`spoiler_${p.id}`, onclick=`javascript:document.getElementById('spoiler_${p.id}').style.display = 'none';`) 42 + h2 43 + != p.over_18 ? 'nsfw' : 'spoiler' 29 44 if isPostGallery(p) 30 45 - var item = postGalleryItems(p)[0] 31 - img(src=item.url onclick=`toggleDetails('${p.id}')`) 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) 32 56 else if isPostImage(p) 33 - - var url = postThumbnail(p) 34 - img(src=url onclick=`toggleDetails('${p.id}')`) 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) 35 60 else if isPostVideo(p) 36 - - var url = p.secure_media.reddit_video.scrubber_media_url 37 - video(src=url data-dashjs-player width='100px' height='100px' onclick=`toggleDetails('${p.id}')`) 61 + - var decodedVideos = decodePostVideoUrls(p) 62 + if query.view == "card" 63 + video(controls="" muted="" data-dashjs-player="" preload="metadata" poster=decodedVideos[4]) 64 + // HLS 65 + source(src=decodedVideos[0]) 66 + // Dash 67 + source(src=decodedVideos[1]) 68 + // Fallback 69 + 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]) 38 74 else if isPostLink(p) 39 75 a(href=p.url) 76 + if (query.view == 'card') 77 + | #{p.domain} 40 78 | ↗ 41 79 42 - if isPostGallery(p) 43 - details(id=`${p.id}`) 44 - summary.expand-post expand gallery 45 - div.gallery 46 - each item in postGalleryItems(p) 47 - div.gallery-item 48 - div.gallery-item-idx 49 - | #{`${item.idx}/${item.total}`} 50 - a(href=`/media/${item.url}`) 51 - img(src=item.url loading="lazy") 52 - button(onclick=`toggleDetails('${p.id}')`) close 53 - else if isPostImage(p) 54 - details(id=`${p.id}`) 55 - summary.expand-post expand image 56 - a(href=`/media/${p.url}`) 57 - img(src=p.url loading="lazy").post-media 58 - button(onclick=`toggleDetails('${p.id}')`) close 59 - else if isPostVideo(p) 60 - details(id=`${p.id}`) 61 - summary.expand-post expand video 62 - - var url = p.secure_media.reddit_video.dash_url 63 - video(src=url controls data-dashjs-player loading="lazy").post-media 64 - button(onclick=`toggleDetails('${p.id}')`) close 65 - else if isPostLink(p) 80 + if query.view == "compact" && (isPostGallery(p) || isPostImage(p) || isPostVideo(p)) 66 81 details(id=`${p.id}`) 67 - summary.expand-post expand link 68 - a(href=`${p.url}`) 69 - | #{p.url} 70 - br 71 - button(onclick=`toggleDetails('${p.id}')`) close 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
+37 -6
src/mixins/postUtils.pug
··· 10 10 } 11 11 - 12 12 function postThumbnail(p) { 13 - if (p.thumbnail == "image" || p.thumbnail == "") { 14 - return p.url; 15 - } else if (p.over_18) { 16 - return "/nsfw.svg"; 13 + if (p.over_18) { 14 + return "/nsfw.svg"; 17 15 } else if (p.thumbnail == "spoiler") { 18 - return "/spoiler.svg"; 16 + return "/spoiler.svg"; 17 + } else if (p.thumbnail == "image" || p.thumbnail == "") { 18 + return p.url; 19 19 } else { 20 - return p.thumbnail; 20 + return p.thumbnail; 21 21 } 22 22 } 23 23 - ··· 51 51 return null; 52 52 } 53 53 } 54 + - 55 + function convertInlineImageLinks(html) { 56 + // Find all anchors that href to preview.redd.it, i.redd.it, i.imgur.com 57 + // and contain just a link to the same href 58 + const expression = /<a href="(http[s]?:\/\/(?:preview\.redd\.it|i\.redd\.it|i\.imgur\.com).*?)">\1?<\/a>/g; 59 + const matches = html.matchAll(expression); 60 + var result = html; 61 + matches.forEach((match) => { 62 + // Replace each occurrence with an actual img tag 63 + result = result.replace(match[0], '<a href="' + match[1] + '"><img class="inline" src="' + match[1] + '"></a>'); 64 + }) 65 + 66 + return result; 67 + } 68 + - 69 + function decodePostVideoUrls(p) { 70 + // Video URLs have querystring separators that are HTML-encoded, so replace them. 71 + const expression = /&amp;/g; 72 + 73 + var hls_url = p.secure_media && p.secure_media.reddit_video && p.secure_media.reddit_video.hls_url ? p.secure_media.reddit_video.hls_url.replace(expression, '&') : ''; 74 + 75 + var dash_url = p.secure_media && p.secure_media.reddit_video && p.secure_media.reddit_video.dash_url ? p.secure_media.reddit_video.dash_url.replace(expression, '&') : ''; 76 + 77 + var fallback_url = p.secure_media && p.secure_media.reddit_video && p.secure_media.reddit_video.fallback_url ? p.secure_media.reddit_video.fallback_url.replace(expression, '&') : ''; 78 + 79 + var scrubber_url = p.secure_media && p.secure_media.reddit_video && p.secure_media.reddit_video.scrubber_media_url ? p.secure_media.reddit_video.scrubber_media_url.replace(expression, '&') : ''; 80 + 81 + var poster_url = p.preview && p.preview.images ? p.preview.images[0].source.url.replace(expression, '&') : ''; 82 + 83 + return [hls_url, dash_url, fallback_url, scrubber_url, poster_url]; 84 + }
+210 -15
src/public/styles.css
··· 47 47 color: var(--text-color); 48 48 } 49 49 50 + body:has(details.card[open]) { 51 + overflow: hidden; 52 + } 53 + 54 + body.media-maximized { 55 + /* Fix for Safari User Agent stylesheet */ 56 + margin: 0; 57 + } 58 + 59 + body.media-maximized.zoom, 60 + div.media-maximized.container.zoom { 61 + overflow: auto; 62 + } 63 + 64 + img.media-maximized { 65 + cursor: zoom-in; 66 + } 67 + 68 + img.media-maximized.zoom { 69 + max-width: unset; 70 + max-height: unset; 71 + cursor: zoom-out; 72 + } 73 + 50 74 main { 51 75 display: flex; 52 76 flex-direction: column; ··· 58 82 .info-container a, 59 83 .comment-info-container a, 60 84 .sort-opts a, 85 + .view-opts a, 61 86 .more a 62 87 { 63 88 text-decoration: none; ··· 92 117 align-items: center; 93 118 } 94 119 95 - .sorting { 120 + .sorting, 121 + .viewing { 96 122 margin-top: 20px; 97 123 } 98 124 99 - .sort-opts { 125 + .sort-opts, 126 + .view-opts { 100 127 display: grid; 101 128 margin: 10px; 102 129 } 103 130 104 - .sort-opts { 131 + .sort-opts, 132 + .view-opts { 105 133 grid-template-columns: repeat(2, 1fr); 106 134 grid-template-rows: repeat(5, 1fr); 107 135 grid-auto-flow: column; 136 + } 137 + 138 + .view-opts { 139 + grid-template-rows: repeat(2, 1fr); 108 140 } 109 141 110 142 .footer { ··· 135 167 font-size: 0.9rem; 136 168 } 137 169 170 + .post-container.card { 171 + border: 1px solid var(--bg-color-muted); 172 + border-radius: 8px; 173 + display: block; 174 + } 175 + 176 + .post-text.card { 177 + padding: 0.9rem; 178 + padding-top: 0.3rem; 179 + } 180 + 181 + .self-text-overflow.card { 182 + /* For spoiler positioning */ 183 + position: relative; 184 + padding-top: 0.3rem; 185 + max-height: 10vh; 186 + overflow: hidden; 187 + overflow-wrap: break-word; 188 + display: block; 189 + } 190 + 191 + .self-text.card { 192 + overflow: hidden; 193 + display: -webkit-box; 194 + /* Safari on iOS <= 17 */ 195 + -webkit-box-orient: vertical; 196 + -webkit-line-clamp: 3; 197 + line-clamp: 3; 198 + text-overflow: ellipsis; 199 + } 200 + 201 + .media-preview.card { 202 + position: relative; 203 + padding: 0.3rem; 204 + padding-bottom: 0.3rem; 205 + } 206 + 207 + .media-preview.card > img { 208 + cursor: pointer; 209 + } 210 + 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 + .spoiler { 227 + background-color: rbga(var(--bg-color-muted), 0.2); 228 + /* Safari on iOS <= 17 */ 229 + -webkit-backdrop-filter: blur(3rem); 230 + backdrop-filter: blur(3rem); 231 + border-radius: 4px; 232 + 233 + position: absolute; 234 + top: 0; 235 + left: 0; 236 + 237 + box-sizing: border-box; 238 + display: flex; 239 + height: 100%; 240 + width: 100%; 241 + 242 + justify-content: center; 243 + align-items: center; 244 + 245 + cursor: pointer; 246 + 247 + z-index: 10; 248 + } 249 + 250 + .gallery-item-idx.card, 251 + .spoiler > h2 { 252 + text-shadow: 0.1rem 0.1rem 1rem var(--bg-color-muted); 253 + } 254 + 138 255 .comments-container { 139 256 font-size: 0.9rem; 140 257 } ··· 179 296 height: 4rem; 180 297 } 181 298 299 + .media-preview.card { 300 + padding: unset; 301 + } 302 + 303 + .media-preview.card img, 304 + .media-preview.card video { 305 + border-radius: 6px; 306 + 307 + max-height: 40vh; 308 + max-width: 95%; 309 + 310 + display: block; 311 + width: unset; 312 + height: unset; 313 + margin-left: auto; 314 + margin-right: auto; 315 + margin-bottom: 0.5rem; 316 + 317 + object-fit: fill; 318 + } 319 + 320 + .media-preview.card a { 321 + font-size: 1.5rem; 322 + padding: unset; 323 + padding-left: 1rem; 324 + } 325 + 326 + .media-preview.card a:has(img) { 327 + font-size: 0rem; 328 + padding: unset; 329 + } 330 + 182 331 .media-preview a { 183 332 font-size: 2rem; 184 333 text-decoration: none; 185 334 padding: 1rem; 186 335 } 187 336 188 - .media-maximized-container { 189 - display: flex; 190 - justify-content: center; 191 - align-items: center; 192 - width: 100vw; 193 - height: 100vh; 194 - overflow: hidden; 195 - } 196 - 197 337 .media-maximized { 198 338 max-width: 100vw; 199 339 max-height: 100vh; ··· 204 344 object-fit: contain; 205 345 } 206 346 347 + .media-maximized.container { 348 + display: flex; 349 + justify-content: center; 350 + align-items: center; 351 + width: 100vw; 352 + height: 100vh; 353 + overflow: hidden; 354 + } 355 + 207 356 .post-author { 208 357 display: none 209 358 } ··· 233 382 width: 5rem; 234 383 height: 5rem; 235 384 } 385 + .media-preview.card img, 386 + .media-preview.card video 387 + { 388 + max-height: 50vh; 389 + } 390 + .media-preview.card a { 391 + font-size: 1rem; 392 + margin: 0.7rem; 393 + padding: initial; 394 + } 395 + .self-text.card { 396 + -webkit-line-clamp: 4; 397 + line-clamp: 4; 398 + } 236 399 .post-author { 237 400 display: inline 238 401 } ··· 242 405 form { 243 406 width: 40%; 244 407 } 245 - .sort-opts { 408 + .sort-opts, 409 + .view-opts { 246 410 grid-template-columns: repeat(9, 1fr); 247 411 grid-template-rows: repeat(1, 1fr); 248 412 grid-auto-flow: row; ··· 259 423 { 260 424 width: 5rem; 261 425 height: 5rem; 426 + } 427 + .media-preview.card img, 428 + .media-preview.card video 429 + { 430 + max-height: 30vh; 262 431 } 263 432 .media-preview a { 264 433 font-size: 2rem; 265 434 padding: 2rem; 266 435 } 436 + .media-preview.card a { 437 + font-size: 1rem; 438 + margin: 0.5rem; 439 + padding: initial; 440 + } 441 + .self-text.card { 442 + -webkit-line-clamp: 6; 443 + line-clamp: 6; 444 + } 267 445 .post-author { 268 446 display: inline 269 447 } ··· 273 451 form { 274 452 width: 20%; 275 453 } 276 - .sort-opts { 454 + .sort-opts, 455 + .view-opts { 277 456 grid-template-columns: repeat(9, 1fr); 278 457 grid-template-rows: repeat(1, 1fr); 279 458 grid-auto-flow: row; ··· 285 464 flex: 1 1 40%; 286 465 width: 40%; 287 466 } 288 - .sort-opts { 467 + .media-preview.card img, 468 + .media-preview.card video 469 + { 470 + max-height: 20vh; 471 + } 472 + .sort-opts, 473 + .view-opts { 289 474 grid-template-columns: repeat(9, 1fr); 290 475 grid-template-rows: repeat(1, 1fr); 291 476 grid-auto-flow: row; ··· 353 538 text-decoration: none; 354 539 } 355 540 541 + .title-container.card > a { 542 + font-size: 1.125rem; 543 + font-weight: bold; 544 + } 545 + 356 546 .title-container > a:hover { 357 547 text-decoration: underline; 358 548 } ··· 363 553 364 554 .header a, 365 555 .sort-opts a, 556 + .view-opts a, 366 557 .sub-title a { 367 558 color: var(--text-color); 368 559 } ··· 630 821 border-radius: 2px; 631 822 border: 4px solid var(--sticky-color); 632 823 } 824 + 825 + .inline { 826 + max-width: 100%; 827 + }
+35 -8
src/routes/index.js
··· 16 16 const subs = db 17 17 .query("SELECT * FROM subscriptions WHERE user_id = $id") 18 18 .all({ id: req.user.id }); 19 + 20 + const qs = req.query ? ('?' + new URLSearchParams(req.query).toString()) : ''; 21 + 19 22 if (subs.length === 0) { 20 - res.redirect("/r/all"); 23 + res.redirect(`/r/all${qs}`); 21 24 } else { 22 25 const p = subs.map((s) => s.subreddit).join("+"); 23 - res.redirect(`/r/${p}`); 26 + res.redirect(`/r/${p}${qs}`); 24 27 } 25 28 }); 26 29 ··· 31 34 const query = req.query ? req.query : {}; 32 35 if (!query.sort) { 33 36 query.sort = "hot"; 37 + } 38 + if (!query.view) { 39 + query.view = "compact"; 34 40 } 35 41 36 42 let isSubbed = false; ··· 47 53 48 54 const [posts, about] = await Promise.all([postsReq, aboutReq]); 49 55 56 + if (query.view == 'card' && posts && posts.posts) { 57 + posts.posts.forEach(unescape_selftext); 58 + } 59 + 50 60 res.render("index", { 51 61 subreddit, 52 62 posts, ··· 71 81 data: unescape_submission(response), 72 82 user: req.user, 73 83 from: req.query.from, 84 + query: req.query, 74 85 }); 75 86 }); 76 87 ··· 104 115 ) 105 116 .all({ id: req.user.id }); 106 117 107 - res.render("subs", { subs, user: req.user }); 118 + res.render("subs", { subs, user: req.user, query: req.query }); 108 119 }); 109 120 110 121 // GET /search 111 122 router.get("/search", authenticateToken, async (req, res) => { 112 - res.render("search", { user: req.user }); 123 + res.render("search", { user: req.user, query: req.query }); 113 124 }); 114 125 115 126 // GET /sub-search ··· 133 144 message, 134 145 user: req.user, 135 146 original_query: req.query.q, 147 + query: req.query, 136 148 }); 137 149 } 138 150 }); ··· 147 159 items.length === 0 148 160 ? "no results found" 149 161 : `showing ${items.length} results`; 162 + 163 + if (req.query.view == 'card' && items) { 164 + items.forEach(unescape_selftext); 165 + } 166 + 150 167 res.render("post-search", { 151 168 items, 152 169 after, ··· 154 171 user: req.user, 155 172 original_query: req.query.q, 156 173 currentUrl: req.url, 174 + query: req.query, 157 175 }); 158 176 } 159 177 }); ··· 176 194 usedAt: Date.parse(inv.usedAt), 177 195 })); 178 196 } 179 - res.render("dashboard", { invites, isAdmin, user: req.user }); 197 + res.render("dashboard", { invites, isAdmin, user: req.user, query: req.query }); 180 198 }); 181 199 182 200 router.get("/create-invite", authenticateAdmin, async (req, res) => { ··· 359 377 const post = response.submission.data; 360 378 const comments = response.comments; 361 379 380 + unescape_selftext(post); 381 + comments.forEach(unescape_comment); 382 + 383 + return { post, comments }; 384 + } 385 + 386 + function unescape_selftext(post) { 387 + // If called after getSubmissions 388 + if (post.data && post.data.selftext_html) { 389 + post.data.selftext_html = he.decode(post.data.selftext_html); 390 + } 391 + // If called after getSubmissionComments 362 392 if (post.selftext_html) { 363 393 post.selftext_html = he.decode(post.selftext_html); 364 394 } 365 - comments.forEach(unescape_comment); 366 - 367 - return { post, comments }; 368 395 } 369 396 370 397 function unescape_comment(comment) {
+4 -2
src/views/comments.pug
··· 6 6 7 7 - var post = data.post 8 8 - var comments = data.comments 9 + - var viewQuery = 'view=' + (query && query.view ? query.view : 'compact') 10 + - var sortQuery = 'sort=' + (query && query.sort ? query.sort + (query.t ? '&t=' + query.t : '') : 'hot') 9 11 doctype html 10 12 html 11 13 +head(post.title) ··· 27 29 | &nbsp;&nbsp; 28 30 | · 29 31 | &nbsp;&nbsp; 30 - a(href=`/r/${post.subreddit}`) r/#{post.subreddit} 32 + a(href=`/r/${post.subreddit}?${sortQuery}&${viewQuery}`) r/#{post.subreddit} 31 33 32 34 div.info-container 33 35 - var domain = (new URL(post.url)).hostname ··· 65 67 66 68 if post.selftext_html 67 69 div.self-text 68 - != post.selftext_html 70 + != convertInlineImageLinks(post.selftext_html) 69 71 70 72 hr 71 73
+21 -12
src/views/index.pug
··· 2 2 include ../mixins/header 3 3 include ../mixins/head 4 4 include ../utils 5 + - var viewQuery = query && query.view ? query.view : 'compact' 6 + - var sortQuery = query && query.sort ? query.sort + (query.t ? '&t=' + query.t : '') : 'hot' 5 7 doctype html 6 8 html 7 9 +head("home") ··· 14 16 div.sub-title 15 17 h1 16 18 if isMulti 17 - a(href=`/`) lurker 19 + a(href=`/?sort=${sortQuery}&view=${viewQuery}`) lurker 18 20 else 19 - a(href=`/r/${subreddit}`) 21 + a(href=`/r/${subreddit}?sort=${sortQuery}&view=${viewQuery}`) 20 22 | r/#{subreddit} 21 23 if !isMulti 22 24 div#button-container ··· 32 34 a(href="https://donate.stripe.com/dR62bTaZH1295Da4gg") oppiliappan 33 35 |, author of lurker 34 36 hr 35 - details 37 + details.sort-details 36 38 summary.sorting sorting by #{query.sort + (query.t?' '+query.t:'')} 37 39 div.sort-opts 38 40 div 39 - a(href=`/r/${subreddit}?sort=hot`) hot 41 + a(href=`/r/${subreddit}?sort=hot&view=${viewQuery}`) hot 40 42 div 41 - a(href=`/r/${subreddit}?sort=new`) new 43 + a(href=`/r/${subreddit}?sort=new&view=${viewQuery}`) new 42 44 div 43 - a(href=`/r/${subreddit}?sort=rising`) rising 45 + a(href=`/r/${subreddit}?sort=rising&view=${viewQuery}`) rising 46 + div 47 + a(href=`/r/${subreddit}?sort=top&view=${viewQuery}`) top 48 + div 49 + a(href=`/r/${subreddit}?sort=top&t=day&view=${viewQuery}`) top day 44 50 div 45 - a(href=`/r/${subreddit}?sort=top`) top 51 + a(href=`/r/${subreddit}?sort=top&t=week&view=${viewQuery}`) top week 46 52 div 47 - a(href=`/r/${subreddit}?sort=top&t=day`) top day 53 + a(href=`/r/${subreddit}?sort=top&t=month&view=${viewQuery}`) top month 48 54 div 49 - a(href=`/r/${subreddit}?sort=top&t=week`) top week 55 + a(href=`/r/${subreddit}?sort=top&t=year&view=${viewQuery}`) top year 50 56 div 51 - a(href=`/r/${subreddit}?sort=top&t=month`) top month 57 + a(href=`/r/${subreddit}?sort=top&t=all&view=${viewQuery}`) top all 58 + details.view-details 59 + summary.viewing viewing as #{viewQuery} 60 + div.view-opts 52 61 div 53 - a(href=`/r/${subreddit}?sort=top&t=year`) top year 62 + a(href=`/r/${subreddit}?sort=${sortQuery}&view=compact`) compact 54 63 div 55 - a(href=`/r/${subreddit}?sort=top&t=all`) top all 64 + a(href=`/r/${subreddit}?sort=${sortQuery}&view=card`) card 56 65 57 66 if posts 58 67 each child in posts.posts
+8 -3
src/views/media.pug
··· 2 2 doctype html 3 3 html 4 4 +head("home") 5 - body 6 - div.media-maximized-container 5 + script(type='text/javascript'). 6 + function toggleZoom() { 7 + Array.from(document.getElementsByClassName('media-maximized')).forEach(element => element.classList.toggle('zoom')); 8 + } 9 + 10 + body.media-maximized 11 + div.media-maximized.container 7 12 if kind == 'img' 8 - img(src=url).media-maximized 13 + img(src=url onclick=`toggleZoom()`).media-maximized 9 14 else 10 15 video(src=url controls).media-maximized
+4
src/views/post-search.pug
··· 2 2 include ../mixins/header 3 3 include ../mixins/head 4 4 5 + - var viewQuery = query && query.view ? query.view : 'compact' 6 + - var sortQuery = query && query.sort ? query.sort + (query.t ? '&t=' + query.t : '') : 'hot' 5 7 doctype html 6 8 html 7 9 +head("search posts") ··· 14 16 form(action="/post-search" method="get").search-bar 15 17 - var prefill = original_query ?? ""; 16 18 input(type="text" name="q" placeholder="type in a search term..." value=prefill required).search-input 19 + input(type="hidden" name="sort" value=sortQuery) 20 + input(type="hidden" name="view" value=viewQuery) 17 21 button(type="submit").search-button go 18 22 if message 19 23 div.search-message
+6
src/views/search.pug
··· 1 1 include ../mixins/header 2 2 include ../mixins/head 3 3 4 + - var viewQuery = query && query.view ? query.view : 'compact' 5 + - var sortQuery = query && query.sort ? query.sort + (query.t ? '&t=' + query.t : '') : 'hot' 4 6 doctype html 5 7 html 6 8 +head("search subreddits") ··· 14 16 form(action="/sub-search" method="get").search-bar 15 17 - var prefill = original_query ?? ""; 16 18 input(type="text" name="q" placeholder="type in a search term..." value=prefill required).search-input 19 + input(type="hidden" name="sort" value=sortQuery) 20 + input(type="hidden" name="view" value=viewQuery) 17 21 button(type="submit").search-button go 18 22 19 23 hr ··· 23 27 form(action="/post-search" method="get").search-bar 24 28 - var prefill = original_query ?? ""; 25 29 input(type="text" name="q" placeholder="type in a search term..." value=prefill required).search-input 30 + input(type="hidden" name="sort" value=sortQuery) 31 + input(type="hidden" name="view" value=viewQuery) 26 32 button(type="submit").search-button go 27 33 p 28 34 | you can narrow search results using filters:
+5 -1
src/views/sub-search.pug
··· 1 1 include ../mixins/header 2 2 include ../mixins/head 3 3 4 + - var viewQuery = (query && query.view) ? query.view : 'compact' 5 + - var sortQuery = (query && query.sort) ? query.sort + (query.t ? '&t=' + query.t : '') : 'hot' 4 6 doctype html 5 7 html 6 8 +head("search subreddits") ··· 13 15 form(action="/sub-search" method="get").search-bar 14 16 - var prefill = original_query ?? ""; 15 17 input(type="text" name="q" placeholder="type in a search term..." value=prefill required).search-input 18 + input(type="hidden" name="sort" value=sortQuery) 19 + input(type="hidden" name="view" value=viewQuery) 16 20 button(type="submit").search-button go 17 21 if message 18 22 div.search-message ··· 25 29 - var isSubbed = subs.includes(subreddit) 26 30 div.sub-title 27 31 h3 28 - a(href=`/r/${subreddit}`) 32 + a(href=`/r/${subreddit}?sort=${sortQuery}&view=${viewQuery}`) 29 33 | r/#{subreddit} 30 34 div#button-container 31 35 if isSubbed
+3 -1
src/views/subs.pug
··· 1 1 include ../mixins/header 2 2 include ../mixins/head 3 3 4 + - var viewQuery = query && query.view ? query.view : 'compact' 5 + - var sortQuery = query && query.sort ? query.sort + (query.t ? '&t=' + query.t : '') : 'hot' 4 6 doctype html 5 7 html 6 8 +head("subscriptions") ··· 16 18 - var isSubbed = true 17 19 div.sub-title 18 20 h4 19 - a(href=`/r/${subreddit}`) 21 + a(href=`/r/${subreddit}?sort=${sortQuery}&view=${viewQuery}`) 20 22 | r/#{subreddit} 21 23 div#button-container 22 24 if isSubbed