/ index.html
index.html
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <!-- Immediate redirect for development environments --> 5 <script> 6 if (window.location.href.includes('deploy-preview') || 7 window.location.href.includes('localhost')) { 8 window.location.replace('app/'); 9 } 10 </script> 11 <meta charset="UTF-8"> 12 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> 13 <meta name="description" content="A minimalistic, elegant video player built to enjoy a seamless shuffle stream of Daoko MVs"> 14 <meta name="theme-color" content="#000000"> 15 <meta name="apple-mobile-web-app-capable" content="yes"> 16 <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> 17 <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"> 18 <meta http-equiv="Pragma" content="no-cache"> 19 <meta http-equiv="Expires" content="0"> 20 <!-- SEO stuff --> 21 <meta property="og:type" content="website"> 22 <meta property="og:url" content="https://xn--vck1b.shop/"> 23 <meta property="og:title" content="DaokoTube"> 24 <meta property="og:description" content="A minimalistic, elegant video player built to enjoy a seamless shuffle stream of Daoko MVs"> 25 <meta property="og:image" content="https://xn--vck1b.shop/app/assets/social-preview.svg"> 26 <meta property="og:image:alt" content="DaokoTube Logo"> 27 <meta property="twitter:card" content="summary_large_image"> 28 <meta property="twitter:url" content="https://xn--vck1b.shop/"> 29 <meta property="twitter:title" content="DaokoTube"> 30 <meta property="twitter:description" content="A minimalistic, elegant video player built to enjoy a seamless shuffle stream of Daoko MVs"> 31 <meta property="twitter:image" content="https://xn--vck1b.shop/app/assets/social-preview.svg"> 32 <meta property="twitter:image:alt" content="DaokoTube Logo"> 33 <title>DaokoTube - Connecting...</title> 34 <link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,%3Csvg%20width%3D%2732px%27%20height%3D%2732px%27%20viewBox%3D%270%200%2032%2032%27%20xmlns%3D%27http%3A//www.w3.org/2000/svg%27%3E%3Cpath%20fill%3D%27%231E88E5%27%20d%3D%27M16%2028.261c0%200-14-7.926-14-17.413%200-5.016%203.836-6.609%207.043-6.609%202.282%200%204.368%201.076%206.957%203.739%202.589-2.663%204.675-3.739%206.957-3.739%203.207%200%207.043%201.593%207.043%206.609%200%209.487-14%2017.413-14%2017.413z%27/%3E%3C/svg%3E" /> 35 <script type="application/ld+json"> 36 { 37 "@context": "https://schema.org", 38 "@type": "WebApplication", 39 "name": "DaokoTube", 40 "description": "A minimalistic, elegant video player built to enjoy a seamless shuffle stream of Daoko MVs", 41 "url": "https://xn--vck1b.shop/", 42 "applicationCategory": "MultimediaApplication", 43 "operatingSystem": "Any", 44 "offers": { 45 "@type": "Offer", 46 "price": "0", 47 "priceCurrency": "USD" 48 }, 49 "author": { 50 "@type": "Person", 51 "name": "daoch4n" 52 } 53 } 54 </script> 55 <style> 56 :root { 57 --hue: 320; 58 --primary: hsl(var(--hue), 95%, 60%); 59 --primary-glow: hsla(var(--hue), 95%, 60%, 0.6); 60 --secondary: hsl(var(--hue), 70%, 95%); 61 --background: hsl(var(--hue), 20%, 8%); 62 --bg-color: #000; 63 --text-color: #f0f0f0; 64 --error: #ff4d4f; 65 --success: #4caf50; 66 --cursor-butterfly: url('https://img.icons8.com/neon/24/butterfly.png') 12 12, auto; 67 } 68 * { 69 box-sizing: border-box; 70 margin: 0; 71 padding: 0; 72 } 73 html, 74 body { 75 height: 100%; 76 width: 100%; 77 overflow: hidden; 78 } 79 body { 80 background: linear-gradient(135deg, var(--background) 0%, var(--bg-color) 100%); 81 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 82 color: var(--text-color); 83 display: flex; 84 justify-content: center; 85 align-items: center; 86 flex-direction: column; 87 text-align: center; 88 cursor: var(--cursor-butterfly); 89 } 90 .headline { 91 font-size: 2rem; 92 font-weight: bold; 93 margin-bottom: 0.5em; 94 color: var(--primary); 95 letter-spacing: 0.01em; 96 } 97 .description { 98 font-size: 1.1rem; 99 margin-bottom: 1.2em; 100 color: var(--secondary); 101 opacity: 0.95; 102 } 103 .loader-container { 104 position: relative; 105 width: 200px; 106 height: 200px; 107 display: flex; 108 justify-content: center; 109 align-items: center; 110 margin-bottom: 20px; 111 } 112 .logo-container { 113 position: absolute; 114 width: 100px; 115 height: 100px; 116 z-index: 2; 117 } 118 .logo-svg { 119 width: 100%; 120 height: 100%; 121 fill: var(--primary); 122 filter: drop-shadow(0 0 8px var(--primary-glow)); 123 } 124 .spinner { 125 position: absolute; 126 width: 150px; 127 height: 150px; 128 border-radius: 50%; 129 border: 4px solid transparent; 130 border-top-color: var(--primary); 131 border-right-color: hsla(var(--hue), 95%, 60%, 0.6); 132 border-bottom-color: hsla(var(--hue), 95%, 60%, 0.3); 133 border-left-color: hsla(var(--hue), 95%, 60%, 0.6); 134 animation: spin 1.5s linear infinite; 135 z-index: 1; 136 } 137 .status-text { 138 margin-top: 10px; 139 font-size: 1.1em; 140 opacity: 0.9; 141 max-width: 90%; 142 } 143 .endpoints-group { 144 width: 100%; 145 max-width: 420px; 146 margin: 0 auto 0.5em auto; 147 } 148 .endpoints-label { 149 font-size: 1rem; 150 font-weight: 600; 151 color: var(--primary); 152 margin: 0.5em 0 0.2em 0; 153 text-align: left; 154 opacity: 0.85; 155 } 156 .endpoints { 157 margin-top: 0.2em; 158 display: flex; 159 flex-wrap: wrap; 160 justify-content: center; 161 gap: 8px; 162 max-width: 100%; 163 opacity: 0.9; 164 } 165 .endpoint { 166 padding: 10px 16px; 167 border-radius: 16px; 168 background-color: rgba(255, 255, 255, 0.1); 169 font-size: 1rem; 170 min-width: 44px; 171 min-height: 44px; 172 white-space: nowrap; 173 transition: background-color 0.3s, opacity 0.3s, transform 0.2s; 174 cursor: var(--cursor-butterfly); 175 outline: none; 176 border: none; 177 display: flex; 178 align-items: center; 179 justify-content: center; 180 } 181 .endpoint.testing { 182 background-color: rgba(255, 255, 255, 0.2); 183 animation: pulse-text 1.5s infinite ease-in-out; 184 transform: scale(1.05); 185 } 186 .endpoint.success { 187 background-color: var(--success); 188 font-weight: bold; 189 opacity: 1; 190 color: var(--secondary); 191 transform: scale(1.1); 192 box-shadow: 0 0 0 3px var(--primary-glow); 193 } 194 .endpoint.failed { 195 background-color: var(--error); 196 text-decoration: line-through; 197 opacity: 0.5; 198 color: #fff; 199 } 200 @keyframes spin { 201 to { transform: rotate(360deg); } 202 } 203 @keyframes pulse-logo { 204 50% { transform: scale(1.08); opacity: 0.85; } 205 } 206 @keyframes pulse-text { 207 50% { opacity: 1; } 208 } 209 .logo-g-play { 210 animation: pulse-logo 2s infinite ease-in-out; 211 transform-origin: center; 212 } 213 .fallback { 214 display: none; 215 margin-top: 2em; 216 background: rgba(0,0,0,0.7); 217 border-radius: 12px; 218 padding: 1.2em 1em; 219 color: var(--text-color); 220 max-width: 420px; 221 width: 100%; 222 box-shadow: 0 2px 12px rgba(0,0,0,0.2); 223 } 224 .fallback.active { 225 display: block; 226 } 227 .fallback p { 228 margin-bottom: 0.7em; 229 } 230 .fallback ul { 231 list-style: none; 232 padding: 0; 233 margin: 0 0 1em 0; 234 display: flex; 235 flex-wrap: wrap; 236 gap: 0.5em; 237 justify-content: center; 238 } 239 .fallback li { 240 margin: 0; 241 } 242 .fallback a { 243 color: var(--primary); 244 text-decoration: underline; 245 font-weight: 500; 246 font-size: 1rem; 247 } 248 .fallback button { 249 margin: 0.5em 0.5em 0.5em 0; 250 padding: 0.5em 1.2em; 251 font-size: 1rem; 252 border-radius: 8px; 253 border: none; 254 background: var(--primary); 255 color: #fff; 256 cursor: pointer; 257 font-weight: 600; 258 transition: background 0.2s; 259 } 260 .fallback button:hover { 261 background: var(--success); 262 } 263 .fallback .report-link { 264 display: inline-block; 265 margin-top: 0.5em; 266 color: var(--secondary); 267 font-size: 0.95em; 268 } 269 .endpoint:focus, .fallback button:focus, .fallback a:focus { 270 outline: 2px solid var(--primary); 271 outline-offset: 2px; 272 } 273 .noscript-message { 274 position: fixed; 275 top: 0; 276 left: 0; 277 width: 100%; 278 height: 100%; 279 background: var(--background); 280 color: var(--text-color); 281 display: flex; 282 flex-direction: column; 283 justify-content: center; 284 align-items: center; 285 text-align: center; 286 padding: 20px; 287 z-index: 9999; 288 } 289 .noscript-message h2 { 290 margin-bottom: 20px; 291 color: var(--primary); 292 font-size: 1.8rem; 293 } 294 .noscript-message p { 295 margin: 10px 0; 296 max-width: 80%; 297 font-size: 1.2rem; 298 } 299 @media (max-width: 600px) { 300 .headline { font-size: 1.3rem; } 301 .description { font-size: 1rem; } 302 .loader-container { width: 140px; height: 140px; } 303 .logo-container { width: 70px; height: 70px; } 304 .spinner { width: 100px; height: 100px; } 305 .endpoints-group { max-width: 98vw; } 306 .fallback { max-width: 98vw; } 307 } 308 </style> 309 </head> 310 <body> 311 <noscript> 312 <div class="noscript-message"> 313 <h2>πΎ JavaScript Required πΎ</h2> 314 <p>πΎ DaokoTube requires JavaScript to be enabled in your browser. πΎ</p> 315 <p>πΎ Please enable JavaScript and reload the page to continue. πΎ</p> 316 </div> 317 </noscript> 318 <div class="headline" id="headline">Connecting you to DaokoTube</div> 319 <div class="description" id="description"> 320 We're connecting you to the best available DaokoTube mirror for speed and reliability.<br> 321 This ensures you always get the fastest, most reliable experience. 322 </div> 323 <div class="loader-container"> 324 <div class="spinner"></div> 325 <div class="logo-container" style="cursor: pointer;" onclick="window.location.href='app/index.html'" title="Go to local app"> 326 <!-- SVG Logo --> 327 <svg class="logo-svg" viewBox="0 0 100 35" xmlns="http://www.w3.org/2000/svg"> 328 <title>DaokoTube Logo - Click to go to local app</title> 329 <g class="logo-g-play"> 330 <path d="M52.83 2.35l-17.5 30.31h35L52.83 2.35z" /> 331 </g> 332 <g class="logo-g-left"> 333 <path d="M15.15 17.5L0 0v35l15.15-17.5z" /> 334 </g> 335 <g class="logo-g-right"> 336 <path d="M84.85 17.5L100 35V0l-15.15 17.5z" /> 337 </g> 338 <g class="logo-g-center-dot"> 339 <circle cx="35" cy="17.5" r="7.78" /> 340 </g> 341 <g class="logo-g-right-dot"> 342 <circle cx="67.5" cy="17.5" r="7.78" /> 343 </g> 344 </svg> 345 </div> 346 </div> 347 <div class="status-text" id="statusText">Checking available mirrors...</div> 348 <div class="endpoints-group" id="web3-group"> 349 <div class="endpoints-label">Decentralized Mirrors</div> 350 <div class="endpoints" id="endpoints-web3"></div> 351 </div> 352 <div class="endpoints-group" id="web2-group"> 353 <div class="endpoints-label">CDN Mirrors</div> 354 <div class="endpoints" id="endpoints-web2"></div> 355 </div> 356 <div class="fallback" id="fallback" style="display: none;"> 357 <p>Click one of the buttons above to choose a mirror.</p> 358 <button id="retry-btn" type="button">Try Again</button> 359 <a href="https://github.com/dtub/DaokoTube/issues/new" class="report-link">Report a problem</a> 360 </div> 361 <script> 362 const ENDPOINTS = [ 363 // Decentralized 364 { n: 'π§ IPFS', u: 'https://k51qzi5uqu5dmi4eyd0kuy7b9fktlz87xys3usvrrdx19qhyqy68476sjs6x1i.ipns.dweb.link/', r: 1, group: 'web3' }, 365 { n: 'π Orbiter', u: 'https://daoko.orbiter.website/', r: 1, group: 'web3' }, 366 // CDN 367 { n: 'π Netlify', u: 'https://daoch4n.is-a.dev/', r: 1, group: 'web2' }, 368 { n: 'πΊ Vercel', u: 'https://xn--vck1b.shop/', r: 1, group: 'web2' }, 369 { n: 'β€οΈβπ₯ Firebase', u: 'https://daokotube.web.app/', r: 1, group: 'web2' }, 370 { n: 'ποΈ Codeberg', u: 'https://daoko.codeberg.page/', r: 1, group: 'web2' }, 371 { n: 'π¦ Gitlab', u: 'https://d-191eee.gitlab.io/', r: 1, group: 'web2' }, 372 { n: 'πββ¬ Github', u: 'https://dtub3.github.io/', r: 1, group: 'web2' }, 373 { n: 'βΎοΈ Infinityfree', u: 'https://daoko.likesyou.org/', r: 1, group: 'web2' }, 374 { n: 'π¦ Surge', u: 'https://daoko.surge.sh/', r: 1, group: 'web2' } 375 ]; 376 const MAX_WAIT_MS = 2222; 377 const FETCH_TIMEOUT_MS = 777; 378 const RETRY_DELAY_MS = 111; 379 const APP_PATH = 'app/'; 380 const START_STAGGER_MS = 15; 381 // --- DOM Elements --- 382 const statusTextElement = document.getElementById('statusText'); 383 const endpointsWeb3 = document.getElementById('endpoints-web3'); 384 const endpointsWeb2 = document.getElementById('endpoints-web2'); 385 const fallbackDiv = document.getElementById('fallback'); 386 const retryBtn = document.getElementById('retry-btn'); 387 // --- State Variables --- 388 let endpointElements = {}; 389 let isDone = false; 390 let fallbackTimeoutId = null; 391 let startTime = 0; 392 // --- Core Functions --- 393 function initialize() { 394 startTime = performance.now(); 395 statusTextElement.textContent = `Checking available mirrors...`; 396 // Create UI elements for each endpoint, grouped 397 ENDPOINTS.forEach(endpoint => { 398 const element = document.createElement('div'); 399 element.className = 'endpoint'; 400 element.setAttribute('tabindex', '0'); 401 element.textContent = endpoint.n; 402 // Make endpoint button clickable 403 const mirrorUrl = endpoint.u.endsWith('/') ? endpoint.u + APP_PATH : endpoint.u + '/' + APP_PATH; 404 element.style.cursor = 'var(--cursor-butterfly)'; 405 element.addEventListener('click', (e) => { 406 window.open(mirrorUrl, '_blank', 'noopener,noreferrer'); 407 }); 408 element.addEventListener('keydown', (e) => { 409 if (e.key === 'Enter' || e.key === ' ') { 410 e.preventDefault(); 411 window.open(mirrorUrl, '_blank', 'noopener,noreferrer'); 412 } 413 }); 414 if (endpoint.group === 'web3') { 415 endpointsWeb3.appendChild(element); 416 } else { 417 endpointsWeb2.appendChild(element); 418 } 419 endpointElements[endpoint.u] = element; 420 }); 421 retryBtn.onclick = () => window.location.reload(); 422 startConnectionTest(); 423 fallbackTimeoutId = setTimeout(triggerFallback, MAX_WAIT_MS); 424 } 425 function startConnectionTest() { 426 statusTextElement.textContent = `Looking for the fastest way to connect you... (max ${Math.round(MAX_WAIT_MS / 1000)}s)`; 427 ENDPOINTS.forEach((endpoint, index) => { 428 setTimeout(() => { 429 testEndpoint(endpoint, 0); 430 }, index * START_STAGGER_MS); 431 }); 432 } 433 async function testEndpoint(endpointConfig, attemptIndex = 0) { 434 const element = endpointElements[endpointConfig.u]; 435 const isRetrying = attemptIndex > 0; 436 if (!isDone && !isRetrying) { 437 element.className = 'endpoint testing'; 438 element.textContent = `${endpointConfig.n}${isRetrying ? ` (retry ${attemptIndex})` : ''}`; 439 } 440 try { 441 await performFetchCheck(endpointConfig); 442 if (!isDone) { 443 isDone = true; 444 clearTimeout(fallbackTimeoutId); 445 element.className = 'endpoint success'; 446 element.innerHTML = `✓ ${endpointConfig.n}`; // checkmark 447 statusTextElement.textContent = `You're being connected via ${endpointConfig.n} mirror!`; 448 redirectTo(endpointConfig); 449 } else { 450 element.className = 'endpoint'; 451 element.style.opacity = '0.6'; 452 element.textContent = endpointConfig.n; 453 } 454 } catch (error) { 455 if (isDone) return; 456 element.textContent = `${endpointConfig.n} (retrying...)`; 457 setTimeout(() => { 458 testEndpoint(endpointConfig, attemptIndex + 1); 459 }, RETRY_DELAY_MS); 460 } 461 } 462 async function performFetchCheck(endpointConfig) { 463 const headController = new AbortController(); 464 const headTimeoutId = setTimeout(() => headController.abort(new Error('Timeout (HEAD)')), FETCH_TIMEOUT_MS); 465 try { 466 const headResponse = await fetch(endpointConfig.u, { 467 method: 'HEAD', 468 mode: 'cors', 469 cache: 'no-store', 470 signal: headController.signal, 471 redirect: 'follow' 472 }); 473 clearTimeout(headTimeoutId); 474 if (headResponse.ok) { 475 return; 476 } 477 } catch (err) { 478 clearTimeout(headTimeoutId); 479 } 480 const getController = new AbortController(); 481 const getTimeoutId = setTimeout(() => getController.abort(new Error('Timeout (GET)')), FETCH_TIMEOUT_MS); 482 try { 483 const getResponse = await fetch(endpointConfig.u, { 484 method: 'GET', 485 mode: 'cors', 486 cache: 'no-store', 487 signal: getController.signal, 488 redirect: 'follow' 489 }); 490 clearTimeout(getTimeoutId); 491 492 if (!getResponse.ok) { 493 throw new Error(`HTTP ${getResponse.status} (GET)`); 494 } 495 return; 496 } catch (err) { 497 clearTimeout(getTimeoutId); 498 throw err; 499 } 500 } 501 function redirectTo(endpointConfig) { 502 let url = endpointConfig.u; 503 if (!url.endsWith('/')) { 504 url += '/'; 505 } 506 const finalUrl = url + APP_PATH; 507 window.location.replace(finalUrl); 508 } 509 function triggerFallback() { 510 if (!isDone) { 511 statusTextElement.textContent = `Click one of the buttons above to choose a mirror.`; 512 Object.entries(endpointElements).forEach(([url, element]) => { 513 const classes = element.classList; 514 if (!classes.contains('success') && !classes.contains('testing')) { 515 const endpointConfig = ENDPOINTS.find(ep => ep.u === url); 516 const originalName = endpointConfig ? endpointConfig.n : 'Endpoint'; 517 element.className = 'endpoint'; 518 element.textContent = originalName; 519 } 520 }); 521 fallbackDiv.style.display = 'block'; 522 } 523 } 524 document.addEventListener('DOMContentLoaded', initialize); 525 </script> 526 </body> 527 </html>