this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Dark mode enabled

+390 -208
+145 -26
internal/assets/css/screen.css
··· 1 + :root { 2 + /* Core Colors */ 3 + --bg-body: white; 4 + --text-main: #222; /* Default text color */ 5 + --text-muted: #aaa; 6 + 7 + /* Masthead */ 8 + --masthead-text: #000; 9 + --masthead-border: #000; 10 + 11 + /* Date Widget */ 12 + --date-number: #e0e0e0; 13 + --date-text: #444; 14 + --date-shadow: white; 15 + --date-year: #888; 16 + 17 + /* Content Elements */ 18 + --img-placeholder: #bbb; 19 + --author-color: #aaa; 20 + --link-color: #6c3; 21 + --quote-color: #d5b; 22 + --header-color: #aaa; 23 + 24 + /* OG Preview (Cards) */ 25 + --og-border: #e1e8ed; 26 + --og-bg: #fff; 27 + --og-title: #14171a; 28 + --og-desc: #657786; 29 + 30 + /* Slack-like OG */ 31 + --slack-border: #e0e0e0; 32 + --slack-site-info: #696969; 33 + --slack-title: #1264a3; 34 + --slack-desc: #2d2d2d; 35 + --slack-img-border: #e0e0e0; 36 + 37 + /* Footer */ 38 + --footer-bg: #f5f5f5; 39 + --footer-border: #eee; 40 + --footer-text: #666; 41 + --footer-link: #444; 42 + 43 + /* Icon Fill */ 44 + --icon-fill: #000000; 45 + } 46 + 47 + [data-theme="dark"] { 48 + /* Core Colors */ 49 + --bg-body: #121212; 50 + --text-main: #e0e0e0; 51 + --text-muted: #888; 52 + 53 + /* Masthead */ 54 + --masthead-text: #fff; 55 + --masthead-border: #fff; 56 + 57 + /* Date Widget */ 58 + --date-number: #333; 59 + --date-text: #ccc; 60 + --date-shadow: #000; 61 + --date-year: #666; 62 + 63 + /* Content Elements */ 64 + --img-placeholder: #333; 65 + --author-color: #888; 66 + --link-color: #8e5; 67 + --quote-color: #e6c; 68 + --header-color: #888; 69 + 70 + /* OG Preview (Cards) */ 71 + --og-border: #333; 72 + --og-bg: #1e1e1e; 73 + --og-title: #e0e0e0; 74 + --og-desc: #aaa; 75 + 76 + /* Slack-like OG */ 77 + --slack-border: #444; 78 + --slack-site-info: #888; 79 + --slack-title: #5af; 80 + --slack-desc: #ccc; 81 + --slack-img-border: #444; 82 + 83 + /* Footer */ 84 + --footer-bg: #1a1a1a; 85 + --footer-border: #333; 86 + --footer-text: #888; 87 + --footer-link: #aaa; 88 + 89 + /* Icon Fill */ 90 + --icon-fill: #ffffff; 91 + } 92 + 1 93 body { 2 94 margin: 0; 3 - background-color: white; 95 + background-color: var(--bg-body); 96 + color: var(--text-main); 4 97 width: 100%; 5 98 display: flex; 6 99 flex-direction: column; 7 100 min-height: 100vh; 101 + transition: background-color 0.3s ease, color 0.3s ease; 8 102 } 9 103 10 104 #page { ··· 34 128 line-height: 68px; 35 129 letter-spacing: -5px; 36 130 border-bottom: solid 2px; 131 + border-color: var(--masthead-border); 132 + color: var(--masthead-text); 133 + display: flex; 134 + justify-content: space-between; 135 + align-items: center; 37 136 } 38 137 39 138 #masthead a { 40 139 text-decoration: none; 41 - color: #000; 140 + color: var(--masthead-text); 141 + } 142 + 143 + #theme-toggle { 144 + background: none; 145 + border: none; 146 + cursor: pointer; 147 + padding: 0; 148 + margin-top: 10px; /* Adjust alignment */ 149 + color: var(--masthead-text); 150 + opacity: 0.5; 151 + transition: opacity 0.2s; 152 + } 153 + 154 + #theme-toggle:hover { 155 + opacity: 1; 42 156 } 43 157 44 158 #sidebar { ··· 63 177 .date-date { 64 178 font-size: 52px; 65 179 line-height: 1; 66 - color: #e0e0e0; /* Light grey background number */ 180 + color: var(--date-number); /* Light grey background number */ 67 181 font-weight: bold; 68 182 } 69 183 ··· 85 199 padding: 0; 86 200 font-size: 13px; 87 201 font-weight: bold; 88 - color: #444; /* Darker text for visibility over grey */ 202 + color: var(--date-text); /* Darker text for visibility over grey */ 89 203 text-transform: uppercase; 90 - text-shadow: 0px 0px 2px white; /* Halo to ensure readability */ 204 + text-shadow: 0px 0px 2px var(--date-shadow); /* Halo to ensure readability */ 91 205 } 92 206 93 207 .date-year { ··· 95 209 padding: 0; 96 210 font-size: 10px; 97 211 font-weight: bold; 98 - color: #888; 212 + color: var(--date-year); 99 213 } 100 214 101 215 .item { ··· 108 222 109 223 .item img { 110 224 padding: 3px; 111 - background: #bbb; 225 + background: var(--img-placeholder); 112 226 max-height: 400px; 113 227 max-width: 400px; 114 228 overflow: hidden; ··· 117 231 .author { 118 232 padding-left: 5px; 119 233 font-size: 18px; 120 - color: #aaa; 234 + color: var(--author-color); 121 235 } 122 236 123 237 .link a { 124 238 font-weight: bold; 125 239 text-decoration: none; 126 - color: #6c3; 240 + color: var(--link-color); 127 241 } 128 242 129 243 .text { 130 244 font-weight: bold; 131 245 text-decoration: none; 132 - color: #aaa; 246 + color: var(--text-muted); 133 247 } 134 248 135 249 .quote { 136 250 padding-left: 30px; 137 251 font-weight: bold; 138 - color: #d5b; 252 + color: var(--quote-color); 139 253 background: url(/img/parens.gif) no-repeat; 140 254 } 141 255 ··· 144 258 font-size: 18px; 145 259 font-weight: bold; 146 260 letter-spacing: -1px; 147 - color: #aaa; 261 + color: var(--header-color); 148 262 text-align: right; 149 263 } 150 264 ··· 171 285 } 172 286 173 287 .og-preview-card { 174 - border: 1px solid #e1e8ed; 288 + border: 1px solid var(--og-border); 175 289 border-radius: 8px; 176 290 overflow: hidden; 177 - background: #fff; 291 + background: var(--og-bg); 178 292 max-width: 500px; 179 293 margin-top: 8px; 180 294 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); ··· 200 314 .og-preview-title { 201 315 font-size: 14px; 202 316 font-weight: bold; 203 - color: #14171a; 317 + color: var(--og-title); 204 318 margin-bottom: 4px; 205 319 line-height: 1.4; 206 320 } 207 321 208 322 .og-preview-description { 209 323 font-size: 13px; 210 - color: #657786; 324 + color: var(--og-desc); 211 325 line-height: 1.4; 212 326 } 213 327 ··· 231 345 #footer { 232 346 padding: 10px; 233 347 text-align: center; 234 - background: #f5f5f5; 235 - border-top: 1px solid #eee; 348 + background: var(--footer-bg); 349 + border-top: 1px solid var(--footer-border); 236 350 font-size: 12px; 237 - color: #666; 351 + color: var(--footer-text); 238 352 font-family: sans-serif; 239 353 } 240 354 241 355 #footer a { 242 - color: #444; 356 + color: var(--footer-link); 243 357 text-decoration: none; 244 358 } 245 359 246 360 #footer a:hover { 247 361 text-decoration: underline; 248 362 } 363 + 364 + #footer svg { 365 + fill: var(--icon-fill); 366 + } 367 + 249 368 #navigation { 250 369 width: 525px; 251 370 margin-left: -75px; ··· 258 377 margin-top: 8px; 259 378 margin-bottom: 8px; 260 379 padding-left: 12px; 261 - border-left: 4px solid #e0e0e0; 380 + border-left: 4px solid var(--slack-border); 262 381 font-family: "Slack-Lato", "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 263 382 max-width: 600px; 264 383 } ··· 268 387 align-items: center; 269 388 font-size: 12px; 270 389 font-weight: 700; 271 - color: #696969; /* Slack's lighter grey */ 390 + color: var(--slack-site-info); /* Slack's lighter grey */ 272 391 margin-bottom: 4px; 273 392 } 274 393 ··· 287 406 font-size: 16px; 288 407 font-weight: 700; 289 408 line-height: 24px; 290 - color: #1264a3; 409 + color: var(--slack-title); 291 410 margin-bottom: 2px; 292 411 } 293 412 294 413 .og-title a { 295 - color: #1264a3; 414 + color: var(--slack-title); 296 415 text-decoration: none; 297 416 } 298 417 ··· 303 422 .og-description { 304 423 font-size: 15px; 305 424 line-height: 22px; 306 - color: #2d2d2d; /* Slightly softer than #1d1c1d */ 425 + color: var(--slack-desc); /* Slightly softer than #1d1c1d */ 307 426 margin-bottom: 8px; 308 427 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; 309 428 letter-spacing: normal; ··· 321 440 .og-image img { 322 441 max-width: 100%; 323 442 border-radius: 8px; /* Slack likes rounder corners on media */ 324 - border: 1px solid #e0e0e0; 443 + border: 1px solid var(--slack-img-border); 325 444 display: block; 326 445 /* Reset styles from .item img */ 327 446 padding: 0 !important;
internal/assets/img/next.png

This is a binary file and will not be displayed.

internal/assets/img/parens.gif

This is a binary file and will not be displayed.

internal/assets/img/prev.png

This is a binary file and will not be displayed.

+3 -3
internal/handler/handlers.go
··· 244 244 navP := "" 245 245 navN := "" 246 246 if iParam != "" { 247 - navP = fmt.Sprintf(`<a href="?i=%d"><img src="/img/prev.jpg" border="0" alt="" /></a>`, i+1) 248 - navN = fmt.Sprintf(` &nbsp;<a href="?i=%d"><img src="/img/next.jpg" border="0" alt="" /></a>`, i-1) 247 + navP = fmt.Sprintf(`<a href="?i=%d"><img src="/img/prev.png" border="0" alt="" /></a>`, i+1) 248 + navN = fmt.Sprintf(` &nbsp;<a href="?i=%d"><img src="/img/next.png" border="0" alt="" /></a>`, i-1) 249 249 } else { 250 - navP = `<a href="?i=2"><img src="/img/prev.jpg" border="0" alt="" /></a>` 250 + navP = `<a href="?i=2"><img src="/img/prev.png" border="0" alt="" /></a>` 251 251 } 252 252 if i == 1 { 253 253 navN = "" // Perl: $nav->{'n'} = '' unless $self->{'arg'}->{'i'};
+242 -179
internal/templates/views/index.html
··· 1 1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> 2 2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> 3 + <head> 4 + <title>tumblefish.{{.PageTitle}}</title> 5 + <link 6 + rel="stylesheet" 7 + href="/css/screen.css" 8 + type="text/css" 9 + media="screen" 10 + /> 3 11 4 - <head> 5 - <title>tumblefish.{{.PageTitle}}</title> 6 - <link rel="stylesheet" href="/css/screen.css" type="text/css" media="screen" /> 7 - <!-- Google Analytics (Legacy) --> 8 - <script type="text/javascript"> 9 - var _gaq = _gaq || []; 10 - _gaq.push(['_setAccount', 'UA-24593498-1']); 11 - _gaq.push(['_trackPageview']); 12 + <!-- Theme Init --> 13 + <script> 14 + (function () { 15 + var savedTheme = localStorage.getItem("theme"); 16 + var prefersDark = window.matchMedia( 17 + "(prefers-color-scheme: dark)" 18 + ).matches; 19 + if (savedTheme === "dark" || (!savedTheme && prefersDark)) { 20 + document.documentElement.setAttribute("data-theme", "dark"); 21 + } 22 + })(); 23 + </script> 12 24 13 - (function() { 14 - var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; 15 - ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; 16 - var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); 17 - })(); 18 - </script> 19 - <!-- Twitter Cards --> 20 - <script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+"://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script> 25 + <!-- Twitter Cards --> 26 + <script> 27 + !(function (d, s, id) { 28 + var js, 29 + fjs = d.getElementsByTagName(s)[0], 30 + p = /^http:/.test(d.location) ? "http" : "https"; 31 + if (!d.getElementById(id)) { 32 + js = d.createElement(s); 33 + js.id = id; 34 + js.src = p + "://platform.twitter.com/widgets.js"; 35 + fjs.parentNode.insertBefore(js, fjs); 36 + } 37 + })(document, "script", "twitter-wjs"); 38 + </script> 21 39 22 - <!-- Open Graph Preview Loader --> 23 - <script type="text/javascript"> 24 - (function() { 25 - function loadOGPreview(item) { 26 - var url = item.getAttribute('data-url'); 27 - var contentType = item.getAttribute('data-content-type'); 28 - var previewDiv = item.querySelector('.og-preview'); 40 + <!-- Open Graph Preview Loader --> 41 + <script type="text/javascript"> 42 + (function () { 43 + function loadOGPreview(item) { 44 + var url = item.getAttribute("data-url"); 45 + var contentType = item.getAttribute("data-content-type"); 46 + var previewDiv = item.querySelector(".og-preview"); 29 47 30 - // Only load preview for non-image content (text/html) 31 - if (!url || (contentType && contentType.match(/image/))) { 32 - return; 33 - } 48 + // Only load preview for non-image content (text/html) 49 + if (!url || (contentType && contentType.match(/image/))) { 50 + return; 51 + } 34 52 35 - // Skip OG preview for YouTube links check removed to allow client-side handling 53 + // Skip OG preview for YouTube links (they're embedded directly) 54 + if (url.match(/youtube\.com|youtu\.be/i)) { 55 + return; 56 + } 36 57 37 - // Ensure URL has protocol 38 - if (!url.match(/^https?:\/\//)) { 39 - url = 'http://' + url; 40 - } 58 + // Ensure URL has protocol 59 + if (!url.match(/^https?:\/\//)) { 60 + url = "http://" + url; 61 + } 41 62 42 - var xhr = new XMLHttpRequest(); 43 - // Add timestamp to prevent caching of previous 200 OK responses 44 - var previewUrl = '/ogpreview.cgi?url=' + encodeURIComponent(url) + '&_t=' + new Date().getTime(); 45 - xhr.open('GET', previewUrl, true); 46 - xhr.onreadystatechange = function() { 47 - if (xhr.readyState === 4 && xhr.status === 200) { 48 - try { 49 - var data = JSON.parse(xhr.responseText); 63 + // Create preview endpoint URL 64 + var previewUrl = "/ogpreview.cgi?url=" + encodeURIComponent(url); 50 65 51 - // Handle HTTP Errors (compact display) 52 - if (data.status && data.status >= 400) { 53 - var errorHtml = '<div class="og-error-compact">'; 54 - errorHtml += '<span class="http-error-badge">' + data.status + '</span> '; 55 - errorHtml += '<a href="' + escapeHtml(url) + '" class="broken-link" target="_blank">' + escapeHtml(url) + '</a>'; 56 - errorHtml += '</div>'; 57 - previewDiv.innerHTML = errorHtml; 58 - 59 - // Hide the original link text to reduce clutter 60 - var originalLink = element.querySelector('.link'); 61 - if (originalLink) { 62 - originalLink.style.display = 'none'; 66 + // Load preview asynchronously 67 + var xhr = new XMLHttpRequest(); 68 + xhr.open("GET", previewUrl, true); 69 + xhr.onreadystatechange = function () { 70 + if (xhr.readyState === 4 && xhr.status === 200) { 71 + try { 72 + var data = JSON.parse(xhr.responseText); 73 + if (data.error) { 74 + return; 63 75 } 64 - return; 65 - } 66 76 67 - if (data.error) { 68 - return; 69 - } 77 + // Use Open Graph image, Twitter image, or nothing 78 + var imageUrl = data.image || data.twitter_image || null; 79 + // Use Open Graph title, Twitter title, or fallback title 80 + var title = data.title || data.twitter_title || null; 81 + // Use Open Graph description, Twitter description, or fallback description 82 + var description = 83 + data.description || data.twitter_description || null; 70 84 71 - // Use Open Graph image, Twitter image, or nothing 72 - var imageUrl = data.image || data.twitter_image || null; 73 - // Use Open Graph title, Twitter title, or fallback title 74 - var title = data.title || data.twitter_title || null; 75 - // Use Open Graph description, Twitter description, or fallback description 76 - var description = data.description || data.twitter_description || null; 85 + // Only show preview if we have at least an image, title, or description 86 + if (!imageUrl && !title && !description) { 87 + return; 88 + } 77 89 78 - // Only show preview if we have at least an image, title, or description 79 - if (!imageUrl && !title && !description) { 80 - return; 81 - } 90 + // Build preview HTML 91 + var previewHTML = '<div class="og-card">'; 82 92 83 - // Build preview HTML 84 - var previewHTML = '<div class="og-card">'; 93 + // Site Info (Icon + Name) 94 + var provider = data.provider_name || "Link"; 95 + var iconUrl = "/favicon.ico"; 96 + if (provider === "Reddit") 97 + iconUrl = 98 + "https://www.redditstatic.com/desktop2x/img/favicon/favicon-32x32.png"; 85 99 86 - // Site Info (Icon + Name) 87 - var provider = data.provider_name || 'Link'; 88 - var iconUrl = '/favicon.ico'; 89 - if (provider === 'Reddit') iconUrl = 'https://www.redditstatic.com/desktop2x/img/favicon/favicon-32x32.png'; 100 + previewHTML += 101 + '<div class="og-site-info"><img src="' + 102 + iconUrl + 103 + '" class="og-site-icon" /> ' + 104 + escapeHtml(provider) + 105 + "</div>"; 90 106 91 - previewHTML += '<div class="og-site-info"><img src="' + iconUrl + '" class="og-site-icon" /> ' + escapeHtml(provider) + '</div>'; 92 - 93 - // Title 94 - if (title) { 95 - previewHTML += '<div class="og-title"><a href="' + escapeHtml(url) + '" target="_blank">' + escapeHtml(title) + '</a></div>'; 96 - } 107 + // Title 108 + if (title) { 109 + previewHTML += 110 + '<div class="og-title"><a href="' + 111 + escapeHtml(url) + 112 + '" target="_blank">' + 113 + escapeHtml(title) + 114 + "</a></div>"; 115 + } 97 116 98 - // Description 99 - if (description) { 100 - if (description.length > 300) { 101 - description = description.substring(0, 300) + '...'; 117 + // Description 118 + if (description) { 119 + if (description.length > 300) { 120 + description = description.substring(0, 300) + "..."; 121 + } 122 + previewHTML += 123 + '<div class="og-description">' + 124 + escapeHtml(description) + 125 + "</div>"; 102 126 } 103 - previewHTML += '<div class="og-description">' + escapeHtml(description) + '</div>'; 104 - } 105 127 106 - // Image 107 - if (imageUrl) { 108 - var imageContent = '<img src="' + escapeHtml(imageUrl) + '" alt="" />'; 109 - var extraClass = ''; 110 - var onClickAttr = ''; 128 + // Image 129 + if (imageUrl) { 130 + var imageContent = 131 + '<img src="' + escapeHtml(imageUrl) + '" alt="" />'; 132 + var onClickAttr = ''; 111 133 112 - // Video / YouTube handling 113 - if (data.type === 'video' || url.match(/(youtube\.com|youtu\.be)/)) { 114 - extraClass = 'is-video'; 134 + // If it's a video, add the play button overlay 135 + if (data.type === "video") { 115 136 imageContent += '<div class="og-play-button"></div>'; 137 + onClickAttr = 'onclick="replaceWithVideo(this)"'; 138 + } 116 139 117 - // Click to play logic - safe DOM lookup 118 - onClickAttr = 'onclick="replaceWithVideo(this)"'; 140 + previewHTML += 141 + '<div class="og-image ' + 142 + (data.type === "video" ? "is-video" : "") + 143 + '" ' + onClickAttr + '>' + 144 + imageContent + 145 + "</div>"; 119 146 } 120 147 121 - previewHTML += '<div class="og-image ' + extraClass + '" ' + onClickAttr + '>' + imageContent + '</div>'; 148 + previewHTML += "</div>"; 149 + 150 + previewDiv.innerHTML = previewHTML; 151 + } catch (e) { 152 + // Silently fail if JSON parsing fails 153 + } 122 154 } 123 - 124 - previewHTML += '</div>'; 125 - 126 - previewDiv.innerHTML = previewHTML; 127 - } catch (e) { 128 - // Silently fail if JSON parsing fails 129 - console.error(e); 130 - } 155 + }; 156 + xhr.send(); 131 157 } 132 - }; 133 - xhr.send(); 134 - } 135 158 136 - function escapeHtml(text) { 137 - var div = document.createElement('div'); 138 - div.textContent = text; 139 - return div.innerHTML; 140 - } 141 - 142 - // Global handler for video replacemen 143 - window.replaceWithVideo = function(container) { 144 - // Find URL from parent item 145 - var item = container.closest('.item'); 146 - if (!item) return; 147 - var url = item.getAttribute('data-url'); 148 - if (!url) return; 149 - 150 - var videoID = ''; 151 - var match = url.match(/(?:youtube\.com\/watch\?v=|youtube\.com\/embed\/|youtu\.be\/)([a-zA-Z0-9_-]{11})/); 152 - if (match) { 153 - videoID = match[1]; 154 - } else { 155 - // Try query param scan too 156 - var match2 = url.match(/v=([a-zA-Z0-9_-]{11})/); 157 - if (match2) videoID = match2[1]; 159 + function escapeHtml(text) { 160 + var div = document.createElement("div"); 161 + div.textContent = text; 162 + return div.innerHTML; 158 163 } 159 164 160 - if (videoID) { 161 - var iframe = '<div class="youtube-embed-wrapper"><iframe width="100%" height="100%" src="https://www.youtube.com/embed/' + videoID + '?autoplay=1&rel=0" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>'; 162 - container.innerHTML = iframe; 163 - container.onclick = null; // Remove handler 164 - container.classList.remove('is-video'); 165 - container.style.cursor = 'default'; 166 - } 167 - }; 165 + // Global handler for video replacement 166 + window.replaceWithVideo = function(container) { 167 + // Find URL from parent item 168 + var item = container.closest('.item'); 169 + if (!item) return; 170 + var url = item.getAttribute('data-url'); 171 + if (!url) return; 168 172 173 + var videoID = ''; 174 + var match = url.match(/(?:youtube\.com\/watch\?v=|youtube\.com\/embed\/|youtu\.be\/)([a-zA-Z0-9_-]{11})/); 175 + if (match) { 176 + videoID = match[1]; 177 + } else { 178 + // Try query param scan too 179 + var match2 = url.match(/v=([a-zA-Z0-9_-]{11})/); 180 + if (match2) videoID = match2[1]; 181 + } 169 182 170 - // Load all previews when DOM is ready 171 - function initOGPreviews() { 172 - var items = document.querySelectorAll('.item[data-url]'); 173 - for (var i = 0; i < items.length; i++) { 174 - loadOGPreview(items[i]); 175 - } 176 - } 183 + if (videoID) { 184 + var iframe = '<div class="youtube-embed-wrapper"><iframe width="100%" height="100%" src="https://www.youtube.com/embed/' + videoID + '?autoplay=1&rel=0" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>'; 185 + container.innerHTML = iframe; 186 + container.onclick = null; // Remove handler 187 + container.classList.remove('is-video'); 188 + container.style.cursor = 'default'; 189 + } 190 + }; 177 191 178 - // Run when DOM is ready 179 - if (document.readyState === 'loading') { 180 - document.addEventListener('DOMContentLoaded', initOGPreviews); 181 - } else { 182 - initOGPreviews(); 183 - } 184 - })(); 185 - </script> 192 + // Load all previews when DOM is ready 193 + function initOGPreviews() { 194 + var items = document.querySelectorAll(".item[data-url]"); 195 + for (var i = 0; i < items.length; i++) { 196 + loadOGPreview(items[i]); 197 + } 198 + } 186 199 187 - </head> 200 + // Run when DOM is ready 201 + if (document.readyState === "loading") { 202 + document.addEventListener("DOMContentLoaded", initOGPreviews); 203 + } else { 204 + initOGPreviews(); 205 + } 206 + })(); 207 + </script> 208 + </head> 188 209 189 - <body> 210 + <body> 190 211 <div id="page"> 212 + <div id="masthead"> 213 + <a href="/">tumblefish.</a> 214 + <button id="theme-toggle" aria-label="Toggle Dark Mode"> 215 + <svg 216 + width="20" 217 + height="20" 218 + viewBox="0 0 24 24" 219 + fill="none" 220 + stroke="currentColor" 221 + stroke-width="2" 222 + stroke-linecap="round" 223 + stroke-linejoin="round" 224 + > 225 + <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path> 226 + </svg> 227 + </button> 228 + </div> 191 229 192 - <div id="masthead"><a href="/">tumblefish.</a></div> 193 - 194 - <div id="sidebar"> 230 + <div id="sidebar"> 195 231 <div class="item"> 196 - <div class="header">search it up</div> 197 - <form action="/search.cgi" id="search-form" method="get"> 198 - <input type="text" id="search" name="search" value="" size="15" /> 199 - </form> 232 + <div class="header">search it up</div> 233 + <form action="/search.cgi" id="search-form" method="get"> 234 + <input type="text" id="search" name="search" value="" size="15" /> 235 + </form> 200 236 </div> 201 237 <div class="item"> 202 - <div class="header">last week's hot shit</div> 203 - {{.Hot}} 238 + <div class="header">last week's hot shit</div> 239 + {{.Hot}} 204 240 </div> 205 241 <div class="item"> 206 - <div class="header">also</div> 207 - <div class="sm"><div class="link"><a href="/buttons/">buttons</a></div></div> 208 - <div class="sm"><div class="link"><a href="/index.xml">feed</a></div></div> 242 + <div class="header">also</div> 243 + <div class="sm"> 244 + <div class="link"><a href="/buttons/">buttons</a></div> 245 + </div> 246 + <div class="sm"> 247 + <div class="link"><a href="/index.xml">feed</a></div> 248 + </div> 209 249 </div> 210 - </div> 250 + </div> 211 251 212 - <div id="content"> 213 - {{.Container}} 214 - </div> 215 - 216 - <div id="navigation"> 217 - {{.NavP}} 218 - {{.NavN}} 219 - </div> 252 + <div id="content">{{.Container}}</div> 220 253 254 + <div id="navigation">{{.NavP}} {{.NavN}}</div> 221 255 </div> 222 256 223 257 <div id="footer"> 224 - Source Code Available on <a href="http://github.com/websages/tumble">GitHub</a> 225 - <svg width="16" height="16" viewBox="0 0 16 16" fill="#000000" style="vertical-align: text-bottom; display: inline-block;"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>. 226 - {{if .GitCommit}} Revision: <a href="{{.GitCommitURL}}">{{.GitCommit}}</a>{{end}} 258 + Source Code Available on 259 + <a href="http://github.com/websages/tumble">GitHub</a> 260 + <svg 261 + width="16" 262 + height="16" 263 + viewBox="0 0 16 16" 264 + fill="#000000" 265 + style="vertical-align: text-bottom; display: inline-block" 266 + > 267 + <path 268 + fill-rule="evenodd" 269 + d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" 270 + ></path></svg 271 + >. {{if .GitCommit}} Revision: 272 + <a href="{{.GitCommitURL}}">{{.GitCommit}}</a>{{end}} 227 273 </div> 228 - </body> 229 274 275 + <!-- Theme Toggle Logic --> 276 + <script> 277 + (function () { 278 + var toggle = document.getElementById("theme-toggle"); 279 + var html = document.documentElement; 280 + 281 + toggle.addEventListener("click", function () { 282 + if (html.getAttribute("data-theme") === "dark") { 283 + html.removeAttribute("data-theme"); 284 + localStorage.setItem("theme", "light"); 285 + } else { 286 + html.setAttribute("data-theme", "dark"); 287 + localStorage.setItem("theme", "dark"); 288 + } 289 + }); 290 + })(); 291 + </script> 292 + </body> 230 293 </html>