vod frog, frog with the vods
3
fork

Configure Feed

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

at main 585 lines 13 kB view raw
1<!-- 2 FrogHeader: Site header with the "vod frog" logo and frog mascot. 3 Includes a lilypad menu (settings + credits) that is: 4 - Always visible fixed bottom-left on desktop 5 - Toggled by tapping the frog on mobile, overlaid on the page 6--> 7<script lang="ts"> 8 import LoginButton from './LoginButton.svelte'; 9 import WavyButton from './WavyButton.svelte'; 10 import WavyBorder from './WavyBorder.svelte'; 11 import { playCroak } from './croak'; 12 import { getSettings, setSoundEnabled, setPacifistMode } from './settings.svelte'; 13 import { getModelStatus, getModelProgress, getModelError, loadModel } from './captions.svelte'; 14 15 let { onHomeClick }: { onHomeClick?: () => void } = $props(); 16 const settings = getSettings(); 17 let menuOpen = $state(false); 18 let showCreditsModal = $state(false); 19 let showSettingsModal = $state(false); 20 21 function handleClick(e: MouseEvent) { 22 if (onHomeClick) { 23 e.preventDefault(); 24 onHomeClick(); 25 } 26 } 27 28 function onFrogClick() { 29 playCroak(); 30 if (window.innerWidth <= 600) { 31 menuOpen = !menuOpen; 32 } 33 } 34 35 function onFrogTouch(e: TouchEvent) { 36 e.preventDefault(); 37 onFrogClick(); 38 } 39 40 function openCredits() { 41 playCroak(); 42 showCreditsModal = true; 43 showSettingsModal = false; 44 menuOpen = false; 45 } 46 47 function closeCredits() { 48 showCreditsModal = false; 49 } 50 51 function openSettings() { 52 playCroak(); 53 showSettingsModal = true; 54 showCreditsModal = false; 55 menuOpen = false; 56 } 57 58 function closeSettings() { 59 showSettingsModal = false; 60 } 61 62 function onBackdropClick(e: MouseEvent) { 63 if (e.target === e.currentTarget) { 64 closeCredits(); 65 closeSettings(); 66 } 67 } 68 69 function onKeyDown(e: KeyboardEvent) { 70 if (e.key === 'Escape') { 71 closeCredits(); 72 closeSettings(); 73 } 74 } 75</script> 76 77<svelte:window onkeydown={onKeyDown} /> 78 79<header class="frog-header"> 80 <div class="login-area"> 81 <LoginButton /> 82 </div> 83 84 <div class="title-area"> 85 <a href="/" class="logo-link" onclick={handleClick}><h1 class="logo-text">vod frog</h1></a> 86 <!-- svelte-ignore a11y_no_noninteractive_element_interactions --> 87 <div class="frog-area"> 88 <img 89 src="/froggie.png" 90 alt="A cute frog" 91 class="header-frog" 92 class:menu-open={menuOpen} 93 onclick={onFrogClick} 94 ontouchend={onFrogTouch} 95 /> 96 <!-- Mobile: menu drops down directly under the frog --> 97 <div class="lily-menu mobile-lily" class:open={menuOpen}> 98 <button class="lily-item" onclick={openSettings}> 99 <img src="/lilymenu.svg" alt="" class="lily-img" /> 100 <span class="lily-label">settings</span> 101 </button> 102 <button class="lily-item" onclick={openCredits}> 103 <img src="/lilymenu.svg" alt="" class="lily-img" /> 104 <span class="lily-label">credits</span> 105 </button> 106 </div> 107 </div> 108 </div> 109 110 <div class="subtitle-lines"> 111 <p class="subtitle">an exploration by</p> 112 <p class="subtitle"><a href="https://witchsky.app/profile/goose.art" target="_blank" class="subtitle-link">@goose.art</a> using <a href="https://witchsky.app/profile/stream.place" target="_blank" class="subtitle-link">@stream.place</a></p> 113 </div> 114</header> 115 116<!-- Desktop: lilypad menu fixed bottom-left --> 117<div class="lily-menu desktop-lily"> 118 <button class="lily-item" onclick={openSettings}> 119 <img src="/lilymenu.svg" alt="" class="lily-img" /> 120 <span class="lily-label">settings</span> 121 </button> 122 <button class="lily-item" onclick={openCredits}> 123 <img src="/lilymenu.svg" alt="" class="lily-img" /> 124 <span class="lily-label">credits</span> 125 </button> 126</div> 127 128{#if showCreditsModal} 129 <!-- svelte-ignore a11y_no_static_element_interactions --> 130 <div class="modal-backdrop" onclick={onBackdropClick}> 131 <div class="modal-content"> 132 <WavyBorder seed="credits-modal" fill="#39FF44" strokeColor="#0A182B" strokeWidth={2.5} padding={48}> 133 <div class="modal-body"> 134 <h2 class="modal-title">credits</h2> 135 <div class="credit-item"> 136 <p class="credit-label">Frog Croaking</p> 137 <p class="credit-author">by DrinkingWindGames</p> 138 <a href="https://freesound.org/s/848549/" target="_blank" class="credit-link"> 139 freesound.org/s/848549/ 140 </a> 141 <p class="credit-license">License: Attribution 4.0</p> 142 </div> 143 <WavyButton seed="close-credits" fill="#0A182B" textColor="#FFDEED" onclick={closeCredits}>close</WavyButton> 144 </div> 145 </WavyBorder> 146 </div> 147 </div> 148{/if} 149 150{#if showSettingsModal} 151 <!-- svelte-ignore a11y_no_static_element_interactions --> 152 <div class="modal-backdrop" onclick={onBackdropClick}> 153 <div class="modal-content"> 154 <WavyBorder seed="settings-modal" fill="#39FF44" strokeColor="#0A182B" strokeWidth={2.5} padding={48}> 155 <div class="modal-body"> 156 <h2 class="modal-title">settings</h2> 157 158 <div class="setting-row"> 159 <span class="setting-label">sound</span> 160 <WavyButton 161 seed="toggle-sound" 162 fill={settings.soundEnabled ? '#39FF44' : '#0A182B'} 163 textColor={settings.soundEnabled ? '#0A182B' : '#FFDEED'} 164 onclick={() => setSoundEnabled(!settings.soundEnabled)} 165 >{settings.soundEnabled ? 'on' : 'off'}</WavyButton> 166 </div> 167 168 <div class="setting-row"> 169 <span class="setting-label">pacifist mode</span> 170 <WavyButton 171 seed="toggle-pacifist" 172 fill={settings.pacifistMode ? '#39FF44' : '#0A182B'} 173 textColor={settings.pacifistMode ? '#0A182B' : '#FFDEED'} 174 onclick={() => setPacifistMode(!settings.pacifistMode)} 175 >{settings.pacifistMode ? 'on' : 'off'}</WavyButton> 176 </div> 177 178 <p class="setting-hint">pacifist mode disables the flies</p> 179 180 <hr class="setting-divider" /> 181 182 <h3 class="setting-section-title">closed captions</h3> 183 184 {#if getModelStatus() === 'ready'} 185 <div class="model-status ready"> 186 <span class="model-dot"></span> 187 model loaded 188 </div> 189 {:else if getModelStatus() === 'loading'} 190 <div class="model-loading"> 191 <p class="model-loading-text">downloading model... {Math.round(getModelProgress())}%</p> 192 <div class="progress-track"> 193 <div class="progress-fill" style="width: {getModelProgress()}%;"></div> 194 </div> 195 </div> 196 {:else if getModelStatus() === 'error'} 197 <p class="model-error">{getModelError()}</p> 198 <WavyButton seed="retry-model" fill="#FF3992" textColor="#FFDEED" onclick={loadModel}>retry</WavyButton> 199 {:else} 200 <WavyButton seed="download-model" fill="#0A182B" textColor="#FFDEED" onclick={loadModel}>download</WavyButton> 201 {/if} 202 203 <p class="setting-hint"> 204 downloads whisper-tiny (~40mb) to run speech-to-text locally in your browser. 205 captions are generated on-device — no audio is sent anywhere. 206 results may be inaccurate, especially with background noise. 207 </p> 208 209 <WavyButton seed="close-settings" fill="#0A182B" textColor="#FFDEED" onclick={closeSettings}>close</WavyButton> 210 </div> 211 </WavyBorder> 212 </div> 213 </div> 214{/if} 215 216<style> 217 .frog-header { 218 padding: 30px 20px 50px; 219 position: relative; 220 } 221 222 .login-area { 223 position: absolute; 224 top: 30px; 225 right: 20px; 226 z-index: 10; 227 } 228 229 .title-area { 230 position: relative; 231 display: inline-block; 232 } 233 234 .logo-link { 235 text-decoration: none; 236 color: inherit; 237 } 238 239 .logo-text { 240 font-family: 'PicNic', cursive, system-ui; 241 font-size: clamp(3rem, 8vw, 5.5rem); 242 color: #0A182B; 243 margin: 0; 244 line-height: 0.9; 245 font-weight: 400; 246 letter-spacing: -1px; 247 position: relative; 248 z-index: 1; 249 } 250 251 .frog-area { 252 position: absolute; 253 right: -140px; 254 top: -50px; 255 z-index: 12; 256 } 257 258 .header-frog { 259 width: clamp(150px, 18vw, 280px); 260 height: auto; 261 transform: rotate(-10deg); 262 transform-origin: center center; 263 filter: drop-shadow(2px 4px 6px rgba(10, 24, 43, 0.3)); 264 transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); 265 } 266 267 268 269 /* ---- Lilypad menu ---- */ 270 .lily-menu { 271 display: flex; 272 flex-direction: column; 273 align-items: flex-start; 274 gap: 8px; 275 } 276 277 .desktop-lily { 278 position: fixed; 279 bottom: 16px; 280 left: 16px; 281 z-index: 100; 282 } 283 284 .mobile-lily { 285 display: none; 286 } 287 288 .lily-item { 289 all: unset; 290 position: relative; 291 display: flex; 292 align-items: center; 293 justify-content: center; 294 cursor: pointer; 295 transition: transform 0.2s ease; 296 } 297 298 .lily-item:hover { 299 transform: scale(1.08) rotate(-3deg); 300 } 301 302 /* Offset alternating pads for a natural stagger */ 303 .lily-item:nth-child(even) { 304 margin-left: 24px; 305 } 306 307 .lily-img { 308 width: clamp(70px, 10vw, 100px); 309 height: auto; 310 filter: drop-shadow(1px 2px 3px rgba(10, 24, 43, 0.25)); 311 } 312 313 .lily-label { 314 position: absolute; 315 font-family: 'PicNic', cursive, system-ui; 316 font-size: clamp(0.7rem, 1.2vw, 0.9rem); 317 color: #0A182B; 318 pointer-events: none; 319 } 320 321 /* Credits modal */ 322 .modal-backdrop { 323 position: fixed; 324 inset: 0; 325 background: rgba(10, 24, 43, 0.6); 326 z-index: 1000; 327 display: flex; 328 align-items: center; 329 justify-content: center; 330 padding: 20px; 331 } 332 333 .modal-content { 334 width: min(500px, 90vw); 335 } 336 337 .modal-body { 338 display: flex; 339 flex-direction: column; 340 align-items: center; 341 text-align: center; 342 gap: 12px; 343 } 344 345 .modal-title { 346 font-family: 'PicNic', cursive, system-ui; 347 font-size: clamp(1.6rem, 3.5vw, 2.2rem); 348 color: #0A182B; 349 margin: 0; 350 } 351 352 /* Settings */ 353 .setting-row { 354 display: flex; 355 align-items: center; 356 justify-content: space-between; 357 width: min(280px, 70vw); 358 gap: 16px; 359 } 360 361 .setting-label { 362 font-family: 'PicNic', cursive, system-ui; 363 font-size: 1.1rem; 364 color: #0A182B; 365 } 366 367 368 .setting-hint { 369 font-family: 'Fang', system-ui, sans-serif; 370 font-size: 0.75rem; 371 color: #0A182B; 372 opacity: 0.5; 373 margin: 0; 374 font-style: italic; 375 max-width: 300px; 376 line-height: 1.5; 377 } 378 379 .setting-divider { 380 border: none; 381 border-top: 1.5px solid rgba(10, 24, 43, 0.15); 382 width: min(280px, 70vw); 383 margin: 8px 0; 384 } 385 386 .setting-section-title { 387 font-family: 'PicNic', cursive, system-ui; 388 font-size: 1.2rem; 389 color: #0A182B; 390 margin: 0; 391 } 392 393 .model-status { 394 font-family: 'Fang', system-ui, sans-serif; 395 font-size: 0.9rem; 396 color: #0A182B; 397 display: flex; 398 align-items: center; 399 gap: 8px; 400 } 401 402 .model-dot { 403 width: 10px; 404 height: 10px; 405 border-radius: 50%; 406 background: #39FF44; 407 box-shadow: 0 0 6px #39FF44; 408 } 409 410 .model-loading { 411 display: flex; 412 flex-direction: column; 413 align-items: center; 414 gap: 6px; 415 width: min(280px, 70vw); 416 } 417 418 .model-loading-text { 419 font-family: 'Fang', system-ui, sans-serif; 420 font-size: 0.85rem; 421 color: #0A182B; 422 margin: 0; 423 } 424 425 .progress-track { 426 width: 100%; 427 height: 8px; 428 background: rgba(10, 24, 43, 0.15); 429 border-radius: 4px; 430 overflow: hidden; 431 } 432 433 .progress-fill { 434 height: 100%; 435 background: #3992FF; 436 border-radius: 4px; 437 transition: width 0.3s ease; 438 } 439 440 .model-error { 441 font-family: 'Fang', system-ui, sans-serif; 442 font-size: 0.8rem; 443 color: #FF3992; 444 margin: 0; 445 } 446 447 .credit-item { 448 display: flex; 449 flex-direction: column; 450 gap: 4px; 451 } 452 453 .credit-label { 454 font-family: 'PicNic', cursive, system-ui; 455 font-size: 1.1rem; 456 color: #0A182B; 457 margin: 0; 458 } 459 460 .credit-author { 461 font-family: 'Fang', system-ui, sans-serif; 462 font-size: 0.95rem; 463 color: #0A182B; 464 margin: 0; 465 opacity: 0.8; 466 } 467 468 .credit-link { 469 font-family: 'Fang', system-ui, sans-serif; 470 font-size: 0.9rem; 471 color: #0A182B; 472 text-decoration: underline; 473 text-decoration-color: #FF3992; 474 transition: color 0.15s; 475 } 476 477 .credit-link:hover { 478 color: #FF3992; 479 } 480 481 .credit-license { 482 font-family: 'Fang', system-ui, sans-serif; 483 font-size: 0.8rem; 484 color: #0A182B; 485 opacity: 0.65; 486 margin: 0; 487 font-style: italic; 488 } 489 490 491 492 .subtitle-lines { 493 margin-top: 6px; 494 position: relative; 495 z-index: 0; 496 } 497 498 .subtitle-link { 499 color: #0A182B; 500 text-decoration: underline; 501 text-decoration-color: #FF3992; 502 transition: color 0.15s; 503 } 504 505 .subtitle-link:hover { 506 color: #FF3992; 507 } 508 509 .subtitle { 510 font-family: 'PicNic', cursive, system-ui; 511 font-size: clamp(0.85rem, 2vw, 1.15rem); 512 color: #0A182B; 513 margin: 0; 514 line-height: 1.4; 515 opacity: 0.85; 516 } 517 518 /* ---- Mobile ---- */ 519 @media (max-width: 600px) { 520 .frog-header { 521 padding: 20px 16px 8px; 522 } 523 524 .frog-area { 525 right: -75px; 526 top: -35px; 527 } 528 529 .header-frog { 530 width: 150px !important; 531 } 532 533 .header-frog.menu-open { 534 transform: rotate(-80deg) scale(1.05); 535 } 536 537 .desktop-lily { 538 display: none; 539 } 540 541 /* On mobile, menu drops from frog, overlaid */ 542 .mobile-lily { 543 display: flex; 544 position: absolute; 545 top: 100%; 546 left: 0; 547 z-index: 100; 548 pointer-events: none; 549 opacity: 0; 550 transition: opacity 0.3s ease; 551 } 552 553 .mobile-lily.open { 554 pointer-events: auto; 555 opacity: 1; 556 } 557 558 .lily-item { 559 opacity: 0; 560 transform: translateY(-20px) rotate(-6deg); 561 transition: opacity 0.3s ease, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); 562 } 563 564 .lily-menu.open .lily-item { 565 opacity: 1; 566 transform: translateY(0) rotate(0deg); 567 } 568 569 .lily-menu.open .lily-item:nth-child(1) { transition-delay: 0.05s; } 570 .lily-menu.open .lily-item:nth-child(2) { transition-delay: 0.15s; } 571 .lily-menu.open .lily-item:nth-child(3) { transition-delay: 0.25s; } 572 573 .lily-menu:not(.open) .lily-item:nth-child(1) { transition-delay: 0.1s; } 574 .lily-menu:not(.open) .lily-item:nth-child(2) { transition-delay: 0.05s; } 575 .lily-menu:not(.open) .lily-item:nth-child(3) { transition-delay: 0s; } 576 577 .lily-img { 578 width: 90px; 579 } 580 581 .lily-label { 582 font-size: 0.8rem; 583 } 584 } 585</style>