vod frog, frog with the vods
5
fork

Configure Feed

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

wrap player info in wavy box, remove close button, more padding

goose.art 70513c12 7a92b71d

+224 -221
+1 -1
.svelte-kit/generated/server/internal.js
··· 22 22 service_worker_options: undefined, 23 23 server_error_boundaries: false, 24 24 templates: { 25 - app: ({ head, body, assets, nonce, env }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<link rel=\"icon\" href=\"/frogcursor.png\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t\t<title>vod frog 🐸</title>\n\t\t" + head + "\n\t</head>\n\t<body>\n\t\t<div style=\"display: contents\">" + body + "</div>\n\t</body>\n</html>\n", 25 + app: ({ head, body, assets, nonce, env }) => "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <link rel=\"icon\" href=\"/frogcursor.png\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>vod frog</title>\n " + head + "\n </head>\n <body>\n <div style=\"display: contents\">" + body + "</div>\n </body>\n</html>\n", 26 26 error: ({ status, message }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>" + message + "</title>\n\n\t\t<style>\n\t\t\tbody {\n\t\t\t\t--bg: white;\n\t\t\t\t--fg: #222;\n\t\t\t\t--divider: #ccc;\n\t\t\t\tbackground: var(--bg);\n\t\t\t\tcolor: var(--fg);\n\t\t\t\tfont-family:\n\t\t\t\t\tsystem-ui,\n\t\t\t\t\t-apple-system,\n\t\t\t\t\tBlinkMacSystemFont,\n\t\t\t\t\t'Segoe UI',\n\t\t\t\t\tRoboto,\n\t\t\t\t\tOxygen,\n\t\t\t\t\tUbuntu,\n\t\t\t\t\tCantarell,\n\t\t\t\t\t'Open Sans',\n\t\t\t\t\t'Helvetica Neue',\n\t\t\t\t\tsans-serif;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\theight: 100vh;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t.error {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tmax-width: 32rem;\n\t\t\t\tmargin: 0 1rem;\n\t\t\t}\n\n\t\t\t.status {\n\t\t\t\tfont-weight: 200;\n\t\t\t\tfont-size: 3rem;\n\t\t\t\tline-height: 1;\n\t\t\t\tposition: relative;\n\t\t\t\ttop: -0.05rem;\n\t\t\t}\n\n\t\t\t.message {\n\t\t\t\tborder-left: 1px solid var(--divider);\n\t\t\t\tpadding: 0 0 0 1rem;\n\t\t\t\tmargin: 0 0 0 1rem;\n\t\t\t\tmin-height: 2.5rem;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t}\n\n\t\t\t.message h1 {\n\t\t\t\tfont-weight: 400;\n\t\t\t\tfont-size: 1em;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t@media (prefers-color-scheme: dark) {\n\t\t\t\tbody {\n\t\t\t\t\t--bg: #222;\n\t\t\t\t\t--fg: #ddd;\n\t\t\t\t\t--divider: #666;\n\t\t\t\t}\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"error\">\n\t\t\t<span class=\"status\">" + status + "</span>\n\t\t\t<div class=\"message\">\n\t\t\t\t<h1>" + message + "</h1>\n\t\t\t</div>\n\t\t</div>\n\t</body>\n</html>\n" 27 27 }, 28 28 version_hash: "tbmlr0"
+10 -10
src/app.html
··· 1 1 <!doctype html> 2 2 <html lang="en"> 3 - <head> 4 - <meta charset="utf-8" /> 5 - <link rel="icon" href="/frogcursor.png" /> 6 - <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 - <title>vod frog 🐸</title> 8 - %sveltekit.head% 9 - </head> 10 - <body> 11 - <div style="display: contents">%sveltekit.body%</div> 12 - </body> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <link rel="icon" href="/frogcursor.png" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 + <title>vod frog</title> 8 + %sveltekit.head% 9 + </head> 10 + <body> 11 + <div style="display: contents">%sveltekit.body%</div> 12 + </body> 13 13 </html>
+1 -1
src/lib/VideoPlayer.svelte
··· 104 104 105 105 // Hop the frog as playback advances 106 106 const delta = newProgress - lastHopProgress; 107 - if (Math.abs(delta) > 0.015) { 107 + if (Math.abs(delta) > 0.002) { 108 108 frogFrame = frogFrame === 0 ? 1 : 0; 109 109 frogFlipped = delta < 0; 110 110 lastHopProgress = newProgress;
+212 -209
src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount } from 'svelte'; 3 - import { 4 - listVideos, 5 - getPlaylistUrl, 6 - formatDuration, 7 - formatDate, 8 - resolveHandle, 9 - type VideoRecord 10 - } from '$lib/api'; 11 - import VideoPlayer from '$lib/VideoPlayer.svelte'; 12 - import VideoCard from '$lib/VideoCard.svelte'; 13 - import FrogHeader from '$lib/FrogHeader.svelte'; 2 + import { onMount } from "svelte"; 3 + import { 4 + listVideos, 5 + getPlaylistUrl, 6 + formatDuration, 7 + formatDate, 8 + resolveHandle, 9 + type VideoRecord, 10 + } from "$lib/api"; 11 + import VideoPlayer from "$lib/VideoPlayer.svelte"; 12 + import VideoCard from "$lib/VideoCard.svelte"; 13 + import FrogHeader from "$lib/FrogHeader.svelte"; 14 + import WavyBorder from "$lib/WavyBorder.svelte"; 14 15 15 - let videos: VideoRecord[] = $state([]); 16 - let cursor: string | undefined = $state(); 17 - let loading = $state(false); 18 - let hasMore = $state(true); 19 - let selectedVideo: VideoRecord | null = $state(null); 20 - let selectedHandle = $state(''); 21 - let error = $state(''); 16 + let videos: VideoRecord[] = $state([]); 17 + let cursor: string | undefined = $state(); 18 + let loading = $state(false); 19 + let hasMore = $state(true); 20 + let selectedVideo: VideoRecord | null = $state(null); 21 + let selectedHandle = $state(""); 22 + let error = $state(""); 22 23 23 - async function loadPage() { 24 - if (loading || !hasMore) return; 25 - loading = true; 26 - error = ''; 27 - try { 28 - const res = await listVideos(cursor, 24); 29 - videos = [...videos, ...res.records]; 30 - cursor = res.cursor; 31 - hasMore = !!res.cursor && res.records.length > 0; 32 - } catch (e: any) { 33 - error = e.message; 34 - } 35 - loading = false; 36 - } 24 + async function loadPage() { 25 + if (loading || !hasMore) return; 26 + loading = true; 27 + error = ""; 28 + try { 29 + const res = await listVideos(cursor, 24); 30 + videos = [...videos, ...res.records]; 31 + cursor = res.cursor; 32 + hasMore = !!res.cursor && res.records.length > 0; 33 + } catch (e: any) { 34 + error = e.message; 35 + } 36 + loading = false; 37 + } 37 38 38 - function selectVideo(video: VideoRecord) { 39 - selectedVideo = video; 40 - resolveHandle(video.value.creator).then((h) => (selectedHandle = h)); 41 - // Update URL with the video URI (at://did/collection/rkey) 42 - const url = new URL(window.location.href); 43 - url.searchParams.set('v', video.uri); 44 - window.history.pushState({}, '', url.toString()); 45 - window.scrollTo({ top: 0, behavior: 'smooth' }); 46 - } 39 + function selectVideo(video: VideoRecord) { 40 + selectedVideo = video; 41 + resolveHandle(video.value.creator).then((h) => (selectedHandle = h)); 42 + // Update URL with the video URI (at://did/collection/rkey) 43 + const url = new URL(window.location.href); 44 + url.searchParams.set("v", video.uri); 45 + window.history.pushState({}, "", url.toString()); 46 + window.scrollTo({ top: 0, behavior: "smooth" }); 47 + } 47 48 48 - function closePlayer() { 49 - selectedVideo = null; 50 - const url = new URL(window.location.href); 51 - url.searchParams.delete('v'); 52 - window.history.pushState({}, '', url.toString()); 53 - } 49 + function closePlayer() { 50 + selectedVideo = null; 51 + const url = new URL(window.location.href); 52 + url.searchParams.delete("v"); 53 + window.history.pushState({}, "", url.toString()); 54 + } 54 55 55 - // On mount: load videos, then check URL for a selected video 56 - onMount(async () => { 57 - await loadPage(); 58 - const params = new URLSearchParams(window.location.search); 59 - const videoUri = params.get('v'); 60 - if (videoUri) { 61 - const found = videos.find((v) => v.uri === videoUri); 62 - if (found) { 63 - selectVideo(found); 64 - } 65 - } 66 - }); 56 + // On mount: load videos, then check URL for a selected video 57 + onMount(async () => { 58 + await loadPage(); 59 + const params = new URLSearchParams(window.location.search); 60 + const videoUri = params.get("v"); 61 + if (videoUri) { 62 + const found = videos.find((v) => v.uri === videoUri); 63 + if (found) { 64 + selectVideo(found); 65 + } 66 + } 67 + }); 67 68 68 - // Handle browser back/forward 69 - function onPopState() { 70 - const params = new URLSearchParams(window.location.search); 71 - const videoUri = params.get('v'); 72 - if (videoUri) { 73 - const found = videos.find((v) => v.uri === videoUri); 74 - if (found) { 75 - selectedVideo = found; 76 - resolveHandle(found.value.creator).then((h) => (selectedHandle = h)); 77 - } 78 - } else { 79 - selectedVideo = null; 80 - } 81 - } 69 + // Handle browser back/forward 70 + function onPopState() { 71 + const params = new URLSearchParams(window.location.search); 72 + const videoUri = params.get("v"); 73 + if (videoUri) { 74 + const found = videos.find((v) => v.uri === videoUri); 75 + if (found) { 76 + selectedVideo = found; 77 + resolveHandle(found.value.creator).then( 78 + (h) => (selectedHandle = h), 79 + ); 80 + } 81 + } else { 82 + selectedVideo = null; 83 + } 84 + } 82 85 </script> 83 86 84 87 <svelte:window onpopstate={onPopState} /> 85 88 86 89 <svelte:head> 87 - <title>vod frog 🐸</title> 90 + <title>vod frog</title> 88 91 </svelte:head> 89 92 90 93 <div class="app"> 91 - <FrogHeader /> 94 + <FrogHeader /> 92 95 93 - {#if selectedVideo} 94 - <section class="player-section"> 95 - <VideoPlayer src={getPlaylistUrl(selectedVideo.uri)} /> 96 - <div class="player-info"> 97 - <div class="player-info-left"> 98 - <h2 class="player-title">{selectedVideo.value.title}</h2> 99 - <p class="player-meta"> 100 - <span class="creator-tag">{selectedHandle || selectedVideo.value.creator}</span> 101 - <span class="dot">·</span> 102 - {formatDate(selectedVideo.value.createdAt)} 103 - <span class="dot">·</span> 104 - {formatDuration(selectedVideo.value.duration)} 105 - </p> 106 - </div> 107 - <button class="close-btn" onclick={closePlayer} title="Close player">✕</button> 108 - </div> 109 - </section> 110 - {/if} 96 + {#if selectedVideo} 97 + <section class="player-section"> 98 + <VideoPlayer src={getPlaylistUrl(selectedVideo.uri)} /> 99 + <div class="player-info"> 100 + <WavyBorder seed="player-info" fill="#39FF44" strokeColor="#0A182B" strokeWidth={1.8} padding="clamp(40px, 6vw, 60px)"> 101 + <h2 class="player-title">{selectedVideo.value.title}</h2> 102 + <p class="player-meta"> 103 + <span class="creator-tag" 104 + >{selectedHandle || 105 + selectedVideo.value.creator}</span 106 + > 107 + <span class="dot">·</span> 108 + {formatDate(selectedVideo.value.createdAt)} 109 + <span class="dot">·</span> 110 + {formatDuration(selectedVideo.value.duration)} 111 + </p> 112 + </WavyBorder> 113 + </div> 114 + </section> 115 + {/if} 111 116 112 - {#if error} 113 - <div class="error">{error}</div> 114 - {/if} 117 + {#if error} 118 + <div class="error">{error}</div> 119 + {/if} 115 120 116 - <section class="grid"> 117 - {#each videos as video (video.uri)} 118 - <VideoCard {video} onSelect={selectVideo} /> 119 - {/each} 120 - </section> 121 + <section class="grid"> 122 + {#each videos as video (video.uri)} 123 + <VideoCard {video} onSelect={selectVideo} /> 124 + {/each} 125 + </section> 121 126 122 - {#if hasMore} 123 - <div class="load-more"> 124 - <button onclick={loadPage} disabled={loading}> 125 - {loading ? 'loading...' : 'load more 🐸'} 126 - </button> 127 - </div> 128 - {/if} 127 + {#if hasMore} 128 + <div class="load-more"> 129 + <button onclick={loadPage} disabled={loading}> 130 + {loading ? "loading..." : "load more"} 131 + </button> 132 + </div> 133 + {/if} 129 134 </div> 130 135 131 136 <style> 132 - .app { 133 - max-width: 1300px; 134 - margin: 0 auto; 135 - padding: 0 clamp(24px, 7vw, 120px) 60px; 136 - } 137 + .app { 138 + max-width: 1300px; 139 + margin: 0 auto; 140 + padding: 0 clamp(24px, 7vw, 120px) 60px; 141 + } 137 142 138 - .player-section { 139 - margin-bottom: 30px; 140 - padding: 0 10px; 141 - } 143 + .player-section { 144 + margin-bottom: 30px; 145 + padding: 0 10px; 146 + } 142 147 143 - .player-info { 144 - display: flex; 145 - justify-content: space-between; 146 - align-items: flex-start; 147 - padding: 14px 4px; 148 - gap: 16px; 149 - } 148 + .player-info { 149 + padding: 14px 4px; 150 + } 150 151 151 - .player-title { 152 - margin: 0; 153 - font-family: 'Fang', system-ui, sans-serif; 154 - font-size: 1.2rem; 155 - color: #0A182B; 156 - } 152 + .player-title { 153 + margin: 0; 154 + font-family: "Fang", system-ui, sans-serif; 155 + font-size: 1.2rem; 156 + color: #0a182b; 157 + } 157 158 158 - .player-meta { 159 - margin: 6px 0 0; 160 - font-family: 'Fang', system-ui, sans-serif; 161 - color: #0A182B; 162 - opacity: 0.7; 163 - font-size: 0.85rem; 164 - } 159 + .player-meta { 160 + margin: 6px 0 0; 161 + font-family: "Fang", system-ui, sans-serif; 162 + color: #0a182b; 163 + opacity: 0.7; 164 + font-size: 0.85rem; 165 + } 165 166 166 - .creator-tag { 167 - color: #3992FF; 168 - text-decoration: underline; 169 - } 167 + .creator-tag { 168 + color: #3992ff; 169 + text-decoration: underline; 170 + } 170 171 171 - .dot { 172 - margin: 0 4px; 173 - color: #0A182B; 174 - opacity: 0.4; 175 - } 172 + .dot { 173 + margin: 0 4px; 174 + color: #0a182b; 175 + opacity: 0.4; 176 + } 176 177 177 - .close-btn { 178 - background: #0A182B; 179 - color: #39FF44; 180 - border: 2px solid #39FF44; 181 - border-radius: 50%; 182 - width: 36px; 183 - height: 36px; 184 - font-size: 1rem; 185 - cursor: pointer; 186 - flex-shrink: 0; 187 - font-family: 'Fang', system-ui, sans-serif; 188 - transition: background 0.15s, color 0.15s; 189 - } 178 + .close-btn { 179 + background: #0a182b; 180 + color: #39ff44; 181 + border: 2px solid #39ff44; 182 + border-radius: 50%; 183 + width: 36px; 184 + height: 36px; 185 + font-size: 1rem; 186 + cursor: pointer; 187 + flex-shrink: 0; 188 + font-family: "Fang", system-ui, sans-serif; 189 + transition: 190 + background 0.15s, 191 + color 0.15s; 192 + } 190 193 191 - .close-btn:hover { 192 - background: #FF3992; 193 - border-color: #FF3992; 194 - color: #FFDEED; 195 - } 194 + .close-btn:hover { 195 + background: #ff3992; 196 + border-color: #ff3992; 197 + color: #ffdeed; 198 + } 196 199 197 - .grid { 198 - display: grid; 199 - grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr)); 200 - gap: clamp(32px, 5vw, 50px); 201 - padding: 20px clamp(16px, 3vw, 24px); 202 - } 200 + .grid { 201 + display: grid; 202 + grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr)); 203 + gap: clamp(32px, 5vw, 50px); 204 + padding: 20px clamp(16px, 3vw, 24px); 205 + } 203 206 204 - .load-more { 205 - text-align: center; 206 - padding: 30px; 207 - } 207 + .load-more { 208 + text-align: center; 209 + padding: 30px; 210 + } 208 211 209 - .load-more button { 210 - background: #0A182B; 211 - color: #39FF44; 212 - border: 3px solid #0A182B; 213 - padding: 12px 36px; 214 - border-radius: 40px; 215 - font-family: 'PicNic', cursive, system-ui; 216 - font-size: 1.1rem; 217 - cursor: pointer; 218 - transition: all 0.2s ease; 219 - letter-spacing: 0.5px; 220 - } 212 + .load-more button { 213 + background: #0a182b; 214 + color: #39ff44; 215 + border: 3px solid #0a182b; 216 + padding: 12px 36px; 217 + border-radius: 40px; 218 + font-family: "PicNic", cursive, system-ui; 219 + font-size: 1.1rem; 220 + cursor: pointer; 221 + transition: all 0.2s ease; 222 + letter-spacing: 0.5px; 223 + } 221 224 222 - .load-more button:hover { 223 - background: #39FF44; 224 - color: #0A182B; 225 - border-color: #0A182B; 226 - } 225 + .load-more button:hover { 226 + background: #39ff44; 227 + color: #0a182b; 228 + border-color: #0a182b; 229 + } 227 230 228 - .load-more button:disabled { 229 - background: #1A8C22; 230 - color: #0A182B; 231 - border-color: #1A8C22; 232 - cursor: wait; 233 - opacity: 0.6; 234 - } 231 + .load-more button:disabled { 232 + background: #1a8c22; 233 + color: #0a182b; 234 + border-color: #1a8c22; 235 + cursor: wait; 236 + opacity: 0.6; 237 + } 235 238 236 - .error { 237 - background: rgba(255, 57, 146, 0.15); 238 - color: #FF3992; 239 - padding: 12px 16px; 240 - border-radius: 8px; 241 - margin: 0 10px 20px; 242 - font-family: 'Fang', system-ui, sans-serif; 243 - border: 2px solid #FF3992; 244 - } 239 + .error { 240 + background: rgba(255, 57, 146, 0.15); 241 + color: #ff3992; 242 + padding: 12px 16px; 243 + border-radius: 8px; 244 + margin: 0 10px 20px; 245 + font-family: "Fang", system-ui, sans-serif; 246 + border: 2px solid #ff3992; 247 + } 245 248 246 - /* Fluid values handle all sizes — no hard breakpoints needed */ 249 + /* Fluid values handle all sizes — no hard breakpoints needed */ 247 250 </style>