1<!DOCTYPE html> 2<html lang="en"> 3<head> 4<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 5<title>StreamPortal</title> 6<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> 7<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"> 8 9<style> 10:root { 11 --bg: #07090e; 12 --card: #141720; 13 --card-hover: #1e2330; 14 --border: rgba(255, 255, 255, 0.08); 15 --text: #f5f5f7; 16 --muted: #9499a8; 17 --accent: #0066cc; 18 --accent-glow: rgba(0, 102, 204, 0.35); 19 --live: #ff3b30; 20 --live-glow: rgba(255, 59, 48, 0.3); 21 --apple-ease: cubic-bezier(0.2, 0.8, 0.2, 1); 22} 23 24* { box-sizing: border-box; margin: 0; padding: 0; } 25 26body { 27 font-family: 'Inter', -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, sans-serif; 28 background: linear-gradient(180deg, #07090e 0%, #030406 100%); 29 color: var(--text); 30 min-height: 100vh; 31 width: 100%; 32 overflow-x: hidden; 33 -webkit-font-smoothing: antialiased; 34 -moz-osx-font-smoothing: grayscale; 35} 36 37/* ===== HEADER ===== */ 38header { 39 position: sticky; 40 top: 0; 41 z-index: 2000; 42 background: rgba(7, 9, 14, 0.75); 43 backdrop-filter: saturate(180%) blur(24px); 44 -webkit-backdrop-filter: saturate(180%) blur(24px); 45 border-bottom: 1px solid var(--border); 46 width: 100%; 47} 48 49.header-inner { 50 max-width: 1400px; 51 margin: auto; 52 padding: 14px 24px; 53 display: grid; 54 grid-template-columns: 1fr auto 1fr; 55 align-items: center; 56 gap: 20px; 57 width: 100%; 58} 59 60.logo { 61 font-weight: 800; 62 font-size: 22px; 63 letter-spacing: -0.8px; 64 user-select: none; 65 justify-self: start; 66 display: flex; 67 align-items: center; 68 color: #fff; 69 text-decoration: none; 70} 71.logo span { 72 color: var(--accent); 73} 74 75/* SEARCH BAR */ 76.search-container { 77 display: flex; 78 align-items: center; 79 position: relative; 80 width: 420px; 81 max-width: 100%; 82 justify-self: center; 83} 84 85.search-wrap { 86 position: relative; 87 width: 100%; 88 display: flex; 89 align-items: center; 90 padding: 12px 16px 12px 42px; 91 border-radius: 999px; 92 background: rgba(255, 255, 255, 0.06); 93 border: 1px solid transparent; 94 transition: all 0.3s var(--apple-ease); 95} 96 97.search-wrap:focus-within { 98 background: rgba(255, 255, 255, 0.1); 99 border-color: var(--accent); 100 box-shadow: 0 0 0 4px var(--accent-glow); 101} 102 103.search-wrap input { 104 width: 100%; 105 background: transparent; 106 border: none; 107 outline: none; 108 color: var(--text); 109 font-size: 15px; 110 font-weight: 500; 111 min-width: 0; 112} 113 114.search-wrap input::placeholder { 115 color: var(--muted); 116} 117 118.search-wrap::before { 119 content: ""; 120 position: absolute; 121 left: 16px; 122 top: 50%; 123 transform: translateY(-50%); 124 width: 16px; 125 height: 16px; 126 background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239499a8' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E") no-repeat center / contain; 127} 128 129#clearSearch { 130 background: none; 131 border: none; 132 color: var(--muted); 133 font-size: 20px; 134 cursor: pointer; 135 padding: 0 4px; 136 opacity: 0; 137 pointer-events: none; 138 transition: opacity 0.2s; 139} 140 141.search-modal-dropdown { 142 position: absolute; 143 top: calc(100% + 12px); 144 left: 0; 145 width: 100%; 146 max-height: 360px; 147 background: rgba(20, 23, 32, 0.95); 148 backdrop-filter: blur(20px); 149 -webkit-backdrop-filter: blur(20px); 150 border: 1px solid var(--border); 151 border-radius: 16px; 152 box-shadow: 0 24px 48px rgba(0,0,0,0.8); 153 overflow-y: auto; 154 display: none; 155 z-index: 99999; 156 scrollbar-width: none; 157 animation: dropdownFade 0.3s var(--apple-ease); 158} 159 160@keyframes dropdownFade { 161 from { opacity: 0; transform: translateY(-10px) scale(0.98); } 162 to { opacity: 1; transform: translateY(0) scale(1); } 163} 164 165.search-modal-dropdown::-webkit-scrollbar { display: none; } 166.search-modal-dropdown.active { display: block; } 167 168.search-modal-item { 169 display: flex; 170 align-items: center; 171 gap: 14px; 172 padding: 12px 16px; 173 cursor: pointer; 174 border-bottom: 1px solid rgba(255,255,255,0.04); 175 transition: background 0.2s; 176} 177.search-modal-item:hover { background: rgba(255, 255, 255, 0.06); } 178.search-modal-item img { width: 56px; height: 32px; object-fit: cover; border-radius: 6px; background: #000; flex-shrink: 0; } 179.search-modal-item .title { font-size: 14px; font-weight: 600; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } 180.search-modal-item .tag { font-size: 12px; color: var(--muted); font-weight: 500; margin-top: 2px;} 181 182.header-actions { 183 display: flex; 184 align-items: center; 185 gap: 10px; 186 justify-self: end; 187} 188 189.multiview-btn, .discord-btn { 190 padding: 10px 20px; 191 border-radius: 999px; 192 font-size: 14px; 193 font-weight: 600; 194 color: #fff; 195 cursor: pointer; 196 border: none; 197 transition: all 0.3s var(--apple-ease); 198 text-decoration: none; 199 display: inline-flex; 200 align-items: center; 201 gap: 6px; 202 white-space: nowrap; 203} 204 205/* Hidden by default on small screens/mobile, shown only on PC/larger screens */ 206.multiview-btn { 207 display: none; 208 background: var(--accent); 209 box-shadow: 0 4px 14px var(--accent-glow); 210} 211@media (min-width: 1024px) { 212 .multiview-btn { 213 display: inline-flex; 214 } 215} 216.multiview-btn:hover { 217 transform: scale(1.03); 218 background: #0055b3; 219 box-shadow: 0 6px 20px var(--accent-glow); 220} 221 222.discord-btn { 223 background: #5865F2; 224 box-shadow: 0 4px 14px rgba(88, 101, 242, 0.35); 225} 226.discord-btn:hover { 227 transform: scale(1.03); 228 background: #4752C4; 229 box-shadow: 0 6px 20px rgba(88, 101, 242, 0.4); 230} 231 232/* ===== HERO COMPONENT ===== */ 233.hero { 234 position: relative; 235 min-height: 45vh; 236 display: flex; 237 align-items: center; 238 justify-content: center; 239 overflow: hidden; 240 padding: 60px 24px; 241 width: 100%; 242} 243.hero-bg { 244 position: absolute; 245 inset: 0; 246 background: 247 linear-gradient(to top, #07090e 0%, rgba(7,9,14,0.3) 100%), 248 radial-gradient(circle at 50% 0%, var(--accent-glow), transparent 60%),
249 url('https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR4MyNgvVqalCBdUOz55t4X_Ef3ALCLN4nHxl4ede4XlIKAL5rSy3Bewik&s=10') center/cover no-repeat; 250 z-index: 0; 251 opacity: 0.6; 252} 253.hero-content { 254 position: relative; 255 z-index: 1; 256 max-width: 800px; 257 width: 100%; 258 display: flex; 259 flex-direction: column; 260 align-items: center; 261 text-align: center; 262} 263.hero-badge { 264 display: inline-flex; 265 align-items: center; 266 gap: 8px; 267 padding: 6px 14px; 268 border-radius: 999px; 269 background: rgba(255, 255, 255, 0.08); 270 border: 1px solid rgba(255, 255, 255, 0.15); 271 color: #fff; 272 font-size: 11.5px; 273 font-weight: 700; 274 margin: 0 auto 20px auto; 275 letter-spacing: 1px; 276 text-transform: uppercase; 277 backdrop-filter: blur(12px); 278 text-align: center; 279} 280.hero-badge::before { 281 content: ""; 282 width: 6px; 283 height: 6px; 284 background: var(--live); 285 border-radius: 50%; 286 box-shadow: 0 0 10px var(--live); 287 flex-shrink: 0; 288} 289.hero h1 { 290 font-size: clamp(32px, 6vw, 64px); 291 font-weight: 800; 292 line-height: 1.05; 293 margin-bottom: 16px; 294 width: 100%; 295 letter-spacing: -1.5px; 296 color: #fff; 297} 298.hero p { color: var(--muted); font-size: clamp(15px, 2vw, 17px); max-width: 600px; margin: 0 auto 32px auto; line-height: 1.5; font-weight: 400; } 299.hero-buttons { display: flex; justify-content: center; gap: 14px; width: 100%; } 300.hero-btn { 301 padding: 14px 28px; 302 font-weight: 600; 303 font-size: 15px; 304 border-radius: 999px; 305 border: none; 306 cursor: pointer; 307 transition: all 0.3s var(--apple-ease); 308} 309.hero-btn.primary { 310 background: var(--accent); 311 color: #fff; 312 box-shadow: 0 8px 24px var(--accent-glow); 313} 314.hero-btn.primary:hover { 315 transform: scale(1.04); 316 background: #0055b3; 317 box-shadow: 0 10px 30px var(--accent-glow); 318} 319 320/* ===== MAIN CONTENT GRID ===== */ 321main { max-width: 1400px; margin: auto; padding: 40px 24px; width: 100%; } 322 323#tabs { 324 display: flex; 325 justify-content: flex-start; 326 @media(min-width: 768px) { justify-content: center; } 327 gap: 10px; 328 overflow-x: auto; 329 padding: 0 4px 16px 4px;
330 margin-bottom: 24px; 331 scrollbar-width: thin; 332 scrollbar-color: var(--accent) rgba(255, 255, 255, 0.05); 333 width: 100%; 334 -webkit-overflow-scrolling: touch; 335} 336 337#tabs::-webkit-scrollbar { 338 height: 4px; 339 display: block; 340} 341#tabs::-webkit-scrollbar-track { 342 background: rgba(255, 255, 255, 0.04); 343 border-radius: 4px; 344} 345#tabs::-webkit-scrollbar-thumb { 346 background: var(--accent); 347 border-radius: 4px; 348} 349 350.tab { 351 padding: 10px 20px; 352 border-radius: 999px; 353 cursor: pointer; 354 font-size: 14px; 355 font-weight: 600; 356 color: var(--muted); 357 background: rgba(255, 255, 255, 0.04); 358 border: 1px solid transparent; 359 transition: all 0.3s var(--apple-ease); 360 white-space: nowrap; 361 flex-shrink: 0; 362} 363.tab:hover { 364 color: #fff; 365 background: rgba(255, 255, 255, 0.08); 366} 367.tab.active { 368 color: #fff; 369 background: var(--accent); 370 box-shadow: 0 4px 14px var(--accent-glow); 371} 372 373#streams { 374 display: grid; 375 grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); 376 gap: 28px; 377 width: 100%; 378} 379 380/* Netflix Style Cards */ 381.stream { 382 background: transparent; 383 border-radius: 12px; 384 overflow: hidden; 385 cursor: pointer; 386 display: flex; 387 flex-direction: column; 388 position: relative; 389 transition: all 0.4s var(--apple-ease); 390 width: 100%; 391} 392 393.poster-wrapper { 394 position: relative; 395 width: 100%; 396 aspect-ratio: 16/9; 397 background: #10131c; 398 border-radius: 12px; 399 overflow: hidden; 400 box-shadow: 0 4px 20px rgba(0,0,0,0.5); 401 border: 1px solid transparent; 402 transition: all 0.4s var(--apple-ease); 403} 404 405.poster { 406 width: 100%; 407 height: 100%; 408 object-fit: cover; 409 transition: transform 0.6s var(--apple-ease); 410} 411 412.stream:hover .poster-wrapper { 413 box-shadow: 0 16px 40px rgba(0, 0, 0, 0.8); 414 border: 1px solid rgba(0, 102, 204, 0.4); 415} 416.stream:hover .poster { 417 transform: scale(1.06); 418} 419.stream:hover { 420 transform: translateY(-6px); 421} 422 423.stream-live-badge { 424 position: absolute; 425 top: 12px; 426 left: 12px; 427 background: var(--live); 428 padding: 4px 10px; 429 border-radius: 6px; 430 display: flex; 431 align-items: center; 432 gap: 6px; 433 font-size: 11px; 434 font-weight: 800; 435 color: #fff; 436 letter-spacing: 0.5px; 437 z-index: 2; 438 box-shadow: 0 4px 12px rgba(255, 59, 48, 0.4); 439} 440.stream-live-badge span { 441 width: 6px; 442 height: 6px; 443 background: #fff; 444 border-radius: 50%; 445 animation: pulseLive 1.5s infinite; 446 flex-shrink: 0; 447} 448@keyframes pulseLive { 449 0% { opacity: 1; transform: scale(1); } 450 50% { opacity: 0.4; transform: scale(0.8); } 451 100% { opacity: 1; transform: scale(1); } 452} 453 454/* Viewer Badges */ 455.card-viewers-badge { 456 position: absolute; 457 top: 12px; 458 right: 12px; 459 background: rgba(0, 0, 0, 0.7); 460 backdrop-filter: blur(8px); 461 -webkit-backdrop-filter: blur(8px); 462 padding: 4px 8px; 463 border-radius: 6px; 464 display: inline-flex; 465 align-items: center; 466 gap: 5px; 467 font-size: 11px; 468 font-weight: 700; 469 color: #fff; 470 border: 1px solid rgba(255, 255, 255, 0.1); 471 z-index: 2; 472} 473 474.card-viewers-badge .dot { 475 width: 6px; 476 height: 6px; 477 background: #ff3b30; 478 border-radius: 50%; 479 box-shadow: 0 0 6px #ff3b30; 480} 481 482.theater-viewers-pill { 483 display: inline-flex; 484 align-items: center; 485 gap: 6px; 486 background: rgba(255, 59, 48, 0.15); 487 border: 1px solid rgba(255, 59, 48, 0.3); 488 color: #ff3b30; 489 padding: 8px 14px; 490 border-radius: 999px; 491 font-size: 13px; 492 font-weight: 700; 493} 494 495.theater-viewers-pill .dot { 496 width: 7px; 497 height: 7px; 498 background: #ff3b30; 499 border-radius: 50%; 500 animation: pulseLive 1.5s infinite; 501} 502 503.info { padding: 16px 4px 0 4px; flex: 1; display: flex; flex-direction: column; justify-content: flex-start; gap: 8px; width: 100%; } 504.info strong { display: block; font-size: 16px; font-weight: 700; color: #fff; line-height: 1.3; letter-spacing: -0.2px; word-break: break-word;} 505.meta { font-size: 13px; color: var(--muted); display: flex; align-items: center; justify-content: space-between; font-weight: 500; gap: 8px; } 506 507.countdown { font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 6px; background: rgba(255,255,255,0.06); display: inline-block; letter-spacing: 0.3px; color: #fff; white-space: nowrap;} 508.countdown.live { color: var(--live); background: rgba(255, 59, 48, 0.15); font-weight: 700; } 509.countdown.upcoming { color: var(--text); } 510 511/* ===== THEATER / SIDEBAR ===== */ 512#playerView { display: none; padding: 0 0 24px 0; width: 100%; } 513#playerView.active { display: block; } 514 515.player-layout { 516 display: grid; 517 grid-template-columns: 340px 1fr; 518 gap: 24px; 519 align-items: start; 520 width: 100%; 521} 522 523#sideStreams { 524 display: flex; 525 flex-direction: column; 526 gap: 16px; 527 max-height: calc(100vh - 120px); 528 overflow-y: auto; 529 padding-right: 8px; 530 scrollbar-width: thin; 531 scrollbar-color: rgba(255,255,255,0.2) transparent; 532} 533 534#sideStreams::-webkit-scrollbar { width: 4px; } 535#sideStreams::-webkit-scrollbar-track { background: transparent; } 536#sideStreams::-webkit-scrollbar-thumb { background-color: rgba(255,255,255,0.2); border-radius: 4px; } 537 538#sideStreams .stream { 539 display: flex; 540 flex-direction: row; 541 height: 84px; 542 flex-shrink: 0; 543 padding: 0; 544 gap: 14px; 545 background: transparent; 546 border-radius: 10px; 547 align-items: center; 548 width: 100%; 549} 550 551#sideStreams .poster-wrapper { 552 width: 130px; 553 height: 100%; 554 aspect-ratio: 16/9; 555 border-radius: 10px; 556 flex-shrink: 0; 557} 558 559#sideStreams .info { 560 padding: 0; 561 flex: 1; 562 display: flex; 563 flex-direction: column; 564 justify-content: center; 565 gap: 6px; 566 overflow: hidden; 567 min-width: 0; 568} 569 570#sideStreams .info strong { 571 font-size: 13px; 572 margin: 0; 573 white-space: normal; 574 display: -webkit-box; 575 -webkit-line-clamp: 2; 576 -webkit-box-orient: vertical; 577 overflow: hidden; 578} 579 580#sideStreams .meta { 581 justify-content: flex-start; 582 gap: 8px; 583 font-size: 11px; 584} 585 586.player-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; gap: 16px; width: 100%; flex-wrap: wrap; } 587.player-nav-group { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; } 588.player-header h2 { font-size: 20px; font-weight: 700; color: #fff; letter-spacing: -0.4px; word-break: break-word; } 589 590.back-btn { 591 background: rgba(255,255,255,0.06); 592 border: none; 593 color: white; 594 padding: 10px 16px; 595 border-radius: 999px; 596 cursor: pointer; 597 font-weight: 600; 598 font-size: 14px; 599 transition: all 0.2s var(--apple-ease); 600 display: flex; 601 align-items: center; 602 gap: 6px; 603 flex-shrink: 0; 604} 605.back-btn:hover { background: rgba(255,255,255,0.12); transform: scale(1.05); } 606 607.iframe-wrap { width: 100%; aspect-ratio: 16/9; background: #000; border-radius: 16px; overflow: hidden; position: relative; border: 1px solid rgba(255,255,255,0.05); box-shadow: 0 20px 60px rgba(0,0,0,0.8); } 608.iframe-wrap iframe { width: 100%; height: 100%; border: none; } 609 610.now-card { 611 padding: 16px; 612 background: var(--card); 613 border-radius: 16px; 614 margin-bottom: 8px; 615 display: flex; 616 flex-direction: column; 617 gap: 6px; 618 border: 1px solid rgba(0, 102, 204, 0.3); 619 width: 100%; 620} 621.now-label { font-size: 11px; font-weight: 800; color: var(--accent); letter-spacing: 1px; text-transform: uppercase; } 622.now-title { font-size: 15px; font-weight: 700; color: white; white-space: normal; line-height: 1.4; word-break: break-word; } 623.now-meta { font-size: 12px; color: var(--muted); } 624 625.stream-loader { position: absolute; inset: 0; background: rgba(7,9,14,0.9); display: flex; align-items: center; justify-content: center; z-index: 100; backdrop-filter: blur(10px); } 626.spinner { width: 40px; height: 40px; border: 3px solid rgba(255,255,255,0.1); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s cubic-bezier(0.4, 0, 0.2, 1) infinite; } 627@keyframes spin { to { transform: rotate(360deg); } } 628
629/* ===== SOURCE BACKUP MODALS ===== */ 630.source-modal { position: fixed; inset: 0; background: rgba(3,4,6,0.85); backdrop-filter: saturate(150%) blur(20px); z-index: 3000; display: flex; align-items: center; justify-content: center; padding: 16px; animation: modalFade 0.3s var(--apple-ease); } 631@keyframes modalFade { from { opacity: 0; transform: scale(1.05); } to { opacity: 1; transform: scale(1); } } 632 633.source-box { 634 background: var(--card); 635 padding: 24px; 636 border-radius: 24px; 637 width: 420px; 638 max-width: 100%; 639 max-height: 75vh; 640 overflow-y: auto; 641 box-shadow: 0 40px 80px rgba(0,0,0,0.9); 642 border: 1px solid rgba(0, 102, 204, 0.3); 643} 644 645.source-box::-webkit-scrollbar { width: 4px; } 646.source-box::-webkit-scrollbar-track { background: transparent; } 647.source-box::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 4px; } 648.source-box h3 { margin-bottom: 20px; font-size: 18px; font-weight: 700; letter-spacing: -0.4px; } 649 650.source-option { 651 display: block; 652 width: 100%; 653 padding: 14px 16px; 654 background: rgba(255,255,255,0.03); 655 border: 1px solid transparent; 656 border-radius: 12px; 657 color: white; 658 cursor: pointer; 659 text-align: left; 660 margin-bottom: 10px; 661 font-weight: 600; 662 font-size: 14px; 663 transition: all 0.3s var(--apple-ease); 664} 665.source-option:hover { background: rgba(255,255,255,0.06); transform: scale(1.02); } 666.source-option.active { background: var(--accent); color: #fff; box-shadow: 0 4px 14px var(--accent-glow); } 667 668.player-actions-wrapper { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } 669.source-label-btn { 670 background: rgba(0, 102, 204, 0.2); 671 border: 1px solid rgba(0, 102, 204, 0.5); 672 color: #fff; 673 padding: 10px 20px; 674 border-radius: 999px; 675 font-size: 13px; 676 font-weight: 700; 677 cursor: pointer; 678 transition: all 0.3s var(--apple-ease); 679 letter-spacing: 0.5px; 680} 681.source-label-btn:hover { background: var(--accent); transform: scale(1.05); box-shadow: 0 4px 14px var(--accent-glow); } 682 683.refresh-btn { 684 background: rgba(255,255,255,0.06); 685 border: none; 686 color: white; 687 padding: 10px; 688 border-radius: 50%; 689 cursor: pointer; 690 display: flex; 691 align-items: center; 692 justify-content: center; 693 transition: all 0.3s var(--apple-ease); 694 flex-shrink: 0; 695} 696.refresh-btn:hover { background: rgba(255,255,255,0.12); transform: scale(1.1) rotate(15deg); } 697 698footer { margin-top: 80px; padding: 40px 24px; border-top: 1px solid rgba(255,255,255,0.05); text-align: center; color: var(--muted); font-size: 13px; line-height: 1.6; width: 100%; } 699footer ul { display: flex; justify-content: center; gap: 24px; list-style: none; margin-top: 12px; margin-bottom: 20px; flex-wrap: wrap; } 700footer a { color: #fff; text-decoration: none; font-weight: 600; transition: color 0.2s; } 701footer a:hover { color: var(--accent); } 702 703/* ===== RESPONSIVE MEDIA QUERIES ===== */ 704@media (max-width: 1024px) { 705 .player-layout { grid-template-columns: 1fr !important; } 706 #sideStreams { display: none !important; } 707} 708 709@media (max-width: 768px) { 710 body { overflow-x: hidden !important; width: 100% !important; } 711 main { padding: 20px 16px !important; } 712 #streams { grid-template-columns: 1fr !important; gap: 20px !important; } 713 714 .header-inner { 715 grid-template-columns: 1fr !important; 716 padding: 12px 16px !important; 717 gap: 12px; 718 text-align: center; 719 } 720 .logo { justify-self: center !important; } 721 .search-container { width: 100% !important; margin-top: 0 !important; } 722 .header-actions { justify-self: center !important; width: 100%; justify-content: center; flex-wrap: wrap; } 723 724 .hero { min-height: 35vh; padding: 40px 16px; } 725 .hero h1 { font-size: 30px !important; letter-spacing: -1px; } 726 .hero p { font-size: 14px !important; } 727 728 .player-header { 729 flex-direction: column !important; 730 align-items: stretch !important; 731 gap: 14px !important; 732 margin-bottom: 16px !important; 733 } 734 .player-header h2 { 735 order: -1 !important; 736 width: 100% !important; 737 text-align: left !important; 738 font-size: 18px !important; 739 margin: 0 !important; 740 } 741 742 .player-nav-group, .player-actions-wrapper { 743 display: flex !important; 744 width: 100% !important; 745 justify-content: space-between !important; 746 gap: 10px !important; 747 } 748 749 .iframe-wrap { width: 100% !important; border-radius: 12px; } 750 .source-box { padding: 20px; width: 100%; } 751} 752 753@media (max-width: 480px) { 754 .header-actions { flex-direction: column; width: 100%; } 755 .discord-btn { width: 100%; justify-content: center; } 756 .player-actions-wrapper { flex-wrap: wrap; justify-content: flex-start !important; } 757} 758</style> 759</head> 760<body> 761 762<header> 763 <div class="header-inner"> 764 <a href="/" class="logo">Stream<span>Portal</span></a> 765 <div class="search-container"> 766 <div class="search-wrap"> 767 <input id="globalSearch" type="text" placeholder="Search events, sports..."> 768 <button id="clearSearch">×</button> 769 </div> 770 <div id="searchDropdown" class="search-modal-dropdown"></div> 771 </div> 772 <div class="header-actions"> 773 <a href="https://discord.gg/5XNJzcVxW3" target="_blank" class="discord-btn"> 774 <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994.021-.041.001-.09-.041-.106a13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.927 1.793 8.18
7741.793 12.061 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.028zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg> 775 Discord 776 </a> 777 <a href="https://streamportal.pages.dev/multiview.html" class="multiview-btn">Multi-view</a> 778 </div> 779 </div> 780</header> 781 782<section class="hero" id="heroSection"> 783 <div class="hero-bg"></div> 784 <div class="hero-content"> 785 <div class="hero-badge">LIVE SPORTS · HD STREAMS · MULTI-VIEW</div> 786 <h1>Watch Every Big Match.<br>Instantly.</h1> 787 <p>Fast, clean and reliable live sports streaming with real-time events, multi-view support and backup streams built in.</p> 788 <div class="hero-buttons">
789 <button class="hero-btn primary" onclick="scrollToStreams()">Explore Streams</button> 790 </div> 791 </div> 792</section> 793 794<main> 795 <div id="listView"> 796 <div id="tabs"></div> 797 <div id="streams">Loading Events...</div> 798 </div> 799 800 <div id="playerView"> 801 <div class="player-layout"> 802 <div id="sideStreams"></div> 803 <div id="player"></div> 804 </div> 805 </div> 806 807 <footer> 808 <div>STUCK ON LOADING? USE A FREE VPN HERE</div> 809 <ul> 810 <li><a href="https://chromewebstore.google.com/detail/urban-vpn-proxy/eppiocemhmnlbhjplcgkofciiegomcon" target="_blank">PC</a></li> 811 <li><a href="https://play.google.com/store/apps/details?id=com.free.vpn.super.hotspot.open" target="_blank">Android</a></li> 812 <li><a href="https://apps.apple.com/us/app/vpn-super-unlimited-proxy/id1370293473" target="_blank">Apple</a></li> 813 </ul> 814 Disclaimer: We only link to streams available on the internet. We do not host any content. 815 816 <div style="max-width: 300px; margin: 30px auto 0 auto; width: 100%;"> 817 818 </div> 819 </footer> 820</main> 821
822<script> 823const WORKER_URL = 'https://stream.streamportal1111.workers.dev'; 824const API = `${WORKER_URL}/streams`; 825const fallbackLogo = 'images/logo.jpg'; 826 827let categories = []; 828let countdownInterval = null; 829let activeCategoryId = null; 830 831// Safe Firebase key helper function 832function getStreamId(stream) { 833 return (stream.name || 'stream').replace(/[^a-zA-Z0-9_-]/g, '_'); 834} 835 836function scrollToStreams() { 837 const tabsEl = document.getElementById('tabs'); 838 const headerHeight = document.querySelector('header').offsetHeight; 839 const topPos = tabsEl.getBoundingClientRect().top + window.pageYOffset - headerHeight - 30; 840 window.scrollTo({ top: topPos, behavior: 'smooth' }); 841} 842 843async function init() { 844 try { 845 const res = await fetch(`${API}?t=${Date.now()}`, { cache: 'no-store' }); 846 const data = await res.json(); 847 848 let freshCategories = data.streams || []; 849 850 freshCategories.forEach(cat => { 851 let name = cat.category.trim(); 852 if (name.toLowerCase() === 'fight') { 853 cat.category = 'Fighting'; 854 } 855 if(cat.streams) { 856 cat.streams.forEach(s => s.tag = cat.category); 857 } 858 }); 859 860 freshCategories.forEach(cat => { 861 if (cat.streams) { 862 cat.streams = cat.streams.filter(s => { 863 const hasSources = s.sources && Array.isArray(s.sources) && s.sources.length > 0; 864 if (!hasSources) return false; 865 866 const hasValidLinks = s.sources.every(src => src.source && src.id); 867 return hasValidLinks && !s.noLinksAvailable; 868 }); 869 } 870 }); 871 872const now = Math.floor(Date.now() / 1000); 873const getSportDuration = (tag) => { 874 const t = (tag || '').toLowerCase(); 875 if (t.includes('football') || t.includes('soccer')) return 10800; // 3 hours (covers 90 mins + extra time & penalties) 876 if (t.includes('basketball')) return 12600; // 3.5 hours (handles multiple OTs) 877 if (t.includes('hockey') || t.includes('rugby')) return 12600; // 3.5 hours (handles OTs) 878 if (t.includes('american football')) return 14400; // 4 hours 879 if (t.includes('baseball')) return 14400; // 4 hours 880 if (t.includes('fight')) return 21600; // 6 hours (full cards) 881 if (t.includes('cricket')) return 28800; // 8 hours 882 return 14400; // Default 4 hours 883}; 884 885 freshCategories.forEach(cat => { 886 if (cat.streams) { 887 cat.streams = cat.streams.filter(s => { 888 const start = parseInt(s.starts_at) || 0; 889 let end = parseInt(s.ends_at) || 0; 890 const alwaysLive = s.always_live == '1'; 891 892 if (end === 0 && start > 0) { 893 end = start + getSportDuration(s.tag); 894 } 895 896 if (!alwaysLive && end > 0 && now >= end) { 897 return false; 898 } 899 return true; 900 }); 901 } 902 }); 903 904 freshCategories = freshCategories.filter(cat => cat.streams && cat.streams.length > 0); 905 906 freshCategories.sort((a, b) => { 907 const aIsFootball = a.category.toLowerCase().trim() === 'football'; 908 const bIsFootball = b.category.toLowerCase().trim() === 'football'; 909 if (aIsFootball && !bIsFootball) return -1; 910 if (!aIsFootball && bIsFootball) return 1; 911 return a.category.localeCompare(b.category); 912 }); 913 914 categories = freshCategories; 915 renderTabs(); 916 917 if (activeCategoryId && categories.some(c => c.id == activeCategoryId)) { 918 selectCategory(activeCategoryId); 919 } else if (categories.length > 0) { 920 const footballCat = categories.find(c => c.category.toLowerCase().trim() === 'football'); 921 selectCategory(footballCat ? footballCat.id : categories[0].id); 922 } else { 923 document.getElementById('streams').innerHTML = '<div style="padding: 40px; color: var(--muted); text-align: center; width: 100%; font-size: 16px;">No live streams available right now.</div>'; 924 } 925 } catch (err) { 926 console.error("Sync error:", err); 927 document.getElementById('streams').innerHTML = ` 928 <div style="padding: 40px; color: var(--muted); text-align: center; width: 100%; font-size: 16px; display: flex; flex-direction: column; align-items: center; gap: 16px; grid-column: 1/-1;">
929 <span>Failed to connect to stream server.</span> 930 <button onclick="init()" style="padding: 10px 24px; border-radius: 999px; background: var(--accent); color: #fff; border: none; font-weight: 600; cursor: pointer; box-shadow: 0 4px 14px var(--accent-glow);">Refresh</button> 931 </div> 932 `; 933 } 934} 935 936function renderTabs() { 937 const container = document.getElementById('tabs'); 938 container.innerHTML = ''; 939 categories.forEach(cat => { 940 const div = document.createElement('div'); 941 div.className = 'tab'; 942 div.textContent = cat.category; 943 div.dataset.id = cat.id; 944 div.onclick = () => selectCategory(cat.id); 945 container.appendChild(div); 946 }); 947} 948 949function selectCategory(id) { 950 activeCategoryId = id; 951 document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.id == id)); 952 const cat = categories.find(c => c.id == id); 953 if(cat && cat.streams) { 954 renderGrid(cat.streams); 955 } 956} 957 958function renderGrid(streams) { 959 const container = document.getElementById('streams'); 960 container.innerHTML = ''; 961 962 if (!streams || streams.length === 0) { 963 container.innerHTML = '<div style="padding: 40px; color: var(--muted); text-align: center; width: 100%; grid-column: 1/-1; font-size: 16px;">No streams available right now.</div>'; 964 return; 965 } 966 967const now = Math.floor(Date.now() / 1000); 968const getSportDuration = (tag) => { 969 const t = (tag || '').toLowerCase(); 970 if (t.includes('football') || t.includes('soccer')) return 10800; // 3 hours (covers 90 mins + extra time & penalties) 971 if (t.includes('basketball')) return 12600; // 3.5 hours (handles multiple OTs) 972 if (t.includes('hockey') || t.includes('rugby')) return 12600; // 3.5 hours (handles OTs) 973 if (t.includes('american football')) return 14400; // 4 hours 974 if (t.includes('baseball')) return 14400; // 4 hours 975 if (t.includes('fight')) return 21600; // 6 hours (full cards) 976 if (t.includes('cricket')) return 28800; // 8 hours 977 return 14400; // Default 4 hours 978}; 979 980 const processedStreams = []; 981 982 streams.forEach(s => { 983 const start = parseInt(s.starts_at) || 0; 984 let end = parseInt(s.ends_at) || 0; 985 const alwaysLive = s.always_live == '1'; 986 987 if (end === 0 && start > 0) { 988 end = start + getSportDuration(s.tag); 989 } 990 991 if (!alwaysLive && end > 0 && now >= end) { 992 return; 993 } 994 995 const isLive = alwaysLive || (start <= now && (end === 0 || now < end)); 996 const isUpcoming = start > now; 997 998 let weight = 2; 999 if (isLive) weight = 0; 1000 else if (isUpcoming) weight = 1; 1001 1002 if (weight !== 2) { 1003 processedStreams.push({ 1004 ...s, 1005 _sortWeight: weight, 1006 _computedStart: start, 1007 _computedEnd: end, 1008 _isLive: isLive 1009 }); 1010 } 1011 }); 1012 1013 if (processedStreams.length === 0) { 1014 container.innerHTML = '<div style="padding: 40px; color: var(--muted); text-align: center; width: 100%; grid-column: 1/-1; font-size: 16px;">No active or upcoming streams available.</div>'; 1015 return; 1016 } 1017 1018 processedStreams.sort((a, b) => { 1019 if (a._sortWeight !== b._sortWeight) { 1020 return a._sortWeight - b._sortWeight; 1021 } 1022 return a._computedStart - b._computedStart; 1023 }); 1024 1025 processedStreams.forEach(s => { 1026 const posterSrc = s.poster ? `${WORKER_URL}/image?url=${encodeURIComponent(s.poster)}` : fallbackLogo; 1027 const streamId = getStreamId(s); 1028 const div = document.createElement('div'); 1029 div.className = 'stream'; 1030 1031 div.innerHTML = ` 1032 <div class="poster-wrapper"> 1033 ${s._isLive ? `<div class="stream-live-badge"><span></span>LIVE</div>` : ''} 1034 <div class="card-viewers-badge viewer-badge" style="display: none;"> 1035 <span class="dot"></span> 1036 <span class="stream-viewer-count" data-stream-id="${streamId}">0</span> 1037 </div> 1038 <img class="poster" src="${posterSrc}" onerror="this.src='${fallbackLogo}';" loading="lazy"> 1039 </div> 1040 <div class="info"> 1041 <strong>${s.name}</strong> 1042 <div class="meta">
1043 <span>${s.tag || ''}</span> 1044 <div class="countdown" data-start="${s.starts_at || 0}" data-end="${s.ends_at || 0}" data-always="${s.always_live || 0}"></div> 1045 </div> 1046 </div> 1047 `; 1048 div.onclick = () => openTheater(s); 1049 container.appendChild(div); 1050 }); 1051 1052 if (window.refreshViewerBadges) window.refreshViewerBadges(); 1053 updateTimers(); 1054} 1055 1056function getFilteredSidebarStreams(currentStreamName) { 1057 const now = Math.floor(Date.now() / 1000); 1058 const DAY = 86400; 1059 const exclude = ["Rally TV", "Willow Cricket", "Tennis Channel", "Fox Cricket", "Fox League", "ESPN"]; 1060 1061 const allStreams = categories.flatMap(c => c.streams || []).filter(s => 1062 s.name !== currentStreamName && !exclude.includes(s.name) && s.sources && s.sources.length > 0 && !s.noLinksAvailable 1063 ); 1064 1065 const live = []; 1066 const upcoming = []; 1067 1068 for (const s of allStreams) { 1069 const start = parseInt(s.starts_at) || 0; 1070 const end = parseInt(s.ends_at) || 0; 1071 const always = s.always_live == '1'; 1072 1073 const isLive = always || (start <= now && (!end || now < end)); 1074 const isUpcoming = start && start > now && (start - now <= DAY); 1075 1076 if (isLive) live.push(s); 1077 else if (isUpcoming) upcoming.push(s); 1078 } 1079 1080 live.sort((a, b) => (parseInt(a.starts_at) || 0) - (parseInt(b.starts_at) || 0)); 1081 upcoming.sort((a, b) => (parseInt(a.starts_at) || 0) - (parseInt(b.starts_at) || 0)); 1082 1083 return [...live, ...upcoming].slice(0, 20); 1084} 1085 1086async function openTheater(stream) { 1087 document.getElementById('heroSection').style.display = 'none'; 1088 document.getElementById('listView').style.display = 'none'; 1089 1090 const pv = document.getElementById('playerView'); 1091 pv.classList.add('active'); 1092 window.scrollTo({ top: 0, behavior: 'smooth' }); 1093 1094 const side = document.getElementById('sideStreams'); 1095 side.innerHTML = ` 1096 <div class="now-card"> 1097 <div class="now-label">NOW WATCHING</div> 1098 <div class="now-title">${stream.name}</div> 1099 <div class="now-meta">${stream.tag || ''}</div> 1100 </div> 1101 `; 1102 1103 const targetedStreams = getFilteredSidebarStreams(stream.name); 1104 targetedStreams.forEach(s => { 1105 const sidePosterSrc = s.poster ? `${WORKER_URL}/image?url=${encodeURIComponent(s.poster)}` : fallbackLogo; 1106 const item = document.createElement('div'); 1107 item.className = 'stream'; 1108 item.innerHTML = ` 1109 <div class="poster-wrapper"> 1110 <img class="poster" src="${sidePosterSrc}" onerror="this.src='${fallbackLogo}';" loading="lazy"> 1111 </div> 1112 <div class="info"> 1113 <strong>${s.name}</strong> 1114 <div class="meta"> 1115 <span>${s.tag || ''}</span> 1116 <div class="countdown" data-start="${s.starts_at || 0}" data-end="${s.ends_at || 0}" data-always="${s.always_live || 0}">LIVE</div> 1117 </div> 1118 </div> 1119 `; 1120 item.onclick = () => openTheater(s); 1121 side.appendChild(item); 1122 }); 1123 1124 const player = document.getElementById('player'); 1125 player.innerHTML = ` 1126 <div class="player-header"> 1127 <div class="player-nav-group"> 1128 <button class="back-btn" id="backToGridBtn"> 1129 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg> 1130 Back 1131 </button> 1132 <h2>${stream.name}</h2> 1133 </div> 1134 <div class="player-actions-wrapper"> 1135 <div class="theater-viewers-pill"> 1136 <span class="dot"></span> 1137 <span id="theaterViewerCountValue">1</span> Watching 1138 </div> 1139 <button class="source-label-btn" id="sourceToggleBtn" style="opacity: 0.5; pointer-events: none;">LOADING...</button> 1140 <button class="refresh-btn" id="nextSourceBtn" title="Skip to Next Backup" style="display: none;"> 1141 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 4 15 12 5 20 5 4"></polygon><line x1="19" y1="5" x2="19" y2="19"></line></svg> 1142 </button> 1143 <button class="refresh-btn" id="streamRefreshBtn" title="Refresh Feed Source">
1144 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin='round'><path d='M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67'></path></svg> 1145 </button> 1146 </div> 1147 </div> 1148 <div class="iframe-wrap"><div class="stream-loader"><div class="spinner"></div></div></div> 1149 `; 1150 1151 // Track viewer presence entry for this stream 1152 const streamId = getStreamId(stream); 1153 if (window.enterStreamTracker) window.enterStreamTracker(streamId); 1154 1155 const wrap = player.querySelector('.iframe-wrap'); 1156 let sources = []; 1157 let currentIndex = 0; 1158 let animationFrameId = null; 1159 1160 const loadSource = (idx) => { 1161 if (!sources[idx]) return; 1162 currentIndex = idx; 1163 1164 if (animationFrameId) { 1165 cancelAnimationFrame(animationFrameId); 1166 animationFrameId = null; 1167 } 1168 1169 wrap.innerHTML = '<div class="stream-loader"><div class="spinner"></div></div>'; 1170 1171 if (sources[idx].isPlaceholder || sources[idx].isFinishedPlaceholder) { 1172 const isFinished = sources[idx].isFinishedPlaceholder; 1173 1174 wrap.innerHTML = ` 1175 <div style="position: absolute; inset: 0; background: #07090e; display: flex; flex-direction: column; align-items: center; justify-content: center; overflow: hidden; border-radius: 16px;"> 1176 <canvas id="movingGateCanvas" style="position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; opacity: 0.5;"></canvas> 1177 1178 <div style="position: relative; z-index: 2; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 30px 20px; text-align: center; background: rgba(20, 23, 32, 0.75); backdrop-filter: saturate(150%) blur(30px); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 24px; box-shadow: 0 30px 60px rgba(0,0,0,0.8); max-width: 90%;"> 1179 <div style="position: relative; width: 64px; height: 64px; margin-bottom: 24px; display: flex; align-items: center; justify-content: center;"> 1180 <div style="position: absolute; inset: 0; border: 2px dashed ${isFinished ? '#ff3b30' : 'var(--accent)'}; border-radius: 50%; animation: spin 20s linear infinite; opacity: 0.4;"></div> 1181 <div style="position: absolute; inset: 6px; border: 1px solid ${isFinished ? 'rgba(255, 59, 48, 0.3)' : 'var(--accent-glow)'}; border-radius: 50%; background: ${isFinished ? 'rgba(255, 59, 48, 0.1)' : 'rgba(0, 102, 204, 0.1)'};"></div> 1182 ${isFinished ? 1183 `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ff3b30" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="position: relative; z-index: 3;"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>` : 1184 `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="position: relative; z-index: 3;"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>` 1185 } 1186 </div> 1187 1188 <h3 style="font-size: clamp(18px, 2.5vw, 22px); font-weight: 800; letter-spacing: -0.5px; margin-bottom: 8px; color: #fff;"> 1189 ${isFinished ? 'EVENT CONCLUDED' : 'STREAM STARTING SOON'} 1190 </h3> 1191 <p style="color: var(--muted); font-size: 14.5px; max-width: 360px; margin-bottom: 28px; line-height: 1.5;"> 1192 ${isFinished 1193 ? 'This match has ended. Check out our other live events.' 1194 : ((parseInt(stream.starts_at) || 0) - Math.floor(Date.now() / 1000) <= 900) 1195 ? 'We are waiting for the broadcaster. Tap refresh to check if the stream has started.' 1196 : 'Streams for this event are not yet available. Please check back about 15 minutes before kick-off.' 1197 } 1198 </p> 1199 1200 ${isFinished ? 1201 `<button onclick="document.getElementById('backToGridBtn').click()" style="font-size: 14px; font-weight: 700; padding: 12px 28px; background: var(--accent); color: #fff; border: none; border-radius: 999px; cursor: pointer; transition: all 0.3s; transform-origin: center; box-shadow: 0 4px 14px var(--accent-glow);">Return to Dashboard</button>` : 1202 `<div class="countdown" data-start="${stream.starts_at || 0}" data-end="${stream.ends_at || 0}" data-always="${stream.always_live || 0}" style="font-size: 16px; font-weight: 700; font-family: 'Inter', monospace; letter-spacing: 0.5px; padding: 10px 24px; background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.15); color: #fff; border-radius: 12px;">00H 00M 00S</div>` 1203 } 1204 </div> 1205 </div> 1206 `; 1207 1208 setTimeout(() => { 1209 const canvas = document.getElementById('movingGateCanvas'); 1210 if (!canvas) return; 1211 const ctx = canvas.getContext('2d'); 1212 1213 const resize = () => { 1214 canvas.width = canvas.offsetWidth * window.devicePixelRatio; 1215 canvas.height = canvas.offsetHeight * window.devicePixelRatio; 1216 ctx.scale(window.devicePixelRatio, window.devicePixelRatio); 1217 }; 1218 resize(); 1219 1220 const particles = []; 1221 for (let i = 0; i < 40; i++) { 1222 particles.push({ 1223 x: Math.random() * (canvas.width / window.devicePixelRatio), 1224 y: Math.random() * (canvas.height / window.devicePixelRatio), 1225 size: Math.random() * 2 + 0.5, 1226 speedY: -(Math.random() * 0.3 + 0.1), 1227 alpha: Math.random() * 0.3 + 0.1 1228 }); 1229 } 1230 1231 let wavePhase = 0; 1232 const renderLoop = () => { 1233 if (!document.getElementById('movingGateCanvas')) return; 1234 const w = canvas.width / window.devicePixelRatio; 1235 const h = canvas.height / window.devicePixelRatio; 1236 ctx.clearRect(0, 0, w, h); 1237
1238 particles.forEach(p => { 1239 p.y += p.speedY; 1240 if (p.y < -10) p.y = h + 10; 1241 ctx.fillStyle = isFinished ? `rgba(255, 59, 48, ${p.alpha})` : `rgba(0, 102, 204, ${p.alpha})`; 1242 ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill(); 1243 }); 1244 1245 wavePhase += 0.005; 1246 ctx.fillStyle = isFinished ? 'rgba(255, 59, 48, 0.02)' : 'rgba(0, 102, 204, 0.03)'; 1247 ctx.beginPath(); ctx.moveTo(0, h); 1248 for (let i = 0; i <= w; i += 10) { 1249 const y = h - 40 + Math.sin(i * 0.006 + wavePhase) * 20; 1250 ctx.lineTo(i, y); 1251 } 1252 ctx.lineTo(w, h); ctx.fill(); 1253 1254 animationFrameId = requestAnimationFrame(renderLoop); 1255 }; 1256 renderLoop(); 1257 }, 50); 1258 1259 updateTimers(); 1260 return; 1261 } 1262 1263 const iframe = document.createElement('iframe'); 1264 iframe.src = sources[idx].url; 1265 iframe.allowFullscreen = true; 1266 iframe.onload = () => { 1267 const loader = wrap.querySelector('.stream-loader'); 1268 if (loader) loader.remove(); 1269 }; 1270 wrap.appendChild(iframe); 1271 attachGate(wrap); 1272 }; 1273 1274 if (stream.sources && stream.sources.length > 0) { 1275 try { 1276 const resolvePromises = stream.sources.map(src => 1277 fetch(`${WORKER_URL}/resolve-stream?source=${src.source}&id=${src.id}`).then(r => { 1278 if (!r.ok) return []; 1279 return r.json(); 1280 }).catch(() => []) 1281 ); 1282 1283 const results = await Promise.all(resolvePromises); 1284 results.forEach((streamList) => { 1285 if (Array.isArray(streamList)) { 1286 streamList.forEach((item) => { 1287 if (item.embedUrl) { 1288 const qualityLabel = item.hd ? 'HD' : 'SD'; 1289 const langLabel = item.language ? item.language.toUpperCase() : 'ENG'; 1290 sources.push({ 1291 url: item.embedUrl, 1292 label: `${langLabel} (${qualityLabel})` 1293 }); 1294 } 1295 }); 1296 } 1297 }); 1298 } catch(err) { 1299 console.error("Error parsing backend stream links:", err); 1300 } 1301 } 1302 1303 const now = Math.floor(Date.now() / 1000); 1304 const startTime = parseInt(stream.starts_at) || 0; 1305 let endTime = parseInt(stream.ends_at) || 0; 1306 1307 if (endTime === 0 && startTime > 0) { 1308 endTime = startTime + 14400; 1309 } 1310 1311 if (sources.length === 0) { 1312 stream.noLinksAvailable = true; 1313 if (now >= endTime) { 1314 sources.push({ url: "", label: "Match Finished", isFinishedPlaceholder: true }); 1315 } else { 1316 sources.push({ url: "", label: "Stream Awaiting Start", isPlaceholder: true }); 1317 } 1318 } 1319 1320 const toggleBtn = document.getElementById('sourceToggleBtn'); 1321 if (sources.length > 0 && !sources[0].isPlaceholder && !sources[0].isFinishedPlaceholder) { 1322 toggleBtn.style.opacity = "1"; 1323 toggleBtn.style.pointerEvents = "auto"; 1324 toggleBtn.textContent = "SOURCES"; 1325 loadSource(0); 1326 } else { 1327 const isFinished = now >= endTime; 1328 toggleBtn.style.opacity = "0.5"; 1329 toggleBtn.style.pointerEvents = "none"; 1330 toggleBtn.textContent = isFinished ? "OFFLINE" : "AWAITING FEEDS"; 1331 loadSource(0); 1332 } 1333 1334 const nextBtn = document.getElementById('nextSourceBtn'); 1335 if (sources.length > 1) { 1336 nextBtn.style.display = 'flex'; 1337 } 1338 1339 nextBtn.onclick = () => { 1340 if (sources.length > 1) { 1341 let nextIdx = (currentIndex + 1) % sources.length; 1342 loadSource(nextIdx); 1343 } 1344 }; 1345 1346 document.getElementById('backToGridBtn').onclick = () => { 1347 if (window.exitStreamTracker) window.exitStreamTracker(); 1348 if (animationFrameId) cancelAnimationFrame(animationFrameId); 1349 pv.classList.remove('active'); 1350 document.getElementById('heroSection').style.display = 'flex'; 1351 document.getElementById('listView').style.display = 'block'; 1352 player.innerHTML = ''; 1353 document.getElementById('globalSearch').value = ''; 1354 document.getElementById('clearSearch').style.opacity = '0'; 1355 document.getElementById('searchDropdown').classList.remove('active'); 1356 selectCategory(activeCategoryId); 1357 }; 1358 1359 document.getElementById('streamRefreshBtn').onclick = () => { 1360 const currentNow = Math.floor(Date.now() / 1000); 1361 if (sources[0]?.isPlaceholder && (parseInt(stream.starts_at) || 0) <= currentNow) { 1362 if (animationFrameId) cancelAnimationFrame(animationFrameId); 1363 openTheater(stream); 1364 } else { 1365 loadSource(currentIndex); 1366 } 1367 }; 1368 1369 toggleBtn.onclick = () => { 1370 if (sources.length === 0 || sources[0].isPlaceholder || sources[0].isFinishedPlaceholder) return; 1371 1372 const modal = document.createElement('div'); 1373 modal.className = 'source-modal'; 1374 const box = document.createElement('div'); 1375 box.className = 'source-box'; 1376 box.style.cssText = "display:flex; flex-direction:column; max-height:75vh; height:460px; overflow:hidden; padding:24px; position:relative;"; 1377
1378 const grouped = {}; 1379 sources.forEach((src, originalIdx) => { 1380 let label = src.label.toUpperCase(); 1381 let lang = 'ENGLISH'; 1382 1383 if (/\b(SPANISH|ESP|SPA|ES)\b/.test(label)) lang = 'SPANISH'; 1384 else if (/\b(FRENCH|FRA|FRE|FR)\b/.test(label)) lang = 'FRENCH'; 1385 else if (/\b(PORTUGUESE|POR|PT)\b/.test(label)) lang = 'PORTUGUESE'; 1386 else if (/\b(ITALIAN|ITA|IT)\b/.test(label)) lang = 'ITALIAN'; 1387 else if (/\b(GERMAN|GER|DEU|DE)\b/.test(label)) lang = 'GERMAN'; 1388 else if (/\b(ARABIC|ARA|AR)\b/.test(label)) lang = 'ARABIC'; 1389 else if (/\b(SWEDISH|SWE|SV|SE)\b/.test(label)) lang = 'SWEDISH'; 1390 else if (/\b(DUTCH|NLD|DUT|NL)\b/.test(label)) lang = 'DUTCH'; 1391 else if (/\b(POLISH|POL|PL)\b/.test(label)) lang = 'POLISH'; 1392 else if (/\b(RUSSIAN|RUS|RU)\b/.test(label)) lang = 'RUSSIAN'; 1393 else if (/\b(TURKISH|TUR|TR)\b/.test(label)) lang = 'TURKISH'; 1394 else if (/\b(GREEK|GRE|ELL|EL|GR)\b/.test(label)) lang = 'GREEK'; 1395 else if (/\b(DANISH|DAN|DA|DK)\b/.test(label)) lang = 'DANISH'; 1396 else if (/\b(NORWEGIAN|NOR|NO)\b/.test(label)) lang = 'NORWEGIAN'; 1397 else if (/\b(FINNISH|FIN|FI)\b/.test(label)) lang = 'FINNISH'; 1398 else if (/\b(JAPANESE|JPN|JA|JP)\b/.test(label)) lang = 'JAPANESE'; 1399 else if (/\b(KOREAN|KOR|KO|KR)\b/.test(label)) lang = 'KOREAN'; 1400 else if (/\b(CHINESE|CHI|ZHO|Mandarin|Cantonese|CN|ZH)\b/.test(label)) lang = 'CHINESE'; 1401 else if (/\b(HINDI|HIN|HI)\b/.test(label)) lang = 'HINDI'; 1402 else if (/\b(ROMANIAN|RON|ROM|RO)\b/.test(label)) lang = 'ROMANIAN'; 1403 else if (/\b(HUNGARIAN|HUN|HU)\b/.test(label)) lang = 'HUNGARIAN'; 1404 else if (/\b(CZECH|CES|CZE|CS|CZ)\b/.test(label)) lang = 'CZECH'; 1405 else if (/\b(UKRAINIAN|UKR|UA)\b/.test(label)) lang = 'UKRAINIAN'; 1406 else if (/\b(VIETNAMESE|VIE|VN)\b/.test(label)) lang = 'VIETNAMESE'; 1407 1408 else if (/[-_](ES|FR|PT|IT|DE|AR|SE|NL|PL|RU|TR|GR|DK|NO|FI|JP|KR|CN|HI|RO|HU|CZ|UA|VN)(\b|_)/i.test(label)) {} 1409 1410 if (!grouped[lang]) grouped[lang] = []; 1411 grouped[lang].push({ src, originalIdx }); 1412 }); 1413 1414 const languages = Object.keys(grouped).sort((a, b) => a === 'ENGLISH' ? -1 : b === 'ENGLISH' ? 1 : a.localeCompare(b)); 1415 let activeLang = languages[0]; 1416 1417 const title = document.createElement('h3'); 1418 title.textContent = 'Select Stream Source'; 1419 box.appendChild(title); 1420 1421 const tabsContainer = document.createElement('div'); 1422 tabsContainer.style.cssText = "display:flex; gap:10px; margin-bottom:20px; overflow-x:auto; white-space:nowrap; padding-bottom:6px; flex-shrink:0; scrollbar-width:none;"; 1423 1424 const optionsContainer = document.createElement('div'); 1425 optionsContainer.style.cssText = "display:flex; flex-direction:column; gap:10px; overflow-y:auto; flex-grow:1; padding-right:6px;"; 1426 1427 const renderLanguageOptions = (lang) => { 1428 optionsContainer.innerHTML = ''; 1429 grouped[lang].forEach(({ src, originalIdx }) => { 1430 const b = document.createElement('button'); 1431 b.className = `source-option ${originalIdx === currentIndex ? 'active' : ''}`; 1432 b.textContent = src.label; 1433 b.onclick = () => { loadSource(originalIdx); modal.remove(); }; 1434 optionsContainer.appendChild(b); 1435 }); 1436 }; 1437
1438 languages.forEach((lang) => { 1439 const tabBtn = document.createElement('button'); 1440 tabBtn.textContent = lang; 1441 tabBtn.style.cssText = `flex: 0 0 auto; padding: 8px 16px; border-radius: 999px; font-size: 13px; font-weight: 700; cursor: pointer; white-space: nowrap; border: none; background: ${lang === activeLang ? 'var(--accent)' : 'rgba(255, 255, 255, 0.06)'}; color: #fff; transition: all 0.3s; box-shadow: ${lang === activeLang ? '0 4px 14px var(--accent-glow)' : 'none'};`; 1442 1443 tabBtn.onclick = () => { 1444 activeLang = lang; 1445 Array.from(tabsContainer.children).forEach(btn => { 1446 const isActive = (btn.textContent === lang); 1447 btn.style.background = isActive ? 'var(--accent)' : 'rgba(255, 255, 255, 0.06)'; 1448 btn.style.boxShadow = isActive ? '0 4px 14px var(--accent-glow)' : 'none'; 1449 }); 1450 renderLanguageOptions(lang); 1451 }; 1452 tabsContainer.appendChild(tabBtn); 1453 }); 1454 1455 box.appendChild(tabsContainer); 1456 box.appendChild(optionsContainer); 1457 renderLanguageOptions(activeLang); 1458 1459 modal.appendChild(box); 1460 document.body.appendChild(modal); 1461 modal.onclick = e => { if (e.target === modal) modal.remove(); }; 1462 }; 1463 updateTimers(); 1464} 1465 1466function attachGate(wrap) { 1467 wrap.querySelectorAll('.stream-gate').forEach(g => g.remove()); 1468 const iframe = wrap.querySelector('iframe'); 1469 if (!iframe) return; 1470 1471 iframe.style.pointerEvents = "none"; 1472 const links = ["https://hai8g.com/4/11608660", "https://hai8g.com/4/11610289"]; 1473 let clicks = 0; 1474 1475 const gate = document.createElement('div'); 1476 Object.assign(gate.style, { 1477 position: 'absolute', inset: '0', zIndex: '999', background: 'transparent', cursor: 'pointer' 1478 }); 1479 1480 gate.onclick = (e) => { 1481 e.preventDefault(); e.stopPropagation(); 1482 window.open(links[clicks], '_blank'); 1483 clicks++; 1484 if(clicks >= 2) { 1485 gate.remove(); 1486 iframe.style.pointerEvents = "auto"; 1487 } 1488 }; 1489 wrap.appendChild(gate); 1490} 1491 1492function updateTimers() { 1493 const now = Math.floor(Date.now() / 1000); 1494 document.querySelectorAll('.countdown').forEach(el => { 1495 const start = parseInt(el.dataset.start), end = parseInt(el.dataset.end), always = el.dataset.always == '1'; 1496 if(always || (start <= now && (!end || now < end))) { 1497 el.textContent = "LIVE"; el.className = 'countdown live'; 1498 } else if (start > now) { 1499 const diff = start - now; 1500 const m = Math.floor(diff / 60) % 60, h = Math.floor(diff / 3600); 1501 el.textContent = `${h}h ${m}m`; el.className = 'countdown upcoming'; 1502 } else { 1503 el.textContent = "LIVE"; el.className = 'countdown live'; 1504 } 1505 }); 1506} 1507 1508const searchInput = document.getElementById('globalSearch'); 1509const clearSearch = document.getElementById('clearSearch'); 1510const searchDropdown = document.getElementById('searchDropdown'); 1511 1512searchInput.addEventListener('input', () => { 1513 const val = searchInput.value.toLowerCase().trim(); 1514 if(!val) { 1515 clearSearch.style.opacity = '0'; 1516 clearSearch.style.pointerEvents = 'none'; 1517 searchDropdown.classList.remove('active'); 1518 if (document.getElementById('playerView').classList.contains('active') === false) { 1519 selectCategory(activeCategoryId); 1520 } 1521 return; 1522 } 1523 clearSearch.style.opacity = '1'; 1524 clearSearch.style.pointerEvents = 'auto'; 1525 1526 const dataset = categories.flatMap(c => c.streams || []).filter(s => s.sources && s.sources.length > 0 && !s.noLinksAvailable); 1527 const matching = dataset.filter(s => s.name.toLowerCase().includes(val) || (s.tag && s.tag.toLowerCase().includes(val))); 1528 1529 if (document.getElementById('playerView').classList.contains('active')) { 1530 searchDropdown.innerHTML = ''; 1531 if(!matching.length) { 1532 searchDropdown.innerHTML = '<div style="padding: 16px; font-size: 14px; color: var(--muted); text-align: center;">No matches found</div>'; 1533 } else { 1534 matching.slice(0, 10).forEach(s => { 1535 const item = document.createElement('div'); 1536 item.className = 'search-modal-item'; 1537 const imgUrl = s.poster ? `${WORKER_URL}/image?url=${encodeURIComponent(s.poster)}` : fallbackLogo; 1538 item.innerHTML = ` 1539 <img src="${imgUrl}" onerror="this.src='${fallbackLogo}';"> 1540 <div> 1541 <div class="title">${s.name}</div> 1542 <div class="tag">${s.tag || ''}</div> 1543 </div> 1544 `; 1545 item.onclick = () => { 1546 openTheater(s); 1547 searchInput.value = ''; 1548 clearSearch.style.opacity = '0'; 1549 searchDropdown.classList.remove('active'); 1550 }; 1551 searchDropdown.appendChild(item); 1552 }); 1553 } 1554 searchDropdown.classList.add('active'); 1555 } else { 1556 searchDropdown.classList.remove('active'); 1557 renderGrid(matching); 1558 } 1559}); 1560 1561clearSearch.onclick = () => { 1562 searchInput.value = ''; 1563 clearSearch.style.opacity = '0'; 1564 clearSearch.style.pointerEvents = 'none'; 1565 searchDropdown.classList.remove('active'); 1566 if (document.getElementById('playerView').classList.contains('active') === false) { 1567 selectCategory(activeCategoryId); 1568 } 1569}; 1570 1571document.addEventListener('click', (e) => { 1572 if (!searchDropdown.contains(e.target) && e.target !== searchInput) { 1573 searchDropdown.classList.remove('active'); 1574 } 1575}); 1576 1577if (!countdownInterval) countdownInterval = setInterval(updateTimers, 30000); 1578setInterval(init, 60000); 1579init(); 1580</script>
1580 1581 1582<!-- Optimized Firebase Presence Script -->
1583<script type="module"> 1584 import { initializeApp } from "https://www.gstatic.com/firebasejs/10.8.1/firebase-app.js"; 1585 import { getDatabase, ref, onDisconnect, set, push, remove, get } from "https://www.gstatic.com/firebasejs/10.8.1/firebase-database.js"; 1586 1587 const firebaseConfig = { 1588 apiKey: "AIzaSyDWSAaJ4SLuXxTcn6ArxhqSMSzI9dV5vCU", 1589 authDomain: "viewers-9f704.firebaseapp.com", 1590 databaseURL: "https://viewers-9f704-default-rtdb.firebaseio.com", 1591 projectId: "viewers-9f704", 1592 storageBucket: "viewers-9f704.firebasestorage.app", 1593 messagingSenderId: "502411700095", 1594 appId: "1:502411700095:web:bc94c92f72f7bc25f55c4a" 1595 }; 1596 1597 const app = initializeApp(firebaseConfig); 1598 const db = getDatabase(app); 1599 1600 let currentConnRef = null; 1601 let currentStreamId = null; 1602 1603 window.enterStreamTracker = function(streamId) { 1604 window.exitStreamTracker(); 1605 currentStreamId = streamId; 1606 1607 const trackingRef = push(ref(db, `stream_presence/${streamId}`)); 1608 currentConnRef = trackingRef; 1609 1610 set(trackingRef, true).then(() => { 1611 onDisconnect(trackingRef).remove(); 1612 }); 1613 1614 // Instantly update count for the opened stream 1615 fetchSingleStreamCount(streamId); 1616 }; 1617 1618 window.exitStreamTracker = function() { 1619 if (currentConnRef) { 1620 onDisconnect(currentConnRef).cancel().catch(() => {}); 1621 remove(currentConnRef).catch(() => {}); 1622 currentConnRef = null; 1623 currentStreamId = null; 1624 } 1625 }; 1626 1627 window.addEventListener('beforeunload', () => { 1628 window.exitStreamTracker(); 1629 }); 1630 1631 // Pull counts periodically using get() instead of an expensive full-tree real-time socket 1632 async function fetchAllCounts() { 1633 try { 1634 // Only fetch viewer data for streams currently displayed 1635 const streamIds = [...new Set( 1636 [...document.querySelectorAll('.stream-viewer-count')] 1637 .map(el => el.dataset.streamId) 1638 .filter(Boolean) 1639 )]; 1640 1641 if (!streamIds.length) return; 1642 1643 const results = await Promise.all( 1644 streamIds.map(async (streamId) => { 1645 try { 1646 const snapshot = await get( 1647 ref(db, `stream_presence/${streamId}`) 1648 ); 1649 1650 const count = snapshot.exists() 1651 ? Object.keys(snapshot.val() || {}).length 1652 : 0; 1653 1654 return [streamId, count]; 1655 } catch { 1656 return [streamId, 0]; 1657 } 1658 }) 1659 ); 1660 1661 const counts = Object.fromEntries(results); 1662 1663 updateUI(counts); 1664 1665 } catch (err) { 1666 console.error("Firebase fetch error:", err); 1667 } 1668} 1669 1670 async function fetchSingleStreamCount(streamId) { 1671 try { 1672 const snapshot = await get(ref(db, `stream_presence/${streamId}`)); 1673 const count = snapshot.exists() ? Object.keys(snapshot.val() || {}).length : 1; 1674 const theaterCountEl = document.getElementById('theaterViewerCountValue'); 1675 if (theaterCountEl) { 1676 theaterCountEl.textContent = count; 1677 } 1678 } catch (err) {} 1679 } 1680 1681 function updateUI(counts) { 1682 document.querySelectorAll('.stream-viewer-count').forEach(el => { 1683 const sid = el.dataset.streamId; 1684 const count = counts[sid] || 0; 1685 el.textContent = count; 1686 const badge = el.closest('.viewer-badge'); 1687 if (badge) { 1688 badge.style.display = count > 0 ? 'inline-flex' : 'none'; 1689 } 1690 }); 1691 1692 if (currentStreamId) { 1693 const theaterCountEl = document.getElementById('theaterViewerCountValue'); 1694 if (theaterCountEl) { 1695 theaterCountEl.textContent = counts[currentStreamId] || 1; 1696 } 1697 } 1698 } 1699 1700 window.refreshViewerBadges = function() { 1701 fetchAllCounts(); 1702 }; 1703 1704 // Initial fetch on page load, then poll every 45 seconds to drastically reduce bandwidth 1705 fetchAllCounts(); 1706 setInterval(fetchAllCounts, 120000); 1707</script>
1707 1708
1709<script id="cid0020000432197706336" data-cfasync="false" async src="//st.chatango.com/js/gz/emb.js" style="width: 200px;height: 324px;">{"handle":"streamportal1","arch":"js","styles":{"a":"255fd8","b":100,"c":"FFFFFF","d":"FFFFFF","e":"255fd8","g":"ffffff","k":"ffffff","l":"255fd8","m":"000066","n":"FFFFFF","p":"10","q":"255fd8","r":100,"t":0,"ab":false,"usricon":0,"pos":"bl","cv":1,"cvfntw":"bold","cvbg":"255fd8","cvw":180,"cvh":30,"surl":0,"allowpm":0,"cnrs":"1"}}</script>
1709 1710 1711</body> 1712</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.