/ 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 = `&#10003; ${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>