Spaces:
Build error
Build error
Upload 59 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- frontend/.env.example +11 -0
- frontend/.env.local +3 -0
- frontend/.env.production.local +4 -0
- frontend/Dockerfile +12 -0
- frontend/dist/assets/index-BOX6jOo8.css +1 -0
- frontend/dist/assets/index-BYVU7BLe.js +0 -0
- frontend/dist/index.html +20 -0
- frontend/dist/music-api.js +151 -0
- frontend/dist/music.css +2197 -0
- frontend/dist/music.html +274 -0
- frontend/dist/music.js +1729 -0
- frontend/index.html +19 -0
- frontend/package-lock.json +1747 -0
- frontend/package.json +21 -0
- frontend/public/music-api.js +151 -0
- frontend/public/music.css +2197 -0
- frontend/public/music.html +274 -0
- frontend/public/music.js +1729 -0
- frontend/src/App.jsx +59 -0
- frontend/src/api.js +115 -0
- frontend/src/components/Ambient.jsx +159 -0
- frontend/src/components/AnimeCard.jsx +55 -0
- frontend/src/components/AnimeRow.jsx +44 -0
- frontend/src/components/AuthModal.jsx +206 -0
- frontend/src/components/ContinueWatching.jsx +123 -0
- frontend/src/components/ErrorBoundary.jsx +51 -0
- frontend/src/components/Header.jsx +196 -0
- frontend/src/components/HeroCarousel.jsx +98 -0
- frontend/src/components/HeroSection.jsx +148 -0
- frontend/src/components/HlsPlayer.jsx +1012 -0
- frontend/src/components/LibraryMarquee.jsx +39 -0
- frontend/src/components/MangaCard.jsx +46 -0
- frontend/src/components/MediaCard.jsx +34 -0
- frontend/src/components/MediaRow.jsx +22 -0
- frontend/src/components/MobileNav.jsx +106 -0
- frontend/src/components/MovieCard.jsx +59 -0
- frontend/src/components/MovieRow.jsx +41 -0
- frontend/src/components/Sidebar.jsx +116 -0
- frontend/src/components/Toast.jsx +45 -0
- frontend/src/index.css +1072 -0
- frontend/src/main.jsx +13 -0
- frontend/src/pages/Anime.jsx +196 -0
- frontend/src/pages/AuthCallback.jsx +51 -0
- frontend/src/pages/Home.jsx +180 -0
- frontend/src/pages/Landing.jsx +93 -0
- frontend/src/pages/Manga.jsx +142 -0
- frontend/src/pages/MangaDetails.jsx +185 -0
- frontend/src/pages/MovieDetails.jsx +285 -0
- frontend/src/pages/MovieWatch.jsx +326 -0
- frontend/src/pages/Movies.jsx +291 -0
frontend/.env.example
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Backend REST API base (leave as /api to use the Vite dev proxy)
|
| 2 |
+
VITE_API_URL=/api
|
| 3 |
+
|
| 4 |
+
# HLS proxy worker endpoint (leave as /hls to use the Vite dev proxy).
|
| 5 |
+
# To use the deployed Cloudflare worker instead (recommended — the local
|
| 6 |
+
# dev proxy must be running for /hls to work):
|
| 7 |
+
# VITE_STREAM_PROXY_URL=https://anidoom-proxy.shawnmwask1234.workers.dev/hls
|
| 8 |
+
VITE_STREAM_PROXY_URL=/hls
|
| 9 |
+
|
| 10 |
+
# Only needed if the worker requires auth (STREAM_KEY secret)
|
| 11 |
+
VITE_STREAM_KEY=
|
frontend/.env.local
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use the deployed anicove-proxy Cloudflare Worker for all HLS/MP4 streaming
|
| 2 |
+
# instead of the local dev proxy (:8787, which is not always running).
|
| 3 |
+
VITE_STREAM_PROXY_URL=https://anicove-proxy.shawnmwask1234.workers.dev/hls
|
frontend/.env.production.local
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Production build: stream everything through the deployed anicove-proxy worker.
|
| 2 |
+
# Set VITE_API_URL here too if the backend is deployed somewhere else.
|
| 3 |
+
VITE_API_URL=https://frantic-dollie-mwask-009a226f.koyeb.app
|
| 4 |
+
VITE_STREAM_PROXY_URL=https://anicove-proxy.shawnmwask1234.workers.dev/hls
|
frontend/Dockerfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-alpine
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY package*.json ./
|
| 6 |
+
RUN npm install
|
| 7 |
+
|
| 8 |
+
COPY . .
|
| 9 |
+
|
| 10 |
+
EXPOSE 5173
|
| 11 |
+
|
| 12 |
+
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
frontend/dist/assets/index-BOX6jOo8.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
:root{--bg-base: #080808;--bg-surface: #0d0d0d;--bg-elevated: #111111;--bg-card: #0f0f0f;--bg-card-hover: #1a1a1a;--accent-v: #7c3aed;--accent-v2: #9d6ef8;--accent-c: #5b21b6;--accent-c2: #c4b5fd;--accent-green: #22c55e;--accent-amber: #f59e0b;--grad-accent: linear-gradient(135deg, #7c3aed, #4c1d95);--grad-card: linear-gradient(180deg, transparent 35%, rgba(8,8,8,.99) 100%);--text-primary: #ffffff;--text-secondary: #777790;--text-muted: #404050;--border: rgba(255,255,255,.07);--border-bright: rgba(124,58,237,.5);--glass-border: rgba(255,255,255,.06);--radius-sm: 6px;--radius-md: 10px;--radius-lg: 16px;--radius-pill: 999px;--shadow-hover: 0 8px 40px rgba(124,58,237,.35);--transition: all .22s cubic-bezier(.4,0,.2,1);--header-h: 62px;--max-w: 1380px}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html{scroll-behavior:smooth;scrollbar-width:none}::-webkit-scrollbar{display:none}body{font-family:Outfit,Inter,sans-serif;background:radial-gradient(circle at top,rgba(124,58,237,.22),transparent 32%),linear-gradient(180deg,#06070a,#0b0b12 36%,#08090d);color:var(--text-primary);min-height:100vh;line-height:1.6;overflow-x:hidden}#root{min-height:100vh}img{display:block;max-width:100%}a{color:inherit;text-decoration:none}button{cursor:pointer;border:none;background:none;font-family:inherit;color:inherit}ul,ol{list-style:none}input{font-family:inherit}.container{max-width:var(--max-w);margin:0 auto;padding:0 28px}.main-content{position:relative;min-height:100vh;overflow-x:hidden}.page-shell{padding-top:calc(var(--header-h) + 30px);padding-bottom:80px}.catalog-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:20px 16px}.search-shell{display:flex;flex-direction:column;gap:18px}.section-header.compact{margin-bottom:16px}#header{position:fixed;top:0;left:0;right:0;z-index:100;height:var(--header-h);transition:var(--transition)}#header:before{content:"";position:absolute;top:0;right:0;bottom:0;left:0;background:linear-gradient(180deg,#0a0a12d1,#0a0a1261);-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);opacity:0;transition:var(--transition);pointer-events:none}#header.scrolled:before,#header:not(.scrolled){opacity:1}#header.scrolled{border-bottom:1px solid var(--border);box-shadow:0 12px 28px #0000002e}#header .container{height:100%;display:flex;align-items:center;gap:24px;position:relative;z-index:1}.logo{display:flex;align-items:center;flex-shrink:0}.logo-wordmark{display:flex;align-items:flex-end;line-height:1}.logo-wordmark .l{font-family:Bebas Neue,Outfit,sans-serif;font-size:2.2rem;font-weight:400;line-height:1;color:#fff;letter-spacing:.02em}nav{display:flex;align-items:center;gap:2px}nav a{padding:6px 14px;border-radius:var(--radius-pill);font-size:.88rem;font-weight:500;color:var(--text-secondary);transition:var(--transition)}nav a:hover,nav a.active{color:var(--text-primary);background:#8b5cf626}.search-wrap{flex:1;max-width:360px;position:relative}.search-input{width:100%;padding:9px 16px 9px 42px;border-radius:var(--radius-pill);background:#ffffff0f;border:1px solid var(--border);color:var(--text-primary);font-size:.88rem;outline:none;transition:var(--transition)}.search-input::placeholder{color:var(--text-muted)}.search-input:focus{background:#ffffff1a;border-color:var(--accent-v);box-shadow:0 0 0 3px #8b5cf61f}.search-icon{position:absolute;left:14px;top:50%;transform:translateY(-50%);pointer-events:none;color:var(--text-muted)}.search-icon svg{width:16px;height:16px}.auth-nav{margin-left:auto;flex-shrink:0}.auth-avatar-btn{display:flex;align-items:center;gap:8px;padding:7px 14px 7px 8px;border-radius:var(--radius-pill);background:#ffffff0f;border:1px solid var(--border);transition:var(--transition);font-size:.85rem;font-weight:700;letter-spacing:.01em;color:var(--text-primary);box-shadow:0 8px 18px #0000001f}.auth-avatar-btn:hover{background:#ffffff1a;border-color:var(--border-bright);transform:translateY(-1px)}.auth-avatar-btn.open{border-color:var(--border-bright);background:linear-gradient(180deg,#7c3aed2e,#ffffff14);box-shadow:0 12px 30px #7c3aed2e}.auth-avatar-btn.guest{background:linear-gradient(135deg,#7c3aed2e,#ffffff14);border-color:#7c3aed6b}.auth-avatar-btn.guest:hover{border-color:#7c3aedb3;box-shadow:0 12px 32px #7c3aed38}.auth-avatar-circle{width:28px;height:28px;border-radius:50%;background:#ffffff14;display:flex;align-items:center;justify-content:center;flex-shrink:0}.auth-avatar-icon{background:linear-gradient(135deg,#7c3aed42,#ffffff14);color:var(--accent-v2)}.auth-avatar-img{object-fit:cover}.auth-nav{position:relative}@media (max-width: 1024px){.container{padding:0 20px}nav{display:none}.search-wrap{max-width:260px}}@media (max-width: 768px){.container{padding:0 16px}.search-wrap{display:none}.anime-card{width:130px}.hero-next,.hero-prev{display:none!important}.hero-cover-wrap{display:none}.home-layout{grid-template-columns:1fr;gap:14px}.home-sidebar{position:static}}@media (max-width: 680px){.container{padding:0 16px}.auth-avatar-btn{gap:0;padding:5px 8px 5px 6px;min-width:42px;justify-content:center}.auth-avatar-btn span,.auth-avatar-btn svg:last-child{display:none}.auth-avatar-circle{width:26px;height:26px}.auth-avatar-icon svg{width:14px;height:14px}.main-content{padding-bottom:calc(66px + env(safe-area-inset-bottom))}.home-layout{grid-template-columns:1fr;gap:14px}.spotlight-featured{min-height:262px;border-radius:14px}.spotlight-featured-body{padding:12px 12px 38px}.spotlight-featured-title{font-size:1.28rem;margin-bottom:4px}.spotlight-nav-btn{width:30px;height:30px;opacity:1}.spotlight-nav-btn.prev{left:8px}.spotlight-nav-btn.next{right:8px}.genre-row-wrap,.genre-row{gap:8px}.genre-pill{padding:7px 12px;font-size:.74rem}.section-header{margin-bottom:14px}.section-title{font-size:1.02rem}.section-title:before{height:19px}.scroll-row{gap:10px}.anime-card{width:138px}.card-body{padding:8px 9px 10px}.card-title{font-size:.78rem}.btn{padding:10px 18px;font-size:.85rem}.btn-sm{padding:6px 13px;font-size:.78rem}}@media (max-width: 420px){.anime-card{width:126px}.hero-ctas{flex-direction:column}.hero-cta{width:100%;justify-content:center}.spotlight-featured{min-height:240px}.auth-dropdown{left:12px;right:12px;min-width:0}}.auth-dropdown{position:absolute;top:calc(100% + 10px);right:0;z-index:130;min-width:240px;background:#0c0c14f5;-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);border:1px solid var(--border-bright);border-radius:var(--radius-lg);box-shadow:0 22px 52px #000000b3;padding:10px;animation:fadeUp .18s ease both;overflow:hidden}.auth-dropdown-head{padding:12px 12px 10px;border-bottom:1px solid var(--border);margin-bottom:8px;background:linear-gradient(180deg,rgba(124,58,237,.1),transparent);border-radius:8px 8px 0 0}.auth-dropdown-name{font-size:.9rem;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.auth-dropdown-sub{font-size:.72rem;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.auth-dropdown-item{display:flex;align-items:center;gap:9px;width:100%;padding:10px 12px;border-radius:var(--radius-md);font-size:.85rem;font-weight:600;color:var(--text-primary);transition:var(--transition);cursor:pointer;background:none;border:none;text-align:left;margin-top:2px}.auth-dropdown-item svg{color:var(--accent-v2);flex-shrink:0}.auth-dropdown-item:hover{background:linear-gradient(90deg,#7c3aed29,#ffffff08);border-color:transparent}.auth-dropdown-item.danger{color:#fda4af}.auth-dropdown-item.danger svg{color:#fda4af}.auth-dropdown-item.danger:hover{background:#e11d481f}.auth-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:200;background:#05050ab8;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;padding:20px;animation:fadeIn .2s ease both}.auth-modal{position:relative;width:100%;max-width:400px;background:var(--bg-surface);border:1px solid var(--border-bright);border-radius:var(--radius-lg);padding:30px 28px 26px;box-shadow:0 30px 90px #000000b3;animation:fadeUp .22s ease both}.auth-modal-close{position:absolute;top:14px;right:14px;width:30px;height:30px;border-radius:50%;background:#ffffff0f;border:1px solid var(--border);color:var(--text-secondary);font-size:.85rem;display:flex;align-items:center;justify-content:center;transition:var(--transition);cursor:pointer}.auth-modal-close:hover{background:#ffffff1f;color:#fff}.auth-modal-logo{font-family:Bebas Neue,Outfit,sans-serif;font-size:1.9rem;letter-spacing:.05em;text-align:center;margin-bottom:20px;color:#fff}.auth-tabs{display:flex;background:#ffffff0d;border:1px solid var(--border);border-radius:var(--radius-pill);padding:4px;margin-bottom:20px}.auth-tab{flex:1;padding:8px 0;border-radius:var(--radius-pill);font-size:.84rem;font-weight:700;color:var(--text-secondary);transition:var(--transition);cursor:pointer;background:none;border:none}.auth-tab.active{background:var(--grad-accent);color:#fff;box-shadow:0 0 16px #7c3aed66}.auth-form{display:flex;flex-direction:column;gap:14px}.auth-field{display:flex;flex-direction:column;gap:6px}.auth-field span{font-size:.72rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--text-muted)}.auth-field input{padding:11px 14px;border-radius:var(--radius-md);background:#ffffff0d;border:1px solid var(--border);color:var(--text-primary);font-size:.9rem;outline:none;transition:var(--transition)}.auth-field input:focus{background:#ffffff14;border-color:var(--accent-v);box-shadow:0 0 0 3px #8b5cf61f}.auth-field input::placeholder{color:var(--text-muted)}.auth-error{font-size:.8rem;font-weight:600;color:#fda4af;background:#e11d481a;border:1px solid rgba(225,29,72,.25);border-radius:var(--radius-sm);padding:8px 12px}.auth-submit{width:100%;justify-content:center;margin-top:4px}.auth-submit:disabled{opacity:.5;cursor:not-allowed}.auth-divider{display:flex;align-items:center;gap:12px;margin:18px 0 14px;color:var(--text-muted);font-size:.72rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase}.auth-divider:before,.auth-divider:after{content:"";flex:1;height:1px;background:var(--border)}.auth-anilist-btn{width:100%;display:flex;align-items:center;justify-content:center;gap:9px;padding:11px;border-radius:var(--radius-md);background:#06b6d41f;border:1px solid rgba(6,182,212,.35);color:#67e8f9;font-size:.86rem;font-weight:700;transition:var(--transition);cursor:pointer}.auth-anilist-btn:hover{background:#06b6d438;border-color:#06b6d499}.btn{display:inline-flex;align-items:center;gap:8px;padding:11px 24px;border-radius:var(--radius-pill);font-size:.9rem;font-weight:600;transition:var(--transition);white-space:nowrap;cursor:pointer;border:none}.btn-primary{background:var(--grad-accent);color:#fff;box-shadow:0 0 22px #7c3aed59}.btn-primary:hover{transform:translateY(-2px);box-shadow:0 4px 30px #7c3aed8c}.btn-primary:disabled{opacity:.4;cursor:not-allowed;transform:none}.btn-ghost{background:#ffffff12;color:var(--text-primary);border:1px solid var(--border)}.btn-ghost:hover{background:#ffffff1f;border-color:var(--border-bright)}.btn-ghost:disabled{opacity:.4;cursor:not-allowed}.btn-sm{padding:7px 16px;font-size:.82rem}.badge{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border-radius:var(--radius-pill);font-size:.72rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase}.badge-score{background:#f59e0b26;color:#fcd34d;border:1px solid rgba(245,158,11,.3)}.badge-green{background:#22c55e26;color:#4ade80;border:1px solid rgba(34,197,94,.3)}.badge-dark{background:#ffffff12;color:var(--text-secondary);border:1px solid var(--border)}.section-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:22px}.section-title{display:flex;align-items:center;gap:10px;font-size:1.2rem;font-weight:700}.section-title:before{content:"";display:block;width:4px;height:22px;background:var(--grad-accent);border-radius:2px}.section-link{font-size:.84rem;font-weight:500;color:var(--accent-v2);display:flex;align-items:center;gap:4px;transition:var(--transition)}.section-link:hover{color:var(--accent-c2);gap:8px}.anime-card{position:relative;flex-shrink:0;width:158px;border-radius:var(--radius-md);overflow:hidden;background:var(--bg-card);cursor:pointer;transition:var(--transition);border:1px solid var(--border)}.anime-card:hover{transform:translateY(-6px) scale(1.02);border-color:var(--border-bright);box-shadow:var(--shadow-hover)}.card-poster{position:relative;aspect-ratio:2/3;overflow:hidden}.card-poster img{width:100%;height:100%;object-fit:cover;transition:transform .45s ease}.anime-card:hover .card-poster img{transform:scale(1.05)}.card-overlay{position:absolute;top:0;right:0;bottom:0;left:0;background:var(--grad-card);opacity:0;transition:var(--transition)}.anime-card:hover .card-overlay{opacity:1}.card-quick{position:absolute;bottom:8px;left:8px;right:8px;opacity:0;transform:translateY(8px);transition:var(--transition)}.anime-card:hover .card-quick{opacity:1;transform:translateY(0)}.card-quick-btn{width:100%;padding:7px;border-radius:var(--radius-sm);background:var(--grad-accent);color:#fff;font-size:.78rem;font-weight:600;text-align:center}.card-body{padding:10px 10px 12px}.card-title{font-size:.84rem;font-weight:600;line-height:1.35;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;margin-bottom:6px}.card-meta{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.card-score{font-size:.72rem;font-weight:700;color:#6ee7b7}.card-meta-item{font-size:.7rem;color:var(--text-muted)}.scroll-row{display:flex;gap:14px;overflow-x:auto;scroll-snap-type:x mandatory;padding:12px 0;scrollbar-width:thin;scrollbar-color:var(--accent-v) transparent}.scroll-row::-webkit-scrollbar{height:4px;display:block}.scroll-row::-webkit-scrollbar-thumb{background:var(--accent-v);border-radius:99px}.scroll-row .anime-card{scroll-snap-align:start}.skeleton{background:linear-gradient(90deg,var(--bg-elevated) 25%,var(--bg-card-hover) 50%,var(--bg-elevated) 75%);background-size:200% 100%;animation:shimmer 1.5s infinite;border-radius:var(--radius-md)}.skel-card{width:158px;flex-shrink:0}.skel-poster{width:100%;aspect-ratio:2/3;border-radius:var(--radius-md) var(--radius-md) 0 0}.skel-line{height:11px;margin:8px 10px;border-radius:6px}.skel-line-sm{width:55%}@keyframes shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}@keyframes fadeUp{0%{opacity:0;transform:translateY(24px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes spin{to{transform:rotate(360deg)}}@keyframes marquee{0%{transform:translate(0)}to{transform:translate(-50%)}}@keyframes bounce{0%,to{transform:translate(-50%) translateY(0)}50%{transform:translate(-50%) translateY(6px)}}.spinner{width:34px;height:34px;border:3px solid var(--border);border-top-color:var(--accent-v);border-radius:50%;animation:spin .7s linear infinite}.loader{display:flex;justify-content:center;align-items:center;padding:60px;flex-direction:column;gap:12px;color:var(--text-muted)}.home-hero{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;position:relative;overflow:hidden;padding:calc(var(--header-h) + 40px) 20px 60px;text-align:center;background:radial-gradient(circle at 20% 15%,rgba(124,58,237,.26),transparent 22%),radial-gradient(circle at 80% 18%,rgba(139,92,246,.18),transparent 18%)}.hero-glow{position:absolute;top:0;right:0;bottom:0;left:0;background:radial-gradient(ellipse 70% 70% at 15% 50%,rgba(88,28,235,.35) 0%,transparent 60%),radial-gradient(ellipse 40% 40% at 85% 50%,rgba(88,28,235,.15) 0%,transparent 60%);pointer-events:none}.hero-badge{display:inline-flex;align-items:center;gap:8px;padding:6px 16px;border-radius:var(--radius-pill);background:#7c3aed1f;border:1px solid rgba(124,58,237,.3);font-size:.75rem;font-weight:700;letter-spacing:.1em;color:var(--accent-v2);margin-bottom:32px;animation:fadeUp .5s ease both;box-shadow:0 14px 32px #7c3aed1f}.hero-badge-dot{width:6px;height:6px;border-radius:50%;background:var(--accent-v)}.hero-logo-text{font-family:Rubik Dirt,Bebas Neue,sans-serif;font-size:clamp(5rem,16vw,14rem);font-weight:400;line-height:.9;margin-bottom:16px;background:linear-gradient(180deg,#fff 40%,#c8b4ffb8);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;animation:fadeUp .6s ease .1s both;text-shadow:0 0 26px rgba(124,58,237,.18)}.hero-logo-e{display:inline-block;transform:scaleX(-1)}.hero-subtitle{font-size:clamp(1.4rem,3.5vw,2.4rem);font-weight:700;color:var(--text-secondary);margin-bottom:14px;animation:fadeUp .6s ease .2s both}.hero-desc{font-size:1rem;color:var(--text-secondary);max-width:500px;margin:0 auto 32px;line-height:1.7;animation:fadeUp .6s ease .25s both}.hero-desc strong{color:var(--text-primary)}.hero-filters{display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:24px;animation:fadeUp .6s ease .3s both;flex-wrap:wrap}.hero-filter-pill{padding:7px 20px;border-radius:var(--radius-pill);background:#ffffff0f;border:1px solid var(--border);font-size:.86rem;font-weight:500;color:var(--text-secondary);transition:var(--transition);cursor:pointer}.hero-filter-pill.active{background:var(--accent-v);border-color:var(--accent-v);color:#fff}.hero-filter-pill:hover:not(.active){border-color:var(--border-bright);color:var(--text-primary)}.hero-search-wrap{position:relative;width:min(620px,100%);margin:0 auto 28px;animation:fadeUp .6s ease .35s both}.hero-search-input{width:100%;padding:16px 60px 16px 22px;border-radius:var(--radius-pill);background:#ffffff12;border:1px solid rgba(255,255,255,.1);color:var(--text-primary);font-size:.95rem;outline:none;transition:var(--transition)}.hero-search-input::placeholder{color:var(--text-muted)}.hero-search-input:focus{background:#ffffff1a;border-color:var(--accent-v);box-shadow:0 0 0 4px #7c3aed26}.hero-search-btn{position:absolute;right:6px;top:50%;transform:translateY(-50%);width:42px;height:42px;border-radius:50%;background:var(--accent-v);color:#fff;display:flex;align-items:center;justify-content:center;transition:var(--transition)}.hero-search-btn:hover{background:var(--accent-v2);transform:translateY(-50%) scale(1.08)}.hero-search-btn svg{width:18px;height:18px}.hero-popular{margin-bottom:36px;animation:fadeUp .6s ease .4s both}.hero-popular-label{font-size:.72rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--text-muted);margin-bottom:12px}.hero-popular-tags{display:flex;justify-content:center;flex-wrap:wrap;gap:8px}.hero-popular-tag{padding:6px 16px;border-radius:var(--radius-pill);background:#ffffff0d;border:1px solid var(--border);font-size:.82rem;font-weight:500;color:var(--text-secondary);transition:var(--transition);cursor:pointer}.hero-popular-tag:hover{background:#7c3aed1f;border-color:var(--border-bright);color:var(--accent-v2)}.hero-ctas{display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:12px;margin-bottom:52px;animation:fadeUp .6s ease .45s both}.hero-cta{display:inline-flex;align-items:center;gap:10px;padding:13px 26px;border-radius:var(--radius-pill);font-size:.9rem;font-weight:700;transition:var(--transition);cursor:pointer;border:1px solid transparent}.hero-cta-anime{background:var(--grad-accent);color:#fff;box-shadow:0 0 28px #7c3aed66}.hero-cta-anime:hover{transform:translateY(-2px);box-shadow:0 6px 36px #7c3aed8c}.hero-cta-manga{background:#ffffff12;border-color:var(--border);color:var(--text-primary)}.hero-cta-manga:hover{background:#ffffff1f;border-color:var(--border-bright)}.hero-cta-movies{background:#06b6d426;border-color:#06b6d466;color:#67e8f9}.hero-cta-movies:hover{background:#06b6d440;transform:translateY(-2px)}.hero-cta-music{background:#8b5cf62e;border-color:#8b5cf666;color:#a78bfa}.hero-cta-music:hover{background:#8b5cf647;transform:translateY(-2px)}.hero-stats{display:flex;align-items:center;justify-content:center;animation:fadeUp .6s ease .5s both}.hero-stat{padding:0 36px;text-align:center;position:relative}.hero-stat+.hero-stat:before{content:"";position:absolute;left:0;top:15%;bottom:15%;width:1px;background:var(--border)}.hero-stat-val{font-size:1.6rem;font-weight:800;letter-spacing:-.02em;margin-bottom:2px}.hero-stat-label{font-size:.68rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--text-muted)}.scroll-indicator{position:absolute;bottom:24px;left:50%;transform:translate(-50%);color:var(--text-muted);animation:bounce 2s ease infinite}.library-section{padding:60px 0;overflow:hidden;border-top:1px solid var(--border);border-bottom:1px solid var(--border)}.library-header{display:flex;align-items:center;justify-content:center;gap:20px;margin-bottom:28px}.library-header-line{flex:1;height:1px;background:linear-gradient(to right,transparent,var(--border-bright));max-width:200px}.library-header-line.right{background:linear-gradient(to left,transparent,var(--border-bright))}.library-header-text{font-size:.7rem;font-weight:700;letter-spacing:.15em;text-transform:uppercase;color:var(--text-muted)}.marquee-track{overflow:hidden}.marquee-row{display:flex;gap:10px;width:max-content;animation:marquee 30s linear infinite;margin-bottom:10px}.marquee-row.reverse{animation:marquee 35s linear infinite reverse}.marquee-row:last-child{margin-bottom:0}.marquee-poster{width:100px;height:140px;flex-shrink:0;border-radius:var(--radius-md);overflow:hidden;border:1px solid var(--border);transition:var(--transition);cursor:pointer}.marquee-poster:hover{border-color:var(--border-bright);transform:scale(1.04)}.marquee-poster img{width:100%;height:100%;object-fit:cover}.feature-section{padding:80px 0}.feature-eyebrow{font-size:.75rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--accent-v2);margin-bottom:16px}.feature-heading{font-size:clamp(2rem,4vw,3.2rem);font-weight:800;line-height:1.15;margin-bottom:20px}.feature-heading .accent{color:var(--accent-v2)}.feature-desc{font-size:1rem;color:var(--text-secondary);line-height:1.75;max-width:480px;margin-bottom:40px}.feature-card{background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:24px;max-width:560px}.feature-card-icon{width:44px;height:44px;border-radius:var(--radius-md);background:#7c3aed26;border:1px solid rgba(124,58,237,.3);display:flex;align-items:center;justify-content:center;color:var(--accent-v2);margin-bottom:16px}.feature-card-eyebrow{font-size:.72rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--accent-v2);margin-bottom:6px}.feature-card-title{font-size:1.1rem;font-weight:800;margin-bottom:8px}.feature-card-desc{font-size:.88rem;color:var(--text-secondary);line-height:1.7;margin-bottom:16px}.feature-track-item{display:flex;align-items:center;gap:12px;padding:12px 14px;background:var(--bg-elevated);border-radius:var(--radius-md);margin-bottom:8px}.feature-track-name{font-size:.88rem;font-weight:600}.feature-track-sub{font-size:.75rem;color:var(--text-muted)}.feature-track-badge{margin-left:auto;font-size:.72rem;font-weight:700;color:var(--accent-green)}.content-section{padding:40px 0}.content-section:first-child{padding-top:calc(var(--header-h) + 26px)}.section-sub-label{font-size:.7rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--text-muted)}.info-hero{position:relative;min-height:520px;overflow:hidden;display:flex;align-items:flex-end;padding-top:var(--header-h);background:#0a0a0a}.info-hero:before,.info-hero:after{content:"";position:absolute;top:0;right:0;bottom:0;left:0;pointer-events:none}.info-hero:before{background:radial-gradient(120% 80% at 30% 30%,#00000040,#000000d9 70%,#0a0a0a)}.info-hero:after{background:linear-gradient(180deg,rgba(10,10,10,0) 40%,rgba(10,10,10,.85) 80%,var(--bg-base) 100%)}.info-hero-bg{position:absolute;top:0;right:0;bottom:0;left:0;background-size:cover;background-position:center;background-repeat:no-repeat;transform:scale(1.05);filter:brightness(.55)}.info-hero-inner{position:relative;z-index:2;display:flex;gap:36px;padding:96px 28px 56px;align-items:flex-end}.info-poster{flex:0 0 240px;width:240px;aspect-ratio:2/3;border-radius:14px;overflow:hidden;box-shadow:0 24px 60px #0009,0 0 0 1px #ffffff0f;background:#111;border:1px solid rgba(255,255,255,.06)}.info-poster img{width:100%;height:100%;object-fit:cover;display:block}.info-head{flex:1;min-width:0;display:flex;flex-direction:column;gap:18px}.info-eyebrow{display:inline-flex;align-items:center;gap:10px;color:#ffffffb3;font-size:.78rem;font-weight:600;letter-spacing:.18em;text-transform:uppercase}.info-eyebrow .dot{width:6px;height:6px;border-radius:50%;background:var(--accent-v);box-shadow:0 0 12px var(--accent-v)}.info-title{font-family:Poppins,Inter,sans-serif;font-size:clamp(1.8rem,3.6vw,3.2rem);font-weight:900;line-height:1.05;color:#fff;margin:0;text-shadow:0 6px 28px rgba(0,0,0,.6)}.info-subtitle{font-size:.95rem;color:#ffffffa6;font-weight:500;margin-top:-6px}.info-meta-row{display:flex;flex-wrap:wrap;gap:16px;align-items:center;color:#ffffffd9;font-size:.85rem;font-weight:600}.meta-pill{background:#ffffff14;border:1px solid rgba(255,255,255,.08);padding:6px 12px;border-radius:999px;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px)}.meta-score{color:#fbbf24}.meta-status-airing{color:#22c55e}.info-genres{display:flex;gap:8px;flex-wrap:wrap}.info-genre-tag{background:#ffffff0f;border:1px solid rgba(255,255,255,.08);padding:5px 12px;border-radius:999px;font-size:.72rem;font-weight:600;color:#ffffffd9;cursor:pointer;transition:all .2s ease}.info-genre-tag:hover{background:#ffffff1f;transform:translateY(-1px)}.info-cta{display:flex;gap:12px;margin-top:4px;flex-wrap:wrap}.btn-watch{display:inline-flex;align-items:center;gap:10px;padding:14px 28px;border-radius:12px;background:linear-gradient(135deg,var(--accent-v) 0%,var(--accent-v2) 100%);color:#fff;font-weight:800;font-size:.95rem;letter-spacing:.04em;text-decoration:none;border:none;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;box-shadow:0 12px 30px #7c3aed59}.btn-watch:hover{transform:translateY(-2px);box-shadow:0 18px 40px #7c3aed80}.btn-secondary{display:inline-flex;align-items:center;gap:8px;padding:14px 22px;border-radius:12px;background:#ffffff14;color:#fff;font-weight:700;font-size:.9rem;text-decoration:none;border:1px solid rgba(255,255,255,.1);cursor:pointer;transition:background .2s ease}.btn-secondary:hover{background:#ffffff24}.info-body{padding:40px 28px 80px}.info-section{margin-bottom:48px}.info-section-title{display:flex;align-items:center;gap:12px;font-family:Poppins,Inter,sans-serif;font-size:1.3rem;font-weight:800;color:#fff;margin:0 0 18px}.info-section-title:before{content:"";width:4px;height:22px;border-radius:4px;background:var(--accent-v)}.info-grid-2{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,1fr);gap:32px;align-items:flex-start}.info-desc{font-size:.96rem;line-height:1.75;color:#ffffffc7;margin:0;position:relative;max-height:22em;overflow:hidden;transition:max-height .3s ease}.info-desc.expanded{max-height:200em}.info-desc:after{content:"";position:absolute;inset:auto 0 0 0;height:3em;background:linear-gradient(180deg,rgba(10,10,10,0) 0%,var(--bg-base) 100%);pointer-events:none;transition:opacity .3s ease}.info-desc.expanded:after{opacity:0}.info-desc-toggle{display:inline-block;margin-top:8px;color:var(--accent-v2);font-weight:700;font-size:.85rem;cursor:pointer;background:none;border:none;padding:0}.info-stats{display:grid;grid-template-columns:1fr 1fr;gap:14px;background:#ffffff05;border:1px solid rgba(255,255,255,.06);border-radius:14px;padding:22px}.info-stat-row{display:flex;flex-direction:column;gap:4px}.info-stat-row .label{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.12em;color:#ffffff73}.info-stat-row .value{font-size:.95rem;font-weight:700;color:#fff}.info-tags{display:flex;flex-wrap:wrap;gap:6px}.info-tag{background:#ffffff0a;border:1px solid rgba(255,255,255,.06);padding:4px 10px;border-radius:6px;font-size:.72rem;font-weight:600;color:#ffffffb3}@media (max-width: 880px){.info-hero-inner{flex-direction:column;align-items:center;text-align:center;padding:80px 22px 40px;gap:24px}.info-poster{flex:0 0 180px;width:180px}.info-meta-row,.info-genres,.info-cta{justify-content:center}.info-grid-2{grid-template-columns:1fr}.info-hero{min-height:auto}.info-body{padding:32px 22px 80px}}@media (max-width: 540px){.info-hero-inner{padding:72px 20px 36px}.info-poster{flex:0 0 150px;width:150px}.btn-watch{padding:12px 22px;font-size:.88rem}.info-body{padding:28px 20px 80px}}.countdown-wrap{display:inline-flex;flex-direction:column;gap:10px;padding:14px 18px;margin-bottom:22px;border-radius:var(--radius-md);background:#7c3aed14;border:1px solid rgba(124,58,237,.3);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);width:fit-content}.countdown-label{font-size:.72rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--accent-v2)}.countdown-cards{display:flex;gap:8px}.countdown-cell{display:flex;flex-direction:column;align-items:center;gap:2px;min-width:58px;padding:8px 6px;border-radius:var(--radius-sm);background:#08080899;border:1px solid var(--border-bright)}.countdown-num{font-size:1.35rem;font-weight:800;font-variant-numeric:tabular-nums;line-height:1;color:#fff}.countdown-unit{font-size:.6rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--text-muted)}.info-genre-pill{padding:5px 14px;background:#ffffff12;border:1px solid var(--border);border-radius:var(--radius-pill);font-size:.8rem;font-weight:500;color:var(--text-secondary);transition:var(--transition);cursor:pointer}.info-genre-pill:hover{border-color:var(--border-bright);color:var(--accent-v2)}.info-actions{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.info-btn-watch{background:var(--grad-accent);color:#fff;box-shadow:0 0 24px #7c3aed59}.info-btn-watch:hover{transform:translateY(-2px);box-shadow:0 6px 32px #7c3aed8c}.info-btn-ghost{background:#ffffff14;color:var(--text-primary);border:1px solid var(--border)}.info-btn-ghost:hover{background:#ffffff24;border-color:var(--border-bright)}.info-body{padding:44px 0 80px}.info-layout{display:grid;grid-template-columns:1fr 300px;gap:48px;align-items:start}.info-section-label{font-size:1.15rem;font-weight:700;display:flex;align-items:center;gap:10px;margin-bottom:16px}.info-section-label:before{content:"";width:4px;height:20px;background:var(--grad-accent);border-radius:2px}.info-synopsis{font-size:.93rem;color:var(--text-secondary);line-height:1.8;margin-bottom:8px}.info-synopsis.clamped{display:-webkit-box;-webkit-line-clamp:5;-webkit-box-orient:vertical;overflow:hidden}.show-more-btn{color:var(--accent-v2);font-size:.88rem;font-weight:600;display:inline-flex;align-items:center;gap:4px;transition:var(--transition);margin-top:8px;cursor:pointer;background:none;border:none}.show-more-btn:hover{color:var(--accent-c2)}.info-tags{display:flex;flex-wrap:wrap;gap:8px;margin:20px 0 32px}.info-tag{padding:5px 14px;background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-pill);font-size:.78rem;font-weight:500;color:var(--text-secondary);transition:var(--transition);cursor:pointer}.info-tag:hover{border-color:var(--border-bright);color:var(--accent-v2)}.info-stats-grid{background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;display:grid;grid-template-columns:1fr 1fr}.info-stat-cell{padding:18px 20px;border-right:1px solid var(--border);border-bottom:1px solid var(--border)}.info-stat-cell:nth-child(2n){border-right:none}.info-stat-cell:nth-last-child(-n+2){border-bottom:none}.info-stat-label{font-size:.68rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--text-muted);margin-bottom:5px}.info-stat-value{font-size:.95rem;font-weight:700}.char-row{display:flex;gap:16px;overflow-x:auto;padding:4px 0 12px}.char-card{display:flex;flex-direction:column;align-items:center;gap:8px;flex-shrink:0;cursor:pointer}.char-avatar{width:80px;height:80px;border-radius:50%;overflow:hidden;border:2px solid var(--border);transition:var(--transition)}.char-avatar img{width:100%;height:100%;object-fit:cover}.char-card:hover .char-avatar{border-color:var(--border-bright);transform:scale(1.06)}.char-name{font-size:.78rem;font-weight:600;text-align:center;max-width:84px;line-height:1.3}.char-role{font-size:.68rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em}.watch-page{padding-top:var(--header-h)}.watch-container{padding:0 28px 80px;max-width:var(--max-w);margin:0 auto}.watch-layout{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:18px;align-items:start;padding-top:12px}.watch-main,.watch-sidebar-col{min-width:0}.player-wrapper{position:relative;width:100%;aspect-ratio:16/9;background:#080a12;border-radius:14px;overflow:hidden;border:1px solid var(--border)}.player-wrapper iframe,.player-wrapper video,.player-wrapper .custom-player{width:100%;height:100%;border:none;display:block}.player-placeholder{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;color:var(--text-muted);background:#070a12f2}.player-placeholder svg{width:52px;height:52px;opacity:.3}.player-placeholder span{font-size:.9rem;font-weight:500}.player-error-box{color:#f43f5e}.server-warning{display:flex;align-items:center;gap:10px;padding:10px 16px;background:#f59e0b12;border:1px solid rgba(245,158,11,.2);border-radius:var(--radius-md);margin:10px 0;font-size:.83rem;color:#fcd34d}.server-warning-close{margin-left:auto;opacity:.5;transition:var(--transition);font-size:1.1rem;line-height:1;cursor:pointer;border:none;background:none;color:inherit}.server-warning-close:hover{opacity:1}.watch-warn-banner{display:flex;align-items:center;gap:10px;padding:12px 14px;background:#f59e0b14;border:1px solid rgba(245,158,11,.2);border-radius:12px;margin:10px 0 14px;font-size:.83rem;color:#fcd34d}.wwb-icon{width:16px;height:16px;flex-shrink:0}.wwb-text{display:block;flex:1}.wwb-close{margin-left:auto;border:none;background:none;color:inherit;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0}.wwb-close svg{width:16px;height:16px}.kv-prefs-bar{display:flex;align-items:center;gap:18px;padding:11px 14px;background:var(--bg-surface);border:1px solid var(--border);border-radius:12px;margin:10px 0;flex-wrap:wrap}.kv-pref-btn{display:flex;align-items:center;gap:8px;padding:0;background:none;border:none;color:var(--text-secondary);font-size:.82rem;font-weight:600;cursor:pointer}.kv-pref-btn svg{opacity:.8}.kv-pref-pill{position:relative;display:inline-flex;align-items:center;width:36px;height:20px;border-radius:999px;background:#ffffff14;border:1px solid var(--border);margin-left:4px}.kv-pref-knob{position:absolute;left:3px;width:12px;height:12px;border-radius:50%;background:#fff;box-shadow:0 1px 4px #0006}.kv-pref-btn.on{color:var(--text-primary)}.kv-pref-btn.on svg{color:var(--accent-v2)}.kv-pref-pill.on{background:var(--accent-v);border-color:var(--accent-v)}.kv-pref-pill.on .kv-pref-knob{left:19px}.kv-pref-knob{transition:left .18s ease}.action-btn.is-active{color:var(--accent-v2);border-color:#7c3aed73;background:#7c3aed1f}.kv-prefs-sep{width:1px;height:16px;background:#ffffff14}.watch-player-info{padding-top:14px}.watch-ep-label{font-size:.72rem;font-weight:800;color:var(--accent-v2);letter-spacing:.12em;text-transform:uppercase;margin:0 0 8px}.watch-ep-title{font-size:1.45rem;font-weight:800;line-height:1.25;margin:0 0 8px}.watch-filler-warn{color:#facc15;font-size:.82rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;margin-top:8px}.action-bar{display:flex;align-items:center;gap:12px;padding:14px 0 18px;border-bottom:1px solid var(--border);margin-bottom:18px}.action-anime{display:flex;align-items:center;gap:12px;min-width:0;flex:1;text-decoration:none;color:inherit}.aa-poster{width:52px;height:52px;border-radius:10px;overflow:hidden;flex-shrink:0;border:1px solid rgba(255,255,255,.08)}.aa-poster img{width:100%;height:100%;object-fit:cover;display:block}.aa-info{min-width:0;display:flex;flex-direction:column;gap:4px}.aa-name{font-size:.94rem;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.aa-meta{font-size:.72rem;color:var(--text-muted)}.action-buttons{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end;margin-left:auto}.audio-toggle{display:flex;align-items:center;background:#ffffff0a;border:1px solid var(--border);border-radius:999px;padding:4px;gap:4px}.audio-btn{padding:6px 12px;border-radius:999px;font-size:.78rem;font-weight:700;background:transparent;border:none;color:var(--text-secondary);cursor:pointer;transition:var(--transition)}.audio-btn.active{background:var(--accent-v);border-color:var(--accent-v);color:#fff}.audio-btn:disabled{opacity:.5;cursor:not-allowed}.action-btn{display:inline-flex;align-items:center;gap:8px;padding:8px 14px;border-radius:999px;background:#ffffff0f;border:1px solid var(--border);color:var(--text-primary);font-weight:700;font-size:.8rem;cursor:pointer;transition:var(--transition)}.action-btn svg{width:15px;height:15px}.action-btn:hover,.audio-btn:not(.active):hover{border-color:var(--border-bright)}.audio-lang-selected{font-size:.78rem;color:var(--text-muted)}.server-panel{background:var(--bg-surface);border:1px solid var(--border);border-radius:14px;padding:14px;margin:10px 0 0;box-shadow:0 20px 40px #0000003d}.server-panel.is-hidden{display:none}.server-panel-row{display:flex;align-items:flex-start;gap:14px;flex-wrap:wrap}.server-group{display:flex;flex-direction:column;gap:10px;flex:1;min-width:180px}.server-group-toggle{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:10px;background:#ffffff0a;border:1px solid var(--border);color:var(--text-primary);font-weight:700;cursor:pointer}.sg-icon{flex-shrink:0}.sg-label{flex:1;text-align:left}.sg-count{font-size:.72rem;padding:2px 7px;border-radius:999px;background:#ffffff14;color:var(--text-muted)}.server-btn-row{display:flex;gap:8px;flex-wrap:wrap;padding-left:2px}.server-option{padding:7px 12px;border-radius:10px;border:1px solid var(--border);background:#ffffff08;color:var(--text-secondary);font-size:.78rem;font-weight:600;cursor:pointer}.server-option.selected{background:#7c3aed29;border-color:#7c3aed6b;color:var(--text-primary)}.server-quality-wrap{margin-left:auto}.server-quality-select{padding:8px 10px;border-radius:10px;border:1px solid var(--border);background:#ffffff0a;color:var(--text-primary)}.server-loading,.server-error{display:flex;align-items:center;gap:8px;padding-top:12px;font-size:.8rem;color:var(--text-muted)}.server-loading-dot{width:8px;height:8px;border-radius:50%;background:var(--accent-v);display:inline-block;animation:pulse 1.3s ease-in-out infinite}@keyframes pulse{0%,to{opacity:.45;transform:scale(.9)}50%{opacity:1;transform:scale(1.1)}}.watch-desc-card{background:var(--bg-surface);border:1px solid var(--border);border-radius:14px;padding:18px 20px;margin-top:18px}.watch-sidebar{background:var(--bg-surface);border:1px solid var(--border);border-radius:14px;overflow:hidden;position:sticky;top:calc(var(--header-h) + 12px);max-height:calc(100vh - var(--header-h) - 24px);display:flex;flex-direction:column}.up-next-header{padding:14px 14px 0}.un-info{display:flex;flex-direction:column;gap:3px}.un-title{font-size:1rem;font-weight:800}.un-subtitle{font-size:.76rem;color:var(--accent-v2)}.next-ep-strip{padding:12px 14px 0}.next-ep-pill{display:flex;flex-direction:column;gap:8px;padding:10px 12px;border-radius:12px;background:#7c3aed14;border:1px solid rgba(124,58,237,.22);color:var(--text-primary)}.nep-bell{width:15px;height:15px}.nep-label{font-size:.72rem;font-weight:700;color:var(--text-muted);letter-spacing:.08em;text-transform:uppercase}.nep-units{display:flex;align-items:flex-end;gap:8px}.nec-unit{display:flex;align-items:flex-end;gap:2px;font-weight:800;color:#fff}.nec-num{font-size:.9rem}.nec-u{font-size:.6rem;color:var(--text-muted)}.ep-search-wrap{padding:12px 14px 0}.ep-search-input{width:100%;padding:9px 12px;background:var(--bg-elevated);border:1px solid var(--border);border-radius:10px;color:var(--text-primary);font-size:.82rem;outline:none}.ep-search-input::placeholder{color:var(--text-muted)}.ep-view-bar{display:flex;align-items:center;justify-content:space-between;padding:12px 14px 8px}.ep-view-bar-label{font-size:.68rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--text-muted)}.ep-view-toggle-btn{padding:5px 9px;border:1px solid var(--border);border-radius:8px;color:var(--text-secondary);font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em}.ep-view-toggle-btn:hover{color:var(--text-primary);border-color:var(--border-bright)}.ep-list{flex:1;overflow-y:auto;padding:0 8px 12px;scrollbar-width:thin;scrollbar-color:var(--accent-v) transparent}.ep-list::-webkit-scrollbar{width:3px;display:block}.ep-list::-webkit-scrollbar-thumb{background:var(--accent-v);border-radius:99px}.ep-list.compact{display:grid;grid-template-columns:repeat(auto-fill,minmax(54px,1fr));gap:7px;align-content:start}.ep-list.compact .ep-item{justify-content:center;margin:0;padding:9px 5px}.ep-list.compact .ep-thumb{width:auto;height:auto;background:transparent;overflow:visible}.ep-list.compact .ep-thumb img,.ep-list.compact .ep-info{display:none}.ep-list.compact .ep-thumb-label{position:static;background:transparent;padding:0;color:var(--text-secondary);font-size:.76rem}.ep-list.compact .ep-item.active .ep-thumb-label{color:var(--accent-v2)}.ep-item{display:flex;align-items:center;gap:10px;padding:7px 8px;border-radius:10px;cursor:pointer;transition:var(--transition);margin-bottom:2px;border:1px solid transparent}.ep-item:hover{background:#ffffff09}.ep-item.active{background:#7c3aed1f;border-color:#7c3aed40}.ep-thumb{width:72px;height:46px;flex-shrink:0;background:var(--bg-elevated);border-radius:8px;overflow:hidden;display:flex;align-items:center;justify-content:center;font-size:.68rem;font-weight:700;color:var(--text-muted);position:relative}.ep-thumb img{width:100%;height:100%;object-fit:cover}.ep-thumb-label{position:absolute;bottom:3px;left:3px;background:#000000b3;color:#fff;font-size:.62rem;font-weight:700;padding:1px 5px;border-radius:4px}.ep-info{flex:1;min-width:0}.ep-info-title{font-size:.8rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--text-primary)}.ep-info-sub{font-size:.7rem;color:var(--text-muted);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.audio-notice{display:flex;align-items:center;gap:10px;padding:9px 14px;background:#7c3aed14;border:1px solid rgba(124,58,237,.28);border-radius:var(--radius-md);margin:12px 0;font-size:.83rem;font-weight:500;color:var(--accent-c2);animation:fadeUp .2s ease both}.audio-notice svg{flex-shrink:0;color:var(--accent-v2)}.audio-notice .server-warning-close{margin-left:auto;color:var(--text-muted)}@media (max-width: 1024px){.watch-layout{grid-template-columns:1fr}.watch-sidebar{position:static;max-height:540px}}@media (max-width: 680px){.watch-container{padding:0 12px 60px}.watch-layout{gap:12px;padding-top:8px}.watch-warn-banner{padding:10px 12px}.action-bar{flex-wrap:wrap;border-bottom:none;padding-bottom:8px}.action-anime{width:100%}.action-buttons{width:100%;justify-content:flex-start}.watch-ep-title{font-size:1.15rem}.watch-desc-card{padding:14px 16px}.watch-sidebar{position:static;max-height:420px}.ep-thumb{width:64px;height:40px}.aa-poster{width:44px;height:44px}}.movie-search-wrap{position:relative}.movie-tabs{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px}.movie-tabs button{padding:8px 20px;border-radius:var(--radius-pill);background:#ffffff0f;border:1px solid var(--border);font-size:.86rem;font-weight:600;color:var(--text-secondary);transition:var(--transition);cursor:pointer}.movie-tabs button:hover{border-color:var(--border-bright);color:var(--text-primary)}.movie-tabs button.active{background:var(--accent-v);border-color:var(--accent-v);color:#fff;box-shadow:0 0 18px #7c3aed59}.movie-sort{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:0 0 24px}.movie-sort-label{font-size:.7rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--text-muted);margin-right:6px}.movie-sort button{padding:5px 13px;border-radius:var(--radius-pill);background:#ffffff0a;border:1px solid var(--border);font-size:.76rem;font-weight:600;color:var(--text-secondary);transition:var(--transition);cursor:pointer}.movie-sort button:hover{border-color:var(--border-bright);color:var(--text-primary)}.movie-sort button.active{background:#7c3aed2e;border-color:#7c3aed80;color:var(--accent-c2)}.movie-suggest{position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:60;background:var(--bg-elevated);border:1px solid var(--border-bright);border-radius:var(--radius-md);box-shadow:0 18px 50px #0009;overflow:hidden;max-height:320px;overflow-y:auto}.movie-suggest-item{display:flex;align-items:center;gap:10px;width:100%;padding:11px 16px;font-size:.88rem;color:var(--text-secondary);transition:var(--transition);cursor:pointer;text-align:left;background:none;border:none;border-bottom:1px solid var(--border)}.movie-suggest-item:last-child{border-bottom:none}.movie-suggest-item:hover{background:#7c3aed1f;color:var(--text-primary)}.movie-suggest-item svg{color:var(--accent-v2);flex-shrink:0}.load-more-wrap{display:flex;justify-content:center;margin-top:36px}.load-more-btn{min-width:220px;justify-content:center}.cast-row{display:flex;gap:14px;overflow-x:auto;padding:4px 0 14px;scrollbar-width:thin;scrollbar-color:var(--accent-v) transparent}.cast-chip{flex-shrink:0;width:96px;text-align:center;cursor:default}.cast-chip img{width:72px;height:72px;border-radius:50%;object-fit:cover;margin:0 auto 8px;border:2px solid var(--border);transition:var(--transition)}.cast-chip:hover img{border-color:var(--border-bright);transform:scale(1.06)}.cast-avatar-fallback{width:72px;height:72px;border-radius:50%;margin:0 auto 8px;background:var(--grad-accent);display:flex;align-items:center;justify-content:center;font-size:1.4rem;font-weight:800;color:#fff}.cast-chip-name{font-size:.76rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.cast-chip-role{font-size:.66rem;color:var(--text-muted);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ep-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(76px,1fr));gap:10px}.ep-grid-btn{display:flex;flex-direction:column;align-items:center;gap:6px;padding:12px 8px;background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-md);transition:var(--transition);cursor:pointer;color:var(--text-secondary)}.ep-grid-btn:hover{background:#7c3aed1f;border-color:var(--border-bright);color:var(--text-primary);transform:translateY(-2px)}.ep-grid-num{font-size:.82rem;font-weight:700}.ep-grid-play{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:50%;background:#7c3aed2e;color:var(--accent-v2);transition:var(--transition)}.ep-grid-btn:hover .ep-grid-play{background:var(--accent-v);color:#fff}.player-error{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;color:var(--text-muted);padding:20px;text-align:center}.player-error .big{font-size:2.6rem}.player-error p{font-size:.9rem;max-width:420px}.custom-player{position:relative;width:100%;height:100%;background:#000;outline:none;overflow:hidden;cursor:default}.custom-player video{width:100%;height:100%;object-fit:contain;background:#000}.custom-player video::-webkit-media-controls{display:none!important}.cp-fade{position:absolute;left:0;right:0;bottom:0;height:130px;background:linear-gradient(to top,rgba(0,0,0,.88),transparent);pointer-events:none;transition:opacity .25s ease;z-index:3}.cp-controls{position:absolute;left:0;right:0;bottom:0;padding:14px 16px 12px;display:flex;flex-direction:column;gap:8px;transition:opacity .25s ease,transform .25s ease;z-index:5}.cp-controls.hide,.cp-fade.hide{opacity:0;pointer-events:none;transform:translateY(10px)}.cp-seek{position:relative;height:18px;display:flex;align-items:center;cursor:pointer;touch-action:none}.cp-seek-track{position:absolute;left:0;right:0;height:4px;border-radius:99px;background:#ffffff38;transition:height .15s ease}.cp-seek:hover .cp-seek-track{height:6px}.cp-buffered{position:absolute;left:0;height:4px;border-radius:99px;background:#ffffff52;transition:height .15s ease}.cp-seek:hover .cp-buffered{height:6px}.cp-progress{position:absolute;left:0;height:4px;border-radius:99px;background:var(--accent-v);box-shadow:0 0 12px #7c3aeda6;transition:height .15s ease}.cp-seek:hover .cp-progress{height:6px}.cp-thumb{position:absolute;top:50%;width:13px;height:13px;border-radius:50%;background:#fff;transform:translate(-50%,-50%);opacity:0;transition:opacity .15s ease;box-shadow:0 1px 6px #00000080;pointer-events:none}.cp-seek:hover .cp-thumb{opacity:1}.cp-hover-time{position:absolute;top:-28px;transform:translate(-50%);background:#0a0a12eb;border:1px solid var(--border-bright);color:#fff;font-size:.72rem;font-weight:600;padding:2px 8px;border-radius:6px;pointer-events:none;white-space:nowrap}.cp-row{display:flex;align-items:center;gap:4px}.cp-btn{width:38px;height:38px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#ffffffeb;transition:var(--transition);background:transparent;flex-shrink:0}.cp-btn:hover{background:#ffffff1f;color:#fff}.cp-btn.active{color:var(--accent-v2);background:#7c3aed38}.cp-time{font-size:.8rem;font-weight:600;color:#ffffffd9;font-variant-numeric:tabular-nums;margin-left:6px;white-space:nowrap}.cp-time em{font-style:normal;color:#fff6;margin:0 4px}.cp-spacer{flex:1}.cp-vol-slider{width:74px;accent-color:var(--accent-v);cursor:pointer;flex-shrink:0}.cp-menu{position:absolute;bottom:56px;right:12px;background:#0c0c14f2;-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);border:1px solid var(--border-bright);border-radius:var(--radius-md);padding:8px 10px 10px;min-width:172px;box-shadow:0 16px 44px #0009;z-index:10;animation:fadeUp .18s ease both;max-height:min(340px,60vh);overflow-y:auto}.cp-menu-title{font-size:.64rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--text-muted);padding:8px 10px 4px}.cp-menu-title:first-child{padding-top:2px}.cp-menu-item{display:block;width:100%;text-align:left;padding:7px 10px;border-radius:var(--radius-sm);font-size:.82rem;font-weight:600;color:var(--text-secondary);transition:var(--transition);cursor:pointer;background:none;border:none}.cp-menu-item:hover{background:#ffffff0f;color:var(--text-primary)}.cp-menu-item.active{background:var(--accent-v);color:#fff}.cp-menu-note{font-size:.7rem;color:var(--text-muted);padding:8px 10px 2px}.cp-center-play{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:76px;height:76px;border-radius:50%;background:#0a0a1299;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);border:1px solid rgba(255,255,255,.18);color:#fff;display:flex;align-items:center;justify-content:center;transition:var(--transition);z-index:4}.cp-center-play:hover{background:var(--accent-v);border-color:var(--accent-v);transform:translate(-50%,-50%) scale(1.08);box-shadow:0 0 34px #7c3aed99}.skip-btn{position:absolute;right:18px;bottom:78px;z-index:6;padding:9px 18px;border-radius:var(--radius-pill);background:#0c0c14d9;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid var(--border-bright);color:#fff;font-size:.84rem;font-weight:700;opacity:0;transform:translateY(10px);pointer-events:none;transition:var(--transition);display:flex;align-items:center;gap:6px}.skip-btn.show{opacity:1;transform:translateY(0);pointer-events:auto}.skip-btn:hover{background:var(--accent-v);border-color:var(--accent-v)}.movie-quality-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:12px 16px;background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-md);margin:10px 0}.movie-quality-label{font-size:.7rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--text-muted);margin-right:4px}.movie-quality-bar button{padding:5px 14px;border-radius:var(--radius-pill);font-size:.78rem;font-weight:700;background:#ffffff0f;border:1px solid var(--border);color:var(--text-secondary);transition:var(--transition);cursor:pointer}.movie-quality-bar button:hover{border-color:var(--border-bright);color:var(--text-primary)}.movie-quality-bar button.active{background:var(--accent-v);border-color:var(--accent-v);color:#fff}.genre-nav-wrap{position:relative}.row-nav-btn{position:absolute;top:50%;transform:translateY(-50%);z-index:6;width:38px;height:38px;border-radius:50%;background:#0a0a12d9;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid var(--border-bright);color:var(--text-primary);display:flex;align-items:center;justify-content:center;opacity:0;transition:var(--transition);cursor:pointer;box-shadow:0 6px 20px #00000080}.genre-nav-wrap:hover .row-nav-btn{opacity:1}.row-nav-btn:hover{background:var(--accent-v);border-color:var(--accent-v);transform:translateY(-50%) scale(1.08)}.row-nav-btn.prev{left:-14px}.row-nav-btn.next{right:-14px}.cw-subtitle{font-size:.78rem;font-weight:500;color:var(--text-muted)}.cw-card{position:relative;flex-shrink:0;width:250px;border-radius:var(--radius-md);overflow:hidden;background:var(--bg-card);cursor:pointer;transition:var(--transition);border:1px solid var(--border)}.cw-card:hover{transform:translateY(-4px);border-color:var(--border-bright);box-shadow:var(--shadow-hover)}.cw-poster{position:relative;aspect-ratio:16/9;overflow:hidden;background:var(--bg-elevated)}.cw-poster img{width:100%;height:100%;object-fit:cover;transition:transform .45s ease}.cw-card:hover .cw-poster img{transform:scale(1.06)}.cw-overlay{position:absolute;top:0;right:0;bottom:0;left:0;background:#08080c8c;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);opacity:0;transition:var(--transition);display:flex;align-items:center;justify-content:center;gap:8px;color:#fff;font-size:.86rem;font-weight:700}.cw-card:hover .cw-overlay{opacity:1}.cw-overlay svg{width:20px;height:20px}.cw-badge{position:absolute;top:8px;left:8px;padding:2px 8px;border-radius:var(--radius-pill);background:#080808c7;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);border:1px solid var(--border);color:#fff;font-size:.64rem;font-weight:700;letter-spacing:.04em}.cw-progress{position:absolute;left:0;right:0;bottom:0;height:4px;background:#ffffff29}.cw-progress-fill{height:100%;background:var(--grad-accent);box-shadow:0 0 8px #7c3aed99}.cw-remove{position:absolute;top:6px;right:6px;width:22px;height:22px;border-radius:50%;background:#080808cc;border:1px solid var(--border);color:var(--text-secondary);font-size:.9rem;line-height:1;display:flex;align-items:center;justify-content:center;opacity:0;transition:var(--transition);cursor:pointer;padding:0}.cw-card:hover .cw-remove{opacity:1}.cw-remove:hover{background:#e11d48;border-color:#e11d48;color:#fff}.cw-body{padding:10px 12px 12px}.cw-body h3{font-size:.88rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:4px}.cw-meta{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:.7rem;color:var(--text-muted)}.cw-meta-left{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.cw-left{color:var(--accent-v2);font-weight:600;font-variant-numeric:tabular-nums;white-space:nowrap}.spotlight-featured{position:relative;width:100%;min-height:500px;border-radius:16px;overflow:hidden;background:var(--bg-elevated) center/cover no-repeat;border:1px solid var(--border);cursor:pointer;transition:var(--transition)}.spotlight-featured:hover{transform:translateY(-4px);border-color:var(--border-bright);box-shadow:var(--shadow-hover)}.spotlight-featured-grad{position:absolute;top:0;right:0;bottom:0;left:0;background:linear-gradient(180deg,#08080c14,#08080c3d 34%,#08080cb8 75%,#08080ce6)}.spotlight-featured-body{position:absolute;left:0;right:0;bottom:0;padding:26px 26px 24px;z-index:2}.spotlight-featured-meta-row{display:flex;align-items:center;gap:10px;font-size:.72rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:#f8d6d6;opacity:.9;margin-bottom:12px}.spotlight-featured-bubble{display:inline-flex;align-items:center;justify-content:center;margin-bottom:12px}.spotlight-featured-bubble-inner{display:flex;flex-direction:column;align-items:flex-start;padding:11px 16px;border-radius:999px;background:#123a8261;border:1px solid rgba(255,255,255,.18);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.spotlight-featured-bubble-kicker{font-size:.68rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#dbeafe}.spotlight-featured-bubble-title{font-size:1rem;font-weight:800;color:#f2fbff;line-height:1.15}.spotlight-featured-title{font-family:Bebas Neue,Outfit,sans-serif;font-size:clamp(2.2rem,4.6vw,4.4rem);font-weight:400;line-height:.9;margin:0 0 10px;text-shadow:0 8px 30px rgba(0,0,0,.7);letter-spacing:.02em;max-width:52%}.spotlight-featured-caption{max-width:48%;font-size:.9rem;color:#fffc;line-height:1.55;margin-bottom:18px}.spotlight-featured-actions{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.spotlight-featured-btn{position:relative;z-index:2}.spotlight-featured-btn.secondary{background:#ffffff14;border:1px solid rgba(255,255,255,.12);color:#fff}.spotlight-slide{animation:spotFade .5s ease both}@keyframes spotFade{0%{opacity:0;transform:scale(1.03)}to{opacity:1;transform:scale(1)}}.spotlight-nav-btn{position:absolute;top:50%;transform:translateY(-50%);z-index:3;width:36px;height:36px;border-radius:50%;background:#0808089e;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);border:1px solid rgba(255,255,255,.16);color:#fff;display:flex;align-items:center;justify-content:center;opacity:.7;transition:var(--transition);cursor:pointer}.spotlight-featured:hover .spotlight-nav-btn{opacity:1}.spotlight-nav-btn:hover{background:var(--accent-v);border-color:var(--accent-v);transform:translateY(-50%) scale(1.08)}.spotlight-nav-btn.prev{left:12px}.spotlight-nav-btn.next{right:12px}.spotlight-dots{position:absolute;bottom:14px;left:0;right:0;display:flex;justify-content:center;gap:6px;z-index:3}.spotlight-dot{width:7px;height:7px;border-radius:50%;background:#ffffff59;transition:var(--transition);cursor:pointer;padding:0}.spotlight-dot.active{background:var(--accent-v2);width:20px;border-radius:99px;box-shadow:0 0 10px #7c3aed99}.genre-section{padding-top:8px;padding-bottom:12px}.genre-row-wrap{position:relative;display:flex;align-items:center;gap:12px}.genre-row{display:flex;align-items:center;gap:10px;overflow-x:auto;scrollbar-width:none;padding:6px 0}.genre-row::-webkit-scrollbar{display:none}.genre-pill{padding:8px 16px;border-radius:var(--radius-pill);background:#ffffff09;border:1px solid var(--border);color:var(--text-secondary);font-size:.82rem;font-weight:600;transition:var(--transition);cursor:pointer;white-space:nowrap}.genre-pill:hover{background:#7c3aed1f;border-color:var(--border-bright);color:var(--text-primary)}.home-dashboard{padding-top:4px}.home-layout{display:grid;grid-template-columns:minmax(0,1.9fr) 300px;gap:22px;align-items:start}.home-main-column{display:flex;flex-direction:column;gap:10px}.home-sidebar{display:flex;flex-direction:column;gap:18px;position:sticky;top:88px}.sidebar-panel{background:#ffffff05;border:1px solid var(--border);border-radius:12px;padding:18px 14px}.sidebar-panel h3{font-size:.96rem;font-weight:800;letter-spacing:.02em;margin:0 0 14px}.sidebar-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}.sidebar-list li{display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid rgba(255,255,255,.04);cursor:pointer}.sidebar-list li:last-child{border-bottom:none}.sidebar-rank{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:8px;background:var(--grad-accent);font-size:.72rem;font-weight:800;color:#fff;flex-shrink:0}.sidebar-rank.alt{background:linear-gradient(135deg,#0ea5e9e6,#7c3aede6)}.sidebar-info{display:flex;flex-direction:column;min-width:0}.sidebar-info strong{font-size:.8rem;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sidebar-info small{font-size:.67rem;color:var(--text-muted);margin-top:2px}.landing-enter{position:relative;padding:90px 24px;overflow:hidden;text-align:center;border-top:1px solid var(--border)}.landing-enter-glow{position:absolute;top:0;right:0;bottom:0;left:0;background:radial-gradient(ellipse 55% 80% at 50% 110%,rgba(124,58,237,.3) 0%,transparent 65%);pointer-events:none}.landing-enter-inner{position:relative;max-width:560px;margin:0 auto}.landing-enter-label{font-size:.72rem;font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:var(--accent-v2);margin-bottom:14px}.landing-enter-title{font-family:Bebas Neue,Outfit,sans-serif;font-size:clamp(2rem,4vw,3.2rem);font-weight:400;line-height:1.05;margin-bottom:12px}.landing-enter-desc{font-size:.95rem;color:var(--text-secondary);line-height:1.7;margin-bottom:26px}.empty{padding:40px;text-align:center;color:var(--text-muted)}.empty .big{font-size:2.6rem;margin-bottom:12px}.empty p{font-size:.9rem}.card-rank{position:absolute;top:8px;left:8px;z-index:2;width:26px;height:26px;border-radius:8px;background:var(--grad-accent);color:#fff;font-size:.78rem;font-weight:800;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 14px #00000080}.card-type-badge{position:absolute;top:8px;right:8px;z-index:2;padding:3px 9px;border-radius:var(--radius-pill);background:#080808b8;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);border:1px solid var(--border-bright);color:var(--accent-c2);font-size:.62rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase}.card-type-badge.coming-soon{background:#f59e0b2e;border-color:#f59e0b8c;color:#fcd34d}.card-quick-btn.coming-soon{background:#f59e0be6;color:#1a1205}.card-ep-badge{position:absolute;top:8px;left:8px;z-index:2;padding:3px 9px;border-radius:var(--radius-pill);background:#080808b8;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);border:1px solid var(--border);color:#fff;font-size:.62rem;font-weight:700;letter-spacing:.04em}#root .mobile-nav{display:flex!important;position:fixed;left:0;right:0;bottom:0;z-index:120;height:calc(60px + env(safe-area-inset-bottom));padding-bottom:env(safe-area-inset-bottom);background:#0a0a12f5;-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);border-top:1px solid var(--border);justify-content:space-around;align-items:stretch;box-shadow:0 -16px 34px #0a0a0ea6}#root .mobile-nav-item{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;color:var(--text-muted);font-size:.6rem;font-weight:600;letter-spacing:.02em;transition:var(--transition)}#root .mobile-nav-item.active{color:var(--accent-v2);background:linear-gradient(180deg,#7c3aed1f,#7c3aed05)}#root .mobile-nav-item.active svg{filter:drop-shadow(0 0 8px rgba(124,58,237,.6))}#root .mobile-nav-item:active{transform:scale(.94)}@media (max-width:1024px){.search-wrap{max-width:260px}nav{gap:0}nav a{padding:6px 10px;font-size:.8rem}.container{padding:0 20px}.page-shell{padding-top:calc(var(--header-h) + 24px);padding-bottom:64px}.catalog-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:18px 14px}.watch-grid,.info-layout{grid-template-columns:1fr}.watch-sidebar{position:static;max-height:500px}.info-hero-body{flex-direction:column;align-items:flex-start}.spotlight-featured{min-height:340px}}@media (max-width:900px){.main-content{padding-bottom:calc(66px + env(safe-area-inset-bottom))}.home-hero{padding-bottom:110px}.scroll-indicator{bottom:calc(80px + env(safe-area-inset-bottom))}.landing-enter{padding-bottom:120px}}@media (max-width:680px){.container{padding:0 16px}nav{display:none}.page-shell{padding-top:calc(var(--header-h) + 18px);padding-bottom:60px}.catalog-grid{grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:16px 12px}.search-shell{gap:12px}#header .container{gap:10px}.logo-wordmark .l{font-size:1.6rem}.search-wrap{display:none}.auth-avatar-btn{padding:5px 7px;gap:0;border-radius:14px}.auth-avatar-btn span,.auth-avatar-btn>svg:last-child{display:none}.auth-avatar-circle{width:30px;height:30px}.auth-dropdown{min-width:220px;max-width:calc(100vw - 24px)}.content-section{padding:26px 0}.section-header{margin-bottom:14px}.section-title{font-size:1.02rem}.section-title:before{height:19px}.scroll-row{gap:10px}.anime-card{width:138px;border-radius:var(--radius-sm)}.skel-card{width:138px}.card-body{padding:8px 9px 10px}.card-title{font-size:.78rem}.cw-card{width:198px}.spotlight-featured{min-height:262px;border-radius:14px}.spotlight-featured-body{padding:12px 12px 38px}.spotlight-featured-score{font-size:.68rem;margin-bottom:4px}.spotlight-featured-title{font-size:1.28rem;margin-bottom:4px}.spotlight-featured-meta{font-size:.7rem;margin-bottom:10px}.spotlight-featured-btn{padding:8px 15px;font-size:.76rem}.spotlight-nav-btn{opacity:1;width:30px;height:30px}.spotlight-nav-btn.prev{left:8px}.spotlight-nav-btn.next{right:8px}.spotlight-dots{bottom:9px;gap:5px}.spotlight-dot{width:6px;height:6px}.spotlight-dot.active{width:16px}.genre-row-wrap,.genre-row{gap:8px}.genre-pill{padding:7px 12px;font-size:.74rem}.home-layout{grid-template-columns:1fr;gap:14px}.home-sidebar{position:static}.sidebar-panel{padding:14px 12px}.btn{padding:10px 18px;font-size:.85rem}.btn-sm{padding:6px 13px;font-size:.78rem}.section-link{font-size:.78rem}.home-hero{padding:calc(var(--header-h) + 30px) 14px 0}.home-hero{padding-bottom:110px}.hero-badge{padding:5px 13px;font-size:.66rem;margin-bottom:22px}.hero-logo-text{font-size:clamp(3.2rem,17vw,5.6rem);margin-bottom:12px}.hero-subtitle{font-size:clamp(1.05rem,3.6vw,1.45rem);margin-bottom:10px}.hero-desc{font-size:.88rem;margin-bottom:22px}.hero-filters{gap:6px;margin-bottom:18px}.hero-filter-pill{padding:6px 14px;font-size:.8rem}.hero-search-input{padding:12px 52px 12px 16px;font-size:.9rem}.hero-search-btn{width:38px;height:38px}.hero-ctas{gap:9px;margin-bottom:32px}.hero-cta{padding:11px 17px;font-size:.84rem;gap:7px}.hero-stat{padding:0 14px}.hero-stat-val{font-size:1.15rem}.hero-stat-label{font-size:.6rem}.library-section{padding:44px 0}.library-header{gap:12px;margin-bottom:20px}.library-header-text{font-size:.62rem}.library-header-line{max-width:90px}.marquee-poster{width:86px;height:120px}.marquee-row{gap:8px}.feature-section{padding:48px 0}.feature-eyebrow{font-size:.7rem;margin-bottom:12px}.feature-heading{font-size:1.7rem;line-height:1.2;margin-bottom:14px}.feature-desc{font-size:.9rem;margin-bottom:26px}.feature-card{padding:18px}.landing-enter{padding:56px 18px 118px}.landing-enter-title{font-size:1.9rem}.landing-enter-desc{font-size:.88rem;margin-bottom:20px}.info-hero{min-height:320px}.info-hero-content{padding:20px 0}.info-hero-body{gap:16px}.info-poster{width:108px}.info-title{font-size:1.7rem;margin-bottom:10px}.info-type-tag{font-size:.72rem;margin-bottom:8px}.info-badges{margin-bottom:12px}.info-genres{gap:6px;margin-bottom:16px}.info-genre-pill{padding:4px 11px;font-size:.74rem}.info-actions{gap:8px}.countdown-wrap{padding:10px 14px;gap:8px;margin-bottom:16px}.countdown-cell{min-width:50px;padding:6px 4px}.countdown-num{font-size:1.05rem}.countdown-unit{font-size:.56rem}.info-body{padding:26px 0 60px}.info-layout{gap:30px}.info-section-label{font-size:1.02rem}.info-synopsis{font-size:.88rem}.info-stat-cell{padding:13px 14px}.char-avatar{width:64px;height:64px}.char-name{font-size:.72rem;max-width:72px}.info-btn-watch{padding:10px 20px}.watch-container{padding:0 12px 60px}.watch-grid{gap:12px;padding-top:8px}.watch-anime-row{flex-wrap:wrap}.watch-info-card{padding:14px 16px}.watch-info-title{font-size:.95rem}.ep-title{font-size:1.1rem;margin-bottom:12px}.prefs-bar{padding:10px 12px;gap:12px}.pref-item{font-size:.78rem}.audio-btn,.server-btn{padding:4px 10px;font-size:.74rem}.audio-notice{margin:10px 0;font-size:.78rem;padding:8px 12px}.watch-anime-name{font-size:.85rem}.sidebar-upnext{font-size:.92rem}.ep-thumb{width:64px;height:40px}.ep-item{padding:6px}.ep-info-title{font-size:.76rem}.ep-grid{grid-template-columns:repeat(auto-fill,minmax(64px,1fr));gap:8px}.movie-quality-bar{padding:10px 12px;gap:6px}.movie-quality-bar button{padding:4px 11px;font-size:.72rem}.movie-tabs{flex-wrap:nowrap;overflow-x:auto;padding-bottom:4px;scrollbar-width:none}.movie-tabs::-webkit-scrollbar{display:none}.movie-tabs button{white-space:nowrap;padding:7px 16px;font-size:.8rem}.movie-sort button{padding:5px 11px;font-size:.72rem}.load-more-btn{min-width:170px}.cp-controls{padding:10px 8px 8px}.cp-btn{width:34px;height:34px}.cp-vol-slider{width:52px}.cp-time{font-size:.72rem}.cp-menu{right:6px;bottom:52px;min-width:150px}.cp-center-play{width:60px;height:60px}.skip-btn{bottom:72px;right:12px;padding:8px 14px;font-size:.8rem}.auth-overlay{padding:0;align-items:flex-end}.auth-modal{max-width:100%;border-radius:20px 20px 0 0;padding:24px 20px calc(20px + env(safe-area-inset-bottom));animation:sheetUp .25s ease both}@keyframes sheetUp{0%{transform:translateY(44px);opacity:0}to{transform:translateY(0);opacity:1}}}@media (max-width:420px){.anime-card,.skel-card{width:126px}.cw-card{width:172px}.marquee-poster{width:74px;height:104px}.info-poster{width:94px}.hero-logo-text{font-size:3rem}.hero-ctas{flex-direction:column}.hero-cta{width:100%;justify-content:center}.spotlight-featured{min-height:240px}.auth-dropdown{left:12px;right:12px;min-width:0}}#hero{position:relative;height:56vh;min-height:600px;max-height:720px;overflow:hidden;background:var(--bg-base)}#hero .hero-slide{position:absolute;top:0;right:0;bottom:0;left:0;opacity:0;transition:opacity .7s ease;pointer-events:none;will-change:opacity}#hero .hero-slide.active{opacity:1;pointer-events:auto}#hero .hero-bg{position:absolute;top:0;right:0;bottom:0;left:0}#hero .hero-bg img{width:100%;height:100%;object-fit:cover;object-position:center top;transform:scale(1.025);transition:transform 8s ease;filter:saturate(.9) contrast(1.04)}#hero .hero-slide.active .hero-bg img{transform:scale(1)}#hero .hero-gradient{position:absolute;top:0;right:0;bottom:0;left:0;background:linear-gradient(90deg,#06070af5,#06070aa3 40%,#06070a1a 72%),linear-gradient(0deg,rgba(6,7,10,.98) 0%,transparent 52%)}#hero .hero-content{position:relative;z-index:2;height:100%;display:flex;align-items:flex-end;padding:calc(var(--header-h) + 20px) 0 72px}#hero .hero-content .container{width:100%}#hero .hero-layout{display:flex;align-items:center;justify-content:space-between;gap:40px;width:100%}#hero .hero-info{max-width:560px;animation:fadeUp .6s ease both}#hero .hero-title-block{display:flex;flex-direction:column;align-items:flex-start;gap:10px}#hero .hero-badges{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:6px}#hero .hero-title{font-size:clamp(2rem,4vw,3.35rem);font-weight:800;line-height:1.12;letter-spacing:-.035em;margin-bottom:14px;text-wrap:balance}#hero .hero-desc{font-size:.95rem;color:var(--text-secondary);line-height:1.7;margin:0 0 18px;max-width:470px;display:-webkit-box;-webkit-line-clamp:3;line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;text-align:left}#hero .hero-genres{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:24px}#hero .hero-genre-tag{padding:4px 10px;border-radius:var(--radius-pill);background:#ffffff14;border:1px solid rgba(255,255,255,.1);font-size:.75rem;font-weight:600;color:var(--text-secondary);transition:var(--transition)}#hero .hero-genre-tag:hover{background:#7c3aed33;border-color:var(--accent-v);color:var(--accent-v2)}#hero .hero-actions{display:flex;gap:12px;flex-wrap:wrap}#hero .hero-stats{display:flex;gap:20px;margin-bottom:14px;flex-wrap:wrap}#hero .hero-stat{display:flex;align-items:center;gap:6px;font-size:.82rem;color:var(--text-secondary)}#hero .hero-stat svg{color:var(--accent-v2);flex-shrink:0}#hero .hero-cover-wrap{position:relative;flex-shrink:0;animation:fadeIn .8s ease .3s both}#hero .hero-cover{width:190px;aspect-ratio:2/3;object-fit:cover;border-radius:var(--radius-lg);box-shadow:0 8px 48px #0009,0 0 0 1px var(--border);display:block}#hero .hero-cover-glow{position:absolute;top:-14px;right:-14px;bottom:-14px;left:-14px;border-radius:32px;background:radial-gradient(ellipse at center,rgba(139,92,246,.28),transparent 70%);z-index:-1;animation:glow-pulse 3s ease infinite}#hero .hero-prev,#hero .hero-next{position:absolute;top:50%;z-index:5;width:48px;height:48px;border-radius:50%;background:#0a0a12b8;border:1px solid var(--border);display:flex;align-items:center;justify-content:center;color:var(--text-primary);transition:var(--transition);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);opacity:0;pointer-events:none}#hero .hero-prev{left:40px;transform:translateY(-50%)}#hero .hero-next{right:40px;transform:translateY(-50%)}#hero:hover .hero-next,#hero:hover .hero-prev{opacity:1;pointer-events:auto}#hero .hero-prev:hover{background:#7c3aed59;border-color:var(--accent-v);transform:translate(-4px,-50%)}#hero .hero-next:hover{background:#7c3aed59;border-color:var(--accent-v);transform:translate(4px,-50%)}#hero .hero-indicators{position:absolute;bottom:25px;left:50%;transform:translate(-50%);display:flex;gap:8px;z-index:5}#hero .hero-dot{width:8px;height:8px;border-radius:99px;background:#ffffff4d;transition:var(--transition)}#hero .hero-dot.active{width:28px;background:var(--accent-v)}.genre-bar{display:flex;align-items:center;gap:8px;overflow-x:auto;padding:14px 0 12px;border-bottom:1px solid var(--border);scrollbar-width:none;margin-bottom:24px}.genre-bar::-webkit-scrollbar{display:none}.genre-pill{flex-shrink:0;padding:6px 14px;border-radius:var(--radius-pill);background:#ffffff0f;border:1px solid rgba(255,255,255,.08);font-size:.8rem;font-weight:500;color:var(--text-secondary);transition:var(--transition);white-space:nowrap}.genre-pill:hover{background:#7c3aed2e;border-color:var(--accent-v);color:var(--accent-v2);transform:translateY(-1px)}.home-grid{display:grid;grid-template-columns:minmax(0,1fr) 260px;gap:28px;align-items:start}.home-sections{min-width:0}.home-grid .sidebar-widget{position:sticky;top:calc(var(--header-h) + 16px);background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-md);overflow:hidden}.home-grid .sidebar-tabs{display:flex;gap:16px;padding:14px 16px 0;border-bottom:1px solid var(--border);background:#ffffff05}.home-grid .sidebar-tab{padding:0 0 10px;font-size:.82rem;font-weight:700;text-transform:uppercase;color:var(--text-secondary);border-bottom:2px solid transparent;transition:var(--transition)}.home-grid .sidebar-tab:hover{color:var(--text-primary)}.home-grid .sidebar-tab.active{color:var(--accent-v2);border-bottom-color:var(--accent-v)}.home-grid .sidebar-item{display:flex;align-items:center;gap:12px;width:100%;padding:12px 16px;transition:var(--transition);border-bottom:1px solid var(--border);color:inherit;text-align:left}.home-grid .sidebar-item:last-child{border-bottom:none}.home-grid .sidebar-item:hover{background:var(--bg-elevated)}.home-grid .sidebar-thumb{width:50px;height:68px;object-fit:cover;border-radius:6px;flex-shrink:0}.home-grid .sidebar-rank{font-size:.72rem;font-weight:800;color:var(--text-muted);width:18px;flex-shrink:0}.home-grid .sidebar-title{font-size:.86rem;font-weight:600;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.home-grid .sidebar-meta{font-size:.7rem;color:var(--text-muted);margin-top:4px}.catalog-grid{grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:22px}@media (max-width:1024px){.home-grid{grid-template-columns:1fr}.home-grid .sidebar-widget{position:static;margin-top:20px}}@media (max-width:680px){#hero{height:620px;min-height:0}#hero .hero-content{align-items:flex-end;padding:calc(var(--header-h) + 18px) 0 58px}#hero .hero-cover-wrap,#hero .hero-prev,#hero .hero-next{display:none}#hero .hero-title{font-size:clamp(1.8rem,9vw,2.5rem)}#hero .hero-desc{-webkit-line-clamp:4;line-clamp:4}#hero .hero-indicators{bottom:22px}.genre-bar{margin-bottom:10px}}@media (max-width:600px){.catalog-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.catalog-grid .anime-card{width:100%}}@media (min-width:601px) and (max-width:768px){.anime-card{width:130px}}@media (max-width:420px){.anime-card{width:126px}}.watch-desc-card{display:flex;gap:18px;align-items:flex-start;position:relative;isolation:isolate;overflow:hidden;background:linear-gradient(135deg,#7c3aed29,#0d0d14f5 38%,#0b0b11fa);border-color:#8b5cf642;box-shadow:0 18px 42px #0003}.watch-desc-card:before{content:"";position:absolute;top:0;right:0;bottom:0;left:0;background:radial-gradient(circle at 0 0,rgba(167,139,250,.15),transparent 42%);z-index:-1}.watch-info-poster{width:92px;aspect-ratio:2/3;object-fit:cover;border-radius:10px;box-shadow:0 10px 24px #0000006b;flex-shrink:0}.watch-info-body{min-width:0;flex:1}.watch-info-kicker{font-size:.66rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--accent-v2);margin-bottom:5px}.watch-info-title{font-size:1.22rem;font-weight:800;line-height:1.22;letter-spacing:-.02em;margin-bottom:10px}.watch-info-meta{display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin-bottom:12px}.watch-meta-item,.watch-meta-score{display:inline-flex;align-items:center;min-height:23px;padding:3px 8px;border-radius:999px;background:#ffffff0e;border:1px solid rgba(255,255,255,.075);font-size:.7rem;font-weight:700;color:var(--text-secondary)}.watch-meta-score{color:#fcd34d;background:#f59e0b1a;border-color:#f59e0b33}.watch-meta-dot{display:none}.watch-genres{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 11px}.watch-genre-pill{padding:3px 8px;border-radius:999px;background:#7c3aed21;border:1px solid rgba(124,58,237,.22);color:#c4b5fd;font-size:.68rem;font-weight:700}.watch-synopsis{margin:0;color:var(--text-secondary);font-size:.86rem;line-height:1.62;display:-webkit-box;-webkit-line-clamp:3;line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}@media (max-width:600px){.watch-desc-card{gap:12px;padding:14px}.watch-info-poster{width:72px;border-radius:8px}.watch-info-title{font-size:1.04rem}.watch-synopsis{-webkit-line-clamp:4;line-clamp:4}}
|
frontend/dist/assets/index-BYVU7BLe.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/dist/index.html
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<meta name="description" content="anidoom - Stream thousands of anime series free in HD. Subbed and dubbed. No ads, no limits." />
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 9 |
+
<link
|
| 10 |
+
href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800;900&family=Bebas+Neue&family=Rubik+Dirt&display=swap"
|
| 11 |
+
rel="stylesheet"
|
| 12 |
+
/>
|
| 13 |
+
<title>anidoom — Anime, Manga, Movies & Music in One Place</title>
|
| 14 |
+
<script type="module" crossorigin src="/assets/index-BYVU7BLe.js"></script>
|
| 15 |
+
<link rel="stylesheet" crossorigin href="/assets/index-BOX6jOo8.css">
|
| 16 |
+
</head>
|
| 17 |
+
<body>
|
| 18 |
+
<div id="root"></div>
|
| 19 |
+
</body>
|
| 20 |
+
</html>
|
frontend/dist/music-api.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// music-api.js — fetchAPI bridge for music.js
|
| 2 |
+
// Connects music.js directly to AnimeThemes API v2.
|
| 3 |
+
|
| 4 |
+
(function () {
|
| 5 |
+
const AT = 'https://api.animethemes.moe';
|
| 6 |
+
|
| 7 |
+
async function _get(url) {
|
| 8 |
+
const r = await fetch(url, { headers: { Accept: 'application/json' } });
|
| 9 |
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
| 10 |
+
return r.json();
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
function normalizeImages(images) {
|
| 14 |
+
if (!images || !Array.isArray(images)) return [];
|
| 15 |
+
return images.map(img => {
|
| 16 |
+
let link = img.link || img.url;
|
| 17 |
+
if (!link && img.path) link = `https://static.animethemes.moe/${img.path}`;
|
| 18 |
+
if (!link && img.basename) link = `https://static.animethemes.moe/${img.basename}`;
|
| 19 |
+
return {
|
| 20 |
+
facet: img.facet || 'Large Cover',
|
| 21 |
+
link: link || ''
|
| 22 |
+
};
|
| 23 |
+
});
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
function normalizeTheme(t, animeRef) {
|
| 27 |
+
if (!t) return null;
|
| 28 |
+
const entries = t.animethemeentries || t.entries || [];
|
| 29 |
+
const normEntries = entries.map(e => {
|
| 30 |
+
const videos = e.videos || [];
|
| 31 |
+
const normVideos = videos.map(v => {
|
| 32 |
+
let audioLink = v.audio?.link || v.link || v.url;
|
| 33 |
+
if (!audioLink && v.basename) audioLink = `https://v.animethemes.moe/${v.basename}`;
|
| 34 |
+
if (!audioLink && v.filename) audioLink = `https://v.animethemes.moe/${v.filename}`;
|
| 35 |
+
return {
|
| 36 |
+
...v,
|
| 37 |
+
audio: { id: v.audio?.id || v.id || t.id || '1', link: audioLink }
|
| 38 |
+
};
|
| 39 |
+
});
|
| 40 |
+
return { ...e, videos: normVideos };
|
| 41 |
+
});
|
| 42 |
+
return {
|
| 43 |
+
...t,
|
| 44 |
+
id: t.id || Math.random().toString(36).substring(7),
|
| 45 |
+
slug: t.slug || `${t.type || 'OP'}${t.sequence || ''}`,
|
| 46 |
+
type: t.type || 'OP',
|
| 47 |
+
animethemeentries: normEntries,
|
| 48 |
+
anime: animeRef || t.anime
|
| 49 |
+
};
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
function normalizeAnime(a) {
|
| 53 |
+
if (!a) return null;
|
| 54 |
+
const rawThemes = a.animethemes || a.themes || [];
|
| 55 |
+
const animeObj = {
|
| 56 |
+
id: a.slug || String(a.id),
|
| 57 |
+
rawId: a.id,
|
| 58 |
+
name: a.name || a.title || a.name_jp || 'Anime',
|
| 59 |
+
slug: a.slug || String(a.id),
|
| 60 |
+
year: a.year || (a.season ? a.season : ''),
|
| 61 |
+
media_format: a.media_format || a.format || 'TV',
|
| 62 |
+
images: normalizeImages(a.images)
|
| 63 |
+
};
|
| 64 |
+
animeObj.animethemes = rawThemes.map(t => normalizeTheme(t, animeObj));
|
| 65 |
+
return animeObj;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
function normalizeArtist(art) {
|
| 69 |
+
if (!art) return null;
|
| 70 |
+
return {
|
| 71 |
+
id: art.slug || String(art.id),
|
| 72 |
+
name: art.name || 'Artist',
|
| 73 |
+
slug: art.slug || String(art.id),
|
| 74 |
+
images: normalizeImages(art.images),
|
| 75 |
+
songs: art.songs || []
|
| 76 |
+
};
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
async function fetchAPI(path) {
|
| 80 |
+
try {
|
| 81 |
+
// /themes/search?q=<q>
|
| 82 |
+
if (path.startsWith('/themes/search')) {
|
| 83 |
+
const q = new URL(path, 'https://x').searchParams.get('q') || '';
|
| 84 |
+
const data = await _get(`${AT}/search?q=${encodeURIComponent(q)}&include[anime]=animethemes.animethemeentries.videos.audio,animethemes.song.artists,images`);
|
| 85 |
+
const list = (data.search?.anime || data.anime || []).map(normalizeAnime);
|
| 86 |
+
return { anime: list };
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
// /themes/anime/<id_or_slug>
|
| 90 |
+
if (path.startsWith('/themes/anime/')) {
|
| 91 |
+
const param = decodeURIComponent(path.replace('/themes/anime/', '').split('?')[0]);
|
| 92 |
+
let rawAnime = null;
|
| 93 |
+
|
| 94 |
+
// 1. Direct slug endpoint fetch with full relations
|
| 95 |
+
const data = await _get(`${AT}/anime/${encodeURIComponent(param)}?include=animethemes.animethemeentries.videos.audio,animethemes.song.artists,images`).catch(() => null);
|
| 96 |
+
if (data?.anime) rawAnime = data.anime;
|
| 97 |
+
else if (data?.data) rawAnime = Array.isArray(data.data) ? data.data[0] : data.data;
|
| 98 |
+
|
| 99 |
+
// 2. Filter search if direct fetch failed
|
| 100 |
+
if (!rawAnime) {
|
| 101 |
+
const filterData = await _get(`${AT}/anime?filter[slug]=${encodeURIComponent(param)}&include=animethemes.animethemeentries.videos.audio,animethemes.song.artists,images`).catch(() => null)
|
| 102 |
+
|| await _get(`${AT}/anime?filter[anilist_id]=${encodeURIComponent(param)}&include=animethemes.animethemeentries.videos.audio,animethemes.song.artists,images`).catch(() => null);
|
| 103 |
+
const list = filterData?.anime || filterData?.data || [];
|
| 104 |
+
if (list.length) rawAnime = list[0];
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
return { anime: rawAnime ? normalizeAnime(rawAnime) : null };
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// /themes/artist/<slug>
|
| 111 |
+
if (path.startsWith('/themes/artist/')) {
|
| 112 |
+
const slug = decodeURIComponent(path.replace('/themes/artist/', '').split('?')[0]);
|
| 113 |
+
const data = await _get(`${AT}/artist/${encodeURIComponent(slug)}?include=animethemes.anime.images,animethemes.song`).catch(() => null);
|
| 114 |
+
const raw = data?.artist || (Array.isArray(data?.data) ? data.data[0] : data?.data);
|
| 115 |
+
return { artist: raw ? normalizeArtist(raw) : null };
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// /themes/artists or /themes/top-artists
|
| 119 |
+
if (path.startsWith('/themes/artists') || path.startsWith('/themes/top-artists')) {
|
| 120 |
+
const data = await _get(`${AT}/artist?page[size]=24&include=images`);
|
| 121 |
+
const list = data?.artist || data?.data || [];
|
| 122 |
+
return { artists: list.map(normalizeArtist) };
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// /themes/recent
|
| 126 |
+
if (path.startsWith('/themes/recent')) {
|
| 127 |
+
const data = await _get(`${AT}/anime?sort=-created_at&page[size]=24&include=animethemes.animethemeentries.videos.audio,animethemes.song.artists,images`);
|
| 128 |
+
const list = data?.anime || data?.data || [];
|
| 129 |
+
return { anime: list.map(normalizeAnime) };
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
// /themes/year/<year>
|
| 133 |
+
if (path.startsWith('/themes/year/')) {
|
| 134 |
+
const year = decodeURIComponent(path.replace('/themes/year/', '').split('?')[0]);
|
| 135 |
+
const data = await _get(`${AT}/anime?filter[year]=${year}&sort=name&page[size]=24&include=animethemes.animethemeentries.videos.audio,animethemes.song.artists,images`);
|
| 136 |
+
const list = data?.anime || data?.data || [];
|
| 137 |
+
return { anime: list.map(normalizeAnime) };
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
// Default home discover feed
|
| 141 |
+
const defaultData = await _get(`${AT}/anime?page[size]=24&include=animethemes.animethemeentries.videos.audio,animethemes.song.artists,images`);
|
| 142 |
+
const defaultList = defaultData?.anime || defaultData?.data || [];
|
| 143 |
+
return { anime: defaultList.map(normalizeAnime) };
|
| 144 |
+
} catch (e) {
|
| 145 |
+
console.error('[music-api] fetch error:', e);
|
| 146 |
+
return { anime: null, artists: [] };
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
window.fetchAPI = fetchAPI;
|
| 151 |
+
})();
|
frontend/dist/music.css
ADDED
|
@@ -0,0 +1,2197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
:root {
|
| 3 |
+
--ms-bg: #0a0a0f;
|
| 4 |
+
--ms-sidebar: #000000;
|
| 5 |
+
--ms-surface: #121218;
|
| 6 |
+
--ms-surface-2: #1a1a22;
|
| 7 |
+
--ms-surface-3: #232330;
|
| 8 |
+
--ms-text: #ffffff;
|
| 9 |
+
--ms-text-dim: #a0a0b8;
|
| 10 |
+
--ms-text-mute: #6a6a82;
|
| 11 |
+
--ms-divider: rgba(255, 255, 255, 0.07);
|
| 12 |
+
--ms-accent: #7c3aed;
|
| 13 |
+
--ms-accent-hot: #9d6ef8;
|
| 14 |
+
--ms-accent-soft: rgba(124, 58, 237, 0.18);
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
.music-body {
|
| 18 |
+
overflow: hidden;
|
| 19 |
+
background:
|
| 20 |
+
radial-gradient(circle at top, rgba(124,58,237,0.22), transparent 22%),
|
| 21 |
+
linear-gradient(180deg, #07070d 0%, #090910 100%);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.music-body .float-nav { display: none !important; }
|
| 25 |
+
|
| 26 |
+
.music-body #mobile-nav { display: none !important; }
|
| 27 |
+
|
| 28 |
+
.music-body #header {
|
| 29 |
+
background: rgba(0, 0, 0, 0.92);
|
| 30 |
+
border-bottom: 1px solid var(--ms-divider);
|
| 31 |
+
backdrop-filter: blur(20px);
|
| 32 |
+
-webkit-backdrop-filter: blur(20px);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.music-app {
|
| 36 |
+
position: fixed;
|
| 37 |
+
top: var(--header-h);
|
| 38 |
+
left: 0;
|
| 39 |
+
right: 0;
|
| 40 |
+
bottom: 92px;
|
| 41 |
+
display: grid;
|
| 42 |
+
grid-template-columns: 280px 1fr;
|
| 43 |
+
gap: 8px;
|
| 44 |
+
padding: 8px;
|
| 45 |
+
background: #000;
|
| 46 |
+
transition: top 0.32s cubic-bezier(0.22, 1, 0.36, 1),
|
| 47 |
+
bottom 0.28s cubic-bezier(0.22, 1, 0.36, 1),
|
| 48 |
+
grid-template-columns 0.32s cubic-bezier(0.22, 1, 0.36, 1);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.music-body #header {
|
| 52 |
+
transition: transform 0.32s cubic-bezier(0.22, 1, 0.36, 1);
|
| 53 |
+
will-change: transform;
|
| 54 |
+
}
|
| 55 |
+
.music-body #header.is-hidden {
|
| 56 |
+
transform: translateY(-100%);
|
| 57 |
+
}
|
| 58 |
+
.music-body.header-hidden .music-app {
|
| 59 |
+
top: 0;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
@media (max-width: 1024px) {
|
| 63 |
+
.music-body .music-sidebar {
|
| 64 |
+
transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1),
|
| 65 |
+
top 0.32s cubic-bezier(0.22, 1, 0.36, 1);
|
| 66 |
+
}
|
| 67 |
+
.music-body.header-hidden .music-sidebar { top: 0; }
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
@media (min-width: 1025px) {
|
| 71 |
+
.music-app.sb-collapsed {
|
| 72 |
+
grid-template-columns: 0 1fr;
|
| 73 |
+
gap: 0;
|
| 74 |
+
}
|
| 75 |
+
.music-app.sb-collapsed .music-sidebar {
|
| 76 |
+
transform: translateX(-110%);
|
| 77 |
+
opacity: 0;
|
| 78 |
+
pointer-events: none;
|
| 79 |
+
}
|
| 80 |
+
.music-sidebar {
|
| 81 |
+
transition: transform 0.32s cubic-bezier(0.22, 1, 0.36, 1),
|
| 82 |
+
opacity 0.22s ease;
|
| 83 |
+
will-change: transform;
|
| 84 |
+
}
|
| 85 |
+
.ms-sidebar-toggle { display: flex; }
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.music-body.pb-hidden .music-app { bottom: 0; }
|
| 89 |
+
.music-body.pb-hidden .player-bar { display: none; }
|
| 90 |
+
|
| 91 |
+
.music-sidebar {
|
| 92 |
+
background: var(--ms-sidebar);
|
| 93 |
+
border-radius: 10px;
|
| 94 |
+
padding: 16px 12px;
|
| 95 |
+
overflow-y: auto;
|
| 96 |
+
overflow-x: hidden;
|
| 97 |
+
scrollbar-width: thin;
|
| 98 |
+
scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.music-sidebar::-webkit-scrollbar { width: 8px; }
|
| 102 |
+
.music-sidebar::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.16); border-radius: 4px; }
|
| 103 |
+
|
| 104 |
+
.ms-block { margin-bottom: 22px; }
|
| 105 |
+
|
| 106 |
+
.ms-block-title {
|
| 107 |
+
display: flex;
|
| 108 |
+
align-items: center;
|
| 109 |
+
gap: 10px;
|
| 110 |
+
color: var(--ms-text-dim);
|
| 111 |
+
font-size: 0.75rem;
|
| 112 |
+
font-weight: 700;
|
| 113 |
+
text-transform: uppercase;
|
| 114 |
+
letter-spacing: 0.08em;
|
| 115 |
+
padding: 6px 10px 10px;
|
| 116 |
+
}
|
| 117 |
+
.ms-block-title svg { width: 16px; height: 16px; }
|
| 118 |
+
|
| 119 |
+
.ms-link {
|
| 120 |
+
display: flex;
|
| 121 |
+
align-items: center;
|
| 122 |
+
gap: 14px;
|
| 123 |
+
padding: 10px 12px;
|
| 124 |
+
border-radius: 6px;
|
| 125 |
+
color: var(--ms-text-dim);
|
| 126 |
+
font-size: 0.92rem;
|
| 127 |
+
font-weight: 600;
|
| 128 |
+
transition: color 0.15s ease, background 0.15s ease;
|
| 129 |
+
cursor: pointer;
|
| 130 |
+
}
|
| 131 |
+
.ms-link svg { width: 20px; height: 20px; flex-shrink: 0; }
|
| 132 |
+
.ms-link:hover { color: #fff; background: transparent; }
|
| 133 |
+
.ms-link.active { color: #fff; background: var(--ms-surface-2); }
|
| 134 |
+
.ms-link.active svg { color: var(--ms-accent-hot); }
|
| 135 |
+
|
| 136 |
+
.ms-year-grid {
|
| 137 |
+
display: grid;
|
| 138 |
+
grid-template-columns: repeat(2, 1fr);
|
| 139 |
+
gap: 6px;
|
| 140 |
+
padding: 4px 6px;
|
| 141 |
+
}
|
| 142 |
+
.ms-year-grid a {
|
| 143 |
+
background: var(--ms-surface-2);
|
| 144 |
+
border-radius: 6px;
|
| 145 |
+
padding: 9px 10px;
|
| 146 |
+
text-align: center;
|
| 147 |
+
font-size: 0.85rem;
|
| 148 |
+
font-weight: 600;
|
| 149 |
+
color: var(--ms-text-dim);
|
| 150 |
+
transition: color 0.15s, background 0.15s;
|
| 151 |
+
}
|
| 152 |
+
.ms-year-grid a:hover {
|
| 153 |
+
background: var(--ms-surface-3);
|
| 154 |
+
color: #fff;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
.music-main {
|
| 158 |
+
background: linear-gradient(180deg, #1a1228 0, #0e0e14 320px, var(--ms-surface) 100%);
|
| 159 |
+
border-radius: 10px;
|
| 160 |
+
overflow-y: auto;
|
| 161 |
+
overflow-x: hidden;
|
| 162 |
+
position: relative;
|
| 163 |
+
scrollbar-width: thin;
|
| 164 |
+
scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
|
| 165 |
+
}
|
| 166 |
+
.music-main::-webkit-scrollbar { width: 12px; }
|
| 167 |
+
.music-main::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.16); border-radius: 6px; }
|
| 168 |
+
.music-main::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.28); }
|
| 169 |
+
|
| 170 |
+
.ms-topbar {
|
| 171 |
+
position: sticky;
|
| 172 |
+
top: 0;
|
| 173 |
+
z-index: 5;
|
| 174 |
+
display: flex;
|
| 175 |
+
align-items: center;
|
| 176 |
+
gap: 14px;
|
| 177 |
+
padding: 14px 24px;
|
| 178 |
+
background: rgba(10, 10, 16, 0.65);
|
| 179 |
+
backdrop-filter: blur(18px);
|
| 180 |
+
-webkit-backdrop-filter: blur(18px);
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.ms-nav-btn {
|
| 184 |
+
width: 32px; height: 32px;
|
| 185 |
+
border-radius: 50%;
|
| 186 |
+
background: rgba(0, 0, 0, 0.7);
|
| 187 |
+
color: #fff;
|
| 188 |
+
display: flex; align-items: center; justify-content: center;
|
| 189 |
+
transition: background 0.15s, transform 0.15s;
|
| 190 |
+
flex-shrink: 0;
|
| 191 |
+
}
|
| 192 |
+
.ms-nav-btn svg { width: 14px; height: 14px; }
|
| 193 |
+
.ms-nav-btn:hover:not([disabled]) { background: #000; transform: scale(1.06); }
|
| 194 |
+
.ms-nav-btn[disabled] { opacity: 0.45; cursor: not-allowed; }
|
| 195 |
+
|
| 196 |
+
.ms-search {
|
| 197 |
+
position: relative;
|
| 198 |
+
flex: 1;
|
| 199 |
+
max-width: 360px;
|
| 200 |
+
display: flex;
|
| 201 |
+
align-items: center;
|
| 202 |
+
}
|
| 203 |
+
.ms-search-icon {
|
| 204 |
+
position: absolute;
|
| 205 |
+
left: 14px;
|
| 206 |
+
top: 50%;
|
| 207 |
+
transform: translateY(-50%);
|
| 208 |
+
color: #1a1a22;
|
| 209 |
+
display: flex;
|
| 210 |
+
pointer-events: none;
|
| 211 |
+
}
|
| 212 |
+
.ms-search-icon svg { width: 18px; height: 18px; }
|
| 213 |
+
|
| 214 |
+
.ms-search input {
|
| 215 |
+
width: 100%;
|
| 216 |
+
padding: 11px 40px 11px 42px;
|
| 217 |
+
background: #fff;
|
| 218 |
+
border: 2px solid transparent;
|
| 219 |
+
border-radius: 999px;
|
| 220 |
+
color: #1a1a22;
|
| 221 |
+
font-family: 'Outfit', sans-serif;
|
| 222 |
+
font-size: 0.92rem;
|
| 223 |
+
font-weight: 500;
|
| 224 |
+
outline: none;
|
| 225 |
+
transition: box-shadow 0.15s;
|
| 226 |
+
}
|
| 227 |
+
.ms-search input::placeholder { color: #6a6a82; font-weight: 500; }
|
| 228 |
+
.ms-search input:focus {
|
| 229 |
+
box-shadow: 0 0 0 2px var(--ms-accent);
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.ms-search-clear {
|
| 233 |
+
position: absolute;
|
| 234 |
+
right: 8px;
|
| 235 |
+
top: 50%;
|
| 236 |
+
transform: translateY(-50%);
|
| 237 |
+
width: 26px; height: 26px;
|
| 238 |
+
border-radius: 50%;
|
| 239 |
+
color: #1a1a22;
|
| 240 |
+
display: flex; align-items: center; justify-content: center;
|
| 241 |
+
transition: background 0.15s;
|
| 242 |
+
}
|
| 243 |
+
.ms-search-clear svg { width: 14px; height: 14px; }
|
| 244 |
+
.ms-search-clear:hover { background: rgba(0, 0, 0, 0.08); }
|
| 245 |
+
|
| 246 |
+
.ms-sidebar-toggle {
|
| 247 |
+
display: flex;
|
| 248 |
+
width: 38px; height: 38px;
|
| 249 |
+
border-radius: 10px;
|
| 250 |
+
color: #fff;
|
| 251 |
+
background: rgba(255, 255, 255, 0.06);
|
| 252 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 253 |
+
align-items: center; justify-content: center;
|
| 254 |
+
margin: 0;
|
| 255 |
+
|
| 256 |
+
order: -1;
|
| 257 |
+
flex-shrink: 0;
|
| 258 |
+
transition: background 0.16s ease, transform 0.16s ease, border-color 0.16s ease;
|
| 259 |
+
cursor: pointer;
|
| 260 |
+
}
|
| 261 |
+
.ms-sidebar-toggle:hover {
|
| 262 |
+
background: rgba(167, 139, 250, 0.18);
|
| 263 |
+
border-color: rgba(167, 139, 250, 0.35);
|
| 264 |
+
transform: scale(1.04);
|
| 265 |
+
}
|
| 266 |
+
.ms-sidebar-toggle:active { transform: scale(0.94); }
|
| 267 |
+
.ms-sidebar-toggle svg { width: 20px; height: 20px; }
|
| 268 |
+
|
| 269 |
+
.music-app.sb-collapsed .ms-sidebar-toggle svg { transform: rotate(180deg); transition: transform 0.22s ease; }
|
| 270 |
+
|
| 271 |
+
.ms-view {
|
| 272 |
+
padding: 0 24px 40px;
|
| 273 |
+
min-height: calc(100% - 70px);
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
.ms-section-title {
|
| 277 |
+
display: flex;
|
| 278 |
+
align-items: baseline;
|
| 279 |
+
justify-content: space-between;
|
| 280 |
+
gap: 14px;
|
| 281 |
+
margin: 30px 0 16px;
|
| 282 |
+
}
|
| 283 |
+
.ms-section-title h2 {
|
| 284 |
+
font-size: 1.5rem;
|
| 285 |
+
font-weight: 800;
|
| 286 |
+
letter-spacing: -0.01em;
|
| 287 |
+
color: #fff;
|
| 288 |
+
}
|
| 289 |
+
.ms-section-title a {
|
| 290 |
+
color: var(--ms-text-dim);
|
| 291 |
+
font-size: 0.78rem;
|
| 292 |
+
font-weight: 700;
|
| 293 |
+
text-transform: uppercase;
|
| 294 |
+
letter-spacing: 0.08em;
|
| 295 |
+
transition: color 0.15s;
|
| 296 |
+
}
|
| 297 |
+
.ms-section-title a:hover { color: #fff; text-decoration: underline; }
|
| 298 |
+
|
| 299 |
+
.ms-card-grid {
|
| 300 |
+
display: grid;
|
| 301 |
+
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
| 302 |
+
gap: 24px;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
.ms-card {
|
| 306 |
+
background: var(--ms-surface);
|
| 307 |
+
border-radius: 8px;
|
| 308 |
+
padding: 16px;
|
| 309 |
+
transition: background 0.2s ease;
|
| 310 |
+
cursor: pointer;
|
| 311 |
+
position: relative;
|
| 312 |
+
text-align: left;
|
| 313 |
+
isolation: isolate;
|
| 314 |
+
}
|
| 315 |
+
.ms-card:hover { background: var(--ms-surface-2); }
|
| 316 |
+
.ms-card:hover .ms-card-play {
|
| 317 |
+
opacity: 1;
|
| 318 |
+
transform: translateY(0);
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.ms-card-cover-wrap {
|
| 322 |
+
position: relative;
|
| 323 |
+
width: 100%;
|
| 324 |
+
aspect-ratio: 1;
|
| 325 |
+
border-radius: 6px;
|
| 326 |
+
overflow: hidden;
|
| 327 |
+
background: var(--ms-surface-2);
|
| 328 |
+
margin-bottom: 14px;
|
| 329 |
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
| 330 |
+
|
| 331 |
+
contain: layout paint;
|
| 332 |
+
flex-shrink: 0;
|
| 333 |
+
}
|
| 334 |
+
.ms-card.is-artist .ms-card-cover-wrap {
|
| 335 |
+
border-radius: 50%;
|
| 336 |
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.55);
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
.ms-card-cover {
|
| 340 |
+
width: 100%;
|
| 341 |
+
height: 100%;
|
| 342 |
+
object-fit: cover;
|
| 343 |
+
transition: transform 0.4s ease;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
.ms-card-cover-fallback {
|
| 347 |
+
width: 100%;
|
| 348 |
+
height: 100%;
|
| 349 |
+
display: flex;
|
| 350 |
+
align-items: center;
|
| 351 |
+
justify-content: center;
|
| 352 |
+
background: linear-gradient(135deg, var(--ms-accent), #2a1656);
|
| 353 |
+
color: #fff;
|
| 354 |
+
}
|
| 355 |
+
.ms-card-cover-fallback svg { width: 48px; height: 48px; opacity: 0.7; }
|
| 356 |
+
|
| 357 |
+
.ms-card-play {
|
| 358 |
+
position: absolute;
|
| 359 |
+
right: 8px;
|
| 360 |
+
bottom: 8px;
|
| 361 |
+
width: 48px; height: 48px;
|
| 362 |
+
border-radius: 50%;
|
| 363 |
+
background: var(--ms-accent);
|
| 364 |
+
color: #fff;
|
| 365 |
+
display: flex;
|
| 366 |
+
align-items: center;
|
| 367 |
+
justify-content: center;
|
| 368 |
+
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.6);
|
| 369 |
+
opacity: 0;
|
| 370 |
+
transform: translateY(8px);
|
| 371 |
+
transition: opacity 0.18s ease, transform 0.18s ease, background 0.18s ease, scale 0.12s;
|
| 372 |
+
z-index: 2;
|
| 373 |
+
}
|
| 374 |
+
.ms-card-play svg { width: 22px; height: 22px; transform: translateX(1px); }
|
| 375 |
+
.ms-card-play:hover { background: var(--ms-accent-hot); transform: translateY(0) scale(1.06); }
|
| 376 |
+
.ms-card-play:active { transform: translateY(0) scale(0.96); }
|
| 377 |
+
|
| 378 |
+
.ms-card-title {
|
| 379 |
+
font-size: 1rem;
|
| 380 |
+
font-weight: 700;
|
| 381 |
+
color: #fff;
|
| 382 |
+
margin-bottom: 6px;
|
| 383 |
+
overflow: hidden;
|
| 384 |
+
text-overflow: ellipsis;
|
| 385 |
+
display: -webkit-box;
|
| 386 |
+
-webkit-line-clamp: 1;
|
| 387 |
+
-webkit-box-orient: vertical;
|
| 388 |
+
line-clamp: 1;
|
| 389 |
+
}
|
| 390 |
+
.ms-card-sub {
|
| 391 |
+
font-size: 0.85rem;
|
| 392 |
+
color: var(--ms-text-dim);
|
| 393 |
+
overflow: hidden;
|
| 394 |
+
text-overflow: ellipsis;
|
| 395 |
+
display: -webkit-box;
|
| 396 |
+
-webkit-line-clamp: 2;
|
| 397 |
+
-webkit-box-orient: vertical;
|
| 398 |
+
line-clamp: 2;
|
| 399 |
+
line-height: 1.4;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.ms-empty {
|
| 403 |
+
padding: 60px 20px;
|
| 404 |
+
text-align: center;
|
| 405 |
+
color: var(--ms-text-dim);
|
| 406 |
+
}
|
| 407 |
+
.ms-empty svg { width: 56px; height: 56px; margin-bottom: 14px; opacity: 0.4; }
|
| 408 |
+
.ms-empty h3 { font-size: 1.4rem; color: #fff; margin-bottom: 8px; font-weight: 800; }
|
| 409 |
+
.ms-empty p { font-size: 0.95rem; }
|
| 410 |
+
|
| 411 |
+
.ms-loading {
|
| 412 |
+
padding: 80px 20px;
|
| 413 |
+
text-align: center;
|
| 414 |
+
color: var(--ms-text-dim);
|
| 415 |
+
}
|
| 416 |
+
.ms-spinner {
|
| 417 |
+
width: 36px; height: 36px;
|
| 418 |
+
border: 3px solid rgba(124, 58, 237, 0.18);
|
| 419 |
+
border-top-color: var(--ms-accent);
|
| 420 |
+
border-radius: 50%;
|
| 421 |
+
margin: 0 auto 16px;
|
| 422 |
+
animation: msSpin 0.8s linear infinite;
|
| 423 |
+
}
|
| 424 |
+
@keyframes msSpin { to { transform: rotate(360deg); } }
|
| 425 |
+
|
| 426 |
+
.ms-album-hero {
|
| 427 |
+
display: flex;
|
| 428 |
+
gap: 28px;
|
| 429 |
+
align-items: flex-end;
|
| 430 |
+
padding: 36px 0 24px;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.ms-album-cover {
|
| 434 |
+
width: 232px; height: 232px;
|
| 435 |
+
border-radius: 6px;
|
| 436 |
+
overflow: hidden;
|
| 437 |
+
flex-shrink: 0;
|
| 438 |
+
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.85);
|
| 439 |
+
background: linear-gradient(135deg, var(--ms-accent), #2a1656);
|
| 440 |
+
}
|
| 441 |
+
.ms-album-cover img { width: 100%; height: 100%; object-fit: cover; }
|
| 442 |
+
|
| 443 |
+
.ms-album-info { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
|
| 444 |
+
.ms-album-eyebrow {
|
| 445 |
+
font-size: 0.78rem;
|
| 446 |
+
font-weight: 700;
|
| 447 |
+
text-transform: uppercase;
|
| 448 |
+
letter-spacing: 0.08em;
|
| 449 |
+
color: #fff;
|
| 450 |
+
}
|
| 451 |
+
.ms-album-title {
|
| 452 |
+
font-size: clamp(2rem, 6vw, 4rem);
|
| 453 |
+
font-weight: 900;
|
| 454 |
+
color: #fff;
|
| 455 |
+
line-height: 1.05;
|
| 456 |
+
letter-spacing: -0.03em;
|
| 457 |
+
margin: 4px 0 8px;
|
| 458 |
+
word-break: break-word;
|
| 459 |
+
}
|
| 460 |
+
.ms-album-meta { color: var(--ms-text-dim); font-size: 0.9rem; }
|
| 461 |
+
.ms-album-meta strong { color: #fff; font-weight: 700; }
|
| 462 |
+
|
| 463 |
+
.ms-album-actions {
|
| 464 |
+
display: flex;
|
| 465 |
+
gap: 16px;
|
| 466 |
+
align-items: center;
|
| 467 |
+
padding: 24px 0 10px;
|
| 468 |
+
}
|
| 469 |
+
.ms-play-big {
|
| 470 |
+
width: 60px; height: 60px;
|
| 471 |
+
border-radius: 50%;
|
| 472 |
+
background: linear-gradient(135deg, var(--ms-accent) 0%, var(--ms-accent-hot) 100%);
|
| 473 |
+
color: #fff;
|
| 474 |
+
display: flex; align-items: center; justify-content: center;
|
| 475 |
+
transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1),
|
| 476 |
+
box-shadow 0.18s ease,
|
| 477 |
+
filter 0.18s ease;
|
| 478 |
+
box-shadow: 0 10px 28px rgba(167, 139, 250, 0.45),
|
| 479 |
+
0 4px 12px rgba(0, 0, 0, 0.5),
|
| 480 |
+
inset 0 1px 0 rgba(255, 255, 255, 0.25);
|
| 481 |
+
position: relative;
|
| 482 |
+
overflow: hidden;
|
| 483 |
+
}
|
| 484 |
+
.ms-play-big::before {
|
| 485 |
+
content: "";
|
| 486 |
+
position: absolute;
|
| 487 |
+
inset: 0;
|
| 488 |
+
border-radius: inherit;
|
| 489 |
+
background: radial-gradient(circle at 30% 25%, rgba(255, 255, 255, 0.35), transparent 55%);
|
| 490 |
+
pointer-events: none;
|
| 491 |
+
}
|
| 492 |
+
.ms-play-big svg { width: 28px; height: 28px; transform: translateX(2px); position: relative; z-index: 1; }
|
| 493 |
+
.ms-play-big:hover {
|
| 494 |
+
transform: scale(1.08) translateY(-1px);
|
| 495 |
+
filter: brightness(1.08);
|
| 496 |
+
box-shadow: 0 14px 36px rgba(167, 139, 250, 0.6),
|
| 497 |
+
0 6px 14px rgba(0, 0, 0, 0.55),
|
| 498 |
+
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
| 499 |
+
}
|
| 500 |
+
.ms-play-big:active { transform: scale(0.95); transition-duration: 0.08s; }
|
| 501 |
+
.ms-play-big:focus-visible {
|
| 502 |
+
outline: none;
|
| 503 |
+
box-shadow: 0 0 0 3px rgba(167, 139, 250, 0.5),
|
| 504 |
+
0 10px 28px rgba(167, 139, 250, 0.45),
|
| 505 |
+
0 4px 12px rgba(0, 0, 0, 0.5);
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
.ms-icon-btn {
|
| 509 |
+
width: 38px; height: 38px;
|
| 510 |
+
border-radius: 50%;
|
| 511 |
+
color: var(--ms-text-dim);
|
| 512 |
+
display: flex; align-items: center; justify-content: center;
|
| 513 |
+
transition: color 0.12s, transform 0.12s;
|
| 514 |
+
}
|
| 515 |
+
.ms-icon-btn svg { width: 24px; height: 24px; }
|
| 516 |
+
.ms-icon-btn:hover { color: #fff; transform: scale(1.05); }
|
| 517 |
+
.ms-icon-btn.is-on { color: var(--ms-accent-hot); }
|
| 518 |
+
.ms-icon-btn.is-on svg { fill: currentColor; }
|
| 519 |
+
|
| 520 |
+
.ms-tracks {
|
| 521 |
+
margin-top: 8px;
|
| 522 |
+
padding-bottom: 16px;
|
| 523 |
+
}
|
| 524 |
+
.ms-tracks-head {
|
| 525 |
+
display: grid;
|
| 526 |
+
grid-template-columns: 36px 1fr 0.8fr 100px 60px;
|
| 527 |
+
gap: 14px;
|
| 528 |
+
padding: 8px 16px;
|
| 529 |
+
border-bottom: 1px solid var(--ms-divider);
|
| 530 |
+
margin-bottom: 8px;
|
| 531 |
+
color: var(--ms-text-dim);
|
| 532 |
+
font-size: 0.74rem;
|
| 533 |
+
text-transform: uppercase;
|
| 534 |
+
letter-spacing: 0.1em;
|
| 535 |
+
font-weight: 600;
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
.ms-track {
|
| 539 |
+
display: grid;
|
| 540 |
+
grid-template-columns: 36px 1fr 0.8fr 100px 60px;
|
| 541 |
+
gap: 14px;
|
| 542 |
+
align-items: center;
|
| 543 |
+
padding: 8px 16px;
|
| 544 |
+
border-radius: 4px;
|
| 545 |
+
cursor: pointer;
|
| 546 |
+
transition: background 0.12s ease;
|
| 547 |
+
color: var(--ms-text-dim);
|
| 548 |
+
position: relative;
|
| 549 |
+
}
|
| 550 |
+
.ms-track:hover { background: rgba(255, 255, 255, 0.07); color: #fff; }
|
| 551 |
+
.ms-track:hover .ms-track-num .ms-num { display: none; }
|
| 552 |
+
.ms-track:hover .ms-track-num .ms-play-mini { display: flex; }
|
| 553 |
+
.ms-track.is-playing { color: var(--ms-accent-hot); }
|
| 554 |
+
.ms-track.is-playing .ms-track-title { color: var(--ms-accent-hot); }
|
| 555 |
+
|
| 556 |
+
.ms-track-num {
|
| 557 |
+
text-align: center;
|
| 558 |
+
font-size: 0.95rem;
|
| 559 |
+
color: var(--ms-text-dim);
|
| 560 |
+
width: 36px; height: 36px;
|
| 561 |
+
display: flex; align-items: center; justify-content: center;
|
| 562 |
+
}
|
| 563 |
+
.ms-track-num .ms-play-mini { display: none; color: #fff; }
|
| 564 |
+
.ms-track-num .ms-play-mini svg { width: 16px; height: 16px; fill: currentColor; }
|
| 565 |
+
.ms-track.is-playing .ms-num { display: none; }
|
| 566 |
+
.ms-track.is-playing .ms-now-bars { display: flex; }
|
| 567 |
+
.ms-now-bars { display: none; gap: 2px; align-items: flex-end; height: 14px; }
|
| 568 |
+
.ms-now-bars span {
|
| 569 |
+
display: block; width: 3px; background: var(--ms-accent-hot); border-radius: 1px;
|
| 570 |
+
animation: msBar 0.8s ease-in-out infinite alternate;
|
| 571 |
+
}
|
| 572 |
+
.ms-now-bars span:nth-child(1) { animation-delay: -0.5s; }
|
| 573 |
+
.ms-now-bars span:nth-child(2) { animation-delay: -0.2s; }
|
| 574 |
+
.ms-now-bars span:nth-child(3) { animation-delay: -0.8s; }
|
| 575 |
+
@keyframes msBar { from { height: 4px; } to { height: 14px; } }
|
| 576 |
+
|
| 577 |
+
.ms-track-meta { display: flex; gap: 12px; align-items: center; min-width: 0; }
|
| 578 |
+
.ms-track-cover {
|
| 579 |
+
width: 40px; height: 40px;
|
| 580 |
+
border-radius: 4px;
|
| 581 |
+
object-fit: cover;
|
| 582 |
+
background: var(--ms-surface-2);
|
| 583 |
+
flex-shrink: 0;
|
| 584 |
+
}
|
| 585 |
+
.ms-track-text { min-width: 0; }
|
| 586 |
+
.ms-track-title {
|
| 587 |
+
color: #fff;
|
| 588 |
+
font-size: 0.95rem;
|
| 589 |
+
font-weight: 500;
|
| 590 |
+
white-space: nowrap;
|
| 591 |
+
overflow: hidden;
|
| 592 |
+
text-overflow: ellipsis;
|
| 593 |
+
}
|
| 594 |
+
.ms-track-sub {
|
| 595 |
+
color: var(--ms-text-dim);
|
| 596 |
+
font-size: 0.82rem;
|
| 597 |
+
white-space: nowrap;
|
| 598 |
+
overflow: hidden;
|
| 599 |
+
text-overflow: ellipsis;
|
| 600 |
+
}
|
| 601 |
+
.ms-track-sub a { transition: color 0.12s; }
|
| 602 |
+
.ms-track-sub a:hover { color: #fff; text-decoration: underline; }
|
| 603 |
+
|
| 604 |
+
.ms-track-anime {
|
| 605 |
+
color: var(--ms-text-dim);
|
| 606 |
+
font-size: 0.85rem;
|
| 607 |
+
white-space: nowrap;
|
| 608 |
+
overflow: hidden;
|
| 609 |
+
text-overflow: ellipsis;
|
| 610 |
+
}
|
| 611 |
+
.ms-track-anime:hover { color: #fff; text-decoration: underline; }
|
| 612 |
+
|
| 613 |
+
.ms-track-tag {
|
| 614 |
+
display: inline-flex;
|
| 615 |
+
align-items: center;
|
| 616 |
+
padding: 3px 10px;
|
| 617 |
+
border-radius: 999px;
|
| 618 |
+
background: rgba(124, 58, 237, 0.18);
|
| 619 |
+
border: 1px solid rgba(124, 58, 237, 0.32);
|
| 620 |
+
color: #c4b5fd;
|
| 621 |
+
font-size: 0.7rem;
|
| 622 |
+
font-weight: 700;
|
| 623 |
+
letter-spacing: 0.04em;
|
| 624 |
+
width: fit-content;
|
| 625 |
+
}
|
| 626 |
+
.ms-track-tag.ed { background: rgba(190, 24, 93, 0.16); border-color: rgba(190, 24, 93, 0.4); color: #f9a8d4; }
|
| 627 |
+
.ms-track-tag.in { background: rgba(34, 197, 94, 0.13); border-color: rgba(34, 197, 94, 0.35); color: #86efac; }
|
| 628 |
+
|
| 629 |
+
.ms-track-like {
|
| 630 |
+
width: 28px; height: 28px;
|
| 631 |
+
color: var(--ms-text-dim);
|
| 632 |
+
display: flex; align-items: center; justify-content: center;
|
| 633 |
+
border-radius: 50%;
|
| 634 |
+
transition: color 0.12s, transform 0.12s, opacity 0.12s;
|
| 635 |
+
opacity: 0;
|
| 636 |
+
}
|
| 637 |
+
.ms-track-like svg { width: 16px; height: 16px; }
|
| 638 |
+
.ms-track:hover .ms-track-like { opacity: 1; }
|
| 639 |
+
.ms-track-like.is-liked { color: var(--ms-accent-hot); opacity: 1; }
|
| 640 |
+
.ms-track-like.is-liked svg { fill: currentColor; }
|
| 641 |
+
.ms-track-like:hover { color: #fff; transform: scale(1.1); }
|
| 642 |
+
|
| 643 |
+
.ms-search-tabs {
|
| 644 |
+
display: flex;
|
| 645 |
+
gap: 8px;
|
| 646 |
+
padding: 18px 0 6px;
|
| 647 |
+
flex-wrap: wrap;
|
| 648 |
+
}
|
| 649 |
+
.ms-tab {
|
| 650 |
+
padding: 8px 18px;
|
| 651 |
+
border-radius: 999px;
|
| 652 |
+
background: var(--ms-surface-2);
|
| 653 |
+
color: #fff;
|
| 654 |
+
font-size: 0.86rem;
|
| 655 |
+
font-weight: 600;
|
| 656 |
+
transition: background 0.15s, color 0.15s;
|
| 657 |
+
cursor: pointer;
|
| 658 |
+
}
|
| 659 |
+
.ms-tab:hover { background: var(--ms-surface-3); }
|
| 660 |
+
.ms-tab.active { background: #fff; color: #000; }
|
| 661 |
+
|
| 662 |
+
.ms-search-block { margin-bottom: 36px; }
|
| 663 |
+
|
| 664 |
+
.ms-top-result {
|
| 665 |
+
display: grid;
|
| 666 |
+
grid-template-columns: 1.05fr 1.6fr;
|
| 667 |
+
gap: 24px;
|
| 668 |
+
align-items: stretch;
|
| 669 |
+
margin: 6px 0 30px;
|
| 670 |
+
}
|
| 671 |
+
@media (max-width: 900px) {
|
| 672 |
+
.ms-top-result { grid-template-columns: 1fr; }
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
.ms-top-card {
|
| 676 |
+
display: grid;
|
| 677 |
+
grid-template-columns: 120px 1fr;
|
| 678 |
+
grid-template-rows: auto auto auto 1fr;
|
| 679 |
+
grid-template-areas:
|
| 680 |
+
"cover name"
|
| 681 |
+
"cover tag"
|
| 682 |
+
"cover ."
|
| 683 |
+
"play .";
|
| 684 |
+
column-gap: 22px;
|
| 685 |
+
row-gap: 8px;
|
| 686 |
+
background: var(--ms-surface);
|
| 687 |
+
border-radius: 10px;
|
| 688 |
+
padding: 22px;
|
| 689 |
+
transition: background 0.2s;
|
| 690 |
+
cursor: pointer;
|
| 691 |
+
position: relative;
|
| 692 |
+
min-height: 220px;
|
| 693 |
+
overflow: hidden;
|
| 694 |
+
}
|
| 695 |
+
.ms-top-card:hover { background: var(--ms-surface-2); }
|
| 696 |
+
.ms-top-card:hover .ms-top-play { opacity: 1; transform: translateY(0); }
|
| 697 |
+
|
| 698 |
+
.ms-top-cover {
|
| 699 |
+
grid-area: cover;
|
| 700 |
+
width: 120px; height: 120px;
|
| 701 |
+
border-radius: 6px;
|
| 702 |
+
object-fit: cover;
|
| 703 |
+
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.6);
|
| 704 |
+
align-self: start;
|
| 705 |
+
}
|
| 706 |
+
.ms-top-card.is-artist .ms-top-cover { border-radius: 50%; }
|
| 707 |
+
.ms-top-card .ms-top-name { grid-area: name; align-self: end; margin: 0; }
|
| 708 |
+
.ms-top-card .ms-top-tag { grid-area: tag; justify-self: start; }
|
| 709 |
+
|
| 710 |
+
.ms-top-play {
|
| 711 |
+
grid-area: play;
|
| 712 |
+
justify-self: end;
|
| 713 |
+
align-self: end;
|
| 714 |
+
width: 56px; height: 56px;
|
| 715 |
+
border-radius: 50%;
|
| 716 |
+
background: var(--ms-accent);
|
| 717 |
+
color: #fff;
|
| 718 |
+
display: flex; align-items: center; justify-content: center;
|
| 719 |
+
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.55);
|
| 720 |
+
opacity: 0;
|
| 721 |
+
transform: translateY(8px);
|
| 722 |
+
transition: opacity 0.18s, transform 0.18s, background 0.18s;
|
| 723 |
+
z-index: 2;
|
| 724 |
+
}
|
| 725 |
+
.ms-top-play:hover { background: var(--ms-accent-hot); transform: translateY(0) scale(1.06); }
|
| 726 |
+
.ms-top-play svg { width: 26px; height: 26px; transform: translateX(1px); }
|
| 727 |
+
|
| 728 |
+
@media (max-width: 720px) {
|
| 729 |
+
.ms-top-card {
|
| 730 |
+
grid-template-columns: 84px 1fr;
|
| 731 |
+
grid-template-areas:
|
| 732 |
+
"cover name"
|
| 733 |
+
"cover tag"
|
| 734 |
+
"play play";
|
| 735 |
+
column-gap: 16px;
|
| 736 |
+
padding: 16px;
|
| 737 |
+
min-height: auto;
|
| 738 |
+
}
|
| 739 |
+
.ms-top-cover { width: 84px; height: 84px; }
|
| 740 |
+
.ms-top-card .ms-top-name { font-size: 1.4rem; }
|
| 741 |
+
.ms-top-play { opacity: 1; transform: none; width: 48px; height: 48px; margin-top: 6px; }
|
| 742 |
+
.ms-top-play svg { width: 22px; height: 22px; }
|
| 743 |
+
}
|
| 744 |
+
.ms-top-name {
|
| 745 |
+
font-size: 2rem;
|
| 746 |
+
font-weight: 900;
|
| 747 |
+
color: #fff;
|
| 748 |
+
margin-bottom: 10px;
|
| 749 |
+
line-height: 1.05;
|
| 750 |
+
letter-spacing: -0.02em;
|
| 751 |
+
overflow: hidden;
|
| 752 |
+
display: -webkit-box;
|
| 753 |
+
-webkit-line-clamp: 2;
|
| 754 |
+
-webkit-box-orient: vertical;
|
| 755 |
+
line-clamp: 2;
|
| 756 |
+
}
|
| 757 |
+
.ms-top-tag {
|
| 758 |
+
display: inline-flex;
|
| 759 |
+
align-items: center;
|
| 760 |
+
padding: 5px 12px;
|
| 761 |
+
background: rgba(0, 0, 0, 0.55);
|
| 762 |
+
border-radius: 4px;
|
| 763 |
+
color: #fff;
|
| 764 |
+
font-size: 0.72rem;
|
| 765 |
+
font-weight: 700;
|
| 766 |
+
text-transform: uppercase;
|
| 767 |
+
letter-spacing: 0.08em;
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
.ms-top-songs {
|
| 771 |
+
background: transparent;
|
| 772 |
+
border-radius: 8px;
|
| 773 |
+
padding: 0;
|
| 774 |
+
}
|
| 775 |
+
.ms-top-songs h3 {
|
| 776 |
+
font-size: 1.5rem;
|
| 777 |
+
font-weight: 800;
|
| 778 |
+
color: #fff;
|
| 779 |
+
margin: 0 0 12px;
|
| 780 |
+
letter-spacing: -0.01em;
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
+
.ms-songlist { display: flex; flex-direction: column; }
|
| 784 |
+
.ms-song-row {
|
| 785 |
+
display: grid;
|
| 786 |
+
grid-template-columns: 40px 1fr 28px;
|
| 787 |
+
gap: 14px;
|
| 788 |
+
align-items: center;
|
| 789 |
+
padding: 8px 12px;
|
| 790 |
+
border-radius: 6px;
|
| 791 |
+
cursor: pointer;
|
| 792 |
+
transition: background 0.12s;
|
| 793 |
+
}
|
| 794 |
+
.ms-song-row:hover { background: var(--ms-surface-2); }
|
| 795 |
+
.ms-song-row.is-playing .ms-song-title { color: var(--ms-accent-hot); }
|
| 796 |
+
|
| 797 |
+
.ms-song-cover {
|
| 798 |
+
width: 40px; height: 40px;
|
| 799 |
+
border-radius: 4px;
|
| 800 |
+
object-fit: cover;
|
| 801 |
+
background: var(--ms-surface-2);
|
| 802 |
+
flex-shrink: 0;
|
| 803 |
+
position: relative;
|
| 804 |
+
}
|
| 805 |
+
.ms-song-cover-wrap {
|
| 806 |
+
position: relative;
|
| 807 |
+
width: 40px; height: 40px;
|
| 808 |
+
flex-shrink: 0;
|
| 809 |
+
}
|
| 810 |
+
.ms-song-cover-wrap .ms-song-play {
|
| 811 |
+
position: absolute;
|
| 812 |
+
inset: 0;
|
| 813 |
+
display: none;
|
| 814 |
+
align-items: center;
|
| 815 |
+
justify-content: center;
|
| 816 |
+
background: rgba(0, 0, 0, 0.6);
|
| 817 |
+
color: #fff;
|
| 818 |
+
border-radius: 4px;
|
| 819 |
+
}
|
| 820 |
+
.ms-song-cover-wrap .ms-song-play svg { width: 16px; height: 16px; }
|
| 821 |
+
.ms-song-row:hover .ms-song-play { display: flex; }
|
| 822 |
+
|
| 823 |
+
.ms-song-text { min-width: 0; }
|
| 824 |
+
.ms-song-title {
|
| 825 |
+
color: #fff;
|
| 826 |
+
font-size: 0.92rem;
|
| 827 |
+
font-weight: 500;
|
| 828 |
+
white-space: nowrap;
|
| 829 |
+
overflow: hidden;
|
| 830 |
+
text-overflow: ellipsis;
|
| 831 |
+
}
|
| 832 |
+
.ms-song-sub {
|
| 833 |
+
color: var(--ms-text-dim);
|
| 834 |
+
font-size: 0.8rem;
|
| 835 |
+
white-space: nowrap;
|
| 836 |
+
overflow: hidden;
|
| 837 |
+
text-overflow: ellipsis;
|
| 838 |
+
}
|
| 839 |
+
.ms-song-sub a:hover { color: #fff; text-decoration: underline; }
|
| 840 |
+
|
| 841 |
+
.ms-song-like {
|
| 842 |
+
width: 28px; height: 28px;
|
| 843 |
+
color: var(--ms-text-dim);
|
| 844 |
+
display: flex; align-items: center; justify-content: center;
|
| 845 |
+
border-radius: 50%;
|
| 846 |
+
transition: color 0.12s, opacity 0.12s, transform 0.12s;
|
| 847 |
+
opacity: 0;
|
| 848 |
+
}
|
| 849 |
+
.ms-song-like svg { width: 16px; height: 16px; }
|
| 850 |
+
.ms-song-row:hover .ms-song-like { opacity: 1; }
|
| 851 |
+
.ms-song-like.is-liked { color: var(--ms-accent-hot); opacity: 1; }
|
| 852 |
+
.ms-song-like.is-liked svg { fill: currentColor; }
|
| 853 |
+
|
| 854 |
+
.player-bar {
|
| 855 |
+
position: fixed;
|
| 856 |
+
left: 0; right: 0; bottom: 0;
|
| 857 |
+
height: 90px;
|
| 858 |
+
background: #000;
|
| 859 |
+
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
| 860 |
+
display: grid;
|
| 861 |
+
grid-template-columns: minmax(180px, 30%) 1fr minmax(180px, 30%);
|
| 862 |
+
align-items: center;
|
| 863 |
+
padding: 0 16px;
|
| 864 |
+
gap: 16px;
|
| 865 |
+
z-index: 100;
|
| 866 |
+
box-shadow: 0 -10px 32px rgba(0, 0, 0, 0.6);
|
| 867 |
+
}
|
| 868 |
+
.player-bar[data-empty="1"] .pb-controls,
|
| 869 |
+
.player-bar[data-empty="1"] .pb-progress,
|
| 870 |
+
.player-bar[data-empty="1"] .pb-like { opacity: 0.5; pointer-events: none; }
|
| 871 |
+
|
| 872 |
+
.pb-track {
|
| 873 |
+
display: flex;
|
| 874 |
+
align-items: center;
|
| 875 |
+
gap: 14px;
|
| 876 |
+
min-width: 0;
|
| 877 |
+
}
|
| 878 |
+
.pb-cover {
|
| 879 |
+
width: 56px; height: 56px;
|
| 880 |
+
border-radius: 4px;
|
| 881 |
+
background: var(--ms-surface-2);
|
| 882 |
+
object-fit: cover;
|
| 883 |
+
flex-shrink: 0;
|
| 884 |
+
}
|
| 885 |
+
|
| 886 |
+
.pb-cover:not([src]),
|
| 887 |
+
.pb-cover[src=""],
|
| 888 |
+
.pb-cover.is-failed {
|
| 889 |
+
visibility: hidden;
|
| 890 |
+
}
|
| 891 |
+
.player-bar[data-empty="1"] .pb-cover {
|
| 892 |
+
visibility: hidden;
|
| 893 |
+
}
|
| 894 |
+
|
| 895 |
+
.pb-cover-fallback {
|
| 896 |
+
position: absolute;
|
| 897 |
+
inset: 0;
|
| 898 |
+
display: none;
|
| 899 |
+
align-items: center;
|
| 900 |
+
justify-content: center;
|
| 901 |
+
background: linear-gradient(135deg, #432470 0%, #1a0f33 100%);
|
| 902 |
+
color: rgba(255, 255, 255, 0.7);
|
| 903 |
+
border-radius: 4px;
|
| 904 |
+
}
|
| 905 |
+
.pb-cover-fallback svg { width: 22px; height: 22px; }
|
| 906 |
+
.pb-cover-btn:not(.has-cover) .pb-cover-fallback,
|
| 907 |
+
.player-bar[data-empty="1"] .pb-cover-fallback { display: flex; }
|
| 908 |
+
|
| 909 |
+
.pb-dismiss {
|
| 910 |
+
width: 28px; height: 28px;
|
| 911 |
+
color: var(--ms-text-mute);
|
| 912 |
+
display: flex; align-items: center; justify-content: center;
|
| 913 |
+
border-radius: 50%;
|
| 914 |
+
transition: color 0.15s, background 0.15s;
|
| 915 |
+
flex-shrink: 0;
|
| 916 |
+
}
|
| 917 |
+
.pb-dismiss svg { width: 14px; height: 14px; }
|
| 918 |
+
.pb-dismiss:hover { color: #fff; background: rgba(255, 255, 255, 0.08); }
|
| 919 |
+
.pb-meta { min-width: 0; flex: 1; }
|
| 920 |
+
.pb-title {
|
| 921 |
+
color: #fff;
|
| 922 |
+
font-size: 0.9rem;
|
| 923 |
+
font-weight: 500;
|
| 924 |
+
white-space: nowrap;
|
| 925 |
+
overflow: hidden;
|
| 926 |
+
text-overflow: ellipsis;
|
| 927 |
+
display: block;
|
| 928 |
+
transition: color 0.12s;
|
| 929 |
+
background: transparent;
|
| 930 |
+
border: 0;
|
| 931 |
+
padding: 0;
|
| 932 |
+
cursor: pointer;
|
| 933 |
+
text-align: left;
|
| 934 |
+
width: 100%;
|
| 935 |
+
font-family: inherit;
|
| 936 |
+
}
|
| 937 |
+
.pb-title:hover { text-decoration: underline; }
|
| 938 |
+
.pb-artist {
|
| 939 |
+
color: var(--ms-text-dim);
|
| 940 |
+
font-size: 0.74rem;
|
| 941 |
+
white-space: nowrap;
|
| 942 |
+
overflow: hidden;
|
| 943 |
+
text-overflow: ellipsis;
|
| 944 |
+
display: block;
|
| 945 |
+
transition: color 0.12s;
|
| 946 |
+
}
|
| 947 |
+
.pb-artist:hover { color: #fff; text-decoration: underline; }
|
| 948 |
+
.pb-like {
|
| 949 |
+
width: 32px; height: 32px;
|
| 950 |
+
color: var(--ms-text-dim);
|
| 951 |
+
display: flex; align-items: center; justify-content: center;
|
| 952 |
+
border-radius: 50%;
|
| 953 |
+
transition: color 0.12s, transform 0.12s;
|
| 954 |
+
flex-shrink: 0;
|
| 955 |
+
}
|
| 956 |
+
.pb-like svg { width: 18px; height: 18px; }
|
| 957 |
+
.pb-like:hover { color: #fff; transform: scale(1.1); }
|
| 958 |
+
.pb-like.is-liked { color: var(--ms-accent-hot); }
|
| 959 |
+
.pb-like.is-liked svg { fill: currentColor; }
|
| 960 |
+
|
| 961 |
+
.pb-center {
|
| 962 |
+
display: flex;
|
| 963 |
+
flex-direction: column;
|
| 964 |
+
align-items: center;
|
| 965 |
+
gap: 6px;
|
| 966 |
+
width: 100%;
|
| 967 |
+
max-width: 720px;
|
| 968 |
+
margin: 0 auto;
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
.pb-controls {
|
| 972 |
+
display: flex;
|
| 973 |
+
align-items: center;
|
| 974 |
+
justify-content: center;
|
| 975 |
+
gap: 18px;
|
| 976 |
+
}
|
| 977 |
+
|
| 978 |
+
.pb-btn {
|
| 979 |
+
width: 32px; height: 32px;
|
| 980 |
+
color: var(--ms-text-dim);
|
| 981 |
+
display: flex; align-items: center; justify-content: center;
|
| 982 |
+
border-radius: 50%;
|
| 983 |
+
transition: color 0.12s, transform 0.12s;
|
| 984 |
+
}
|
| 985 |
+
.pb-btn svg { width: 20px; height: 20px; }
|
| 986 |
+
.pb-btn:hover { color: #fff; transform: scale(1.08); }
|
| 987 |
+
.pb-btn.is-on { color: var(--ms-accent-hot); }
|
| 988 |
+
.pb-btn.is-on::after {
|
| 989 |
+
content: "";
|
| 990 |
+
width: 4px; height: 4px;
|
| 991 |
+
background: var(--ms-accent-hot);
|
| 992 |
+
border-radius: 50%;
|
| 993 |
+
position: absolute;
|
| 994 |
+
margin-top: 30px;
|
| 995 |
+
}
|
| 996 |
+
|
| 997 |
+
.pb-play {
|
| 998 |
+
width: 38px; height: 38px;
|
| 999 |
+
border-radius: 50%;
|
| 1000 |
+
background: #fff;
|
| 1001 |
+
color: #000;
|
| 1002 |
+
display: flex; align-items: center; justify-content: center;
|
| 1003 |
+
transition: transform 0.12s, background 0.15s;
|
| 1004 |
+
}
|
| 1005 |
+
.pb-play svg { width: 22px; height: 22px; }
|
| 1006 |
+
.pb-play:hover { transform: scale(1.07); background: #f1eaff; }
|
| 1007 |
+
.pb-play:active { transform: scale(0.96); }
|
| 1008 |
+
.player-bar[data-empty="1"] .pb-play { opacity: 0.6; }
|
| 1009 |
+
|
| 1010 |
+
.pb-progress {
|
| 1011 |
+
display: flex;
|
| 1012 |
+
align-items: center;
|
| 1013 |
+
gap: 12px;
|
| 1014 |
+
width: 100%;
|
| 1015 |
+
padding: 0 12px;
|
| 1016 |
+
}
|
| 1017 |
+
.pb-time {
|
| 1018 |
+
font-size: 0.7rem;
|
| 1019 |
+
color: var(--ms-text-dim);
|
| 1020 |
+
font-variant-numeric: tabular-nums;
|
| 1021 |
+
width: 38px;
|
| 1022 |
+
text-align: center;
|
| 1023 |
+
flex-shrink: 0;
|
| 1024 |
+
}
|
| 1025 |
+
.pb-bar {
|
| 1026 |
+
flex: 1;
|
| 1027 |
+
height: 4px;
|
| 1028 |
+
background: rgba(255, 255, 255, 0.18);
|
| 1029 |
+
border-radius: 2px;
|
| 1030 |
+
position: relative;
|
| 1031 |
+
cursor: pointer;
|
| 1032 |
+
transition: height 0.12s;
|
| 1033 |
+
}
|
| 1034 |
+
.pb-bar:hover { height: 6px; }
|
| 1035 |
+
.pb-bar:hover .pb-bar-fill { background: var(--ms-accent-hot); }
|
| 1036 |
+
.pb-bar:hover .pb-bar-knob { opacity: 1; }
|
| 1037 |
+
.pb-bar-fill {
|
| 1038 |
+
position: absolute;
|
| 1039 |
+
left: 0; top: 0; bottom: 0;
|
| 1040 |
+
width: 0%;
|
| 1041 |
+
background: #fff;
|
| 1042 |
+
border-radius: 2px;
|
| 1043 |
+
transition: background 0.12s;
|
| 1044 |
+
}
|
| 1045 |
+
.pb-bar-knob {
|
| 1046 |
+
position: absolute;
|
| 1047 |
+
top: 50%;
|
| 1048 |
+
width: 12px; height: 12px;
|
| 1049 |
+
background: #fff;
|
| 1050 |
+
border-radius: 50%;
|
| 1051 |
+
transform: translate(-50%, -50%);
|
| 1052 |
+
opacity: 0;
|
| 1053 |
+
transition: opacity 0.12s;
|
| 1054 |
+
pointer-events: none;
|
| 1055 |
+
}
|
| 1056 |
+
|
| 1057 |
+
.pb-right {
|
| 1058 |
+
display: flex;
|
| 1059 |
+
align-items: center;
|
| 1060 |
+
justify-content: flex-end;
|
| 1061 |
+
gap: 8px;
|
| 1062 |
+
}
|
| 1063 |
+
.pb-mobile-play { display: none; }
|
| 1064 |
+
.pb-vol {
|
| 1065 |
+
width: 100px;
|
| 1066 |
+
height: 4px;
|
| 1067 |
+
background: rgba(255, 255, 255, 0.18);
|
| 1068 |
+
border-radius: 2px;
|
| 1069 |
+
position: relative;
|
| 1070 |
+
cursor: pointer;
|
| 1071 |
+
}
|
| 1072 |
+
.pb-vol:hover .pb-bar-fill { background: var(--ms-accent-hot); }
|
| 1073 |
+
.pb-vol:hover .pb-bar-knob { opacity: 1; }
|
| 1074 |
+
|
| 1075 |
+
.ms-sidebar-backdrop {
|
| 1076 |
+
display: none;
|
| 1077 |
+
position: fixed;
|
| 1078 |
+
inset: 0;
|
| 1079 |
+
background: rgba(0, 0, 0, 0.6);
|
| 1080 |
+
z-index: 60;
|
| 1081 |
+
opacity: 0;
|
| 1082 |
+
transition: opacity 0.22s ease;
|
| 1083 |
+
}
|
| 1084 |
+
.ms-sidebar-backdrop.active {
|
| 1085 |
+
display: block;
|
| 1086 |
+
opacity: 1;
|
| 1087 |
+
}
|
| 1088 |
+
|
| 1089 |
+
@media (hover: none), (max-width: 800px) {
|
| 1090 |
+
.ms-card-play {
|
| 1091 |
+
opacity: 1;
|
| 1092 |
+
transform: translateY(0);
|
| 1093 |
+
width: 44px; height: 44px;
|
| 1094 |
+
right: 8px; bottom: 8px;
|
| 1095 |
+
}
|
| 1096 |
+
.ms-card-play svg { width: 18px; height: 18px; }
|
| 1097 |
+
.ms-track-like { opacity: 1; }
|
| 1098 |
+
.ms-song-like { opacity: 1; }
|
| 1099 |
+
.ms-song-cover-wrap .ms-song-play { display: none; }
|
| 1100 |
+
}
|
| 1101 |
+
|
| 1102 |
+
@media (max-width: 1024px) {
|
| 1103 |
+
.music-app { grid-template-columns: 1fr; }
|
| 1104 |
+
.music-sidebar {
|
| 1105 |
+
position: fixed;
|
| 1106 |
+
top: var(--header-h);
|
| 1107 |
+
left: 0;
|
| 1108 |
+
bottom: 92px;
|
| 1109 |
+
width: 280px;
|
| 1110 |
+
z-index: 80;
|
| 1111 |
+
margin: 8px;
|
| 1112 |
+
transform: translateX(-110%);
|
| 1113 |
+
transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
|
| 1114 |
+
pointer-events: none;
|
| 1115 |
+
}
|
| 1116 |
+
.music-sidebar.open {
|
| 1117 |
+
transform: translateX(0);
|
| 1118 |
+
pointer-events: auto;
|
| 1119 |
+
}
|
| 1120 |
+
|
| 1121 |
+
.music-sidebar.open .ms-link,
|
| 1122 |
+
.music-sidebar.open .ms-year-grid a,
|
| 1123 |
+
.music-sidebar.open .ms-block-title { pointer-events: auto; }
|
| 1124 |
+
|
| 1125 |
+
.music-sidebar .ms-link { padding: 12px 14px; font-size: 0.98rem; }
|
| 1126 |
+
|
| 1127 |
+
.music-sidebar .ms-link,
|
| 1128 |
+
.music-sidebar .ms-year-grid a,
|
| 1129 |
+
.ms-sidebar-toggle {
|
| 1130 |
+
touch-action: manipulation;
|
| 1131 |
+
-webkit-tap-highlight-color: rgba(167, 139, 250, 0.18);
|
| 1132 |
+
}
|
| 1133 |
+
.ms-sidebar-toggle { display: flex; }
|
| 1134 |
+
.ms-sidebar-backdrop { z-index: 70; }
|
| 1135 |
+
}
|
| 1136 |
+
|
| 1137 |
+
@media (max-width: 800px) {
|
| 1138 |
+
.ms-album-hero {
|
| 1139 |
+
flex-direction: column;
|
| 1140 |
+
align-items: flex-start;
|
| 1141 |
+
gap: 18px;
|
| 1142 |
+
padding: 16px 0;
|
| 1143 |
+
}
|
| 1144 |
+
.ms-album-cover { width: 180px; height: 180px; align-self: center; }
|
| 1145 |
+
.ms-album-info { align-items: flex-start; text-align: left; width: 100%; }
|
| 1146 |
+
.ms-tracks-head, .ms-track {
|
| 1147 |
+
grid-template-columns: 36px 1fr 56px;
|
| 1148 |
+
}
|
| 1149 |
+
.ms-tracks-head .ms-th-anime, .ms-tracks-head .ms-th-tag,
|
| 1150 |
+
.ms-track .ms-track-anime, .ms-track .ms-track-tag-cell { display: none; }
|
| 1151 |
+
.ms-view { padding: 0 14px 24px; }
|
| 1152 |
+
.ms-topbar { padding: 12px 14px; gap: 8px; }
|
| 1153 |
+
.ms-search { max-width: none; }
|
| 1154 |
+
.ms-card-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 16px; }
|
| 1155 |
+
.ms-top-name { font-size: 1.5rem; }
|
| 1156 |
+
}
|
| 1157 |
+
|
| 1158 |
+
@media (max-width: 720px) {
|
| 1159 |
+
.player-bar {
|
| 1160 |
+
height: 64px;
|
| 1161 |
+
grid-template-columns: 1fr auto;
|
| 1162 |
+
padding: 0 10px;
|
| 1163 |
+
gap: 10px;
|
| 1164 |
+
bottom: 0;
|
| 1165 |
+
border-radius: 0;
|
| 1166 |
+
}
|
| 1167 |
+
.pb-cover-btn { width: 44px; height: 44px; }
|
| 1168 |
+
.pb-cover { width: 44px; height: 44px; }
|
| 1169 |
+
.pb-title { font-size: 0.86rem; }
|
| 1170 |
+
.pb-artist { font-size: 0.72rem; }
|
| 1171 |
+
.pb-like, .pb-center { display: none; }
|
| 1172 |
+
.pb-mobile-play {
|
| 1173 |
+
display: flex;
|
| 1174 |
+
width: 40px; height: 40px;
|
| 1175 |
+
color: #fff;
|
| 1176 |
+
background: transparent;
|
| 1177 |
+
border-radius: 50%;
|
| 1178 |
+
}
|
| 1179 |
+
.pb-mobile-play svg { width: 24px; height: 24px; }
|
| 1180 |
+
.pb-right .pb-btn:not(#pb-play-mobile), .pb-right .pb-vol { display: none; }
|
| 1181 |
+
.music-app {
|
| 1182 |
+
bottom: 64px;
|
| 1183 |
+
padding: 6px;
|
| 1184 |
+
gap: 6px;
|
| 1185 |
+
top: 10px;
|
| 1186 |
+
}
|
| 1187 |
+
.music-sidebar { bottom: 64px; }
|
| 1188 |
+
.player-bar[data-empty="1"] { display: none; }
|
| 1189 |
+
body.music-body:has(.player-bar[data-empty="1"]) .music-app,
|
| 1190 |
+
body.music-body:has(.player-bar[data-empty="1"]) .music-sidebar {
|
| 1191 |
+
bottom: 0;
|
| 1192 |
+
}
|
| 1193 |
+
.ms-topbar {
|
| 1194 |
+
padding: 12px 12px;
|
| 1195 |
+
gap: 8px;
|
| 1196 |
+
}
|
| 1197 |
+
.ms-search {
|
| 1198 |
+
max-width: none;
|
| 1199 |
+
}
|
| 1200 |
+
.ms-search input {
|
| 1201 |
+
padding: 10px 38px 10px 38px;
|
| 1202 |
+
font-size: 0.84rem;
|
| 1203 |
+
}
|
| 1204 |
+
.ms-sidebar-toggle {
|
| 1205 |
+
width: 34px;
|
| 1206 |
+
height: 34px;
|
| 1207 |
+
}
|
| 1208 |
+
}
|
| 1209 |
+
|
| 1210 |
+
@media (max-width: 480px) {
|
| 1211 |
+
.ms-card-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
|
| 1212 |
+
.ms-section-title h2 { font-size: 1.2rem; }
|
| 1213 |
+
.ms-top-name { font-size: 1.25rem; }
|
| 1214 |
+
.ms-years-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
|
| 1215 |
+
.ms-year-num { font-size: 1.8rem; }
|
| 1216 |
+
}
|
| 1217 |
+
|
| 1218 |
+
.ms-home-hero {
|
| 1219 |
+
margin: 18px 0 28px;
|
| 1220 |
+
}
|
| 1221 |
+
.ms-home-greet {
|
| 1222 |
+
font-size: 2rem;
|
| 1223 |
+
font-weight: 900;
|
| 1224 |
+
letter-spacing: -0.02em;
|
| 1225 |
+
color: #fff;
|
| 1226 |
+
margin: 0 0 18px;
|
| 1227 |
+
}
|
| 1228 |
+
.ms-quick-grid {
|
| 1229 |
+
display: grid;
|
| 1230 |
+
grid-template-columns: repeat(3, 1fr);
|
| 1231 |
+
gap: 10px;
|
| 1232 |
+
}
|
| 1233 |
+
@media (max-width: 900px) { .ms-quick-grid { grid-template-columns: repeat(2, 1fr); } }
|
| 1234 |
+
@media (max-width: 540px) { .ms-quick-grid { grid-template-columns: 1fr; } }
|
| 1235 |
+
|
| 1236 |
+
.ms-quick-tile {
|
| 1237 |
+
display: flex;
|
| 1238 |
+
align-items: center;
|
| 1239 |
+
gap: 12px;
|
| 1240 |
+
background: rgba(255, 255, 255, 0.06);
|
| 1241 |
+
border-radius: 6px;
|
| 1242 |
+
padding: 0 14px 0 0;
|
| 1243 |
+
height: 64px;
|
| 1244 |
+
overflow: hidden;
|
| 1245 |
+
position: relative;
|
| 1246 |
+
transition: background 0.15s;
|
| 1247 |
+
}
|
| 1248 |
+
.ms-quick-tile:hover { background: rgba(255, 255, 255, 0.12); }
|
| 1249 |
+
.ms-quick-cover {
|
| 1250 |
+
width: 64px; height: 64px;
|
| 1251 |
+
object-fit: cover;
|
| 1252 |
+
flex-shrink: 0;
|
| 1253 |
+
background: var(--ms-surface-2);
|
| 1254 |
+
}
|
| 1255 |
+
.ms-quick-name {
|
| 1256 |
+
flex: 1;
|
| 1257 |
+
font-weight: 700;
|
| 1258 |
+
color: #fff;
|
| 1259 |
+
font-size: 0.9rem;
|
| 1260 |
+
overflow: hidden;
|
| 1261 |
+
text-overflow: ellipsis;
|
| 1262 |
+
white-space: nowrap;
|
| 1263 |
+
min-width: 0;
|
| 1264 |
+
}
|
| 1265 |
+
.ms-quick-play {
|
| 1266 |
+
width: 44px; height: 44px;
|
| 1267 |
+
border-radius: 50%;
|
| 1268 |
+
background: var(--ms-accent);
|
| 1269 |
+
color: #fff;
|
| 1270 |
+
display: flex; align-items: center; justify-content: center;
|
| 1271 |
+
flex-shrink: 0;
|
| 1272 |
+
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.45);
|
| 1273 |
+
opacity: 0;
|
| 1274 |
+
transform: translateX(8px);
|
| 1275 |
+
transition: opacity 0.15s, transform 0.15s, background 0.15s;
|
| 1276 |
+
}
|
| 1277 |
+
.ms-quick-tile:hover .ms-quick-play { opacity: 1; transform: translateX(0); }
|
| 1278 |
+
.ms-quick-play:hover { background: var(--ms-accent-hot); transform: scale(1.06); }
|
| 1279 |
+
.ms-quick-play svg { width: 18px; height: 18px; transform: translateX(1px); }
|
| 1280 |
+
.ms-quick-empty {
|
| 1281 |
+
grid-column: 1 / -1;
|
| 1282 |
+
color: var(--ms-text-dim);
|
| 1283 |
+
background: var(--ms-surface);
|
| 1284 |
+
border-radius: 6px;
|
| 1285 |
+
padding: 18px;
|
| 1286 |
+
font-size: 0.92rem;
|
| 1287 |
+
}
|
| 1288 |
+
|
| 1289 |
+
.ms-years-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px; }
|
| 1290 |
+
.ms-year-card {
|
| 1291 |
+
display: block;
|
| 1292 |
+
background: var(--ms-surface);
|
| 1293 |
+
padding: 14px;
|
| 1294 |
+
border-radius: 8px;
|
| 1295 |
+
transition: background 0.18s;
|
| 1296 |
+
text-align: left;
|
| 1297 |
+
}
|
| 1298 |
+
.ms-year-card:hover { background: var(--ms-surface-2); }
|
| 1299 |
+
.ms-year-cover {
|
| 1300 |
+
width: 100%;
|
| 1301 |
+
aspect-ratio: 1 / 1;
|
| 1302 |
+
border-radius: 6px;
|
| 1303 |
+
display: flex;
|
| 1304 |
+
align-items: center;
|
| 1305 |
+
justify-content: center;
|
| 1306 |
+
margin-bottom: 12px;
|
| 1307 |
+
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.55);
|
| 1308 |
+
overflow: hidden;
|
| 1309 |
+
position: relative;
|
| 1310 |
+
}
|
| 1311 |
+
.ms-year-cover::after {
|
| 1312 |
+
content: "";
|
| 1313 |
+
position: absolute;
|
| 1314 |
+
inset: 0;
|
| 1315 |
+
background: radial-gradient(circle at 30% 25%, rgba(255,255,255,0.12), transparent 55%);
|
| 1316 |
+
pointer-events: none;
|
| 1317 |
+
}
|
| 1318 |
+
.ms-year-num {
|
| 1319 |
+
font-size: 2.4rem;
|
| 1320 |
+
font-weight: 900;
|
| 1321 |
+
letter-spacing: -0.04em;
|
| 1322 |
+
color: #fff;
|
| 1323 |
+
text-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
|
| 1324 |
+
}
|
| 1325 |
+
|
| 1326 |
+
.pb-cover-btn {
|
| 1327 |
+
position: relative;
|
| 1328 |
+
width: 56px; height: 56px;
|
| 1329 |
+
border-radius: 4px;
|
| 1330 |
+
overflow: hidden;
|
| 1331 |
+
flex-shrink: 0;
|
| 1332 |
+
background: transparent;
|
| 1333 |
+
cursor: pointer;
|
| 1334 |
+
display: block;
|
| 1335 |
+
}
|
| 1336 |
+
.pb-cover-btn .pb-cover { width: 100%; height: 100%; display: block; }
|
| 1337 |
+
.pb-cover-overlay {
|
| 1338 |
+
position: absolute;
|
| 1339 |
+
inset: 0;
|
| 1340 |
+
display: flex;
|
| 1341 |
+
align-items: center;
|
| 1342 |
+
justify-content: center;
|
| 1343 |
+
background: rgba(0, 0, 0, 0.55);
|
| 1344 |
+
color: #fff;
|
| 1345 |
+
opacity: 0;
|
| 1346 |
+
transition: opacity 0.15s;
|
| 1347 |
+
}
|
| 1348 |
+
.pb-cover-overlay svg { width: 18px; height: 18px; }
|
| 1349 |
+
.pb-cover-btn:hover .pb-cover-overlay { opacity: 1; }
|
| 1350 |
+
.player-bar[data-empty="1"] .pb-cover-btn { pointer-events: none; }
|
| 1351 |
+
|
| 1352 |
+
.np-modal {
|
| 1353 |
+
position: fixed;
|
| 1354 |
+
inset: 0;
|
| 1355 |
+
z-index: 10001;
|
| 1356 |
+
color: #fff;
|
| 1357 |
+
overflow: hidden;
|
| 1358 |
+
animation: npFadeIn 0.22s ease-out;
|
| 1359 |
+
}
|
| 1360 |
+
.np-modal[hidden] { display: none; }
|
| 1361 |
+
body.np-open { overflow: hidden; }
|
| 1362 |
+
body.np-open .float-nav { display: none !important; }
|
| 1363 |
+
|
| 1364 |
+
body.np-open #header { display: none !important; }
|
| 1365 |
+
body.np-open .np-modal { top: 0; }
|
| 1366 |
+
@keyframes npFadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
| 1367 |
+
|
| 1368 |
+
.np-bg {
|
| 1369 |
+
position: absolute;
|
| 1370 |
+
inset: 0;
|
| 1371 |
+
background-size: cover;
|
| 1372 |
+
background-position: center;
|
| 1373 |
+
filter: blur(38px) saturate(150%) brightness(0.6);
|
| 1374 |
+
transform: scale(1.2);
|
| 1375 |
+
z-index: 0;
|
| 1376 |
+
transition: filter 0.35s ease;
|
| 1377 |
+
}
|
| 1378 |
+
.np-bg::after {
|
| 1379 |
+
content: "";
|
| 1380 |
+
position: absolute;
|
| 1381 |
+
inset: 0;
|
| 1382 |
+
background: linear-gradient(180deg, rgba(10,5,25,0.55) 0%, rgba(10,5,25,0.85) 60%, #07050d 100%);
|
| 1383 |
+
transition: opacity 0.35s ease;
|
| 1384 |
+
}
|
| 1385 |
+
|
| 1386 |
+
body.np-lyrics-open .np-bg {
|
| 1387 |
+
filter: blur(46px) saturate(140%) brightness(0.45);
|
| 1388 |
+
}
|
| 1389 |
+
body.np-lyrics-open .np-bg::after {
|
| 1390 |
+
background: linear-gradient(180deg, rgba(8,4,20,0.72) 0%, rgba(8,4,20,0.88) 60%, #050208 100%);
|
| 1391 |
+
opacity: 1;
|
| 1392 |
+
}
|
| 1393 |
+
|
| 1394 |
+
body.np-lyrics-open .np-stage { box-shadow: none; background: transparent; }
|
| 1395 |
+
body.np-lyrics-open .np-cover,
|
| 1396 |
+
body.np-lyrics-open .np-cover-fallback { display: none !important; }
|
| 1397 |
+
|
| 1398 |
+
.np-inner {
|
| 1399 |
+
position: relative;
|
| 1400 |
+
z-index: 1;
|
| 1401 |
+
height: 100vh;
|
| 1402 |
+
height: 100dvh;
|
| 1403 |
+
max-width: 520px;
|
| 1404 |
+
margin: 0 auto;
|
| 1405 |
+
padding: 14px 22px 22px;
|
| 1406 |
+
display: grid;
|
| 1407 |
+
grid-template-rows: auto minmax(0, 1fr) auto auto auto auto;
|
| 1408 |
+
gap: 14px;
|
| 1409 |
+
}
|
| 1410 |
+
@media (min-width: 900px) {
|
| 1411 |
+
.np-inner { max-width: 560px; padding: 18px 28px 26px; gap: 16px; }
|
| 1412 |
+
}
|
| 1413 |
+
@media (max-width: 480px) {
|
| 1414 |
+
.np-inner { padding: 10px 16px 18px; gap: 10px; }
|
| 1415 |
+
}
|
| 1416 |
+
|
| 1417 |
+
.np-header {
|
| 1418 |
+
display: flex;
|
| 1419 |
+
align-items: center;
|
| 1420 |
+
justify-content: space-between;
|
| 1421 |
+
gap: 12px;
|
| 1422 |
+
}
|
| 1423 |
+
.np-icon-btn {
|
| 1424 |
+
min-width: 36px; height: 36px;
|
| 1425 |
+
display: flex; align-items: center; justify-content: center;
|
| 1426 |
+
gap: 6px;
|
| 1427 |
+
padding: 0 6px;
|
| 1428 |
+
color: #fff;
|
| 1429 |
+
border-radius: 18px;
|
| 1430 |
+
transition: background 0.15s;
|
| 1431 |
+
}
|
| 1432 |
+
.np-icon-btn:hover { background: rgba(255, 255, 255, 0.12); }
|
| 1433 |
+
.np-icon-btn svg { width: 22px; height: 22px; }
|
| 1434 |
+
|
| 1435 |
+
.np-back-btn span {
|
| 1436 |
+
font-size: 0.85rem;
|
| 1437 |
+
font-weight: 600;
|
| 1438 |
+
padding-right: 6px;
|
| 1439 |
+
}
|
| 1440 |
+
@media (max-width: 600px) {
|
| 1441 |
+
.np-back-btn span { display: none; }
|
| 1442 |
+
.np-back-btn { padding: 0; min-width: 36px; border-radius: 50%; }
|
| 1443 |
+
}
|
| 1444 |
+
.np-source { text-align: center; min-width: 0; flex: 1; }
|
| 1445 |
+
.np-eyebrow {
|
| 1446 |
+
font-size: 0.66rem;
|
| 1447 |
+
text-transform: uppercase;
|
| 1448 |
+
letter-spacing: 0.18em;
|
| 1449 |
+
color: rgba(255, 255, 255, 0.7);
|
| 1450 |
+
font-weight: 700;
|
| 1451 |
+
margin-bottom: 2px;
|
| 1452 |
+
}
|
| 1453 |
+
.np-source-name {
|
| 1454 |
+
font-size: 0.86rem;
|
| 1455 |
+
font-weight: 600;
|
| 1456 |
+
color: #fff;
|
| 1457 |
+
text-decoration: none;
|
| 1458 |
+
overflow: hidden;
|
| 1459 |
+
text-overflow: ellipsis;
|
| 1460 |
+
white-space: nowrap;
|
| 1461 |
+
display: block;
|
| 1462 |
+
}
|
| 1463 |
+
.np-source-name:hover { text-decoration: underline; }
|
| 1464 |
+
|
| 1465 |
+
.np-stage {
|
| 1466 |
+
position: relative;
|
| 1467 |
+
|
| 1468 |
+
width: min(100%, 70vh, 560px);
|
| 1469 |
+
max-width: 100%;
|
| 1470 |
+
aspect-ratio: 1 / 1;
|
| 1471 |
+
max-height: min(70vh, 560px);
|
| 1472 |
+
margin: 0 auto;
|
| 1473 |
+
border-radius: 12px;
|
| 1474 |
+
overflow: hidden;
|
| 1475 |
+
background: linear-gradient(135deg, #432470 0%, #1a0f33 100%);
|
| 1476 |
+
box-shadow: 0 26px 60px rgba(0, 0, 0, 0.6);
|
| 1477 |
+
align-self: center;
|
| 1478 |
+
flex-shrink: 0;
|
| 1479 |
+
}
|
| 1480 |
+
@media (max-width: 600px) {
|
| 1481 |
+
.np-stage {
|
| 1482 |
+
|
| 1483 |
+
width: min(100%, 54vh, 460px);
|
| 1484 |
+
max-height: 54vh;
|
| 1485 |
+
}
|
| 1486 |
+
}
|
| 1487 |
+
@media (max-width: 380px) {
|
| 1488 |
+
.np-stage {
|
| 1489 |
+
width: min(100%, 48vh, 380px);
|
| 1490 |
+
max-height: 48vh;
|
| 1491 |
+
}
|
| 1492 |
+
}
|
| 1493 |
+
|
| 1494 |
+
@media (max-height: 520px) and (orientation: landscape) {
|
| 1495 |
+
.np-stage {
|
| 1496 |
+
width: min(82vh, 400px);
|
| 1497 |
+
max-height: 82vh;
|
| 1498 |
+
}
|
| 1499 |
+
}
|
| 1500 |
+
.np-cover, .np-video {
|
| 1501 |
+
position: absolute;
|
| 1502 |
+
inset: 0;
|
| 1503 |
+
width: 100%;
|
| 1504 |
+
height: 100%;
|
| 1505 |
+
object-fit: cover;
|
| 1506 |
+
display: block;
|
| 1507 |
+
}
|
| 1508 |
+
.np-video { background: #000; object-fit: contain; }
|
| 1509 |
+
|
| 1510 |
+
.np-cover[hidden], .np-video[hidden] { display: none !important; }
|
| 1511 |
+
|
| 1512 |
+
.np-cover:not([src]),
|
| 1513 |
+
.np-cover[src=""],
|
| 1514 |
+
.np-cover.is-failed { visibility: hidden; }
|
| 1515 |
+
|
| 1516 |
+
.np-cover-fallback {
|
| 1517 |
+
position: absolute;
|
| 1518 |
+
inset: 0;
|
| 1519 |
+
display: flex;
|
| 1520 |
+
align-items: center;
|
| 1521 |
+
justify-content: center;
|
| 1522 |
+
color: rgba(255, 255, 255, 0.55);
|
| 1523 |
+
pointer-events: none;
|
| 1524 |
+
}
|
| 1525 |
+
.np-cover-fallback svg { width: 72px; height: 72px; }
|
| 1526 |
+
|
| 1527 |
+
.np-loading {
|
| 1528 |
+
position: absolute;
|
| 1529 |
+
inset: 0;
|
| 1530 |
+
display: flex;
|
| 1531 |
+
align-items: center;
|
| 1532 |
+
justify-content: center;
|
| 1533 |
+
background: rgba(0, 0, 0, 0.45);
|
| 1534 |
+
backdrop-filter: blur(2px);
|
| 1535 |
+
-webkit-backdrop-filter: blur(2px);
|
| 1536 |
+
z-index: 3;
|
| 1537 |
+
animation: npFadeIn 0.18s ease-out;
|
| 1538 |
+
}
|
| 1539 |
+
.np-loading[hidden] { display: none; }
|
| 1540 |
+
.np-spinner {
|
| 1541 |
+
width: 56px;
|
| 1542 |
+
height: 56px;
|
| 1543 |
+
border-radius: 50%;
|
| 1544 |
+
border: 3px solid rgba(255, 255, 255, 0.18);
|
| 1545 |
+
border-top-color: var(--ms-accent-hot);
|
| 1546 |
+
animation: npSpin 0.85s linear infinite;
|
| 1547 |
+
}
|
| 1548 |
+
@keyframes npSpin { to { transform: rotate(360deg); } }
|
| 1549 |
+
|
| 1550 |
+
.np-panel-close {
|
| 1551 |
+
width: 32px; height: 32px;
|
| 1552 |
+
color: rgba(255, 255, 255, 0.7);
|
| 1553 |
+
display: flex; align-items: center; justify-content: center;
|
| 1554 |
+
border-radius: 50%;
|
| 1555 |
+
transition: background 0.15s, color 0.15s;
|
| 1556 |
+
flex-shrink: 0;
|
| 1557 |
+
}
|
| 1558 |
+
.np-panel-close svg { width: 16px; height: 16px; }
|
| 1559 |
+
.np-panel-close:hover { color: #fff; background: rgba(255, 255, 255, 0.12); }
|
| 1560 |
+
.np-lyrics-head {
|
| 1561 |
+
display: flex;
|
| 1562 |
+
align-items: center;
|
| 1563 |
+
justify-content: space-between;
|
| 1564 |
+
margin-bottom: 12px;
|
| 1565 |
+
}
|
| 1566 |
+
.np-lyrics-head h3 { margin: 0; }
|
| 1567 |
+
|
| 1568 |
+
.np-lyrics {
|
| 1569 |
+
position: absolute;
|
| 1570 |
+
inset: 0;
|
| 1571 |
+
|
| 1572 |
+
background: radial-gradient(120% 80% at 50% 0%, #1a0d2e 0%, #0a0612 55%, #050208 100%);
|
| 1573 |
+
overflow: hidden;
|
| 1574 |
+
padding: 18px 22px;
|
| 1575 |
+
display: flex;
|
| 1576 |
+
align-items: center;
|
| 1577 |
+
justify-content: center;
|
| 1578 |
+
}
|
| 1579 |
+
.np-lyrics[hidden] { display: none; }
|
| 1580 |
+
.np-lyrics-inner { width: 100%; max-width: 420px; }
|
| 1581 |
+
.np-lyrics-inner h3 {
|
| 1582 |
+
margin: 0 0 12px;
|
| 1583 |
+
font-size: 1.1rem;
|
| 1584 |
+
font-weight: 800;
|
| 1585 |
+
color: #fff;
|
| 1586 |
+
}
|
| 1587 |
+
.np-lyrics-body {
|
| 1588 |
+
color: rgba(255, 255, 255, 0.92);
|
| 1589 |
+
line-height: 1.55;
|
| 1590 |
+
font-size: 0.96rem;
|
| 1591 |
+
margin: 0 0 18px;
|
| 1592 |
+
}
|
| 1593 |
+
.np-lyrics-meta {
|
| 1594 |
+
display: grid;
|
| 1595 |
+
gap: 10px;
|
| 1596 |
+
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
| 1597 |
+
padding-top: 14px;
|
| 1598 |
+
}
|
| 1599 |
+
.np-lyrics-meta div {
|
| 1600 |
+
display: flex;
|
| 1601 |
+
justify-content: space-between;
|
| 1602 |
+
gap: 16px;
|
| 1603 |
+
font-size: 0.85rem;
|
| 1604 |
+
}
|
| 1605 |
+
.np-lyrics-meta span {
|
| 1606 |
+
color: rgba(255, 255, 255, 0.55);
|
| 1607 |
+
text-transform: uppercase;
|
| 1608 |
+
letter-spacing: 0.08em;
|
| 1609 |
+
font-weight: 700;
|
| 1610 |
+
font-size: 0.7rem;
|
| 1611 |
+
}
|
| 1612 |
+
.np-lyrics-meta strong {
|
| 1613 |
+
color: #fff;
|
| 1614 |
+
font-weight: 600;
|
| 1615 |
+
text-align: right;
|
| 1616 |
+
overflow: hidden;
|
| 1617 |
+
text-overflow: ellipsis;
|
| 1618 |
+
white-space: nowrap;
|
| 1619 |
+
max-width: 60%;
|
| 1620 |
+
}
|
| 1621 |
+
|
| 1622 |
+
.np-info {
|
| 1623 |
+
display: flex;
|
| 1624 |
+
align-items: center;
|
| 1625 |
+
gap: 16px;
|
| 1626 |
+
}
|
| 1627 |
+
.np-info-text { flex: 1; min-width: 0; }
|
| 1628 |
+
.np-title {
|
| 1629 |
+
display: block;
|
| 1630 |
+
color: #fff;
|
| 1631 |
+
font-size: 1.4rem;
|
| 1632 |
+
font-weight: 800;
|
| 1633 |
+
letter-spacing: -0.01em;
|
| 1634 |
+
text-decoration: none;
|
| 1635 |
+
overflow: hidden;
|
| 1636 |
+
text-overflow: ellipsis;
|
| 1637 |
+
white-space: nowrap;
|
| 1638 |
+
margin-bottom: 4px;
|
| 1639 |
+
}
|
| 1640 |
+
.np-title:hover { text-decoration: underline; }
|
| 1641 |
+
.np-artist {
|
| 1642 |
+
display: block;
|
| 1643 |
+
color: rgba(255, 255, 255, 0.75);
|
| 1644 |
+
font-size: 0.95rem;
|
| 1645 |
+
text-decoration: none;
|
| 1646 |
+
overflow: hidden;
|
| 1647 |
+
text-overflow: ellipsis;
|
| 1648 |
+
white-space: nowrap;
|
| 1649 |
+
}
|
| 1650 |
+
.np-artist:hover { color: #fff; text-decoration: underline; }
|
| 1651 |
+
.np-like {
|
| 1652 |
+
width: 40px; height: 40px;
|
| 1653 |
+
color: rgba(255, 255, 255, 0.85);
|
| 1654 |
+
display: flex; align-items: center; justify-content: center;
|
| 1655 |
+
border-radius: 50%;
|
| 1656 |
+
transition: color 0.15s, transform 0.15s;
|
| 1657 |
+
flex-shrink: 0;
|
| 1658 |
+
}
|
| 1659 |
+
.np-like svg { width: 24px; height: 24px; }
|
| 1660 |
+
.np-like:hover { color: #fff; transform: scale(1.08); }
|
| 1661 |
+
.np-like.is-liked { color: var(--ms-accent-hot); }
|
| 1662 |
+
.np-like.is-liked svg { fill: currentColor; }
|
| 1663 |
+
|
| 1664 |
+
.np-progress { display: grid; gap: 6px; }
|
| 1665 |
+
.np-bar {
|
| 1666 |
+
position: relative;
|
| 1667 |
+
height: 4px;
|
| 1668 |
+
background: rgba(255, 255, 255, 0.18);
|
| 1669 |
+
border-radius: 2px;
|
| 1670 |
+
cursor: pointer;
|
| 1671 |
+
transition: height 0.12s;
|
| 1672 |
+
}
|
| 1673 |
+
.np-bar:hover { height: 6px; }
|
| 1674 |
+
.np-bar-fill {
|
| 1675 |
+
position: absolute;
|
| 1676 |
+
left: 0; top: 0; bottom: 0;
|
| 1677 |
+
width: 0;
|
| 1678 |
+
background: #fff;
|
| 1679 |
+
border-radius: 2px;
|
| 1680 |
+
}
|
| 1681 |
+
.np-bar:hover .np-bar-fill { background: var(--ms-accent-hot); }
|
| 1682 |
+
.np-bar-knob {
|
| 1683 |
+
position: absolute;
|
| 1684 |
+
top: 50%;
|
| 1685 |
+
width: 12px; height: 12px;
|
| 1686 |
+
background: #fff;
|
| 1687 |
+
border-radius: 50%;
|
| 1688 |
+
transform: translate(-50%, -50%);
|
| 1689 |
+
opacity: 0;
|
| 1690 |
+
transition: opacity 0.12s;
|
| 1691 |
+
}
|
| 1692 |
+
.np-bar:hover .np-bar-knob { opacity: 1; }
|
| 1693 |
+
.np-times {
|
| 1694 |
+
display: flex;
|
| 1695 |
+
justify-content: space-between;
|
| 1696 |
+
color: rgba(255, 255, 255, 0.7);
|
| 1697 |
+
font-size: 0.72rem;
|
| 1698 |
+
font-variant-numeric: tabular-nums;
|
| 1699 |
+
}
|
| 1700 |
+
|
| 1701 |
+
.np-controls {
|
| 1702 |
+
display: flex;
|
| 1703 |
+
align-items: center;
|
| 1704 |
+
justify-content: space-between;
|
| 1705 |
+
padding: 0 4px;
|
| 1706 |
+
}
|
| 1707 |
+
.np-ctl {
|
| 1708 |
+
width: 44px; height: 44px;
|
| 1709 |
+
color: rgba(255, 255, 255, 0.85);
|
| 1710 |
+
display: flex; align-items: center; justify-content: center;
|
| 1711 |
+
border-radius: 50%;
|
| 1712 |
+
transition: color 0.15s, transform 0.15s;
|
| 1713 |
+
position: relative;
|
| 1714 |
+
}
|
| 1715 |
+
.np-ctl svg { width: 26px; height: 26px; }
|
| 1716 |
+
.np-ctl:hover { color: #fff; transform: scale(1.08); }
|
| 1717 |
+
.np-ctl.is-on { color: var(--ms-accent-hot); }
|
| 1718 |
+
.np-ctl.is-on::after {
|
| 1719 |
+
content: "";
|
| 1720 |
+
position: absolute;
|
| 1721 |
+
bottom: 6px;
|
| 1722 |
+
left: 50%;
|
| 1723 |
+
transform: translateX(-50%);
|
| 1724 |
+
width: 4px; height: 4px;
|
| 1725 |
+
background: var(--ms-accent-hot);
|
| 1726 |
+
border-radius: 50%;
|
| 1727 |
+
}
|
| 1728 |
+
.np-play-big {
|
| 1729 |
+
width: 64px; height: 64px;
|
| 1730 |
+
border-radius: 50%;
|
| 1731 |
+
background: #fff;
|
| 1732 |
+
color: #000;
|
| 1733 |
+
display: flex; align-items: center; justify-content: center;
|
| 1734 |
+
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.45);
|
| 1735 |
+
transition: transform 0.12s;
|
| 1736 |
+
}
|
| 1737 |
+
.np-play-big svg { width: 30px; height: 30px; transform: translateX(1px); }
|
| 1738 |
+
.np-play-big:hover { transform: scale(1.06); }
|
| 1739 |
+
.np-play-big:active { transform: scale(0.96); }
|
| 1740 |
+
|
| 1741 |
+
.np-tools {
|
| 1742 |
+
display: flex;
|
| 1743 |
+
align-items: center;
|
| 1744 |
+
justify-content: center;
|
| 1745 |
+
gap: 12px;
|
| 1746 |
+
padding-top: 4px;
|
| 1747 |
+
}
|
| 1748 |
+
.np-tool {
|
| 1749 |
+
display: inline-flex;
|
| 1750 |
+
align-items: center;
|
| 1751 |
+
gap: 8px;
|
| 1752 |
+
padding: 9px 18px;
|
| 1753 |
+
border-radius: 999px;
|
| 1754 |
+
background: rgba(255, 255, 255, 0.10);
|
| 1755 |
+
color: rgba(255, 255, 255, 0.92);
|
| 1756 |
+
font-weight: 600;
|
| 1757 |
+
font-size: 0.84rem;
|
| 1758 |
+
letter-spacing: 0.01em;
|
| 1759 |
+
transition: background 0.15s, color 0.15s, transform 0.12s;
|
| 1760 |
+
}
|
| 1761 |
+
.np-tool svg { width: 18px; height: 18px; }
|
| 1762 |
+
.np-tool:hover { background: rgba(255, 255, 255, 0.18); color: #fff; }
|
| 1763 |
+
.np-tool.is-on {
|
| 1764 |
+
background: var(--ms-accent);
|
| 1765 |
+
color: #fff;
|
| 1766 |
+
}
|
| 1767 |
+
.np-tool.is-on:hover { background: var(--ms-accent-hot); }
|
| 1768 |
+
.np-tool.is-disabled { opacity: 0.45; pointer-events: none; }
|
| 1769 |
+
|
| 1770 |
+
@media (min-width: 720px) {
|
| 1771 |
+
.np-inner { max-width: 620px; padding: 28px 32px 36px; gap: 22px; }
|
| 1772 |
+
.np-title { font-size: 1.7rem; }
|
| 1773 |
+
}
|
| 1774 |
+
|
| 1775 |
+
#toast-container {
|
| 1776 |
+
position: fixed;
|
| 1777 |
+
bottom: 110px;
|
| 1778 |
+
left: 50%;
|
| 1779 |
+
transform: translateX(-50%);
|
| 1780 |
+
z-index: 300;
|
| 1781 |
+
display: flex;
|
| 1782 |
+
flex-direction: column;
|
| 1783 |
+
align-items: center;
|
| 1784 |
+
gap: 8px;
|
| 1785 |
+
pointer-events: none;
|
| 1786 |
+
}
|
| 1787 |
+
.kv-toast {
|
| 1788 |
+
background: rgba(20, 14, 36, 0.96);
|
| 1789 |
+
color: #fff;
|
| 1790 |
+
border: 1px solid rgba(255, 255, 255, 0.10);
|
| 1791 |
+
padding: 10px 18px;
|
| 1792 |
+
border-radius: 999px;
|
| 1793 |
+
font-size: 0.85rem;
|
| 1794 |
+
font-weight: 600;
|
| 1795 |
+
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45);
|
| 1796 |
+
opacity: 0;
|
| 1797 |
+
transform: translateY(8px);
|
| 1798 |
+
transition: opacity 0.2s, transform 0.2s;
|
| 1799 |
+
}
|
| 1800 |
+
.kv-toast.show { opacity: 1; transform: none; }
|
| 1801 |
+
|
| 1802 |
+
.np-queue {
|
| 1803 |
+
position: absolute;
|
| 1804 |
+
inset: 0;
|
| 1805 |
+
background: rgba(8, 4, 18, 0.92);
|
| 1806 |
+
backdrop-filter: blur(28px);
|
| 1807 |
+
-webkit-backdrop-filter: blur(28px);
|
| 1808 |
+
border-radius: inherit;
|
| 1809 |
+
display: flex;
|
| 1810 |
+
flex-direction: column;
|
| 1811 |
+
overflow: hidden;
|
| 1812 |
+
animation: npFadeIn 0.18s ease-out;
|
| 1813 |
+
}
|
| 1814 |
+
.np-queue[hidden] { display: none; }
|
| 1815 |
+
.np-queue-head {
|
| 1816 |
+
flex-shrink: 0;
|
| 1817 |
+
padding: 16px 18px 10px;
|
| 1818 |
+
display: flex;
|
| 1819 |
+
align-items: baseline;
|
| 1820 |
+
justify-content: space-between;
|
| 1821 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
| 1822 |
+
}
|
| 1823 |
+
.np-queue-head h3 {
|
| 1824 |
+
font-size: 1rem;
|
| 1825 |
+
font-weight: 800;
|
| 1826 |
+
letter-spacing: 0.04em;
|
| 1827 |
+
text-transform: uppercase;
|
| 1828 |
+
}
|
| 1829 |
+
.np-queue-head span {
|
| 1830 |
+
font-size: 0.78rem;
|
| 1831 |
+
color: var(--ms-text-dim);
|
| 1832 |
+
font-weight: 600;
|
| 1833 |
+
}
|
| 1834 |
+
.np-queue-list {
|
| 1835 |
+
flex: 1;
|
| 1836 |
+
overflow-y: auto;
|
| 1837 |
+
padding: 6px 8px 12px;
|
| 1838 |
+
scrollbar-width: thin;
|
| 1839 |
+
scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
|
| 1840 |
+
}
|
| 1841 |
+
.np-queue-empty {
|
| 1842 |
+
color: var(--ms-text-dim);
|
| 1843 |
+
text-align: center;
|
| 1844 |
+
padding: 30px 16px;
|
| 1845 |
+
font-size: 0.92rem;
|
| 1846 |
+
}
|
| 1847 |
+
.np-queue-row {
|
| 1848 |
+
width: 100%;
|
| 1849 |
+
display: grid;
|
| 1850 |
+
grid-template-columns: 44px 1fr auto;
|
| 1851 |
+
gap: 12px;
|
| 1852 |
+
align-items: center;
|
| 1853 |
+
padding: 8px 10px;
|
| 1854 |
+
border-radius: 8px;
|
| 1855 |
+
background: transparent;
|
| 1856 |
+
color: #fff;
|
| 1857 |
+
text-align: left;
|
| 1858 |
+
cursor: pointer;
|
| 1859 |
+
transition: background 0.15s;
|
| 1860 |
+
}
|
| 1861 |
+
.np-queue-row:hover { background: rgba(255, 255, 255, 0.07); }
|
| 1862 |
+
.np-queue-row.is-current { background: rgba(124, 58, 237, 0.18); }
|
| 1863 |
+
.np-queue-cover {
|
| 1864 |
+
position: relative;
|
| 1865 |
+
width: 44px; height: 44px;
|
| 1866 |
+
border-radius: 6px;
|
| 1867 |
+
overflow: hidden;
|
| 1868 |
+
background: var(--ms-surface-2);
|
| 1869 |
+
flex-shrink: 0;
|
| 1870 |
+
}
|
| 1871 |
+
.np-queue-cover img { width: 100%; height: 100%; object-fit: cover; }
|
| 1872 |
+
.np-queue-eq {
|
| 1873 |
+
position: absolute;
|
| 1874 |
+
inset: 0;
|
| 1875 |
+
background: rgba(0, 0, 0, 0.55);
|
| 1876 |
+
display: flex;
|
| 1877 |
+
align-items: flex-end;
|
| 1878 |
+
justify-content: center;
|
| 1879 |
+
gap: 2px;
|
| 1880 |
+
padding-bottom: 6px;
|
| 1881 |
+
}
|
| 1882 |
+
.np-queue-eq i {
|
| 1883 |
+
width: 3px; height: 12px;
|
| 1884 |
+
background: var(--ms-accent-hot);
|
| 1885 |
+
border-radius: 1px;
|
| 1886 |
+
animation: eqBars 1s ease-in-out infinite;
|
| 1887 |
+
animation-play-state: paused;
|
| 1888 |
+
transform: scaleY(0.4);
|
| 1889 |
+
transform-origin: bottom center;
|
| 1890 |
+
}
|
| 1891 |
+
.np-queue-eq i:nth-child(2) { animation-delay: 0.15s; height: 16px; }
|
| 1892 |
+
.np-queue-eq i:nth-child(3) { animation-delay: 0.3s; height: 10px; }
|
| 1893 |
+
|
| 1894 |
+
body.is-playing .np-queue-eq i { animation-play-state: running; }
|
| 1895 |
+
@keyframes eqBars {
|
| 1896 |
+
0%, 100% { transform: scaleY(0.4); }
|
| 1897 |
+
50% { transform: scaleY(1); }
|
| 1898 |
+
}
|
| 1899 |
+
|
| 1900 |
+
.np-lyric-line {
|
| 1901 |
+
display: block;
|
| 1902 |
+
padding: 6px 4px;
|
| 1903 |
+
line-height: 1.55;
|
| 1904 |
+
color: rgba(255, 255, 255, 0.55);
|
| 1905 |
+
transition: color 0.25s ease, transform 0.25s ease, font-weight 0.2s ease;
|
| 1906 |
+
font-size: 1rem;
|
| 1907 |
+
}
|
| 1908 |
+
.np-lyric-line.is-active {
|
| 1909 |
+
color: #fff;
|
| 1910 |
+
font-weight: 700;
|
| 1911 |
+
transform: scale(1.04);
|
| 1912 |
+
transform-origin: left center;
|
| 1913 |
+
text-shadow: 0 0 18px rgba(157, 110, 248, 0.55);
|
| 1914 |
+
}
|
| 1915 |
+
.np-lyric-line.is-loading,
|
| 1916 |
+
.np-lyric-line.is-empty {
|
| 1917 |
+
color: rgba(255, 255, 255, 0.5);
|
| 1918 |
+
font-style: italic;
|
| 1919 |
+
}
|
| 1920 |
+
.np-queue-text {
|
| 1921 |
+
display: flex;
|
| 1922 |
+
flex-direction: column;
|
| 1923 |
+
gap: 2px;
|
| 1924 |
+
min-width: 0;
|
| 1925 |
+
}
|
| 1926 |
+
.np-queue-title {
|
| 1927 |
+
font-size: 0.95rem;
|
| 1928 |
+
font-weight: 600;
|
| 1929 |
+
color: #fff;
|
| 1930 |
+
overflow: hidden;
|
| 1931 |
+
text-overflow: ellipsis;
|
| 1932 |
+
white-space: nowrap;
|
| 1933 |
+
}
|
| 1934 |
+
.np-queue-row.is-current .np-queue-title { color: var(--ms-accent-hot); }
|
| 1935 |
+
.np-queue-sub {
|
| 1936 |
+
font-size: 0.78rem;
|
| 1937 |
+
color: var(--ms-text-dim);
|
| 1938 |
+
overflow: hidden;
|
| 1939 |
+
text-overflow: ellipsis;
|
| 1940 |
+
white-space: nowrap;
|
| 1941 |
+
}
|
| 1942 |
+
.np-queue-tag {
|
| 1943 |
+
font-size: 0.7rem;
|
| 1944 |
+
color: var(--ms-text-dim);
|
| 1945 |
+
font-weight: 700;
|
| 1946 |
+
text-transform: uppercase;
|
| 1947 |
+
letter-spacing: 0.06em;
|
| 1948 |
+
flex-shrink: 0;
|
| 1949 |
+
}
|
| 1950 |
+
|
| 1951 |
+
.ms-link:hover {
|
| 1952 |
+
color: #fff;
|
| 1953 |
+
background: rgba(255, 255, 255, 0.06);
|
| 1954 |
+
}
|
| 1955 |
+
.ms-link:active { background: rgba(255, 255, 255, 0.12); }
|
| 1956 |
+
|
| 1957 |
+
.pb-cover-btn { cursor: pointer; }
|
| 1958 |
+
|
| 1959 |
+
@media (max-width: 720px) {
|
| 1960 |
+
.np-inner {
|
| 1961 |
+
padding: 14px 16px 22px;
|
| 1962 |
+
gap: 14px;
|
| 1963 |
+
}
|
| 1964 |
+
.np-stage { aspect-ratio: 1 / 1; }
|
| 1965 |
+
.np-tools { gap: 8px; }
|
| 1966 |
+
.np-tool { padding: 8px 12px; font-size: 0.78rem; }
|
| 1967 |
+
.np-tool span { display: inline; }
|
| 1968 |
+
.np-title { font-size: 1.1rem; }
|
| 1969 |
+
.np-artist { font-size: 0.88rem; }
|
| 1970 |
+
.np-controls { gap: 18px; }
|
| 1971 |
+
.np-play-big { width: 64px; height: 64px; }
|
| 1972 |
+
.np-play-big svg { width: 26px; height: 26px; }
|
| 1973 |
+
.np-queue-row { grid-template-columns: 40px 1fr auto; }
|
| 1974 |
+
.np-queue-cover { width: 40px; height: 40px; }
|
| 1975 |
+
}
|
| 1976 |
+
|
| 1977 |
+
@media (max-width: 380px) {
|
| 1978 |
+
.np-tool span { display: none; }
|
| 1979 |
+
.np-tool { padding: 8px 10px; }
|
| 1980 |
+
.np-controls { gap: 12px; }
|
| 1981 |
+
.np-controls .pb-icon-btn { width: 36px; height: 36px; }
|
| 1982 |
+
.np-play-big { width: 58px; height: 58px; }
|
| 1983 |
+
}
|
| 1984 |
+
|
| 1985 |
+
@media (min-width: 721px) and (max-width: 1024px) {
|
| 1986 |
+
.np-inner { max-width: 460px; padding: 22px 28px 30px; }
|
| 1987 |
+
}
|
| 1988 |
+
|
| 1989 |
+
@media (min-width: 1200px) {
|
| 1990 |
+
.np-inner { max-width: 560px; }
|
| 1991 |
+
.np-stage { max-height: 420px; }
|
| 1992 |
+
}
|
| 1993 |
+
|
| 1994 |
+
.np-inner {
|
| 1995 |
+
grid-template-rows: auto minmax(0, 1fr) auto auto auto auto;
|
| 1996 |
+
align-content: stretch;
|
| 1997 |
+
gap: 14px;
|
| 1998 |
+
padding: 14px 22px max(20px, env(safe-area-inset-bottom));
|
| 1999 |
+
overflow: hidden;
|
| 2000 |
+
overscroll-behavior: contain;
|
| 2001 |
+
box-sizing: border-box;
|
| 2002 |
+
}
|
| 2003 |
+
.np-stage {
|
| 2004 |
+
max-height: min(60vh, 480px);
|
| 2005 |
+
min-height: 0;
|
| 2006 |
+
}
|
| 2007 |
+
@media (max-height: 780px) {
|
| 2008 |
+
.np-inner { gap: 10px; padding: 10px 18px max(14px, env(safe-area-inset-bottom)); }
|
| 2009 |
+
.np-stage { max-height: min(52vh, 400px); }
|
| 2010 |
+
.np-controls { gap: 14px; }
|
| 2011 |
+
.np-tools { padding-top: 0; gap: 8px; }
|
| 2012 |
+
.np-tool { padding: 7px 12px; font-size: 0.78rem; }
|
| 2013 |
+
}
|
| 2014 |
+
@media (max-height: 640px) {
|
| 2015 |
+
.np-inner { gap: 6px; padding: 8px 14px max(10px, env(safe-area-inset-bottom)); }
|
| 2016 |
+
.np-stage { max-height: min(42vh, 300px); }
|
| 2017 |
+
.np-info { padding: 6px 10px; }
|
| 2018 |
+
.np-title { font-size: 1rem; margin-bottom: 2px; }
|
| 2019 |
+
.np-artist { font-size: 0.82rem; }
|
| 2020 |
+
.np-play-big { width: 54px; height: 54px; }
|
| 2021 |
+
.np-play-big svg { width: 22px; height: 22px; }
|
| 2022 |
+
.np-ctl { width: 38px; height: 38px; }
|
| 2023 |
+
.np-ctl svg { width: 22px; height: 22px; }
|
| 2024 |
+
}
|
| 2025 |
+
@media (max-height: 520px) {
|
| 2026 |
+
.np-stage { max-height: 30vh; }
|
| 2027 |
+
.np-inner { gap: 4px; padding: 6px 12px max(8px, env(safe-area-inset-bottom)); }
|
| 2028 |
+
.np-tool span { display: none; }
|
| 2029 |
+
.np-tool { padding: 6px 10px; }
|
| 2030 |
+
}
|
| 2031 |
+
|
| 2032 |
+
body.np-lyrics-open .np-inner {
|
| 2033 |
+
grid-template-rows: auto minmax(140px, 1fr) auto auto auto auto;
|
| 2034 |
+
align-content: stretch;
|
| 2035 |
+
overflow: hidden;
|
| 2036 |
+
}
|
| 2037 |
+
body.np-lyrics-open .np-stage {
|
| 2038 |
+
width: 100%;
|
| 2039 |
+
max-width: 100%;
|
| 2040 |
+
aspect-ratio: auto;
|
| 2041 |
+
max-height: none;
|
| 2042 |
+
height: 100%;
|
| 2043 |
+
border-radius: 8px;
|
| 2044 |
+
}
|
| 2045 |
+
|
| 2046 |
+
.np-back-btn {
|
| 2047 |
+
background: rgba(255, 255, 255, 0.08);
|
| 2048 |
+
border: 1px solid rgba(255, 255, 255, 0.10);
|
| 2049 |
+
padding: 0 12px 0 8px !important;
|
| 2050 |
+
min-width: auto !important;
|
| 2051 |
+
border-radius: 18px !important;
|
| 2052 |
+
height: 36px;
|
| 2053 |
+
}
|
| 2054 |
+
.np-back-btn span { display: inline !important; padding-right: 0 !important; }
|
| 2055 |
+
.np-back-btn:hover { background: rgba(255, 255, 255, 0.16); }
|
| 2056 |
+
|
| 2057 |
+
.np-lyrics {
|
| 2058 |
+
align-items: stretch;
|
| 2059 |
+
justify-content: center;
|
| 2060 |
+
padding: 28px 18px;
|
| 2061 |
+
overscroll-behavior: contain;
|
| 2062 |
+
}
|
| 2063 |
+
.np-lyrics-inner {
|
| 2064 |
+
width: 100%;
|
| 2065 |
+
max-width: 640px;
|
| 2066 |
+
height: 100%;
|
| 2067 |
+
margin: 0 auto;
|
| 2068 |
+
text-align: center;
|
| 2069 |
+
display: flex;
|
| 2070 |
+
flex-direction: column;
|
| 2071 |
+
min-height: 0;
|
| 2072 |
+
}
|
| 2073 |
+
.np-lyrics-inner h3 { text-align: center; }
|
| 2074 |
+
.np-lyrics-head { flex: 0 0 auto; justify-content: center; gap: 14px; position: relative; }
|
| 2075 |
+
.np-lyrics-head .np-panel-close { position: absolute; right: 0; top: 50%; transform: translateY(-50%); }
|
| 2076 |
+
.np-lyrics-body {
|
| 2077 |
+
flex: 1 1 0;
|
| 2078 |
+
min-height: 0;
|
| 2079 |
+
text-align: center;
|
| 2080 |
+
font-size: 1.45rem;
|
| 2081 |
+
line-height: 1.5;
|
| 2082 |
+
margin: 0;
|
| 2083 |
+
color: #fff;
|
| 2084 |
+
|
| 2085 |
+
padding: 45% 6px;
|
| 2086 |
+
overflow-y: auto;
|
| 2087 |
+
overflow-x: hidden;
|
| 2088 |
+
|
| 2089 |
+
-webkit-overflow-scrolling: touch;
|
| 2090 |
+
scrollbar-width: none;
|
| 2091 |
+
|
| 2092 |
+
-webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 14%, #000 86%, transparent 100%);
|
| 2093 |
+
mask-image: linear-gradient(180deg, transparent 0%, #000 14%, #000 86%, transparent 100%);
|
| 2094 |
+
}
|
| 2095 |
+
.np-lyrics-body::-webkit-scrollbar { width: 0; height: 0; display: none; }
|
| 2096 |
+
.np-lyrics-meta { flex: 0 0 auto; padding-top: 8px; }
|
| 2097 |
+
|
| 2098 |
+
.np-lyric-line {
|
| 2099 |
+
display: block;
|
| 2100 |
+
padding: 10px 6px;
|
| 2101 |
+
line-height: 1.35;
|
| 2102 |
+
font-size: 1.4rem;
|
| 2103 |
+
|
| 2104 |
+
font-weight: 500;
|
| 2105 |
+
letter-spacing: -0.005em;
|
| 2106 |
+
color: rgba(255, 255, 255, 0.55);
|
| 2107 |
+
transform: scale(0.96);
|
| 2108 |
+
transform-origin: center;
|
| 2109 |
+
transition: opacity 0.45s cubic-bezier(0.22, 1, 0.36, 1),
|
| 2110 |
+
transform 0.55s cubic-bezier(0.22, 1, 0.36, 1),
|
| 2111 |
+
color 0.4s cubic-bezier(0.22, 1, 0.36, 1),
|
| 2112 |
+
font-weight 0.25s ease,
|
| 2113 |
+
text-shadow 0.5s ease;
|
| 2114 |
+
text-align: center;
|
| 2115 |
+
will-change: transform, color;
|
| 2116 |
+
|
| 2117 |
+
max-width: 100%;
|
| 2118 |
+
word-break: break-word;
|
| 2119 |
+
overflow-wrap: anywhere;
|
| 2120 |
+
hyphens: auto;
|
| 2121 |
+
white-space: normal;
|
| 2122 |
+
}
|
| 2123 |
+
|
| 2124 |
+
.np-lyric-line.is-past {
|
| 2125 |
+
color: rgba(255, 255, 255, 0.32);
|
| 2126 |
+
font-weight: 500;
|
| 2127 |
+
}
|
| 2128 |
+
|
| 2129 |
+
.np-lyric-line.is-active {
|
| 2130 |
+
transform: scale(1.12);
|
| 2131 |
+
color: #ffffff;
|
| 2132 |
+
font-weight: 900;
|
| 2133 |
+
letter-spacing: 0;
|
| 2134 |
+
text-shadow: 0 0 22px rgba(167, 139, 250, 0.55),
|
| 2135 |
+
0 2px 14px rgba(0, 0, 0, 0.55);
|
| 2136 |
+
}
|
| 2137 |
+
|
| 2138 |
+
.np-lyric-line.is-active ~ .np-lyric-line {
|
| 2139 |
+
color: rgba(255, 255, 255, 0.55);
|
| 2140 |
+
font-weight: 500;
|
| 2141 |
+
transform: scale(0.96);
|
| 2142 |
+
}
|
| 2143 |
+
.np-lyric-line.is-loading,
|
| 2144 |
+
.np-lyric-line.is-empty {
|
| 2145 |
+
transform: none;
|
| 2146 |
+
font-style: italic;
|
| 2147 |
+
font-weight: 500;
|
| 2148 |
+
font-size: 1rem;
|
| 2149 |
+
color: rgba(255, 255, 255, 0.7);
|
| 2150 |
+
}
|
| 2151 |
+
|
| 2152 |
+
@media (max-width: 600px) {
|
| 2153 |
+
.np-lyrics { padding: 18px 10px; }
|
| 2154 |
+
.np-lyrics-body { font-size: 1.15rem; }
|
| 2155 |
+
.np-lyric-line { font-size: 1.15rem; padding: 8px 4px; }
|
| 2156 |
+
.np-lyric-line.is-active { transform: scale(1.08); }
|
| 2157 |
+
}
|
| 2158 |
+
|
| 2159 |
+
.np-info {
|
| 2160 |
+
background: linear-gradient(135deg, rgba(124, 58, 237, 0.10), rgba(255, 255, 255, 0.02));
|
| 2161 |
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
| 2162 |
+
border-radius: 14px;
|
| 2163 |
+
padding: 10px 14px;
|
| 2164 |
+
}
|
| 2165 |
+
.np-like {
|
| 2166 |
+
width: 44px !important;
|
| 2167 |
+
height: 44px !important;
|
| 2168 |
+
background: rgba(255, 255, 255, 0.06);
|
| 2169 |
+
border: 1px solid rgba(255, 255, 255, 0.10);
|
| 2170 |
+
color: rgba(255, 255, 255, 0.85);
|
| 2171 |
+
transition: color 0.18s ease, background 0.18s ease, transform 0.18s ease, border-color 0.18s ease, box-shadow 0.2s ease;
|
| 2172 |
+
}
|
| 2173 |
+
.np-like svg { width: 22px !important; height: 22px !important; transition: transform 0.18s ease; }
|
| 2174 |
+
.np-like:hover {
|
| 2175 |
+
background: rgba(244, 63, 94, 0.12);
|
| 2176 |
+
border-color: rgba(244, 63, 94, 0.45);
|
| 2177 |
+
color: #fff;
|
| 2178 |
+
transform: scale(1.06);
|
| 2179 |
+
}
|
| 2180 |
+
.np-like.is-liked {
|
| 2181 |
+
color: #fff;
|
| 2182 |
+
background: linear-gradient(135deg, #f43f5e, #ec4899);
|
| 2183 |
+
border-color: rgba(244, 63, 94, 0.65);
|
| 2184 |
+
box-shadow: 0 8px 22px rgba(244, 63, 94, 0.45);
|
| 2185 |
+
}
|
| 2186 |
+
.np-like.is-liked svg { fill: currentColor; transform: scale(1.05); }
|
| 2187 |
+
.np-like.just-liked { animation: npLikePop 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) 1; }
|
| 2188 |
+
@keyframes npLikePop {
|
| 2189 |
+
0% { transform: scale(1); }
|
| 2190 |
+
40% { transform: scale(1.25); }
|
| 2191 |
+
100% { transform: scale(1.06); }
|
| 2192 |
+
}
|
| 2193 |
+
@media (max-width: 600px) {
|
| 2194 |
+
.np-info { padding: 8px 12px; gap: 12px; }
|
| 2195 |
+
.np-like { width: 40px !important; height: 40px !important; }
|
| 2196 |
+
.np-like svg { width: 20px !important; height: 20px !important; }
|
| 2197 |
+
}
|
frontend/dist/music.html
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
| 6 |
+
<title>Music · anidoom</title>
|
| 7 |
+
<meta name="description" content="Listen to anime openings, endings and OSTs. Powered by AnimeThemes.">
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
| 11 |
+
<link rel="stylesheet" href="/music.css">
|
| 12 |
+
<style>
|
| 13 |
+
:root {
|
| 14 |
+
--header-h: 0px;
|
| 15 |
+
}
|
| 16 |
+
body {
|
| 17 |
+
font-family: 'Outfit', sans-serif;
|
| 18 |
+
margin: 0;
|
| 19 |
+
padding: 0;
|
| 20 |
+
}
|
| 21 |
+
</style>
|
| 22 |
+
</head>
|
| 23 |
+
|
| 24 |
+
<body class="music-body">
|
| 25 |
+
<main class="music-app" id="music-app">
|
| 26 |
+
<aside class="music-sidebar" id="music-sidebar">
|
| 27 |
+
<div class="ms-block">
|
| 28 |
+
<div class="ms-block-title">
|
| 29 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2h-4v-7h-6v7H5a2 2 0 0 1-2-2z"/></svg>
|
| 30 |
+
<span>Browse</span>
|
| 31 |
+
</div>
|
| 32 |
+
<a class="ms-link" data-route="#/" href="#/">
|
| 33 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20"/><path d="M12 2a14.5 14.5 0 0 1 0 20"/><line x1="2" y1="12" x2="22" y2="12"/></svg>
|
| 34 |
+
<span>Discover</span>
|
| 35 |
+
</a>
|
| 36 |
+
<a class="ms-link" data-route="#/recent" href="#/recent">
|
| 37 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
| 38 |
+
<span>Newest Themes</span>
|
| 39 |
+
</a>
|
| 40 |
+
<a class="ms-link" data-route="#/artists" href="#/artists">
|
| 41 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
| 42 |
+
<span>Artists</span>
|
| 43 |
+
</a>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<div class="ms-block">
|
| 47 |
+
<div class="ms-block-title">
|
| 48 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
| 49 |
+
<span>Library</span>
|
| 50 |
+
</div>
|
| 51 |
+
<a class="ms-link" data-route="#/liked" href="#/liked">
|
| 52 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
| 53 |
+
<span>Liked Songs</span>
|
| 54 |
+
</a>
|
| 55 |
+
<a class="ms-link" data-route="#/history" href="#/history">
|
| 56 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
|
| 57 |
+
<span>Recently Played</span>
|
| 58 |
+
</a>
|
| 59 |
+
</div>
|
| 60 |
+
|
| 61 |
+
<div class="ms-block">
|
| 62 |
+
<div class="ms-block-title">
|
| 63 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
|
| 64 |
+
<span>By Year</span>
|
| 65 |
+
</div>
|
| 66 |
+
<div class="ms-year-grid" id="ms-year-grid"></div>
|
| 67 |
+
</div>
|
| 68 |
+
</aside>
|
| 69 |
+
|
| 70 |
+
<section class="music-main" id="music-main">
|
| 71 |
+
<div class="ms-topbar">
|
| 72 |
+
<button class="ms-nav-btn" id="ms-back" aria-label="Back" disabled>
|
| 73 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
|
| 74 |
+
</button>
|
| 75 |
+
<button class="ms-nav-btn" id="ms-fwd" aria-label="Forward" disabled>
|
| 76 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
| 77 |
+
</button>
|
| 78 |
+
|
| 79 |
+
<div class="ms-search">
|
| 80 |
+
<span class="ms-search-icon">
|
| 81 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
| 82 |
+
</span>
|
| 83 |
+
<input id="ms-search-input" type="text" placeholder="What do you want to listen to?" autocomplete="off">
|
| 84 |
+
<button class="ms-search-clear" id="ms-search-clear" aria-label="Clear" hidden>
|
| 85 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
| 86 |
+
</button>
|
| 87 |
+
</div>
|
| 88 |
+
|
| 89 |
+
<button class="ms-sidebar-toggle" id="ms-sidebar-toggle" aria-label="Library">
|
| 90 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
| 91 |
+
</button>
|
| 92 |
+
</div>
|
| 93 |
+
|
| 94 |
+
<div class="ms-view" id="ms-view"></div>
|
| 95 |
+
</section>
|
| 96 |
+
</main>
|
| 97 |
+
|
| 98 |
+
<div class="ms-sidebar-backdrop" id="ms-sidebar-backdrop"></div>
|
| 99 |
+
|
| 100 |
+
<div class="player-bar" id="player-bar" data-empty="1">
|
| 101 |
+
<div class="pb-track">
|
| 102 |
+
<button class="pb-cover-btn" id="pb-expand" aria-label="Expand player">
|
| 103 |
+
<img class="pb-cover" id="pb-cover" alt="" referrerpolicy="no-referrer">
|
| 104 |
+
<span class="pb-cover-fallback" id="pb-cover-fallback" aria-hidden="true">
|
| 105 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
|
| 106 |
+
</span>
|
| 107 |
+
<span class="pb-cover-overlay">
|
| 108 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
|
| 109 |
+
</span>
|
| 110 |
+
</button>
|
| 111 |
+
<div class="pb-meta">
|
| 112 |
+
<button class="pb-title" id="pb-title" type="button">Pick a song to start</button>
|
| 113 |
+
<a class="pb-artist" id="pb-artist" href="#">—</a>
|
| 114 |
+
</div>
|
| 115 |
+
<button class="pb-like" id="pb-like" aria-label="Like">
|
| 116 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
| 117 |
+
</button>
|
| 118 |
+
<button class="pb-dismiss" id="pb-dismiss" aria-label="Hide player">
|
| 119 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
| 120 |
+
</button>
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
<div class="pb-center">
|
| 124 |
+
<div class="pb-controls">
|
| 125 |
+
<button class="pb-btn" id="pb-shuffle" aria-label="Shuffle">
|
| 126 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>
|
| 127 |
+
</button>
|
| 128 |
+
<button class="pb-btn" id="pb-prev" aria-label="Previous">
|
| 129 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
|
| 130 |
+
</button>
|
| 131 |
+
<button class="pb-play" id="pb-play" aria-label="Play">
|
| 132 |
+
<svg id="pb-play-svg" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
| 133 |
+
</button>
|
| 134 |
+
<button class="pb-btn" id="pb-next" aria-label="Next">
|
| 135 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zm10-12v12h2V6z"/></svg>
|
| 136 |
+
</button>
|
| 137 |
+
<button class="pb-btn" id="pb-loop" aria-label="Loop">
|
| 138 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
|
| 139 |
+
</button>
|
| 140 |
+
</div>
|
| 141 |
+
<div class="pb-progress">
|
| 142 |
+
<span class="pb-time" id="pb-cur">0:00</span>
|
| 143 |
+
<div class="pb-bar" id="pb-seek">
|
| 144 |
+
<div class="pb-bar-fill" id="pb-seek-fill"></div>
|
| 145 |
+
<div class="pb-bar-knob" id="pb-seek-knob"></div>
|
| 146 |
+
</div>
|
| 147 |
+
<span class="pb-time" id="pb-dur">0:00</span>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
|
| 151 |
+
<div class="pb-right">
|
| 152 |
+
<button class="pb-btn pb-mobile-play" id="pb-play-mobile" aria-label="Play">
|
| 153 |
+
<svg id="pb-play-mobile-svg" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
| 154 |
+
</button>
|
| 155 |
+
<button class="pb-btn" id="pb-mute" aria-label="Mute">
|
| 156 |
+
<svg id="pb-vol-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/></svg>
|
| 157 |
+
</button>
|
| 158 |
+
<div class="pb-vol" id="pb-vol">
|
| 159 |
+
<div class="pb-bar-fill" id="pb-vol-fill"></div>
|
| 160 |
+
<div class="pb-bar-knob" id="pb-vol-knob"></div>
|
| 161 |
+
</div>
|
| 162 |
+
</div>
|
| 163 |
+
|
| 164 |
+
<audio id="pb-audio" preload="metadata"></audio>
|
| 165 |
+
</div>
|
| 166 |
+
|
| 167 |
+
<div class="np-modal" id="np-modal" hidden>
|
| 168 |
+
<div class="np-bg" id="np-bg"></div>
|
| 169 |
+
<div class="np-inner">
|
| 170 |
+
<div class="np-header">
|
| 171 |
+
<button class="np-icon-btn np-back-btn" id="np-close" aria-label="Back">
|
| 172 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
|
| 173 |
+
<span>Back</span>
|
| 174 |
+
</button>
|
| 175 |
+
<div class="np-source">
|
| 176 |
+
<div class="np-eyebrow">Playing from</div>
|
| 177 |
+
<a class="np-source-name" id="np-source-name" href="#">—</a>
|
| 178 |
+
</div>
|
| 179 |
+
<div style="width:36px"></div>
|
| 180 |
+
</div>
|
| 181 |
+
|
| 182 |
+
<div class="np-stage" id="np-stage">
|
| 183 |
+
<span class="np-cover-fallback" id="np-cover-fallback" aria-hidden="true">
|
| 184 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
|
| 185 |
+
</span>
|
| 186 |
+
<img class="np-cover" id="np-cover" alt="" referrerpolicy="no-referrer">
|
| 187 |
+
<video class="np-video" id="np-video" playsinline preload="metadata" hidden></video>
|
| 188 |
+
<div class="np-loading" id="np-loading" hidden><div class="np-spinner"></div></div>
|
| 189 |
+
<div class="np-lyrics" id="np-lyrics" hidden>
|
| 190 |
+
<div class="np-lyrics-inner">
|
| 191 |
+
<div class="np-lyrics-head">
|
| 192 |
+
<h3>Lyrics</h3>
|
| 193 |
+
<button class="np-panel-close" id="np-lyrics-close" aria-label="Close lyrics">
|
| 194 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
| 195 |
+
</button>
|
| 196 |
+
</div>
|
| 197 |
+
<p class="np-lyrics-body" id="np-lyrics-body">Pick a song to load lyrics.</p>
|
| 198 |
+
<div class="np-lyrics-meta" id="np-lyrics-meta"></div>
|
| 199 |
+
</div>
|
| 200 |
+
</div>
|
| 201 |
+
<div class="np-queue" id="np-queue" hidden>
|
| 202 |
+
<div class="np-queue-head">
|
| 203 |
+
<h3>Queue</h3>
|
| 204 |
+
<span id="np-queue-count">0 tracks</span>
|
| 205 |
+
<button class="np-panel-close" id="np-queue-close" aria-label="Close queue">
|
| 206 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
| 207 |
+
</button>
|
| 208 |
+
</div>
|
| 209 |
+
<div class="np-queue-list" id="np-queue-list"></div>
|
| 210 |
+
</div>
|
| 211 |
+
</div>
|
| 212 |
+
|
| 213 |
+
<div class="np-info">
|
| 214 |
+
<div class="np-info-text">
|
| 215 |
+
<a class="np-title" id="np-title" href="#">—</a>
|
| 216 |
+
<a class="np-artist" id="np-artist" href="#">—</a>
|
| 217 |
+
</div>
|
| 218 |
+
<button class="np-like" id="np-like" aria-label="Like">
|
| 219 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
| 220 |
+
</button>
|
| 221 |
+
</div>
|
| 222 |
+
|
| 223 |
+
<div class="np-progress">
|
| 224 |
+
<div class="np-bar" id="np-seek">
|
| 225 |
+
<div class="np-bar-fill" id="np-seek-fill"></div>
|
| 226 |
+
<div class="np-bar-knob" id="np-seek-knob"></div>
|
| 227 |
+
</div>
|
| 228 |
+
<div class="np-times">
|
| 229 |
+
<span id="np-cur">0:00</span>
|
| 230 |
+
<span id="np-dur">0:00</span>
|
| 231 |
+
</div>
|
| 232 |
+
</div>
|
| 233 |
+
|
| 234 |
+
<div class="np-controls">
|
| 235 |
+
<button class="np-ctl" id="np-shuffle" aria-label="Shuffle">
|
| 236 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>
|
| 237 |
+
</button>
|
| 238 |
+
<button class="np-ctl" id="np-prev" aria-label="Previous">
|
| 239 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
|
| 240 |
+
</button>
|
| 241 |
+
<button class="np-play-big" id="np-play" aria-label="Play">
|
| 242 |
+
<svg id="np-play-svg" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
| 243 |
+
</button>
|
| 244 |
+
<button class="np-ctl" id="np-next" aria-label="Next">
|
| 245 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zm10-12v12h2V6z"/></svg>
|
| 246 |
+
</button>
|
| 247 |
+
<button class="np-ctl" id="np-loop" aria-label="Loop">
|
| 248 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
|
| 249 |
+
</button>
|
| 250 |
+
</div>
|
| 251 |
+
|
| 252 |
+
<div class="np-tools">
|
| 253 |
+
<button class="np-tool" id="np-video-btn" aria-label="Toggle video">
|
| 254 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg>
|
| 255 |
+
<span>Video</span>
|
| 256 |
+
</button>
|
| 257 |
+
<button class="np-tool" id="np-lyrics-btn" aria-label="Toggle lyrics">
|
| 258 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 10h12M4 14h16M4 18h8"/></svg>
|
| 259 |
+
<span>Lyrics</span>
|
| 260 |
+
</button>
|
| 261 |
+
<button class="np-tool" id="np-queue-btn" aria-label="Toggle queue">
|
| 262 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
|
| 263 |
+
<span>Queue</span>
|
| 264 |
+
</button>
|
| 265 |
+
</div>
|
| 266 |
+
</div>
|
| 267 |
+
</div>
|
| 268 |
+
|
| 269 |
+
<div id="toast-container"></div>
|
| 270 |
+
|
| 271 |
+
<script src="/music-api.js"></script>
|
| 272 |
+
<script src="/music.js"></script>
|
| 273 |
+
</body>
|
| 274 |
+
</html>
|
frontend/dist/music.js
ADDED
|
@@ -0,0 +1,1729 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
(function () {
|
| 2 |
+
const $ = (s, r) => (r || document).querySelector(s);
|
| 3 |
+
const $$ = (s, r) => Array.from((r || document).querySelectorAll(s));
|
| 4 |
+
|
| 5 |
+
const audio = $('#pb-audio');
|
| 6 |
+
const video = $('#np-video');
|
| 7 |
+
const view = $('#ms-view');
|
| 8 |
+
const playerBar = $('#player-bar');
|
| 9 |
+
const npModal = $('#np-modal');
|
| 10 |
+
|
| 11 |
+
let mediaMode = 'audio';
|
| 12 |
+
function activeMedia() { return mediaMode === 'video' ? video : audio; }
|
| 13 |
+
|
| 14 |
+
const STORE = {
|
| 15 |
+
liked: 'kv_music_liked',
|
| 16 |
+
history: 'kv_music_history',
|
| 17 |
+
vol: 'kv_music_vol',
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
const state = {
|
| 21 |
+
queue: [],
|
| 22 |
+
index: -1,
|
| 23 |
+
liked: new Set(JSON.parse(localStorage.getItem(STORE.liked) || '[]')),
|
| 24 |
+
history: JSON.parse(localStorage.getItem(STORE.history) || '[]'),
|
| 25 |
+
volume: parseFloat(localStorage.getItem(STORE.vol) || '0.8'),
|
| 26 |
+
muted: false,
|
| 27 |
+
loop: false,
|
| 28 |
+
shuffle: false,
|
| 29 |
+
navStack: [],
|
| 30 |
+
navFwd: [],
|
| 31 |
+
scrubbing: false,
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
+
const api = {
|
| 35 |
+
search: (q) => fetchAPI(`/themes/search?q=${encodeURIComponent(q)}`),
|
| 36 |
+
anime: (id) => fetchAPI(`/themes/anime/${encodeURIComponent(id)}`),
|
| 37 |
+
artist: (slug) => fetchAPI(`/themes/artist/${encodeURIComponent(slug)}`),
|
| 38 |
+
recent: () => fetchAPI('/themes/recent'),
|
| 39 |
+
year: (y) => fetchAPI(`/themes/year/${encodeURIComponent(y)}`),
|
| 40 |
+
artists: () => fetchAPI('/themes/top-artists'),
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
const esc = (s) => String(s == null ? '' : s).replace(/[<>&"']/g, c => ({
|
| 44 |
+
'<': '<', '>': '>', '&': '&', '"': '"', "'": ''',
|
| 45 |
+
}[c]));
|
| 46 |
+
|
| 47 |
+
const fmtTime = (sec) => {
|
| 48 |
+
if (!isFinite(sec) || sec < 0) sec = 0;
|
| 49 |
+
const m = Math.floor(sec / 60);
|
| 50 |
+
const s = Math.floor(sec % 60);
|
| 51 |
+
return `${m}:${s < 10 ? '0' : ''}${s}`;
|
| 52 |
+
};
|
| 53 |
+
|
| 54 |
+
const placeholderCover = (label) => {
|
| 55 |
+
const t = (label || '?').slice(0, 1).toUpperCase();
|
| 56 |
+
return `<div class="ms-card-cover-fallback"><span style="font-size:2.4rem;font-weight:900;letter-spacing:-0.04em;">${esc(t)}</span></div>`;
|
| 57 |
+
};
|
| 58 |
+
|
| 59 |
+
function coverImg(url, label, opts) {
|
| 60 |
+
opts = opts || {};
|
| 61 |
+
const cls = opts.cls || 'ms-card-cover';
|
| 62 |
+
if (!url) return placeholderCover(label || '');
|
| 63 |
+
return `<img class="${cls}" data-cover-fallback="${esc(label || '')}"`
|
| 64 |
+
+ ` src="${esc(url)}" alt="" loading="lazy" referrerpolicy="no-referrer">`;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
document.addEventListener('error', (e) => {
|
| 68 |
+
const img = e.target;
|
| 69 |
+
if (!(img instanceof HTMLImageElement)) return;
|
| 70 |
+
if (!img.dataset || img.dataset.coverFallback == null) return;
|
| 71 |
+
const label = img.dataset.coverFallback;
|
| 72 |
+
const wrap = img.parentNode;
|
| 73 |
+
if (!wrap) return;
|
| 74 |
+
|
| 75 |
+
const tmp = document.createElement('div');
|
| 76 |
+
tmp.innerHTML = placeholderCover(label);
|
| 77 |
+
const node = tmp.firstChild;
|
| 78 |
+
if (node) wrap.replaceChild(node, img);
|
| 79 |
+
}, true);
|
| 80 |
+
|
| 81 |
+
const coverUrl = (anime) => {
|
| 82 |
+
if (!anime) return '';
|
| 83 |
+
const imgs = anime.images || [];
|
| 84 |
+
const large = imgs.find(i => i.facet === 'Large Cover');
|
| 85 |
+
const small = imgs.find(i => i.facet === 'Small Cover');
|
| 86 |
+
return (large || small || imgs[0])?.link || '';
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
const artistImageUrl = (artist) => {
|
| 90 |
+
if (!artist) return '';
|
| 91 |
+
const imgs = artist.images || [];
|
| 92 |
+
const large = imgs.find(i => i.facet === 'Large Cover');
|
| 93 |
+
const small = imgs.find(i => i.facet === 'Small Cover');
|
| 94 |
+
return (large || small || imgs[0])?.link || '';
|
| 95 |
+
};
|
| 96 |
+
|
| 97 |
+
function trackFromTheme(anime, theme, animeOverride) {
|
| 98 |
+
const animeRef = animeOverride || theme.anime || anime;
|
| 99 |
+
if (!animeRef) return null;
|
| 100 |
+
const entries = theme.animethemeentries || [];
|
| 101 |
+
let pickedAudio = null, pickedVideo = null, pickedEntry = null;
|
| 102 |
+
for (const e of entries) {
|
| 103 |
+
for (const v of (e.videos || [])) {
|
| 104 |
+
if (v.audio && v.audio.link) {
|
| 105 |
+
pickedAudio = v.audio;
|
| 106 |
+
pickedVideo = v;
|
| 107 |
+
pickedEntry = e;
|
| 108 |
+
break;
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
if (pickedAudio) break;
|
| 112 |
+
}
|
| 113 |
+
if (!pickedAudio) return null;
|
| 114 |
+
const cover = coverUrl(animeRef) || (theme.song?.artists?.[0] && artistImageUrl(theme.song.artists[0])) || '';
|
| 115 |
+
const slug = theme.slug || `${theme.type || ''}${theme.sequence || ''}`;
|
| 116 |
+
return {
|
| 117 |
+
id: `${theme.id}-${pickedAudio.id}`,
|
| 118 |
+
themeId: theme.id,
|
| 119 |
+
title: theme.song?.title || slug || 'Untitled',
|
| 120 |
+
type: slug,
|
| 121 |
+
typeKind: theme.type || '',
|
| 122 |
+
artists: theme.song?.artists || [],
|
| 123 |
+
animeId: animeRef.id,
|
| 124 |
+
animeName: animeRef.name,
|
| 125 |
+
animeSlug: animeRef.slug,
|
| 126 |
+
cover,
|
| 127 |
+
audio: pickedAudio.link,
|
| 128 |
+
videoLink: pickedVideo?.link,
|
| 129 |
+
episodes: pickedEntry?.episodes || '',
|
| 130 |
+
};
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
function tracksFromAnime(anime) {
|
| 134 |
+
return (anime.animethemes || [])
|
| 135 |
+
.map(t => trackFromTheme(anime, t))
|
| 136 |
+
.filter(Boolean);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
function persistLiked() { localStorage.setItem(STORE.liked, JSON.stringify(Array.from(state.liked))); }
|
| 140 |
+
function persistHistory() { localStorage.setItem(STORE.history, JSON.stringify(state.history.slice(0, 60))); }
|
| 141 |
+
function persistVol() { localStorage.setItem(STORE.vol, String(state.volume)); }
|
| 142 |
+
|
| 143 |
+
function isLiked(track) { return state.liked.has(track.id); }
|
| 144 |
+
function toggleLike(track) {
|
| 145 |
+
if (state.liked.has(track.id)) {
|
| 146 |
+
state.liked.delete(track.id);
|
| 147 |
+
try { localStorage.removeItem(`kv_music_track_${track.id}`); } catch (_) { }
|
| 148 |
+
} else {
|
| 149 |
+
state.liked.add(track.id);
|
| 150 |
+
try { localStorage.setItem(`kv_music_track_${track.id}`, JSON.stringify(track)); } catch (_) { }
|
| 151 |
+
}
|
| 152 |
+
persistLiked();
|
| 153 |
+
updatePlayerLike();
|
| 154 |
+
$$(`.ms-track-like[data-tid="${CSS.escape(track.id)}"]`).forEach(b => {
|
| 155 |
+
b.classList.toggle('is-liked', state.liked.has(track.id));
|
| 156 |
+
});
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
function pushHistory(track) {
|
| 160 |
+
try { localStorage.setItem(`kv_music_track_${track.id}`, JSON.stringify(track)); } catch (_) { }
|
| 161 |
+
state.history = [track.id, ...state.history.filter(id => id !== track.id)].slice(0, 60);
|
| 162 |
+
persistHistory();
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
function loadStoredTracks(ids) {
|
| 166 |
+
const out = [];
|
| 167 |
+
for (const id of ids) {
|
| 168 |
+
try {
|
| 169 |
+
const raw = localStorage.getItem(`kv_music_track_${id}`);
|
| 170 |
+
if (raw) out.push(JSON.parse(raw));
|
| 171 |
+
} catch (_) { }
|
| 172 |
+
}
|
| 173 |
+
return out;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
function play(queue, idx) {
|
| 177 |
+
if (!queue || !queue.length) return;
|
| 178 |
+
state.queue = queue.slice();
|
| 179 |
+
state.index = Math.max(0, Math.min(idx || 0, state.queue.length - 1));
|
| 180 |
+
loadCurrent(true);
|
| 181 |
+
renderQueue();
|
| 182 |
+
|
| 183 |
+
if (window.innerWidth <= 720) openNP();
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
function jumpTo(idx) {
|
| 187 |
+
if (idx < 0 || idx >= state.queue.length) return;
|
| 188 |
+
state.index = idx;
|
| 189 |
+
loadCurrent(true);
|
| 190 |
+
renderQueue();
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
function renderQueue() {
|
| 194 |
+
const list = $('#np-queue-list');
|
| 195 |
+
const count = $('#np-queue-count');
|
| 196 |
+
if (!list || !count) return;
|
| 197 |
+
const q = state.queue;
|
| 198 |
+
count.textContent = q.length ? `${q.length} track${q.length > 1 ? 's' : ''}` : 'Empty';
|
| 199 |
+
if (!q.length) {
|
| 200 |
+
list.innerHTML = `<div class="np-queue-empty">Pick a song to start playing.</div>`;
|
| 201 |
+
return;
|
| 202 |
+
}
|
| 203 |
+
list.innerHTML = q.map((t, i) => `
|
| 204 |
+
<button class="np-queue-row${i === state.index ? ' is-current' : ''}" data-qjump="${i}">
|
| 205 |
+
<span class="np-queue-cover">
|
| 206 |
+
${t.cover ? `<img src="${esc(t.cover)}" alt="" referrerpolicy="no-referrer">` : ''}
|
| 207 |
+
${i === state.index ? '<span class="np-queue-eq"><i></i><i></i><i></i></span>' : ''}
|
| 208 |
+
</span>
|
| 209 |
+
<span class="np-queue-text">
|
| 210 |
+
<span class="np-queue-title">${esc(t.title)}</span>
|
| 211 |
+
<span class="np-queue-sub">${esc((t.artists || []).map(a => a.name).join(', ') || t.animeName || '')}</span>
|
| 212 |
+
</span>
|
| 213 |
+
<span class="np-queue-tag">${i < state.index ? 'Played' : (i === state.index ? 'Now' : 'Next')}</span>
|
| 214 |
+
</button>
|
| 215 |
+
`).join('');
|
| 216 |
+
$$('[data-qjump]', list).forEach(btn => {
|
| 217 |
+
btn.addEventListener('click', () => jumpTo(parseInt(btn.dataset.qjump, 10)));
|
| 218 |
+
});
|
| 219 |
+
|
| 220 |
+
const cur = list.querySelector('.is-current');
|
| 221 |
+
if (cur) cur.scrollIntoView({ block: 'nearest' });
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
function loadCurrent(autoplay) {
|
| 225 |
+
const t = state.queue[state.index];
|
| 226 |
+
if (!t) return;
|
| 227 |
+
playerBar.dataset.empty = '0';
|
| 228 |
+
|
| 229 |
+
document.body.classList.remove('pb-hidden');
|
| 230 |
+
|
| 231 |
+
if (mediaMode === 'video' && !t.videoLink) {
|
| 232 |
+
setVideoMode(false, false);
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
if (mediaMode === 'video') {
|
| 236 |
+
video.src = t.videoLink;
|
| 237 |
+
try { audio.removeAttribute('src'); audio.load(); } catch (_) { }
|
| 238 |
+
video.muted = state.muted;
|
| 239 |
+
video.volume = state.volume;
|
| 240 |
+
} else {
|
| 241 |
+
audio.src = t.audio;
|
| 242 |
+
try { video.removeAttribute('src'); video.load(); } catch (_) { }
|
| 243 |
+
audio.muted = state.muted;
|
| 244 |
+
audio.volume = state.volume;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
if (autoplay) {
|
| 248 |
+
const p = activeMedia().play();
|
| 249 |
+
if (p && p.catch) p.catch(() => { });
|
| 250 |
+
}
|
| 251 |
+
renderPlayerMeta(t);
|
| 252 |
+
pushHistory(t);
|
| 253 |
+
updateNowPlayingMarkers();
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
function setVideoMode(on, reload = true) {
|
| 257 |
+
const t = state.queue[state.index];
|
| 258 |
+
if (on && (!t || !t.videoLink)) {
|
| 259 |
+
toast && toast('No video available for this track');
|
| 260 |
+
return;
|
| 261 |
+
}
|
| 262 |
+
const wasPaused = activeMedia().paused;
|
| 263 |
+
const ct = activeMedia().currentTime || 0;
|
| 264 |
+
activeMedia().pause();
|
| 265 |
+
|
| 266 |
+
mediaMode = on ? 'video' : 'audio';
|
| 267 |
+
document.body.classList.toggle('np-video-on', mediaMode === 'video');
|
| 268 |
+
$('#np-video-btn').classList.toggle('is-on', mediaMode === 'video');
|
| 269 |
+
const npVid = $('#np-video');
|
| 270 |
+
const npCov = $('#np-cover');
|
| 271 |
+
if (npVid) npVid.hidden = mediaMode !== 'video';
|
| 272 |
+
if (npCov) npCov.hidden = mediaMode === 'video';
|
| 273 |
+
|
| 274 |
+
if (!t) return;
|
| 275 |
+
|
| 276 |
+
if (reload) {
|
| 277 |
+
if (mediaMode === 'video') {
|
| 278 |
+
video.src = t.videoLink;
|
| 279 |
+
try { audio.removeAttribute('src'); audio.load(); } catch (_) { }
|
| 280 |
+
} else {
|
| 281 |
+
audio.src = t.audio;
|
| 282 |
+
try { video.removeAttribute('src'); video.load(); } catch (_) { }
|
| 283 |
+
}
|
| 284 |
+
const m = activeMedia();
|
| 285 |
+
m.muted = state.muted;
|
| 286 |
+
m.volume = state.volume;
|
| 287 |
+
const onMeta = () => {
|
| 288 |
+
try { m.currentTime = ct; } catch (_) { }
|
| 289 |
+
if (!wasPaused) m.play().catch(() => { });
|
| 290 |
+
m.removeEventListener('loadedmetadata', onMeta);
|
| 291 |
+
};
|
| 292 |
+
m.addEventListener('loadedmetadata', onMeta);
|
| 293 |
+
}
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
function toast(msg) {
|
| 297 |
+
const c = $('#toast-container');
|
| 298 |
+
if (!c) return;
|
| 299 |
+
const el = document.createElement('div');
|
| 300 |
+
el.className = 'kv-toast';
|
| 301 |
+
el.textContent = msg;
|
| 302 |
+
c.appendChild(el);
|
| 303 |
+
setTimeout(() => el.classList.add('show'), 10);
|
| 304 |
+
setTimeout(() => { el.classList.remove('show'); setTimeout(() => el.remove(), 300); }, 2200);
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
function next(auto) {
|
| 308 |
+
if (!state.queue.length) return;
|
| 309 |
+
if (state.shuffle && state.queue.length > 1) {
|
| 310 |
+
let n;
|
| 311 |
+
do { n = Math.floor(Math.random() * state.queue.length); } while (n === state.index);
|
| 312 |
+
state.index = n;
|
| 313 |
+
} else {
|
| 314 |
+
state.index = state.index + 1;
|
| 315 |
+
if (state.index >= state.queue.length) {
|
| 316 |
+
if (state.loop || !auto) state.index = 0;
|
| 317 |
+
else { state.index = state.queue.length - 1; activeMedia().pause(); return; }
|
| 318 |
+
}
|
| 319 |
+
}
|
| 320 |
+
loadCurrent(true);
|
| 321 |
+
renderQueue();
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
function prev() {
|
| 325 |
+
if (!state.queue.length) return;
|
| 326 |
+
if (activeMedia().currentTime > 3) { activeMedia().currentTime = 0; return; }
|
| 327 |
+
state.index = (state.index - 1 + state.queue.length) % state.queue.length;
|
| 328 |
+
loadCurrent(true);
|
| 329 |
+
renderQueue();
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
function togglePlay() {
|
| 333 |
+
if (playerBar.dataset.empty === '1') return;
|
| 334 |
+
const m = activeMedia();
|
| 335 |
+
if (m.paused) m.play().catch(() => { });
|
| 336 |
+
else m.pause();
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
function setCoverImg(imgEl, btnEl, url) {
|
| 340 |
+
if (!imgEl) return;
|
| 341 |
+
if (url) {
|
| 342 |
+
imgEl.classList.remove('is-failed');
|
| 343 |
+
if (imgEl.getAttribute('src') !== url) imgEl.src = url;
|
| 344 |
+
if (btnEl) btnEl.classList.add('has-cover');
|
| 345 |
+
} else {
|
| 346 |
+
imgEl.removeAttribute('src');
|
| 347 |
+
imgEl.classList.add('is-failed');
|
| 348 |
+
if (btnEl) btnEl.classList.remove('has-cover');
|
| 349 |
+
}
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
function renderPlayerMeta(t) {
|
| 353 |
+
const pbCover = $('#pb-cover');
|
| 354 |
+
const pbCoverBtn = $('#pb-expand');
|
| 355 |
+
setCoverImg(pbCover, pbCoverBtn, t.cover || '');
|
| 356 |
+
const titleEl = $('#pb-title');
|
| 357 |
+
titleEl.textContent = t.title;
|
| 358 |
+
titleEl.dataset.animeId = t.animeId || '';
|
| 359 |
+
const artistEl = $('#pb-artist');
|
| 360 |
+
const artistsLabel = (t.artists && t.artists.length)
|
| 361 |
+
? t.artists.map(a => a.name).join(', ')
|
| 362 |
+
: (t.animeName || '—');
|
| 363 |
+
if (t.artists && t.artists.length) {
|
| 364 |
+
const first = t.artists[0];
|
| 365 |
+
artistEl.textContent = artistsLabel;
|
| 366 |
+
artistEl.href = `#/artist/${encodeURIComponent(first.slug || first.id)}`;
|
| 367 |
+
} else {
|
| 368 |
+
artistEl.textContent = artistsLabel;
|
| 369 |
+
artistEl.href = `#/anime/${encodeURIComponent(t.animeId)}`;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
const npCover = $('#np-cover');
|
| 373 |
+
const npTitle = $('#np-title');
|
| 374 |
+
const npArtist = $('#np-artist');
|
| 375 |
+
const npSource = $('#np-source-name');
|
| 376 |
+
const npBg = $('#np-bg');
|
| 377 |
+
setCoverImg(npCover, null, t.cover || '');
|
| 378 |
+
if (npTitle) { npTitle.textContent = t.title; npTitle.href = `#/anime/${encodeURIComponent(t.animeId)}`; }
|
| 379 |
+
if (npArtist) {
|
| 380 |
+
npArtist.textContent = artistsLabel;
|
| 381 |
+
const first = (t.artists || [])[0];
|
| 382 |
+
npArtist.href = first ? `#/artist/${encodeURIComponent(first.slug || first.id)}` : `#/anime/${encodeURIComponent(t.animeId)}`;
|
| 383 |
+
}
|
| 384 |
+
if (npSource) { npSource.textContent = t.animeName || '—'; npSource.href = `#/anime/${encodeURIComponent(t.animeId)}`; }
|
| 385 |
+
if (npBg && t.cover) npBg.style.backgroundImage = `url("${t.cover}")`;
|
| 386 |
+
|
| 387 |
+
const vidBtn = $('#np-video-btn');
|
| 388 |
+
if (vidBtn) vidBtn.classList.toggle('is-disabled', !t.videoLink);
|
| 389 |
+
|
| 390 |
+
const lyricsMeta = $('#np-lyrics-meta');
|
| 391 |
+
if (lyricsMeta) {
|
| 392 |
+
lyricsMeta.innerHTML = `
|
| 393 |
+
<div><span>Anime</span><strong>${esc(t.animeName || '—')}</strong></div>
|
| 394 |
+
<div><span>Type</span><strong>${esc(t.type || '—')}</strong></div>
|
| 395 |
+
<div><span>Episodes</span><strong>${esc(t.episodes || '—')}</strong></div>
|
| 396 |
+
<div><span>Artists</span><strong>${esc((t.artists || []).map(a => a.name).join(', ') || '—')}</strong></div>
|
| 397 |
+
`;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
_syncedLines = [];
|
| 401 |
+
_activeLyricIdx = -1;
|
| 402 |
+
const lyricsBody = $('#np-lyrics-body');
|
| 403 |
+
if (lyricsBody) {
|
| 404 |
+
lyricsBody.dataset.trackId = t.id;
|
| 405 |
+
lyricsBody.innerHTML = '<span class="np-lyric-line is-loading">Loading lyrics…</span>';
|
| 406 |
+
}
|
| 407 |
+
fetchLyricsFor(t);
|
| 408 |
+
|
| 409 |
+
updatePlayerLike();
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
let _lyricsCache = {};
|
| 413 |
+
let _syncedLines = [];
|
| 414 |
+
let _rawSyncedLines = [];
|
| 415 |
+
let _lyricsDuration = 0;
|
| 416 |
+
let _activeLyricIdx = -1;
|
| 417 |
+
let _lyricRaf = null;
|
| 418 |
+
|
| 419 |
+
function parseLRC(lrc) {
|
| 420 |
+
const out = [];
|
| 421 |
+
const lineRe = /^((?:\[\d+:\d+(?:\.\d+)?\])+)(.*)$/;
|
| 422 |
+
const tagRe = /\[(\d+):(\d+(?:\.\d+)?)\]/g;
|
| 423 |
+
for (const raw of String(lrc).split(/\r?\n/)) {
|
| 424 |
+
const m = raw.match(lineRe);
|
| 425 |
+
if (!m) continue;
|
| 426 |
+
const text = m[2].trim();
|
| 427 |
+
let tag;
|
| 428 |
+
while ((tag = tagRe.exec(m[1])) !== null) {
|
| 429 |
+
const t = parseInt(tag[1], 10) * 60 + parseFloat(tag[2]);
|
| 430 |
+
out.push({ t, text });
|
| 431 |
+
}
|
| 432 |
+
}
|
| 433 |
+
out.sort((a, b) => a.t - b.t);
|
| 434 |
+
return out;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
function renderPlainLyrics(text) {
|
| 438 |
+
const body = $('#np-lyrics-body');
|
| 439 |
+
if (!body) return;
|
| 440 |
+
if (!text || !text.trim()) {
|
| 441 |
+
body.innerHTML = '<span class="np-lyric-line is-empty">We couldn\'t find lyrics for this track yet.</span>';
|
| 442 |
+
return;
|
| 443 |
+
}
|
| 444 |
+
const lines = text.split(/\r?\n/);
|
| 445 |
+
body.innerHTML = lines
|
| 446 |
+
.map(l => `<div class="np-lyric-line">${esc(l) || ' '}</div>`)
|
| 447 |
+
.join('');
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
function renderSyncedLyrics(lines) {
|
| 451 |
+
const body = $('#np-lyrics-body');
|
| 452 |
+
if (!body) return;
|
| 453 |
+
|
| 454 |
+
body.style.transform = '';
|
| 455 |
+
body.scrollTop = 0;
|
| 456 |
+
body.innerHTML = lines
|
| 457 |
+
.map((l, i) => `<div class="np-lyric-line" data-i="${i}">${esc(l.text) || ' '}</div>`)
|
| 458 |
+
.join('');
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
function recomputeSyncedLines() {
|
| 462 |
+
if (!_rawSyncedLines.length) { _syncedLines = []; return; }
|
| 463 |
+
_syncedLines = _rawSyncedLines.slice();
|
| 464 |
+
_activeLyricIdx = -1;
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
function updateLyricsHighlight(currentTime) {
|
| 468 |
+
if (!_syncedLines.length) return;
|
| 469 |
+
|
| 470 |
+
let lo = 0, hi = _syncedLines.length - 1, idx = -1;
|
| 471 |
+
while (lo <= hi) {
|
| 472 |
+
const mid = (lo + hi) >> 1;
|
| 473 |
+
if (_syncedLines[mid].t <= currentTime) { idx = mid; lo = mid + 1; }
|
| 474 |
+
else hi = mid - 1;
|
| 475 |
+
}
|
| 476 |
+
if (idx === _activeLyricIdx) return;
|
| 477 |
+
_activeLyricIdx = idx;
|
| 478 |
+
const body = $('#np-lyrics-body');
|
| 479 |
+
if (!body) return;
|
| 480 |
+
const lines = body.querySelectorAll('.np-lyric-line');
|
| 481 |
+
lines.forEach((el, i) => {
|
| 482 |
+
el.classList.toggle('is-active', i === idx);
|
| 483 |
+
el.classList.toggle('is-past', i < idx);
|
| 484 |
+
});
|
| 485 |
+
if (idx < 0) return;
|
| 486 |
+
const active = lines[idx];
|
| 487 |
+
if (!active) return;
|
| 488 |
+
|
| 489 |
+
const viewH = body.clientHeight;
|
| 490 |
+
if (viewH <= 0) return;
|
| 491 |
+
|
| 492 |
+
const ACTIVE_RATIO = 0.42;
|
| 493 |
+
const target = active.offsetTop + (active.offsetHeight / 2) - (viewH * ACTIVE_RATIO);
|
| 494 |
+
scrollLyricsTo(body, Math.max(0, target));
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
let _lyricScrollRaf = 0;
|
| 498 |
+
function scrollLyricsTo(body, target) {
|
| 499 |
+
if (!body) return;
|
| 500 |
+
target = Math.max(0, Math.min(target, body.scrollHeight - body.clientHeight));
|
| 501 |
+
if (_lyricScrollRaf) cancelAnimationFrame(_lyricScrollRaf);
|
| 502 |
+
const start = body.scrollTop;
|
| 503 |
+
const delta = target - start;
|
| 504 |
+
if (Math.abs(delta) < 0.5) { body.scrollTop = target; return; }
|
| 505 |
+
|
| 506 |
+
const dist = Math.abs(delta);
|
| 507 |
+
const dur = Math.min(900, Math.max(280, dist * 0.9));
|
| 508 |
+
const t0 = performance.now();
|
| 509 |
+
const ease = (x) => 1 - Math.pow(1 - x, 3);
|
| 510 |
+
const step = (now) => {
|
| 511 |
+
const k = Math.min(1, (now - t0) / dur);
|
| 512 |
+
body.scrollTop = start + delta * ease(k);
|
| 513 |
+
if (k < 1) _lyricScrollRaf = requestAnimationFrame(step);
|
| 514 |
+
else _lyricScrollRaf = 0;
|
| 515 |
+
};
|
| 516 |
+
_lyricScrollRaf = requestAnimationFrame(step);
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
function cleanLyricTitle(raw) {
|
| 520 |
+
if (!raw) return '';
|
| 521 |
+
let s = String(raw);
|
| 522 |
+
|
| 523 |
+
s = s.replace(/^\s*(?:OP|ED|IN)\s*\d*\s*[-–:]\s*/i, '');
|
| 524 |
+
s = s.replace(/^\s*(?:Opening|Ending|Insert\s*Song|Theme)\s*\d*\s*[-–:]\s*/i, '');
|
| 525 |
+
|
| 526 |
+
s = s.replace(/\([^)]*\)/g, '');
|
| 527 |
+
s = s.replace(/\[[^\]]*\]/g, '');
|
| 528 |
+
|
| 529 |
+
s = s.replace(/^[\s"'“”『「]+|[\s"'“”』」]+$/g, '');
|
| 530 |
+
return s.trim();
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
function waitForAudioDuration(m, timeoutMs) {
|
| 534 |
+
return new Promise((resolve) => {
|
| 535 |
+
if (!m) return resolve(0);
|
| 536 |
+
if (isFinite(m.duration) && m.duration > 0) return resolve(m.duration);
|
| 537 |
+
let done = false;
|
| 538 |
+
const finish = (v) => {
|
| 539 |
+
if (done) return;
|
| 540 |
+
done = true;
|
| 541 |
+
m.removeEventListener('loadedmetadata', onMeta);
|
| 542 |
+
m.removeEventListener('durationchange', onMeta);
|
| 543 |
+
resolve(v);
|
| 544 |
+
};
|
| 545 |
+
const onMeta = () => {
|
| 546 |
+
if (isFinite(m.duration) && m.duration > 0) finish(m.duration);
|
| 547 |
+
};
|
| 548 |
+
m.addEventListener('loadedmetadata', onMeta);
|
| 549 |
+
m.addEventListener('durationchange', onMeta);
|
| 550 |
+
setTimeout(() => finish(0), timeoutMs || 4000);
|
| 551 |
+
});
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
async function fetchLyricsFor(track) {
|
| 555 |
+
if (!track) return;
|
| 556 |
+
const lyricsBody = $('#np-lyrics-body');
|
| 557 |
+
if (!lyricsBody) return;
|
| 558 |
+
const key = track.id;
|
| 559 |
+
const apply = (data) => {
|
| 560 |
+
if (lyricsBody.dataset.trackId !== key) return;
|
| 561 |
+
const synced = data && data.synced;
|
| 562 |
+
const plain = data && data.lyrics;
|
| 563 |
+
_lyricsDuration = (data && Number(data.duration)) || 0;
|
| 564 |
+
if (synced) {
|
| 565 |
+
_rawSyncedLines = parseLRC(synced).filter(l => l.text || l.text === '');
|
| 566 |
+
if (_rawSyncedLines.length) {
|
| 567 |
+
recomputeSyncedLines();
|
| 568 |
+
renderSyncedLyrics(_syncedLines);
|
| 569 |
+
return;
|
| 570 |
+
}
|
| 571 |
+
}
|
| 572 |
+
_rawSyncedLines = [];
|
| 573 |
+
_syncedLines = [];
|
| 574 |
+
renderPlainLyrics(plain || (synced ? String(synced).replace(/^\[\d+:\d+(?:\.\d+)?\]\s?/gm, '') : ''));
|
| 575 |
+
};
|
| 576 |
+
|
| 577 |
+
const audioDur = await waitForAudioDuration(activeMedia(), 4000);
|
| 578 |
+
if (lyricsBody.dataset.trackId !== key) return;
|
| 579 |
+
|
| 580 |
+
const durBucket = audioDur ? Math.round(audioDur / 5) * 5 : 0;
|
| 581 |
+
const cacheKey = `${key}|${durBucket}`;
|
| 582 |
+
if (_lyricsCache[cacheKey] != null) { apply(_lyricsCache[cacheKey]); return; }
|
| 583 |
+
|
| 584 |
+
const title = cleanLyricTitle(track.title || '');
|
| 585 |
+
|
| 586 |
+
const firstArtist = ((track.artists || [])[0] || {}).name || '';
|
| 587 |
+
try {
|
| 588 |
+
const durQs = audioDur ? `&duration=${audioDur.toFixed(2)}` : '';
|
| 589 |
+
const url = `/api/lyrics?title=${encodeURIComponent(title)}&artist=${encodeURIComponent(firstArtist)}${durQs}`;
|
| 590 |
+
const res = await fetch(url);
|
| 591 |
+
const data = await res.json();
|
| 592 |
+
_lyricsCache[cacheKey] = data;
|
| 593 |
+
apply(data);
|
| 594 |
+
} catch (_) {
|
| 595 |
+
apply({});
|
| 596 |
+
}
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
function updatePlayerLike() {
|
| 600 |
+
const t = state.queue[state.index];
|
| 601 |
+
const btn = $('#pb-like');
|
| 602 |
+
const npBtn = $('#np-like');
|
| 603 |
+
const liked = !!(t && state.liked.has(t.id));
|
| 604 |
+
if (btn) btn.classList.toggle('is-liked', liked);
|
| 605 |
+
if (npBtn) npBtn.classList.toggle('is-liked', liked);
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
function updateNowPlayingMarkers() {
|
| 609 |
+
const t = state.queue[state.index];
|
| 610 |
+
$$('.ms-track').forEach(row => {
|
| 611 |
+
row.classList.toggle('is-playing', !!(t && row.dataset.tid === t.id));
|
| 612 |
+
});
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
function setPlayUI(playing) {
|
| 616 |
+
const pause = '<path d="M6 5h4v14H6zm8 0h4v14h-4z"/>';
|
| 617 |
+
const play = '<path d="M8 5v14l11-7z"/>';
|
| 618 |
+
const ic = playing ? pause : play;
|
| 619 |
+
const set = (id) => {
|
| 620 |
+
const el = $('#' + id);
|
| 621 |
+
if (el) el.outerHTML = `<svg id="${id}" viewBox="0 0 24 24" fill="currentColor">${ic}</svg>`;
|
| 622 |
+
};
|
| 623 |
+
set('pb-play-svg');
|
| 624 |
+
set('pb-play-mobile-svg');
|
| 625 |
+
set('np-play-svg');
|
| 626 |
+
|
| 627 |
+
document.body.classList.toggle('is-playing', !!playing);
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
function openNP() {
|
| 631 |
+
if (playerBar.dataset.empty === '1') return;
|
| 632 |
+
npModal.hidden = false;
|
| 633 |
+
document.body.classList.add('np-open');
|
| 634 |
+
|
| 635 |
+
$('#np-shuffle').classList.toggle('is-on', state.shuffle);
|
| 636 |
+
$('#np-loop').classList.toggle('is-on', state.loop);
|
| 637 |
+
$('#np-video-btn').classList.toggle('is-on', mediaMode === 'video');
|
| 638 |
+
setSeek();
|
| 639 |
+
updatePlayerLike();
|
| 640 |
+
}
|
| 641 |
+
function closeNP() {
|
| 642 |
+
npModal.hidden = true;
|
| 643 |
+
document.body.classList.remove('np-open');
|
| 644 |
+
document.body.classList.remove('np-lyrics-open');
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
function setVolUI() {
|
| 648 |
+
const v = state.muted ? 0 : state.volume;
|
| 649 |
+
$('#pb-vol-fill').style.width = (v * 100) + '%';
|
| 650 |
+
const knob = $('#pb-vol-knob');
|
| 651 |
+
if (knob) knob.style.left = (v * 100) + '%';
|
| 652 |
+
const svg = $('#pb-vol-svg');
|
| 653 |
+
if (!svg) return;
|
| 654 |
+
let path;
|
| 655 |
+
if (v === 0) path = '<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/>';
|
| 656 |
+
else if (v < 0.5) path = '<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>';
|
| 657 |
+
else path = '<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>';
|
| 658 |
+
svg.innerHTML = path;
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
function setSeek(t) {
|
| 662 |
+
const m = activeMedia();
|
| 663 |
+
const dur = m.duration;
|
| 664 |
+
const cur = t == null ? m.currentTime : t;
|
| 665 |
+
$('#pb-cur').textContent = fmtTime(cur);
|
| 666 |
+
$('#pb-dur').textContent = fmtTime(dur);
|
| 667 |
+
const pct = dur ? (cur / dur) * 100 : 0;
|
| 668 |
+
$('#pb-seek-fill').style.width = pct + '%';
|
| 669 |
+
const knob = $('#pb-seek-knob');
|
| 670 |
+
if (knob) knob.style.left = pct + '%';
|
| 671 |
+
|
| 672 |
+
const npCur = $('#np-cur'), npDur = $('#np-dur'), npFill = $('#np-seek-fill'), npKnob = $('#np-seek-knob');
|
| 673 |
+
if (npCur) npCur.textContent = fmtTime(cur);
|
| 674 |
+
if (npDur) npDur.textContent = fmtTime(dur);
|
| 675 |
+
if (npFill) npFill.style.width = pct + '%';
|
| 676 |
+
if (npKnob) npKnob.style.left = pct + '%';
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
function bindBar(bar, onChange) {
|
| 680 |
+
let pending = null;
|
| 681 |
+
let rafId = 0;
|
| 682 |
+
const flush = () => {
|
| 683 |
+
rafId = 0;
|
| 684 |
+
if (pending != null) { onChange(pending); pending = null; }
|
| 685 |
+
};
|
| 686 |
+
const schedule = (pct) => {
|
| 687 |
+
pending = pct;
|
| 688 |
+
if (!rafId) rafId = requestAnimationFrame(flush);
|
| 689 |
+
};
|
| 690 |
+
const setFromEvent = (e) => {
|
| 691 |
+
const r = bar.getBoundingClientRect();
|
| 692 |
+
const x = (e.touches ? e.touches[0].clientX : e.clientX) - r.left;
|
| 693 |
+
const pct = Math.max(0, Math.min(1, x / r.width));
|
| 694 |
+
schedule(pct);
|
| 695 |
+
};
|
| 696 |
+
let dragging = false;
|
| 697 |
+
const onUp = () => {
|
| 698 |
+
dragging = false;
|
| 699 |
+
state.scrubbing = false;
|
| 700 |
+
document.removeEventListener('mousemove', onMove);
|
| 701 |
+
document.removeEventListener('touchmove', onMove);
|
| 702 |
+
document.removeEventListener('mouseup', onUp);
|
| 703 |
+
document.removeEventListener('touchend', onUp);
|
| 704 |
+
if (rafId) { cancelAnimationFrame(rafId); rafId = 0; flush(); }
|
| 705 |
+
};
|
| 706 |
+
const onMove = (e) => { if (dragging) setFromEvent(e); };
|
| 707 |
+
bar.addEventListener('mousedown', (e) => {
|
| 708 |
+
dragging = true; state.scrubbing = true; setFromEvent(e);
|
| 709 |
+
document.addEventListener('mousemove', onMove);
|
| 710 |
+
document.addEventListener('mouseup', onUp);
|
| 711 |
+
});
|
| 712 |
+
bar.addEventListener('touchstart', (e) => {
|
| 713 |
+
dragging = true; state.scrubbing = true; setFromEvent(e);
|
| 714 |
+
document.addEventListener('touchmove', onMove, { passive: true });
|
| 715 |
+
document.addEventListener('touchend', onUp);
|
| 716 |
+
}, { passive: true });
|
| 717 |
+
bar.addEventListener('click', setFromEvent);
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
let _stageSpinnerTimer = 0;
|
| 721 |
+
function showStageLoading(on) {
|
| 722 |
+
const el = $('#np-loading');
|
| 723 |
+
if (!el) return;
|
| 724 |
+
if (on) {
|
| 725 |
+
if (_stageSpinnerTimer) return;
|
| 726 |
+
_stageSpinnerTimer = setTimeout(() => {
|
| 727 |
+
_stageSpinnerTimer = 0;
|
| 728 |
+
el.hidden = false;
|
| 729 |
+
}, 350);
|
| 730 |
+
} else {
|
| 731 |
+
if (_stageSpinnerTimer) { clearTimeout(_stageSpinnerTimer); _stageSpinnerTimer = 0; }
|
| 732 |
+
el.hidden = true;
|
| 733 |
+
}
|
| 734 |
+
}
|
| 735 |
+
|
| 736 |
+
function navigate(hash) {
|
| 737 |
+
if (location.hash !== hash) location.hash = hash;
|
| 738 |
+
else handleRoute();
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
function handleRoute() {
|
| 742 |
+
const hash = location.hash || '#/';
|
| 743 |
+
if (state.navStack[state.navStack.length - 1] !== hash) {
|
| 744 |
+
state.navStack.push(hash);
|
| 745 |
+
state.navFwd = [];
|
| 746 |
+
}
|
| 747 |
+
$('#ms-back').disabled = state.navStack.length <= 1;
|
| 748 |
+
$('#ms-fwd').disabled = state.navFwd.length === 0;
|
| 749 |
+
|
| 750 |
+
$$('.ms-link').forEach(l => {
|
| 751 |
+
l.classList.toggle('active', l.dataset.route === hash || (hash === '#/' && l.dataset.route === '#/'));
|
| 752 |
+
});
|
| 753 |
+
|
| 754 |
+
if (hash.startsWith('#/search?')) {
|
| 755 |
+
const q = decodeURIComponent(hash.split('q=')[1] || '');
|
| 756 |
+
$('#ms-search-input').value = q;
|
| 757 |
+
$('#ms-search-clear').hidden = !q;
|
| 758 |
+
renderSearch(q);
|
| 759 |
+
} else if (hash.startsWith('#/anime/')) {
|
| 760 |
+
renderAnime(decodeURIComponent(hash.split('/')[2] || ''));
|
| 761 |
+
} else if (hash.startsWith('#/artist/')) {
|
| 762 |
+
renderArtist(decodeURIComponent(hash.split('/')[2] || ''));
|
| 763 |
+
} else if (hash.startsWith('#/year/')) {
|
| 764 |
+
renderYear(hash.split('/')[2]);
|
| 765 |
+
} else if (hash === '#/recent') renderRecent();
|
| 766 |
+
else if (hash === '#/liked') renderLiked();
|
| 767 |
+
else if (hash === '#/history') renderHistory();
|
| 768 |
+
else if (hash === '#/artists') renderTopArtists();
|
| 769 |
+
else renderHome();
|
| 770 |
+
|
| 771 |
+
view.scrollTo({ top: 0 });
|
| 772 |
+
$('#music-main').scrollTo({ top: 0 });
|
| 773 |
+
closeSidebarIfMobile();
|
| 774 |
+
}
|
| 775 |
+
|
| 776 |
+
function loadingHTML(label) {
|
| 777 |
+
return `<div class="ms-loading"><div class="ms-spinner"></div><div>${esc(label || 'Loading…')}</div></div>`;
|
| 778 |
+
}
|
| 779 |
+
|
| 780 |
+
function emptyHTML(title, sub) {
|
| 781 |
+
return `<div class="ms-empty">
|
| 782 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
|
| 783 |
+
<h3>${esc(title)}</h3>
|
| 784 |
+
<p>${esc(sub || '')}</p>
|
| 785 |
+
</div>`;
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
function animeCardHTML(anime) {
|
| 789 |
+
const cover = coverUrl(anime);
|
| 790 |
+
const themesCount = (anime.animethemes || []).length;
|
| 791 |
+
const sub = themesCount ? `${themesCount} theme${themesCount > 1 ? 's' : ''} · ${anime.year || ''}` : `${anime.year || ''} · ${anime.media_format || ''}`;
|
| 792 |
+
return `<a class="ms-card" data-anime-id="${esc(anime.id)}" href="#/anime/${encodeURIComponent(anime.id)}">
|
| 793 |
+
<div class="ms-card-cover-wrap">
|
| 794 |
+
${coverImg(cover, anime.name)}
|
| 795 |
+
<button class="ms-card-play" data-play-anime="${esc(anime.id)}" aria-label="Play">
|
| 796 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
| 797 |
+
</button>
|
| 798 |
+
</div>
|
| 799 |
+
<div class="ms-card-title">${esc(anime.name)}</div>
|
| 800 |
+
<div class="ms-card-sub">${esc(sub)}</div>
|
| 801 |
+
</a>`;
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
function artistCardHTML(artist) {
|
| 805 |
+
const img = artistImageUrl(artist);
|
| 806 |
+
return `<a class="ms-card is-artist" href="#/artist/${encodeURIComponent(artist.slug || artist.id)}">
|
| 807 |
+
<div class="ms-card-cover-wrap">
|
| 808 |
+
${coverImg(img, artist.name)}
|
| 809 |
+
</div>
|
| 810 |
+
<div class="ms-card-title">${esc(artist.name)}</div>
|
| 811 |
+
<div class="ms-card-sub">Artist</div>
|
| 812 |
+
</a>`;
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
function songCardHTML(song) {
|
| 816 |
+
const themes = song.animethemes || [];
|
| 817 |
+
const firstAnime = themes[0]?.anime;
|
| 818 |
+
const artists = (song.artists || []).map(a => a.name).join(', ');
|
| 819 |
+
const href = firstAnime ? `#/anime/${encodeURIComponent(firstAnime.id)}` : '#/';
|
| 820 |
+
return `<a class="ms-card" href="${href}">
|
| 821 |
+
<div class="ms-card-cover-wrap">
|
| 822 |
+
${placeholderCover(song.title)}
|
| 823 |
+
${firstAnime ? `<button class="ms-card-play" data-play-song-anime="${esc(firstAnime.id)}" data-song-theme="${esc(themes[0].id)}" aria-label="Play">
|
| 824 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
| 825 |
+
</button>` : ''}
|
| 826 |
+
</div>
|
| 827 |
+
<div class="ms-card-title">${esc(song.title)}</div>
|
| 828 |
+
<div class="ms-card-sub">${esc(artists || (firstAnime?.name || ''))}</div>
|
| 829 |
+
</a>`;
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
function bindCardClicks(scope) {
|
| 833 |
+
$$('[data-play-anime]', scope).forEach(btn => {
|
| 834 |
+
btn.addEventListener('click', async (e) => {
|
| 835 |
+
e.preventDefault();
|
| 836 |
+
e.stopPropagation();
|
| 837 |
+
const id = btn.dataset.playAnime;
|
| 838 |
+
btn.disabled = true;
|
| 839 |
+
try {
|
| 840 |
+
const data = await api.anime(id);
|
| 841 |
+
const a = data.anime;
|
| 842 |
+
if (!a) return;
|
| 843 |
+
const tracks = tracksFromAnime(a);
|
| 844 |
+
if (tracks.length) play(tracks, 0);
|
| 845 |
+
} finally { btn.disabled = false; }
|
| 846 |
+
});
|
| 847 |
+
});
|
| 848 |
+
$$('[data-play-song-anime]', scope).forEach(btn => {
|
| 849 |
+
btn.addEventListener('click', async (e) => {
|
| 850 |
+
e.preventDefault();
|
| 851 |
+
e.stopPropagation();
|
| 852 |
+
const aid = btn.dataset.playSongAnime;
|
| 853 |
+
const themeId = btn.dataset.songTheme;
|
| 854 |
+
btn.disabled = true;
|
| 855 |
+
try {
|
| 856 |
+
const data = await api.anime(aid);
|
| 857 |
+
const a = data.anime;
|
| 858 |
+
if (!a) return;
|
| 859 |
+
const tracks = tracksFromAnime(a);
|
| 860 |
+
if (!tracks.length) return;
|
| 861 |
+
const idx = Math.max(0, tracks.findIndex(t => String(t.themeId) === String(themeId)));
|
| 862 |
+
play(tracks, idx);
|
| 863 |
+
} finally { btn.disabled = false; }
|
| 864 |
+
});
|
| 865 |
+
});
|
| 866 |
+
}
|
| 867 |
+
|
| 868 |
+
function renderHome() {
|
| 869 |
+
const hour = new Date().getHours();
|
| 870 |
+
const greet = hour < 5 ? 'Good evening' : hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening';
|
| 871 |
+
|
| 872 |
+
view.innerHTML = `
|
| 873 |
+
<div class="ms-home-hero">
|
| 874 |
+
<h1 class="ms-home-greet">${greet}</h1>
|
| 875 |
+
<div class="ms-quick-grid" id="home-quick"></div>
|
| 876 |
+
</div>
|
| 877 |
+
|
| 878 |
+
<div class="ms-section-title">
|
| 879 |
+
<h2>Newest themes</h2>
|
| 880 |
+
<a href="#/recent">View all</a>
|
| 881 |
+
</div>
|
| 882 |
+
<div class="ms-card-grid" id="home-recent">${loadingHTML()}</div>
|
| 883 |
+
|
| 884 |
+
<div class="ms-section-title">
|
| 885 |
+
<h2>Popular artists</h2>
|
| 886 |
+
<a href="#/artists">View all</a>
|
| 887 |
+
</div>
|
| 888 |
+
<div class="ms-card-grid" id="home-artists">${loadingHTML()}</div>
|
| 889 |
+
|
| 890 |
+
<div class="ms-section-title">
|
| 891 |
+
<h2>From your library</h2>
|
| 892 |
+
<a href="#/liked">View all</a>
|
| 893 |
+
</div>
|
| 894 |
+
<div id="home-liked"></div>
|
| 895 |
+
|
| 896 |
+
<div class="ms-section-title">
|
| 897 |
+
<h2>Browse by year</h2>
|
| 898 |
+
</div>
|
| 899 |
+
<div class="ms-card-grid ms-years-grid" id="home-years"></div>
|
| 900 |
+
`;
|
| 901 |
+
|
| 902 |
+
const quickEl = $('#home-quick');
|
| 903 |
+
const quickIds = [];
|
| 904 |
+
Array.from(state.liked).slice(0, 4).forEach(id => quickIds.push(id));
|
| 905 |
+
state.history.slice(0, 6).forEach(id => { if (!quickIds.includes(id)) quickIds.push(id); });
|
| 906 |
+
const quickTracks = loadStoredTracks(quickIds.slice(0, 6));
|
| 907 |
+
if (quickTracks.length) {
|
| 908 |
+
quickEl.innerHTML = quickTracks.map(t => `
|
| 909 |
+
<a class="ms-quick-tile" href="#/anime/${encodeURIComponent(t.animeId)}" data-quick-tid="${esc(t.id)}">
|
| 910 |
+
${t.cover ? `<img class="ms-quick-cover" src="${esc(t.cover)}" alt="" loading="lazy" referrerpolicy="no-referrer">` : `<div class="ms-quick-cover" style="background:linear-gradient(135deg,#4c1d95,#1e1b4b)"></div>`}
|
| 911 |
+
<span class="ms-quick-name">${esc(t.title)}</span>
|
| 912 |
+
<button class="ms-quick-play" data-play-quick="${esc(t.id)}" aria-label="Play">
|
| 913 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
| 914 |
+
</button>
|
| 915 |
+
</a>
|
| 916 |
+
`).join('');
|
| 917 |
+
$$('[data-play-quick]', quickEl).forEach(btn => {
|
| 918 |
+
btn.addEventListener('click', (e) => {
|
| 919 |
+
e.preventDefault(); e.stopPropagation();
|
| 920 |
+
const t = quickTracks.find(x => x.id === btn.dataset.playQuick);
|
| 921 |
+
if (t) play([t], 0);
|
| 922 |
+
});
|
| 923 |
+
});
|
| 924 |
+
} else {
|
| 925 |
+
quickEl.innerHTML = `<div class="ms-quick-empty">Like songs and they'll show up here for one-tap play.</div>`;
|
| 926 |
+
}
|
| 927 |
+
|
| 928 |
+
api.recent().then(d => {
|
| 929 |
+
const items = (d.anime || []).slice(0, 12);
|
| 930 |
+
$('#home-recent').innerHTML = items.length
|
| 931 |
+
? items.map(animeCardHTML).join('')
|
| 932 |
+
: emptyHTML('No themes found', '');
|
| 933 |
+
bindCardClicks($('#home-recent'));
|
| 934 |
+
}).catch(() => { $('#home-recent').innerHTML = emptyHTML('Could not load', 'Try again in a moment.'); });
|
| 935 |
+
|
| 936 |
+
api.artists().then(d => {
|
| 937 |
+
const items = (d.artists || []).slice(0, 8);
|
| 938 |
+
$('#home-artists').innerHTML = items.length
|
| 939 |
+
? items.map(artistCardHTML).join('')
|
| 940 |
+
: emptyHTML('No artists', '');
|
| 941 |
+
}).catch(() => { $('#home-artists').innerHTML = emptyHTML('Could not load', 'Try again in a moment.'); });
|
| 942 |
+
|
| 943 |
+
const likedEl = $('#home-liked');
|
| 944 |
+
const likedTracks = loadStoredTracks(Array.from(state.liked).slice(0, 6));
|
| 945 |
+
if (likedTracks.length) {
|
| 946 |
+
likedEl.innerHTML = `<div class="ms-card-grid">${likedTracks.map(t => `
|
| 947 |
+
<div class="ms-card" data-tid="${esc(t.id)}">
|
| 948 |
+
<div class="ms-card-cover-wrap">
|
| 949 |
+
${t.cover ? `<img class="ms-card-cover" src="${esc(t.cover)}" alt="" loading="lazy" referrerpolicy="no-referrer">` : placeholderCover(t.title)}
|
| 950 |
+
<button class="ms-card-play" data-play-liked="${esc(t.id)}" aria-label="Play">
|
| 951 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
| 952 |
+
</button>
|
| 953 |
+
</div>
|
| 954 |
+
<div class="ms-card-title">${esc(t.title)}</div>
|
| 955 |
+
<div class="ms-card-sub">${esc((t.artists || []).map(a => a.name).join(', ') || t.animeName || '')}</div>
|
| 956 |
+
</div>
|
| 957 |
+
`).join('')}</div>`;
|
| 958 |
+
$$('[data-play-liked]', likedEl).forEach(btn => {
|
| 959 |
+
btn.addEventListener('click', (e) => {
|
| 960 |
+
e.preventDefault(); e.stopPropagation();
|
| 961 |
+
const t = likedTracks.find(x => x.id === btn.dataset.playLiked);
|
| 962 |
+
if (t) play([t], 0);
|
| 963 |
+
});
|
| 964 |
+
});
|
| 965 |
+
} else {
|
| 966 |
+
likedEl.innerHTML = `<div class="ms-empty" style="padding:36px 12px"><h3>Your library is empty</h3><p>Like songs to see them here.</p></div>`;
|
| 967 |
+
}
|
| 968 |
+
|
| 969 |
+
const yearsEl = $('#home-years');
|
| 970 |
+
const cy = new Date().getFullYear();
|
| 971 |
+
const years = [];
|
| 972 |
+
for (let y = cy; y >= cy - 7; y--) years.push(y);
|
| 973 |
+
const yearGradients = [
|
| 974 |
+
['#7c3aed', '#1e1b4b'], ['#5b21b6', '#0f172a'], ['#4c1d95', '#1e293b'],
|
| 975 |
+
['#9d6ef8', '#312e81'], ['#6d28d9', '#0b1023'], ['#7c3aed', '#0f0d1f'],
|
| 976 |
+
['#5b21b6', '#1f1430'], ['#4c1d95', '#0a0712'],
|
| 977 |
+
];
|
| 978 |
+
yearsEl.innerHTML = years.map((y, i) => {
|
| 979 |
+
const [a, b] = yearGradients[i % yearGradients.length];
|
| 980 |
+
return `<a class="ms-year-card" href="#/year/${y}">
|
| 981 |
+
<div class="ms-year-cover" style="background:linear-gradient(135deg, ${a} 0%, ${b} 100%);">
|
| 982 |
+
<span class="ms-year-num">${y}</span>
|
| 983 |
+
</div>
|
| 984 |
+
<div class="ms-card-title">${y}</div>
|
| 985 |
+
<div class="ms-card-sub">Themes from ${y}</div>
|
| 986 |
+
</a>`;
|
| 987 |
+
}).join('');
|
| 988 |
+
}
|
| 989 |
+
|
| 990 |
+
function renderRecent() {
|
| 991 |
+
view.innerHTML = `<div class="ms-section-title" style="margin-top:14px"><h2>Newest themes</h2></div>
|
| 992 |
+
<div class="ms-card-grid" id="recent-grid">${loadingHTML()}</div>`;
|
| 993 |
+
api.recent().then(d => {
|
| 994 |
+
const items = d.anime || [];
|
| 995 |
+
$('#recent-grid').innerHTML = items.length
|
| 996 |
+
? items.map(animeCardHTML).join('')
|
| 997 |
+
: emptyHTML('No themes available', '');
|
| 998 |
+
bindCardClicks($('#recent-grid'));
|
| 999 |
+
});
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
function renderYear(y) {
|
| 1003 |
+
view.innerHTML = `<div class="ms-section-title" style="margin-top:14px"><h2>Themes from ${esc(y)}</h2></div>
|
| 1004 |
+
<div class="ms-card-grid" id="year-grid">${loadingHTML()}</div>`;
|
| 1005 |
+
api.year(y).then(d => {
|
| 1006 |
+
const items = d.anime || [];
|
| 1007 |
+
$('#year-grid').innerHTML = items.length
|
| 1008 |
+
? items.map(animeCardHTML).join('')
|
| 1009 |
+
: emptyHTML('Nothing here yet', `No themes catalogued for ${y}.`);
|
| 1010 |
+
bindCardClicks($('#year-grid'));
|
| 1011 |
+
});
|
| 1012 |
+
}
|
| 1013 |
+
|
| 1014 |
+
function renderTopArtists() {
|
| 1015 |
+
view.innerHTML = `<div class="ms-section-title" style="margin-top:14px"><h2>Artists</h2></div>
|
| 1016 |
+
<div class="ms-card-grid" id="artists-grid">${loadingHTML()}</div>`;
|
| 1017 |
+
api.artists().then(d => {
|
| 1018 |
+
const items = d.artists || [];
|
| 1019 |
+
$('#artists-grid').innerHTML = items.length
|
| 1020 |
+
? items.map(artistCardHTML).join('')
|
| 1021 |
+
: emptyHTML('No artists', '');
|
| 1022 |
+
});
|
| 1023 |
+
}
|
| 1024 |
+
|
| 1025 |
+
function renderLiked() {
|
| 1026 |
+
const tracks = loadStoredTracks(Array.from(state.liked));
|
| 1027 |
+
if (!tracks.length) {
|
| 1028 |
+
view.innerHTML = `<div class="ms-album-hero">
|
| 1029 |
+
<div class="ms-album-cover" style="display:flex;align-items:center;justify-content:center;">
|
| 1030 |
+
<svg viewBox="0 0 24 24" width="80" height="80" fill="#fff" style="opacity:0.85"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
| 1031 |
+
</div>
|
| 1032 |
+
<div class="ms-album-info">
|
| 1033 |
+
<div class="ms-album-eyebrow">Playlist</div>
|
| 1034 |
+
<h1 class="ms-album-title">Liked Songs</h1>
|
| 1035 |
+
<div class="ms-album-meta">0 tracks</div>
|
| 1036 |
+
</div>
|
| 1037 |
+
</div>${emptyHTML('No liked songs yet', 'Tap the heart on any track to save it here.')}`;
|
| 1038 |
+
return;
|
| 1039 |
+
}
|
| 1040 |
+
renderTracklist({
|
| 1041 |
+
eyebrow: 'Playlist',
|
| 1042 |
+
title: 'Liked Songs',
|
| 1043 |
+
cover: tracks[0].cover,
|
| 1044 |
+
countLabel: `${tracks.length} track${tracks.length > 1 ? 's' : ''}`,
|
| 1045 |
+
tracks,
|
| 1046 |
+
asPlaylist: true,
|
| 1047 |
+
});
|
| 1048 |
+
}
|
| 1049 |
+
|
| 1050 |
+
function renderHistory() {
|
| 1051 |
+
const tracks = loadStoredTracks(state.history);
|
| 1052 |
+
if (!tracks.length) {
|
| 1053 |
+
view.innerHTML = emptyHTML('Nothing recently played', 'Play some songs to build your history.');
|
| 1054 |
+
return;
|
| 1055 |
+
}
|
| 1056 |
+
renderTracklist({
|
| 1057 |
+
eyebrow: 'History',
|
| 1058 |
+
title: 'Recently Played',
|
| 1059 |
+
cover: tracks[0].cover,
|
| 1060 |
+
countLabel: `${tracks.length} track${tracks.length > 1 ? 's' : ''}`,
|
| 1061 |
+
tracks,
|
| 1062 |
+
asPlaylist: true,
|
| 1063 |
+
});
|
| 1064 |
+
}
|
| 1065 |
+
|
| 1066 |
+
async function renderAnime(id) {
|
| 1067 |
+
view.innerHTML = loadingHTML('Loading album…');
|
| 1068 |
+
try {
|
| 1069 |
+
const data = await api.anime(id);
|
| 1070 |
+
const a = data.anime;
|
| 1071 |
+
if (!a) { view.innerHTML = emptyHTML('Not found', 'This anime has no themes catalogued.'); return; }
|
| 1072 |
+
const tracks = tracksFromAnime(a);
|
| 1073 |
+
renderTracklist({
|
| 1074 |
+
eyebrow: a.media_format || 'Anime',
|
| 1075 |
+
title: a.name,
|
| 1076 |
+
cover: coverUrl(a),
|
| 1077 |
+
countLabel: `${tracks.length} track${tracks.length === 1 ? '' : 's'} · ${a.year || ''}${a.season ? ' · ' + a.season : ''}`,
|
| 1078 |
+
tracks,
|
| 1079 |
+
asPlaylist: false,
|
| 1080 |
+
});
|
| 1081 |
+
} catch (e) {
|
| 1082 |
+
view.innerHTML = emptyHTML('Could not load', 'Please try again.');
|
| 1083 |
+
}
|
| 1084 |
+
}
|
| 1085 |
+
|
| 1086 |
+
async function renderArtist(slug) {
|
| 1087 |
+
view.innerHTML = loadingHTML('Loading artist…');
|
| 1088 |
+
try {
|
| 1089 |
+
const data = await api.artist(slug);
|
| 1090 |
+
const a = data.artist;
|
| 1091 |
+
if (!a) { view.innerHTML = emptyHTML('Artist not found', ''); return; }
|
| 1092 |
+
const tracks = [];
|
| 1093 |
+
for (const song of (a.songs || [])) {
|
| 1094 |
+
for (const theme of (song.animethemes || [])) {
|
| 1095 |
+
if (!theme.anime) continue;
|
| 1096 |
+
const merged = { ...theme, song };
|
| 1097 |
+
const tk = trackFromTheme(theme.anime, merged);
|
| 1098 |
+
if (tk) tracks.push(tk);
|
| 1099 |
+
}
|
| 1100 |
+
}
|
| 1101 |
+
renderTracklist({
|
| 1102 |
+
eyebrow: 'Artist',
|
| 1103 |
+
title: a.name,
|
| 1104 |
+
cover: artistImageUrl(a),
|
| 1105 |
+
round: true,
|
| 1106 |
+
countLabel: `${tracks.length} track${tracks.length === 1 ? '' : 's'}`,
|
| 1107 |
+
tracks,
|
| 1108 |
+
asPlaylist: true,
|
| 1109 |
+
});
|
| 1110 |
+
} catch (e) {
|
| 1111 |
+
view.innerHTML = emptyHTML('Could not load', 'Please try again.');
|
| 1112 |
+
}
|
| 1113 |
+
}
|
| 1114 |
+
|
| 1115 |
+
function renderTracklist({ eyebrow, title, cover, countLabel, tracks, asPlaylist, round }) {
|
| 1116 |
+
const heroCoverHTML = cover
|
| 1117 |
+
? `<img src="${esc(cover)}" alt="" referrerpolicy="no-referrer">`
|
| 1118 |
+
: `<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#fff;font-size:4rem;font-weight:900;">${esc(title.slice(0, 1))}</div>`;
|
| 1119 |
+
view.innerHTML = `
|
| 1120 |
+
<div class="ms-album-hero">
|
| 1121 |
+
<div class="ms-album-cover" ${round ? 'style="border-radius:50%;"' : ''}>${heroCoverHTML}</div>
|
| 1122 |
+
<div class="ms-album-info">
|
| 1123 |
+
<div class="ms-album-eyebrow">${esc(eyebrow)}</div>
|
| 1124 |
+
<h1 class="ms-album-title">${esc(title)}</h1>
|
| 1125 |
+
<div class="ms-album-meta">${esc(countLabel)}</div>
|
| 1126 |
+
</div>
|
| 1127 |
+
</div>
|
| 1128 |
+
<div class="ms-album-actions">
|
| 1129 |
+
<button class="ms-play-big" id="album-play" aria-label="Play all">
|
| 1130 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
| 1131 |
+
</button>
|
| 1132 |
+
<button class="ms-icon-btn" id="album-shuffle" aria-label="Shuffle">
|
| 1133 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>
|
| 1134 |
+
</button>
|
| 1135 |
+
</div>
|
| 1136 |
+
<div class="ms-tracks">
|
| 1137 |
+
${tracks.length ? `
|
| 1138 |
+
<div class="ms-tracks-head">
|
| 1139 |
+
<span style="text-align:center">#</span>
|
| 1140 |
+
<span>Title</span>
|
| 1141 |
+
<span class="ms-th-anime">${asPlaylist ? 'Anime' : 'Type'}</span>
|
| 1142 |
+
<span class="ms-th-tag">${asPlaylist ? 'Type' : 'Episodes'}</span>
|
| 1143 |
+
<span></span>
|
| 1144 |
+
</div>
|
| 1145 |
+
${tracks.map((t, i) => trackRowHTML(t, i, asPlaylist)).join('')}
|
| 1146 |
+
` : emptyHTML('No tracks found', '')}
|
| 1147 |
+
</div>
|
| 1148 |
+
`;
|
| 1149 |
+
const playAll = $('#album-play');
|
| 1150 |
+
if (playAll) playAll.addEventListener('click', () => { if (tracks.length) play(tracks, 0); });
|
| 1151 |
+
const shuf = $('#album-shuffle');
|
| 1152 |
+
if (shuf) shuf.addEventListener('click', () => {
|
| 1153 |
+
if (!tracks.length) return;
|
| 1154 |
+
const shuffled = tracks.slice().sort(() => Math.random() - 0.5);
|
| 1155 |
+
state.shuffle = false;
|
| 1156 |
+
play(shuffled, 0);
|
| 1157 |
+
});
|
| 1158 |
+
bindTrackRows(tracks);
|
| 1159 |
+
updateNowPlayingMarkers();
|
| 1160 |
+
}
|
| 1161 |
+
|
| 1162 |
+
function trackRowHTML(t, i, asPlaylist) {
|
| 1163 |
+
const tagClass = (t.typeKind || '').toUpperCase() === 'ED' ? 'ed' : ((t.typeKind || '').toUpperCase() === 'IN' ? 'in' : '');
|
| 1164 |
+
const artistsText = (t.artists || []).map(a => `<a href="#/artist/${encodeURIComponent(a.slug || a.id)}" data-stop>${esc(a.name)}</a>`).join(', ');
|
| 1165 |
+
const middleCol = asPlaylist
|
| 1166 |
+
? `<a class="ms-track-anime" href="#/anime/${encodeURIComponent(t.animeId)}" data-stop>${esc(t.animeName)}</a>`
|
| 1167 |
+
: `<span class="ms-track-tag-cell"><span class="ms-track-tag ${tagClass}">${esc(t.type)}</span></span>`;
|
| 1168 |
+
const fourthCol = asPlaylist
|
| 1169 |
+
? `<span class="ms-track-tag-cell"><span class="ms-track-tag ${tagClass}">${esc(t.type)}</span></span>`
|
| 1170 |
+
: `<span class="ms-track-anime">${esc(t.episodes || '—')}</span>`;
|
| 1171 |
+
return `<div class="ms-track" data-tid="${esc(t.id)}" data-idx="${i}">
|
| 1172 |
+
<div class="ms-track-num">
|
| 1173 |
+
<span class="ms-num">${i + 1}</span>
|
| 1174 |
+
<span class="ms-now-bars"><span></span><span></span><span></span></span>
|
| 1175 |
+
<span class="ms-play-mini"><svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg></span>
|
| 1176 |
+
</div>
|
| 1177 |
+
<div class="ms-track-meta">
|
| 1178 |
+
${t.cover ? `<img class="ms-track-cover" src="${esc(t.cover)}" alt="" loading="lazy" referrerpolicy="no-referrer">` : '<div class="ms-track-cover"></div>'}
|
| 1179 |
+
<div class="ms-track-text">
|
| 1180 |
+
<div class="ms-track-title">${esc(t.title)}</div>
|
| 1181 |
+
<div class="ms-track-sub">${artistsText || esc(t.animeName)}</div>
|
| 1182 |
+
</div>
|
| 1183 |
+
</div>
|
| 1184 |
+
${middleCol}
|
| 1185 |
+
${fourthCol}
|
| 1186 |
+
<button class="ms-track-like ${state.liked.has(t.id) ? 'is-liked' : ''}" data-tid="${esc(t.id)}" data-stop aria-label="Like">
|
| 1187 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
| 1188 |
+
</button>
|
| 1189 |
+
</div>`;
|
| 1190 |
+
}
|
| 1191 |
+
|
| 1192 |
+
function bindTrackRows(tracks) {
|
| 1193 |
+
$$('.ms-track', view).forEach(row => {
|
| 1194 |
+
row.addEventListener('click', (e) => {
|
| 1195 |
+
if (e.target.closest('[data-stop]')) return;
|
| 1196 |
+
const idx = parseInt(row.dataset.idx, 10);
|
| 1197 |
+
if (!isNaN(idx)) play(tracks, idx);
|
| 1198 |
+
});
|
| 1199 |
+
});
|
| 1200 |
+
$$('.ms-track-like', view).forEach(btn => {
|
| 1201 |
+
btn.addEventListener('click', (e) => {
|
| 1202 |
+
e.preventDefault(); e.stopPropagation();
|
| 1203 |
+
const id = btn.dataset.tid;
|
| 1204 |
+
const t = tracks.find(x => x.id === id);
|
| 1205 |
+
if (t) toggleLike(t);
|
| 1206 |
+
});
|
| 1207 |
+
});
|
| 1208 |
+
}
|
| 1209 |
+
|
| 1210 |
+
function songRowHTML(track, idx) {
|
| 1211 |
+
const artistsText = (track.artists || []).map(a => `<a href="#/artist/${encodeURIComponent(a.slug || a.id)}" data-stop>${esc(a.name)}</a>`).join(', ');
|
| 1212 |
+
return `<div class="ms-song-row" data-tid="${esc(track.id)}" data-idx="${idx}">
|
| 1213 |
+
<div class="ms-song-cover-wrap">
|
| 1214 |
+
${track.cover ? `<img class="ms-song-cover" src="${esc(track.cover)}" alt="" loading="lazy" referrerpolicy="no-referrer">` : '<div class="ms-song-cover"></div>'}
|
| 1215 |
+
<span class="ms-song-play"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg></span>
|
| 1216 |
+
</div>
|
| 1217 |
+
<div class="ms-song-text">
|
| 1218 |
+
<div class="ms-song-title">${esc(track.title)}</div>
|
| 1219 |
+
<div class="ms-song-sub">${artistsText || '<a href="#/anime/' + encodeURIComponent(track.animeId) + '" data-stop>' + esc(track.animeName) + '</a>'}</div>
|
| 1220 |
+
</div>
|
| 1221 |
+
<button class="ms-song-like ${state.liked.has(track.id) ? 'is-liked' : ''}" data-tid="${esc(track.id)}" data-stop aria-label="Like">
|
| 1222 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
| 1223 |
+
</button>
|
| 1224 |
+
</div>`;
|
| 1225 |
+
}
|
| 1226 |
+
|
| 1227 |
+
function bindSongRows(scope, tracks) {
|
| 1228 |
+
$$('.ms-song-row', scope).forEach(row => {
|
| 1229 |
+
row.addEventListener('click', (e) => {
|
| 1230 |
+
if (e.target.closest('[data-stop]')) return;
|
| 1231 |
+
const idx = parseInt(row.dataset.idx, 10);
|
| 1232 |
+
if (!isNaN(idx) && tracks[idx]) play(tracks, idx);
|
| 1233 |
+
});
|
| 1234 |
+
});
|
| 1235 |
+
$$('.ms-song-like', scope).forEach(btn => {
|
| 1236 |
+
btn.addEventListener('click', (e) => {
|
| 1237 |
+
e.preventDefault(); e.stopPropagation();
|
| 1238 |
+
const id = btn.dataset.tid;
|
| 1239 |
+
const t = tracks.find(x => x.id === id);
|
| 1240 |
+
if (t) toggleLike(t);
|
| 1241 |
+
});
|
| 1242 |
+
});
|
| 1243 |
+
}
|
| 1244 |
+
|
| 1245 |
+
async function renderSearch(q) {
|
| 1246 |
+
if (!q) { renderHome(); return; }
|
| 1247 |
+
view.innerHTML = loadingHTML(`Searching "${q}"…`);
|
| 1248 |
+
const filter = (state.searchFilter || 'all');
|
| 1249 |
+
try {
|
| 1250 |
+
const data = await api.search(q);
|
| 1251 |
+
window.__lastSearch = data;
|
| 1252 |
+
const anime = data.anime || [];
|
| 1253 |
+
const themes = data.animethemes || [];
|
| 1254 |
+
const artists = data.artists || [];
|
| 1255 |
+
const songs = data.songs || [];
|
| 1256 |
+
|
| 1257 |
+
const themeTracks = themes
|
| 1258 |
+
.map(th => th.anime ? trackFromTheme(th.anime, th) : null)
|
| 1259 |
+
.filter(Boolean);
|
| 1260 |
+
|
| 1261 |
+
const tabsHTML = `<div class="ms-search-tabs">
|
| 1262 |
+
${['all','anime','songs','artists'].map(f =>
|
| 1263 |
+
`<button class="ms-tab ${filter===f?'active':''}" data-filter="${f}">${f[0].toUpperCase()+f.slice(1)}</button>`
|
| 1264 |
+
).join('')}
|
| 1265 |
+
</div>`;
|
| 1266 |
+
|
| 1267 |
+
const noResults = !anime.length && !artists.length && !songs.length && !themeTracks.length;
|
| 1268 |
+
if (noResults) {
|
| 1269 |
+
view.innerHTML = `${tabsHTML}${emptyHTML(`No results for "${q}"`, 'Try a different anime, song, or artist.')}`;
|
| 1270 |
+
bindSearchTabs(q);
|
| 1271 |
+
return;
|
| 1272 |
+
}
|
| 1273 |
+
|
| 1274 |
+
let topResultHTML = '';
|
| 1275 |
+
const topAnime = anime[0];
|
| 1276 |
+
if (topAnime && (filter === 'all' || filter === 'anime')) {
|
| 1277 |
+
const cover = coverUrl(topAnime);
|
| 1278 |
+
topResultHTML = `<div class="ms-top-result">
|
| 1279 |
+
<div>
|
| 1280 |
+
<div class="ms-section-title" style="margin:0 0 14px"><h2>Top result</h2></div>
|
| 1281 |
+
<a class="ms-top-card" href="#/anime/${encodeURIComponent(topAnime.id)}">
|
| 1282 |
+
${cover ? `<img class="ms-top-cover" src="${esc(cover)}" alt="" referrerpolicy="no-referrer">` : `<div class="ms-top-cover" style="background:linear-gradient(135deg,#4c1d95,#1e1b4b)"></div>`}
|
| 1283 |
+
<div class="ms-top-name">${esc(topAnime.name)}</div>
|
| 1284 |
+
<span class="ms-top-tag">Anime${topAnime.year ? ' · ' + esc(topAnime.year) : ''}</span>
|
| 1285 |
+
<button class="ms-top-play" data-play-anime="${esc(topAnime.id)}" aria-label="Play">
|
| 1286 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
| 1287 |
+
</button>
|
| 1288 |
+
</a>
|
| 1289 |
+
</div>
|
| 1290 |
+
${themeTracks.length ? `<div class="ms-top-songs">
|
| 1291 |
+
<h3>Songs</h3>
|
| 1292 |
+
<div class="ms-songlist" id="search-top-songs">
|
| 1293 |
+
${themeTracks.slice(0, 4).map((tk, i) => songRowHTML(tk, i)).join('')}
|
| 1294 |
+
</div>
|
| 1295 |
+
</div>` : ''}
|
| 1296 |
+
</div>`;
|
| 1297 |
+
}
|
| 1298 |
+
|
| 1299 |
+
const animeBlock = (filter === 'all' || filter === 'anime') && anime.length ? `<div class="ms-search-block">
|
| 1300 |
+
<div class="ms-section-title"><h2>Anime</h2></div>
|
| 1301 |
+
<div class="ms-card-grid">${anime.slice(0, filter === 'anime' ? 24 : 8).map(animeCardHTML).join('')}</div>
|
| 1302 |
+
</div>` : '';
|
| 1303 |
+
|
| 1304 |
+
const artistsBlock = (filter === 'all' || filter === 'artists') && artists.length ? `<div class="ms-search-block">
|
| 1305 |
+
<div class="ms-section-title"><h2>Artists</h2></div>
|
| 1306 |
+
<div class="ms-card-grid">${artists.slice(0, filter === 'artists' ? 24 : 8).map(artistCardHTML).join('')}</div>
|
| 1307 |
+
</div>` : '';
|
| 1308 |
+
|
| 1309 |
+
let songsBlock = '';
|
| 1310 |
+
if ((filter === 'all' || filter === 'songs')) {
|
| 1311 |
+
const songTracksAll = themeTracks.length ? themeTracks : [];
|
| 1312 |
+
if (songTracksAll.length) {
|
| 1313 |
+
const limit = filter === 'songs' ? songTracksAll.length : 8;
|
| 1314 |
+
songsBlock = `<div class="ms-search-block">
|
| 1315 |
+
<div class="ms-section-title"><h2>Songs</h2></div>
|
| 1316 |
+
<div class="ms-songlist" id="search-songs-list">
|
| 1317 |
+
${songTracksAll.slice(0, limit).map((tk, i) => songRowHTML(tk, i)).join('')}
|
| 1318 |
+
</div>
|
| 1319 |
+
</div>`;
|
| 1320 |
+
} else if (songs.length) {
|
| 1321 |
+
songsBlock = `<div class="ms-search-block">
|
| 1322 |
+
<div class="ms-section-title"><h2>Songs</h2></div>
|
| 1323 |
+
<div class="ms-card-grid">${songs.slice(0, 8).map(songCardHTML).join('')}</div>
|
| 1324 |
+
</div>`;
|
| 1325 |
+
}
|
| 1326 |
+
}
|
| 1327 |
+
|
| 1328 |
+
view.innerHTML = `
|
| 1329 |
+
${tabsHTML}
|
| 1330 |
+
${topResultHTML}
|
| 1331 |
+
${animeBlock}
|
| 1332 |
+
${songsBlock}
|
| 1333 |
+
${artistsBlock}
|
| 1334 |
+
`;
|
| 1335 |
+
|
| 1336 |
+
bindSearchTabs(q);
|
| 1337 |
+
bindCardClicks(view);
|
| 1338 |
+
|
| 1339 |
+
const topListEl = $('#search-top-songs');
|
| 1340 |
+
if (topListEl) bindSongRows(topListEl, themeTracks.slice(0, 4));
|
| 1341 |
+
const songsListEl = $('#search-songs-list');
|
| 1342 |
+
if (songsListEl) {
|
| 1343 |
+
const limit = filter === 'songs' ? themeTracks.length : 8;
|
| 1344 |
+
bindSongRows(songsListEl, themeTracks.slice(0, limit));
|
| 1345 |
+
}
|
| 1346 |
+
updateNowPlayingMarkers();
|
| 1347 |
+
} catch (e) {
|
| 1348 |
+
console.error('[music] search failed', e);
|
| 1349 |
+
view.innerHTML = emptyHTML('Search failed', 'Please try again.');
|
| 1350 |
+
}
|
| 1351 |
+
}
|
| 1352 |
+
|
| 1353 |
+
function bindSearchTabs(q) {
|
| 1354 |
+
$$('.ms-tab', view).forEach(tab => {
|
| 1355 |
+
tab.addEventListener('click', () => {
|
| 1356 |
+
state.searchFilter = tab.dataset.filter;
|
| 1357 |
+
renderSearch(q);
|
| 1358 |
+
});
|
| 1359 |
+
});
|
| 1360 |
+
}
|
| 1361 |
+
|
| 1362 |
+
function closeSidebarIfMobile() {
|
| 1363 |
+
if (window.innerWidth <= 1024) {
|
| 1364 |
+
$('#music-sidebar').classList.remove('open');
|
| 1365 |
+
$('#ms-sidebar-backdrop').classList.remove('active');
|
| 1366 |
+
}
|
| 1367 |
+
}
|
| 1368 |
+
|
| 1369 |
+
function openYearSidebarLinks() {
|
| 1370 |
+
const yearGrid = $('#ms-year-grid');
|
| 1371 |
+
const cy = new Date().getFullYear();
|
| 1372 |
+
const years = [];
|
| 1373 |
+
for (let y = cy; y >= cy - 9; y--) years.push(y);
|
| 1374 |
+
yearGrid.innerHTML = years.map(y => `<a data-route="#/year/${y}" href="#/year/${y}">${y}</a>`).join('');
|
| 1375 |
+
}
|
| 1376 |
+
|
| 1377 |
+
function init() {
|
| 1378 |
+
audio.volume = state.volume;
|
| 1379 |
+
setVolUI();
|
| 1380 |
+
|
| 1381 |
+
openYearSidebarLinks();
|
| 1382 |
+
|
| 1383 |
+
let searchTimer;
|
| 1384 |
+
const searchInput = $('#ms-search-input');
|
| 1385 |
+
searchInput.addEventListener('input', () => {
|
| 1386 |
+
const q = searchInput.value.trim();
|
| 1387 |
+
$('#ms-search-clear').hidden = !q;
|
| 1388 |
+
clearTimeout(searchTimer);
|
| 1389 |
+
if (!q) {
|
| 1390 |
+
if (location.hash.startsWith('#/search')) navigate('#/');
|
| 1391 |
+
return;
|
| 1392 |
+
}
|
| 1393 |
+
searchTimer = setTimeout(() => {
|
| 1394 |
+
navigate('#/search?q=' + encodeURIComponent(q));
|
| 1395 |
+
}, 320);
|
| 1396 |
+
});
|
| 1397 |
+
searchInput.addEventListener('keydown', (e) => {
|
| 1398 |
+
if (e.key === 'Escape') { searchInput.value = ''; navigate('#/'); }
|
| 1399 |
+
});
|
| 1400 |
+
$('#ms-search-clear').addEventListener('click', () => {
|
| 1401 |
+
searchInput.value = '';
|
| 1402 |
+
$('#ms-search-clear').hidden = true;
|
| 1403 |
+
navigate('#/');
|
| 1404 |
+
searchInput.focus();
|
| 1405 |
+
});
|
| 1406 |
+
|
| 1407 |
+
$('#pb-play').addEventListener('click', togglePlay);
|
| 1408 |
+
$('#pb-play-mobile').addEventListener('click', togglePlay);
|
| 1409 |
+
$('#pb-prev').addEventListener('click', prev);
|
| 1410 |
+
$('#pb-next').addEventListener('click', () => next(false));
|
| 1411 |
+
$('#pb-shuffle').addEventListener('click', () => {
|
| 1412 |
+
state.shuffle = !state.shuffle;
|
| 1413 |
+
$('#pb-shuffle').classList.toggle('is-on', state.shuffle);
|
| 1414 |
+
});
|
| 1415 |
+
$('#pb-loop').addEventListener('click', () => {
|
| 1416 |
+
state.loop = !state.loop;
|
| 1417 |
+
$('#pb-loop').classList.toggle('is-on', state.loop);
|
| 1418 |
+
audio.loop = false;
|
| 1419 |
+
});
|
| 1420 |
+
$('#pb-mute').addEventListener('click', () => {
|
| 1421 |
+
state.muted = !state.muted;
|
| 1422 |
+
audio.muted = state.muted;
|
| 1423 |
+
video.muted = state.muted;
|
| 1424 |
+
setVolUI();
|
| 1425 |
+
});
|
| 1426 |
+
$('#pb-like').addEventListener('click', () => {
|
| 1427 |
+
const t = state.queue[state.index];
|
| 1428 |
+
if (t) toggleLike(t);
|
| 1429 |
+
});
|
| 1430 |
+
|
| 1431 |
+
bindBar($('#pb-seek'), (pct) => {
|
| 1432 |
+
const m = activeMedia();
|
| 1433 |
+
if (m.duration) m.currentTime = pct * m.duration;
|
| 1434 |
+
setSeek(pct * (m.duration || 0));
|
| 1435 |
+
});
|
| 1436 |
+
bindBar($('#pb-vol'), (pct) => {
|
| 1437 |
+
state.volume = pct;
|
| 1438 |
+
state.muted = false;
|
| 1439 |
+
audio.muted = false;
|
| 1440 |
+
video.muted = false;
|
| 1441 |
+
audio.volume = pct;
|
| 1442 |
+
video.volume = pct;
|
| 1443 |
+
persistVol();
|
| 1444 |
+
setVolUI();
|
| 1445 |
+
});
|
| 1446 |
+
|
| 1447 |
+
[audio, video].forEach((m) => {
|
| 1448 |
+
m.addEventListener('play', () => {
|
| 1449 |
+
if (m === activeMedia()) { state.isPlaying = true; setPlayUI(true); }
|
| 1450 |
+
if (m === video) showStageLoading(false);
|
| 1451 |
+
});
|
| 1452 |
+
m.addEventListener('pause', () => {
|
| 1453 |
+
if (m === activeMedia()) { state.isPlaying = false; setPlayUI(false); }
|
| 1454 |
+
if (m === video) showStageLoading(false);
|
| 1455 |
+
});
|
| 1456 |
+
m.addEventListener('ended', () => {
|
| 1457 |
+
if (m !== activeMedia()) return;
|
| 1458 |
+
if (state.loop) { m.currentTime = 0; m.play().catch(() => { }); }
|
| 1459 |
+
else next(true);
|
| 1460 |
+
});
|
| 1461 |
+
m.addEventListener('timeupdate', () => {
|
| 1462 |
+
if (m === activeMedia() && !state.scrubbing) setSeek();
|
| 1463 |
+
if (m === activeMedia() && _syncedLines.length) updateLyricsHighlight(m.currentTime);
|
| 1464 |
+
});
|
| 1465 |
+
|
| 1466 |
+
m.addEventListener('play', () => {
|
| 1467 |
+
if (m !== activeMedia()) return;
|
| 1468 |
+
if (_lyricRaf) cancelAnimationFrame(_lyricRaf);
|
| 1469 |
+
const tick = () => {
|
| 1470 |
+
if (m !== activeMedia() || m.paused || m.ended) {
|
| 1471 |
+
_lyricRaf = null;
|
| 1472 |
+
return;
|
| 1473 |
+
}
|
| 1474 |
+
if (_syncedLines.length && document.body.classList.contains('np-lyrics-open')) {
|
| 1475 |
+
updateLyricsHighlight(m.currentTime);
|
| 1476 |
+
}
|
| 1477 |
+
_lyricRaf = requestAnimationFrame(tick);
|
| 1478 |
+
};
|
| 1479 |
+
_lyricRaf = requestAnimationFrame(tick);
|
| 1480 |
+
});
|
| 1481 |
+
m.addEventListener('loadedmetadata', () => {
|
| 1482 |
+
if (m !== activeMedia()) return;
|
| 1483 |
+
setSeek();
|
| 1484 |
+
|
| 1485 |
+
if (_rawSyncedLines.length) {
|
| 1486 |
+
recomputeSyncedLines();
|
| 1487 |
+
renderSyncedLyrics(_syncedLines);
|
| 1488 |
+
}
|
| 1489 |
+
});
|
| 1490 |
+
m.addEventListener('durationchange', () => {
|
| 1491 |
+
if (m !== activeMedia() || !_rawSyncedLines.length) return;
|
| 1492 |
+
recomputeSyncedLines();
|
| 1493 |
+
renderSyncedLyrics(_syncedLines);
|
| 1494 |
+
});
|
| 1495 |
+
|
| 1496 |
+
m.addEventListener('seeked', () => {
|
| 1497 |
+
if (m !== activeMedia() || !_syncedLines.length) return;
|
| 1498 |
+
_activeLyricIdx = -2;
|
| 1499 |
+
updateLyricsHighlight(m.currentTime || 0);
|
| 1500 |
+
});
|
| 1501 |
+
m.addEventListener('error', () => {
|
| 1502 |
+
if (m !== activeMedia()) return;
|
| 1503 |
+
if (m === video) showStageLoading(false);
|
| 1504 |
+
const t = state.queue[state.index];
|
| 1505 |
+
if (t) console.warn('[music] failed to play', t.title);
|
| 1506 |
+
setTimeout(() => next(true), 800);
|
| 1507 |
+
});
|
| 1508 |
+
});
|
| 1509 |
+
|
| 1510 |
+
video.addEventListener('waiting', () => { if (mediaMode === 'video' && !video.paused) showStageLoading(true); });
|
| 1511 |
+
video.addEventListener('loadstart', () => { if (mediaMode === 'video') showStageLoading(true); });
|
| 1512 |
+
video.addEventListener('seeking', () => { if (mediaMode === 'video') showStageLoading(true); });
|
| 1513 |
+
video.addEventListener('stalled', () => { if (mediaMode === 'video' && !video.paused) showStageLoading(true); });
|
| 1514 |
+
video.addEventListener('canplay', () => showStageLoading(false));
|
| 1515 |
+
video.addEventListener('canplaythrough', () => showStageLoading(false));
|
| 1516 |
+
video.addEventListener('playing', () => showStageLoading(false));
|
| 1517 |
+
video.addEventListener('seeked', () => showStageLoading(false));
|
| 1518 |
+
video.addEventListener('progress', () => { if (video.readyState >= 3) showStageLoading(false); });
|
| 1519 |
+
|
| 1520 |
+
$('#pb-expand').addEventListener('click', openNP);
|
| 1521 |
+
$('#np-close').addEventListener('click', closeNP);
|
| 1522 |
+
$('#np-shuffle').addEventListener('click', () => {
|
| 1523 |
+
state.shuffle = !state.shuffle;
|
| 1524 |
+
$('#pb-shuffle').classList.toggle('is-on', state.shuffle);
|
| 1525 |
+
$('#np-shuffle').classList.toggle('is-on', state.shuffle);
|
| 1526 |
+
});
|
| 1527 |
+
$('#np-loop').addEventListener('click', () => {
|
| 1528 |
+
state.loop = !state.loop;
|
| 1529 |
+
$('#pb-loop').classList.toggle('is-on', state.loop);
|
| 1530 |
+
$('#np-loop').classList.toggle('is-on', state.loop);
|
| 1531 |
+
});
|
| 1532 |
+
$('#np-prev').addEventListener('click', prev);
|
| 1533 |
+
$('#np-next').addEventListener('click', () => next(false));
|
| 1534 |
+
$('#np-play').addEventListener('click', togglePlay);
|
| 1535 |
+
$('#np-like').addEventListener('click', () => {
|
| 1536 |
+
const t = state.queue[state.index];
|
| 1537 |
+
if (!t) return;
|
| 1538 |
+
toggleLike(t);
|
| 1539 |
+
const btn = $('#np-like');
|
| 1540 |
+
if (btn) {
|
| 1541 |
+
btn.classList.remove('just-liked');
|
| 1542 |
+
|
| 1543 |
+
void btn.offsetWidth;
|
| 1544 |
+
btn.classList.add('just-liked');
|
| 1545 |
+
}
|
| 1546 |
+
});
|
| 1547 |
+
bindBar($('#np-seek'), (pct) => {
|
| 1548 |
+
const m = activeMedia();
|
| 1549 |
+
if (m.duration) m.currentTime = pct * m.duration;
|
| 1550 |
+
setSeek(pct * (m.duration || 0));
|
| 1551 |
+
});
|
| 1552 |
+
function setPanel(which) {
|
| 1553 |
+
|
| 1554 |
+
$('#np-lyrics').hidden = which !== 'lyrics';
|
| 1555 |
+
$('#np-queue').hidden = which !== 'queue';
|
| 1556 |
+
$('#np-lyrics-btn').classList.toggle('is-on', which === 'lyrics');
|
| 1557 |
+
$('#np-queue-btn').classList.toggle('is-on', which === 'queue');
|
| 1558 |
+
|
| 1559 |
+
document.body.classList.toggle('np-lyrics-open', which === 'lyrics');
|
| 1560 |
+
if (which === 'queue') renderQueue();
|
| 1561 |
+
|
| 1562 |
+
if (which === 'lyrics' && _syncedLines.length) {
|
| 1563 |
+
_activeLyricIdx = -2;
|
| 1564 |
+
requestAnimationFrame(() => {
|
| 1565 |
+
updateLyricsHighlight(activeMedia().currentTime || 0);
|
| 1566 |
+
});
|
| 1567 |
+
}
|
| 1568 |
+
}
|
| 1569 |
+
$('#np-video-btn').addEventListener('click', () => {
|
| 1570 |
+
const t = state.queue[state.index];
|
| 1571 |
+
if (!t) return;
|
| 1572 |
+
if (!t.videoLink) { toast('No video available for this track'); return; }
|
| 1573 |
+
setPanel(null);
|
| 1574 |
+
setVideoMode(mediaMode !== 'video');
|
| 1575 |
+
});
|
| 1576 |
+
$('#np-lyrics-btn').addEventListener('click', () => {
|
| 1577 |
+
const showing = $('#np-lyrics').hidden;
|
| 1578 |
+
setPanel(showing ? 'lyrics' : null);
|
| 1579 |
+
});
|
| 1580 |
+
$('#np-queue-btn').addEventListener('click', () => {
|
| 1581 |
+
const showing = $('#np-queue').hidden;
|
| 1582 |
+
setPanel(showing ? 'queue' : null);
|
| 1583 |
+
});
|
| 1584 |
+
|
| 1585 |
+
$('#np-lyrics-close')?.addEventListener('click', () => setPanel(null));
|
| 1586 |
+
$('#np-queue-close') ?.addEventListener('click', () => setPanel(null));
|
| 1587 |
+
|
| 1588 |
+
$('#np-bg').addEventListener('click', closeNP);
|
| 1589 |
+
document.addEventListener('keydown', (e) => {
|
| 1590 |
+
if (e.key === 'Escape' && !npModal.hidden) closeNP();
|
| 1591 |
+
});
|
| 1592 |
+
|
| 1593 |
+
$('#pb-title').addEventListener('click', () => {
|
| 1594 |
+
if (playerBar.dataset.empty === '1') return;
|
| 1595 |
+
openNP();
|
| 1596 |
+
});
|
| 1597 |
+
|
| 1598 |
+
$('#pb-dismiss')?.addEventListener('click', () => {
|
| 1599 |
+
try { audio.pause(); video.pause(); } catch (_) {}
|
| 1600 |
+
document.body.classList.add('pb-hidden');
|
| 1601 |
+
});
|
| 1602 |
+
|
| 1603 |
+
$('#ms-back').addEventListener('click', () => {
|
| 1604 |
+
if (state.navStack.length > 1) {
|
| 1605 |
+
const cur = state.navStack.pop();
|
| 1606 |
+
state.navFwd.push(cur);
|
| 1607 |
+
const prev = state.navStack[state.navStack.length - 1];
|
| 1608 |
+
state.navStack.pop();
|
| 1609 |
+
location.hash = prev;
|
| 1610 |
+
}
|
| 1611 |
+
});
|
| 1612 |
+
$('#ms-fwd').addEventListener('click', () => {
|
| 1613 |
+
const fwd = state.navFwd.pop();
|
| 1614 |
+
if (fwd) location.hash = fwd;
|
| 1615 |
+
});
|
| 1616 |
+
|
| 1617 |
+
$('#ms-sidebar-toggle').addEventListener('click', () => {
|
| 1618 |
+
const isDesktop = window.innerWidth > 1024;
|
| 1619 |
+
if (isDesktop) {
|
| 1620 |
+
$('#music-app').classList.toggle('sb-collapsed');
|
| 1621 |
+
} else {
|
| 1622 |
+
$('#music-sidebar').classList.toggle('open');
|
| 1623 |
+
$('#ms-sidebar-backdrop').classList.toggle('active');
|
| 1624 |
+
}
|
| 1625 |
+
});
|
| 1626 |
+
$('#ms-sidebar-backdrop').addEventListener('click', closeSidebarIfMobile);
|
| 1627 |
+
|
| 1628 |
+
const sidebarEl = $('#music-sidebar');
|
| 1629 |
+
if (sidebarEl) {
|
| 1630 |
+
let downTarget = null;
|
| 1631 |
+
let downX = 0, downY = 0;
|
| 1632 |
+
let pointerHandledAt = 0;
|
| 1633 |
+
const TAP_SLOP = 10;
|
| 1634 |
+
|
| 1635 |
+
sidebarEl.addEventListener('pointerdown', (e) => {
|
| 1636 |
+
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
| 1637 |
+
downTarget = e.target.closest('a, .ms-link, [data-route]');
|
| 1638 |
+
downX = e.clientX; downY = e.clientY;
|
| 1639 |
+
});
|
| 1640 |
+
|
| 1641 |
+
const fireTap = (e) => {
|
| 1642 |
+
const link = e.target.closest('a, .ms-link, [data-route]');
|
| 1643 |
+
if (!link || !sidebarEl.contains(link)) return false;
|
| 1644 |
+
|
| 1645 |
+
if (downTarget && downTarget !== link) return false;
|
| 1646 |
+
if (Math.abs(e.clientX - downX) > TAP_SLOP) return false;
|
| 1647 |
+
if (Math.abs(e.clientY - downY) > TAP_SLOP) return false;
|
| 1648 |
+
const route = link.dataset.route || link.getAttribute('href');
|
| 1649 |
+
if (route && route.startsWith('#')) {
|
| 1650 |
+
navigate(route);
|
| 1651 |
+
} else {
|
| 1652 |
+
closeSidebarIfMobile();
|
| 1653 |
+
}
|
| 1654 |
+
return true;
|
| 1655 |
+
};
|
| 1656 |
+
|
| 1657 |
+
sidebarEl.addEventListener('pointerup', (e) => {
|
| 1658 |
+
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
| 1659 |
+
if (fireTap(e)) pointerHandledAt = performance.now();
|
| 1660 |
+
downTarget = null;
|
| 1661 |
+
});
|
| 1662 |
+
|
| 1663 |
+
sidebarEl.addEventListener('click', (e) => {
|
| 1664 |
+
const link = e.target.closest('a, .ms-link, [data-route]');
|
| 1665 |
+
if (!link || !sidebarEl.contains(link)) return;
|
| 1666 |
+
e.preventDefault();
|
| 1667 |
+
if (performance.now() - pointerHandledAt < 600) return;
|
| 1668 |
+
const route = link.dataset.route || link.getAttribute('href');
|
| 1669 |
+
if (route && route.startsWith('#')) navigate(route);
|
| 1670 |
+
else closeSidebarIfMobile();
|
| 1671 |
+
});
|
| 1672 |
+
|
| 1673 |
+
sidebarEl.addEventListener('pointercancel', () => { downTarget = null; });
|
| 1674 |
+
}
|
| 1675 |
+
|
| 1676 |
+
document.body.classList.remove('header-hidden');
|
| 1677 |
+
const _hdr = document.getElementById('header');
|
| 1678 |
+
if (_hdr) _hdr.classList.remove('is-hidden');
|
| 1679 |
+
const musicMain = document.querySelector('.music-main');
|
| 1680 |
+
if (musicMain && _hdr) {
|
| 1681 |
+
let lastY = 0;
|
| 1682 |
+
let ticking = false;
|
| 1683 |
+
const SCROLL_THRESHOLD = 8;
|
| 1684 |
+
const apply = () => {
|
| 1685 |
+
ticking = false;
|
| 1686 |
+
if (window.innerWidth > 1024) {
|
| 1687 |
+
|
| 1688 |
+
_hdr.classList.remove('is-hidden');
|
| 1689 |
+
document.body.classList.remove('header-hidden');
|
| 1690 |
+
return;
|
| 1691 |
+
}
|
| 1692 |
+
const y = musicMain.scrollTop;
|
| 1693 |
+
const dy = y - lastY;
|
| 1694 |
+
if (Math.abs(dy) < SCROLL_THRESHOLD) return;
|
| 1695 |
+
if (dy > 0 && y > 40) {
|
| 1696 |
+
_hdr.classList.add('is-hidden');
|
| 1697 |
+
document.body.classList.add('header-hidden');
|
| 1698 |
+
} else if (dy < 0) {
|
| 1699 |
+
_hdr.classList.remove('is-hidden');
|
| 1700 |
+
document.body.classList.remove('header-hidden');
|
| 1701 |
+
}
|
| 1702 |
+
lastY = y;
|
| 1703 |
+
};
|
| 1704 |
+
musicMain.addEventListener('scroll', () => {
|
| 1705 |
+
if (ticking) return;
|
| 1706 |
+
ticking = true;
|
| 1707 |
+
requestAnimationFrame(apply);
|
| 1708 |
+
}, { passive: true });
|
| 1709 |
+
window.addEventListener('resize', apply);
|
| 1710 |
+
}
|
| 1711 |
+
|
| 1712 |
+
window.addEventListener('hashchange', handleRoute);
|
| 1713 |
+
|
| 1714 |
+
document.addEventListener('keydown', (e) => {
|
| 1715 |
+
if (e.target.matches('input, textarea')) return;
|
| 1716 |
+
if (e.code === 'Space') { e.preventDefault(); togglePlay(); }
|
| 1717 |
+
else if (e.key === 'ArrowRight' && e.shiftKey) next(false);
|
| 1718 |
+
else if (e.key === 'ArrowLeft' && e.shiftKey) prev();
|
| 1719 |
+
});
|
| 1720 |
+
|
| 1721 |
+
handleRoute();
|
| 1722 |
+
}
|
| 1723 |
+
|
| 1724 |
+
if (document.readyState === 'loading') {
|
| 1725 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 1726 |
+
} else {
|
| 1727 |
+
init();
|
| 1728 |
+
}
|
| 1729 |
+
})();
|
frontend/index.html
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<meta name="description" content="anicove - Stream thousands of anime series free in HD. Subbed and dubbed. No ads, no limits." />
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 9 |
+
<link
|
| 10 |
+
href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800;900&family=Bebas+Neue&family=Rubik+Dirt&display=swap"
|
| 11 |
+
rel="stylesheet"
|
| 12 |
+
/>
|
| 13 |
+
<title>anicove — Anime, Manga, Movies & Music in One Place</title>
|
| 14 |
+
</head>
|
| 15 |
+
<body>
|
| 16 |
+
<div id="root"></div>
|
| 17 |
+
<script type="module" src="/src/main.jsx"></script>
|
| 18 |
+
</body>
|
| 19 |
+
</html>
|
frontend/package-lock.json
ADDED
|
@@ -0,0 +1,1747 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "anidoom-frontend",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "anidoom-frontend",
|
| 9 |
+
"version": "0.1.0",
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"hls.js": "^1.5.13",
|
| 12 |
+
"react": "^18.3.1",
|
| 13 |
+
"react-dom": "^18.3.1",
|
| 14 |
+
"react-router-dom": "^6.26.2"
|
| 15 |
+
},
|
| 16 |
+
"devDependencies": {
|
| 17 |
+
"@vitejs/plugin-react": "^4.3.1",
|
| 18 |
+
"vite": "^5.4.2"
|
| 19 |
+
}
|
| 20 |
+
},
|
| 21 |
+
"node_modules/@babel/code-frame": {
|
| 22 |
+
"version": "7.29.7",
|
| 23 |
+
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
| 24 |
+
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
|
| 25 |
+
"dev": true,
|
| 26 |
+
"license": "MIT",
|
| 27 |
+
"dependencies": {
|
| 28 |
+
"@babel/helper-validator-identifier": "^7.29.7",
|
| 29 |
+
"js-tokens": "^4.0.0",
|
| 30 |
+
"picocolors": "^1.1.1"
|
| 31 |
+
},
|
| 32 |
+
"engines": {
|
| 33 |
+
"node": ">=6.9.0"
|
| 34 |
+
}
|
| 35 |
+
},
|
| 36 |
+
"node_modules/@babel/compat-data": {
|
| 37 |
+
"version": "7.29.7",
|
| 38 |
+
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
|
| 39 |
+
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
|
| 40 |
+
"dev": true,
|
| 41 |
+
"license": "MIT",
|
| 42 |
+
"engines": {
|
| 43 |
+
"node": ">=6.9.0"
|
| 44 |
+
}
|
| 45 |
+
},
|
| 46 |
+
"node_modules/@babel/core": {
|
| 47 |
+
"version": "7.29.7",
|
| 48 |
+
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
|
| 49 |
+
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
| 50 |
+
"dev": true,
|
| 51 |
+
"license": "MIT",
|
| 52 |
+
"dependencies": {
|
| 53 |
+
"@babel/code-frame": "^7.29.7",
|
| 54 |
+
"@babel/generator": "^7.29.7",
|
| 55 |
+
"@babel/helper-compilation-targets": "^7.29.7",
|
| 56 |
+
"@babel/helper-module-transforms": "^7.29.7",
|
| 57 |
+
"@babel/helpers": "^7.29.7",
|
| 58 |
+
"@babel/parser": "^7.29.7",
|
| 59 |
+
"@babel/template": "^7.29.7",
|
| 60 |
+
"@babel/traverse": "^7.29.7",
|
| 61 |
+
"@babel/types": "^7.29.7",
|
| 62 |
+
"@jridgewell/remapping": "^2.3.5",
|
| 63 |
+
"convert-source-map": "^2.0.0",
|
| 64 |
+
"debug": "^4.1.0",
|
| 65 |
+
"gensync": "^1.0.0-beta.2",
|
| 66 |
+
"json5": "^2.2.3",
|
| 67 |
+
"semver": "^6.3.1"
|
| 68 |
+
},
|
| 69 |
+
"engines": {
|
| 70 |
+
"node": ">=6.9.0"
|
| 71 |
+
},
|
| 72 |
+
"funding": {
|
| 73 |
+
"type": "opencollective",
|
| 74 |
+
"url": "https://opencollective.com/babel"
|
| 75 |
+
}
|
| 76 |
+
},
|
| 77 |
+
"node_modules/@babel/generator": {
|
| 78 |
+
"version": "7.29.8",
|
| 79 |
+
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
|
| 80 |
+
"integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
|
| 81 |
+
"dev": true,
|
| 82 |
+
"license": "MIT",
|
| 83 |
+
"dependencies": {
|
| 84 |
+
"@babel/parser": "^7.29.8",
|
| 85 |
+
"@babel/types": "^7.29.8",
|
| 86 |
+
"@jridgewell/gen-mapping": "^0.3.12",
|
| 87 |
+
"@jridgewell/trace-mapping": "^0.3.28",
|
| 88 |
+
"jsesc": "^3.0.2"
|
| 89 |
+
},
|
| 90 |
+
"engines": {
|
| 91 |
+
"node": ">=6.9.0"
|
| 92 |
+
}
|
| 93 |
+
},
|
| 94 |
+
"node_modules/@babel/helper-compilation-targets": {
|
| 95 |
+
"version": "7.29.7",
|
| 96 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
|
| 97 |
+
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
|
| 98 |
+
"dev": true,
|
| 99 |
+
"license": "MIT",
|
| 100 |
+
"dependencies": {
|
| 101 |
+
"@babel/compat-data": "^7.29.7",
|
| 102 |
+
"@babel/helper-validator-option": "^7.29.7",
|
| 103 |
+
"browserslist": "^4.24.0",
|
| 104 |
+
"lru-cache": "^5.1.1",
|
| 105 |
+
"semver": "^6.3.1"
|
| 106 |
+
},
|
| 107 |
+
"engines": {
|
| 108 |
+
"node": ">=6.9.0"
|
| 109 |
+
}
|
| 110 |
+
},
|
| 111 |
+
"node_modules/@babel/helper-globals": {
|
| 112 |
+
"version": "7.29.7",
|
| 113 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
|
| 114 |
+
"integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
|
| 115 |
+
"dev": true,
|
| 116 |
+
"license": "MIT",
|
| 117 |
+
"engines": {
|
| 118 |
+
"node": ">=6.9.0"
|
| 119 |
+
}
|
| 120 |
+
},
|
| 121 |
+
"node_modules/@babel/helper-module-imports": {
|
| 122 |
+
"version": "7.29.7",
|
| 123 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
|
| 124 |
+
"integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
|
| 125 |
+
"dev": true,
|
| 126 |
+
"license": "MIT",
|
| 127 |
+
"dependencies": {
|
| 128 |
+
"@babel/traverse": "^7.29.7",
|
| 129 |
+
"@babel/types": "^7.29.7"
|
| 130 |
+
},
|
| 131 |
+
"engines": {
|
| 132 |
+
"node": ">=6.9.0"
|
| 133 |
+
}
|
| 134 |
+
},
|
| 135 |
+
"node_modules/@babel/helper-module-transforms": {
|
| 136 |
+
"version": "7.29.7",
|
| 137 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
|
| 138 |
+
"integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
|
| 139 |
+
"dev": true,
|
| 140 |
+
"license": "MIT",
|
| 141 |
+
"dependencies": {
|
| 142 |
+
"@babel/helper-module-imports": "^7.29.7",
|
| 143 |
+
"@babel/helper-validator-identifier": "^7.29.7",
|
| 144 |
+
"@babel/traverse": "^7.29.7"
|
| 145 |
+
},
|
| 146 |
+
"engines": {
|
| 147 |
+
"node": ">=6.9.0"
|
| 148 |
+
},
|
| 149 |
+
"peerDependencies": {
|
| 150 |
+
"@babel/core": "^7.0.0"
|
| 151 |
+
}
|
| 152 |
+
},
|
| 153 |
+
"node_modules/@babel/helper-plugin-utils": {
|
| 154 |
+
"version": "7.29.7",
|
| 155 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
|
| 156 |
+
"integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
|
| 157 |
+
"dev": true,
|
| 158 |
+
"license": "MIT",
|
| 159 |
+
"engines": {
|
| 160 |
+
"node": ">=6.9.0"
|
| 161 |
+
}
|
| 162 |
+
},
|
| 163 |
+
"node_modules/@babel/helper-string-parser": {
|
| 164 |
+
"version": "7.29.7",
|
| 165 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
| 166 |
+
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
| 167 |
+
"dev": true,
|
| 168 |
+
"license": "MIT",
|
| 169 |
+
"engines": {
|
| 170 |
+
"node": ">=6.9.0"
|
| 171 |
+
}
|
| 172 |
+
},
|
| 173 |
+
"node_modules/@babel/helper-validator-identifier": {
|
| 174 |
+
"version": "7.29.7",
|
| 175 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
| 176 |
+
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
| 177 |
+
"dev": true,
|
| 178 |
+
"license": "MIT",
|
| 179 |
+
"engines": {
|
| 180 |
+
"node": ">=6.9.0"
|
| 181 |
+
}
|
| 182 |
+
},
|
| 183 |
+
"node_modules/@babel/helper-validator-option": {
|
| 184 |
+
"version": "7.29.7",
|
| 185 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
|
| 186 |
+
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
|
| 187 |
+
"dev": true,
|
| 188 |
+
"license": "MIT",
|
| 189 |
+
"engines": {
|
| 190 |
+
"node": ">=6.9.0"
|
| 191 |
+
}
|
| 192 |
+
},
|
| 193 |
+
"node_modules/@babel/helpers": {
|
| 194 |
+
"version": "7.29.7",
|
| 195 |
+
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
|
| 196 |
+
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
|
| 197 |
+
"dev": true,
|
| 198 |
+
"license": "MIT",
|
| 199 |
+
"dependencies": {
|
| 200 |
+
"@babel/template": "^7.29.7",
|
| 201 |
+
"@babel/types": "^7.29.7"
|
| 202 |
+
},
|
| 203 |
+
"engines": {
|
| 204 |
+
"node": ">=6.9.0"
|
| 205 |
+
}
|
| 206 |
+
},
|
| 207 |
+
"node_modules/@babel/parser": {
|
| 208 |
+
"version": "7.29.8",
|
| 209 |
+
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
|
| 210 |
+
"integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
|
| 211 |
+
"dev": true,
|
| 212 |
+
"license": "MIT",
|
| 213 |
+
"dependencies": {
|
| 214 |
+
"@babel/types": "^7.29.8"
|
| 215 |
+
},
|
| 216 |
+
"bin": {
|
| 217 |
+
"parser": "bin/babel-parser.js"
|
| 218 |
+
},
|
| 219 |
+
"engines": {
|
| 220 |
+
"node": ">=6.0.0"
|
| 221 |
+
}
|
| 222 |
+
},
|
| 223 |
+
"node_modules/@babel/plugin-transform-react-jsx-self": {
|
| 224 |
+
"version": "7.29.7",
|
| 225 |
+
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
|
| 226 |
+
"integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
|
| 227 |
+
"dev": true,
|
| 228 |
+
"license": "MIT",
|
| 229 |
+
"dependencies": {
|
| 230 |
+
"@babel/helper-plugin-utils": "^7.29.7"
|
| 231 |
+
},
|
| 232 |
+
"engines": {
|
| 233 |
+
"node": ">=6.9.0"
|
| 234 |
+
},
|
| 235 |
+
"peerDependencies": {
|
| 236 |
+
"@babel/core": "^7.0.0-0"
|
| 237 |
+
}
|
| 238 |
+
},
|
| 239 |
+
"node_modules/@babel/plugin-transform-react-jsx-source": {
|
| 240 |
+
"version": "7.29.7",
|
| 241 |
+
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
|
| 242 |
+
"integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
|
| 243 |
+
"dev": true,
|
| 244 |
+
"license": "MIT",
|
| 245 |
+
"dependencies": {
|
| 246 |
+
"@babel/helper-plugin-utils": "^7.29.7"
|
| 247 |
+
},
|
| 248 |
+
"engines": {
|
| 249 |
+
"node": ">=6.9.0"
|
| 250 |
+
},
|
| 251 |
+
"peerDependencies": {
|
| 252 |
+
"@babel/core": "^7.0.0-0"
|
| 253 |
+
}
|
| 254 |
+
},
|
| 255 |
+
"node_modules/@babel/template": {
|
| 256 |
+
"version": "7.29.7",
|
| 257 |
+
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
| 258 |
+
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
|
| 259 |
+
"dev": true,
|
| 260 |
+
"license": "MIT",
|
| 261 |
+
"dependencies": {
|
| 262 |
+
"@babel/code-frame": "^7.29.7",
|
| 263 |
+
"@babel/parser": "^7.29.7",
|
| 264 |
+
"@babel/types": "^7.29.7"
|
| 265 |
+
},
|
| 266 |
+
"engines": {
|
| 267 |
+
"node": ">=6.9.0"
|
| 268 |
+
}
|
| 269 |
+
},
|
| 270 |
+
"node_modules/@babel/traverse": {
|
| 271 |
+
"version": "7.29.8",
|
| 272 |
+
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
|
| 273 |
+
"integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
|
| 274 |
+
"dev": true,
|
| 275 |
+
"license": "MIT",
|
| 276 |
+
"dependencies": {
|
| 277 |
+
"@babel/code-frame": "^7.29.7",
|
| 278 |
+
"@babel/generator": "^7.29.8",
|
| 279 |
+
"@babel/helper-globals": "^7.29.7",
|
| 280 |
+
"@babel/parser": "^7.29.8",
|
| 281 |
+
"@babel/template": "^7.29.7",
|
| 282 |
+
"@babel/types": "^7.29.8",
|
| 283 |
+
"debug": "^4.3.1"
|
| 284 |
+
},
|
| 285 |
+
"engines": {
|
| 286 |
+
"node": ">=6.9.0"
|
| 287 |
+
}
|
| 288 |
+
},
|
| 289 |
+
"node_modules/@babel/types": {
|
| 290 |
+
"version": "7.29.8",
|
| 291 |
+
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
|
| 292 |
+
"integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
|
| 293 |
+
"dev": true,
|
| 294 |
+
"license": "MIT",
|
| 295 |
+
"dependencies": {
|
| 296 |
+
"@babel/helper-string-parser": "^7.29.7",
|
| 297 |
+
"@babel/helper-validator-identifier": "^7.29.7"
|
| 298 |
+
},
|
| 299 |
+
"engines": {
|
| 300 |
+
"node": ">=6.9.0"
|
| 301 |
+
}
|
| 302 |
+
},
|
| 303 |
+
"node_modules/@esbuild/aix-ppc64": {
|
| 304 |
+
"version": "0.21.5",
|
| 305 |
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
| 306 |
+
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
| 307 |
+
"cpu": [
|
| 308 |
+
"ppc64"
|
| 309 |
+
],
|
| 310 |
+
"dev": true,
|
| 311 |
+
"license": "MIT",
|
| 312 |
+
"optional": true,
|
| 313 |
+
"os": [
|
| 314 |
+
"aix"
|
| 315 |
+
],
|
| 316 |
+
"engines": {
|
| 317 |
+
"node": ">=12"
|
| 318 |
+
}
|
| 319 |
+
},
|
| 320 |
+
"node_modules/@esbuild/android-arm": {
|
| 321 |
+
"version": "0.21.5",
|
| 322 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
| 323 |
+
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
| 324 |
+
"cpu": [
|
| 325 |
+
"arm"
|
| 326 |
+
],
|
| 327 |
+
"dev": true,
|
| 328 |
+
"license": "MIT",
|
| 329 |
+
"optional": true,
|
| 330 |
+
"os": [
|
| 331 |
+
"android"
|
| 332 |
+
],
|
| 333 |
+
"engines": {
|
| 334 |
+
"node": ">=12"
|
| 335 |
+
}
|
| 336 |
+
},
|
| 337 |
+
"node_modules/@esbuild/android-arm64": {
|
| 338 |
+
"version": "0.21.5",
|
| 339 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
| 340 |
+
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
| 341 |
+
"cpu": [
|
| 342 |
+
"arm64"
|
| 343 |
+
],
|
| 344 |
+
"dev": true,
|
| 345 |
+
"license": "MIT",
|
| 346 |
+
"optional": true,
|
| 347 |
+
"os": [
|
| 348 |
+
"android"
|
| 349 |
+
],
|
| 350 |
+
"engines": {
|
| 351 |
+
"node": ">=12"
|
| 352 |
+
}
|
| 353 |
+
},
|
| 354 |
+
"node_modules/@esbuild/android-x64": {
|
| 355 |
+
"version": "0.21.5",
|
| 356 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
| 357 |
+
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
| 358 |
+
"cpu": [
|
| 359 |
+
"x64"
|
| 360 |
+
],
|
| 361 |
+
"dev": true,
|
| 362 |
+
"license": "MIT",
|
| 363 |
+
"optional": true,
|
| 364 |
+
"os": [
|
| 365 |
+
"android"
|
| 366 |
+
],
|
| 367 |
+
"engines": {
|
| 368 |
+
"node": ">=12"
|
| 369 |
+
}
|
| 370 |
+
},
|
| 371 |
+
"node_modules/@esbuild/darwin-arm64": {
|
| 372 |
+
"version": "0.21.5",
|
| 373 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
| 374 |
+
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
| 375 |
+
"cpu": [
|
| 376 |
+
"arm64"
|
| 377 |
+
],
|
| 378 |
+
"dev": true,
|
| 379 |
+
"license": "MIT",
|
| 380 |
+
"optional": true,
|
| 381 |
+
"os": [
|
| 382 |
+
"darwin"
|
| 383 |
+
],
|
| 384 |
+
"engines": {
|
| 385 |
+
"node": ">=12"
|
| 386 |
+
}
|
| 387 |
+
},
|
| 388 |
+
"node_modules/@esbuild/darwin-x64": {
|
| 389 |
+
"version": "0.21.5",
|
| 390 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
| 391 |
+
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
| 392 |
+
"cpu": [
|
| 393 |
+
"x64"
|
| 394 |
+
],
|
| 395 |
+
"dev": true,
|
| 396 |
+
"license": "MIT",
|
| 397 |
+
"optional": true,
|
| 398 |
+
"os": [
|
| 399 |
+
"darwin"
|
| 400 |
+
],
|
| 401 |
+
"engines": {
|
| 402 |
+
"node": ">=12"
|
| 403 |
+
}
|
| 404 |
+
},
|
| 405 |
+
"node_modules/@esbuild/freebsd-arm64": {
|
| 406 |
+
"version": "0.21.5",
|
| 407 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
| 408 |
+
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
| 409 |
+
"cpu": [
|
| 410 |
+
"arm64"
|
| 411 |
+
],
|
| 412 |
+
"dev": true,
|
| 413 |
+
"license": "MIT",
|
| 414 |
+
"optional": true,
|
| 415 |
+
"os": [
|
| 416 |
+
"freebsd"
|
| 417 |
+
],
|
| 418 |
+
"engines": {
|
| 419 |
+
"node": ">=12"
|
| 420 |
+
}
|
| 421 |
+
},
|
| 422 |
+
"node_modules/@esbuild/freebsd-x64": {
|
| 423 |
+
"version": "0.21.5",
|
| 424 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
| 425 |
+
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
| 426 |
+
"cpu": [
|
| 427 |
+
"x64"
|
| 428 |
+
],
|
| 429 |
+
"dev": true,
|
| 430 |
+
"license": "MIT",
|
| 431 |
+
"optional": true,
|
| 432 |
+
"os": [
|
| 433 |
+
"freebsd"
|
| 434 |
+
],
|
| 435 |
+
"engines": {
|
| 436 |
+
"node": ">=12"
|
| 437 |
+
}
|
| 438 |
+
},
|
| 439 |
+
"node_modules/@esbuild/linux-arm": {
|
| 440 |
+
"version": "0.21.5",
|
| 441 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
| 442 |
+
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
| 443 |
+
"cpu": [
|
| 444 |
+
"arm"
|
| 445 |
+
],
|
| 446 |
+
"dev": true,
|
| 447 |
+
"license": "MIT",
|
| 448 |
+
"optional": true,
|
| 449 |
+
"os": [
|
| 450 |
+
"linux"
|
| 451 |
+
],
|
| 452 |
+
"engines": {
|
| 453 |
+
"node": ">=12"
|
| 454 |
+
}
|
| 455 |
+
},
|
| 456 |
+
"node_modules/@esbuild/linux-arm64": {
|
| 457 |
+
"version": "0.21.5",
|
| 458 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
| 459 |
+
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
| 460 |
+
"cpu": [
|
| 461 |
+
"arm64"
|
| 462 |
+
],
|
| 463 |
+
"dev": true,
|
| 464 |
+
"license": "MIT",
|
| 465 |
+
"optional": true,
|
| 466 |
+
"os": [
|
| 467 |
+
"linux"
|
| 468 |
+
],
|
| 469 |
+
"engines": {
|
| 470 |
+
"node": ">=12"
|
| 471 |
+
}
|
| 472 |
+
},
|
| 473 |
+
"node_modules/@esbuild/linux-ia32": {
|
| 474 |
+
"version": "0.21.5",
|
| 475 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
| 476 |
+
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
| 477 |
+
"cpu": [
|
| 478 |
+
"ia32"
|
| 479 |
+
],
|
| 480 |
+
"dev": true,
|
| 481 |
+
"license": "MIT",
|
| 482 |
+
"optional": true,
|
| 483 |
+
"os": [
|
| 484 |
+
"linux"
|
| 485 |
+
],
|
| 486 |
+
"engines": {
|
| 487 |
+
"node": ">=12"
|
| 488 |
+
}
|
| 489 |
+
},
|
| 490 |
+
"node_modules/@esbuild/linux-loong64": {
|
| 491 |
+
"version": "0.21.5",
|
| 492 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
| 493 |
+
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
| 494 |
+
"cpu": [
|
| 495 |
+
"loong64"
|
| 496 |
+
],
|
| 497 |
+
"dev": true,
|
| 498 |
+
"license": "MIT",
|
| 499 |
+
"optional": true,
|
| 500 |
+
"os": [
|
| 501 |
+
"linux"
|
| 502 |
+
],
|
| 503 |
+
"engines": {
|
| 504 |
+
"node": ">=12"
|
| 505 |
+
}
|
| 506 |
+
},
|
| 507 |
+
"node_modules/@esbuild/linux-mips64el": {
|
| 508 |
+
"version": "0.21.5",
|
| 509 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
| 510 |
+
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
| 511 |
+
"cpu": [
|
| 512 |
+
"mips64el"
|
| 513 |
+
],
|
| 514 |
+
"dev": true,
|
| 515 |
+
"license": "MIT",
|
| 516 |
+
"optional": true,
|
| 517 |
+
"os": [
|
| 518 |
+
"linux"
|
| 519 |
+
],
|
| 520 |
+
"engines": {
|
| 521 |
+
"node": ">=12"
|
| 522 |
+
}
|
| 523 |
+
},
|
| 524 |
+
"node_modules/@esbuild/linux-ppc64": {
|
| 525 |
+
"version": "0.21.5",
|
| 526 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
| 527 |
+
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
| 528 |
+
"cpu": [
|
| 529 |
+
"ppc64"
|
| 530 |
+
],
|
| 531 |
+
"dev": true,
|
| 532 |
+
"license": "MIT",
|
| 533 |
+
"optional": true,
|
| 534 |
+
"os": [
|
| 535 |
+
"linux"
|
| 536 |
+
],
|
| 537 |
+
"engines": {
|
| 538 |
+
"node": ">=12"
|
| 539 |
+
}
|
| 540 |
+
},
|
| 541 |
+
"node_modules/@esbuild/linux-riscv64": {
|
| 542 |
+
"version": "0.21.5",
|
| 543 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
| 544 |
+
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
| 545 |
+
"cpu": [
|
| 546 |
+
"riscv64"
|
| 547 |
+
],
|
| 548 |
+
"dev": true,
|
| 549 |
+
"license": "MIT",
|
| 550 |
+
"optional": true,
|
| 551 |
+
"os": [
|
| 552 |
+
"linux"
|
| 553 |
+
],
|
| 554 |
+
"engines": {
|
| 555 |
+
"node": ">=12"
|
| 556 |
+
}
|
| 557 |
+
},
|
| 558 |
+
"node_modules/@esbuild/linux-s390x": {
|
| 559 |
+
"version": "0.21.5",
|
| 560 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
| 561 |
+
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
| 562 |
+
"cpu": [
|
| 563 |
+
"s390x"
|
| 564 |
+
],
|
| 565 |
+
"dev": true,
|
| 566 |
+
"license": "MIT",
|
| 567 |
+
"optional": true,
|
| 568 |
+
"os": [
|
| 569 |
+
"linux"
|
| 570 |
+
],
|
| 571 |
+
"engines": {
|
| 572 |
+
"node": ">=12"
|
| 573 |
+
}
|
| 574 |
+
},
|
| 575 |
+
"node_modules/@esbuild/linux-x64": {
|
| 576 |
+
"version": "0.21.5",
|
| 577 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
| 578 |
+
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
| 579 |
+
"cpu": [
|
| 580 |
+
"x64"
|
| 581 |
+
],
|
| 582 |
+
"dev": true,
|
| 583 |
+
"license": "MIT",
|
| 584 |
+
"optional": true,
|
| 585 |
+
"os": [
|
| 586 |
+
"linux"
|
| 587 |
+
],
|
| 588 |
+
"engines": {
|
| 589 |
+
"node": ">=12"
|
| 590 |
+
}
|
| 591 |
+
},
|
| 592 |
+
"node_modules/@esbuild/netbsd-x64": {
|
| 593 |
+
"version": "0.21.5",
|
| 594 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
| 595 |
+
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
| 596 |
+
"cpu": [
|
| 597 |
+
"x64"
|
| 598 |
+
],
|
| 599 |
+
"dev": true,
|
| 600 |
+
"license": "MIT",
|
| 601 |
+
"optional": true,
|
| 602 |
+
"os": [
|
| 603 |
+
"netbsd"
|
| 604 |
+
],
|
| 605 |
+
"engines": {
|
| 606 |
+
"node": ">=12"
|
| 607 |
+
}
|
| 608 |
+
},
|
| 609 |
+
"node_modules/@esbuild/openbsd-x64": {
|
| 610 |
+
"version": "0.21.5",
|
| 611 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
| 612 |
+
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
| 613 |
+
"cpu": [
|
| 614 |
+
"x64"
|
| 615 |
+
],
|
| 616 |
+
"dev": true,
|
| 617 |
+
"license": "MIT",
|
| 618 |
+
"optional": true,
|
| 619 |
+
"os": [
|
| 620 |
+
"openbsd"
|
| 621 |
+
],
|
| 622 |
+
"engines": {
|
| 623 |
+
"node": ">=12"
|
| 624 |
+
}
|
| 625 |
+
},
|
| 626 |
+
"node_modules/@esbuild/sunos-x64": {
|
| 627 |
+
"version": "0.21.5",
|
| 628 |
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
| 629 |
+
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
| 630 |
+
"cpu": [
|
| 631 |
+
"x64"
|
| 632 |
+
],
|
| 633 |
+
"dev": true,
|
| 634 |
+
"license": "MIT",
|
| 635 |
+
"optional": true,
|
| 636 |
+
"os": [
|
| 637 |
+
"sunos"
|
| 638 |
+
],
|
| 639 |
+
"engines": {
|
| 640 |
+
"node": ">=12"
|
| 641 |
+
}
|
| 642 |
+
},
|
| 643 |
+
"node_modules/@esbuild/win32-arm64": {
|
| 644 |
+
"version": "0.21.5",
|
| 645 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
| 646 |
+
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
| 647 |
+
"cpu": [
|
| 648 |
+
"arm64"
|
| 649 |
+
],
|
| 650 |
+
"dev": true,
|
| 651 |
+
"license": "MIT",
|
| 652 |
+
"optional": true,
|
| 653 |
+
"os": [
|
| 654 |
+
"win32"
|
| 655 |
+
],
|
| 656 |
+
"engines": {
|
| 657 |
+
"node": ">=12"
|
| 658 |
+
}
|
| 659 |
+
},
|
| 660 |
+
"node_modules/@esbuild/win32-ia32": {
|
| 661 |
+
"version": "0.21.5",
|
| 662 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
| 663 |
+
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
| 664 |
+
"cpu": [
|
| 665 |
+
"ia32"
|
| 666 |
+
],
|
| 667 |
+
"dev": true,
|
| 668 |
+
"license": "MIT",
|
| 669 |
+
"optional": true,
|
| 670 |
+
"os": [
|
| 671 |
+
"win32"
|
| 672 |
+
],
|
| 673 |
+
"engines": {
|
| 674 |
+
"node": ">=12"
|
| 675 |
+
}
|
| 676 |
+
},
|
| 677 |
+
"node_modules/@esbuild/win32-x64": {
|
| 678 |
+
"version": "0.21.5",
|
| 679 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
| 680 |
+
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
| 681 |
+
"cpu": [
|
| 682 |
+
"x64"
|
| 683 |
+
],
|
| 684 |
+
"dev": true,
|
| 685 |
+
"license": "MIT",
|
| 686 |
+
"optional": true,
|
| 687 |
+
"os": [
|
| 688 |
+
"win32"
|
| 689 |
+
],
|
| 690 |
+
"engines": {
|
| 691 |
+
"node": ">=12"
|
| 692 |
+
}
|
| 693 |
+
},
|
| 694 |
+
"node_modules/@jridgewell/gen-mapping": {
|
| 695 |
+
"version": "0.3.13",
|
| 696 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
| 697 |
+
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
| 698 |
+
"dev": true,
|
| 699 |
+
"license": "MIT",
|
| 700 |
+
"dependencies": {
|
| 701 |
+
"@jridgewell/sourcemap-codec": "^1.5.0",
|
| 702 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 703 |
+
}
|
| 704 |
+
},
|
| 705 |
+
"node_modules/@jridgewell/remapping": {
|
| 706 |
+
"version": "2.3.5",
|
| 707 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
| 708 |
+
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
| 709 |
+
"dev": true,
|
| 710 |
+
"license": "MIT",
|
| 711 |
+
"dependencies": {
|
| 712 |
+
"@jridgewell/gen-mapping": "^0.3.5",
|
| 713 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 714 |
+
}
|
| 715 |
+
},
|
| 716 |
+
"node_modules/@jridgewell/resolve-uri": {
|
| 717 |
+
"version": "3.1.2",
|
| 718 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
| 719 |
+
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
| 720 |
+
"dev": true,
|
| 721 |
+
"license": "MIT",
|
| 722 |
+
"engines": {
|
| 723 |
+
"node": ">=6.0.0"
|
| 724 |
+
}
|
| 725 |
+
},
|
| 726 |
+
"node_modules/@jridgewell/sourcemap-codec": {
|
| 727 |
+
"version": "1.5.5",
|
| 728 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
| 729 |
+
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
| 730 |
+
"dev": true,
|
| 731 |
+
"license": "MIT"
|
| 732 |
+
},
|
| 733 |
+
"node_modules/@jridgewell/trace-mapping": {
|
| 734 |
+
"version": "0.3.31",
|
| 735 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
| 736 |
+
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
| 737 |
+
"dev": true,
|
| 738 |
+
"license": "MIT",
|
| 739 |
+
"dependencies": {
|
| 740 |
+
"@jridgewell/resolve-uri": "^3.1.0",
|
| 741 |
+
"@jridgewell/sourcemap-codec": "^1.4.14"
|
| 742 |
+
}
|
| 743 |
+
},
|
| 744 |
+
"node_modules/@napi-rs/lzma-linux-x64-gnu": {
|
| 745 |
+
"version": "1.5.1",
|
| 746 |
+
"resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
|
| 747 |
+
"integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
|
| 748 |
+
"cpu": [
|
| 749 |
+
"x64"
|
| 750 |
+
],
|
| 751 |
+
"dev": true,
|
| 752 |
+
"license": "MIT",
|
| 753 |
+
"optional": true,
|
| 754 |
+
"os": [
|
| 755 |
+
"linux"
|
| 756 |
+
],
|
| 757 |
+
"engines": {
|
| 758 |
+
"node": "^22.20 || ^24.12 || >=25"
|
| 759 |
+
}
|
| 760 |
+
},
|
| 761 |
+
"node_modules/@remix-run/router": {
|
| 762 |
+
"version": "1.23.3",
|
| 763 |
+
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz",
|
| 764 |
+
"integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==",
|
| 765 |
+
"license": "MIT",
|
| 766 |
+
"engines": {
|
| 767 |
+
"node": ">=14.0.0"
|
| 768 |
+
}
|
| 769 |
+
},
|
| 770 |
+
"node_modules/@rolldown/pluginutils": {
|
| 771 |
+
"version": "1.0.0-beta.27",
|
| 772 |
+
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
| 773 |
+
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
|
| 774 |
+
"dev": true,
|
| 775 |
+
"license": "MIT"
|
| 776 |
+
},
|
| 777 |
+
"node_modules/@rollup/rollup-android-arm-eabi": {
|
| 778 |
+
"version": "4.62.4",
|
| 779 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz",
|
| 780 |
+
"integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==",
|
| 781 |
+
"cpu": [
|
| 782 |
+
"arm"
|
| 783 |
+
],
|
| 784 |
+
"dev": true,
|
| 785 |
+
"license": "MIT",
|
| 786 |
+
"optional": true,
|
| 787 |
+
"os": [
|
| 788 |
+
"android"
|
| 789 |
+
]
|
| 790 |
+
},
|
| 791 |
+
"node_modules/@rollup/rollup-android-arm64": {
|
| 792 |
+
"version": "4.62.4",
|
| 793 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz",
|
| 794 |
+
"integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==",
|
| 795 |
+
"cpu": [
|
| 796 |
+
"arm64"
|
| 797 |
+
],
|
| 798 |
+
"dev": true,
|
| 799 |
+
"license": "MIT",
|
| 800 |
+
"optional": true,
|
| 801 |
+
"os": [
|
| 802 |
+
"android"
|
| 803 |
+
]
|
| 804 |
+
},
|
| 805 |
+
"node_modules/@rollup/rollup-darwin-arm64": {
|
| 806 |
+
"version": "4.62.4",
|
| 807 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz",
|
| 808 |
+
"integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==",
|
| 809 |
+
"cpu": [
|
| 810 |
+
"arm64"
|
| 811 |
+
],
|
| 812 |
+
"dev": true,
|
| 813 |
+
"license": "MIT",
|
| 814 |
+
"optional": true,
|
| 815 |
+
"os": [
|
| 816 |
+
"darwin"
|
| 817 |
+
]
|
| 818 |
+
},
|
| 819 |
+
"node_modules/@rollup/rollup-darwin-x64": {
|
| 820 |
+
"version": "4.62.4",
|
| 821 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz",
|
| 822 |
+
"integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==",
|
| 823 |
+
"cpu": [
|
| 824 |
+
"x64"
|
| 825 |
+
],
|
| 826 |
+
"dev": true,
|
| 827 |
+
"license": "MIT",
|
| 828 |
+
"optional": true,
|
| 829 |
+
"os": [
|
| 830 |
+
"darwin"
|
| 831 |
+
]
|
| 832 |
+
},
|
| 833 |
+
"node_modules/@rollup/rollup-freebsd-arm64": {
|
| 834 |
+
"version": "4.62.4",
|
| 835 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz",
|
| 836 |
+
"integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==",
|
| 837 |
+
"cpu": [
|
| 838 |
+
"arm64"
|
| 839 |
+
],
|
| 840 |
+
"dev": true,
|
| 841 |
+
"license": "MIT",
|
| 842 |
+
"optional": true,
|
| 843 |
+
"os": [
|
| 844 |
+
"freebsd"
|
| 845 |
+
]
|
| 846 |
+
},
|
| 847 |
+
"node_modules/@rollup/rollup-freebsd-x64": {
|
| 848 |
+
"version": "4.62.4",
|
| 849 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz",
|
| 850 |
+
"integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==",
|
| 851 |
+
"cpu": [
|
| 852 |
+
"x64"
|
| 853 |
+
],
|
| 854 |
+
"dev": true,
|
| 855 |
+
"license": "MIT",
|
| 856 |
+
"optional": true,
|
| 857 |
+
"os": [
|
| 858 |
+
"freebsd"
|
| 859 |
+
]
|
| 860 |
+
},
|
| 861 |
+
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
| 862 |
+
"version": "4.62.4",
|
| 863 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz",
|
| 864 |
+
"integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==",
|
| 865 |
+
"cpu": [
|
| 866 |
+
"arm"
|
| 867 |
+
],
|
| 868 |
+
"dev": true,
|
| 869 |
+
"license": "MIT",
|
| 870 |
+
"optional": true,
|
| 871 |
+
"os": [
|
| 872 |
+
"linux"
|
| 873 |
+
]
|
| 874 |
+
},
|
| 875 |
+
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
| 876 |
+
"version": "4.62.4",
|
| 877 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz",
|
| 878 |
+
"integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==",
|
| 879 |
+
"cpu": [
|
| 880 |
+
"arm"
|
| 881 |
+
],
|
| 882 |
+
"dev": true,
|
| 883 |
+
"license": "MIT",
|
| 884 |
+
"optional": true,
|
| 885 |
+
"os": [
|
| 886 |
+
"linux"
|
| 887 |
+
]
|
| 888 |
+
},
|
| 889 |
+
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
| 890 |
+
"version": "4.62.4",
|
| 891 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz",
|
| 892 |
+
"integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==",
|
| 893 |
+
"cpu": [
|
| 894 |
+
"arm64"
|
| 895 |
+
],
|
| 896 |
+
"dev": true,
|
| 897 |
+
"license": "MIT",
|
| 898 |
+
"optional": true,
|
| 899 |
+
"os": [
|
| 900 |
+
"linux"
|
| 901 |
+
]
|
| 902 |
+
},
|
| 903 |
+
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
| 904 |
+
"version": "4.62.4",
|
| 905 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz",
|
| 906 |
+
"integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==",
|
| 907 |
+
"cpu": [
|
| 908 |
+
"arm64"
|
| 909 |
+
],
|
| 910 |
+
"dev": true,
|
| 911 |
+
"license": "MIT",
|
| 912 |
+
"optional": true,
|
| 913 |
+
"os": [
|
| 914 |
+
"linux"
|
| 915 |
+
]
|
| 916 |
+
},
|
| 917 |
+
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
| 918 |
+
"version": "4.62.4",
|
| 919 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz",
|
| 920 |
+
"integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==",
|
| 921 |
+
"cpu": [
|
| 922 |
+
"loong64"
|
| 923 |
+
],
|
| 924 |
+
"dev": true,
|
| 925 |
+
"license": "MIT",
|
| 926 |
+
"optional": true,
|
| 927 |
+
"os": [
|
| 928 |
+
"linux"
|
| 929 |
+
]
|
| 930 |
+
},
|
| 931 |
+
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
| 932 |
+
"version": "4.62.4",
|
| 933 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz",
|
| 934 |
+
"integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==",
|
| 935 |
+
"cpu": [
|
| 936 |
+
"loong64"
|
| 937 |
+
],
|
| 938 |
+
"dev": true,
|
| 939 |
+
"license": "MIT",
|
| 940 |
+
"optional": true,
|
| 941 |
+
"os": [
|
| 942 |
+
"linux"
|
| 943 |
+
]
|
| 944 |
+
},
|
| 945 |
+
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
| 946 |
+
"version": "4.62.4",
|
| 947 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz",
|
| 948 |
+
"integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==",
|
| 949 |
+
"cpu": [
|
| 950 |
+
"ppc64"
|
| 951 |
+
],
|
| 952 |
+
"dev": true,
|
| 953 |
+
"license": "MIT",
|
| 954 |
+
"optional": true,
|
| 955 |
+
"os": [
|
| 956 |
+
"linux"
|
| 957 |
+
]
|
| 958 |
+
},
|
| 959 |
+
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
| 960 |
+
"version": "4.62.4",
|
| 961 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz",
|
| 962 |
+
"integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==",
|
| 963 |
+
"cpu": [
|
| 964 |
+
"ppc64"
|
| 965 |
+
],
|
| 966 |
+
"dev": true,
|
| 967 |
+
"license": "MIT",
|
| 968 |
+
"optional": true,
|
| 969 |
+
"os": [
|
| 970 |
+
"linux"
|
| 971 |
+
]
|
| 972 |
+
},
|
| 973 |
+
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
| 974 |
+
"version": "4.62.4",
|
| 975 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz",
|
| 976 |
+
"integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==",
|
| 977 |
+
"cpu": [
|
| 978 |
+
"riscv64"
|
| 979 |
+
],
|
| 980 |
+
"dev": true,
|
| 981 |
+
"license": "MIT",
|
| 982 |
+
"optional": true,
|
| 983 |
+
"os": [
|
| 984 |
+
"linux"
|
| 985 |
+
]
|
| 986 |
+
},
|
| 987 |
+
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
| 988 |
+
"version": "4.62.4",
|
| 989 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz",
|
| 990 |
+
"integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==",
|
| 991 |
+
"cpu": [
|
| 992 |
+
"riscv64"
|
| 993 |
+
],
|
| 994 |
+
"dev": true,
|
| 995 |
+
"license": "MIT",
|
| 996 |
+
"optional": true,
|
| 997 |
+
"os": [
|
| 998 |
+
"linux"
|
| 999 |
+
]
|
| 1000 |
+
},
|
| 1001 |
+
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
| 1002 |
+
"version": "4.62.4",
|
| 1003 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz",
|
| 1004 |
+
"integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==",
|
| 1005 |
+
"cpu": [
|
| 1006 |
+
"s390x"
|
| 1007 |
+
],
|
| 1008 |
+
"dev": true,
|
| 1009 |
+
"license": "MIT",
|
| 1010 |
+
"optional": true,
|
| 1011 |
+
"os": [
|
| 1012 |
+
"linux"
|
| 1013 |
+
]
|
| 1014 |
+
},
|
| 1015 |
+
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
| 1016 |
+
"version": "4.62.4",
|
| 1017 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz",
|
| 1018 |
+
"integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==",
|
| 1019 |
+
"cpu": [
|
| 1020 |
+
"x64"
|
| 1021 |
+
],
|
| 1022 |
+
"dev": true,
|
| 1023 |
+
"license": "MIT",
|
| 1024 |
+
"optional": true,
|
| 1025 |
+
"os": [
|
| 1026 |
+
"linux"
|
| 1027 |
+
]
|
| 1028 |
+
},
|
| 1029 |
+
"node_modules/@rollup/rollup-linux-x64-musl": {
|
| 1030 |
+
"version": "4.62.4",
|
| 1031 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz",
|
| 1032 |
+
"integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==",
|
| 1033 |
+
"cpu": [
|
| 1034 |
+
"x64"
|
| 1035 |
+
],
|
| 1036 |
+
"dev": true,
|
| 1037 |
+
"license": "MIT",
|
| 1038 |
+
"optional": true,
|
| 1039 |
+
"os": [
|
| 1040 |
+
"linux"
|
| 1041 |
+
]
|
| 1042 |
+
},
|
| 1043 |
+
"node_modules/@rollup/rollup-openbsd-x64": {
|
| 1044 |
+
"version": "4.62.4",
|
| 1045 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz",
|
| 1046 |
+
"integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==",
|
| 1047 |
+
"cpu": [
|
| 1048 |
+
"x64"
|
| 1049 |
+
],
|
| 1050 |
+
"dev": true,
|
| 1051 |
+
"license": "MIT",
|
| 1052 |
+
"optional": true,
|
| 1053 |
+
"os": [
|
| 1054 |
+
"openbsd"
|
| 1055 |
+
]
|
| 1056 |
+
},
|
| 1057 |
+
"node_modules/@rollup/rollup-openharmony-arm64": {
|
| 1058 |
+
"version": "4.62.4",
|
| 1059 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz",
|
| 1060 |
+
"integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==",
|
| 1061 |
+
"cpu": [
|
| 1062 |
+
"arm64"
|
| 1063 |
+
],
|
| 1064 |
+
"dev": true,
|
| 1065 |
+
"license": "MIT",
|
| 1066 |
+
"optional": true,
|
| 1067 |
+
"os": [
|
| 1068 |
+
"openharmony"
|
| 1069 |
+
]
|
| 1070 |
+
},
|
| 1071 |
+
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
| 1072 |
+
"version": "4.62.4",
|
| 1073 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz",
|
| 1074 |
+
"integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==",
|
| 1075 |
+
"cpu": [
|
| 1076 |
+
"arm64"
|
| 1077 |
+
],
|
| 1078 |
+
"dev": true,
|
| 1079 |
+
"license": "MIT",
|
| 1080 |
+
"optional": true,
|
| 1081 |
+
"os": [
|
| 1082 |
+
"win32"
|
| 1083 |
+
]
|
| 1084 |
+
},
|
| 1085 |
+
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
| 1086 |
+
"version": "4.62.4",
|
| 1087 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz",
|
| 1088 |
+
"integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==",
|
| 1089 |
+
"cpu": [
|
| 1090 |
+
"ia32"
|
| 1091 |
+
],
|
| 1092 |
+
"dev": true,
|
| 1093 |
+
"license": "MIT",
|
| 1094 |
+
"optional": true,
|
| 1095 |
+
"os": [
|
| 1096 |
+
"win32"
|
| 1097 |
+
]
|
| 1098 |
+
},
|
| 1099 |
+
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
| 1100 |
+
"version": "4.62.4",
|
| 1101 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz",
|
| 1102 |
+
"integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==",
|
| 1103 |
+
"cpu": [
|
| 1104 |
+
"x64"
|
| 1105 |
+
],
|
| 1106 |
+
"dev": true,
|
| 1107 |
+
"license": "MIT",
|
| 1108 |
+
"optional": true,
|
| 1109 |
+
"os": [
|
| 1110 |
+
"win32"
|
| 1111 |
+
]
|
| 1112 |
+
},
|
| 1113 |
+
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
| 1114 |
+
"version": "4.62.4",
|
| 1115 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz",
|
| 1116 |
+
"integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==",
|
| 1117 |
+
"cpu": [
|
| 1118 |
+
"x64"
|
| 1119 |
+
],
|
| 1120 |
+
"dev": true,
|
| 1121 |
+
"license": "MIT",
|
| 1122 |
+
"optional": true,
|
| 1123 |
+
"os": [
|
| 1124 |
+
"win32"
|
| 1125 |
+
]
|
| 1126 |
+
},
|
| 1127 |
+
"node_modules/@types/babel__core": {
|
| 1128 |
+
"version": "7.20.5",
|
| 1129 |
+
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
| 1130 |
+
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
| 1131 |
+
"dev": true,
|
| 1132 |
+
"license": "MIT",
|
| 1133 |
+
"dependencies": {
|
| 1134 |
+
"@babel/parser": "^7.20.7",
|
| 1135 |
+
"@babel/types": "^7.20.7",
|
| 1136 |
+
"@types/babel__generator": "*",
|
| 1137 |
+
"@types/babel__template": "*",
|
| 1138 |
+
"@types/babel__traverse": "*"
|
| 1139 |
+
}
|
| 1140 |
+
},
|
| 1141 |
+
"node_modules/@types/babel__generator": {
|
| 1142 |
+
"version": "7.27.0",
|
| 1143 |
+
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
|
| 1144 |
+
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
|
| 1145 |
+
"dev": true,
|
| 1146 |
+
"license": "MIT",
|
| 1147 |
+
"dependencies": {
|
| 1148 |
+
"@babel/types": "^7.0.0"
|
| 1149 |
+
}
|
| 1150 |
+
},
|
| 1151 |
+
"node_modules/@types/babel__template": {
|
| 1152 |
+
"version": "7.4.4",
|
| 1153 |
+
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
|
| 1154 |
+
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
|
| 1155 |
+
"dev": true,
|
| 1156 |
+
"license": "MIT",
|
| 1157 |
+
"dependencies": {
|
| 1158 |
+
"@babel/parser": "^7.1.0",
|
| 1159 |
+
"@babel/types": "^7.0.0"
|
| 1160 |
+
}
|
| 1161 |
+
},
|
| 1162 |
+
"node_modules/@types/babel__traverse": {
|
| 1163 |
+
"version": "7.28.0",
|
| 1164 |
+
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
|
| 1165 |
+
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
|
| 1166 |
+
"dev": true,
|
| 1167 |
+
"license": "MIT",
|
| 1168 |
+
"dependencies": {
|
| 1169 |
+
"@babel/types": "^7.28.2"
|
| 1170 |
+
}
|
| 1171 |
+
},
|
| 1172 |
+
"node_modules/@types/estree": {
|
| 1173 |
+
"version": "1.0.9",
|
| 1174 |
+
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
| 1175 |
+
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
| 1176 |
+
"dev": true,
|
| 1177 |
+
"license": "MIT"
|
| 1178 |
+
},
|
| 1179 |
+
"node_modules/@vitejs/plugin-react": {
|
| 1180 |
+
"version": "4.7.0",
|
| 1181 |
+
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
| 1182 |
+
"integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
|
| 1183 |
+
"dev": true,
|
| 1184 |
+
"license": "MIT",
|
| 1185 |
+
"dependencies": {
|
| 1186 |
+
"@babel/core": "^7.28.0",
|
| 1187 |
+
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
|
| 1188 |
+
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
|
| 1189 |
+
"@rolldown/pluginutils": "1.0.0-beta.27",
|
| 1190 |
+
"@types/babel__core": "^7.20.5",
|
| 1191 |
+
"react-refresh": "^0.17.0"
|
| 1192 |
+
},
|
| 1193 |
+
"engines": {
|
| 1194 |
+
"node": "^14.18.0 || >=16.0.0"
|
| 1195 |
+
},
|
| 1196 |
+
"peerDependencies": {
|
| 1197 |
+
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
| 1198 |
+
}
|
| 1199 |
+
},
|
| 1200 |
+
"node_modules/baseline-browser-mapping": {
|
| 1201 |
+
"version": "2.11.12",
|
| 1202 |
+
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz",
|
| 1203 |
+
"integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==",
|
| 1204 |
+
"dev": true,
|
| 1205 |
+
"license": "Apache-2.0",
|
| 1206 |
+
"bin": {
|
| 1207 |
+
"baseline-browser-mapping": "dist/cli.cjs"
|
| 1208 |
+
},
|
| 1209 |
+
"engines": {
|
| 1210 |
+
"node": ">=6.0.0"
|
| 1211 |
+
}
|
| 1212 |
+
},
|
| 1213 |
+
"node_modules/browserslist": {
|
| 1214 |
+
"version": "4.28.7",
|
| 1215 |
+
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz",
|
| 1216 |
+
"integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==",
|
| 1217 |
+
"dev": true,
|
| 1218 |
+
"funding": [
|
| 1219 |
+
{
|
| 1220 |
+
"type": "opencollective",
|
| 1221 |
+
"url": "https://opencollective.com/browserslist"
|
| 1222 |
+
},
|
| 1223 |
+
{
|
| 1224 |
+
"type": "tidelift",
|
| 1225 |
+
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
| 1226 |
+
},
|
| 1227 |
+
{
|
| 1228 |
+
"type": "github",
|
| 1229 |
+
"url": "https://github.com/sponsors/ai"
|
| 1230 |
+
}
|
| 1231 |
+
],
|
| 1232 |
+
"license": "MIT",
|
| 1233 |
+
"dependencies": {
|
| 1234 |
+
"baseline-browser-mapping": "^2.10.44",
|
| 1235 |
+
"caniuse-lite": "^1.0.30001806",
|
| 1236 |
+
"electron-to-chromium": "^1.5.393",
|
| 1237 |
+
"node-releases": "^2.0.51",
|
| 1238 |
+
"update-browserslist-db": "^1.2.3"
|
| 1239 |
+
},
|
| 1240 |
+
"bin": {
|
| 1241 |
+
"browserslist": "cli.js"
|
| 1242 |
+
},
|
| 1243 |
+
"engines": {
|
| 1244 |
+
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
| 1245 |
+
}
|
| 1246 |
+
},
|
| 1247 |
+
"node_modules/caniuse-lite": {
|
| 1248 |
+
"version": "1.0.30001809",
|
| 1249 |
+
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz",
|
| 1250 |
+
"integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==",
|
| 1251 |
+
"dev": true,
|
| 1252 |
+
"funding": [
|
| 1253 |
+
{
|
| 1254 |
+
"type": "opencollective",
|
| 1255 |
+
"url": "https://opencollective.com/browserslist"
|
| 1256 |
+
},
|
| 1257 |
+
{
|
| 1258 |
+
"type": "tidelift",
|
| 1259 |
+
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
| 1260 |
+
},
|
| 1261 |
+
{
|
| 1262 |
+
"type": "github",
|
| 1263 |
+
"url": "https://github.com/sponsors/ai"
|
| 1264 |
+
}
|
| 1265 |
+
],
|
| 1266 |
+
"license": "CC-BY-4.0"
|
| 1267 |
+
},
|
| 1268 |
+
"node_modules/convert-source-map": {
|
| 1269 |
+
"version": "2.0.0",
|
| 1270 |
+
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
| 1271 |
+
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
| 1272 |
+
"dev": true,
|
| 1273 |
+
"license": "MIT"
|
| 1274 |
+
},
|
| 1275 |
+
"node_modules/debug": {
|
| 1276 |
+
"version": "4.4.3",
|
| 1277 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
| 1278 |
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
| 1279 |
+
"dev": true,
|
| 1280 |
+
"license": "MIT",
|
| 1281 |
+
"dependencies": {
|
| 1282 |
+
"ms": "^2.1.3"
|
| 1283 |
+
},
|
| 1284 |
+
"engines": {
|
| 1285 |
+
"node": ">=6.0"
|
| 1286 |
+
},
|
| 1287 |
+
"peerDependenciesMeta": {
|
| 1288 |
+
"supports-color": {
|
| 1289 |
+
"optional": true
|
| 1290 |
+
}
|
| 1291 |
+
}
|
| 1292 |
+
},
|
| 1293 |
+
"node_modules/electron-to-chromium": {
|
| 1294 |
+
"version": "1.5.402",
|
| 1295 |
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.402.tgz",
|
| 1296 |
+
"integrity": "sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA==",
|
| 1297 |
+
"dev": true,
|
| 1298 |
+
"license": "ISC"
|
| 1299 |
+
},
|
| 1300 |
+
"node_modules/esbuild": {
|
| 1301 |
+
"version": "0.21.5",
|
| 1302 |
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
| 1303 |
+
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
| 1304 |
+
"dev": true,
|
| 1305 |
+
"hasInstallScript": true,
|
| 1306 |
+
"license": "MIT",
|
| 1307 |
+
"bin": {
|
| 1308 |
+
"esbuild": "bin/esbuild"
|
| 1309 |
+
},
|
| 1310 |
+
"engines": {
|
| 1311 |
+
"node": ">=12"
|
| 1312 |
+
},
|
| 1313 |
+
"optionalDependencies": {
|
| 1314 |
+
"@esbuild/aix-ppc64": "0.21.5",
|
| 1315 |
+
"@esbuild/android-arm": "0.21.5",
|
| 1316 |
+
"@esbuild/android-arm64": "0.21.5",
|
| 1317 |
+
"@esbuild/android-x64": "0.21.5",
|
| 1318 |
+
"@esbuild/darwin-arm64": "0.21.5",
|
| 1319 |
+
"@esbuild/darwin-x64": "0.21.5",
|
| 1320 |
+
"@esbuild/freebsd-arm64": "0.21.5",
|
| 1321 |
+
"@esbuild/freebsd-x64": "0.21.5",
|
| 1322 |
+
"@esbuild/linux-arm": "0.21.5",
|
| 1323 |
+
"@esbuild/linux-arm64": "0.21.5",
|
| 1324 |
+
"@esbuild/linux-ia32": "0.21.5",
|
| 1325 |
+
"@esbuild/linux-loong64": "0.21.5",
|
| 1326 |
+
"@esbuild/linux-mips64el": "0.21.5",
|
| 1327 |
+
"@esbuild/linux-ppc64": "0.21.5",
|
| 1328 |
+
"@esbuild/linux-riscv64": "0.21.5",
|
| 1329 |
+
"@esbuild/linux-s390x": "0.21.5",
|
| 1330 |
+
"@esbuild/linux-x64": "0.21.5",
|
| 1331 |
+
"@esbuild/netbsd-x64": "0.21.5",
|
| 1332 |
+
"@esbuild/openbsd-x64": "0.21.5",
|
| 1333 |
+
"@esbuild/sunos-x64": "0.21.5",
|
| 1334 |
+
"@esbuild/win32-arm64": "0.21.5",
|
| 1335 |
+
"@esbuild/win32-ia32": "0.21.5",
|
| 1336 |
+
"@esbuild/win32-x64": "0.21.5"
|
| 1337 |
+
}
|
| 1338 |
+
},
|
| 1339 |
+
"node_modules/escalade": {
|
| 1340 |
+
"version": "3.2.0",
|
| 1341 |
+
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
| 1342 |
+
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
| 1343 |
+
"dev": true,
|
| 1344 |
+
"license": "MIT",
|
| 1345 |
+
"engines": {
|
| 1346 |
+
"node": ">=6"
|
| 1347 |
+
}
|
| 1348 |
+
},
|
| 1349 |
+
"node_modules/fsevents": {
|
| 1350 |
+
"version": "2.3.3",
|
| 1351 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
| 1352 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 1353 |
+
"dev": true,
|
| 1354 |
+
"hasInstallScript": true,
|
| 1355 |
+
"license": "MIT",
|
| 1356 |
+
"optional": true,
|
| 1357 |
+
"os": [
|
| 1358 |
+
"darwin"
|
| 1359 |
+
],
|
| 1360 |
+
"engines": {
|
| 1361 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 1362 |
+
}
|
| 1363 |
+
},
|
| 1364 |
+
"node_modules/gensync": {
|
| 1365 |
+
"version": "1.0.0-beta.2",
|
| 1366 |
+
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
| 1367 |
+
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
| 1368 |
+
"dev": true,
|
| 1369 |
+
"license": "MIT",
|
| 1370 |
+
"engines": {
|
| 1371 |
+
"node": ">=6.9.0"
|
| 1372 |
+
}
|
| 1373 |
+
},
|
| 1374 |
+
"node_modules/hls.js": {
|
| 1375 |
+
"version": "1.6.17",
|
| 1376 |
+
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.17.tgz",
|
| 1377 |
+
"integrity": "sha512-NUplVGVuc1hSPwdB/9/cbRkUmLrYi75/hqiXKdA+l300pJNxDu96R7jRb2imDzWJqIUF4I5ThmAdp9GvOCXsuQ==",
|
| 1378 |
+
"license": "Apache-2.0"
|
| 1379 |
+
},
|
| 1380 |
+
"node_modules/js-tokens": {
|
| 1381 |
+
"version": "4.0.0",
|
| 1382 |
+
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
| 1383 |
+
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
| 1384 |
+
"license": "MIT"
|
| 1385 |
+
},
|
| 1386 |
+
"node_modules/jsesc": {
|
| 1387 |
+
"version": "3.1.0",
|
| 1388 |
+
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
| 1389 |
+
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
| 1390 |
+
"dev": true,
|
| 1391 |
+
"license": "MIT",
|
| 1392 |
+
"bin": {
|
| 1393 |
+
"jsesc": "bin/jsesc"
|
| 1394 |
+
},
|
| 1395 |
+
"engines": {
|
| 1396 |
+
"node": ">=6"
|
| 1397 |
+
}
|
| 1398 |
+
},
|
| 1399 |
+
"node_modules/json5": {
|
| 1400 |
+
"version": "2.2.3",
|
| 1401 |
+
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
| 1402 |
+
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
| 1403 |
+
"dev": true,
|
| 1404 |
+
"license": "MIT",
|
| 1405 |
+
"bin": {
|
| 1406 |
+
"json5": "lib/cli.js"
|
| 1407 |
+
},
|
| 1408 |
+
"engines": {
|
| 1409 |
+
"node": ">=6"
|
| 1410 |
+
}
|
| 1411 |
+
},
|
| 1412 |
+
"node_modules/loose-envify": {
|
| 1413 |
+
"version": "1.4.0",
|
| 1414 |
+
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
| 1415 |
+
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
| 1416 |
+
"license": "MIT",
|
| 1417 |
+
"dependencies": {
|
| 1418 |
+
"js-tokens": "^3.0.0 || ^4.0.0"
|
| 1419 |
+
},
|
| 1420 |
+
"bin": {
|
| 1421 |
+
"loose-envify": "cli.js"
|
| 1422 |
+
}
|
| 1423 |
+
},
|
| 1424 |
+
"node_modules/lru-cache": {
|
| 1425 |
+
"version": "5.1.1",
|
| 1426 |
+
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
| 1427 |
+
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
| 1428 |
+
"dev": true,
|
| 1429 |
+
"license": "ISC",
|
| 1430 |
+
"dependencies": {
|
| 1431 |
+
"yallist": "^3.0.2"
|
| 1432 |
+
}
|
| 1433 |
+
},
|
| 1434 |
+
"node_modules/ms": {
|
| 1435 |
+
"version": "2.1.3",
|
| 1436 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 1437 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 1438 |
+
"dev": true,
|
| 1439 |
+
"license": "MIT"
|
| 1440 |
+
},
|
| 1441 |
+
"node_modules/nanoid": {
|
| 1442 |
+
"version": "3.3.18",
|
| 1443 |
+
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
| 1444 |
+
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
| 1445 |
+
"dev": true,
|
| 1446 |
+
"funding": [
|
| 1447 |
+
{
|
| 1448 |
+
"type": "github",
|
| 1449 |
+
"url": "https://github.com/sponsors/ai"
|
| 1450 |
+
}
|
| 1451 |
+
],
|
| 1452 |
+
"license": "MIT",
|
| 1453 |
+
"bin": {
|
| 1454 |
+
"nanoid": "bin/nanoid.cjs"
|
| 1455 |
+
},
|
| 1456 |
+
"engines": {
|
| 1457 |
+
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
| 1458 |
+
}
|
| 1459 |
+
},
|
| 1460 |
+
"node_modules/node-releases": {
|
| 1461 |
+
"version": "2.0.53",
|
| 1462 |
+
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz",
|
| 1463 |
+
"integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==",
|
| 1464 |
+
"dev": true,
|
| 1465 |
+
"license": "MIT",
|
| 1466 |
+
"engines": {
|
| 1467 |
+
"node": ">=18"
|
| 1468 |
+
}
|
| 1469 |
+
},
|
| 1470 |
+
"node_modules/picocolors": {
|
| 1471 |
+
"version": "1.1.1",
|
| 1472 |
+
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
| 1473 |
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
| 1474 |
+
"dev": true,
|
| 1475 |
+
"license": "ISC"
|
| 1476 |
+
},
|
| 1477 |
+
"node_modules/postcss": {
|
| 1478 |
+
"version": "8.5.26",
|
| 1479 |
+
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
| 1480 |
+
"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
|
| 1481 |
+
"dev": true,
|
| 1482 |
+
"funding": [
|
| 1483 |
+
{
|
| 1484 |
+
"type": "opencollective",
|
| 1485 |
+
"url": "https://opencollective.com/postcss/"
|
| 1486 |
+
},
|
| 1487 |
+
{
|
| 1488 |
+
"type": "tidelift",
|
| 1489 |
+
"url": "https://tidelift.com/funding/github/npm/postcss"
|
| 1490 |
+
},
|
| 1491 |
+
{
|
| 1492 |
+
"type": "github",
|
| 1493 |
+
"url": "https://github.com/sponsors/ai"
|
| 1494 |
+
}
|
| 1495 |
+
],
|
| 1496 |
+
"license": "MIT",
|
| 1497 |
+
"dependencies": {
|
| 1498 |
+
"nanoid": "^3.3.17",
|
| 1499 |
+
"picocolors": "^1.1.1",
|
| 1500 |
+
"source-map-js": "^1.2.1"
|
| 1501 |
+
},
|
| 1502 |
+
"engines": {
|
| 1503 |
+
"node": "^10 || ^12 || >=14"
|
| 1504 |
+
}
|
| 1505 |
+
},
|
| 1506 |
+
"node_modules/react": {
|
| 1507 |
+
"version": "18.3.1",
|
| 1508 |
+
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
| 1509 |
+
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
| 1510 |
+
"license": "MIT",
|
| 1511 |
+
"dependencies": {
|
| 1512 |
+
"loose-envify": "^1.1.0"
|
| 1513 |
+
},
|
| 1514 |
+
"engines": {
|
| 1515 |
+
"node": ">=0.10.0"
|
| 1516 |
+
}
|
| 1517 |
+
},
|
| 1518 |
+
"node_modules/react-dom": {
|
| 1519 |
+
"version": "18.3.1",
|
| 1520 |
+
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
| 1521 |
+
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
| 1522 |
+
"license": "MIT",
|
| 1523 |
+
"dependencies": {
|
| 1524 |
+
"loose-envify": "^1.1.0",
|
| 1525 |
+
"scheduler": "^0.23.2"
|
| 1526 |
+
},
|
| 1527 |
+
"peerDependencies": {
|
| 1528 |
+
"react": "^18.3.1"
|
| 1529 |
+
}
|
| 1530 |
+
},
|
| 1531 |
+
"node_modules/react-refresh": {
|
| 1532 |
+
"version": "0.17.0",
|
| 1533 |
+
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
| 1534 |
+
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
|
| 1535 |
+
"dev": true,
|
| 1536 |
+
"license": "MIT",
|
| 1537 |
+
"engines": {
|
| 1538 |
+
"node": ">=0.10.0"
|
| 1539 |
+
}
|
| 1540 |
+
},
|
| 1541 |
+
"node_modules/react-router": {
|
| 1542 |
+
"version": "6.30.4",
|
| 1543 |
+
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz",
|
| 1544 |
+
"integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==",
|
| 1545 |
+
"license": "MIT",
|
| 1546 |
+
"dependencies": {
|
| 1547 |
+
"@remix-run/router": "1.23.3"
|
| 1548 |
+
},
|
| 1549 |
+
"engines": {
|
| 1550 |
+
"node": ">=14.0.0"
|
| 1551 |
+
},
|
| 1552 |
+
"peerDependencies": {
|
| 1553 |
+
"react": ">=16.8"
|
| 1554 |
+
}
|
| 1555 |
+
},
|
| 1556 |
+
"node_modules/react-router-dom": {
|
| 1557 |
+
"version": "6.30.4",
|
| 1558 |
+
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz",
|
| 1559 |
+
"integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==",
|
| 1560 |
+
"license": "MIT",
|
| 1561 |
+
"dependencies": {
|
| 1562 |
+
"@remix-run/router": "1.23.3",
|
| 1563 |
+
"react-router": "6.30.4"
|
| 1564 |
+
},
|
| 1565 |
+
"engines": {
|
| 1566 |
+
"node": ">=14.0.0"
|
| 1567 |
+
},
|
| 1568 |
+
"peerDependencies": {
|
| 1569 |
+
"react": ">=16.8",
|
| 1570 |
+
"react-dom": ">=16.8"
|
| 1571 |
+
}
|
| 1572 |
+
},
|
| 1573 |
+
"node_modules/rollup": {
|
| 1574 |
+
"version": "4.62.4",
|
| 1575 |
+
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz",
|
| 1576 |
+
"integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==",
|
| 1577 |
+
"dev": true,
|
| 1578 |
+
"license": "MIT",
|
| 1579 |
+
"dependencies": {
|
| 1580 |
+
"@types/estree": "1.0.9"
|
| 1581 |
+
},
|
| 1582 |
+
"bin": {
|
| 1583 |
+
"rollup": "dist/bin/rollup"
|
| 1584 |
+
},
|
| 1585 |
+
"engines": {
|
| 1586 |
+
"node": ">=18.0.0",
|
| 1587 |
+
"npm": ">=8.0.0"
|
| 1588 |
+
},
|
| 1589 |
+
"optionalDependencies": {
|
| 1590 |
+
"@napi-rs/lzma-linux-x64-gnu": "1.5.1",
|
| 1591 |
+
"@rollup/rollup-android-arm-eabi": "4.62.4",
|
| 1592 |
+
"@rollup/rollup-android-arm64": "4.62.4",
|
| 1593 |
+
"@rollup/rollup-darwin-arm64": "4.62.4",
|
| 1594 |
+
"@rollup/rollup-darwin-x64": "4.62.4",
|
| 1595 |
+
"@rollup/rollup-freebsd-arm64": "4.62.4",
|
| 1596 |
+
"@rollup/rollup-freebsd-x64": "4.62.4",
|
| 1597 |
+
"@rollup/rollup-linux-arm-gnueabihf": "4.62.4",
|
| 1598 |
+
"@rollup/rollup-linux-arm-musleabihf": "4.62.4",
|
| 1599 |
+
"@rollup/rollup-linux-arm64-gnu": "4.62.4",
|
| 1600 |
+
"@rollup/rollup-linux-arm64-musl": "4.62.4",
|
| 1601 |
+
"@rollup/rollup-linux-loong64-gnu": "4.62.4",
|
| 1602 |
+
"@rollup/rollup-linux-loong64-musl": "4.62.4",
|
| 1603 |
+
"@rollup/rollup-linux-ppc64-gnu": "4.62.4",
|
| 1604 |
+
"@rollup/rollup-linux-ppc64-musl": "4.62.4",
|
| 1605 |
+
"@rollup/rollup-linux-riscv64-gnu": "4.62.4",
|
| 1606 |
+
"@rollup/rollup-linux-riscv64-musl": "4.62.4",
|
| 1607 |
+
"@rollup/rollup-linux-s390x-gnu": "4.62.4",
|
| 1608 |
+
"@rollup/rollup-linux-x64-gnu": "4.62.4",
|
| 1609 |
+
"@rollup/rollup-linux-x64-musl": "4.62.4",
|
| 1610 |
+
"@rollup/rollup-openbsd-x64": "4.62.4",
|
| 1611 |
+
"@rollup/rollup-openharmony-arm64": "4.62.4",
|
| 1612 |
+
"@rollup/rollup-win32-arm64-msvc": "4.62.4",
|
| 1613 |
+
"@rollup/rollup-win32-ia32-msvc": "4.62.4",
|
| 1614 |
+
"@rollup/rollup-win32-x64-gnu": "4.62.4",
|
| 1615 |
+
"@rollup/rollup-win32-x64-msvc": "4.62.4",
|
| 1616 |
+
"fsevents": "~2.3.2"
|
| 1617 |
+
}
|
| 1618 |
+
},
|
| 1619 |
+
"node_modules/scheduler": {
|
| 1620 |
+
"version": "0.23.2",
|
| 1621 |
+
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
| 1622 |
+
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
| 1623 |
+
"license": "MIT",
|
| 1624 |
+
"dependencies": {
|
| 1625 |
+
"loose-envify": "^1.1.0"
|
| 1626 |
+
}
|
| 1627 |
+
},
|
| 1628 |
+
"node_modules/semver": {
|
| 1629 |
+
"version": "6.3.1",
|
| 1630 |
+
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
| 1631 |
+
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
| 1632 |
+
"dev": true,
|
| 1633 |
+
"license": "ISC",
|
| 1634 |
+
"bin": {
|
| 1635 |
+
"semver": "bin/semver.js"
|
| 1636 |
+
}
|
| 1637 |
+
},
|
| 1638 |
+
"node_modules/source-map-js": {
|
| 1639 |
+
"version": "1.2.1",
|
| 1640 |
+
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
| 1641 |
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
| 1642 |
+
"dev": true,
|
| 1643 |
+
"license": "BSD-3-Clause",
|
| 1644 |
+
"engines": {
|
| 1645 |
+
"node": ">=0.10.0"
|
| 1646 |
+
}
|
| 1647 |
+
},
|
| 1648 |
+
"node_modules/update-browserslist-db": {
|
| 1649 |
+
"version": "1.3.0",
|
| 1650 |
+
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.0.tgz",
|
| 1651 |
+
"integrity": "sha512-x/M6q3w4Ybp91CNaS4S69UnliqR3BzRpOT6LWbksjth0S/+jhfaPJsWjt/TewpT8j9eLIojUf5jr29WextHroA==",
|
| 1652 |
+
"dev": true,
|
| 1653 |
+
"funding": [
|
| 1654 |
+
{
|
| 1655 |
+
"type": "opencollective",
|
| 1656 |
+
"url": "https://opencollective.com/browserslist"
|
| 1657 |
+
},
|
| 1658 |
+
{
|
| 1659 |
+
"type": "tidelift",
|
| 1660 |
+
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
| 1661 |
+
},
|
| 1662 |
+
{
|
| 1663 |
+
"type": "github",
|
| 1664 |
+
"url": "https://github.com/sponsors/ai"
|
| 1665 |
+
}
|
| 1666 |
+
],
|
| 1667 |
+
"license": "MIT",
|
| 1668 |
+
"dependencies": {
|
| 1669 |
+
"escalade": "^3.2.0",
|
| 1670 |
+
"picocolors": "^1.1.1"
|
| 1671 |
+
},
|
| 1672 |
+
"bin": {
|
| 1673 |
+
"update-browserslist-db": "cli.js"
|
| 1674 |
+
},
|
| 1675 |
+
"peerDependencies": {
|
| 1676 |
+
"browserslist": ">= 4.21.0"
|
| 1677 |
+
}
|
| 1678 |
+
},
|
| 1679 |
+
"node_modules/vite": {
|
| 1680 |
+
"version": "5.4.21",
|
| 1681 |
+
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
| 1682 |
+
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
| 1683 |
+
"dev": true,
|
| 1684 |
+
"license": "MIT",
|
| 1685 |
+
"dependencies": {
|
| 1686 |
+
"esbuild": "^0.21.3",
|
| 1687 |
+
"postcss": "^8.4.43",
|
| 1688 |
+
"rollup": "^4.20.0"
|
| 1689 |
+
},
|
| 1690 |
+
"bin": {
|
| 1691 |
+
"vite": "bin/vite.js"
|
| 1692 |
+
},
|
| 1693 |
+
"engines": {
|
| 1694 |
+
"node": "^18.0.0 || >=20.0.0"
|
| 1695 |
+
},
|
| 1696 |
+
"funding": {
|
| 1697 |
+
"url": "https://github.com/vitejs/vite?sponsor=1"
|
| 1698 |
+
},
|
| 1699 |
+
"optionalDependencies": {
|
| 1700 |
+
"fsevents": "~2.3.3"
|
| 1701 |
+
},
|
| 1702 |
+
"peerDependencies": {
|
| 1703 |
+
"@types/node": "^18.0.0 || >=20.0.0",
|
| 1704 |
+
"less": "*",
|
| 1705 |
+
"lightningcss": "^1.21.0",
|
| 1706 |
+
"sass": "*",
|
| 1707 |
+
"sass-embedded": "*",
|
| 1708 |
+
"stylus": "*",
|
| 1709 |
+
"sugarss": "*",
|
| 1710 |
+
"terser": "^5.4.0"
|
| 1711 |
+
},
|
| 1712 |
+
"peerDependenciesMeta": {
|
| 1713 |
+
"@types/node": {
|
| 1714 |
+
"optional": true
|
| 1715 |
+
},
|
| 1716 |
+
"less": {
|
| 1717 |
+
"optional": true
|
| 1718 |
+
},
|
| 1719 |
+
"lightningcss": {
|
| 1720 |
+
"optional": true
|
| 1721 |
+
},
|
| 1722 |
+
"sass": {
|
| 1723 |
+
"optional": true
|
| 1724 |
+
},
|
| 1725 |
+
"sass-embedded": {
|
| 1726 |
+
"optional": true
|
| 1727 |
+
},
|
| 1728 |
+
"stylus": {
|
| 1729 |
+
"optional": true
|
| 1730 |
+
},
|
| 1731 |
+
"sugarss": {
|
| 1732 |
+
"optional": true
|
| 1733 |
+
},
|
| 1734 |
+
"terser": {
|
| 1735 |
+
"optional": true
|
| 1736 |
+
}
|
| 1737 |
+
}
|
| 1738 |
+
},
|
| 1739 |
+
"node_modules/yallist": {
|
| 1740 |
+
"version": "3.1.1",
|
| 1741 |
+
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
| 1742 |
+
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
| 1743 |
+
"dev": true,
|
| 1744 |
+
"license": "ISC"
|
| 1745 |
+
}
|
| 1746 |
+
}
|
| 1747 |
+
}
|
frontend/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "anicove-frontend",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.1.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"preview": "vite preview"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"hls.js": "^1.5.13",
|
| 13 |
+
"react": "^18.3.1",
|
| 14 |
+
"react-dom": "^18.3.1",
|
| 15 |
+
"react-router-dom": "^6.26.2"
|
| 16 |
+
},
|
| 17 |
+
"devDependencies": {
|
| 18 |
+
"@vitejs/plugin-react": "^4.3.1",
|
| 19 |
+
"vite": "^5.4.2"
|
| 20 |
+
}
|
| 21 |
+
}
|
frontend/public/music-api.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// music-api.js — fetchAPI bridge for music.js
|
| 2 |
+
// Connects music.js directly to AnimeThemes API v2.
|
| 3 |
+
|
| 4 |
+
(function () {
|
| 5 |
+
const AT = 'https://api.animethemes.moe';
|
| 6 |
+
|
| 7 |
+
async function _get(url) {
|
| 8 |
+
const r = await fetch(url, { headers: { Accept: 'application/json' } });
|
| 9 |
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
| 10 |
+
return r.json();
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
function normalizeImages(images) {
|
| 14 |
+
if (!images || !Array.isArray(images)) return [];
|
| 15 |
+
return images.map(img => {
|
| 16 |
+
let link = img.link || img.url;
|
| 17 |
+
if (!link && img.path) link = `https://static.animethemes.moe/${img.path}`;
|
| 18 |
+
if (!link && img.basename) link = `https://static.animethemes.moe/${img.basename}`;
|
| 19 |
+
return {
|
| 20 |
+
facet: img.facet || 'Large Cover',
|
| 21 |
+
link: link || ''
|
| 22 |
+
};
|
| 23 |
+
});
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
function normalizeTheme(t, animeRef) {
|
| 27 |
+
if (!t) return null;
|
| 28 |
+
const entries = t.animethemeentries || t.entries || [];
|
| 29 |
+
const normEntries = entries.map(e => {
|
| 30 |
+
const videos = e.videos || [];
|
| 31 |
+
const normVideos = videos.map(v => {
|
| 32 |
+
let audioLink = v.audio?.link || v.link || v.url;
|
| 33 |
+
if (!audioLink && v.basename) audioLink = `https://v.animethemes.moe/${v.basename}`;
|
| 34 |
+
if (!audioLink && v.filename) audioLink = `https://v.animethemes.moe/${v.filename}`;
|
| 35 |
+
return {
|
| 36 |
+
...v,
|
| 37 |
+
audio: { id: v.audio?.id || v.id || t.id || '1', link: audioLink }
|
| 38 |
+
};
|
| 39 |
+
});
|
| 40 |
+
return { ...e, videos: normVideos };
|
| 41 |
+
});
|
| 42 |
+
return {
|
| 43 |
+
...t,
|
| 44 |
+
id: t.id || Math.random().toString(36).substring(7),
|
| 45 |
+
slug: t.slug || `${t.type || 'OP'}${t.sequence || ''}`,
|
| 46 |
+
type: t.type || 'OP',
|
| 47 |
+
animethemeentries: normEntries,
|
| 48 |
+
anime: animeRef || t.anime
|
| 49 |
+
};
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
function normalizeAnime(a) {
|
| 53 |
+
if (!a) return null;
|
| 54 |
+
const rawThemes = a.animethemes || a.themes || [];
|
| 55 |
+
const animeObj = {
|
| 56 |
+
id: a.slug || String(a.id),
|
| 57 |
+
rawId: a.id,
|
| 58 |
+
name: a.name || a.title || a.name_jp || 'Anime',
|
| 59 |
+
slug: a.slug || String(a.id),
|
| 60 |
+
year: a.year || (a.season ? a.season : ''),
|
| 61 |
+
media_format: a.media_format || a.format || 'TV',
|
| 62 |
+
images: normalizeImages(a.images)
|
| 63 |
+
};
|
| 64 |
+
animeObj.animethemes = rawThemes.map(t => normalizeTheme(t, animeObj));
|
| 65 |
+
return animeObj;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
function normalizeArtist(art) {
|
| 69 |
+
if (!art) return null;
|
| 70 |
+
return {
|
| 71 |
+
id: art.slug || String(art.id),
|
| 72 |
+
name: art.name || 'Artist',
|
| 73 |
+
slug: art.slug || String(art.id),
|
| 74 |
+
images: normalizeImages(art.images),
|
| 75 |
+
songs: art.songs || []
|
| 76 |
+
};
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
async function fetchAPI(path) {
|
| 80 |
+
try {
|
| 81 |
+
// /themes/search?q=<q>
|
| 82 |
+
if (path.startsWith('/themes/search')) {
|
| 83 |
+
const q = new URL(path, 'https://x').searchParams.get('q') || '';
|
| 84 |
+
const data = await _get(`${AT}/search?q=${encodeURIComponent(q)}&include[anime]=animethemes.animethemeentries.videos.audio,animethemes.song.artists,images`);
|
| 85 |
+
const list = (data.search?.anime || data.anime || []).map(normalizeAnime);
|
| 86 |
+
return { anime: list };
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
// /themes/anime/<id_or_slug>
|
| 90 |
+
if (path.startsWith('/themes/anime/')) {
|
| 91 |
+
const param = decodeURIComponent(path.replace('/themes/anime/', '').split('?')[0]);
|
| 92 |
+
let rawAnime = null;
|
| 93 |
+
|
| 94 |
+
// 1. Direct slug endpoint fetch with full relations
|
| 95 |
+
const data = await _get(`${AT}/anime/${encodeURIComponent(param)}?include=animethemes.animethemeentries.videos.audio,animethemes.song.artists,images`).catch(() => null);
|
| 96 |
+
if (data?.anime) rawAnime = data.anime;
|
| 97 |
+
else if (data?.data) rawAnime = Array.isArray(data.data) ? data.data[0] : data.data;
|
| 98 |
+
|
| 99 |
+
// 2. Filter search if direct fetch failed
|
| 100 |
+
if (!rawAnime) {
|
| 101 |
+
const filterData = await _get(`${AT}/anime?filter[slug]=${encodeURIComponent(param)}&include=animethemes.animethemeentries.videos.audio,animethemes.song.artists,images`).catch(() => null)
|
| 102 |
+
|| await _get(`${AT}/anime?filter[anilist_id]=${encodeURIComponent(param)}&include=animethemes.animethemeentries.videos.audio,animethemes.song.artists,images`).catch(() => null);
|
| 103 |
+
const list = filterData?.anime || filterData?.data || [];
|
| 104 |
+
if (list.length) rawAnime = list[0];
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
return { anime: rawAnime ? normalizeAnime(rawAnime) : null };
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// /themes/artist/<slug>
|
| 111 |
+
if (path.startsWith('/themes/artist/')) {
|
| 112 |
+
const slug = decodeURIComponent(path.replace('/themes/artist/', '').split('?')[0]);
|
| 113 |
+
const data = await _get(`${AT}/artist/${encodeURIComponent(slug)}?include=animethemes.anime.images,animethemes.song`).catch(() => null);
|
| 114 |
+
const raw = data?.artist || (Array.isArray(data?.data) ? data.data[0] : data?.data);
|
| 115 |
+
return { artist: raw ? normalizeArtist(raw) : null };
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// /themes/artists or /themes/top-artists
|
| 119 |
+
if (path.startsWith('/themes/artists') || path.startsWith('/themes/top-artists')) {
|
| 120 |
+
const data = await _get(`${AT}/artist?page[size]=24&include=images`);
|
| 121 |
+
const list = data?.artist || data?.data || [];
|
| 122 |
+
return { artists: list.map(normalizeArtist) };
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// /themes/recent
|
| 126 |
+
if (path.startsWith('/themes/recent')) {
|
| 127 |
+
const data = await _get(`${AT}/anime?sort=-created_at&page[size]=24&include=animethemes.animethemeentries.videos.audio,animethemes.song.artists,images`);
|
| 128 |
+
const list = data?.anime || data?.data || [];
|
| 129 |
+
return { anime: list.map(normalizeAnime) };
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
// /themes/year/<year>
|
| 133 |
+
if (path.startsWith('/themes/year/')) {
|
| 134 |
+
const year = decodeURIComponent(path.replace('/themes/year/', '').split('?')[0]);
|
| 135 |
+
const data = await _get(`${AT}/anime?filter[year]=${year}&sort=name&page[size]=24&include=animethemes.animethemeentries.videos.audio,animethemes.song.artists,images`);
|
| 136 |
+
const list = data?.anime || data?.data || [];
|
| 137 |
+
return { anime: list.map(normalizeAnime) };
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
// Default home discover feed
|
| 141 |
+
const defaultData = await _get(`${AT}/anime?page[size]=24&include=animethemes.animethemeentries.videos.audio,animethemes.song.artists,images`);
|
| 142 |
+
const defaultList = defaultData?.anime || defaultData?.data || [];
|
| 143 |
+
return { anime: defaultList.map(normalizeAnime) };
|
| 144 |
+
} catch (e) {
|
| 145 |
+
console.error('[music-api] fetch error:', e);
|
| 146 |
+
return { anime: null, artists: [] };
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
window.fetchAPI = fetchAPI;
|
| 151 |
+
})();
|
frontend/public/music.css
ADDED
|
@@ -0,0 +1,2197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
:root {
|
| 3 |
+
--ms-bg: #0a0a0f;
|
| 4 |
+
--ms-sidebar: #000000;
|
| 5 |
+
--ms-surface: #121218;
|
| 6 |
+
--ms-surface-2: #1a1a22;
|
| 7 |
+
--ms-surface-3: #232330;
|
| 8 |
+
--ms-text: #ffffff;
|
| 9 |
+
--ms-text-dim: #a0a0b8;
|
| 10 |
+
--ms-text-mute: #6a6a82;
|
| 11 |
+
--ms-divider: rgba(255, 255, 255, 0.07);
|
| 12 |
+
--ms-accent: #7c3aed;
|
| 13 |
+
--ms-accent-hot: #9d6ef8;
|
| 14 |
+
--ms-accent-soft: rgba(124, 58, 237, 0.18);
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
.music-body {
|
| 18 |
+
overflow: hidden;
|
| 19 |
+
background:
|
| 20 |
+
radial-gradient(circle at top, rgba(124,58,237,0.22), transparent 22%),
|
| 21 |
+
linear-gradient(180deg, #07070d 0%, #090910 100%);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.music-body .float-nav { display: none !important; }
|
| 25 |
+
|
| 26 |
+
.music-body #mobile-nav { display: none !important; }
|
| 27 |
+
|
| 28 |
+
.music-body #header {
|
| 29 |
+
background: rgba(0, 0, 0, 0.92);
|
| 30 |
+
border-bottom: 1px solid var(--ms-divider);
|
| 31 |
+
backdrop-filter: blur(20px);
|
| 32 |
+
-webkit-backdrop-filter: blur(20px);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.music-app {
|
| 36 |
+
position: fixed;
|
| 37 |
+
top: var(--header-h);
|
| 38 |
+
left: 0;
|
| 39 |
+
right: 0;
|
| 40 |
+
bottom: 92px;
|
| 41 |
+
display: grid;
|
| 42 |
+
grid-template-columns: 280px 1fr;
|
| 43 |
+
gap: 8px;
|
| 44 |
+
padding: 8px;
|
| 45 |
+
background: #000;
|
| 46 |
+
transition: top 0.32s cubic-bezier(0.22, 1, 0.36, 1),
|
| 47 |
+
bottom 0.28s cubic-bezier(0.22, 1, 0.36, 1),
|
| 48 |
+
grid-template-columns 0.32s cubic-bezier(0.22, 1, 0.36, 1);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.music-body #header {
|
| 52 |
+
transition: transform 0.32s cubic-bezier(0.22, 1, 0.36, 1);
|
| 53 |
+
will-change: transform;
|
| 54 |
+
}
|
| 55 |
+
.music-body #header.is-hidden {
|
| 56 |
+
transform: translateY(-100%);
|
| 57 |
+
}
|
| 58 |
+
.music-body.header-hidden .music-app {
|
| 59 |
+
top: 0;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
@media (max-width: 1024px) {
|
| 63 |
+
.music-body .music-sidebar {
|
| 64 |
+
transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1),
|
| 65 |
+
top 0.32s cubic-bezier(0.22, 1, 0.36, 1);
|
| 66 |
+
}
|
| 67 |
+
.music-body.header-hidden .music-sidebar { top: 0; }
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
@media (min-width: 1025px) {
|
| 71 |
+
.music-app.sb-collapsed {
|
| 72 |
+
grid-template-columns: 0 1fr;
|
| 73 |
+
gap: 0;
|
| 74 |
+
}
|
| 75 |
+
.music-app.sb-collapsed .music-sidebar {
|
| 76 |
+
transform: translateX(-110%);
|
| 77 |
+
opacity: 0;
|
| 78 |
+
pointer-events: none;
|
| 79 |
+
}
|
| 80 |
+
.music-sidebar {
|
| 81 |
+
transition: transform 0.32s cubic-bezier(0.22, 1, 0.36, 1),
|
| 82 |
+
opacity 0.22s ease;
|
| 83 |
+
will-change: transform;
|
| 84 |
+
}
|
| 85 |
+
.ms-sidebar-toggle { display: flex; }
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.music-body.pb-hidden .music-app { bottom: 0; }
|
| 89 |
+
.music-body.pb-hidden .player-bar { display: none; }
|
| 90 |
+
|
| 91 |
+
.music-sidebar {
|
| 92 |
+
background: var(--ms-sidebar);
|
| 93 |
+
border-radius: 10px;
|
| 94 |
+
padding: 16px 12px;
|
| 95 |
+
overflow-y: auto;
|
| 96 |
+
overflow-x: hidden;
|
| 97 |
+
scrollbar-width: thin;
|
| 98 |
+
scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.music-sidebar::-webkit-scrollbar { width: 8px; }
|
| 102 |
+
.music-sidebar::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.16); border-radius: 4px; }
|
| 103 |
+
|
| 104 |
+
.ms-block { margin-bottom: 22px; }
|
| 105 |
+
|
| 106 |
+
.ms-block-title {
|
| 107 |
+
display: flex;
|
| 108 |
+
align-items: center;
|
| 109 |
+
gap: 10px;
|
| 110 |
+
color: var(--ms-text-dim);
|
| 111 |
+
font-size: 0.75rem;
|
| 112 |
+
font-weight: 700;
|
| 113 |
+
text-transform: uppercase;
|
| 114 |
+
letter-spacing: 0.08em;
|
| 115 |
+
padding: 6px 10px 10px;
|
| 116 |
+
}
|
| 117 |
+
.ms-block-title svg { width: 16px; height: 16px; }
|
| 118 |
+
|
| 119 |
+
.ms-link {
|
| 120 |
+
display: flex;
|
| 121 |
+
align-items: center;
|
| 122 |
+
gap: 14px;
|
| 123 |
+
padding: 10px 12px;
|
| 124 |
+
border-radius: 6px;
|
| 125 |
+
color: var(--ms-text-dim);
|
| 126 |
+
font-size: 0.92rem;
|
| 127 |
+
font-weight: 600;
|
| 128 |
+
transition: color 0.15s ease, background 0.15s ease;
|
| 129 |
+
cursor: pointer;
|
| 130 |
+
}
|
| 131 |
+
.ms-link svg { width: 20px; height: 20px; flex-shrink: 0; }
|
| 132 |
+
.ms-link:hover { color: #fff; background: transparent; }
|
| 133 |
+
.ms-link.active { color: #fff; background: var(--ms-surface-2); }
|
| 134 |
+
.ms-link.active svg { color: var(--ms-accent-hot); }
|
| 135 |
+
|
| 136 |
+
.ms-year-grid {
|
| 137 |
+
display: grid;
|
| 138 |
+
grid-template-columns: repeat(2, 1fr);
|
| 139 |
+
gap: 6px;
|
| 140 |
+
padding: 4px 6px;
|
| 141 |
+
}
|
| 142 |
+
.ms-year-grid a {
|
| 143 |
+
background: var(--ms-surface-2);
|
| 144 |
+
border-radius: 6px;
|
| 145 |
+
padding: 9px 10px;
|
| 146 |
+
text-align: center;
|
| 147 |
+
font-size: 0.85rem;
|
| 148 |
+
font-weight: 600;
|
| 149 |
+
color: var(--ms-text-dim);
|
| 150 |
+
transition: color 0.15s, background 0.15s;
|
| 151 |
+
}
|
| 152 |
+
.ms-year-grid a:hover {
|
| 153 |
+
background: var(--ms-surface-3);
|
| 154 |
+
color: #fff;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
.music-main {
|
| 158 |
+
background: linear-gradient(180deg, #1a1228 0, #0e0e14 320px, var(--ms-surface) 100%);
|
| 159 |
+
border-radius: 10px;
|
| 160 |
+
overflow-y: auto;
|
| 161 |
+
overflow-x: hidden;
|
| 162 |
+
position: relative;
|
| 163 |
+
scrollbar-width: thin;
|
| 164 |
+
scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
|
| 165 |
+
}
|
| 166 |
+
.music-main::-webkit-scrollbar { width: 12px; }
|
| 167 |
+
.music-main::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.16); border-radius: 6px; }
|
| 168 |
+
.music-main::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.28); }
|
| 169 |
+
|
| 170 |
+
.ms-topbar {
|
| 171 |
+
position: sticky;
|
| 172 |
+
top: 0;
|
| 173 |
+
z-index: 5;
|
| 174 |
+
display: flex;
|
| 175 |
+
align-items: center;
|
| 176 |
+
gap: 14px;
|
| 177 |
+
padding: 14px 24px;
|
| 178 |
+
background: rgba(10, 10, 16, 0.65);
|
| 179 |
+
backdrop-filter: blur(18px);
|
| 180 |
+
-webkit-backdrop-filter: blur(18px);
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.ms-nav-btn {
|
| 184 |
+
width: 32px; height: 32px;
|
| 185 |
+
border-radius: 50%;
|
| 186 |
+
background: rgba(0, 0, 0, 0.7);
|
| 187 |
+
color: #fff;
|
| 188 |
+
display: flex; align-items: center; justify-content: center;
|
| 189 |
+
transition: background 0.15s, transform 0.15s;
|
| 190 |
+
flex-shrink: 0;
|
| 191 |
+
}
|
| 192 |
+
.ms-nav-btn svg { width: 14px; height: 14px; }
|
| 193 |
+
.ms-nav-btn:hover:not([disabled]) { background: #000; transform: scale(1.06); }
|
| 194 |
+
.ms-nav-btn[disabled] { opacity: 0.45; cursor: not-allowed; }
|
| 195 |
+
|
| 196 |
+
.ms-search {
|
| 197 |
+
position: relative;
|
| 198 |
+
flex: 1;
|
| 199 |
+
max-width: 360px;
|
| 200 |
+
display: flex;
|
| 201 |
+
align-items: center;
|
| 202 |
+
}
|
| 203 |
+
.ms-search-icon {
|
| 204 |
+
position: absolute;
|
| 205 |
+
left: 14px;
|
| 206 |
+
top: 50%;
|
| 207 |
+
transform: translateY(-50%);
|
| 208 |
+
color: #1a1a22;
|
| 209 |
+
display: flex;
|
| 210 |
+
pointer-events: none;
|
| 211 |
+
}
|
| 212 |
+
.ms-search-icon svg { width: 18px; height: 18px; }
|
| 213 |
+
|
| 214 |
+
.ms-search input {
|
| 215 |
+
width: 100%;
|
| 216 |
+
padding: 11px 40px 11px 42px;
|
| 217 |
+
background: #fff;
|
| 218 |
+
border: 2px solid transparent;
|
| 219 |
+
border-radius: 999px;
|
| 220 |
+
color: #1a1a22;
|
| 221 |
+
font-family: 'Outfit', sans-serif;
|
| 222 |
+
font-size: 0.92rem;
|
| 223 |
+
font-weight: 500;
|
| 224 |
+
outline: none;
|
| 225 |
+
transition: box-shadow 0.15s;
|
| 226 |
+
}
|
| 227 |
+
.ms-search input::placeholder { color: #6a6a82; font-weight: 500; }
|
| 228 |
+
.ms-search input:focus {
|
| 229 |
+
box-shadow: 0 0 0 2px var(--ms-accent);
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.ms-search-clear {
|
| 233 |
+
position: absolute;
|
| 234 |
+
right: 8px;
|
| 235 |
+
top: 50%;
|
| 236 |
+
transform: translateY(-50%);
|
| 237 |
+
width: 26px; height: 26px;
|
| 238 |
+
border-radius: 50%;
|
| 239 |
+
color: #1a1a22;
|
| 240 |
+
display: flex; align-items: center; justify-content: center;
|
| 241 |
+
transition: background 0.15s;
|
| 242 |
+
}
|
| 243 |
+
.ms-search-clear svg { width: 14px; height: 14px; }
|
| 244 |
+
.ms-search-clear:hover { background: rgba(0, 0, 0, 0.08); }
|
| 245 |
+
|
| 246 |
+
.ms-sidebar-toggle {
|
| 247 |
+
display: flex;
|
| 248 |
+
width: 38px; height: 38px;
|
| 249 |
+
border-radius: 10px;
|
| 250 |
+
color: #fff;
|
| 251 |
+
background: rgba(255, 255, 255, 0.06);
|
| 252 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 253 |
+
align-items: center; justify-content: center;
|
| 254 |
+
margin: 0;
|
| 255 |
+
|
| 256 |
+
order: -1;
|
| 257 |
+
flex-shrink: 0;
|
| 258 |
+
transition: background 0.16s ease, transform 0.16s ease, border-color 0.16s ease;
|
| 259 |
+
cursor: pointer;
|
| 260 |
+
}
|
| 261 |
+
.ms-sidebar-toggle:hover {
|
| 262 |
+
background: rgba(167, 139, 250, 0.18);
|
| 263 |
+
border-color: rgba(167, 139, 250, 0.35);
|
| 264 |
+
transform: scale(1.04);
|
| 265 |
+
}
|
| 266 |
+
.ms-sidebar-toggle:active { transform: scale(0.94); }
|
| 267 |
+
.ms-sidebar-toggle svg { width: 20px; height: 20px; }
|
| 268 |
+
|
| 269 |
+
.music-app.sb-collapsed .ms-sidebar-toggle svg { transform: rotate(180deg); transition: transform 0.22s ease; }
|
| 270 |
+
|
| 271 |
+
.ms-view {
|
| 272 |
+
padding: 0 24px 40px;
|
| 273 |
+
min-height: calc(100% - 70px);
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
.ms-section-title {
|
| 277 |
+
display: flex;
|
| 278 |
+
align-items: baseline;
|
| 279 |
+
justify-content: space-between;
|
| 280 |
+
gap: 14px;
|
| 281 |
+
margin: 30px 0 16px;
|
| 282 |
+
}
|
| 283 |
+
.ms-section-title h2 {
|
| 284 |
+
font-size: 1.5rem;
|
| 285 |
+
font-weight: 800;
|
| 286 |
+
letter-spacing: -0.01em;
|
| 287 |
+
color: #fff;
|
| 288 |
+
}
|
| 289 |
+
.ms-section-title a {
|
| 290 |
+
color: var(--ms-text-dim);
|
| 291 |
+
font-size: 0.78rem;
|
| 292 |
+
font-weight: 700;
|
| 293 |
+
text-transform: uppercase;
|
| 294 |
+
letter-spacing: 0.08em;
|
| 295 |
+
transition: color 0.15s;
|
| 296 |
+
}
|
| 297 |
+
.ms-section-title a:hover { color: #fff; text-decoration: underline; }
|
| 298 |
+
|
| 299 |
+
.ms-card-grid {
|
| 300 |
+
display: grid;
|
| 301 |
+
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
| 302 |
+
gap: 24px;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
.ms-card {
|
| 306 |
+
background: var(--ms-surface);
|
| 307 |
+
border-radius: 8px;
|
| 308 |
+
padding: 16px;
|
| 309 |
+
transition: background 0.2s ease;
|
| 310 |
+
cursor: pointer;
|
| 311 |
+
position: relative;
|
| 312 |
+
text-align: left;
|
| 313 |
+
isolation: isolate;
|
| 314 |
+
}
|
| 315 |
+
.ms-card:hover { background: var(--ms-surface-2); }
|
| 316 |
+
.ms-card:hover .ms-card-play {
|
| 317 |
+
opacity: 1;
|
| 318 |
+
transform: translateY(0);
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.ms-card-cover-wrap {
|
| 322 |
+
position: relative;
|
| 323 |
+
width: 100%;
|
| 324 |
+
aspect-ratio: 1;
|
| 325 |
+
border-radius: 6px;
|
| 326 |
+
overflow: hidden;
|
| 327 |
+
background: var(--ms-surface-2);
|
| 328 |
+
margin-bottom: 14px;
|
| 329 |
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
| 330 |
+
|
| 331 |
+
contain: layout paint;
|
| 332 |
+
flex-shrink: 0;
|
| 333 |
+
}
|
| 334 |
+
.ms-card.is-artist .ms-card-cover-wrap {
|
| 335 |
+
border-radius: 50%;
|
| 336 |
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.55);
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
.ms-card-cover {
|
| 340 |
+
width: 100%;
|
| 341 |
+
height: 100%;
|
| 342 |
+
object-fit: cover;
|
| 343 |
+
transition: transform 0.4s ease;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
.ms-card-cover-fallback {
|
| 347 |
+
width: 100%;
|
| 348 |
+
height: 100%;
|
| 349 |
+
display: flex;
|
| 350 |
+
align-items: center;
|
| 351 |
+
justify-content: center;
|
| 352 |
+
background: linear-gradient(135deg, var(--ms-accent), #2a1656);
|
| 353 |
+
color: #fff;
|
| 354 |
+
}
|
| 355 |
+
.ms-card-cover-fallback svg { width: 48px; height: 48px; opacity: 0.7; }
|
| 356 |
+
|
| 357 |
+
.ms-card-play {
|
| 358 |
+
position: absolute;
|
| 359 |
+
right: 8px;
|
| 360 |
+
bottom: 8px;
|
| 361 |
+
width: 48px; height: 48px;
|
| 362 |
+
border-radius: 50%;
|
| 363 |
+
background: var(--ms-accent);
|
| 364 |
+
color: #fff;
|
| 365 |
+
display: flex;
|
| 366 |
+
align-items: center;
|
| 367 |
+
justify-content: center;
|
| 368 |
+
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.6);
|
| 369 |
+
opacity: 0;
|
| 370 |
+
transform: translateY(8px);
|
| 371 |
+
transition: opacity 0.18s ease, transform 0.18s ease, background 0.18s ease, scale 0.12s;
|
| 372 |
+
z-index: 2;
|
| 373 |
+
}
|
| 374 |
+
.ms-card-play svg { width: 22px; height: 22px; transform: translateX(1px); }
|
| 375 |
+
.ms-card-play:hover { background: var(--ms-accent-hot); transform: translateY(0) scale(1.06); }
|
| 376 |
+
.ms-card-play:active { transform: translateY(0) scale(0.96); }
|
| 377 |
+
|
| 378 |
+
.ms-card-title {
|
| 379 |
+
font-size: 1rem;
|
| 380 |
+
font-weight: 700;
|
| 381 |
+
color: #fff;
|
| 382 |
+
margin-bottom: 6px;
|
| 383 |
+
overflow: hidden;
|
| 384 |
+
text-overflow: ellipsis;
|
| 385 |
+
display: -webkit-box;
|
| 386 |
+
-webkit-line-clamp: 1;
|
| 387 |
+
-webkit-box-orient: vertical;
|
| 388 |
+
line-clamp: 1;
|
| 389 |
+
}
|
| 390 |
+
.ms-card-sub {
|
| 391 |
+
font-size: 0.85rem;
|
| 392 |
+
color: var(--ms-text-dim);
|
| 393 |
+
overflow: hidden;
|
| 394 |
+
text-overflow: ellipsis;
|
| 395 |
+
display: -webkit-box;
|
| 396 |
+
-webkit-line-clamp: 2;
|
| 397 |
+
-webkit-box-orient: vertical;
|
| 398 |
+
line-clamp: 2;
|
| 399 |
+
line-height: 1.4;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.ms-empty {
|
| 403 |
+
padding: 60px 20px;
|
| 404 |
+
text-align: center;
|
| 405 |
+
color: var(--ms-text-dim);
|
| 406 |
+
}
|
| 407 |
+
.ms-empty svg { width: 56px; height: 56px; margin-bottom: 14px; opacity: 0.4; }
|
| 408 |
+
.ms-empty h3 { font-size: 1.4rem; color: #fff; margin-bottom: 8px; font-weight: 800; }
|
| 409 |
+
.ms-empty p { font-size: 0.95rem; }
|
| 410 |
+
|
| 411 |
+
.ms-loading {
|
| 412 |
+
padding: 80px 20px;
|
| 413 |
+
text-align: center;
|
| 414 |
+
color: var(--ms-text-dim);
|
| 415 |
+
}
|
| 416 |
+
.ms-spinner {
|
| 417 |
+
width: 36px; height: 36px;
|
| 418 |
+
border: 3px solid rgba(124, 58, 237, 0.18);
|
| 419 |
+
border-top-color: var(--ms-accent);
|
| 420 |
+
border-radius: 50%;
|
| 421 |
+
margin: 0 auto 16px;
|
| 422 |
+
animation: msSpin 0.8s linear infinite;
|
| 423 |
+
}
|
| 424 |
+
@keyframes msSpin { to { transform: rotate(360deg); } }
|
| 425 |
+
|
| 426 |
+
.ms-album-hero {
|
| 427 |
+
display: flex;
|
| 428 |
+
gap: 28px;
|
| 429 |
+
align-items: flex-end;
|
| 430 |
+
padding: 36px 0 24px;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.ms-album-cover {
|
| 434 |
+
width: 232px; height: 232px;
|
| 435 |
+
border-radius: 6px;
|
| 436 |
+
overflow: hidden;
|
| 437 |
+
flex-shrink: 0;
|
| 438 |
+
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.85);
|
| 439 |
+
background: linear-gradient(135deg, var(--ms-accent), #2a1656);
|
| 440 |
+
}
|
| 441 |
+
.ms-album-cover img { width: 100%; height: 100%; object-fit: cover; }
|
| 442 |
+
|
| 443 |
+
.ms-album-info { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
|
| 444 |
+
.ms-album-eyebrow {
|
| 445 |
+
font-size: 0.78rem;
|
| 446 |
+
font-weight: 700;
|
| 447 |
+
text-transform: uppercase;
|
| 448 |
+
letter-spacing: 0.08em;
|
| 449 |
+
color: #fff;
|
| 450 |
+
}
|
| 451 |
+
.ms-album-title {
|
| 452 |
+
font-size: clamp(2rem, 6vw, 4rem);
|
| 453 |
+
font-weight: 900;
|
| 454 |
+
color: #fff;
|
| 455 |
+
line-height: 1.05;
|
| 456 |
+
letter-spacing: -0.03em;
|
| 457 |
+
margin: 4px 0 8px;
|
| 458 |
+
word-break: break-word;
|
| 459 |
+
}
|
| 460 |
+
.ms-album-meta { color: var(--ms-text-dim); font-size: 0.9rem; }
|
| 461 |
+
.ms-album-meta strong { color: #fff; font-weight: 700; }
|
| 462 |
+
|
| 463 |
+
.ms-album-actions {
|
| 464 |
+
display: flex;
|
| 465 |
+
gap: 16px;
|
| 466 |
+
align-items: center;
|
| 467 |
+
padding: 24px 0 10px;
|
| 468 |
+
}
|
| 469 |
+
.ms-play-big {
|
| 470 |
+
width: 60px; height: 60px;
|
| 471 |
+
border-radius: 50%;
|
| 472 |
+
background: linear-gradient(135deg, var(--ms-accent) 0%, var(--ms-accent-hot) 100%);
|
| 473 |
+
color: #fff;
|
| 474 |
+
display: flex; align-items: center; justify-content: center;
|
| 475 |
+
transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1),
|
| 476 |
+
box-shadow 0.18s ease,
|
| 477 |
+
filter 0.18s ease;
|
| 478 |
+
box-shadow: 0 10px 28px rgba(167, 139, 250, 0.45),
|
| 479 |
+
0 4px 12px rgba(0, 0, 0, 0.5),
|
| 480 |
+
inset 0 1px 0 rgba(255, 255, 255, 0.25);
|
| 481 |
+
position: relative;
|
| 482 |
+
overflow: hidden;
|
| 483 |
+
}
|
| 484 |
+
.ms-play-big::before {
|
| 485 |
+
content: "";
|
| 486 |
+
position: absolute;
|
| 487 |
+
inset: 0;
|
| 488 |
+
border-radius: inherit;
|
| 489 |
+
background: radial-gradient(circle at 30% 25%, rgba(255, 255, 255, 0.35), transparent 55%);
|
| 490 |
+
pointer-events: none;
|
| 491 |
+
}
|
| 492 |
+
.ms-play-big svg { width: 28px; height: 28px; transform: translateX(2px); position: relative; z-index: 1; }
|
| 493 |
+
.ms-play-big:hover {
|
| 494 |
+
transform: scale(1.08) translateY(-1px);
|
| 495 |
+
filter: brightness(1.08);
|
| 496 |
+
box-shadow: 0 14px 36px rgba(167, 139, 250, 0.6),
|
| 497 |
+
0 6px 14px rgba(0, 0, 0, 0.55),
|
| 498 |
+
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
| 499 |
+
}
|
| 500 |
+
.ms-play-big:active { transform: scale(0.95); transition-duration: 0.08s; }
|
| 501 |
+
.ms-play-big:focus-visible {
|
| 502 |
+
outline: none;
|
| 503 |
+
box-shadow: 0 0 0 3px rgba(167, 139, 250, 0.5),
|
| 504 |
+
0 10px 28px rgba(167, 139, 250, 0.45),
|
| 505 |
+
0 4px 12px rgba(0, 0, 0, 0.5);
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
.ms-icon-btn {
|
| 509 |
+
width: 38px; height: 38px;
|
| 510 |
+
border-radius: 50%;
|
| 511 |
+
color: var(--ms-text-dim);
|
| 512 |
+
display: flex; align-items: center; justify-content: center;
|
| 513 |
+
transition: color 0.12s, transform 0.12s;
|
| 514 |
+
}
|
| 515 |
+
.ms-icon-btn svg { width: 24px; height: 24px; }
|
| 516 |
+
.ms-icon-btn:hover { color: #fff; transform: scale(1.05); }
|
| 517 |
+
.ms-icon-btn.is-on { color: var(--ms-accent-hot); }
|
| 518 |
+
.ms-icon-btn.is-on svg { fill: currentColor; }
|
| 519 |
+
|
| 520 |
+
.ms-tracks {
|
| 521 |
+
margin-top: 8px;
|
| 522 |
+
padding-bottom: 16px;
|
| 523 |
+
}
|
| 524 |
+
.ms-tracks-head {
|
| 525 |
+
display: grid;
|
| 526 |
+
grid-template-columns: 36px 1fr 0.8fr 100px 60px;
|
| 527 |
+
gap: 14px;
|
| 528 |
+
padding: 8px 16px;
|
| 529 |
+
border-bottom: 1px solid var(--ms-divider);
|
| 530 |
+
margin-bottom: 8px;
|
| 531 |
+
color: var(--ms-text-dim);
|
| 532 |
+
font-size: 0.74rem;
|
| 533 |
+
text-transform: uppercase;
|
| 534 |
+
letter-spacing: 0.1em;
|
| 535 |
+
font-weight: 600;
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
.ms-track {
|
| 539 |
+
display: grid;
|
| 540 |
+
grid-template-columns: 36px 1fr 0.8fr 100px 60px;
|
| 541 |
+
gap: 14px;
|
| 542 |
+
align-items: center;
|
| 543 |
+
padding: 8px 16px;
|
| 544 |
+
border-radius: 4px;
|
| 545 |
+
cursor: pointer;
|
| 546 |
+
transition: background 0.12s ease;
|
| 547 |
+
color: var(--ms-text-dim);
|
| 548 |
+
position: relative;
|
| 549 |
+
}
|
| 550 |
+
.ms-track:hover { background: rgba(255, 255, 255, 0.07); color: #fff; }
|
| 551 |
+
.ms-track:hover .ms-track-num .ms-num { display: none; }
|
| 552 |
+
.ms-track:hover .ms-track-num .ms-play-mini { display: flex; }
|
| 553 |
+
.ms-track.is-playing { color: var(--ms-accent-hot); }
|
| 554 |
+
.ms-track.is-playing .ms-track-title { color: var(--ms-accent-hot); }
|
| 555 |
+
|
| 556 |
+
.ms-track-num {
|
| 557 |
+
text-align: center;
|
| 558 |
+
font-size: 0.95rem;
|
| 559 |
+
color: var(--ms-text-dim);
|
| 560 |
+
width: 36px; height: 36px;
|
| 561 |
+
display: flex; align-items: center; justify-content: center;
|
| 562 |
+
}
|
| 563 |
+
.ms-track-num .ms-play-mini { display: none; color: #fff; }
|
| 564 |
+
.ms-track-num .ms-play-mini svg { width: 16px; height: 16px; fill: currentColor; }
|
| 565 |
+
.ms-track.is-playing .ms-num { display: none; }
|
| 566 |
+
.ms-track.is-playing .ms-now-bars { display: flex; }
|
| 567 |
+
.ms-now-bars { display: none; gap: 2px; align-items: flex-end; height: 14px; }
|
| 568 |
+
.ms-now-bars span {
|
| 569 |
+
display: block; width: 3px; background: var(--ms-accent-hot); border-radius: 1px;
|
| 570 |
+
animation: msBar 0.8s ease-in-out infinite alternate;
|
| 571 |
+
}
|
| 572 |
+
.ms-now-bars span:nth-child(1) { animation-delay: -0.5s; }
|
| 573 |
+
.ms-now-bars span:nth-child(2) { animation-delay: -0.2s; }
|
| 574 |
+
.ms-now-bars span:nth-child(3) { animation-delay: -0.8s; }
|
| 575 |
+
@keyframes msBar { from { height: 4px; } to { height: 14px; } }
|
| 576 |
+
|
| 577 |
+
.ms-track-meta { display: flex; gap: 12px; align-items: center; min-width: 0; }
|
| 578 |
+
.ms-track-cover {
|
| 579 |
+
width: 40px; height: 40px;
|
| 580 |
+
border-radius: 4px;
|
| 581 |
+
object-fit: cover;
|
| 582 |
+
background: var(--ms-surface-2);
|
| 583 |
+
flex-shrink: 0;
|
| 584 |
+
}
|
| 585 |
+
.ms-track-text { min-width: 0; }
|
| 586 |
+
.ms-track-title {
|
| 587 |
+
color: #fff;
|
| 588 |
+
font-size: 0.95rem;
|
| 589 |
+
font-weight: 500;
|
| 590 |
+
white-space: nowrap;
|
| 591 |
+
overflow: hidden;
|
| 592 |
+
text-overflow: ellipsis;
|
| 593 |
+
}
|
| 594 |
+
.ms-track-sub {
|
| 595 |
+
color: var(--ms-text-dim);
|
| 596 |
+
font-size: 0.82rem;
|
| 597 |
+
white-space: nowrap;
|
| 598 |
+
overflow: hidden;
|
| 599 |
+
text-overflow: ellipsis;
|
| 600 |
+
}
|
| 601 |
+
.ms-track-sub a { transition: color 0.12s; }
|
| 602 |
+
.ms-track-sub a:hover { color: #fff; text-decoration: underline; }
|
| 603 |
+
|
| 604 |
+
.ms-track-anime {
|
| 605 |
+
color: var(--ms-text-dim);
|
| 606 |
+
font-size: 0.85rem;
|
| 607 |
+
white-space: nowrap;
|
| 608 |
+
overflow: hidden;
|
| 609 |
+
text-overflow: ellipsis;
|
| 610 |
+
}
|
| 611 |
+
.ms-track-anime:hover { color: #fff; text-decoration: underline; }
|
| 612 |
+
|
| 613 |
+
.ms-track-tag {
|
| 614 |
+
display: inline-flex;
|
| 615 |
+
align-items: center;
|
| 616 |
+
padding: 3px 10px;
|
| 617 |
+
border-radius: 999px;
|
| 618 |
+
background: rgba(124, 58, 237, 0.18);
|
| 619 |
+
border: 1px solid rgba(124, 58, 237, 0.32);
|
| 620 |
+
color: #c4b5fd;
|
| 621 |
+
font-size: 0.7rem;
|
| 622 |
+
font-weight: 700;
|
| 623 |
+
letter-spacing: 0.04em;
|
| 624 |
+
width: fit-content;
|
| 625 |
+
}
|
| 626 |
+
.ms-track-tag.ed { background: rgba(190, 24, 93, 0.16); border-color: rgba(190, 24, 93, 0.4); color: #f9a8d4; }
|
| 627 |
+
.ms-track-tag.in { background: rgba(34, 197, 94, 0.13); border-color: rgba(34, 197, 94, 0.35); color: #86efac; }
|
| 628 |
+
|
| 629 |
+
.ms-track-like {
|
| 630 |
+
width: 28px; height: 28px;
|
| 631 |
+
color: var(--ms-text-dim);
|
| 632 |
+
display: flex; align-items: center; justify-content: center;
|
| 633 |
+
border-radius: 50%;
|
| 634 |
+
transition: color 0.12s, transform 0.12s, opacity 0.12s;
|
| 635 |
+
opacity: 0;
|
| 636 |
+
}
|
| 637 |
+
.ms-track-like svg { width: 16px; height: 16px; }
|
| 638 |
+
.ms-track:hover .ms-track-like { opacity: 1; }
|
| 639 |
+
.ms-track-like.is-liked { color: var(--ms-accent-hot); opacity: 1; }
|
| 640 |
+
.ms-track-like.is-liked svg { fill: currentColor; }
|
| 641 |
+
.ms-track-like:hover { color: #fff; transform: scale(1.1); }
|
| 642 |
+
|
| 643 |
+
.ms-search-tabs {
|
| 644 |
+
display: flex;
|
| 645 |
+
gap: 8px;
|
| 646 |
+
padding: 18px 0 6px;
|
| 647 |
+
flex-wrap: wrap;
|
| 648 |
+
}
|
| 649 |
+
.ms-tab {
|
| 650 |
+
padding: 8px 18px;
|
| 651 |
+
border-radius: 999px;
|
| 652 |
+
background: var(--ms-surface-2);
|
| 653 |
+
color: #fff;
|
| 654 |
+
font-size: 0.86rem;
|
| 655 |
+
font-weight: 600;
|
| 656 |
+
transition: background 0.15s, color 0.15s;
|
| 657 |
+
cursor: pointer;
|
| 658 |
+
}
|
| 659 |
+
.ms-tab:hover { background: var(--ms-surface-3); }
|
| 660 |
+
.ms-tab.active { background: #fff; color: #000; }
|
| 661 |
+
|
| 662 |
+
.ms-search-block { margin-bottom: 36px; }
|
| 663 |
+
|
| 664 |
+
.ms-top-result {
|
| 665 |
+
display: grid;
|
| 666 |
+
grid-template-columns: 1.05fr 1.6fr;
|
| 667 |
+
gap: 24px;
|
| 668 |
+
align-items: stretch;
|
| 669 |
+
margin: 6px 0 30px;
|
| 670 |
+
}
|
| 671 |
+
@media (max-width: 900px) {
|
| 672 |
+
.ms-top-result { grid-template-columns: 1fr; }
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
.ms-top-card {
|
| 676 |
+
display: grid;
|
| 677 |
+
grid-template-columns: 120px 1fr;
|
| 678 |
+
grid-template-rows: auto auto auto 1fr;
|
| 679 |
+
grid-template-areas:
|
| 680 |
+
"cover name"
|
| 681 |
+
"cover tag"
|
| 682 |
+
"cover ."
|
| 683 |
+
"play .";
|
| 684 |
+
column-gap: 22px;
|
| 685 |
+
row-gap: 8px;
|
| 686 |
+
background: var(--ms-surface);
|
| 687 |
+
border-radius: 10px;
|
| 688 |
+
padding: 22px;
|
| 689 |
+
transition: background 0.2s;
|
| 690 |
+
cursor: pointer;
|
| 691 |
+
position: relative;
|
| 692 |
+
min-height: 220px;
|
| 693 |
+
overflow: hidden;
|
| 694 |
+
}
|
| 695 |
+
.ms-top-card:hover { background: var(--ms-surface-2); }
|
| 696 |
+
.ms-top-card:hover .ms-top-play { opacity: 1; transform: translateY(0); }
|
| 697 |
+
|
| 698 |
+
.ms-top-cover {
|
| 699 |
+
grid-area: cover;
|
| 700 |
+
width: 120px; height: 120px;
|
| 701 |
+
border-radius: 6px;
|
| 702 |
+
object-fit: cover;
|
| 703 |
+
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.6);
|
| 704 |
+
align-self: start;
|
| 705 |
+
}
|
| 706 |
+
.ms-top-card.is-artist .ms-top-cover { border-radius: 50%; }
|
| 707 |
+
.ms-top-card .ms-top-name { grid-area: name; align-self: end; margin: 0; }
|
| 708 |
+
.ms-top-card .ms-top-tag { grid-area: tag; justify-self: start; }
|
| 709 |
+
|
| 710 |
+
.ms-top-play {
|
| 711 |
+
grid-area: play;
|
| 712 |
+
justify-self: end;
|
| 713 |
+
align-self: end;
|
| 714 |
+
width: 56px; height: 56px;
|
| 715 |
+
border-radius: 50%;
|
| 716 |
+
background: var(--ms-accent);
|
| 717 |
+
color: #fff;
|
| 718 |
+
display: flex; align-items: center; justify-content: center;
|
| 719 |
+
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.55);
|
| 720 |
+
opacity: 0;
|
| 721 |
+
transform: translateY(8px);
|
| 722 |
+
transition: opacity 0.18s, transform 0.18s, background 0.18s;
|
| 723 |
+
z-index: 2;
|
| 724 |
+
}
|
| 725 |
+
.ms-top-play:hover { background: var(--ms-accent-hot); transform: translateY(0) scale(1.06); }
|
| 726 |
+
.ms-top-play svg { width: 26px; height: 26px; transform: translateX(1px); }
|
| 727 |
+
|
| 728 |
+
@media (max-width: 720px) {
|
| 729 |
+
.ms-top-card {
|
| 730 |
+
grid-template-columns: 84px 1fr;
|
| 731 |
+
grid-template-areas:
|
| 732 |
+
"cover name"
|
| 733 |
+
"cover tag"
|
| 734 |
+
"play play";
|
| 735 |
+
column-gap: 16px;
|
| 736 |
+
padding: 16px;
|
| 737 |
+
min-height: auto;
|
| 738 |
+
}
|
| 739 |
+
.ms-top-cover { width: 84px; height: 84px; }
|
| 740 |
+
.ms-top-card .ms-top-name { font-size: 1.4rem; }
|
| 741 |
+
.ms-top-play { opacity: 1; transform: none; width: 48px; height: 48px; margin-top: 6px; }
|
| 742 |
+
.ms-top-play svg { width: 22px; height: 22px; }
|
| 743 |
+
}
|
| 744 |
+
.ms-top-name {
|
| 745 |
+
font-size: 2rem;
|
| 746 |
+
font-weight: 900;
|
| 747 |
+
color: #fff;
|
| 748 |
+
margin-bottom: 10px;
|
| 749 |
+
line-height: 1.05;
|
| 750 |
+
letter-spacing: -0.02em;
|
| 751 |
+
overflow: hidden;
|
| 752 |
+
display: -webkit-box;
|
| 753 |
+
-webkit-line-clamp: 2;
|
| 754 |
+
-webkit-box-orient: vertical;
|
| 755 |
+
line-clamp: 2;
|
| 756 |
+
}
|
| 757 |
+
.ms-top-tag {
|
| 758 |
+
display: inline-flex;
|
| 759 |
+
align-items: center;
|
| 760 |
+
padding: 5px 12px;
|
| 761 |
+
background: rgba(0, 0, 0, 0.55);
|
| 762 |
+
border-radius: 4px;
|
| 763 |
+
color: #fff;
|
| 764 |
+
font-size: 0.72rem;
|
| 765 |
+
font-weight: 700;
|
| 766 |
+
text-transform: uppercase;
|
| 767 |
+
letter-spacing: 0.08em;
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
.ms-top-songs {
|
| 771 |
+
background: transparent;
|
| 772 |
+
border-radius: 8px;
|
| 773 |
+
padding: 0;
|
| 774 |
+
}
|
| 775 |
+
.ms-top-songs h3 {
|
| 776 |
+
font-size: 1.5rem;
|
| 777 |
+
font-weight: 800;
|
| 778 |
+
color: #fff;
|
| 779 |
+
margin: 0 0 12px;
|
| 780 |
+
letter-spacing: -0.01em;
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
+
.ms-songlist { display: flex; flex-direction: column; }
|
| 784 |
+
.ms-song-row {
|
| 785 |
+
display: grid;
|
| 786 |
+
grid-template-columns: 40px 1fr 28px;
|
| 787 |
+
gap: 14px;
|
| 788 |
+
align-items: center;
|
| 789 |
+
padding: 8px 12px;
|
| 790 |
+
border-radius: 6px;
|
| 791 |
+
cursor: pointer;
|
| 792 |
+
transition: background 0.12s;
|
| 793 |
+
}
|
| 794 |
+
.ms-song-row:hover { background: var(--ms-surface-2); }
|
| 795 |
+
.ms-song-row.is-playing .ms-song-title { color: var(--ms-accent-hot); }
|
| 796 |
+
|
| 797 |
+
.ms-song-cover {
|
| 798 |
+
width: 40px; height: 40px;
|
| 799 |
+
border-radius: 4px;
|
| 800 |
+
object-fit: cover;
|
| 801 |
+
background: var(--ms-surface-2);
|
| 802 |
+
flex-shrink: 0;
|
| 803 |
+
position: relative;
|
| 804 |
+
}
|
| 805 |
+
.ms-song-cover-wrap {
|
| 806 |
+
position: relative;
|
| 807 |
+
width: 40px; height: 40px;
|
| 808 |
+
flex-shrink: 0;
|
| 809 |
+
}
|
| 810 |
+
.ms-song-cover-wrap .ms-song-play {
|
| 811 |
+
position: absolute;
|
| 812 |
+
inset: 0;
|
| 813 |
+
display: none;
|
| 814 |
+
align-items: center;
|
| 815 |
+
justify-content: center;
|
| 816 |
+
background: rgba(0, 0, 0, 0.6);
|
| 817 |
+
color: #fff;
|
| 818 |
+
border-radius: 4px;
|
| 819 |
+
}
|
| 820 |
+
.ms-song-cover-wrap .ms-song-play svg { width: 16px; height: 16px; }
|
| 821 |
+
.ms-song-row:hover .ms-song-play { display: flex; }
|
| 822 |
+
|
| 823 |
+
.ms-song-text { min-width: 0; }
|
| 824 |
+
.ms-song-title {
|
| 825 |
+
color: #fff;
|
| 826 |
+
font-size: 0.92rem;
|
| 827 |
+
font-weight: 500;
|
| 828 |
+
white-space: nowrap;
|
| 829 |
+
overflow: hidden;
|
| 830 |
+
text-overflow: ellipsis;
|
| 831 |
+
}
|
| 832 |
+
.ms-song-sub {
|
| 833 |
+
color: var(--ms-text-dim);
|
| 834 |
+
font-size: 0.8rem;
|
| 835 |
+
white-space: nowrap;
|
| 836 |
+
overflow: hidden;
|
| 837 |
+
text-overflow: ellipsis;
|
| 838 |
+
}
|
| 839 |
+
.ms-song-sub a:hover { color: #fff; text-decoration: underline; }
|
| 840 |
+
|
| 841 |
+
.ms-song-like {
|
| 842 |
+
width: 28px; height: 28px;
|
| 843 |
+
color: var(--ms-text-dim);
|
| 844 |
+
display: flex; align-items: center; justify-content: center;
|
| 845 |
+
border-radius: 50%;
|
| 846 |
+
transition: color 0.12s, opacity 0.12s, transform 0.12s;
|
| 847 |
+
opacity: 0;
|
| 848 |
+
}
|
| 849 |
+
.ms-song-like svg { width: 16px; height: 16px; }
|
| 850 |
+
.ms-song-row:hover .ms-song-like { opacity: 1; }
|
| 851 |
+
.ms-song-like.is-liked { color: var(--ms-accent-hot); opacity: 1; }
|
| 852 |
+
.ms-song-like.is-liked svg { fill: currentColor; }
|
| 853 |
+
|
| 854 |
+
.player-bar {
|
| 855 |
+
position: fixed;
|
| 856 |
+
left: 0; right: 0; bottom: 0;
|
| 857 |
+
height: 90px;
|
| 858 |
+
background: #000;
|
| 859 |
+
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
| 860 |
+
display: grid;
|
| 861 |
+
grid-template-columns: minmax(180px, 30%) 1fr minmax(180px, 30%);
|
| 862 |
+
align-items: center;
|
| 863 |
+
padding: 0 16px;
|
| 864 |
+
gap: 16px;
|
| 865 |
+
z-index: 100;
|
| 866 |
+
box-shadow: 0 -10px 32px rgba(0, 0, 0, 0.6);
|
| 867 |
+
}
|
| 868 |
+
.player-bar[data-empty="1"] .pb-controls,
|
| 869 |
+
.player-bar[data-empty="1"] .pb-progress,
|
| 870 |
+
.player-bar[data-empty="1"] .pb-like { opacity: 0.5; pointer-events: none; }
|
| 871 |
+
|
| 872 |
+
.pb-track {
|
| 873 |
+
display: flex;
|
| 874 |
+
align-items: center;
|
| 875 |
+
gap: 14px;
|
| 876 |
+
min-width: 0;
|
| 877 |
+
}
|
| 878 |
+
.pb-cover {
|
| 879 |
+
width: 56px; height: 56px;
|
| 880 |
+
border-radius: 4px;
|
| 881 |
+
background: var(--ms-surface-2);
|
| 882 |
+
object-fit: cover;
|
| 883 |
+
flex-shrink: 0;
|
| 884 |
+
}
|
| 885 |
+
|
| 886 |
+
.pb-cover:not([src]),
|
| 887 |
+
.pb-cover[src=""],
|
| 888 |
+
.pb-cover.is-failed {
|
| 889 |
+
visibility: hidden;
|
| 890 |
+
}
|
| 891 |
+
.player-bar[data-empty="1"] .pb-cover {
|
| 892 |
+
visibility: hidden;
|
| 893 |
+
}
|
| 894 |
+
|
| 895 |
+
.pb-cover-fallback {
|
| 896 |
+
position: absolute;
|
| 897 |
+
inset: 0;
|
| 898 |
+
display: none;
|
| 899 |
+
align-items: center;
|
| 900 |
+
justify-content: center;
|
| 901 |
+
background: linear-gradient(135deg, #432470 0%, #1a0f33 100%);
|
| 902 |
+
color: rgba(255, 255, 255, 0.7);
|
| 903 |
+
border-radius: 4px;
|
| 904 |
+
}
|
| 905 |
+
.pb-cover-fallback svg { width: 22px; height: 22px; }
|
| 906 |
+
.pb-cover-btn:not(.has-cover) .pb-cover-fallback,
|
| 907 |
+
.player-bar[data-empty="1"] .pb-cover-fallback { display: flex; }
|
| 908 |
+
|
| 909 |
+
.pb-dismiss {
|
| 910 |
+
width: 28px; height: 28px;
|
| 911 |
+
color: var(--ms-text-mute);
|
| 912 |
+
display: flex; align-items: center; justify-content: center;
|
| 913 |
+
border-radius: 50%;
|
| 914 |
+
transition: color 0.15s, background 0.15s;
|
| 915 |
+
flex-shrink: 0;
|
| 916 |
+
}
|
| 917 |
+
.pb-dismiss svg { width: 14px; height: 14px; }
|
| 918 |
+
.pb-dismiss:hover { color: #fff; background: rgba(255, 255, 255, 0.08); }
|
| 919 |
+
.pb-meta { min-width: 0; flex: 1; }
|
| 920 |
+
.pb-title {
|
| 921 |
+
color: #fff;
|
| 922 |
+
font-size: 0.9rem;
|
| 923 |
+
font-weight: 500;
|
| 924 |
+
white-space: nowrap;
|
| 925 |
+
overflow: hidden;
|
| 926 |
+
text-overflow: ellipsis;
|
| 927 |
+
display: block;
|
| 928 |
+
transition: color 0.12s;
|
| 929 |
+
background: transparent;
|
| 930 |
+
border: 0;
|
| 931 |
+
padding: 0;
|
| 932 |
+
cursor: pointer;
|
| 933 |
+
text-align: left;
|
| 934 |
+
width: 100%;
|
| 935 |
+
font-family: inherit;
|
| 936 |
+
}
|
| 937 |
+
.pb-title:hover { text-decoration: underline; }
|
| 938 |
+
.pb-artist {
|
| 939 |
+
color: var(--ms-text-dim);
|
| 940 |
+
font-size: 0.74rem;
|
| 941 |
+
white-space: nowrap;
|
| 942 |
+
overflow: hidden;
|
| 943 |
+
text-overflow: ellipsis;
|
| 944 |
+
display: block;
|
| 945 |
+
transition: color 0.12s;
|
| 946 |
+
}
|
| 947 |
+
.pb-artist:hover { color: #fff; text-decoration: underline; }
|
| 948 |
+
.pb-like {
|
| 949 |
+
width: 32px; height: 32px;
|
| 950 |
+
color: var(--ms-text-dim);
|
| 951 |
+
display: flex; align-items: center; justify-content: center;
|
| 952 |
+
border-radius: 50%;
|
| 953 |
+
transition: color 0.12s, transform 0.12s;
|
| 954 |
+
flex-shrink: 0;
|
| 955 |
+
}
|
| 956 |
+
.pb-like svg { width: 18px; height: 18px; }
|
| 957 |
+
.pb-like:hover { color: #fff; transform: scale(1.1); }
|
| 958 |
+
.pb-like.is-liked { color: var(--ms-accent-hot); }
|
| 959 |
+
.pb-like.is-liked svg { fill: currentColor; }
|
| 960 |
+
|
| 961 |
+
.pb-center {
|
| 962 |
+
display: flex;
|
| 963 |
+
flex-direction: column;
|
| 964 |
+
align-items: center;
|
| 965 |
+
gap: 6px;
|
| 966 |
+
width: 100%;
|
| 967 |
+
max-width: 720px;
|
| 968 |
+
margin: 0 auto;
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
.pb-controls {
|
| 972 |
+
display: flex;
|
| 973 |
+
align-items: center;
|
| 974 |
+
justify-content: center;
|
| 975 |
+
gap: 18px;
|
| 976 |
+
}
|
| 977 |
+
|
| 978 |
+
.pb-btn {
|
| 979 |
+
width: 32px; height: 32px;
|
| 980 |
+
color: var(--ms-text-dim);
|
| 981 |
+
display: flex; align-items: center; justify-content: center;
|
| 982 |
+
border-radius: 50%;
|
| 983 |
+
transition: color 0.12s, transform 0.12s;
|
| 984 |
+
}
|
| 985 |
+
.pb-btn svg { width: 20px; height: 20px; }
|
| 986 |
+
.pb-btn:hover { color: #fff; transform: scale(1.08); }
|
| 987 |
+
.pb-btn.is-on { color: var(--ms-accent-hot); }
|
| 988 |
+
.pb-btn.is-on::after {
|
| 989 |
+
content: "";
|
| 990 |
+
width: 4px; height: 4px;
|
| 991 |
+
background: var(--ms-accent-hot);
|
| 992 |
+
border-radius: 50%;
|
| 993 |
+
position: absolute;
|
| 994 |
+
margin-top: 30px;
|
| 995 |
+
}
|
| 996 |
+
|
| 997 |
+
.pb-play {
|
| 998 |
+
width: 38px; height: 38px;
|
| 999 |
+
border-radius: 50%;
|
| 1000 |
+
background: #fff;
|
| 1001 |
+
color: #000;
|
| 1002 |
+
display: flex; align-items: center; justify-content: center;
|
| 1003 |
+
transition: transform 0.12s, background 0.15s;
|
| 1004 |
+
}
|
| 1005 |
+
.pb-play svg { width: 22px; height: 22px; }
|
| 1006 |
+
.pb-play:hover { transform: scale(1.07); background: #f1eaff; }
|
| 1007 |
+
.pb-play:active { transform: scale(0.96); }
|
| 1008 |
+
.player-bar[data-empty="1"] .pb-play { opacity: 0.6; }
|
| 1009 |
+
|
| 1010 |
+
.pb-progress {
|
| 1011 |
+
display: flex;
|
| 1012 |
+
align-items: center;
|
| 1013 |
+
gap: 12px;
|
| 1014 |
+
width: 100%;
|
| 1015 |
+
padding: 0 12px;
|
| 1016 |
+
}
|
| 1017 |
+
.pb-time {
|
| 1018 |
+
font-size: 0.7rem;
|
| 1019 |
+
color: var(--ms-text-dim);
|
| 1020 |
+
font-variant-numeric: tabular-nums;
|
| 1021 |
+
width: 38px;
|
| 1022 |
+
text-align: center;
|
| 1023 |
+
flex-shrink: 0;
|
| 1024 |
+
}
|
| 1025 |
+
.pb-bar {
|
| 1026 |
+
flex: 1;
|
| 1027 |
+
height: 4px;
|
| 1028 |
+
background: rgba(255, 255, 255, 0.18);
|
| 1029 |
+
border-radius: 2px;
|
| 1030 |
+
position: relative;
|
| 1031 |
+
cursor: pointer;
|
| 1032 |
+
transition: height 0.12s;
|
| 1033 |
+
}
|
| 1034 |
+
.pb-bar:hover { height: 6px; }
|
| 1035 |
+
.pb-bar:hover .pb-bar-fill { background: var(--ms-accent-hot); }
|
| 1036 |
+
.pb-bar:hover .pb-bar-knob { opacity: 1; }
|
| 1037 |
+
.pb-bar-fill {
|
| 1038 |
+
position: absolute;
|
| 1039 |
+
left: 0; top: 0; bottom: 0;
|
| 1040 |
+
width: 0%;
|
| 1041 |
+
background: #fff;
|
| 1042 |
+
border-radius: 2px;
|
| 1043 |
+
transition: background 0.12s;
|
| 1044 |
+
}
|
| 1045 |
+
.pb-bar-knob {
|
| 1046 |
+
position: absolute;
|
| 1047 |
+
top: 50%;
|
| 1048 |
+
width: 12px; height: 12px;
|
| 1049 |
+
background: #fff;
|
| 1050 |
+
border-radius: 50%;
|
| 1051 |
+
transform: translate(-50%, -50%);
|
| 1052 |
+
opacity: 0;
|
| 1053 |
+
transition: opacity 0.12s;
|
| 1054 |
+
pointer-events: none;
|
| 1055 |
+
}
|
| 1056 |
+
|
| 1057 |
+
.pb-right {
|
| 1058 |
+
display: flex;
|
| 1059 |
+
align-items: center;
|
| 1060 |
+
justify-content: flex-end;
|
| 1061 |
+
gap: 8px;
|
| 1062 |
+
}
|
| 1063 |
+
.pb-mobile-play { display: none; }
|
| 1064 |
+
.pb-vol {
|
| 1065 |
+
width: 100px;
|
| 1066 |
+
height: 4px;
|
| 1067 |
+
background: rgba(255, 255, 255, 0.18);
|
| 1068 |
+
border-radius: 2px;
|
| 1069 |
+
position: relative;
|
| 1070 |
+
cursor: pointer;
|
| 1071 |
+
}
|
| 1072 |
+
.pb-vol:hover .pb-bar-fill { background: var(--ms-accent-hot); }
|
| 1073 |
+
.pb-vol:hover .pb-bar-knob { opacity: 1; }
|
| 1074 |
+
|
| 1075 |
+
.ms-sidebar-backdrop {
|
| 1076 |
+
display: none;
|
| 1077 |
+
position: fixed;
|
| 1078 |
+
inset: 0;
|
| 1079 |
+
background: rgba(0, 0, 0, 0.6);
|
| 1080 |
+
z-index: 60;
|
| 1081 |
+
opacity: 0;
|
| 1082 |
+
transition: opacity 0.22s ease;
|
| 1083 |
+
}
|
| 1084 |
+
.ms-sidebar-backdrop.active {
|
| 1085 |
+
display: block;
|
| 1086 |
+
opacity: 1;
|
| 1087 |
+
}
|
| 1088 |
+
|
| 1089 |
+
@media (hover: none), (max-width: 800px) {
|
| 1090 |
+
.ms-card-play {
|
| 1091 |
+
opacity: 1;
|
| 1092 |
+
transform: translateY(0);
|
| 1093 |
+
width: 44px; height: 44px;
|
| 1094 |
+
right: 8px; bottom: 8px;
|
| 1095 |
+
}
|
| 1096 |
+
.ms-card-play svg { width: 18px; height: 18px; }
|
| 1097 |
+
.ms-track-like { opacity: 1; }
|
| 1098 |
+
.ms-song-like { opacity: 1; }
|
| 1099 |
+
.ms-song-cover-wrap .ms-song-play { display: none; }
|
| 1100 |
+
}
|
| 1101 |
+
|
| 1102 |
+
@media (max-width: 1024px) {
|
| 1103 |
+
.music-app { grid-template-columns: 1fr; }
|
| 1104 |
+
.music-sidebar {
|
| 1105 |
+
position: fixed;
|
| 1106 |
+
top: var(--header-h);
|
| 1107 |
+
left: 0;
|
| 1108 |
+
bottom: 92px;
|
| 1109 |
+
width: 280px;
|
| 1110 |
+
z-index: 80;
|
| 1111 |
+
margin: 8px;
|
| 1112 |
+
transform: translateX(-110%);
|
| 1113 |
+
transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
|
| 1114 |
+
pointer-events: none;
|
| 1115 |
+
}
|
| 1116 |
+
.music-sidebar.open {
|
| 1117 |
+
transform: translateX(0);
|
| 1118 |
+
pointer-events: auto;
|
| 1119 |
+
}
|
| 1120 |
+
|
| 1121 |
+
.music-sidebar.open .ms-link,
|
| 1122 |
+
.music-sidebar.open .ms-year-grid a,
|
| 1123 |
+
.music-sidebar.open .ms-block-title { pointer-events: auto; }
|
| 1124 |
+
|
| 1125 |
+
.music-sidebar .ms-link { padding: 12px 14px; font-size: 0.98rem; }
|
| 1126 |
+
|
| 1127 |
+
.music-sidebar .ms-link,
|
| 1128 |
+
.music-sidebar .ms-year-grid a,
|
| 1129 |
+
.ms-sidebar-toggle {
|
| 1130 |
+
touch-action: manipulation;
|
| 1131 |
+
-webkit-tap-highlight-color: rgba(167, 139, 250, 0.18);
|
| 1132 |
+
}
|
| 1133 |
+
.ms-sidebar-toggle { display: flex; }
|
| 1134 |
+
.ms-sidebar-backdrop { z-index: 70; }
|
| 1135 |
+
}
|
| 1136 |
+
|
| 1137 |
+
@media (max-width: 800px) {
|
| 1138 |
+
.ms-album-hero {
|
| 1139 |
+
flex-direction: column;
|
| 1140 |
+
align-items: flex-start;
|
| 1141 |
+
gap: 18px;
|
| 1142 |
+
padding: 16px 0;
|
| 1143 |
+
}
|
| 1144 |
+
.ms-album-cover { width: 180px; height: 180px; align-self: center; }
|
| 1145 |
+
.ms-album-info { align-items: flex-start; text-align: left; width: 100%; }
|
| 1146 |
+
.ms-tracks-head, .ms-track {
|
| 1147 |
+
grid-template-columns: 36px 1fr 56px;
|
| 1148 |
+
}
|
| 1149 |
+
.ms-tracks-head .ms-th-anime, .ms-tracks-head .ms-th-tag,
|
| 1150 |
+
.ms-track .ms-track-anime, .ms-track .ms-track-tag-cell { display: none; }
|
| 1151 |
+
.ms-view { padding: 0 14px 24px; }
|
| 1152 |
+
.ms-topbar { padding: 12px 14px; gap: 8px; }
|
| 1153 |
+
.ms-search { max-width: none; }
|
| 1154 |
+
.ms-card-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 16px; }
|
| 1155 |
+
.ms-top-name { font-size: 1.5rem; }
|
| 1156 |
+
}
|
| 1157 |
+
|
| 1158 |
+
@media (max-width: 720px) {
|
| 1159 |
+
.player-bar {
|
| 1160 |
+
height: 64px;
|
| 1161 |
+
grid-template-columns: 1fr auto;
|
| 1162 |
+
padding: 0 10px;
|
| 1163 |
+
gap: 10px;
|
| 1164 |
+
bottom: 0;
|
| 1165 |
+
border-radius: 0;
|
| 1166 |
+
}
|
| 1167 |
+
.pb-cover-btn { width: 44px; height: 44px; }
|
| 1168 |
+
.pb-cover { width: 44px; height: 44px; }
|
| 1169 |
+
.pb-title { font-size: 0.86rem; }
|
| 1170 |
+
.pb-artist { font-size: 0.72rem; }
|
| 1171 |
+
.pb-like, .pb-center { display: none; }
|
| 1172 |
+
.pb-mobile-play {
|
| 1173 |
+
display: flex;
|
| 1174 |
+
width: 40px; height: 40px;
|
| 1175 |
+
color: #fff;
|
| 1176 |
+
background: transparent;
|
| 1177 |
+
border-radius: 50%;
|
| 1178 |
+
}
|
| 1179 |
+
.pb-mobile-play svg { width: 24px; height: 24px; }
|
| 1180 |
+
.pb-right .pb-btn:not(#pb-play-mobile), .pb-right .pb-vol { display: none; }
|
| 1181 |
+
.music-app {
|
| 1182 |
+
bottom: 64px;
|
| 1183 |
+
padding: 6px;
|
| 1184 |
+
gap: 6px;
|
| 1185 |
+
top: 10px;
|
| 1186 |
+
}
|
| 1187 |
+
.music-sidebar { bottom: 64px; }
|
| 1188 |
+
.player-bar[data-empty="1"] { display: none; }
|
| 1189 |
+
body.music-body:has(.player-bar[data-empty="1"]) .music-app,
|
| 1190 |
+
body.music-body:has(.player-bar[data-empty="1"]) .music-sidebar {
|
| 1191 |
+
bottom: 0;
|
| 1192 |
+
}
|
| 1193 |
+
.ms-topbar {
|
| 1194 |
+
padding: 12px 12px;
|
| 1195 |
+
gap: 8px;
|
| 1196 |
+
}
|
| 1197 |
+
.ms-search {
|
| 1198 |
+
max-width: none;
|
| 1199 |
+
}
|
| 1200 |
+
.ms-search input {
|
| 1201 |
+
padding: 10px 38px 10px 38px;
|
| 1202 |
+
font-size: 0.84rem;
|
| 1203 |
+
}
|
| 1204 |
+
.ms-sidebar-toggle {
|
| 1205 |
+
width: 34px;
|
| 1206 |
+
height: 34px;
|
| 1207 |
+
}
|
| 1208 |
+
}
|
| 1209 |
+
|
| 1210 |
+
@media (max-width: 480px) {
|
| 1211 |
+
.ms-card-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
|
| 1212 |
+
.ms-section-title h2 { font-size: 1.2rem; }
|
| 1213 |
+
.ms-top-name { font-size: 1.25rem; }
|
| 1214 |
+
.ms-years-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
|
| 1215 |
+
.ms-year-num { font-size: 1.8rem; }
|
| 1216 |
+
}
|
| 1217 |
+
|
| 1218 |
+
.ms-home-hero {
|
| 1219 |
+
margin: 18px 0 28px;
|
| 1220 |
+
}
|
| 1221 |
+
.ms-home-greet {
|
| 1222 |
+
font-size: 2rem;
|
| 1223 |
+
font-weight: 900;
|
| 1224 |
+
letter-spacing: -0.02em;
|
| 1225 |
+
color: #fff;
|
| 1226 |
+
margin: 0 0 18px;
|
| 1227 |
+
}
|
| 1228 |
+
.ms-quick-grid {
|
| 1229 |
+
display: grid;
|
| 1230 |
+
grid-template-columns: repeat(3, 1fr);
|
| 1231 |
+
gap: 10px;
|
| 1232 |
+
}
|
| 1233 |
+
@media (max-width: 900px) { .ms-quick-grid { grid-template-columns: repeat(2, 1fr); } }
|
| 1234 |
+
@media (max-width: 540px) { .ms-quick-grid { grid-template-columns: 1fr; } }
|
| 1235 |
+
|
| 1236 |
+
.ms-quick-tile {
|
| 1237 |
+
display: flex;
|
| 1238 |
+
align-items: center;
|
| 1239 |
+
gap: 12px;
|
| 1240 |
+
background: rgba(255, 255, 255, 0.06);
|
| 1241 |
+
border-radius: 6px;
|
| 1242 |
+
padding: 0 14px 0 0;
|
| 1243 |
+
height: 64px;
|
| 1244 |
+
overflow: hidden;
|
| 1245 |
+
position: relative;
|
| 1246 |
+
transition: background 0.15s;
|
| 1247 |
+
}
|
| 1248 |
+
.ms-quick-tile:hover { background: rgba(255, 255, 255, 0.12); }
|
| 1249 |
+
.ms-quick-cover {
|
| 1250 |
+
width: 64px; height: 64px;
|
| 1251 |
+
object-fit: cover;
|
| 1252 |
+
flex-shrink: 0;
|
| 1253 |
+
background: var(--ms-surface-2);
|
| 1254 |
+
}
|
| 1255 |
+
.ms-quick-name {
|
| 1256 |
+
flex: 1;
|
| 1257 |
+
font-weight: 700;
|
| 1258 |
+
color: #fff;
|
| 1259 |
+
font-size: 0.9rem;
|
| 1260 |
+
overflow: hidden;
|
| 1261 |
+
text-overflow: ellipsis;
|
| 1262 |
+
white-space: nowrap;
|
| 1263 |
+
min-width: 0;
|
| 1264 |
+
}
|
| 1265 |
+
.ms-quick-play {
|
| 1266 |
+
width: 44px; height: 44px;
|
| 1267 |
+
border-radius: 50%;
|
| 1268 |
+
background: var(--ms-accent);
|
| 1269 |
+
color: #fff;
|
| 1270 |
+
display: flex; align-items: center; justify-content: center;
|
| 1271 |
+
flex-shrink: 0;
|
| 1272 |
+
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.45);
|
| 1273 |
+
opacity: 0;
|
| 1274 |
+
transform: translateX(8px);
|
| 1275 |
+
transition: opacity 0.15s, transform 0.15s, background 0.15s;
|
| 1276 |
+
}
|
| 1277 |
+
.ms-quick-tile:hover .ms-quick-play { opacity: 1; transform: translateX(0); }
|
| 1278 |
+
.ms-quick-play:hover { background: var(--ms-accent-hot); transform: scale(1.06); }
|
| 1279 |
+
.ms-quick-play svg { width: 18px; height: 18px; transform: translateX(1px); }
|
| 1280 |
+
.ms-quick-empty {
|
| 1281 |
+
grid-column: 1 / -1;
|
| 1282 |
+
color: var(--ms-text-dim);
|
| 1283 |
+
background: var(--ms-surface);
|
| 1284 |
+
border-radius: 6px;
|
| 1285 |
+
padding: 18px;
|
| 1286 |
+
font-size: 0.92rem;
|
| 1287 |
+
}
|
| 1288 |
+
|
| 1289 |
+
.ms-years-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px; }
|
| 1290 |
+
.ms-year-card {
|
| 1291 |
+
display: block;
|
| 1292 |
+
background: var(--ms-surface);
|
| 1293 |
+
padding: 14px;
|
| 1294 |
+
border-radius: 8px;
|
| 1295 |
+
transition: background 0.18s;
|
| 1296 |
+
text-align: left;
|
| 1297 |
+
}
|
| 1298 |
+
.ms-year-card:hover { background: var(--ms-surface-2); }
|
| 1299 |
+
.ms-year-cover {
|
| 1300 |
+
width: 100%;
|
| 1301 |
+
aspect-ratio: 1 / 1;
|
| 1302 |
+
border-radius: 6px;
|
| 1303 |
+
display: flex;
|
| 1304 |
+
align-items: center;
|
| 1305 |
+
justify-content: center;
|
| 1306 |
+
margin-bottom: 12px;
|
| 1307 |
+
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.55);
|
| 1308 |
+
overflow: hidden;
|
| 1309 |
+
position: relative;
|
| 1310 |
+
}
|
| 1311 |
+
.ms-year-cover::after {
|
| 1312 |
+
content: "";
|
| 1313 |
+
position: absolute;
|
| 1314 |
+
inset: 0;
|
| 1315 |
+
background: radial-gradient(circle at 30% 25%, rgba(255,255,255,0.12), transparent 55%);
|
| 1316 |
+
pointer-events: none;
|
| 1317 |
+
}
|
| 1318 |
+
.ms-year-num {
|
| 1319 |
+
font-size: 2.4rem;
|
| 1320 |
+
font-weight: 900;
|
| 1321 |
+
letter-spacing: -0.04em;
|
| 1322 |
+
color: #fff;
|
| 1323 |
+
text-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
|
| 1324 |
+
}
|
| 1325 |
+
|
| 1326 |
+
.pb-cover-btn {
|
| 1327 |
+
position: relative;
|
| 1328 |
+
width: 56px; height: 56px;
|
| 1329 |
+
border-radius: 4px;
|
| 1330 |
+
overflow: hidden;
|
| 1331 |
+
flex-shrink: 0;
|
| 1332 |
+
background: transparent;
|
| 1333 |
+
cursor: pointer;
|
| 1334 |
+
display: block;
|
| 1335 |
+
}
|
| 1336 |
+
.pb-cover-btn .pb-cover { width: 100%; height: 100%; display: block; }
|
| 1337 |
+
.pb-cover-overlay {
|
| 1338 |
+
position: absolute;
|
| 1339 |
+
inset: 0;
|
| 1340 |
+
display: flex;
|
| 1341 |
+
align-items: center;
|
| 1342 |
+
justify-content: center;
|
| 1343 |
+
background: rgba(0, 0, 0, 0.55);
|
| 1344 |
+
color: #fff;
|
| 1345 |
+
opacity: 0;
|
| 1346 |
+
transition: opacity 0.15s;
|
| 1347 |
+
}
|
| 1348 |
+
.pb-cover-overlay svg { width: 18px; height: 18px; }
|
| 1349 |
+
.pb-cover-btn:hover .pb-cover-overlay { opacity: 1; }
|
| 1350 |
+
.player-bar[data-empty="1"] .pb-cover-btn { pointer-events: none; }
|
| 1351 |
+
|
| 1352 |
+
.np-modal {
|
| 1353 |
+
position: fixed;
|
| 1354 |
+
inset: 0;
|
| 1355 |
+
z-index: 10001;
|
| 1356 |
+
color: #fff;
|
| 1357 |
+
overflow: hidden;
|
| 1358 |
+
animation: npFadeIn 0.22s ease-out;
|
| 1359 |
+
}
|
| 1360 |
+
.np-modal[hidden] { display: none; }
|
| 1361 |
+
body.np-open { overflow: hidden; }
|
| 1362 |
+
body.np-open .float-nav { display: none !important; }
|
| 1363 |
+
|
| 1364 |
+
body.np-open #header { display: none !important; }
|
| 1365 |
+
body.np-open .np-modal { top: 0; }
|
| 1366 |
+
@keyframes npFadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
| 1367 |
+
|
| 1368 |
+
.np-bg {
|
| 1369 |
+
position: absolute;
|
| 1370 |
+
inset: 0;
|
| 1371 |
+
background-size: cover;
|
| 1372 |
+
background-position: center;
|
| 1373 |
+
filter: blur(38px) saturate(150%) brightness(0.6);
|
| 1374 |
+
transform: scale(1.2);
|
| 1375 |
+
z-index: 0;
|
| 1376 |
+
transition: filter 0.35s ease;
|
| 1377 |
+
}
|
| 1378 |
+
.np-bg::after {
|
| 1379 |
+
content: "";
|
| 1380 |
+
position: absolute;
|
| 1381 |
+
inset: 0;
|
| 1382 |
+
background: linear-gradient(180deg, rgba(10,5,25,0.55) 0%, rgba(10,5,25,0.85) 60%, #07050d 100%);
|
| 1383 |
+
transition: opacity 0.35s ease;
|
| 1384 |
+
}
|
| 1385 |
+
|
| 1386 |
+
body.np-lyrics-open .np-bg {
|
| 1387 |
+
filter: blur(46px) saturate(140%) brightness(0.45);
|
| 1388 |
+
}
|
| 1389 |
+
body.np-lyrics-open .np-bg::after {
|
| 1390 |
+
background: linear-gradient(180deg, rgba(8,4,20,0.72) 0%, rgba(8,4,20,0.88) 60%, #050208 100%);
|
| 1391 |
+
opacity: 1;
|
| 1392 |
+
}
|
| 1393 |
+
|
| 1394 |
+
body.np-lyrics-open .np-stage { box-shadow: none; background: transparent; }
|
| 1395 |
+
body.np-lyrics-open .np-cover,
|
| 1396 |
+
body.np-lyrics-open .np-cover-fallback { display: none !important; }
|
| 1397 |
+
|
| 1398 |
+
.np-inner {
|
| 1399 |
+
position: relative;
|
| 1400 |
+
z-index: 1;
|
| 1401 |
+
height: 100vh;
|
| 1402 |
+
height: 100dvh;
|
| 1403 |
+
max-width: 520px;
|
| 1404 |
+
margin: 0 auto;
|
| 1405 |
+
padding: 14px 22px 22px;
|
| 1406 |
+
display: grid;
|
| 1407 |
+
grid-template-rows: auto minmax(0, 1fr) auto auto auto auto;
|
| 1408 |
+
gap: 14px;
|
| 1409 |
+
}
|
| 1410 |
+
@media (min-width: 900px) {
|
| 1411 |
+
.np-inner { max-width: 560px; padding: 18px 28px 26px; gap: 16px; }
|
| 1412 |
+
}
|
| 1413 |
+
@media (max-width: 480px) {
|
| 1414 |
+
.np-inner { padding: 10px 16px 18px; gap: 10px; }
|
| 1415 |
+
}
|
| 1416 |
+
|
| 1417 |
+
.np-header {
|
| 1418 |
+
display: flex;
|
| 1419 |
+
align-items: center;
|
| 1420 |
+
justify-content: space-between;
|
| 1421 |
+
gap: 12px;
|
| 1422 |
+
}
|
| 1423 |
+
.np-icon-btn {
|
| 1424 |
+
min-width: 36px; height: 36px;
|
| 1425 |
+
display: flex; align-items: center; justify-content: center;
|
| 1426 |
+
gap: 6px;
|
| 1427 |
+
padding: 0 6px;
|
| 1428 |
+
color: #fff;
|
| 1429 |
+
border-radius: 18px;
|
| 1430 |
+
transition: background 0.15s;
|
| 1431 |
+
}
|
| 1432 |
+
.np-icon-btn:hover { background: rgba(255, 255, 255, 0.12); }
|
| 1433 |
+
.np-icon-btn svg { width: 22px; height: 22px; }
|
| 1434 |
+
|
| 1435 |
+
.np-back-btn span {
|
| 1436 |
+
font-size: 0.85rem;
|
| 1437 |
+
font-weight: 600;
|
| 1438 |
+
padding-right: 6px;
|
| 1439 |
+
}
|
| 1440 |
+
@media (max-width: 600px) {
|
| 1441 |
+
.np-back-btn span { display: none; }
|
| 1442 |
+
.np-back-btn { padding: 0; min-width: 36px; border-radius: 50%; }
|
| 1443 |
+
}
|
| 1444 |
+
.np-source { text-align: center; min-width: 0; flex: 1; }
|
| 1445 |
+
.np-eyebrow {
|
| 1446 |
+
font-size: 0.66rem;
|
| 1447 |
+
text-transform: uppercase;
|
| 1448 |
+
letter-spacing: 0.18em;
|
| 1449 |
+
color: rgba(255, 255, 255, 0.7);
|
| 1450 |
+
font-weight: 700;
|
| 1451 |
+
margin-bottom: 2px;
|
| 1452 |
+
}
|
| 1453 |
+
.np-source-name {
|
| 1454 |
+
font-size: 0.86rem;
|
| 1455 |
+
font-weight: 600;
|
| 1456 |
+
color: #fff;
|
| 1457 |
+
text-decoration: none;
|
| 1458 |
+
overflow: hidden;
|
| 1459 |
+
text-overflow: ellipsis;
|
| 1460 |
+
white-space: nowrap;
|
| 1461 |
+
display: block;
|
| 1462 |
+
}
|
| 1463 |
+
.np-source-name:hover { text-decoration: underline; }
|
| 1464 |
+
|
| 1465 |
+
.np-stage {
|
| 1466 |
+
position: relative;
|
| 1467 |
+
|
| 1468 |
+
width: min(100%, 70vh, 560px);
|
| 1469 |
+
max-width: 100%;
|
| 1470 |
+
aspect-ratio: 1 / 1;
|
| 1471 |
+
max-height: min(70vh, 560px);
|
| 1472 |
+
margin: 0 auto;
|
| 1473 |
+
border-radius: 12px;
|
| 1474 |
+
overflow: hidden;
|
| 1475 |
+
background: linear-gradient(135deg, #432470 0%, #1a0f33 100%);
|
| 1476 |
+
box-shadow: 0 26px 60px rgba(0, 0, 0, 0.6);
|
| 1477 |
+
align-self: center;
|
| 1478 |
+
flex-shrink: 0;
|
| 1479 |
+
}
|
| 1480 |
+
@media (max-width: 600px) {
|
| 1481 |
+
.np-stage {
|
| 1482 |
+
|
| 1483 |
+
width: min(100%, 54vh, 460px);
|
| 1484 |
+
max-height: 54vh;
|
| 1485 |
+
}
|
| 1486 |
+
}
|
| 1487 |
+
@media (max-width: 380px) {
|
| 1488 |
+
.np-stage {
|
| 1489 |
+
width: min(100%, 48vh, 380px);
|
| 1490 |
+
max-height: 48vh;
|
| 1491 |
+
}
|
| 1492 |
+
}
|
| 1493 |
+
|
| 1494 |
+
@media (max-height: 520px) and (orientation: landscape) {
|
| 1495 |
+
.np-stage {
|
| 1496 |
+
width: min(82vh, 400px);
|
| 1497 |
+
max-height: 82vh;
|
| 1498 |
+
}
|
| 1499 |
+
}
|
| 1500 |
+
.np-cover, .np-video {
|
| 1501 |
+
position: absolute;
|
| 1502 |
+
inset: 0;
|
| 1503 |
+
width: 100%;
|
| 1504 |
+
height: 100%;
|
| 1505 |
+
object-fit: cover;
|
| 1506 |
+
display: block;
|
| 1507 |
+
}
|
| 1508 |
+
.np-video { background: #000; object-fit: contain; }
|
| 1509 |
+
|
| 1510 |
+
.np-cover[hidden], .np-video[hidden] { display: none !important; }
|
| 1511 |
+
|
| 1512 |
+
.np-cover:not([src]),
|
| 1513 |
+
.np-cover[src=""],
|
| 1514 |
+
.np-cover.is-failed { visibility: hidden; }
|
| 1515 |
+
|
| 1516 |
+
.np-cover-fallback {
|
| 1517 |
+
position: absolute;
|
| 1518 |
+
inset: 0;
|
| 1519 |
+
display: flex;
|
| 1520 |
+
align-items: center;
|
| 1521 |
+
justify-content: center;
|
| 1522 |
+
color: rgba(255, 255, 255, 0.55);
|
| 1523 |
+
pointer-events: none;
|
| 1524 |
+
}
|
| 1525 |
+
.np-cover-fallback svg { width: 72px; height: 72px; }
|
| 1526 |
+
|
| 1527 |
+
.np-loading {
|
| 1528 |
+
position: absolute;
|
| 1529 |
+
inset: 0;
|
| 1530 |
+
display: flex;
|
| 1531 |
+
align-items: center;
|
| 1532 |
+
justify-content: center;
|
| 1533 |
+
background: rgba(0, 0, 0, 0.45);
|
| 1534 |
+
backdrop-filter: blur(2px);
|
| 1535 |
+
-webkit-backdrop-filter: blur(2px);
|
| 1536 |
+
z-index: 3;
|
| 1537 |
+
animation: npFadeIn 0.18s ease-out;
|
| 1538 |
+
}
|
| 1539 |
+
.np-loading[hidden] { display: none; }
|
| 1540 |
+
.np-spinner {
|
| 1541 |
+
width: 56px;
|
| 1542 |
+
height: 56px;
|
| 1543 |
+
border-radius: 50%;
|
| 1544 |
+
border: 3px solid rgba(255, 255, 255, 0.18);
|
| 1545 |
+
border-top-color: var(--ms-accent-hot);
|
| 1546 |
+
animation: npSpin 0.85s linear infinite;
|
| 1547 |
+
}
|
| 1548 |
+
@keyframes npSpin { to { transform: rotate(360deg); } }
|
| 1549 |
+
|
| 1550 |
+
.np-panel-close {
|
| 1551 |
+
width: 32px; height: 32px;
|
| 1552 |
+
color: rgba(255, 255, 255, 0.7);
|
| 1553 |
+
display: flex; align-items: center; justify-content: center;
|
| 1554 |
+
border-radius: 50%;
|
| 1555 |
+
transition: background 0.15s, color 0.15s;
|
| 1556 |
+
flex-shrink: 0;
|
| 1557 |
+
}
|
| 1558 |
+
.np-panel-close svg { width: 16px; height: 16px; }
|
| 1559 |
+
.np-panel-close:hover { color: #fff; background: rgba(255, 255, 255, 0.12); }
|
| 1560 |
+
.np-lyrics-head {
|
| 1561 |
+
display: flex;
|
| 1562 |
+
align-items: center;
|
| 1563 |
+
justify-content: space-between;
|
| 1564 |
+
margin-bottom: 12px;
|
| 1565 |
+
}
|
| 1566 |
+
.np-lyrics-head h3 { margin: 0; }
|
| 1567 |
+
|
| 1568 |
+
.np-lyrics {
|
| 1569 |
+
position: absolute;
|
| 1570 |
+
inset: 0;
|
| 1571 |
+
|
| 1572 |
+
background: radial-gradient(120% 80% at 50% 0%, #1a0d2e 0%, #0a0612 55%, #050208 100%);
|
| 1573 |
+
overflow: hidden;
|
| 1574 |
+
padding: 18px 22px;
|
| 1575 |
+
display: flex;
|
| 1576 |
+
align-items: center;
|
| 1577 |
+
justify-content: center;
|
| 1578 |
+
}
|
| 1579 |
+
.np-lyrics[hidden] { display: none; }
|
| 1580 |
+
.np-lyrics-inner { width: 100%; max-width: 420px; }
|
| 1581 |
+
.np-lyrics-inner h3 {
|
| 1582 |
+
margin: 0 0 12px;
|
| 1583 |
+
font-size: 1.1rem;
|
| 1584 |
+
font-weight: 800;
|
| 1585 |
+
color: #fff;
|
| 1586 |
+
}
|
| 1587 |
+
.np-lyrics-body {
|
| 1588 |
+
color: rgba(255, 255, 255, 0.92);
|
| 1589 |
+
line-height: 1.55;
|
| 1590 |
+
font-size: 0.96rem;
|
| 1591 |
+
margin: 0 0 18px;
|
| 1592 |
+
}
|
| 1593 |
+
.np-lyrics-meta {
|
| 1594 |
+
display: grid;
|
| 1595 |
+
gap: 10px;
|
| 1596 |
+
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
| 1597 |
+
padding-top: 14px;
|
| 1598 |
+
}
|
| 1599 |
+
.np-lyrics-meta div {
|
| 1600 |
+
display: flex;
|
| 1601 |
+
justify-content: space-between;
|
| 1602 |
+
gap: 16px;
|
| 1603 |
+
font-size: 0.85rem;
|
| 1604 |
+
}
|
| 1605 |
+
.np-lyrics-meta span {
|
| 1606 |
+
color: rgba(255, 255, 255, 0.55);
|
| 1607 |
+
text-transform: uppercase;
|
| 1608 |
+
letter-spacing: 0.08em;
|
| 1609 |
+
font-weight: 700;
|
| 1610 |
+
font-size: 0.7rem;
|
| 1611 |
+
}
|
| 1612 |
+
.np-lyrics-meta strong {
|
| 1613 |
+
color: #fff;
|
| 1614 |
+
font-weight: 600;
|
| 1615 |
+
text-align: right;
|
| 1616 |
+
overflow: hidden;
|
| 1617 |
+
text-overflow: ellipsis;
|
| 1618 |
+
white-space: nowrap;
|
| 1619 |
+
max-width: 60%;
|
| 1620 |
+
}
|
| 1621 |
+
|
| 1622 |
+
.np-info {
|
| 1623 |
+
display: flex;
|
| 1624 |
+
align-items: center;
|
| 1625 |
+
gap: 16px;
|
| 1626 |
+
}
|
| 1627 |
+
.np-info-text { flex: 1; min-width: 0; }
|
| 1628 |
+
.np-title {
|
| 1629 |
+
display: block;
|
| 1630 |
+
color: #fff;
|
| 1631 |
+
font-size: 1.4rem;
|
| 1632 |
+
font-weight: 800;
|
| 1633 |
+
letter-spacing: -0.01em;
|
| 1634 |
+
text-decoration: none;
|
| 1635 |
+
overflow: hidden;
|
| 1636 |
+
text-overflow: ellipsis;
|
| 1637 |
+
white-space: nowrap;
|
| 1638 |
+
margin-bottom: 4px;
|
| 1639 |
+
}
|
| 1640 |
+
.np-title:hover { text-decoration: underline; }
|
| 1641 |
+
.np-artist {
|
| 1642 |
+
display: block;
|
| 1643 |
+
color: rgba(255, 255, 255, 0.75);
|
| 1644 |
+
font-size: 0.95rem;
|
| 1645 |
+
text-decoration: none;
|
| 1646 |
+
overflow: hidden;
|
| 1647 |
+
text-overflow: ellipsis;
|
| 1648 |
+
white-space: nowrap;
|
| 1649 |
+
}
|
| 1650 |
+
.np-artist:hover { color: #fff; text-decoration: underline; }
|
| 1651 |
+
.np-like {
|
| 1652 |
+
width: 40px; height: 40px;
|
| 1653 |
+
color: rgba(255, 255, 255, 0.85);
|
| 1654 |
+
display: flex; align-items: center; justify-content: center;
|
| 1655 |
+
border-radius: 50%;
|
| 1656 |
+
transition: color 0.15s, transform 0.15s;
|
| 1657 |
+
flex-shrink: 0;
|
| 1658 |
+
}
|
| 1659 |
+
.np-like svg { width: 24px; height: 24px; }
|
| 1660 |
+
.np-like:hover { color: #fff; transform: scale(1.08); }
|
| 1661 |
+
.np-like.is-liked { color: var(--ms-accent-hot); }
|
| 1662 |
+
.np-like.is-liked svg { fill: currentColor; }
|
| 1663 |
+
|
| 1664 |
+
.np-progress { display: grid; gap: 6px; }
|
| 1665 |
+
.np-bar {
|
| 1666 |
+
position: relative;
|
| 1667 |
+
height: 4px;
|
| 1668 |
+
background: rgba(255, 255, 255, 0.18);
|
| 1669 |
+
border-radius: 2px;
|
| 1670 |
+
cursor: pointer;
|
| 1671 |
+
transition: height 0.12s;
|
| 1672 |
+
}
|
| 1673 |
+
.np-bar:hover { height: 6px; }
|
| 1674 |
+
.np-bar-fill {
|
| 1675 |
+
position: absolute;
|
| 1676 |
+
left: 0; top: 0; bottom: 0;
|
| 1677 |
+
width: 0;
|
| 1678 |
+
background: #fff;
|
| 1679 |
+
border-radius: 2px;
|
| 1680 |
+
}
|
| 1681 |
+
.np-bar:hover .np-bar-fill { background: var(--ms-accent-hot); }
|
| 1682 |
+
.np-bar-knob {
|
| 1683 |
+
position: absolute;
|
| 1684 |
+
top: 50%;
|
| 1685 |
+
width: 12px; height: 12px;
|
| 1686 |
+
background: #fff;
|
| 1687 |
+
border-radius: 50%;
|
| 1688 |
+
transform: translate(-50%, -50%);
|
| 1689 |
+
opacity: 0;
|
| 1690 |
+
transition: opacity 0.12s;
|
| 1691 |
+
}
|
| 1692 |
+
.np-bar:hover .np-bar-knob { opacity: 1; }
|
| 1693 |
+
.np-times {
|
| 1694 |
+
display: flex;
|
| 1695 |
+
justify-content: space-between;
|
| 1696 |
+
color: rgba(255, 255, 255, 0.7);
|
| 1697 |
+
font-size: 0.72rem;
|
| 1698 |
+
font-variant-numeric: tabular-nums;
|
| 1699 |
+
}
|
| 1700 |
+
|
| 1701 |
+
.np-controls {
|
| 1702 |
+
display: flex;
|
| 1703 |
+
align-items: center;
|
| 1704 |
+
justify-content: space-between;
|
| 1705 |
+
padding: 0 4px;
|
| 1706 |
+
}
|
| 1707 |
+
.np-ctl {
|
| 1708 |
+
width: 44px; height: 44px;
|
| 1709 |
+
color: rgba(255, 255, 255, 0.85);
|
| 1710 |
+
display: flex; align-items: center; justify-content: center;
|
| 1711 |
+
border-radius: 50%;
|
| 1712 |
+
transition: color 0.15s, transform 0.15s;
|
| 1713 |
+
position: relative;
|
| 1714 |
+
}
|
| 1715 |
+
.np-ctl svg { width: 26px; height: 26px; }
|
| 1716 |
+
.np-ctl:hover { color: #fff; transform: scale(1.08); }
|
| 1717 |
+
.np-ctl.is-on { color: var(--ms-accent-hot); }
|
| 1718 |
+
.np-ctl.is-on::after {
|
| 1719 |
+
content: "";
|
| 1720 |
+
position: absolute;
|
| 1721 |
+
bottom: 6px;
|
| 1722 |
+
left: 50%;
|
| 1723 |
+
transform: translateX(-50%);
|
| 1724 |
+
width: 4px; height: 4px;
|
| 1725 |
+
background: var(--ms-accent-hot);
|
| 1726 |
+
border-radius: 50%;
|
| 1727 |
+
}
|
| 1728 |
+
.np-play-big {
|
| 1729 |
+
width: 64px; height: 64px;
|
| 1730 |
+
border-radius: 50%;
|
| 1731 |
+
background: #fff;
|
| 1732 |
+
color: #000;
|
| 1733 |
+
display: flex; align-items: center; justify-content: center;
|
| 1734 |
+
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.45);
|
| 1735 |
+
transition: transform 0.12s;
|
| 1736 |
+
}
|
| 1737 |
+
.np-play-big svg { width: 30px; height: 30px; transform: translateX(1px); }
|
| 1738 |
+
.np-play-big:hover { transform: scale(1.06); }
|
| 1739 |
+
.np-play-big:active { transform: scale(0.96); }
|
| 1740 |
+
|
| 1741 |
+
.np-tools {
|
| 1742 |
+
display: flex;
|
| 1743 |
+
align-items: center;
|
| 1744 |
+
justify-content: center;
|
| 1745 |
+
gap: 12px;
|
| 1746 |
+
padding-top: 4px;
|
| 1747 |
+
}
|
| 1748 |
+
.np-tool {
|
| 1749 |
+
display: inline-flex;
|
| 1750 |
+
align-items: center;
|
| 1751 |
+
gap: 8px;
|
| 1752 |
+
padding: 9px 18px;
|
| 1753 |
+
border-radius: 999px;
|
| 1754 |
+
background: rgba(255, 255, 255, 0.10);
|
| 1755 |
+
color: rgba(255, 255, 255, 0.92);
|
| 1756 |
+
font-weight: 600;
|
| 1757 |
+
font-size: 0.84rem;
|
| 1758 |
+
letter-spacing: 0.01em;
|
| 1759 |
+
transition: background 0.15s, color 0.15s, transform 0.12s;
|
| 1760 |
+
}
|
| 1761 |
+
.np-tool svg { width: 18px; height: 18px; }
|
| 1762 |
+
.np-tool:hover { background: rgba(255, 255, 255, 0.18); color: #fff; }
|
| 1763 |
+
.np-tool.is-on {
|
| 1764 |
+
background: var(--ms-accent);
|
| 1765 |
+
color: #fff;
|
| 1766 |
+
}
|
| 1767 |
+
.np-tool.is-on:hover { background: var(--ms-accent-hot); }
|
| 1768 |
+
.np-tool.is-disabled { opacity: 0.45; pointer-events: none; }
|
| 1769 |
+
|
| 1770 |
+
@media (min-width: 720px) {
|
| 1771 |
+
.np-inner { max-width: 620px; padding: 28px 32px 36px; gap: 22px; }
|
| 1772 |
+
.np-title { font-size: 1.7rem; }
|
| 1773 |
+
}
|
| 1774 |
+
|
| 1775 |
+
#toast-container {
|
| 1776 |
+
position: fixed;
|
| 1777 |
+
bottom: 110px;
|
| 1778 |
+
left: 50%;
|
| 1779 |
+
transform: translateX(-50%);
|
| 1780 |
+
z-index: 300;
|
| 1781 |
+
display: flex;
|
| 1782 |
+
flex-direction: column;
|
| 1783 |
+
align-items: center;
|
| 1784 |
+
gap: 8px;
|
| 1785 |
+
pointer-events: none;
|
| 1786 |
+
}
|
| 1787 |
+
.kv-toast {
|
| 1788 |
+
background: rgba(20, 14, 36, 0.96);
|
| 1789 |
+
color: #fff;
|
| 1790 |
+
border: 1px solid rgba(255, 255, 255, 0.10);
|
| 1791 |
+
padding: 10px 18px;
|
| 1792 |
+
border-radius: 999px;
|
| 1793 |
+
font-size: 0.85rem;
|
| 1794 |
+
font-weight: 600;
|
| 1795 |
+
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45);
|
| 1796 |
+
opacity: 0;
|
| 1797 |
+
transform: translateY(8px);
|
| 1798 |
+
transition: opacity 0.2s, transform 0.2s;
|
| 1799 |
+
}
|
| 1800 |
+
.kv-toast.show { opacity: 1; transform: none; }
|
| 1801 |
+
|
| 1802 |
+
.np-queue {
|
| 1803 |
+
position: absolute;
|
| 1804 |
+
inset: 0;
|
| 1805 |
+
background: rgba(8, 4, 18, 0.92);
|
| 1806 |
+
backdrop-filter: blur(28px);
|
| 1807 |
+
-webkit-backdrop-filter: blur(28px);
|
| 1808 |
+
border-radius: inherit;
|
| 1809 |
+
display: flex;
|
| 1810 |
+
flex-direction: column;
|
| 1811 |
+
overflow: hidden;
|
| 1812 |
+
animation: npFadeIn 0.18s ease-out;
|
| 1813 |
+
}
|
| 1814 |
+
.np-queue[hidden] { display: none; }
|
| 1815 |
+
.np-queue-head {
|
| 1816 |
+
flex-shrink: 0;
|
| 1817 |
+
padding: 16px 18px 10px;
|
| 1818 |
+
display: flex;
|
| 1819 |
+
align-items: baseline;
|
| 1820 |
+
justify-content: space-between;
|
| 1821 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
| 1822 |
+
}
|
| 1823 |
+
.np-queue-head h3 {
|
| 1824 |
+
font-size: 1rem;
|
| 1825 |
+
font-weight: 800;
|
| 1826 |
+
letter-spacing: 0.04em;
|
| 1827 |
+
text-transform: uppercase;
|
| 1828 |
+
}
|
| 1829 |
+
.np-queue-head span {
|
| 1830 |
+
font-size: 0.78rem;
|
| 1831 |
+
color: var(--ms-text-dim);
|
| 1832 |
+
font-weight: 600;
|
| 1833 |
+
}
|
| 1834 |
+
.np-queue-list {
|
| 1835 |
+
flex: 1;
|
| 1836 |
+
overflow-y: auto;
|
| 1837 |
+
padding: 6px 8px 12px;
|
| 1838 |
+
scrollbar-width: thin;
|
| 1839 |
+
scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
|
| 1840 |
+
}
|
| 1841 |
+
.np-queue-empty {
|
| 1842 |
+
color: var(--ms-text-dim);
|
| 1843 |
+
text-align: center;
|
| 1844 |
+
padding: 30px 16px;
|
| 1845 |
+
font-size: 0.92rem;
|
| 1846 |
+
}
|
| 1847 |
+
.np-queue-row {
|
| 1848 |
+
width: 100%;
|
| 1849 |
+
display: grid;
|
| 1850 |
+
grid-template-columns: 44px 1fr auto;
|
| 1851 |
+
gap: 12px;
|
| 1852 |
+
align-items: center;
|
| 1853 |
+
padding: 8px 10px;
|
| 1854 |
+
border-radius: 8px;
|
| 1855 |
+
background: transparent;
|
| 1856 |
+
color: #fff;
|
| 1857 |
+
text-align: left;
|
| 1858 |
+
cursor: pointer;
|
| 1859 |
+
transition: background 0.15s;
|
| 1860 |
+
}
|
| 1861 |
+
.np-queue-row:hover { background: rgba(255, 255, 255, 0.07); }
|
| 1862 |
+
.np-queue-row.is-current { background: rgba(124, 58, 237, 0.18); }
|
| 1863 |
+
.np-queue-cover {
|
| 1864 |
+
position: relative;
|
| 1865 |
+
width: 44px; height: 44px;
|
| 1866 |
+
border-radius: 6px;
|
| 1867 |
+
overflow: hidden;
|
| 1868 |
+
background: var(--ms-surface-2);
|
| 1869 |
+
flex-shrink: 0;
|
| 1870 |
+
}
|
| 1871 |
+
.np-queue-cover img { width: 100%; height: 100%; object-fit: cover; }
|
| 1872 |
+
.np-queue-eq {
|
| 1873 |
+
position: absolute;
|
| 1874 |
+
inset: 0;
|
| 1875 |
+
background: rgba(0, 0, 0, 0.55);
|
| 1876 |
+
display: flex;
|
| 1877 |
+
align-items: flex-end;
|
| 1878 |
+
justify-content: center;
|
| 1879 |
+
gap: 2px;
|
| 1880 |
+
padding-bottom: 6px;
|
| 1881 |
+
}
|
| 1882 |
+
.np-queue-eq i {
|
| 1883 |
+
width: 3px; height: 12px;
|
| 1884 |
+
background: var(--ms-accent-hot);
|
| 1885 |
+
border-radius: 1px;
|
| 1886 |
+
animation: eqBars 1s ease-in-out infinite;
|
| 1887 |
+
animation-play-state: paused;
|
| 1888 |
+
transform: scaleY(0.4);
|
| 1889 |
+
transform-origin: bottom center;
|
| 1890 |
+
}
|
| 1891 |
+
.np-queue-eq i:nth-child(2) { animation-delay: 0.15s; height: 16px; }
|
| 1892 |
+
.np-queue-eq i:nth-child(3) { animation-delay: 0.3s; height: 10px; }
|
| 1893 |
+
|
| 1894 |
+
body.is-playing .np-queue-eq i { animation-play-state: running; }
|
| 1895 |
+
@keyframes eqBars {
|
| 1896 |
+
0%, 100% { transform: scaleY(0.4); }
|
| 1897 |
+
50% { transform: scaleY(1); }
|
| 1898 |
+
}
|
| 1899 |
+
|
| 1900 |
+
.np-lyric-line {
|
| 1901 |
+
display: block;
|
| 1902 |
+
padding: 6px 4px;
|
| 1903 |
+
line-height: 1.55;
|
| 1904 |
+
color: rgba(255, 255, 255, 0.55);
|
| 1905 |
+
transition: color 0.25s ease, transform 0.25s ease, font-weight 0.2s ease;
|
| 1906 |
+
font-size: 1rem;
|
| 1907 |
+
}
|
| 1908 |
+
.np-lyric-line.is-active {
|
| 1909 |
+
color: #fff;
|
| 1910 |
+
font-weight: 700;
|
| 1911 |
+
transform: scale(1.04);
|
| 1912 |
+
transform-origin: left center;
|
| 1913 |
+
text-shadow: 0 0 18px rgba(157, 110, 248, 0.55);
|
| 1914 |
+
}
|
| 1915 |
+
.np-lyric-line.is-loading,
|
| 1916 |
+
.np-lyric-line.is-empty {
|
| 1917 |
+
color: rgba(255, 255, 255, 0.5);
|
| 1918 |
+
font-style: italic;
|
| 1919 |
+
}
|
| 1920 |
+
.np-queue-text {
|
| 1921 |
+
display: flex;
|
| 1922 |
+
flex-direction: column;
|
| 1923 |
+
gap: 2px;
|
| 1924 |
+
min-width: 0;
|
| 1925 |
+
}
|
| 1926 |
+
.np-queue-title {
|
| 1927 |
+
font-size: 0.95rem;
|
| 1928 |
+
font-weight: 600;
|
| 1929 |
+
color: #fff;
|
| 1930 |
+
overflow: hidden;
|
| 1931 |
+
text-overflow: ellipsis;
|
| 1932 |
+
white-space: nowrap;
|
| 1933 |
+
}
|
| 1934 |
+
.np-queue-row.is-current .np-queue-title { color: var(--ms-accent-hot); }
|
| 1935 |
+
.np-queue-sub {
|
| 1936 |
+
font-size: 0.78rem;
|
| 1937 |
+
color: var(--ms-text-dim);
|
| 1938 |
+
overflow: hidden;
|
| 1939 |
+
text-overflow: ellipsis;
|
| 1940 |
+
white-space: nowrap;
|
| 1941 |
+
}
|
| 1942 |
+
.np-queue-tag {
|
| 1943 |
+
font-size: 0.7rem;
|
| 1944 |
+
color: var(--ms-text-dim);
|
| 1945 |
+
font-weight: 700;
|
| 1946 |
+
text-transform: uppercase;
|
| 1947 |
+
letter-spacing: 0.06em;
|
| 1948 |
+
flex-shrink: 0;
|
| 1949 |
+
}
|
| 1950 |
+
|
| 1951 |
+
.ms-link:hover {
|
| 1952 |
+
color: #fff;
|
| 1953 |
+
background: rgba(255, 255, 255, 0.06);
|
| 1954 |
+
}
|
| 1955 |
+
.ms-link:active { background: rgba(255, 255, 255, 0.12); }
|
| 1956 |
+
|
| 1957 |
+
.pb-cover-btn { cursor: pointer; }
|
| 1958 |
+
|
| 1959 |
+
@media (max-width: 720px) {
|
| 1960 |
+
.np-inner {
|
| 1961 |
+
padding: 14px 16px 22px;
|
| 1962 |
+
gap: 14px;
|
| 1963 |
+
}
|
| 1964 |
+
.np-stage { aspect-ratio: 1 / 1; }
|
| 1965 |
+
.np-tools { gap: 8px; }
|
| 1966 |
+
.np-tool { padding: 8px 12px; font-size: 0.78rem; }
|
| 1967 |
+
.np-tool span { display: inline; }
|
| 1968 |
+
.np-title { font-size: 1.1rem; }
|
| 1969 |
+
.np-artist { font-size: 0.88rem; }
|
| 1970 |
+
.np-controls { gap: 18px; }
|
| 1971 |
+
.np-play-big { width: 64px; height: 64px; }
|
| 1972 |
+
.np-play-big svg { width: 26px; height: 26px; }
|
| 1973 |
+
.np-queue-row { grid-template-columns: 40px 1fr auto; }
|
| 1974 |
+
.np-queue-cover { width: 40px; height: 40px; }
|
| 1975 |
+
}
|
| 1976 |
+
|
| 1977 |
+
@media (max-width: 380px) {
|
| 1978 |
+
.np-tool span { display: none; }
|
| 1979 |
+
.np-tool { padding: 8px 10px; }
|
| 1980 |
+
.np-controls { gap: 12px; }
|
| 1981 |
+
.np-controls .pb-icon-btn { width: 36px; height: 36px; }
|
| 1982 |
+
.np-play-big { width: 58px; height: 58px; }
|
| 1983 |
+
}
|
| 1984 |
+
|
| 1985 |
+
@media (min-width: 721px) and (max-width: 1024px) {
|
| 1986 |
+
.np-inner { max-width: 460px; padding: 22px 28px 30px; }
|
| 1987 |
+
}
|
| 1988 |
+
|
| 1989 |
+
@media (min-width: 1200px) {
|
| 1990 |
+
.np-inner { max-width: 560px; }
|
| 1991 |
+
.np-stage { max-height: 420px; }
|
| 1992 |
+
}
|
| 1993 |
+
|
| 1994 |
+
.np-inner {
|
| 1995 |
+
grid-template-rows: auto minmax(0, 1fr) auto auto auto auto;
|
| 1996 |
+
align-content: stretch;
|
| 1997 |
+
gap: 14px;
|
| 1998 |
+
padding: 14px 22px max(20px, env(safe-area-inset-bottom));
|
| 1999 |
+
overflow: hidden;
|
| 2000 |
+
overscroll-behavior: contain;
|
| 2001 |
+
box-sizing: border-box;
|
| 2002 |
+
}
|
| 2003 |
+
.np-stage {
|
| 2004 |
+
max-height: min(60vh, 480px);
|
| 2005 |
+
min-height: 0;
|
| 2006 |
+
}
|
| 2007 |
+
@media (max-height: 780px) {
|
| 2008 |
+
.np-inner { gap: 10px; padding: 10px 18px max(14px, env(safe-area-inset-bottom)); }
|
| 2009 |
+
.np-stage { max-height: min(52vh, 400px); }
|
| 2010 |
+
.np-controls { gap: 14px; }
|
| 2011 |
+
.np-tools { padding-top: 0; gap: 8px; }
|
| 2012 |
+
.np-tool { padding: 7px 12px; font-size: 0.78rem; }
|
| 2013 |
+
}
|
| 2014 |
+
@media (max-height: 640px) {
|
| 2015 |
+
.np-inner { gap: 6px; padding: 8px 14px max(10px, env(safe-area-inset-bottom)); }
|
| 2016 |
+
.np-stage { max-height: min(42vh, 300px); }
|
| 2017 |
+
.np-info { padding: 6px 10px; }
|
| 2018 |
+
.np-title { font-size: 1rem; margin-bottom: 2px; }
|
| 2019 |
+
.np-artist { font-size: 0.82rem; }
|
| 2020 |
+
.np-play-big { width: 54px; height: 54px; }
|
| 2021 |
+
.np-play-big svg { width: 22px; height: 22px; }
|
| 2022 |
+
.np-ctl { width: 38px; height: 38px; }
|
| 2023 |
+
.np-ctl svg { width: 22px; height: 22px; }
|
| 2024 |
+
}
|
| 2025 |
+
@media (max-height: 520px) {
|
| 2026 |
+
.np-stage { max-height: 30vh; }
|
| 2027 |
+
.np-inner { gap: 4px; padding: 6px 12px max(8px, env(safe-area-inset-bottom)); }
|
| 2028 |
+
.np-tool span { display: none; }
|
| 2029 |
+
.np-tool { padding: 6px 10px; }
|
| 2030 |
+
}
|
| 2031 |
+
|
| 2032 |
+
body.np-lyrics-open .np-inner {
|
| 2033 |
+
grid-template-rows: auto minmax(140px, 1fr) auto auto auto auto;
|
| 2034 |
+
align-content: stretch;
|
| 2035 |
+
overflow: hidden;
|
| 2036 |
+
}
|
| 2037 |
+
body.np-lyrics-open .np-stage {
|
| 2038 |
+
width: 100%;
|
| 2039 |
+
max-width: 100%;
|
| 2040 |
+
aspect-ratio: auto;
|
| 2041 |
+
max-height: none;
|
| 2042 |
+
height: 100%;
|
| 2043 |
+
border-radius: 8px;
|
| 2044 |
+
}
|
| 2045 |
+
|
| 2046 |
+
.np-back-btn {
|
| 2047 |
+
background: rgba(255, 255, 255, 0.08);
|
| 2048 |
+
border: 1px solid rgba(255, 255, 255, 0.10);
|
| 2049 |
+
padding: 0 12px 0 8px !important;
|
| 2050 |
+
min-width: auto !important;
|
| 2051 |
+
border-radius: 18px !important;
|
| 2052 |
+
height: 36px;
|
| 2053 |
+
}
|
| 2054 |
+
.np-back-btn span { display: inline !important; padding-right: 0 !important; }
|
| 2055 |
+
.np-back-btn:hover { background: rgba(255, 255, 255, 0.16); }
|
| 2056 |
+
|
| 2057 |
+
.np-lyrics {
|
| 2058 |
+
align-items: stretch;
|
| 2059 |
+
justify-content: center;
|
| 2060 |
+
padding: 28px 18px;
|
| 2061 |
+
overscroll-behavior: contain;
|
| 2062 |
+
}
|
| 2063 |
+
.np-lyrics-inner {
|
| 2064 |
+
width: 100%;
|
| 2065 |
+
max-width: 640px;
|
| 2066 |
+
height: 100%;
|
| 2067 |
+
margin: 0 auto;
|
| 2068 |
+
text-align: center;
|
| 2069 |
+
display: flex;
|
| 2070 |
+
flex-direction: column;
|
| 2071 |
+
min-height: 0;
|
| 2072 |
+
}
|
| 2073 |
+
.np-lyrics-inner h3 { text-align: center; }
|
| 2074 |
+
.np-lyrics-head { flex: 0 0 auto; justify-content: center; gap: 14px; position: relative; }
|
| 2075 |
+
.np-lyrics-head .np-panel-close { position: absolute; right: 0; top: 50%; transform: translateY(-50%); }
|
| 2076 |
+
.np-lyrics-body {
|
| 2077 |
+
flex: 1 1 0;
|
| 2078 |
+
min-height: 0;
|
| 2079 |
+
text-align: center;
|
| 2080 |
+
font-size: 1.45rem;
|
| 2081 |
+
line-height: 1.5;
|
| 2082 |
+
margin: 0;
|
| 2083 |
+
color: #fff;
|
| 2084 |
+
|
| 2085 |
+
padding: 45% 6px;
|
| 2086 |
+
overflow-y: auto;
|
| 2087 |
+
overflow-x: hidden;
|
| 2088 |
+
|
| 2089 |
+
-webkit-overflow-scrolling: touch;
|
| 2090 |
+
scrollbar-width: none;
|
| 2091 |
+
|
| 2092 |
+
-webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 14%, #000 86%, transparent 100%);
|
| 2093 |
+
mask-image: linear-gradient(180deg, transparent 0%, #000 14%, #000 86%, transparent 100%);
|
| 2094 |
+
}
|
| 2095 |
+
.np-lyrics-body::-webkit-scrollbar { width: 0; height: 0; display: none; }
|
| 2096 |
+
.np-lyrics-meta { flex: 0 0 auto; padding-top: 8px; }
|
| 2097 |
+
|
| 2098 |
+
.np-lyric-line {
|
| 2099 |
+
display: block;
|
| 2100 |
+
padding: 10px 6px;
|
| 2101 |
+
line-height: 1.35;
|
| 2102 |
+
font-size: 1.4rem;
|
| 2103 |
+
|
| 2104 |
+
font-weight: 500;
|
| 2105 |
+
letter-spacing: -0.005em;
|
| 2106 |
+
color: rgba(255, 255, 255, 0.55);
|
| 2107 |
+
transform: scale(0.96);
|
| 2108 |
+
transform-origin: center;
|
| 2109 |
+
transition: opacity 0.45s cubic-bezier(0.22, 1, 0.36, 1),
|
| 2110 |
+
transform 0.55s cubic-bezier(0.22, 1, 0.36, 1),
|
| 2111 |
+
color 0.4s cubic-bezier(0.22, 1, 0.36, 1),
|
| 2112 |
+
font-weight 0.25s ease,
|
| 2113 |
+
text-shadow 0.5s ease;
|
| 2114 |
+
text-align: center;
|
| 2115 |
+
will-change: transform, color;
|
| 2116 |
+
|
| 2117 |
+
max-width: 100%;
|
| 2118 |
+
word-break: break-word;
|
| 2119 |
+
overflow-wrap: anywhere;
|
| 2120 |
+
hyphens: auto;
|
| 2121 |
+
white-space: normal;
|
| 2122 |
+
}
|
| 2123 |
+
|
| 2124 |
+
.np-lyric-line.is-past {
|
| 2125 |
+
color: rgba(255, 255, 255, 0.32);
|
| 2126 |
+
font-weight: 500;
|
| 2127 |
+
}
|
| 2128 |
+
|
| 2129 |
+
.np-lyric-line.is-active {
|
| 2130 |
+
transform: scale(1.12);
|
| 2131 |
+
color: #ffffff;
|
| 2132 |
+
font-weight: 900;
|
| 2133 |
+
letter-spacing: 0;
|
| 2134 |
+
text-shadow: 0 0 22px rgba(167, 139, 250, 0.55),
|
| 2135 |
+
0 2px 14px rgba(0, 0, 0, 0.55);
|
| 2136 |
+
}
|
| 2137 |
+
|
| 2138 |
+
.np-lyric-line.is-active ~ .np-lyric-line {
|
| 2139 |
+
color: rgba(255, 255, 255, 0.55);
|
| 2140 |
+
font-weight: 500;
|
| 2141 |
+
transform: scale(0.96);
|
| 2142 |
+
}
|
| 2143 |
+
.np-lyric-line.is-loading,
|
| 2144 |
+
.np-lyric-line.is-empty {
|
| 2145 |
+
transform: none;
|
| 2146 |
+
font-style: italic;
|
| 2147 |
+
font-weight: 500;
|
| 2148 |
+
font-size: 1rem;
|
| 2149 |
+
color: rgba(255, 255, 255, 0.7);
|
| 2150 |
+
}
|
| 2151 |
+
|
| 2152 |
+
@media (max-width: 600px) {
|
| 2153 |
+
.np-lyrics { padding: 18px 10px; }
|
| 2154 |
+
.np-lyrics-body { font-size: 1.15rem; }
|
| 2155 |
+
.np-lyric-line { font-size: 1.15rem; padding: 8px 4px; }
|
| 2156 |
+
.np-lyric-line.is-active { transform: scale(1.08); }
|
| 2157 |
+
}
|
| 2158 |
+
|
| 2159 |
+
.np-info {
|
| 2160 |
+
background: linear-gradient(135deg, rgba(124, 58, 237, 0.10), rgba(255, 255, 255, 0.02));
|
| 2161 |
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
| 2162 |
+
border-radius: 14px;
|
| 2163 |
+
padding: 10px 14px;
|
| 2164 |
+
}
|
| 2165 |
+
.np-like {
|
| 2166 |
+
width: 44px !important;
|
| 2167 |
+
height: 44px !important;
|
| 2168 |
+
background: rgba(255, 255, 255, 0.06);
|
| 2169 |
+
border: 1px solid rgba(255, 255, 255, 0.10);
|
| 2170 |
+
color: rgba(255, 255, 255, 0.85);
|
| 2171 |
+
transition: color 0.18s ease, background 0.18s ease, transform 0.18s ease, border-color 0.18s ease, box-shadow 0.2s ease;
|
| 2172 |
+
}
|
| 2173 |
+
.np-like svg { width: 22px !important; height: 22px !important; transition: transform 0.18s ease; }
|
| 2174 |
+
.np-like:hover {
|
| 2175 |
+
background: rgba(244, 63, 94, 0.12);
|
| 2176 |
+
border-color: rgba(244, 63, 94, 0.45);
|
| 2177 |
+
color: #fff;
|
| 2178 |
+
transform: scale(1.06);
|
| 2179 |
+
}
|
| 2180 |
+
.np-like.is-liked {
|
| 2181 |
+
color: #fff;
|
| 2182 |
+
background: linear-gradient(135deg, #f43f5e, #ec4899);
|
| 2183 |
+
border-color: rgba(244, 63, 94, 0.65);
|
| 2184 |
+
box-shadow: 0 8px 22px rgba(244, 63, 94, 0.45);
|
| 2185 |
+
}
|
| 2186 |
+
.np-like.is-liked svg { fill: currentColor; transform: scale(1.05); }
|
| 2187 |
+
.np-like.just-liked { animation: npLikePop 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) 1; }
|
| 2188 |
+
@keyframes npLikePop {
|
| 2189 |
+
0% { transform: scale(1); }
|
| 2190 |
+
40% { transform: scale(1.25); }
|
| 2191 |
+
100% { transform: scale(1.06); }
|
| 2192 |
+
}
|
| 2193 |
+
@media (max-width: 600px) {
|
| 2194 |
+
.np-info { padding: 8px 12px; gap: 12px; }
|
| 2195 |
+
.np-like { width: 40px !important; height: 40px !important; }
|
| 2196 |
+
.np-like svg { width: 20px !important; height: 20px !important; }
|
| 2197 |
+
}
|
frontend/public/music.html
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
| 6 |
+
<title>Music · anicove</title>
|
| 7 |
+
<meta name="description" content="Listen to anime openings, endings and OSTs. Powered by AnimeThemes.">
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
| 11 |
+
<link rel="stylesheet" href="/music.css">
|
| 12 |
+
<style>
|
| 13 |
+
:root {
|
| 14 |
+
--header-h: 0px;
|
| 15 |
+
}
|
| 16 |
+
body {
|
| 17 |
+
font-family: 'Outfit', sans-serif;
|
| 18 |
+
margin: 0;
|
| 19 |
+
padding: 0;
|
| 20 |
+
}
|
| 21 |
+
</style>
|
| 22 |
+
</head>
|
| 23 |
+
|
| 24 |
+
<body class="music-body">
|
| 25 |
+
<main class="music-app" id="music-app">
|
| 26 |
+
<aside class="music-sidebar" id="music-sidebar">
|
| 27 |
+
<div class="ms-block">
|
| 28 |
+
<div class="ms-block-title">
|
| 29 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2h-4v-7h-6v7H5a2 2 0 0 1-2-2z"/></svg>
|
| 30 |
+
<span>Browse</span>
|
| 31 |
+
</div>
|
| 32 |
+
<a class="ms-link" data-route="#/" href="#/">
|
| 33 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20"/><path d="M12 2a14.5 14.5 0 0 1 0 20"/><line x1="2" y1="12" x2="22" y2="12"/></svg>
|
| 34 |
+
<span>Discover</span>
|
| 35 |
+
</a>
|
| 36 |
+
<a class="ms-link" data-route="#/recent" href="#/recent">
|
| 37 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
| 38 |
+
<span>Newest Themes</span>
|
| 39 |
+
</a>
|
| 40 |
+
<a class="ms-link" data-route="#/artists" href="#/artists">
|
| 41 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
| 42 |
+
<span>Artists</span>
|
| 43 |
+
</a>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<div class="ms-block">
|
| 47 |
+
<div class="ms-block-title">
|
| 48 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
| 49 |
+
<span>Library</span>
|
| 50 |
+
</div>
|
| 51 |
+
<a class="ms-link" data-route="#/liked" href="#/liked">
|
| 52 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
| 53 |
+
<span>Liked Songs</span>
|
| 54 |
+
</a>
|
| 55 |
+
<a class="ms-link" data-route="#/history" href="#/history">
|
| 56 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
|
| 57 |
+
<span>Recently Played</span>
|
| 58 |
+
</a>
|
| 59 |
+
</div>
|
| 60 |
+
|
| 61 |
+
<div class="ms-block">
|
| 62 |
+
<div class="ms-block-title">
|
| 63 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
|
| 64 |
+
<span>By Year</span>
|
| 65 |
+
</div>
|
| 66 |
+
<div class="ms-year-grid" id="ms-year-grid"></div>
|
| 67 |
+
</div>
|
| 68 |
+
</aside>
|
| 69 |
+
|
| 70 |
+
<section class="music-main" id="music-main">
|
| 71 |
+
<div class="ms-topbar">
|
| 72 |
+
<button class="ms-nav-btn" id="ms-back" aria-label="Back" disabled>
|
| 73 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
|
| 74 |
+
</button>
|
| 75 |
+
<button class="ms-nav-btn" id="ms-fwd" aria-label="Forward" disabled>
|
| 76 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
| 77 |
+
</button>
|
| 78 |
+
|
| 79 |
+
<div class="ms-search">
|
| 80 |
+
<span class="ms-search-icon">
|
| 81 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
| 82 |
+
</span>
|
| 83 |
+
<input id="ms-search-input" type="text" placeholder="What do you want to listen to?" autocomplete="off">
|
| 84 |
+
<button class="ms-search-clear" id="ms-search-clear" aria-label="Clear" hidden>
|
| 85 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
| 86 |
+
</button>
|
| 87 |
+
</div>
|
| 88 |
+
|
| 89 |
+
<button class="ms-sidebar-toggle" id="ms-sidebar-toggle" aria-label="Library">
|
| 90 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
| 91 |
+
</button>
|
| 92 |
+
</div>
|
| 93 |
+
|
| 94 |
+
<div class="ms-view" id="ms-view"></div>
|
| 95 |
+
</section>
|
| 96 |
+
</main>
|
| 97 |
+
|
| 98 |
+
<div class="ms-sidebar-backdrop" id="ms-sidebar-backdrop"></div>
|
| 99 |
+
|
| 100 |
+
<div class="player-bar" id="player-bar" data-empty="1">
|
| 101 |
+
<div class="pb-track">
|
| 102 |
+
<button class="pb-cover-btn" id="pb-expand" aria-label="Expand player">
|
| 103 |
+
<img class="pb-cover" id="pb-cover" alt="" referrerpolicy="no-referrer">
|
| 104 |
+
<span class="pb-cover-fallback" id="pb-cover-fallback" aria-hidden="true">
|
| 105 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
|
| 106 |
+
</span>
|
| 107 |
+
<span class="pb-cover-overlay">
|
| 108 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
|
| 109 |
+
</span>
|
| 110 |
+
</button>
|
| 111 |
+
<div class="pb-meta">
|
| 112 |
+
<button class="pb-title" id="pb-title" type="button">Pick a song to start</button>
|
| 113 |
+
<a class="pb-artist" id="pb-artist" href="#">—</a>
|
| 114 |
+
</div>
|
| 115 |
+
<button class="pb-like" id="pb-like" aria-label="Like">
|
| 116 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
| 117 |
+
</button>
|
| 118 |
+
<button class="pb-dismiss" id="pb-dismiss" aria-label="Hide player">
|
| 119 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
| 120 |
+
</button>
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
<div class="pb-center">
|
| 124 |
+
<div class="pb-controls">
|
| 125 |
+
<button class="pb-btn" id="pb-shuffle" aria-label="Shuffle">
|
| 126 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>
|
| 127 |
+
</button>
|
| 128 |
+
<button class="pb-btn" id="pb-prev" aria-label="Previous">
|
| 129 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
|
| 130 |
+
</button>
|
| 131 |
+
<button class="pb-play" id="pb-play" aria-label="Play">
|
| 132 |
+
<svg id="pb-play-svg" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
| 133 |
+
</button>
|
| 134 |
+
<button class="pb-btn" id="pb-next" aria-label="Next">
|
| 135 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zm10-12v12h2V6z"/></svg>
|
| 136 |
+
</button>
|
| 137 |
+
<button class="pb-btn" id="pb-loop" aria-label="Loop">
|
| 138 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
|
| 139 |
+
</button>
|
| 140 |
+
</div>
|
| 141 |
+
<div class="pb-progress">
|
| 142 |
+
<span class="pb-time" id="pb-cur">0:00</span>
|
| 143 |
+
<div class="pb-bar" id="pb-seek">
|
| 144 |
+
<div class="pb-bar-fill" id="pb-seek-fill"></div>
|
| 145 |
+
<div class="pb-bar-knob" id="pb-seek-knob"></div>
|
| 146 |
+
</div>
|
| 147 |
+
<span class="pb-time" id="pb-dur">0:00</span>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
|
| 151 |
+
<div class="pb-right">
|
| 152 |
+
<button class="pb-btn pb-mobile-play" id="pb-play-mobile" aria-label="Play">
|
| 153 |
+
<svg id="pb-play-mobile-svg" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
| 154 |
+
</button>
|
| 155 |
+
<button class="pb-btn" id="pb-mute" aria-label="Mute">
|
| 156 |
+
<svg id="pb-vol-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/></svg>
|
| 157 |
+
</button>
|
| 158 |
+
<div class="pb-vol" id="pb-vol">
|
| 159 |
+
<div class="pb-bar-fill" id="pb-vol-fill"></div>
|
| 160 |
+
<div class="pb-bar-knob" id="pb-vol-knob"></div>
|
| 161 |
+
</div>
|
| 162 |
+
</div>
|
| 163 |
+
|
| 164 |
+
<audio id="pb-audio" preload="metadata"></audio>
|
| 165 |
+
</div>
|
| 166 |
+
|
| 167 |
+
<div class="np-modal" id="np-modal" hidden>
|
| 168 |
+
<div class="np-bg" id="np-bg"></div>
|
| 169 |
+
<div class="np-inner">
|
| 170 |
+
<div class="np-header">
|
| 171 |
+
<button class="np-icon-btn np-back-btn" id="np-close" aria-label="Back">
|
| 172 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
|
| 173 |
+
<span>Back</span>
|
| 174 |
+
</button>
|
| 175 |
+
<div class="np-source">
|
| 176 |
+
<div class="np-eyebrow">Playing from</div>
|
| 177 |
+
<a class="np-source-name" id="np-source-name" href="#">—</a>
|
| 178 |
+
</div>
|
| 179 |
+
<div style="width:36px"></div>
|
| 180 |
+
</div>
|
| 181 |
+
|
| 182 |
+
<div class="np-stage" id="np-stage">
|
| 183 |
+
<span class="np-cover-fallback" id="np-cover-fallback" aria-hidden="true">
|
| 184 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
|
| 185 |
+
</span>
|
| 186 |
+
<img class="np-cover" id="np-cover" alt="" referrerpolicy="no-referrer">
|
| 187 |
+
<video class="np-video" id="np-video" playsinline preload="metadata" hidden></video>
|
| 188 |
+
<div class="np-loading" id="np-loading" hidden><div class="np-spinner"></div></div>
|
| 189 |
+
<div class="np-lyrics" id="np-lyrics" hidden>
|
| 190 |
+
<div class="np-lyrics-inner">
|
| 191 |
+
<div class="np-lyrics-head">
|
| 192 |
+
<h3>Lyrics</h3>
|
| 193 |
+
<button class="np-panel-close" id="np-lyrics-close" aria-label="Close lyrics">
|
| 194 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
| 195 |
+
</button>
|
| 196 |
+
</div>
|
| 197 |
+
<p class="np-lyrics-body" id="np-lyrics-body">Pick a song to load lyrics.</p>
|
| 198 |
+
<div class="np-lyrics-meta" id="np-lyrics-meta"></div>
|
| 199 |
+
</div>
|
| 200 |
+
</div>
|
| 201 |
+
<div class="np-queue" id="np-queue" hidden>
|
| 202 |
+
<div class="np-queue-head">
|
| 203 |
+
<h3>Queue</h3>
|
| 204 |
+
<span id="np-queue-count">0 tracks</span>
|
| 205 |
+
<button class="np-panel-close" id="np-queue-close" aria-label="Close queue">
|
| 206 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
| 207 |
+
</button>
|
| 208 |
+
</div>
|
| 209 |
+
<div class="np-queue-list" id="np-queue-list"></div>
|
| 210 |
+
</div>
|
| 211 |
+
</div>
|
| 212 |
+
|
| 213 |
+
<div class="np-info">
|
| 214 |
+
<div class="np-info-text">
|
| 215 |
+
<a class="np-title" id="np-title" href="#">—</a>
|
| 216 |
+
<a class="np-artist" id="np-artist" href="#">—</a>
|
| 217 |
+
</div>
|
| 218 |
+
<button class="np-like" id="np-like" aria-label="Like">
|
| 219 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
| 220 |
+
</button>
|
| 221 |
+
</div>
|
| 222 |
+
|
| 223 |
+
<div class="np-progress">
|
| 224 |
+
<div class="np-bar" id="np-seek">
|
| 225 |
+
<div class="np-bar-fill" id="np-seek-fill"></div>
|
| 226 |
+
<div class="np-bar-knob" id="np-seek-knob"></div>
|
| 227 |
+
</div>
|
| 228 |
+
<div class="np-times">
|
| 229 |
+
<span id="np-cur">0:00</span>
|
| 230 |
+
<span id="np-dur">0:00</span>
|
| 231 |
+
</div>
|
| 232 |
+
</div>
|
| 233 |
+
|
| 234 |
+
<div class="np-controls">
|
| 235 |
+
<button class="np-ctl" id="np-shuffle" aria-label="Shuffle">
|
| 236 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>
|
| 237 |
+
</button>
|
| 238 |
+
<button class="np-ctl" id="np-prev" aria-label="Previous">
|
| 239 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
|
| 240 |
+
</button>
|
| 241 |
+
<button class="np-play-big" id="np-play" aria-label="Play">
|
| 242 |
+
<svg id="np-play-svg" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
| 243 |
+
</button>
|
| 244 |
+
<button class="np-ctl" id="np-next" aria-label="Next">
|
| 245 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zm10-12v12h2V6z"/></svg>
|
| 246 |
+
</button>
|
| 247 |
+
<button class="np-ctl" id="np-loop" aria-label="Loop">
|
| 248 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
|
| 249 |
+
</button>
|
| 250 |
+
</div>
|
| 251 |
+
|
| 252 |
+
<div class="np-tools">
|
| 253 |
+
<button class="np-tool" id="np-video-btn" aria-label="Toggle video">
|
| 254 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg>
|
| 255 |
+
<span>Video</span>
|
| 256 |
+
</button>
|
| 257 |
+
<button class="np-tool" id="np-lyrics-btn" aria-label="Toggle lyrics">
|
| 258 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 10h12M4 14h16M4 18h8"/></svg>
|
| 259 |
+
<span>Lyrics</span>
|
| 260 |
+
</button>
|
| 261 |
+
<button class="np-tool" id="np-queue-btn" aria-label="Toggle queue">
|
| 262 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
|
| 263 |
+
<span>Queue</span>
|
| 264 |
+
</button>
|
| 265 |
+
</div>
|
| 266 |
+
</div>
|
| 267 |
+
</div>
|
| 268 |
+
|
| 269 |
+
<div id="toast-container"></div>
|
| 270 |
+
|
| 271 |
+
<script src="/music-api.js"></script>
|
| 272 |
+
<script src="/music.js"></script>
|
| 273 |
+
</body>
|
| 274 |
+
</html>
|
frontend/public/music.js
ADDED
|
@@ -0,0 +1,1729 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
(function () {
|
| 2 |
+
const $ = (s, r) => (r || document).querySelector(s);
|
| 3 |
+
const $$ = (s, r) => Array.from((r || document).querySelectorAll(s));
|
| 4 |
+
|
| 5 |
+
const audio = $('#pb-audio');
|
| 6 |
+
const video = $('#np-video');
|
| 7 |
+
const view = $('#ms-view');
|
| 8 |
+
const playerBar = $('#player-bar');
|
| 9 |
+
const npModal = $('#np-modal');
|
| 10 |
+
|
| 11 |
+
let mediaMode = 'audio';
|
| 12 |
+
function activeMedia() { return mediaMode === 'video' ? video : audio; }
|
| 13 |
+
|
| 14 |
+
const STORE = {
|
| 15 |
+
liked: 'kv_music_liked',
|
| 16 |
+
history: 'kv_music_history',
|
| 17 |
+
vol: 'kv_music_vol',
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
const state = {
|
| 21 |
+
queue: [],
|
| 22 |
+
index: -1,
|
| 23 |
+
liked: new Set(JSON.parse(localStorage.getItem(STORE.liked) || '[]')),
|
| 24 |
+
history: JSON.parse(localStorage.getItem(STORE.history) || '[]'),
|
| 25 |
+
volume: parseFloat(localStorage.getItem(STORE.vol) || '0.8'),
|
| 26 |
+
muted: false,
|
| 27 |
+
loop: false,
|
| 28 |
+
shuffle: false,
|
| 29 |
+
navStack: [],
|
| 30 |
+
navFwd: [],
|
| 31 |
+
scrubbing: false,
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
+
const api = {
|
| 35 |
+
search: (q) => fetchAPI(`/themes/search?q=${encodeURIComponent(q)}`),
|
| 36 |
+
anime: (id) => fetchAPI(`/themes/anime/${encodeURIComponent(id)}`),
|
| 37 |
+
artist: (slug) => fetchAPI(`/themes/artist/${encodeURIComponent(slug)}`),
|
| 38 |
+
recent: () => fetchAPI('/themes/recent'),
|
| 39 |
+
year: (y) => fetchAPI(`/themes/year/${encodeURIComponent(y)}`),
|
| 40 |
+
artists: () => fetchAPI('/themes/top-artists'),
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
const esc = (s) => String(s == null ? '' : s).replace(/[<>&"']/g, c => ({
|
| 44 |
+
'<': '<', '>': '>', '&': '&', '"': '"', "'": ''',
|
| 45 |
+
}[c]));
|
| 46 |
+
|
| 47 |
+
const fmtTime = (sec) => {
|
| 48 |
+
if (!isFinite(sec) || sec < 0) sec = 0;
|
| 49 |
+
const m = Math.floor(sec / 60);
|
| 50 |
+
const s = Math.floor(sec % 60);
|
| 51 |
+
return `${m}:${s < 10 ? '0' : ''}${s}`;
|
| 52 |
+
};
|
| 53 |
+
|
| 54 |
+
const placeholderCover = (label) => {
|
| 55 |
+
const t = (label || '?').slice(0, 1).toUpperCase();
|
| 56 |
+
return `<div class="ms-card-cover-fallback"><span style="font-size:2.4rem;font-weight:900;letter-spacing:-0.04em;">${esc(t)}</span></div>`;
|
| 57 |
+
};
|
| 58 |
+
|
| 59 |
+
function coverImg(url, label, opts) {
|
| 60 |
+
opts = opts || {};
|
| 61 |
+
const cls = opts.cls || 'ms-card-cover';
|
| 62 |
+
if (!url) return placeholderCover(label || '');
|
| 63 |
+
return `<img class="${cls}" data-cover-fallback="${esc(label || '')}"`
|
| 64 |
+
+ ` src="${esc(url)}" alt="" loading="lazy" referrerpolicy="no-referrer">`;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
document.addEventListener('error', (e) => {
|
| 68 |
+
const img = e.target;
|
| 69 |
+
if (!(img instanceof HTMLImageElement)) return;
|
| 70 |
+
if (!img.dataset || img.dataset.coverFallback == null) return;
|
| 71 |
+
const label = img.dataset.coverFallback;
|
| 72 |
+
const wrap = img.parentNode;
|
| 73 |
+
if (!wrap) return;
|
| 74 |
+
|
| 75 |
+
const tmp = document.createElement('div');
|
| 76 |
+
tmp.innerHTML = placeholderCover(label);
|
| 77 |
+
const node = tmp.firstChild;
|
| 78 |
+
if (node) wrap.replaceChild(node, img);
|
| 79 |
+
}, true);
|
| 80 |
+
|
| 81 |
+
const coverUrl = (anime) => {
|
| 82 |
+
if (!anime) return '';
|
| 83 |
+
const imgs = anime.images || [];
|
| 84 |
+
const large = imgs.find(i => i.facet === 'Large Cover');
|
| 85 |
+
const small = imgs.find(i => i.facet === 'Small Cover');
|
| 86 |
+
return (large || small || imgs[0])?.link || '';
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
const artistImageUrl = (artist) => {
|
| 90 |
+
if (!artist) return '';
|
| 91 |
+
const imgs = artist.images || [];
|
| 92 |
+
const large = imgs.find(i => i.facet === 'Large Cover');
|
| 93 |
+
const small = imgs.find(i => i.facet === 'Small Cover');
|
| 94 |
+
return (large || small || imgs[0])?.link || '';
|
| 95 |
+
};
|
| 96 |
+
|
| 97 |
+
function trackFromTheme(anime, theme, animeOverride) {
|
| 98 |
+
const animeRef = animeOverride || theme.anime || anime;
|
| 99 |
+
if (!animeRef) return null;
|
| 100 |
+
const entries = theme.animethemeentries || [];
|
| 101 |
+
let pickedAudio = null, pickedVideo = null, pickedEntry = null;
|
| 102 |
+
for (const e of entries) {
|
| 103 |
+
for (const v of (e.videos || [])) {
|
| 104 |
+
if (v.audio && v.audio.link) {
|
| 105 |
+
pickedAudio = v.audio;
|
| 106 |
+
pickedVideo = v;
|
| 107 |
+
pickedEntry = e;
|
| 108 |
+
break;
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
if (pickedAudio) break;
|
| 112 |
+
}
|
| 113 |
+
if (!pickedAudio) return null;
|
| 114 |
+
const cover = coverUrl(animeRef) || (theme.song?.artists?.[0] && artistImageUrl(theme.song.artists[0])) || '';
|
| 115 |
+
const slug = theme.slug || `${theme.type || ''}${theme.sequence || ''}`;
|
| 116 |
+
return {
|
| 117 |
+
id: `${theme.id}-${pickedAudio.id}`,
|
| 118 |
+
themeId: theme.id,
|
| 119 |
+
title: theme.song?.title || slug || 'Untitled',
|
| 120 |
+
type: slug,
|
| 121 |
+
typeKind: theme.type || '',
|
| 122 |
+
artists: theme.song?.artists || [],
|
| 123 |
+
animeId: animeRef.id,
|
| 124 |
+
animeName: animeRef.name,
|
| 125 |
+
animeSlug: animeRef.slug,
|
| 126 |
+
cover,
|
| 127 |
+
audio: pickedAudio.link,
|
| 128 |
+
videoLink: pickedVideo?.link,
|
| 129 |
+
episodes: pickedEntry?.episodes || '',
|
| 130 |
+
};
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
function tracksFromAnime(anime) {
|
| 134 |
+
return (anime.animethemes || [])
|
| 135 |
+
.map(t => trackFromTheme(anime, t))
|
| 136 |
+
.filter(Boolean);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
function persistLiked() { localStorage.setItem(STORE.liked, JSON.stringify(Array.from(state.liked))); }
|
| 140 |
+
function persistHistory() { localStorage.setItem(STORE.history, JSON.stringify(state.history.slice(0, 60))); }
|
| 141 |
+
function persistVol() { localStorage.setItem(STORE.vol, String(state.volume)); }
|
| 142 |
+
|
| 143 |
+
function isLiked(track) { return state.liked.has(track.id); }
|
| 144 |
+
function toggleLike(track) {
|
| 145 |
+
if (state.liked.has(track.id)) {
|
| 146 |
+
state.liked.delete(track.id);
|
| 147 |
+
try { localStorage.removeItem(`kv_music_track_${track.id}`); } catch (_) { }
|
| 148 |
+
} else {
|
| 149 |
+
state.liked.add(track.id);
|
| 150 |
+
try { localStorage.setItem(`kv_music_track_${track.id}`, JSON.stringify(track)); } catch (_) { }
|
| 151 |
+
}
|
| 152 |
+
persistLiked();
|
| 153 |
+
updatePlayerLike();
|
| 154 |
+
$$(`.ms-track-like[data-tid="${CSS.escape(track.id)}"]`).forEach(b => {
|
| 155 |
+
b.classList.toggle('is-liked', state.liked.has(track.id));
|
| 156 |
+
});
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
function pushHistory(track) {
|
| 160 |
+
try { localStorage.setItem(`kv_music_track_${track.id}`, JSON.stringify(track)); } catch (_) { }
|
| 161 |
+
state.history = [track.id, ...state.history.filter(id => id !== track.id)].slice(0, 60);
|
| 162 |
+
persistHistory();
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
function loadStoredTracks(ids) {
|
| 166 |
+
const out = [];
|
| 167 |
+
for (const id of ids) {
|
| 168 |
+
try {
|
| 169 |
+
const raw = localStorage.getItem(`kv_music_track_${id}`);
|
| 170 |
+
if (raw) out.push(JSON.parse(raw));
|
| 171 |
+
} catch (_) { }
|
| 172 |
+
}
|
| 173 |
+
return out;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
function play(queue, idx) {
|
| 177 |
+
if (!queue || !queue.length) return;
|
| 178 |
+
state.queue = queue.slice();
|
| 179 |
+
state.index = Math.max(0, Math.min(idx || 0, state.queue.length - 1));
|
| 180 |
+
loadCurrent(true);
|
| 181 |
+
renderQueue();
|
| 182 |
+
|
| 183 |
+
if (window.innerWidth <= 720) openNP();
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
function jumpTo(idx) {
|
| 187 |
+
if (idx < 0 || idx >= state.queue.length) return;
|
| 188 |
+
state.index = idx;
|
| 189 |
+
loadCurrent(true);
|
| 190 |
+
renderQueue();
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
function renderQueue() {
|
| 194 |
+
const list = $('#np-queue-list');
|
| 195 |
+
const count = $('#np-queue-count');
|
| 196 |
+
if (!list || !count) return;
|
| 197 |
+
const q = state.queue;
|
| 198 |
+
count.textContent = q.length ? `${q.length} track${q.length > 1 ? 's' : ''}` : 'Empty';
|
| 199 |
+
if (!q.length) {
|
| 200 |
+
list.innerHTML = `<div class="np-queue-empty">Pick a song to start playing.</div>`;
|
| 201 |
+
return;
|
| 202 |
+
}
|
| 203 |
+
list.innerHTML = q.map((t, i) => `
|
| 204 |
+
<button class="np-queue-row${i === state.index ? ' is-current' : ''}" data-qjump="${i}">
|
| 205 |
+
<span class="np-queue-cover">
|
| 206 |
+
${t.cover ? `<img src="${esc(t.cover)}" alt="" referrerpolicy="no-referrer">` : ''}
|
| 207 |
+
${i === state.index ? '<span class="np-queue-eq"><i></i><i></i><i></i></span>' : ''}
|
| 208 |
+
</span>
|
| 209 |
+
<span class="np-queue-text">
|
| 210 |
+
<span class="np-queue-title">${esc(t.title)}</span>
|
| 211 |
+
<span class="np-queue-sub">${esc((t.artists || []).map(a => a.name).join(', ') || t.animeName || '')}</span>
|
| 212 |
+
</span>
|
| 213 |
+
<span class="np-queue-tag">${i < state.index ? 'Played' : (i === state.index ? 'Now' : 'Next')}</span>
|
| 214 |
+
</button>
|
| 215 |
+
`).join('');
|
| 216 |
+
$$('[data-qjump]', list).forEach(btn => {
|
| 217 |
+
btn.addEventListener('click', () => jumpTo(parseInt(btn.dataset.qjump, 10)));
|
| 218 |
+
});
|
| 219 |
+
|
| 220 |
+
const cur = list.querySelector('.is-current');
|
| 221 |
+
if (cur) cur.scrollIntoView({ block: 'nearest' });
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
function loadCurrent(autoplay) {
|
| 225 |
+
const t = state.queue[state.index];
|
| 226 |
+
if (!t) return;
|
| 227 |
+
playerBar.dataset.empty = '0';
|
| 228 |
+
|
| 229 |
+
document.body.classList.remove('pb-hidden');
|
| 230 |
+
|
| 231 |
+
if (mediaMode === 'video' && !t.videoLink) {
|
| 232 |
+
setVideoMode(false, false);
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
if (mediaMode === 'video') {
|
| 236 |
+
video.src = t.videoLink;
|
| 237 |
+
try { audio.removeAttribute('src'); audio.load(); } catch (_) { }
|
| 238 |
+
video.muted = state.muted;
|
| 239 |
+
video.volume = state.volume;
|
| 240 |
+
} else {
|
| 241 |
+
audio.src = t.audio;
|
| 242 |
+
try { video.removeAttribute('src'); video.load(); } catch (_) { }
|
| 243 |
+
audio.muted = state.muted;
|
| 244 |
+
audio.volume = state.volume;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
if (autoplay) {
|
| 248 |
+
const p = activeMedia().play();
|
| 249 |
+
if (p && p.catch) p.catch(() => { });
|
| 250 |
+
}
|
| 251 |
+
renderPlayerMeta(t);
|
| 252 |
+
pushHistory(t);
|
| 253 |
+
updateNowPlayingMarkers();
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
function setVideoMode(on, reload = true) {
|
| 257 |
+
const t = state.queue[state.index];
|
| 258 |
+
if (on && (!t || !t.videoLink)) {
|
| 259 |
+
toast && toast('No video available for this track');
|
| 260 |
+
return;
|
| 261 |
+
}
|
| 262 |
+
const wasPaused = activeMedia().paused;
|
| 263 |
+
const ct = activeMedia().currentTime || 0;
|
| 264 |
+
activeMedia().pause();
|
| 265 |
+
|
| 266 |
+
mediaMode = on ? 'video' : 'audio';
|
| 267 |
+
document.body.classList.toggle('np-video-on', mediaMode === 'video');
|
| 268 |
+
$('#np-video-btn').classList.toggle('is-on', mediaMode === 'video');
|
| 269 |
+
const npVid = $('#np-video');
|
| 270 |
+
const npCov = $('#np-cover');
|
| 271 |
+
if (npVid) npVid.hidden = mediaMode !== 'video';
|
| 272 |
+
if (npCov) npCov.hidden = mediaMode === 'video';
|
| 273 |
+
|
| 274 |
+
if (!t) return;
|
| 275 |
+
|
| 276 |
+
if (reload) {
|
| 277 |
+
if (mediaMode === 'video') {
|
| 278 |
+
video.src = t.videoLink;
|
| 279 |
+
try { audio.removeAttribute('src'); audio.load(); } catch (_) { }
|
| 280 |
+
} else {
|
| 281 |
+
audio.src = t.audio;
|
| 282 |
+
try { video.removeAttribute('src'); video.load(); } catch (_) { }
|
| 283 |
+
}
|
| 284 |
+
const m = activeMedia();
|
| 285 |
+
m.muted = state.muted;
|
| 286 |
+
m.volume = state.volume;
|
| 287 |
+
const onMeta = () => {
|
| 288 |
+
try { m.currentTime = ct; } catch (_) { }
|
| 289 |
+
if (!wasPaused) m.play().catch(() => { });
|
| 290 |
+
m.removeEventListener('loadedmetadata', onMeta);
|
| 291 |
+
};
|
| 292 |
+
m.addEventListener('loadedmetadata', onMeta);
|
| 293 |
+
}
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
function toast(msg) {
|
| 297 |
+
const c = $('#toast-container');
|
| 298 |
+
if (!c) return;
|
| 299 |
+
const el = document.createElement('div');
|
| 300 |
+
el.className = 'kv-toast';
|
| 301 |
+
el.textContent = msg;
|
| 302 |
+
c.appendChild(el);
|
| 303 |
+
setTimeout(() => el.classList.add('show'), 10);
|
| 304 |
+
setTimeout(() => { el.classList.remove('show'); setTimeout(() => el.remove(), 300); }, 2200);
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
function next(auto) {
|
| 308 |
+
if (!state.queue.length) return;
|
| 309 |
+
if (state.shuffle && state.queue.length > 1) {
|
| 310 |
+
let n;
|
| 311 |
+
do { n = Math.floor(Math.random() * state.queue.length); } while (n === state.index);
|
| 312 |
+
state.index = n;
|
| 313 |
+
} else {
|
| 314 |
+
state.index = state.index + 1;
|
| 315 |
+
if (state.index >= state.queue.length) {
|
| 316 |
+
if (state.loop || !auto) state.index = 0;
|
| 317 |
+
else { state.index = state.queue.length - 1; activeMedia().pause(); return; }
|
| 318 |
+
}
|
| 319 |
+
}
|
| 320 |
+
loadCurrent(true);
|
| 321 |
+
renderQueue();
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
function prev() {
|
| 325 |
+
if (!state.queue.length) return;
|
| 326 |
+
if (activeMedia().currentTime > 3) { activeMedia().currentTime = 0; return; }
|
| 327 |
+
state.index = (state.index - 1 + state.queue.length) % state.queue.length;
|
| 328 |
+
loadCurrent(true);
|
| 329 |
+
renderQueue();
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
function togglePlay() {
|
| 333 |
+
if (playerBar.dataset.empty === '1') return;
|
| 334 |
+
const m = activeMedia();
|
| 335 |
+
if (m.paused) m.play().catch(() => { });
|
| 336 |
+
else m.pause();
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
function setCoverImg(imgEl, btnEl, url) {
|
| 340 |
+
if (!imgEl) return;
|
| 341 |
+
if (url) {
|
| 342 |
+
imgEl.classList.remove('is-failed');
|
| 343 |
+
if (imgEl.getAttribute('src') !== url) imgEl.src = url;
|
| 344 |
+
if (btnEl) btnEl.classList.add('has-cover');
|
| 345 |
+
} else {
|
| 346 |
+
imgEl.removeAttribute('src');
|
| 347 |
+
imgEl.classList.add('is-failed');
|
| 348 |
+
if (btnEl) btnEl.classList.remove('has-cover');
|
| 349 |
+
}
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
function renderPlayerMeta(t) {
|
| 353 |
+
const pbCover = $('#pb-cover');
|
| 354 |
+
const pbCoverBtn = $('#pb-expand');
|
| 355 |
+
setCoverImg(pbCover, pbCoverBtn, t.cover || '');
|
| 356 |
+
const titleEl = $('#pb-title');
|
| 357 |
+
titleEl.textContent = t.title;
|
| 358 |
+
titleEl.dataset.animeId = t.animeId || '';
|
| 359 |
+
const artistEl = $('#pb-artist');
|
| 360 |
+
const artistsLabel = (t.artists && t.artists.length)
|
| 361 |
+
? t.artists.map(a => a.name).join(', ')
|
| 362 |
+
: (t.animeName || '—');
|
| 363 |
+
if (t.artists && t.artists.length) {
|
| 364 |
+
const first = t.artists[0];
|
| 365 |
+
artistEl.textContent = artistsLabel;
|
| 366 |
+
artistEl.href = `#/artist/${encodeURIComponent(first.slug || first.id)}`;
|
| 367 |
+
} else {
|
| 368 |
+
artistEl.textContent = artistsLabel;
|
| 369 |
+
artistEl.href = `#/anime/${encodeURIComponent(t.animeId)}`;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
const npCover = $('#np-cover');
|
| 373 |
+
const npTitle = $('#np-title');
|
| 374 |
+
const npArtist = $('#np-artist');
|
| 375 |
+
const npSource = $('#np-source-name');
|
| 376 |
+
const npBg = $('#np-bg');
|
| 377 |
+
setCoverImg(npCover, null, t.cover || '');
|
| 378 |
+
if (npTitle) { npTitle.textContent = t.title; npTitle.href = `#/anime/${encodeURIComponent(t.animeId)}`; }
|
| 379 |
+
if (npArtist) {
|
| 380 |
+
npArtist.textContent = artistsLabel;
|
| 381 |
+
const first = (t.artists || [])[0];
|
| 382 |
+
npArtist.href = first ? `#/artist/${encodeURIComponent(first.slug || first.id)}` : `#/anime/${encodeURIComponent(t.animeId)}`;
|
| 383 |
+
}
|
| 384 |
+
if (npSource) { npSource.textContent = t.animeName || '—'; npSource.href = `#/anime/${encodeURIComponent(t.animeId)}`; }
|
| 385 |
+
if (npBg && t.cover) npBg.style.backgroundImage = `url("${t.cover}")`;
|
| 386 |
+
|
| 387 |
+
const vidBtn = $('#np-video-btn');
|
| 388 |
+
if (vidBtn) vidBtn.classList.toggle('is-disabled', !t.videoLink);
|
| 389 |
+
|
| 390 |
+
const lyricsMeta = $('#np-lyrics-meta');
|
| 391 |
+
if (lyricsMeta) {
|
| 392 |
+
lyricsMeta.innerHTML = `
|
| 393 |
+
<div><span>Anime</span><strong>${esc(t.animeName || '—')}</strong></div>
|
| 394 |
+
<div><span>Type</span><strong>${esc(t.type || '—')}</strong></div>
|
| 395 |
+
<div><span>Episodes</span><strong>${esc(t.episodes || '—')}</strong></div>
|
| 396 |
+
<div><span>Artists</span><strong>${esc((t.artists || []).map(a => a.name).join(', ') || '—')}</strong></div>
|
| 397 |
+
`;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
_syncedLines = [];
|
| 401 |
+
_activeLyricIdx = -1;
|
| 402 |
+
const lyricsBody = $('#np-lyrics-body');
|
| 403 |
+
if (lyricsBody) {
|
| 404 |
+
lyricsBody.dataset.trackId = t.id;
|
| 405 |
+
lyricsBody.innerHTML = '<span class="np-lyric-line is-loading">Loading lyrics…</span>';
|
| 406 |
+
}
|
| 407 |
+
fetchLyricsFor(t);
|
| 408 |
+
|
| 409 |
+
updatePlayerLike();
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
let _lyricsCache = {};
|
| 413 |
+
let _syncedLines = [];
|
| 414 |
+
let _rawSyncedLines = [];
|
| 415 |
+
let _lyricsDuration = 0;
|
| 416 |
+
let _activeLyricIdx = -1;
|
| 417 |
+
let _lyricRaf = null;
|
| 418 |
+
|
| 419 |
+
function parseLRC(lrc) {
|
| 420 |
+
const out = [];
|
| 421 |
+
const lineRe = /^((?:\[\d+:\d+(?:\.\d+)?\])+)(.*)$/;
|
| 422 |
+
const tagRe = /\[(\d+):(\d+(?:\.\d+)?)\]/g;
|
| 423 |
+
for (const raw of String(lrc).split(/\r?\n/)) {
|
| 424 |
+
const m = raw.match(lineRe);
|
| 425 |
+
if (!m) continue;
|
| 426 |
+
const text = m[2].trim();
|
| 427 |
+
let tag;
|
| 428 |
+
while ((tag = tagRe.exec(m[1])) !== null) {
|
| 429 |
+
const t = parseInt(tag[1], 10) * 60 + parseFloat(tag[2]);
|
| 430 |
+
out.push({ t, text });
|
| 431 |
+
}
|
| 432 |
+
}
|
| 433 |
+
out.sort((a, b) => a.t - b.t);
|
| 434 |
+
return out;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
function renderPlainLyrics(text) {
|
| 438 |
+
const body = $('#np-lyrics-body');
|
| 439 |
+
if (!body) return;
|
| 440 |
+
if (!text || !text.trim()) {
|
| 441 |
+
body.innerHTML = '<span class="np-lyric-line is-empty">We couldn\'t find lyrics for this track yet.</span>';
|
| 442 |
+
return;
|
| 443 |
+
}
|
| 444 |
+
const lines = text.split(/\r?\n/);
|
| 445 |
+
body.innerHTML = lines
|
| 446 |
+
.map(l => `<div class="np-lyric-line">${esc(l) || ' '}</div>`)
|
| 447 |
+
.join('');
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
function renderSyncedLyrics(lines) {
|
| 451 |
+
const body = $('#np-lyrics-body');
|
| 452 |
+
if (!body) return;
|
| 453 |
+
|
| 454 |
+
body.style.transform = '';
|
| 455 |
+
body.scrollTop = 0;
|
| 456 |
+
body.innerHTML = lines
|
| 457 |
+
.map((l, i) => `<div class="np-lyric-line" data-i="${i}">${esc(l.text) || ' '}</div>`)
|
| 458 |
+
.join('');
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
function recomputeSyncedLines() {
|
| 462 |
+
if (!_rawSyncedLines.length) { _syncedLines = []; return; }
|
| 463 |
+
_syncedLines = _rawSyncedLines.slice();
|
| 464 |
+
_activeLyricIdx = -1;
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
function updateLyricsHighlight(currentTime) {
|
| 468 |
+
if (!_syncedLines.length) return;
|
| 469 |
+
|
| 470 |
+
let lo = 0, hi = _syncedLines.length - 1, idx = -1;
|
| 471 |
+
while (lo <= hi) {
|
| 472 |
+
const mid = (lo + hi) >> 1;
|
| 473 |
+
if (_syncedLines[mid].t <= currentTime) { idx = mid; lo = mid + 1; }
|
| 474 |
+
else hi = mid - 1;
|
| 475 |
+
}
|
| 476 |
+
if (idx === _activeLyricIdx) return;
|
| 477 |
+
_activeLyricIdx = idx;
|
| 478 |
+
const body = $('#np-lyrics-body');
|
| 479 |
+
if (!body) return;
|
| 480 |
+
const lines = body.querySelectorAll('.np-lyric-line');
|
| 481 |
+
lines.forEach((el, i) => {
|
| 482 |
+
el.classList.toggle('is-active', i === idx);
|
| 483 |
+
el.classList.toggle('is-past', i < idx);
|
| 484 |
+
});
|
| 485 |
+
if (idx < 0) return;
|
| 486 |
+
const active = lines[idx];
|
| 487 |
+
if (!active) return;
|
| 488 |
+
|
| 489 |
+
const viewH = body.clientHeight;
|
| 490 |
+
if (viewH <= 0) return;
|
| 491 |
+
|
| 492 |
+
const ACTIVE_RATIO = 0.42;
|
| 493 |
+
const target = active.offsetTop + (active.offsetHeight / 2) - (viewH * ACTIVE_RATIO);
|
| 494 |
+
scrollLyricsTo(body, Math.max(0, target));
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
let _lyricScrollRaf = 0;
|
| 498 |
+
function scrollLyricsTo(body, target) {
|
| 499 |
+
if (!body) return;
|
| 500 |
+
target = Math.max(0, Math.min(target, body.scrollHeight - body.clientHeight));
|
| 501 |
+
if (_lyricScrollRaf) cancelAnimationFrame(_lyricScrollRaf);
|
| 502 |
+
const start = body.scrollTop;
|
| 503 |
+
const delta = target - start;
|
| 504 |
+
if (Math.abs(delta) < 0.5) { body.scrollTop = target; return; }
|
| 505 |
+
|
| 506 |
+
const dist = Math.abs(delta);
|
| 507 |
+
const dur = Math.min(900, Math.max(280, dist * 0.9));
|
| 508 |
+
const t0 = performance.now();
|
| 509 |
+
const ease = (x) => 1 - Math.pow(1 - x, 3);
|
| 510 |
+
const step = (now) => {
|
| 511 |
+
const k = Math.min(1, (now - t0) / dur);
|
| 512 |
+
body.scrollTop = start + delta * ease(k);
|
| 513 |
+
if (k < 1) _lyricScrollRaf = requestAnimationFrame(step);
|
| 514 |
+
else _lyricScrollRaf = 0;
|
| 515 |
+
};
|
| 516 |
+
_lyricScrollRaf = requestAnimationFrame(step);
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
function cleanLyricTitle(raw) {
|
| 520 |
+
if (!raw) return '';
|
| 521 |
+
let s = String(raw);
|
| 522 |
+
|
| 523 |
+
s = s.replace(/^\s*(?:OP|ED|IN)\s*\d*\s*[-–:]\s*/i, '');
|
| 524 |
+
s = s.replace(/^\s*(?:Opening|Ending|Insert\s*Song|Theme)\s*\d*\s*[-–:]\s*/i, '');
|
| 525 |
+
|
| 526 |
+
s = s.replace(/\([^)]*\)/g, '');
|
| 527 |
+
s = s.replace(/\[[^\]]*\]/g, '');
|
| 528 |
+
|
| 529 |
+
s = s.replace(/^[\s"'“”『「]+|[\s"'“”』」]+$/g, '');
|
| 530 |
+
return s.trim();
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
function waitForAudioDuration(m, timeoutMs) {
|
| 534 |
+
return new Promise((resolve) => {
|
| 535 |
+
if (!m) return resolve(0);
|
| 536 |
+
if (isFinite(m.duration) && m.duration > 0) return resolve(m.duration);
|
| 537 |
+
let done = false;
|
| 538 |
+
const finish = (v) => {
|
| 539 |
+
if (done) return;
|
| 540 |
+
done = true;
|
| 541 |
+
m.removeEventListener('loadedmetadata', onMeta);
|
| 542 |
+
m.removeEventListener('durationchange', onMeta);
|
| 543 |
+
resolve(v);
|
| 544 |
+
};
|
| 545 |
+
const onMeta = () => {
|
| 546 |
+
if (isFinite(m.duration) && m.duration > 0) finish(m.duration);
|
| 547 |
+
};
|
| 548 |
+
m.addEventListener('loadedmetadata', onMeta);
|
| 549 |
+
m.addEventListener('durationchange', onMeta);
|
| 550 |
+
setTimeout(() => finish(0), timeoutMs || 4000);
|
| 551 |
+
});
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
async function fetchLyricsFor(track) {
|
| 555 |
+
if (!track) return;
|
| 556 |
+
const lyricsBody = $('#np-lyrics-body');
|
| 557 |
+
if (!lyricsBody) return;
|
| 558 |
+
const key = track.id;
|
| 559 |
+
const apply = (data) => {
|
| 560 |
+
if (lyricsBody.dataset.trackId !== key) return;
|
| 561 |
+
const synced = data && data.synced;
|
| 562 |
+
const plain = data && data.lyrics;
|
| 563 |
+
_lyricsDuration = (data && Number(data.duration)) || 0;
|
| 564 |
+
if (synced) {
|
| 565 |
+
_rawSyncedLines = parseLRC(synced).filter(l => l.text || l.text === '');
|
| 566 |
+
if (_rawSyncedLines.length) {
|
| 567 |
+
recomputeSyncedLines();
|
| 568 |
+
renderSyncedLyrics(_syncedLines);
|
| 569 |
+
return;
|
| 570 |
+
}
|
| 571 |
+
}
|
| 572 |
+
_rawSyncedLines = [];
|
| 573 |
+
_syncedLines = [];
|
| 574 |
+
renderPlainLyrics(plain || (synced ? String(synced).replace(/^\[\d+:\d+(?:\.\d+)?\]\s?/gm, '') : ''));
|
| 575 |
+
};
|
| 576 |
+
|
| 577 |
+
const audioDur = await waitForAudioDuration(activeMedia(), 4000);
|
| 578 |
+
if (lyricsBody.dataset.trackId !== key) return;
|
| 579 |
+
|
| 580 |
+
const durBucket = audioDur ? Math.round(audioDur / 5) * 5 : 0;
|
| 581 |
+
const cacheKey = `${key}|${durBucket}`;
|
| 582 |
+
if (_lyricsCache[cacheKey] != null) { apply(_lyricsCache[cacheKey]); return; }
|
| 583 |
+
|
| 584 |
+
const title = cleanLyricTitle(track.title || '');
|
| 585 |
+
|
| 586 |
+
const firstArtist = ((track.artists || [])[0] || {}).name || '';
|
| 587 |
+
try {
|
| 588 |
+
const durQs = audioDur ? `&duration=${audioDur.toFixed(2)}` : '';
|
| 589 |
+
const url = `/api/lyrics?title=${encodeURIComponent(title)}&artist=${encodeURIComponent(firstArtist)}${durQs}`;
|
| 590 |
+
const res = await fetch(url);
|
| 591 |
+
const data = await res.json();
|
| 592 |
+
_lyricsCache[cacheKey] = data;
|
| 593 |
+
apply(data);
|
| 594 |
+
} catch (_) {
|
| 595 |
+
apply({});
|
| 596 |
+
}
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
function updatePlayerLike() {
|
| 600 |
+
const t = state.queue[state.index];
|
| 601 |
+
const btn = $('#pb-like');
|
| 602 |
+
const npBtn = $('#np-like');
|
| 603 |
+
const liked = !!(t && state.liked.has(t.id));
|
| 604 |
+
if (btn) btn.classList.toggle('is-liked', liked);
|
| 605 |
+
if (npBtn) npBtn.classList.toggle('is-liked', liked);
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
function updateNowPlayingMarkers() {
|
| 609 |
+
const t = state.queue[state.index];
|
| 610 |
+
$$('.ms-track').forEach(row => {
|
| 611 |
+
row.classList.toggle('is-playing', !!(t && row.dataset.tid === t.id));
|
| 612 |
+
});
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
function setPlayUI(playing) {
|
| 616 |
+
const pause = '<path d="M6 5h4v14H6zm8 0h4v14h-4z"/>';
|
| 617 |
+
const play = '<path d="M8 5v14l11-7z"/>';
|
| 618 |
+
const ic = playing ? pause : play;
|
| 619 |
+
const set = (id) => {
|
| 620 |
+
const el = $('#' + id);
|
| 621 |
+
if (el) el.outerHTML = `<svg id="${id}" viewBox="0 0 24 24" fill="currentColor">${ic}</svg>`;
|
| 622 |
+
};
|
| 623 |
+
set('pb-play-svg');
|
| 624 |
+
set('pb-play-mobile-svg');
|
| 625 |
+
set('np-play-svg');
|
| 626 |
+
|
| 627 |
+
document.body.classList.toggle('is-playing', !!playing);
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
function openNP() {
|
| 631 |
+
if (playerBar.dataset.empty === '1') return;
|
| 632 |
+
npModal.hidden = false;
|
| 633 |
+
document.body.classList.add('np-open');
|
| 634 |
+
|
| 635 |
+
$('#np-shuffle').classList.toggle('is-on', state.shuffle);
|
| 636 |
+
$('#np-loop').classList.toggle('is-on', state.loop);
|
| 637 |
+
$('#np-video-btn').classList.toggle('is-on', mediaMode === 'video');
|
| 638 |
+
setSeek();
|
| 639 |
+
updatePlayerLike();
|
| 640 |
+
}
|
| 641 |
+
function closeNP() {
|
| 642 |
+
npModal.hidden = true;
|
| 643 |
+
document.body.classList.remove('np-open');
|
| 644 |
+
document.body.classList.remove('np-lyrics-open');
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
function setVolUI() {
|
| 648 |
+
const v = state.muted ? 0 : state.volume;
|
| 649 |
+
$('#pb-vol-fill').style.width = (v * 100) + '%';
|
| 650 |
+
const knob = $('#pb-vol-knob');
|
| 651 |
+
if (knob) knob.style.left = (v * 100) + '%';
|
| 652 |
+
const svg = $('#pb-vol-svg');
|
| 653 |
+
if (!svg) return;
|
| 654 |
+
let path;
|
| 655 |
+
if (v === 0) path = '<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/>';
|
| 656 |
+
else if (v < 0.5) path = '<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>';
|
| 657 |
+
else path = '<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>';
|
| 658 |
+
svg.innerHTML = path;
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
function setSeek(t) {
|
| 662 |
+
const m = activeMedia();
|
| 663 |
+
const dur = m.duration;
|
| 664 |
+
const cur = t == null ? m.currentTime : t;
|
| 665 |
+
$('#pb-cur').textContent = fmtTime(cur);
|
| 666 |
+
$('#pb-dur').textContent = fmtTime(dur);
|
| 667 |
+
const pct = dur ? (cur / dur) * 100 : 0;
|
| 668 |
+
$('#pb-seek-fill').style.width = pct + '%';
|
| 669 |
+
const knob = $('#pb-seek-knob');
|
| 670 |
+
if (knob) knob.style.left = pct + '%';
|
| 671 |
+
|
| 672 |
+
const npCur = $('#np-cur'), npDur = $('#np-dur'), npFill = $('#np-seek-fill'), npKnob = $('#np-seek-knob');
|
| 673 |
+
if (npCur) npCur.textContent = fmtTime(cur);
|
| 674 |
+
if (npDur) npDur.textContent = fmtTime(dur);
|
| 675 |
+
if (npFill) npFill.style.width = pct + '%';
|
| 676 |
+
if (npKnob) npKnob.style.left = pct + '%';
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
function bindBar(bar, onChange) {
|
| 680 |
+
let pending = null;
|
| 681 |
+
let rafId = 0;
|
| 682 |
+
const flush = () => {
|
| 683 |
+
rafId = 0;
|
| 684 |
+
if (pending != null) { onChange(pending); pending = null; }
|
| 685 |
+
};
|
| 686 |
+
const schedule = (pct) => {
|
| 687 |
+
pending = pct;
|
| 688 |
+
if (!rafId) rafId = requestAnimationFrame(flush);
|
| 689 |
+
};
|
| 690 |
+
const setFromEvent = (e) => {
|
| 691 |
+
const r = bar.getBoundingClientRect();
|
| 692 |
+
const x = (e.touches ? e.touches[0].clientX : e.clientX) - r.left;
|
| 693 |
+
const pct = Math.max(0, Math.min(1, x / r.width));
|
| 694 |
+
schedule(pct);
|
| 695 |
+
};
|
| 696 |
+
let dragging = false;
|
| 697 |
+
const onUp = () => {
|
| 698 |
+
dragging = false;
|
| 699 |
+
state.scrubbing = false;
|
| 700 |
+
document.removeEventListener('mousemove', onMove);
|
| 701 |
+
document.removeEventListener('touchmove', onMove);
|
| 702 |
+
document.removeEventListener('mouseup', onUp);
|
| 703 |
+
document.removeEventListener('touchend', onUp);
|
| 704 |
+
if (rafId) { cancelAnimationFrame(rafId); rafId = 0; flush(); }
|
| 705 |
+
};
|
| 706 |
+
const onMove = (e) => { if (dragging) setFromEvent(e); };
|
| 707 |
+
bar.addEventListener('mousedown', (e) => {
|
| 708 |
+
dragging = true; state.scrubbing = true; setFromEvent(e);
|
| 709 |
+
document.addEventListener('mousemove', onMove);
|
| 710 |
+
document.addEventListener('mouseup', onUp);
|
| 711 |
+
});
|
| 712 |
+
bar.addEventListener('touchstart', (e) => {
|
| 713 |
+
dragging = true; state.scrubbing = true; setFromEvent(e);
|
| 714 |
+
document.addEventListener('touchmove', onMove, { passive: true });
|
| 715 |
+
document.addEventListener('touchend', onUp);
|
| 716 |
+
}, { passive: true });
|
| 717 |
+
bar.addEventListener('click', setFromEvent);
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
let _stageSpinnerTimer = 0;
|
| 721 |
+
function showStageLoading(on) {
|
| 722 |
+
const el = $('#np-loading');
|
| 723 |
+
if (!el) return;
|
| 724 |
+
if (on) {
|
| 725 |
+
if (_stageSpinnerTimer) return;
|
| 726 |
+
_stageSpinnerTimer = setTimeout(() => {
|
| 727 |
+
_stageSpinnerTimer = 0;
|
| 728 |
+
el.hidden = false;
|
| 729 |
+
}, 350);
|
| 730 |
+
} else {
|
| 731 |
+
if (_stageSpinnerTimer) { clearTimeout(_stageSpinnerTimer); _stageSpinnerTimer = 0; }
|
| 732 |
+
el.hidden = true;
|
| 733 |
+
}
|
| 734 |
+
}
|
| 735 |
+
|
| 736 |
+
function navigate(hash) {
|
| 737 |
+
if (location.hash !== hash) location.hash = hash;
|
| 738 |
+
else handleRoute();
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
function handleRoute() {
|
| 742 |
+
const hash = location.hash || '#/';
|
| 743 |
+
if (state.navStack[state.navStack.length - 1] !== hash) {
|
| 744 |
+
state.navStack.push(hash);
|
| 745 |
+
state.navFwd = [];
|
| 746 |
+
}
|
| 747 |
+
$('#ms-back').disabled = state.navStack.length <= 1;
|
| 748 |
+
$('#ms-fwd').disabled = state.navFwd.length === 0;
|
| 749 |
+
|
| 750 |
+
$$('.ms-link').forEach(l => {
|
| 751 |
+
l.classList.toggle('active', l.dataset.route === hash || (hash === '#/' && l.dataset.route === '#/'));
|
| 752 |
+
});
|
| 753 |
+
|
| 754 |
+
if (hash.startsWith('#/search?')) {
|
| 755 |
+
const q = decodeURIComponent(hash.split('q=')[1] || '');
|
| 756 |
+
$('#ms-search-input').value = q;
|
| 757 |
+
$('#ms-search-clear').hidden = !q;
|
| 758 |
+
renderSearch(q);
|
| 759 |
+
} else if (hash.startsWith('#/anime/')) {
|
| 760 |
+
renderAnime(decodeURIComponent(hash.split('/')[2] || ''));
|
| 761 |
+
} else if (hash.startsWith('#/artist/')) {
|
| 762 |
+
renderArtist(decodeURIComponent(hash.split('/')[2] || ''));
|
| 763 |
+
} else if (hash.startsWith('#/year/')) {
|
| 764 |
+
renderYear(hash.split('/')[2]);
|
| 765 |
+
} else if (hash === '#/recent') renderRecent();
|
| 766 |
+
else if (hash === '#/liked') renderLiked();
|
| 767 |
+
else if (hash === '#/history') renderHistory();
|
| 768 |
+
else if (hash === '#/artists') renderTopArtists();
|
| 769 |
+
else renderHome();
|
| 770 |
+
|
| 771 |
+
view.scrollTo({ top: 0 });
|
| 772 |
+
$('#music-main').scrollTo({ top: 0 });
|
| 773 |
+
closeSidebarIfMobile();
|
| 774 |
+
}
|
| 775 |
+
|
| 776 |
+
function loadingHTML(label) {
|
| 777 |
+
return `<div class="ms-loading"><div class="ms-spinner"></div><div>${esc(label || 'Loading…')}</div></div>`;
|
| 778 |
+
}
|
| 779 |
+
|
| 780 |
+
function emptyHTML(title, sub) {
|
| 781 |
+
return `<div class="ms-empty">
|
| 782 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
|
| 783 |
+
<h3>${esc(title)}</h3>
|
| 784 |
+
<p>${esc(sub || '')}</p>
|
| 785 |
+
</div>`;
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
function animeCardHTML(anime) {
|
| 789 |
+
const cover = coverUrl(anime);
|
| 790 |
+
const themesCount = (anime.animethemes || []).length;
|
| 791 |
+
const sub = themesCount ? `${themesCount} theme${themesCount > 1 ? 's' : ''} · ${anime.year || ''}` : `${anime.year || ''} · ${anime.media_format || ''}`;
|
| 792 |
+
return `<a class="ms-card" data-anime-id="${esc(anime.id)}" href="#/anime/${encodeURIComponent(anime.id)}">
|
| 793 |
+
<div class="ms-card-cover-wrap">
|
| 794 |
+
${coverImg(cover, anime.name)}
|
| 795 |
+
<button class="ms-card-play" data-play-anime="${esc(anime.id)}" aria-label="Play">
|
| 796 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
| 797 |
+
</button>
|
| 798 |
+
</div>
|
| 799 |
+
<div class="ms-card-title">${esc(anime.name)}</div>
|
| 800 |
+
<div class="ms-card-sub">${esc(sub)}</div>
|
| 801 |
+
</a>`;
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
function artistCardHTML(artist) {
|
| 805 |
+
const img = artistImageUrl(artist);
|
| 806 |
+
return `<a class="ms-card is-artist" href="#/artist/${encodeURIComponent(artist.slug || artist.id)}">
|
| 807 |
+
<div class="ms-card-cover-wrap">
|
| 808 |
+
${coverImg(img, artist.name)}
|
| 809 |
+
</div>
|
| 810 |
+
<div class="ms-card-title">${esc(artist.name)}</div>
|
| 811 |
+
<div class="ms-card-sub">Artist</div>
|
| 812 |
+
</a>`;
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
function songCardHTML(song) {
|
| 816 |
+
const themes = song.animethemes || [];
|
| 817 |
+
const firstAnime = themes[0]?.anime;
|
| 818 |
+
const artists = (song.artists || []).map(a => a.name).join(', ');
|
| 819 |
+
const href = firstAnime ? `#/anime/${encodeURIComponent(firstAnime.id)}` : '#/';
|
| 820 |
+
return `<a class="ms-card" href="${href}">
|
| 821 |
+
<div class="ms-card-cover-wrap">
|
| 822 |
+
${placeholderCover(song.title)}
|
| 823 |
+
${firstAnime ? `<button class="ms-card-play" data-play-song-anime="${esc(firstAnime.id)}" data-song-theme="${esc(themes[0].id)}" aria-label="Play">
|
| 824 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
| 825 |
+
</button>` : ''}
|
| 826 |
+
</div>
|
| 827 |
+
<div class="ms-card-title">${esc(song.title)}</div>
|
| 828 |
+
<div class="ms-card-sub">${esc(artists || (firstAnime?.name || ''))}</div>
|
| 829 |
+
</a>`;
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
function bindCardClicks(scope) {
|
| 833 |
+
$$('[data-play-anime]', scope).forEach(btn => {
|
| 834 |
+
btn.addEventListener('click', async (e) => {
|
| 835 |
+
e.preventDefault();
|
| 836 |
+
e.stopPropagation();
|
| 837 |
+
const id = btn.dataset.playAnime;
|
| 838 |
+
btn.disabled = true;
|
| 839 |
+
try {
|
| 840 |
+
const data = await api.anime(id);
|
| 841 |
+
const a = data.anime;
|
| 842 |
+
if (!a) return;
|
| 843 |
+
const tracks = tracksFromAnime(a);
|
| 844 |
+
if (tracks.length) play(tracks, 0);
|
| 845 |
+
} finally { btn.disabled = false; }
|
| 846 |
+
});
|
| 847 |
+
});
|
| 848 |
+
$$('[data-play-song-anime]', scope).forEach(btn => {
|
| 849 |
+
btn.addEventListener('click', async (e) => {
|
| 850 |
+
e.preventDefault();
|
| 851 |
+
e.stopPropagation();
|
| 852 |
+
const aid = btn.dataset.playSongAnime;
|
| 853 |
+
const themeId = btn.dataset.songTheme;
|
| 854 |
+
btn.disabled = true;
|
| 855 |
+
try {
|
| 856 |
+
const data = await api.anime(aid);
|
| 857 |
+
const a = data.anime;
|
| 858 |
+
if (!a) return;
|
| 859 |
+
const tracks = tracksFromAnime(a);
|
| 860 |
+
if (!tracks.length) return;
|
| 861 |
+
const idx = Math.max(0, tracks.findIndex(t => String(t.themeId) === String(themeId)));
|
| 862 |
+
play(tracks, idx);
|
| 863 |
+
} finally { btn.disabled = false; }
|
| 864 |
+
});
|
| 865 |
+
});
|
| 866 |
+
}
|
| 867 |
+
|
| 868 |
+
function renderHome() {
|
| 869 |
+
const hour = new Date().getHours();
|
| 870 |
+
const greet = hour < 5 ? 'Good evening' : hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening';
|
| 871 |
+
|
| 872 |
+
view.innerHTML = `
|
| 873 |
+
<div class="ms-home-hero">
|
| 874 |
+
<h1 class="ms-home-greet">${greet}</h1>
|
| 875 |
+
<div class="ms-quick-grid" id="home-quick"></div>
|
| 876 |
+
</div>
|
| 877 |
+
|
| 878 |
+
<div class="ms-section-title">
|
| 879 |
+
<h2>Newest themes</h2>
|
| 880 |
+
<a href="#/recent">View all</a>
|
| 881 |
+
</div>
|
| 882 |
+
<div class="ms-card-grid" id="home-recent">${loadingHTML()}</div>
|
| 883 |
+
|
| 884 |
+
<div class="ms-section-title">
|
| 885 |
+
<h2>Popular artists</h2>
|
| 886 |
+
<a href="#/artists">View all</a>
|
| 887 |
+
</div>
|
| 888 |
+
<div class="ms-card-grid" id="home-artists">${loadingHTML()}</div>
|
| 889 |
+
|
| 890 |
+
<div class="ms-section-title">
|
| 891 |
+
<h2>From your library</h2>
|
| 892 |
+
<a href="#/liked">View all</a>
|
| 893 |
+
</div>
|
| 894 |
+
<div id="home-liked"></div>
|
| 895 |
+
|
| 896 |
+
<div class="ms-section-title">
|
| 897 |
+
<h2>Browse by year</h2>
|
| 898 |
+
</div>
|
| 899 |
+
<div class="ms-card-grid ms-years-grid" id="home-years"></div>
|
| 900 |
+
`;
|
| 901 |
+
|
| 902 |
+
const quickEl = $('#home-quick');
|
| 903 |
+
const quickIds = [];
|
| 904 |
+
Array.from(state.liked).slice(0, 4).forEach(id => quickIds.push(id));
|
| 905 |
+
state.history.slice(0, 6).forEach(id => { if (!quickIds.includes(id)) quickIds.push(id); });
|
| 906 |
+
const quickTracks = loadStoredTracks(quickIds.slice(0, 6));
|
| 907 |
+
if (quickTracks.length) {
|
| 908 |
+
quickEl.innerHTML = quickTracks.map(t => `
|
| 909 |
+
<a class="ms-quick-tile" href="#/anime/${encodeURIComponent(t.animeId)}" data-quick-tid="${esc(t.id)}">
|
| 910 |
+
${t.cover ? `<img class="ms-quick-cover" src="${esc(t.cover)}" alt="" loading="lazy" referrerpolicy="no-referrer">` : `<div class="ms-quick-cover" style="background:linear-gradient(135deg,#4c1d95,#1e1b4b)"></div>`}
|
| 911 |
+
<span class="ms-quick-name">${esc(t.title)}</span>
|
| 912 |
+
<button class="ms-quick-play" data-play-quick="${esc(t.id)}" aria-label="Play">
|
| 913 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
| 914 |
+
</button>
|
| 915 |
+
</a>
|
| 916 |
+
`).join('');
|
| 917 |
+
$$('[data-play-quick]', quickEl).forEach(btn => {
|
| 918 |
+
btn.addEventListener('click', (e) => {
|
| 919 |
+
e.preventDefault(); e.stopPropagation();
|
| 920 |
+
const t = quickTracks.find(x => x.id === btn.dataset.playQuick);
|
| 921 |
+
if (t) play([t], 0);
|
| 922 |
+
});
|
| 923 |
+
});
|
| 924 |
+
} else {
|
| 925 |
+
quickEl.innerHTML = `<div class="ms-quick-empty">Like songs and they'll show up here for one-tap play.</div>`;
|
| 926 |
+
}
|
| 927 |
+
|
| 928 |
+
api.recent().then(d => {
|
| 929 |
+
const items = (d.anime || []).slice(0, 12);
|
| 930 |
+
$('#home-recent').innerHTML = items.length
|
| 931 |
+
? items.map(animeCardHTML).join('')
|
| 932 |
+
: emptyHTML('No themes found', '');
|
| 933 |
+
bindCardClicks($('#home-recent'));
|
| 934 |
+
}).catch(() => { $('#home-recent').innerHTML = emptyHTML('Could not load', 'Try again in a moment.'); });
|
| 935 |
+
|
| 936 |
+
api.artists().then(d => {
|
| 937 |
+
const items = (d.artists || []).slice(0, 8);
|
| 938 |
+
$('#home-artists').innerHTML = items.length
|
| 939 |
+
? items.map(artistCardHTML).join('')
|
| 940 |
+
: emptyHTML('No artists', '');
|
| 941 |
+
}).catch(() => { $('#home-artists').innerHTML = emptyHTML('Could not load', 'Try again in a moment.'); });
|
| 942 |
+
|
| 943 |
+
const likedEl = $('#home-liked');
|
| 944 |
+
const likedTracks = loadStoredTracks(Array.from(state.liked).slice(0, 6));
|
| 945 |
+
if (likedTracks.length) {
|
| 946 |
+
likedEl.innerHTML = `<div class="ms-card-grid">${likedTracks.map(t => `
|
| 947 |
+
<div class="ms-card" data-tid="${esc(t.id)}">
|
| 948 |
+
<div class="ms-card-cover-wrap">
|
| 949 |
+
${t.cover ? `<img class="ms-card-cover" src="${esc(t.cover)}" alt="" loading="lazy" referrerpolicy="no-referrer">` : placeholderCover(t.title)}
|
| 950 |
+
<button class="ms-card-play" data-play-liked="${esc(t.id)}" aria-label="Play">
|
| 951 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
| 952 |
+
</button>
|
| 953 |
+
</div>
|
| 954 |
+
<div class="ms-card-title">${esc(t.title)}</div>
|
| 955 |
+
<div class="ms-card-sub">${esc((t.artists || []).map(a => a.name).join(', ') || t.animeName || '')}</div>
|
| 956 |
+
</div>
|
| 957 |
+
`).join('')}</div>`;
|
| 958 |
+
$$('[data-play-liked]', likedEl).forEach(btn => {
|
| 959 |
+
btn.addEventListener('click', (e) => {
|
| 960 |
+
e.preventDefault(); e.stopPropagation();
|
| 961 |
+
const t = likedTracks.find(x => x.id === btn.dataset.playLiked);
|
| 962 |
+
if (t) play([t], 0);
|
| 963 |
+
});
|
| 964 |
+
});
|
| 965 |
+
} else {
|
| 966 |
+
likedEl.innerHTML = `<div class="ms-empty" style="padding:36px 12px"><h3>Your library is empty</h3><p>Like songs to see them here.</p></div>`;
|
| 967 |
+
}
|
| 968 |
+
|
| 969 |
+
const yearsEl = $('#home-years');
|
| 970 |
+
const cy = new Date().getFullYear();
|
| 971 |
+
const years = [];
|
| 972 |
+
for (let y = cy; y >= cy - 7; y--) years.push(y);
|
| 973 |
+
const yearGradients = [
|
| 974 |
+
['#7c3aed', '#1e1b4b'], ['#5b21b6', '#0f172a'], ['#4c1d95', '#1e293b'],
|
| 975 |
+
['#9d6ef8', '#312e81'], ['#6d28d9', '#0b1023'], ['#7c3aed', '#0f0d1f'],
|
| 976 |
+
['#5b21b6', '#1f1430'], ['#4c1d95', '#0a0712'],
|
| 977 |
+
];
|
| 978 |
+
yearsEl.innerHTML = years.map((y, i) => {
|
| 979 |
+
const [a, b] = yearGradients[i % yearGradients.length];
|
| 980 |
+
return `<a class="ms-year-card" href="#/year/${y}">
|
| 981 |
+
<div class="ms-year-cover" style="background:linear-gradient(135deg, ${a} 0%, ${b} 100%);">
|
| 982 |
+
<span class="ms-year-num">${y}</span>
|
| 983 |
+
</div>
|
| 984 |
+
<div class="ms-card-title">${y}</div>
|
| 985 |
+
<div class="ms-card-sub">Themes from ${y}</div>
|
| 986 |
+
</a>`;
|
| 987 |
+
}).join('');
|
| 988 |
+
}
|
| 989 |
+
|
| 990 |
+
function renderRecent() {
|
| 991 |
+
view.innerHTML = `<div class="ms-section-title" style="margin-top:14px"><h2>Newest themes</h2></div>
|
| 992 |
+
<div class="ms-card-grid" id="recent-grid">${loadingHTML()}</div>`;
|
| 993 |
+
api.recent().then(d => {
|
| 994 |
+
const items = d.anime || [];
|
| 995 |
+
$('#recent-grid').innerHTML = items.length
|
| 996 |
+
? items.map(animeCardHTML).join('')
|
| 997 |
+
: emptyHTML('No themes available', '');
|
| 998 |
+
bindCardClicks($('#recent-grid'));
|
| 999 |
+
});
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
function renderYear(y) {
|
| 1003 |
+
view.innerHTML = `<div class="ms-section-title" style="margin-top:14px"><h2>Themes from ${esc(y)}</h2></div>
|
| 1004 |
+
<div class="ms-card-grid" id="year-grid">${loadingHTML()}</div>`;
|
| 1005 |
+
api.year(y).then(d => {
|
| 1006 |
+
const items = d.anime || [];
|
| 1007 |
+
$('#year-grid').innerHTML = items.length
|
| 1008 |
+
? items.map(animeCardHTML).join('')
|
| 1009 |
+
: emptyHTML('Nothing here yet', `No themes catalogued for ${y}.`);
|
| 1010 |
+
bindCardClicks($('#year-grid'));
|
| 1011 |
+
});
|
| 1012 |
+
}
|
| 1013 |
+
|
| 1014 |
+
function renderTopArtists() {
|
| 1015 |
+
view.innerHTML = `<div class="ms-section-title" style="margin-top:14px"><h2>Artists</h2></div>
|
| 1016 |
+
<div class="ms-card-grid" id="artists-grid">${loadingHTML()}</div>`;
|
| 1017 |
+
api.artists().then(d => {
|
| 1018 |
+
const items = d.artists || [];
|
| 1019 |
+
$('#artists-grid').innerHTML = items.length
|
| 1020 |
+
? items.map(artistCardHTML).join('')
|
| 1021 |
+
: emptyHTML('No artists', '');
|
| 1022 |
+
});
|
| 1023 |
+
}
|
| 1024 |
+
|
| 1025 |
+
function renderLiked() {
|
| 1026 |
+
const tracks = loadStoredTracks(Array.from(state.liked));
|
| 1027 |
+
if (!tracks.length) {
|
| 1028 |
+
view.innerHTML = `<div class="ms-album-hero">
|
| 1029 |
+
<div class="ms-album-cover" style="display:flex;align-items:center;justify-content:center;">
|
| 1030 |
+
<svg viewBox="0 0 24 24" width="80" height="80" fill="#fff" style="opacity:0.85"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
| 1031 |
+
</div>
|
| 1032 |
+
<div class="ms-album-info">
|
| 1033 |
+
<div class="ms-album-eyebrow">Playlist</div>
|
| 1034 |
+
<h1 class="ms-album-title">Liked Songs</h1>
|
| 1035 |
+
<div class="ms-album-meta">0 tracks</div>
|
| 1036 |
+
</div>
|
| 1037 |
+
</div>${emptyHTML('No liked songs yet', 'Tap the heart on any track to save it here.')}`;
|
| 1038 |
+
return;
|
| 1039 |
+
}
|
| 1040 |
+
renderTracklist({
|
| 1041 |
+
eyebrow: 'Playlist',
|
| 1042 |
+
title: 'Liked Songs',
|
| 1043 |
+
cover: tracks[0].cover,
|
| 1044 |
+
countLabel: `${tracks.length} track${tracks.length > 1 ? 's' : ''}`,
|
| 1045 |
+
tracks,
|
| 1046 |
+
asPlaylist: true,
|
| 1047 |
+
});
|
| 1048 |
+
}
|
| 1049 |
+
|
| 1050 |
+
function renderHistory() {
|
| 1051 |
+
const tracks = loadStoredTracks(state.history);
|
| 1052 |
+
if (!tracks.length) {
|
| 1053 |
+
view.innerHTML = emptyHTML('Nothing recently played', 'Play some songs to build your history.');
|
| 1054 |
+
return;
|
| 1055 |
+
}
|
| 1056 |
+
renderTracklist({
|
| 1057 |
+
eyebrow: 'History',
|
| 1058 |
+
title: 'Recently Played',
|
| 1059 |
+
cover: tracks[0].cover,
|
| 1060 |
+
countLabel: `${tracks.length} track${tracks.length > 1 ? 's' : ''}`,
|
| 1061 |
+
tracks,
|
| 1062 |
+
asPlaylist: true,
|
| 1063 |
+
});
|
| 1064 |
+
}
|
| 1065 |
+
|
| 1066 |
+
async function renderAnime(id) {
|
| 1067 |
+
view.innerHTML = loadingHTML('Loading album…');
|
| 1068 |
+
try {
|
| 1069 |
+
const data = await api.anime(id);
|
| 1070 |
+
const a = data.anime;
|
| 1071 |
+
if (!a) { view.innerHTML = emptyHTML('Not found', 'This anime has no themes catalogued.'); return; }
|
| 1072 |
+
const tracks = tracksFromAnime(a);
|
| 1073 |
+
renderTracklist({
|
| 1074 |
+
eyebrow: a.media_format || 'Anime',
|
| 1075 |
+
title: a.name,
|
| 1076 |
+
cover: coverUrl(a),
|
| 1077 |
+
countLabel: `${tracks.length} track${tracks.length === 1 ? '' : 's'} · ${a.year || ''}${a.season ? ' · ' + a.season : ''}`,
|
| 1078 |
+
tracks,
|
| 1079 |
+
asPlaylist: false,
|
| 1080 |
+
});
|
| 1081 |
+
} catch (e) {
|
| 1082 |
+
view.innerHTML = emptyHTML('Could not load', 'Please try again.');
|
| 1083 |
+
}
|
| 1084 |
+
}
|
| 1085 |
+
|
| 1086 |
+
async function renderArtist(slug) {
|
| 1087 |
+
view.innerHTML = loadingHTML('Loading artist…');
|
| 1088 |
+
try {
|
| 1089 |
+
const data = await api.artist(slug);
|
| 1090 |
+
const a = data.artist;
|
| 1091 |
+
if (!a) { view.innerHTML = emptyHTML('Artist not found', ''); return; }
|
| 1092 |
+
const tracks = [];
|
| 1093 |
+
for (const song of (a.songs || [])) {
|
| 1094 |
+
for (const theme of (song.animethemes || [])) {
|
| 1095 |
+
if (!theme.anime) continue;
|
| 1096 |
+
const merged = { ...theme, song };
|
| 1097 |
+
const tk = trackFromTheme(theme.anime, merged);
|
| 1098 |
+
if (tk) tracks.push(tk);
|
| 1099 |
+
}
|
| 1100 |
+
}
|
| 1101 |
+
renderTracklist({
|
| 1102 |
+
eyebrow: 'Artist',
|
| 1103 |
+
title: a.name,
|
| 1104 |
+
cover: artistImageUrl(a),
|
| 1105 |
+
round: true,
|
| 1106 |
+
countLabel: `${tracks.length} track${tracks.length === 1 ? '' : 's'}`,
|
| 1107 |
+
tracks,
|
| 1108 |
+
asPlaylist: true,
|
| 1109 |
+
});
|
| 1110 |
+
} catch (e) {
|
| 1111 |
+
view.innerHTML = emptyHTML('Could not load', 'Please try again.');
|
| 1112 |
+
}
|
| 1113 |
+
}
|
| 1114 |
+
|
| 1115 |
+
function renderTracklist({ eyebrow, title, cover, countLabel, tracks, asPlaylist, round }) {
|
| 1116 |
+
const heroCoverHTML = cover
|
| 1117 |
+
? `<img src="${esc(cover)}" alt="" referrerpolicy="no-referrer">`
|
| 1118 |
+
: `<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#fff;font-size:4rem;font-weight:900;">${esc(title.slice(0, 1))}</div>`;
|
| 1119 |
+
view.innerHTML = `
|
| 1120 |
+
<div class="ms-album-hero">
|
| 1121 |
+
<div class="ms-album-cover" ${round ? 'style="border-radius:50%;"' : ''}>${heroCoverHTML}</div>
|
| 1122 |
+
<div class="ms-album-info">
|
| 1123 |
+
<div class="ms-album-eyebrow">${esc(eyebrow)}</div>
|
| 1124 |
+
<h1 class="ms-album-title">${esc(title)}</h1>
|
| 1125 |
+
<div class="ms-album-meta">${esc(countLabel)}</div>
|
| 1126 |
+
</div>
|
| 1127 |
+
</div>
|
| 1128 |
+
<div class="ms-album-actions">
|
| 1129 |
+
<button class="ms-play-big" id="album-play" aria-label="Play all">
|
| 1130 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
| 1131 |
+
</button>
|
| 1132 |
+
<button class="ms-icon-btn" id="album-shuffle" aria-label="Shuffle">
|
| 1133 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>
|
| 1134 |
+
</button>
|
| 1135 |
+
</div>
|
| 1136 |
+
<div class="ms-tracks">
|
| 1137 |
+
${tracks.length ? `
|
| 1138 |
+
<div class="ms-tracks-head">
|
| 1139 |
+
<span style="text-align:center">#</span>
|
| 1140 |
+
<span>Title</span>
|
| 1141 |
+
<span class="ms-th-anime">${asPlaylist ? 'Anime' : 'Type'}</span>
|
| 1142 |
+
<span class="ms-th-tag">${asPlaylist ? 'Type' : 'Episodes'}</span>
|
| 1143 |
+
<span></span>
|
| 1144 |
+
</div>
|
| 1145 |
+
${tracks.map((t, i) => trackRowHTML(t, i, asPlaylist)).join('')}
|
| 1146 |
+
` : emptyHTML('No tracks found', '')}
|
| 1147 |
+
</div>
|
| 1148 |
+
`;
|
| 1149 |
+
const playAll = $('#album-play');
|
| 1150 |
+
if (playAll) playAll.addEventListener('click', () => { if (tracks.length) play(tracks, 0); });
|
| 1151 |
+
const shuf = $('#album-shuffle');
|
| 1152 |
+
if (shuf) shuf.addEventListener('click', () => {
|
| 1153 |
+
if (!tracks.length) return;
|
| 1154 |
+
const shuffled = tracks.slice().sort(() => Math.random() - 0.5);
|
| 1155 |
+
state.shuffle = false;
|
| 1156 |
+
play(shuffled, 0);
|
| 1157 |
+
});
|
| 1158 |
+
bindTrackRows(tracks);
|
| 1159 |
+
updateNowPlayingMarkers();
|
| 1160 |
+
}
|
| 1161 |
+
|
| 1162 |
+
function trackRowHTML(t, i, asPlaylist) {
|
| 1163 |
+
const tagClass = (t.typeKind || '').toUpperCase() === 'ED' ? 'ed' : ((t.typeKind || '').toUpperCase() === 'IN' ? 'in' : '');
|
| 1164 |
+
const artistsText = (t.artists || []).map(a => `<a href="#/artist/${encodeURIComponent(a.slug || a.id)}" data-stop>${esc(a.name)}</a>`).join(', ');
|
| 1165 |
+
const middleCol = asPlaylist
|
| 1166 |
+
? `<a class="ms-track-anime" href="#/anime/${encodeURIComponent(t.animeId)}" data-stop>${esc(t.animeName)}</a>`
|
| 1167 |
+
: `<span class="ms-track-tag-cell"><span class="ms-track-tag ${tagClass}">${esc(t.type)}</span></span>`;
|
| 1168 |
+
const fourthCol = asPlaylist
|
| 1169 |
+
? `<span class="ms-track-tag-cell"><span class="ms-track-tag ${tagClass}">${esc(t.type)}</span></span>`
|
| 1170 |
+
: `<span class="ms-track-anime">${esc(t.episodes || '—')}</span>`;
|
| 1171 |
+
return `<div class="ms-track" data-tid="${esc(t.id)}" data-idx="${i}">
|
| 1172 |
+
<div class="ms-track-num">
|
| 1173 |
+
<span class="ms-num">${i + 1}</span>
|
| 1174 |
+
<span class="ms-now-bars"><span></span><span></span><span></span></span>
|
| 1175 |
+
<span class="ms-play-mini"><svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg></span>
|
| 1176 |
+
</div>
|
| 1177 |
+
<div class="ms-track-meta">
|
| 1178 |
+
${t.cover ? `<img class="ms-track-cover" src="${esc(t.cover)}" alt="" loading="lazy" referrerpolicy="no-referrer">` : '<div class="ms-track-cover"></div>'}
|
| 1179 |
+
<div class="ms-track-text">
|
| 1180 |
+
<div class="ms-track-title">${esc(t.title)}</div>
|
| 1181 |
+
<div class="ms-track-sub">${artistsText || esc(t.animeName)}</div>
|
| 1182 |
+
</div>
|
| 1183 |
+
</div>
|
| 1184 |
+
${middleCol}
|
| 1185 |
+
${fourthCol}
|
| 1186 |
+
<button class="ms-track-like ${state.liked.has(t.id) ? 'is-liked' : ''}" data-tid="${esc(t.id)}" data-stop aria-label="Like">
|
| 1187 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
| 1188 |
+
</button>
|
| 1189 |
+
</div>`;
|
| 1190 |
+
}
|
| 1191 |
+
|
| 1192 |
+
function bindTrackRows(tracks) {
|
| 1193 |
+
$$('.ms-track', view).forEach(row => {
|
| 1194 |
+
row.addEventListener('click', (e) => {
|
| 1195 |
+
if (e.target.closest('[data-stop]')) return;
|
| 1196 |
+
const idx = parseInt(row.dataset.idx, 10);
|
| 1197 |
+
if (!isNaN(idx)) play(tracks, idx);
|
| 1198 |
+
});
|
| 1199 |
+
});
|
| 1200 |
+
$$('.ms-track-like', view).forEach(btn => {
|
| 1201 |
+
btn.addEventListener('click', (e) => {
|
| 1202 |
+
e.preventDefault(); e.stopPropagation();
|
| 1203 |
+
const id = btn.dataset.tid;
|
| 1204 |
+
const t = tracks.find(x => x.id === id);
|
| 1205 |
+
if (t) toggleLike(t);
|
| 1206 |
+
});
|
| 1207 |
+
});
|
| 1208 |
+
}
|
| 1209 |
+
|
| 1210 |
+
function songRowHTML(track, idx) {
|
| 1211 |
+
const artistsText = (track.artists || []).map(a => `<a href="#/artist/${encodeURIComponent(a.slug || a.id)}" data-stop>${esc(a.name)}</a>`).join(', ');
|
| 1212 |
+
return `<div class="ms-song-row" data-tid="${esc(track.id)}" data-idx="${idx}">
|
| 1213 |
+
<div class="ms-song-cover-wrap">
|
| 1214 |
+
${track.cover ? `<img class="ms-song-cover" src="${esc(track.cover)}" alt="" loading="lazy" referrerpolicy="no-referrer">` : '<div class="ms-song-cover"></div>'}
|
| 1215 |
+
<span class="ms-song-play"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg></span>
|
| 1216 |
+
</div>
|
| 1217 |
+
<div class="ms-song-text">
|
| 1218 |
+
<div class="ms-song-title">${esc(track.title)}</div>
|
| 1219 |
+
<div class="ms-song-sub">${artistsText || '<a href="#/anime/' + encodeURIComponent(track.animeId) + '" data-stop>' + esc(track.animeName) + '</a>'}</div>
|
| 1220 |
+
</div>
|
| 1221 |
+
<button class="ms-song-like ${state.liked.has(track.id) ? 'is-liked' : ''}" data-tid="${esc(track.id)}" data-stop aria-label="Like">
|
| 1222 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
| 1223 |
+
</button>
|
| 1224 |
+
</div>`;
|
| 1225 |
+
}
|
| 1226 |
+
|
| 1227 |
+
function bindSongRows(scope, tracks) {
|
| 1228 |
+
$$('.ms-song-row', scope).forEach(row => {
|
| 1229 |
+
row.addEventListener('click', (e) => {
|
| 1230 |
+
if (e.target.closest('[data-stop]')) return;
|
| 1231 |
+
const idx = parseInt(row.dataset.idx, 10);
|
| 1232 |
+
if (!isNaN(idx) && tracks[idx]) play(tracks, idx);
|
| 1233 |
+
});
|
| 1234 |
+
});
|
| 1235 |
+
$$('.ms-song-like', scope).forEach(btn => {
|
| 1236 |
+
btn.addEventListener('click', (e) => {
|
| 1237 |
+
e.preventDefault(); e.stopPropagation();
|
| 1238 |
+
const id = btn.dataset.tid;
|
| 1239 |
+
const t = tracks.find(x => x.id === id);
|
| 1240 |
+
if (t) toggleLike(t);
|
| 1241 |
+
});
|
| 1242 |
+
});
|
| 1243 |
+
}
|
| 1244 |
+
|
| 1245 |
+
async function renderSearch(q) {
|
| 1246 |
+
if (!q) { renderHome(); return; }
|
| 1247 |
+
view.innerHTML = loadingHTML(`Searching "${q}"…`);
|
| 1248 |
+
const filter = (state.searchFilter || 'all');
|
| 1249 |
+
try {
|
| 1250 |
+
const data = await api.search(q);
|
| 1251 |
+
window.__lastSearch = data;
|
| 1252 |
+
const anime = data.anime || [];
|
| 1253 |
+
const themes = data.animethemes || [];
|
| 1254 |
+
const artists = data.artists || [];
|
| 1255 |
+
const songs = data.songs || [];
|
| 1256 |
+
|
| 1257 |
+
const themeTracks = themes
|
| 1258 |
+
.map(th => th.anime ? trackFromTheme(th.anime, th) : null)
|
| 1259 |
+
.filter(Boolean);
|
| 1260 |
+
|
| 1261 |
+
const tabsHTML = `<div class="ms-search-tabs">
|
| 1262 |
+
${['all','anime','songs','artists'].map(f =>
|
| 1263 |
+
`<button class="ms-tab ${filter===f?'active':''}" data-filter="${f}">${f[0].toUpperCase()+f.slice(1)}</button>`
|
| 1264 |
+
).join('')}
|
| 1265 |
+
</div>`;
|
| 1266 |
+
|
| 1267 |
+
const noResults = !anime.length && !artists.length && !songs.length && !themeTracks.length;
|
| 1268 |
+
if (noResults) {
|
| 1269 |
+
view.innerHTML = `${tabsHTML}${emptyHTML(`No results for "${q}"`, 'Try a different anime, song, or artist.')}`;
|
| 1270 |
+
bindSearchTabs(q);
|
| 1271 |
+
return;
|
| 1272 |
+
}
|
| 1273 |
+
|
| 1274 |
+
let topResultHTML = '';
|
| 1275 |
+
const topAnime = anime[0];
|
| 1276 |
+
if (topAnime && (filter === 'all' || filter === 'anime')) {
|
| 1277 |
+
const cover = coverUrl(topAnime);
|
| 1278 |
+
topResultHTML = `<div class="ms-top-result">
|
| 1279 |
+
<div>
|
| 1280 |
+
<div class="ms-section-title" style="margin:0 0 14px"><h2>Top result</h2></div>
|
| 1281 |
+
<a class="ms-top-card" href="#/anime/${encodeURIComponent(topAnime.id)}">
|
| 1282 |
+
${cover ? `<img class="ms-top-cover" src="${esc(cover)}" alt="" referrerpolicy="no-referrer">` : `<div class="ms-top-cover" style="background:linear-gradient(135deg,#4c1d95,#1e1b4b)"></div>`}
|
| 1283 |
+
<div class="ms-top-name">${esc(topAnime.name)}</div>
|
| 1284 |
+
<span class="ms-top-tag">Anime${topAnime.year ? ' · ' + esc(topAnime.year) : ''}</span>
|
| 1285 |
+
<button class="ms-top-play" data-play-anime="${esc(topAnime.id)}" aria-label="Play">
|
| 1286 |
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
| 1287 |
+
</button>
|
| 1288 |
+
</a>
|
| 1289 |
+
</div>
|
| 1290 |
+
${themeTracks.length ? `<div class="ms-top-songs">
|
| 1291 |
+
<h3>Songs</h3>
|
| 1292 |
+
<div class="ms-songlist" id="search-top-songs">
|
| 1293 |
+
${themeTracks.slice(0, 4).map((tk, i) => songRowHTML(tk, i)).join('')}
|
| 1294 |
+
</div>
|
| 1295 |
+
</div>` : ''}
|
| 1296 |
+
</div>`;
|
| 1297 |
+
}
|
| 1298 |
+
|
| 1299 |
+
const animeBlock = (filter === 'all' || filter === 'anime') && anime.length ? `<div class="ms-search-block">
|
| 1300 |
+
<div class="ms-section-title"><h2>Anime</h2></div>
|
| 1301 |
+
<div class="ms-card-grid">${anime.slice(0, filter === 'anime' ? 24 : 8).map(animeCardHTML).join('')}</div>
|
| 1302 |
+
</div>` : '';
|
| 1303 |
+
|
| 1304 |
+
const artistsBlock = (filter === 'all' || filter === 'artists') && artists.length ? `<div class="ms-search-block">
|
| 1305 |
+
<div class="ms-section-title"><h2>Artists</h2></div>
|
| 1306 |
+
<div class="ms-card-grid">${artists.slice(0, filter === 'artists' ? 24 : 8).map(artistCardHTML).join('')}</div>
|
| 1307 |
+
</div>` : '';
|
| 1308 |
+
|
| 1309 |
+
let songsBlock = '';
|
| 1310 |
+
if ((filter === 'all' || filter === 'songs')) {
|
| 1311 |
+
const songTracksAll = themeTracks.length ? themeTracks : [];
|
| 1312 |
+
if (songTracksAll.length) {
|
| 1313 |
+
const limit = filter === 'songs' ? songTracksAll.length : 8;
|
| 1314 |
+
songsBlock = `<div class="ms-search-block">
|
| 1315 |
+
<div class="ms-section-title"><h2>Songs</h2></div>
|
| 1316 |
+
<div class="ms-songlist" id="search-songs-list">
|
| 1317 |
+
${songTracksAll.slice(0, limit).map((tk, i) => songRowHTML(tk, i)).join('')}
|
| 1318 |
+
</div>
|
| 1319 |
+
</div>`;
|
| 1320 |
+
} else if (songs.length) {
|
| 1321 |
+
songsBlock = `<div class="ms-search-block">
|
| 1322 |
+
<div class="ms-section-title"><h2>Songs</h2></div>
|
| 1323 |
+
<div class="ms-card-grid">${songs.slice(0, 8).map(songCardHTML).join('')}</div>
|
| 1324 |
+
</div>`;
|
| 1325 |
+
}
|
| 1326 |
+
}
|
| 1327 |
+
|
| 1328 |
+
view.innerHTML = `
|
| 1329 |
+
${tabsHTML}
|
| 1330 |
+
${topResultHTML}
|
| 1331 |
+
${animeBlock}
|
| 1332 |
+
${songsBlock}
|
| 1333 |
+
${artistsBlock}
|
| 1334 |
+
`;
|
| 1335 |
+
|
| 1336 |
+
bindSearchTabs(q);
|
| 1337 |
+
bindCardClicks(view);
|
| 1338 |
+
|
| 1339 |
+
const topListEl = $('#search-top-songs');
|
| 1340 |
+
if (topListEl) bindSongRows(topListEl, themeTracks.slice(0, 4));
|
| 1341 |
+
const songsListEl = $('#search-songs-list');
|
| 1342 |
+
if (songsListEl) {
|
| 1343 |
+
const limit = filter === 'songs' ? themeTracks.length : 8;
|
| 1344 |
+
bindSongRows(songsListEl, themeTracks.slice(0, limit));
|
| 1345 |
+
}
|
| 1346 |
+
updateNowPlayingMarkers();
|
| 1347 |
+
} catch (e) {
|
| 1348 |
+
console.error('[music] search failed', e);
|
| 1349 |
+
view.innerHTML = emptyHTML('Search failed', 'Please try again.');
|
| 1350 |
+
}
|
| 1351 |
+
}
|
| 1352 |
+
|
| 1353 |
+
function bindSearchTabs(q) {
|
| 1354 |
+
$$('.ms-tab', view).forEach(tab => {
|
| 1355 |
+
tab.addEventListener('click', () => {
|
| 1356 |
+
state.searchFilter = tab.dataset.filter;
|
| 1357 |
+
renderSearch(q);
|
| 1358 |
+
});
|
| 1359 |
+
});
|
| 1360 |
+
}
|
| 1361 |
+
|
| 1362 |
+
function closeSidebarIfMobile() {
|
| 1363 |
+
if (window.innerWidth <= 1024) {
|
| 1364 |
+
$('#music-sidebar').classList.remove('open');
|
| 1365 |
+
$('#ms-sidebar-backdrop').classList.remove('active');
|
| 1366 |
+
}
|
| 1367 |
+
}
|
| 1368 |
+
|
| 1369 |
+
function openYearSidebarLinks() {
|
| 1370 |
+
const yearGrid = $('#ms-year-grid');
|
| 1371 |
+
const cy = new Date().getFullYear();
|
| 1372 |
+
const years = [];
|
| 1373 |
+
for (let y = cy; y >= cy - 9; y--) years.push(y);
|
| 1374 |
+
yearGrid.innerHTML = years.map(y => `<a data-route="#/year/${y}" href="#/year/${y}">${y}</a>`).join('');
|
| 1375 |
+
}
|
| 1376 |
+
|
| 1377 |
+
function init() {
|
| 1378 |
+
audio.volume = state.volume;
|
| 1379 |
+
setVolUI();
|
| 1380 |
+
|
| 1381 |
+
openYearSidebarLinks();
|
| 1382 |
+
|
| 1383 |
+
let searchTimer;
|
| 1384 |
+
const searchInput = $('#ms-search-input');
|
| 1385 |
+
searchInput.addEventListener('input', () => {
|
| 1386 |
+
const q = searchInput.value.trim();
|
| 1387 |
+
$('#ms-search-clear').hidden = !q;
|
| 1388 |
+
clearTimeout(searchTimer);
|
| 1389 |
+
if (!q) {
|
| 1390 |
+
if (location.hash.startsWith('#/search')) navigate('#/');
|
| 1391 |
+
return;
|
| 1392 |
+
}
|
| 1393 |
+
searchTimer = setTimeout(() => {
|
| 1394 |
+
navigate('#/search?q=' + encodeURIComponent(q));
|
| 1395 |
+
}, 320);
|
| 1396 |
+
});
|
| 1397 |
+
searchInput.addEventListener('keydown', (e) => {
|
| 1398 |
+
if (e.key === 'Escape') { searchInput.value = ''; navigate('#/'); }
|
| 1399 |
+
});
|
| 1400 |
+
$('#ms-search-clear').addEventListener('click', () => {
|
| 1401 |
+
searchInput.value = '';
|
| 1402 |
+
$('#ms-search-clear').hidden = true;
|
| 1403 |
+
navigate('#/');
|
| 1404 |
+
searchInput.focus();
|
| 1405 |
+
});
|
| 1406 |
+
|
| 1407 |
+
$('#pb-play').addEventListener('click', togglePlay);
|
| 1408 |
+
$('#pb-play-mobile').addEventListener('click', togglePlay);
|
| 1409 |
+
$('#pb-prev').addEventListener('click', prev);
|
| 1410 |
+
$('#pb-next').addEventListener('click', () => next(false));
|
| 1411 |
+
$('#pb-shuffle').addEventListener('click', () => {
|
| 1412 |
+
state.shuffle = !state.shuffle;
|
| 1413 |
+
$('#pb-shuffle').classList.toggle('is-on', state.shuffle);
|
| 1414 |
+
});
|
| 1415 |
+
$('#pb-loop').addEventListener('click', () => {
|
| 1416 |
+
state.loop = !state.loop;
|
| 1417 |
+
$('#pb-loop').classList.toggle('is-on', state.loop);
|
| 1418 |
+
audio.loop = false;
|
| 1419 |
+
});
|
| 1420 |
+
$('#pb-mute').addEventListener('click', () => {
|
| 1421 |
+
state.muted = !state.muted;
|
| 1422 |
+
audio.muted = state.muted;
|
| 1423 |
+
video.muted = state.muted;
|
| 1424 |
+
setVolUI();
|
| 1425 |
+
});
|
| 1426 |
+
$('#pb-like').addEventListener('click', () => {
|
| 1427 |
+
const t = state.queue[state.index];
|
| 1428 |
+
if (t) toggleLike(t);
|
| 1429 |
+
});
|
| 1430 |
+
|
| 1431 |
+
bindBar($('#pb-seek'), (pct) => {
|
| 1432 |
+
const m = activeMedia();
|
| 1433 |
+
if (m.duration) m.currentTime = pct * m.duration;
|
| 1434 |
+
setSeek(pct * (m.duration || 0));
|
| 1435 |
+
});
|
| 1436 |
+
bindBar($('#pb-vol'), (pct) => {
|
| 1437 |
+
state.volume = pct;
|
| 1438 |
+
state.muted = false;
|
| 1439 |
+
audio.muted = false;
|
| 1440 |
+
video.muted = false;
|
| 1441 |
+
audio.volume = pct;
|
| 1442 |
+
video.volume = pct;
|
| 1443 |
+
persistVol();
|
| 1444 |
+
setVolUI();
|
| 1445 |
+
});
|
| 1446 |
+
|
| 1447 |
+
[audio, video].forEach((m) => {
|
| 1448 |
+
m.addEventListener('play', () => {
|
| 1449 |
+
if (m === activeMedia()) { state.isPlaying = true; setPlayUI(true); }
|
| 1450 |
+
if (m === video) showStageLoading(false);
|
| 1451 |
+
});
|
| 1452 |
+
m.addEventListener('pause', () => {
|
| 1453 |
+
if (m === activeMedia()) { state.isPlaying = false; setPlayUI(false); }
|
| 1454 |
+
if (m === video) showStageLoading(false);
|
| 1455 |
+
});
|
| 1456 |
+
m.addEventListener('ended', () => {
|
| 1457 |
+
if (m !== activeMedia()) return;
|
| 1458 |
+
if (state.loop) { m.currentTime = 0; m.play().catch(() => { }); }
|
| 1459 |
+
else next(true);
|
| 1460 |
+
});
|
| 1461 |
+
m.addEventListener('timeupdate', () => {
|
| 1462 |
+
if (m === activeMedia() && !state.scrubbing) setSeek();
|
| 1463 |
+
if (m === activeMedia() && _syncedLines.length) updateLyricsHighlight(m.currentTime);
|
| 1464 |
+
});
|
| 1465 |
+
|
| 1466 |
+
m.addEventListener('play', () => {
|
| 1467 |
+
if (m !== activeMedia()) return;
|
| 1468 |
+
if (_lyricRaf) cancelAnimationFrame(_lyricRaf);
|
| 1469 |
+
const tick = () => {
|
| 1470 |
+
if (m !== activeMedia() || m.paused || m.ended) {
|
| 1471 |
+
_lyricRaf = null;
|
| 1472 |
+
return;
|
| 1473 |
+
}
|
| 1474 |
+
if (_syncedLines.length && document.body.classList.contains('np-lyrics-open')) {
|
| 1475 |
+
updateLyricsHighlight(m.currentTime);
|
| 1476 |
+
}
|
| 1477 |
+
_lyricRaf = requestAnimationFrame(tick);
|
| 1478 |
+
};
|
| 1479 |
+
_lyricRaf = requestAnimationFrame(tick);
|
| 1480 |
+
});
|
| 1481 |
+
m.addEventListener('loadedmetadata', () => {
|
| 1482 |
+
if (m !== activeMedia()) return;
|
| 1483 |
+
setSeek();
|
| 1484 |
+
|
| 1485 |
+
if (_rawSyncedLines.length) {
|
| 1486 |
+
recomputeSyncedLines();
|
| 1487 |
+
renderSyncedLyrics(_syncedLines);
|
| 1488 |
+
}
|
| 1489 |
+
});
|
| 1490 |
+
m.addEventListener('durationchange', () => {
|
| 1491 |
+
if (m !== activeMedia() || !_rawSyncedLines.length) return;
|
| 1492 |
+
recomputeSyncedLines();
|
| 1493 |
+
renderSyncedLyrics(_syncedLines);
|
| 1494 |
+
});
|
| 1495 |
+
|
| 1496 |
+
m.addEventListener('seeked', () => {
|
| 1497 |
+
if (m !== activeMedia() || !_syncedLines.length) return;
|
| 1498 |
+
_activeLyricIdx = -2;
|
| 1499 |
+
updateLyricsHighlight(m.currentTime || 0);
|
| 1500 |
+
});
|
| 1501 |
+
m.addEventListener('error', () => {
|
| 1502 |
+
if (m !== activeMedia()) return;
|
| 1503 |
+
if (m === video) showStageLoading(false);
|
| 1504 |
+
const t = state.queue[state.index];
|
| 1505 |
+
if (t) console.warn('[music] failed to play', t.title);
|
| 1506 |
+
setTimeout(() => next(true), 800);
|
| 1507 |
+
});
|
| 1508 |
+
});
|
| 1509 |
+
|
| 1510 |
+
video.addEventListener('waiting', () => { if (mediaMode === 'video' && !video.paused) showStageLoading(true); });
|
| 1511 |
+
video.addEventListener('loadstart', () => { if (mediaMode === 'video') showStageLoading(true); });
|
| 1512 |
+
video.addEventListener('seeking', () => { if (mediaMode === 'video') showStageLoading(true); });
|
| 1513 |
+
video.addEventListener('stalled', () => { if (mediaMode === 'video' && !video.paused) showStageLoading(true); });
|
| 1514 |
+
video.addEventListener('canplay', () => showStageLoading(false));
|
| 1515 |
+
video.addEventListener('canplaythrough', () => showStageLoading(false));
|
| 1516 |
+
video.addEventListener('playing', () => showStageLoading(false));
|
| 1517 |
+
video.addEventListener('seeked', () => showStageLoading(false));
|
| 1518 |
+
video.addEventListener('progress', () => { if (video.readyState >= 3) showStageLoading(false); });
|
| 1519 |
+
|
| 1520 |
+
$('#pb-expand').addEventListener('click', openNP);
|
| 1521 |
+
$('#np-close').addEventListener('click', closeNP);
|
| 1522 |
+
$('#np-shuffle').addEventListener('click', () => {
|
| 1523 |
+
state.shuffle = !state.shuffle;
|
| 1524 |
+
$('#pb-shuffle').classList.toggle('is-on', state.shuffle);
|
| 1525 |
+
$('#np-shuffle').classList.toggle('is-on', state.shuffle);
|
| 1526 |
+
});
|
| 1527 |
+
$('#np-loop').addEventListener('click', () => {
|
| 1528 |
+
state.loop = !state.loop;
|
| 1529 |
+
$('#pb-loop').classList.toggle('is-on', state.loop);
|
| 1530 |
+
$('#np-loop').classList.toggle('is-on', state.loop);
|
| 1531 |
+
});
|
| 1532 |
+
$('#np-prev').addEventListener('click', prev);
|
| 1533 |
+
$('#np-next').addEventListener('click', () => next(false));
|
| 1534 |
+
$('#np-play').addEventListener('click', togglePlay);
|
| 1535 |
+
$('#np-like').addEventListener('click', () => {
|
| 1536 |
+
const t = state.queue[state.index];
|
| 1537 |
+
if (!t) return;
|
| 1538 |
+
toggleLike(t);
|
| 1539 |
+
const btn = $('#np-like');
|
| 1540 |
+
if (btn) {
|
| 1541 |
+
btn.classList.remove('just-liked');
|
| 1542 |
+
|
| 1543 |
+
void btn.offsetWidth;
|
| 1544 |
+
btn.classList.add('just-liked');
|
| 1545 |
+
}
|
| 1546 |
+
});
|
| 1547 |
+
bindBar($('#np-seek'), (pct) => {
|
| 1548 |
+
const m = activeMedia();
|
| 1549 |
+
if (m.duration) m.currentTime = pct * m.duration;
|
| 1550 |
+
setSeek(pct * (m.duration || 0));
|
| 1551 |
+
});
|
| 1552 |
+
function setPanel(which) {
|
| 1553 |
+
|
| 1554 |
+
$('#np-lyrics').hidden = which !== 'lyrics';
|
| 1555 |
+
$('#np-queue').hidden = which !== 'queue';
|
| 1556 |
+
$('#np-lyrics-btn').classList.toggle('is-on', which === 'lyrics');
|
| 1557 |
+
$('#np-queue-btn').classList.toggle('is-on', which === 'queue');
|
| 1558 |
+
|
| 1559 |
+
document.body.classList.toggle('np-lyrics-open', which === 'lyrics');
|
| 1560 |
+
if (which === 'queue') renderQueue();
|
| 1561 |
+
|
| 1562 |
+
if (which === 'lyrics' && _syncedLines.length) {
|
| 1563 |
+
_activeLyricIdx = -2;
|
| 1564 |
+
requestAnimationFrame(() => {
|
| 1565 |
+
updateLyricsHighlight(activeMedia().currentTime || 0);
|
| 1566 |
+
});
|
| 1567 |
+
}
|
| 1568 |
+
}
|
| 1569 |
+
$('#np-video-btn').addEventListener('click', () => {
|
| 1570 |
+
const t = state.queue[state.index];
|
| 1571 |
+
if (!t) return;
|
| 1572 |
+
if (!t.videoLink) { toast('No video available for this track'); return; }
|
| 1573 |
+
setPanel(null);
|
| 1574 |
+
setVideoMode(mediaMode !== 'video');
|
| 1575 |
+
});
|
| 1576 |
+
$('#np-lyrics-btn').addEventListener('click', () => {
|
| 1577 |
+
const showing = $('#np-lyrics').hidden;
|
| 1578 |
+
setPanel(showing ? 'lyrics' : null);
|
| 1579 |
+
});
|
| 1580 |
+
$('#np-queue-btn').addEventListener('click', () => {
|
| 1581 |
+
const showing = $('#np-queue').hidden;
|
| 1582 |
+
setPanel(showing ? 'queue' : null);
|
| 1583 |
+
});
|
| 1584 |
+
|
| 1585 |
+
$('#np-lyrics-close')?.addEventListener('click', () => setPanel(null));
|
| 1586 |
+
$('#np-queue-close') ?.addEventListener('click', () => setPanel(null));
|
| 1587 |
+
|
| 1588 |
+
$('#np-bg').addEventListener('click', closeNP);
|
| 1589 |
+
document.addEventListener('keydown', (e) => {
|
| 1590 |
+
if (e.key === 'Escape' && !npModal.hidden) closeNP();
|
| 1591 |
+
});
|
| 1592 |
+
|
| 1593 |
+
$('#pb-title').addEventListener('click', () => {
|
| 1594 |
+
if (playerBar.dataset.empty === '1') return;
|
| 1595 |
+
openNP();
|
| 1596 |
+
});
|
| 1597 |
+
|
| 1598 |
+
$('#pb-dismiss')?.addEventListener('click', () => {
|
| 1599 |
+
try { audio.pause(); video.pause(); } catch (_) {}
|
| 1600 |
+
document.body.classList.add('pb-hidden');
|
| 1601 |
+
});
|
| 1602 |
+
|
| 1603 |
+
$('#ms-back').addEventListener('click', () => {
|
| 1604 |
+
if (state.navStack.length > 1) {
|
| 1605 |
+
const cur = state.navStack.pop();
|
| 1606 |
+
state.navFwd.push(cur);
|
| 1607 |
+
const prev = state.navStack[state.navStack.length - 1];
|
| 1608 |
+
state.navStack.pop();
|
| 1609 |
+
location.hash = prev;
|
| 1610 |
+
}
|
| 1611 |
+
});
|
| 1612 |
+
$('#ms-fwd').addEventListener('click', () => {
|
| 1613 |
+
const fwd = state.navFwd.pop();
|
| 1614 |
+
if (fwd) location.hash = fwd;
|
| 1615 |
+
});
|
| 1616 |
+
|
| 1617 |
+
$('#ms-sidebar-toggle').addEventListener('click', () => {
|
| 1618 |
+
const isDesktop = window.innerWidth > 1024;
|
| 1619 |
+
if (isDesktop) {
|
| 1620 |
+
$('#music-app').classList.toggle('sb-collapsed');
|
| 1621 |
+
} else {
|
| 1622 |
+
$('#music-sidebar').classList.toggle('open');
|
| 1623 |
+
$('#ms-sidebar-backdrop').classList.toggle('active');
|
| 1624 |
+
}
|
| 1625 |
+
});
|
| 1626 |
+
$('#ms-sidebar-backdrop').addEventListener('click', closeSidebarIfMobile);
|
| 1627 |
+
|
| 1628 |
+
const sidebarEl = $('#music-sidebar');
|
| 1629 |
+
if (sidebarEl) {
|
| 1630 |
+
let downTarget = null;
|
| 1631 |
+
let downX = 0, downY = 0;
|
| 1632 |
+
let pointerHandledAt = 0;
|
| 1633 |
+
const TAP_SLOP = 10;
|
| 1634 |
+
|
| 1635 |
+
sidebarEl.addEventListener('pointerdown', (e) => {
|
| 1636 |
+
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
| 1637 |
+
downTarget = e.target.closest('a, .ms-link, [data-route]');
|
| 1638 |
+
downX = e.clientX; downY = e.clientY;
|
| 1639 |
+
});
|
| 1640 |
+
|
| 1641 |
+
const fireTap = (e) => {
|
| 1642 |
+
const link = e.target.closest('a, .ms-link, [data-route]');
|
| 1643 |
+
if (!link || !sidebarEl.contains(link)) return false;
|
| 1644 |
+
|
| 1645 |
+
if (downTarget && downTarget !== link) return false;
|
| 1646 |
+
if (Math.abs(e.clientX - downX) > TAP_SLOP) return false;
|
| 1647 |
+
if (Math.abs(e.clientY - downY) > TAP_SLOP) return false;
|
| 1648 |
+
const route = link.dataset.route || link.getAttribute('href');
|
| 1649 |
+
if (route && route.startsWith('#')) {
|
| 1650 |
+
navigate(route);
|
| 1651 |
+
} else {
|
| 1652 |
+
closeSidebarIfMobile();
|
| 1653 |
+
}
|
| 1654 |
+
return true;
|
| 1655 |
+
};
|
| 1656 |
+
|
| 1657 |
+
sidebarEl.addEventListener('pointerup', (e) => {
|
| 1658 |
+
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
| 1659 |
+
if (fireTap(e)) pointerHandledAt = performance.now();
|
| 1660 |
+
downTarget = null;
|
| 1661 |
+
});
|
| 1662 |
+
|
| 1663 |
+
sidebarEl.addEventListener('click', (e) => {
|
| 1664 |
+
const link = e.target.closest('a, .ms-link, [data-route]');
|
| 1665 |
+
if (!link || !sidebarEl.contains(link)) return;
|
| 1666 |
+
e.preventDefault();
|
| 1667 |
+
if (performance.now() - pointerHandledAt < 600) return;
|
| 1668 |
+
const route = link.dataset.route || link.getAttribute('href');
|
| 1669 |
+
if (route && route.startsWith('#')) navigate(route);
|
| 1670 |
+
else closeSidebarIfMobile();
|
| 1671 |
+
});
|
| 1672 |
+
|
| 1673 |
+
sidebarEl.addEventListener('pointercancel', () => { downTarget = null; });
|
| 1674 |
+
}
|
| 1675 |
+
|
| 1676 |
+
document.body.classList.remove('header-hidden');
|
| 1677 |
+
const _hdr = document.getElementById('header');
|
| 1678 |
+
if (_hdr) _hdr.classList.remove('is-hidden');
|
| 1679 |
+
const musicMain = document.querySelector('.music-main');
|
| 1680 |
+
if (musicMain && _hdr) {
|
| 1681 |
+
let lastY = 0;
|
| 1682 |
+
let ticking = false;
|
| 1683 |
+
const SCROLL_THRESHOLD = 8;
|
| 1684 |
+
const apply = () => {
|
| 1685 |
+
ticking = false;
|
| 1686 |
+
if (window.innerWidth > 1024) {
|
| 1687 |
+
|
| 1688 |
+
_hdr.classList.remove('is-hidden');
|
| 1689 |
+
document.body.classList.remove('header-hidden');
|
| 1690 |
+
return;
|
| 1691 |
+
}
|
| 1692 |
+
const y = musicMain.scrollTop;
|
| 1693 |
+
const dy = y - lastY;
|
| 1694 |
+
if (Math.abs(dy) < SCROLL_THRESHOLD) return;
|
| 1695 |
+
if (dy > 0 && y > 40) {
|
| 1696 |
+
_hdr.classList.add('is-hidden');
|
| 1697 |
+
document.body.classList.add('header-hidden');
|
| 1698 |
+
} else if (dy < 0) {
|
| 1699 |
+
_hdr.classList.remove('is-hidden');
|
| 1700 |
+
document.body.classList.remove('header-hidden');
|
| 1701 |
+
}
|
| 1702 |
+
lastY = y;
|
| 1703 |
+
};
|
| 1704 |
+
musicMain.addEventListener('scroll', () => {
|
| 1705 |
+
if (ticking) return;
|
| 1706 |
+
ticking = true;
|
| 1707 |
+
requestAnimationFrame(apply);
|
| 1708 |
+
}, { passive: true });
|
| 1709 |
+
window.addEventListener('resize', apply);
|
| 1710 |
+
}
|
| 1711 |
+
|
| 1712 |
+
window.addEventListener('hashchange', handleRoute);
|
| 1713 |
+
|
| 1714 |
+
document.addEventListener('keydown', (e) => {
|
| 1715 |
+
if (e.target.matches('input, textarea')) return;
|
| 1716 |
+
if (e.code === 'Space') { e.preventDefault(); togglePlay(); }
|
| 1717 |
+
else if (e.key === 'ArrowRight' && e.shiftKey) next(false);
|
| 1718 |
+
else if (e.key === 'ArrowLeft' && e.shiftKey) prev();
|
| 1719 |
+
});
|
| 1720 |
+
|
| 1721 |
+
handleRoute();
|
| 1722 |
+
}
|
| 1723 |
+
|
| 1724 |
+
if (document.readyState === 'loading') {
|
| 1725 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 1726 |
+
} else {
|
| 1727 |
+
init();
|
| 1728 |
+
}
|
| 1729 |
+
})();
|
frontend/src/App.jsx
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect } from "react";
|
| 2 |
+
import { Route, Routes, useLocation } from "react-router-dom";
|
| 3 |
+
import Header from "./components/Header.jsx";
|
| 4 |
+
import MobileNav from "./components/MobileNav.jsx";
|
| 5 |
+
import ErrorBoundary from "./components/ErrorBoundary.jsx";
|
| 6 |
+
import Landing from "./pages/Landing.jsx";
|
| 7 |
+
import Home from "./pages/Home.jsx";
|
| 8 |
+
import AuthCallback from "./pages/AuthCallback.jsx";
|
| 9 |
+
import Search from "./pages/Search.jsx";
|
| 10 |
+
import Schedule from "./pages/Schedule.jsx";
|
| 11 |
+
import Anime from "./pages/Anime.jsx";
|
| 12 |
+
import Watch from "./pages/Watch.jsx";
|
| 13 |
+
import Manga from "./pages/Manga.jsx";
|
| 14 |
+
import MangaDetails from "./pages/MangaDetails.jsx";
|
| 15 |
+
import Reader from "./pages/Reader.jsx";
|
| 16 |
+
import Music from "./pages/Music.jsx";
|
| 17 |
+
import Movies from "./pages/Movies.jsx";
|
| 18 |
+
import MovieDetails from "./pages/MovieDetails.jsx";
|
| 19 |
+
import MovieWatch from "./pages/MovieWatch.jsx";
|
| 20 |
+
|
| 21 |
+
function ScrollToTop() {
|
| 22 |
+
const { pathname } = useLocation();
|
| 23 |
+
useEffect(() => {
|
| 24 |
+
window.scrollTo({ top: 0 });
|
| 25 |
+
}, [pathname]);
|
| 26 |
+
return null;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export default function App() {
|
| 30 |
+
return (
|
| 31 |
+
<>
|
| 32 |
+
<ScrollToTop />
|
| 33 |
+
<Header />
|
| 34 |
+
<MobileNav />
|
| 35 |
+
<main className="main-content">
|
| 36 |
+
<ErrorBoundary>
|
| 37 |
+
<Routes>
|
| 38 |
+
<Route path="/" element={<Landing />} />
|
| 39 |
+
<Route path="/home" element={<Home />} />
|
| 40 |
+
<Route path="/auth/callback" element={<AuthCallback />} />
|
| 41 |
+
<Route path="/search" element={<Search />} />
|
| 42 |
+
<Route path="/browse" element={<Search />} />
|
| 43 |
+
<Route path="/schedule" element={<Schedule />} />
|
| 44 |
+
<Route path="/anime/:id" element={<Anime />} />
|
| 45 |
+
<Route path="/watch/*" element={<Watch />} />
|
| 46 |
+
<Route path="/manga" element={<Manga />} />
|
| 47 |
+
<Route path="/manga/read" element={<Reader />} />
|
| 48 |
+
<Route path="/manga/:source/:id" element={<MangaDetails />} />
|
| 49 |
+
<Route path="/music" element={<Music />} />
|
| 50 |
+
<Route path="/movies" element={<Movies />} />
|
| 51 |
+
<Route path="/movies/:slug" element={<MovieDetails />} />
|
| 52 |
+
<Route path="/movies/:slug/watch" element={<MovieWatch />} />
|
| 53 |
+
<Route path="*" element={<Landing />} />
|
| 54 |
+
</Routes>
|
| 55 |
+
</ErrorBoundary>
|
| 56 |
+
</main>
|
| 57 |
+
</>
|
| 58 |
+
);
|
| 59 |
+
}
|
frontend/src/api.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const API_URL = (import.meta.env.VITE_API_URL || "/api").replace(/\/$/, "");
|
| 2 |
+
const STREAM_PROXY = (import.meta.env.VITE_STREAM_PROXY_URL || "/hls").replace(/\/$/, "");
|
| 3 |
+
export const STREAM_KEY = import.meta.env.VITE_STREAM_KEY || "";
|
| 4 |
+
import { getToken } from "./utils/auth.js";
|
| 5 |
+
|
| 6 |
+
async function req(path, options = {}) {
|
| 7 |
+
const res = await fetch(`${API_URL}${path}`, {
|
| 8 |
+
headers: { Accept: "application/json", ...(options.headers || {}) },
|
| 9 |
+
...options,
|
| 10 |
+
});
|
| 11 |
+
if (!res.ok) {
|
| 12 |
+
let detail = `HTTP ${res.status}`;
|
| 13 |
+
try {
|
| 14 |
+
const body = await res.json();
|
| 15 |
+
detail = body.detail?.error || body.detail?.hint || body.detail || detail;
|
| 16 |
+
// FastAPI validation errors are an array of { msg } objects — flatten them.
|
| 17 |
+
if (Array.isArray(detail)) detail = detail.map((d) => d.msg).join("; ");
|
| 18 |
+
} catch {
|
| 19 |
+
/* ignore */
|
| 20 |
+
}
|
| 21 |
+
const err = new Error(detail);
|
| 22 |
+
err.status = res.status; // lets pages distinguish 404 from server errors
|
| 23 |
+
throw err;
|
| 24 |
+
}
|
| 25 |
+
return res.json();
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export const api = {
|
| 29 |
+
// catalog
|
| 30 |
+
search: (q, page = 1, mal = false) =>
|
| 31 |
+
req(`/anime/search?q=${encodeURIComponent(q)}&page=${page}&mal=${mal ? 1 : 0}`),
|
| 32 |
+
trending: (page = 1) => req(`/anime/trending?page=${page}`),
|
| 33 |
+
popular: (page = 1) => req(`/anime/popular?page=${page}`),
|
| 34 |
+
upcoming: (page = 1) => req(`/anime/upcoming?page=${page}`),
|
| 35 |
+
recent: (page = 1) => req(`/anime/recent?page=${page}`),
|
| 36 |
+
schedule: (page = 1) => req(`/anime/schedule?page=${page}`),
|
| 37 |
+
details: (id) => req(`/anime/${id}`),
|
| 38 |
+
mal: (id) => req(`/anime/${id}/mal`),
|
| 39 |
+
// streaming
|
| 40 |
+
episodes: (id) => req(`/anime/${id}/episodes`),
|
| 41 |
+
watch: (episodeId) => req(`/watch/${episodeId}`),
|
| 42 |
+
// manga (backed by the MangaVault sidecar)
|
| 43 |
+
mangaHome: () => req(`/manga/home`),
|
| 44 |
+
mangaSearch: (q) => req(`/manga/search?q=${encodeURIComponent(q)}`),
|
| 45 |
+
mangaDetails: (source, id) => req(`/manga/${source}/${encodeURIComponent(id)}/details`),
|
| 46 |
+
mangaChapterImages: (path) => req(`/manga/chapter-images?path=${encodeURIComponent(path)}`),
|
| 47 |
+
mangaImg: (url, src) => `${API_URL}/manga/img?url=${encodeURIComponent(url)}&src=${src}`,
|
| 48 |
+
// auth — local accounts + AniList OAuth
|
| 49 |
+
authRegister: (username, email, password) =>
|
| 50 |
+
req("/auth/register", {
|
| 51 |
+
method: "POST",
|
| 52 |
+
headers: { "Content-Type": "application/json" },
|
| 53 |
+
body: JSON.stringify({ username, email, password }),
|
| 54 |
+
}),
|
| 55 |
+
authLogin: (identifier, password) =>
|
| 56 |
+
req("/auth/login", {
|
| 57 |
+
method: "POST",
|
| 58 |
+
headers: { "Content-Type": "application/json" },
|
| 59 |
+
body: JSON.stringify({ identifier, password }),
|
| 60 |
+
}),
|
| 61 |
+
authMe: () => req("/auth/me", { headers: { Authorization: `Bearer ${getToken()}` } }),
|
| 62 |
+
authLogout: () =>
|
| 63 |
+
req("/auth/logout", {
|
| 64 |
+
method: "POST",
|
| 65 |
+
headers: { Authorization: `Bearer ${getToken()}` },
|
| 66 |
+
}),
|
| 67 |
+
// movies & tv (backed by the Moviebox-API sidecar)
|
| 68 |
+
movieHome: () => req(`/movies/home`),
|
| 69 |
+
movieCatalog: (type = "movie", page = 1, sort = "RECOMMEND") =>
|
| 70 |
+
req(`/movies/catalog?type=${type}&page=${page}&sort=${sort}`),
|
| 71 |
+
movieSearch: (q, page = 1) =>
|
| 72 |
+
req(`/movies/search?q=${encodeURIComponent(q)}&page=${page}`),
|
| 73 |
+
movieSuggest: (q) => req(`/movies/suggest?q=${encodeURIComponent(q)}`),
|
| 74 |
+
movieDetails: (slug) => req(`/movies/${encodeURIComponent(slug)}`),
|
| 75 |
+
movieStream: (slug, se = 0, ep = 0) =>
|
| 76 |
+
req(`/movies/${encodeURIComponent(slug)}/stream?se=${se}&ep=${ep}`),
|
| 77 |
+
// images
|
| 78 |
+
img: (url) => (url ? `${API_URL}/img?url=${encodeURIComponent(url)}` : ""),
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
/** Wrap an upstream m3u8/segment URL so it flows through the HLS proxy worker.
|
| 82 |
+
* `ref` (optional) sets the Referer/Origin the worker sends upstream. */
|
| 83 |
+
export function proxiedStreamUrl(url, ref) {
|
| 84 |
+
let out = `${STREAM_PROXY}?url=${encodeURIComponent(url)}`;
|
| 85 |
+
if (ref) out += `&ref=${encodeURIComponent(ref)}`;
|
| 86 |
+
return out;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
export function streamHeaders() {
|
| 90 |
+
return STREAM_KEY ? { "x-stream-key": STREAM_KEY } : {};
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
/** Parse "watch/<provider>/<anilistId>/<category>/<ref>" → parts. */
|
| 94 |
+
export function parseEpisodeId(episodeId) {
|
| 95 |
+
const parts = (episodeId || "").split("/");
|
| 96 |
+
if (parts.length >= 4 && parts[0] === "watch") {
|
| 97 |
+
return {
|
| 98 |
+
provider: parts[1],
|
| 99 |
+
anilistId: Number(parts[2]),
|
| 100 |
+
category: parts[3],
|
| 101 |
+
ref: parts.slice(4).join("/"),
|
| 102 |
+
};
|
| 103 |
+
}
|
| 104 |
+
return null;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/** Pick the best title from a media object. */
|
| 108 |
+
export function titleOf(media) {
|
| 109 |
+
return (
|
| 110 |
+
media?.title?.english ||
|
| 111 |
+
media?.title?.romaji ||
|
| 112 |
+
media?.title?.native ||
|
| 113 |
+
"Untitled"
|
| 114 |
+
);
|
| 115 |
+
}
|
frontend/src/components/Ambient.jsx
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect } from "react";
|
| 2 |
+
|
| 3 |
+
const INTERACTIVES = 'a, button, .card, .nav-item, .try-btn, .copy-btn, label, [role="button"]';
|
| 4 |
+
const TILT_SELECTOR = ".card, .info-cell";
|
| 5 |
+
|
| 6 |
+
export default function Ambient() {
|
| 7 |
+
useEffect(() => {
|
| 8 |
+
/* ── custom cursor (dot + ring, lerped) ─────────────────────── */
|
| 9 |
+
const dot = document.querySelector(".cursor-dot");
|
| 10 |
+
const ring = document.querySelector(".cursor-ring");
|
| 11 |
+
let cursorCleanup = () => {};
|
| 12 |
+
|
| 13 |
+
if (dot && ring) {
|
| 14 |
+
let mx = -200, my = -200;
|
| 15 |
+
let rx = -200, ry = -200;
|
| 16 |
+
let visible = false;
|
| 17 |
+
let rafId = 0;
|
| 18 |
+
const LERP = 0.22;
|
| 19 |
+
|
| 20 |
+
const tick = () => {
|
| 21 |
+
rx += (mx - rx) * LERP;
|
| 22 |
+
ry += (my - ry) * LERP;
|
| 23 |
+
dot.style.transform = `translate(${mx}px,${my}px)`;
|
| 24 |
+
ring.style.transform = `translate(${Math.round(rx * 10) / 10}px,${Math.round(ry * 10) / 10}px)`;
|
| 25 |
+
rafId = requestAnimationFrame(tick);
|
| 26 |
+
};
|
| 27 |
+
rafId = requestAnimationFrame(tick);
|
| 28 |
+
|
| 29 |
+
const onMove = (e) => {
|
| 30 |
+
mx = e.clientX;
|
| 31 |
+
my = e.clientY;
|
| 32 |
+
if (!visible) {
|
| 33 |
+
visible = true;
|
| 34 |
+
dot.classList.remove("is-hidden");
|
| 35 |
+
ring.classList.remove("is-hidden");
|
| 36 |
+
rx = mx; ry = my;
|
| 37 |
+
}
|
| 38 |
+
};
|
| 39 |
+
const onLeave = () => {
|
| 40 |
+
dot.classList.add("is-hidden");
|
| 41 |
+
ring.classList.add("is-hidden");
|
| 42 |
+
visible = false;
|
| 43 |
+
};
|
| 44 |
+
const onEnter = () => {
|
| 45 |
+
dot.classList.remove("is-hidden");
|
| 46 |
+
ring.classList.remove("is-hidden");
|
| 47 |
+
visible = true;
|
| 48 |
+
};
|
| 49 |
+
const onDown = () => {
|
| 50 |
+
ring.classList.add("is-clicking");
|
| 51 |
+
ring.classList.remove("is-hovering");
|
| 52 |
+
};
|
| 53 |
+
const onUp = () => ring.classList.remove("is-clicking");
|
| 54 |
+
const onOver = (e) => {
|
| 55 |
+
if (e.target.closest(INTERACTIVES)) {
|
| 56 |
+
dot.classList.add("is-hovering");
|
| 57 |
+
ring.classList.add("is-hovering");
|
| 58 |
+
}
|
| 59 |
+
};
|
| 60 |
+
const onOut = (e) => {
|
| 61 |
+
if (e.target.closest(INTERACTIVES)) {
|
| 62 |
+
dot.classList.remove("is-hovering");
|
| 63 |
+
ring.classList.remove("is-hovering");
|
| 64 |
+
}
|
| 65 |
+
};
|
| 66 |
+
const onTouch = () => {
|
| 67 |
+
dot.style.display = "none";
|
| 68 |
+
ring.style.display = "none";
|
| 69 |
+
document.body.style.cursor = "";
|
| 70 |
+
};
|
| 71 |
+
|
| 72 |
+
document.addEventListener("mousemove", onMove, { passive: true });
|
| 73 |
+
document.addEventListener("mouseleave", onLeave, { passive: true });
|
| 74 |
+
document.addEventListener("mouseenter", onEnter, { passive: true });
|
| 75 |
+
document.addEventListener("mousedown", onDown, { passive: true });
|
| 76 |
+
document.addEventListener("mouseup", onUp, { passive: true });
|
| 77 |
+
document.addEventListener("mouseover", onOver, { passive: true });
|
| 78 |
+
document.addEventListener("mouseout", onOut, { passive: true });
|
| 79 |
+
window.addEventListener("touchstart", onTouch, { once: true, passive: true });
|
| 80 |
+
|
| 81 |
+
cursorCleanup = () => {
|
| 82 |
+
cancelAnimationFrame(rafId);
|
| 83 |
+
document.removeEventListener("mousemove", onMove);
|
| 84 |
+
document.removeEventListener("mouseleave", onLeave);
|
| 85 |
+
document.removeEventListener("mouseenter", onEnter);
|
| 86 |
+
document.removeEventListener("mousedown", onDown);
|
| 87 |
+
document.removeEventListener("mouseup", onUp);
|
| 88 |
+
document.removeEventListener("mouseover", onOver);
|
| 89 |
+
document.removeEventListener("mouseout", onOut);
|
| 90 |
+
window.removeEventListener("touchstart", onTouch);
|
| 91 |
+
};
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/* ── reveal-on-scroll ───────────────────────────────────────── */
|
| 95 |
+
const revealObs = new IntersectionObserver(
|
| 96 |
+
(entries) => {
|
| 97 |
+
entries.forEach((e) => {
|
| 98 |
+
if (e.isIntersecting) {
|
| 99 |
+
e.target.classList.add("visible");
|
| 100 |
+
revealObs.unobserve(e.target);
|
| 101 |
+
}
|
| 102 |
+
});
|
| 103 |
+
},
|
| 104 |
+
{ threshold: 0.07, rootMargin: "0px 0px -40px 0px" }
|
| 105 |
+
);
|
| 106 |
+
const observeNew = () => {
|
| 107 |
+
document.querySelectorAll(".section-reveal:not(.observed)").forEach((el) => {
|
| 108 |
+
el.classList.add("observed");
|
| 109 |
+
revealObs.observe(el);
|
| 110 |
+
});
|
| 111 |
+
};
|
| 112 |
+
observeNew();
|
| 113 |
+
const mo = new MutationObserver(observeNew);
|
| 114 |
+
mo.observe(document.body, { childList: true, subtree: true });
|
| 115 |
+
|
| 116 |
+
/* ── 3D tilt delegation (dynamic-friendly) ──────────────────── */
|
| 117 |
+
let tiltRaf = null;
|
| 118 |
+
const onTilt = (e) => {
|
| 119 |
+
const el = e.target.closest(TILT_SELECTOR);
|
| 120 |
+
if (!el) return;
|
| 121 |
+
if (tiltRaf) return;
|
| 122 |
+
tiltRaf = requestAnimationFrame(() => {
|
| 123 |
+
const r = el.getBoundingClientRect();
|
| 124 |
+
const x = (e.clientX - r.left) / r.width - 0.5;
|
| 125 |
+
const y = (e.clientY - r.top) / r.height - 0.5;
|
| 126 |
+
const maxDeg = el.classList.contains("info-cell") ? 5 : 6;
|
| 127 |
+
el.style.setProperty("--rx", `${(-y * maxDeg).toFixed(2)}deg`);
|
| 128 |
+
el.style.setProperty("--ry", `${(x * maxDeg).toFixed(2)}deg`);
|
| 129 |
+
tiltRaf = null;
|
| 130 |
+
});
|
| 131 |
+
};
|
| 132 |
+
const onTiltOut = (e) => {
|
| 133 |
+
const el = e.target.closest(TILT_SELECTOR);
|
| 134 |
+
if (!el) return;
|
| 135 |
+
// mouseout bubbles — ignore moves that stay inside the element
|
| 136 |
+
// (e.g. poster img → title), only reset when actually leaving it.
|
| 137 |
+
if (e.relatedTarget && el.contains(e.relatedTarget)) return;
|
| 138 |
+
el.style.setProperty("--rx", "0deg");
|
| 139 |
+
el.style.setProperty("--ry", "0deg");
|
| 140 |
+
};
|
| 141 |
+
document.addEventListener("mousemove", onTilt, { passive: true });
|
| 142 |
+
document.addEventListener("mouseout", onTiltOut, { passive: true });
|
| 143 |
+
|
| 144 |
+
return () => {
|
| 145 |
+
cursorCleanup();
|
| 146 |
+
revealObs.disconnect();
|
| 147 |
+
mo.disconnect();
|
| 148 |
+
document.removeEventListener("mousemove", onTilt);
|
| 149 |
+
document.removeEventListener("mouseout", onTiltOut);
|
| 150 |
+
};
|
| 151 |
+
}, []);
|
| 152 |
+
|
| 153 |
+
return (
|
| 154 |
+
<>
|
| 155 |
+
<div className="cursor-dot" aria-hidden="true" />
|
| 156 |
+
<div className="cursor-ring" aria-hidden="true" />
|
| 157 |
+
</>
|
| 158 |
+
);
|
| 159 |
+
}
|
frontend/src/components/AnimeCard.jsx
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useNavigate } from "react-router-dom";
|
| 2 |
+
import { api, titleOf } from "../api.js";
|
| 3 |
+
|
| 4 |
+
export default function AnimeCard({ item, rank, isEpisode }) {
|
| 5 |
+
const navigate = useNavigate();
|
| 6 |
+
|
| 7 |
+
if (!item) return null;
|
| 8 |
+
|
| 9 |
+
const title = titleOf(item);
|
| 10 |
+
const poster = item.coverImage || item.image || item.poster;
|
| 11 |
+
const score = item.score ? (item.score > 10 ? (item.score / 10).toFixed(1) : item.score) : null;
|
| 12 |
+
const targetUrl = isEpisode ? `/watch/${item.id}` : `/anime/${item.id}`;
|
| 13 |
+
|
| 14 |
+
return (
|
| 15 |
+
<div className="anime-card" onClick={() => navigate(targetUrl)}>
|
| 16 |
+
<div className="card-poster">
|
| 17 |
+
{poster ? (
|
| 18 |
+
<img src={api.img(poster)} alt={title} loading="lazy" />
|
| 19 |
+
) : (
|
| 20 |
+
<div className="skeleton" style={{ width: "100%", height: "100%" }} />
|
| 21 |
+
)}
|
| 22 |
+
<div className="card-overlay" />
|
| 23 |
+
{rank ? <div className="card-rank">{rank}</div> : null}
|
| 24 |
+
{item.episodes ? (
|
| 25 |
+
<div className="card-ep-badge">EP {item.episodes}</div>
|
| 26 |
+
) : item.type ? (
|
| 27 |
+
<div className="card-type-badge">{item.type}</div>
|
| 28 |
+
) : null}
|
| 29 |
+
<div className="card-quick">
|
| 30 |
+
<div className="card-quick-btn">
|
| 31 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
| 32 |
+
<polygon points="5 3 19 12 5 21 5 3" />
|
| 33 |
+
</svg>
|
| 34 |
+
Watch Now
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
<div className="card-body">
|
| 39 |
+
<div className="card-title" title={title}>{title}</div>
|
| 40 |
+
<div className="card-meta">
|
| 41 |
+
{score ? (
|
| 42 |
+
<span className="card-score">
|
| 43 |
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor">
|
| 44 |
+
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
|
| 45 |
+
</svg>
|
| 46 |
+
{score}
|
| 47 |
+
</span>
|
| 48 |
+
) : null}
|
| 49 |
+
{item.format ? <span className="card-meta-item">• {item.format}</span> : null}
|
| 50 |
+
{item.seasonYear ? <span className="card-meta-item">• {item.seasonYear}</span> : null}
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
);
|
| 55 |
+
}
|
frontend/src/components/AnimeRow.jsx
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useRef } from "react";
|
| 2 |
+
import AnimeCard from "./AnimeCard.jsx";
|
| 3 |
+
|
| 4 |
+
export default function AnimeRow({ title, items = [], isRanked = false, viewAllUrl = null }) {
|
| 5 |
+
const rowRef = useRef(null);
|
| 6 |
+
|
| 7 |
+
function scroll(dir) {
|
| 8 |
+
if (!rowRef.current) return;
|
| 9 |
+
const scrollAmount = dir === "left" ? -600 : 600;
|
| 10 |
+
rowRef.current.scrollBy({ left: scrollAmount, behavior: "smooth" });
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
if (!items || items.length === 0) return null;
|
| 14 |
+
|
| 15 |
+
return (
|
| 16 |
+
<section className="content-section">
|
| 17 |
+
<div className="container">
|
| 18 |
+
<div className="section-header">
|
| 19 |
+
<div className="section-title">{title}</div>
|
| 20 |
+
{viewAllUrl ? (
|
| 21 |
+
<a href={viewAllUrl} className="section-link">View All</a>
|
| 22 |
+
) : null}
|
| 23 |
+
</div>
|
| 24 |
+
<div className="genre-nav-wrap" style={{ position: "relative" }}>
|
| 25 |
+
<button className="row-nav-btn prev" onClick={() => scroll("left")} aria-label="Scroll left">
|
| 26 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
| 27 |
+
<path d="M15 18l-6-6 6-6" />
|
| 28 |
+
</svg>
|
| 29 |
+
</button>
|
| 30 |
+
<div className="scroll-row" ref={rowRef}>
|
| 31 |
+
{items.map((item, index) => (
|
| 32 |
+
<AnimeCard key={item.id || index} item={item} rank={isRanked ? index + 1 : null} />
|
| 33 |
+
))}
|
| 34 |
+
</div>
|
| 35 |
+
<button className="row-nav-btn next" onClick={() => scroll("right")} aria-label="Scroll right">
|
| 36 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
| 37 |
+
<path d="M9 18l6-6-6-6" />
|
| 38 |
+
</svg>
|
| 39 |
+
</button>
|
| 40 |
+
</div>
|
| 41 |
+
</div>
|
| 42 |
+
</section>
|
| 43 |
+
);
|
| 44 |
+
}
|
frontend/src/components/AuthModal.jsx
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from "react";
|
| 2 |
+
import { api } from "../api.js";
|
| 3 |
+
import { setSession } from "../utils/auth.js";
|
| 4 |
+
|
| 5 |
+
const SQLI_RE = /(\b(select|insert|update|delete|drop|union|exec|cast|convert|declare|xp_|char|nchar|varchar|alter|create|truncate|sleep|benchmark|waitfor|information_schema)\b|--|;|\/\*|\*\/|0x[0-9a-fA-F]+|\bor\b\s+.{0,30}[=<>]|\band\b\s+.{0,30}[=<>])/i;
|
| 6 |
+
function hasSQLI(val) { return SQLI_RE.test(val); }
|
| 7 |
+
|
| 8 |
+
function getPasswordStrength(pw) {
|
| 9 |
+
const score = [/.{8,}/, /[A-Z]/, /[a-z]/, /[0-9]/, /[^A-Za-z0-9]/].filter(r => r.test(pw)).length;
|
| 10 |
+
const widths = ['0%', '25%', '45%', '70%', '100%'];
|
| 11 |
+
const colors = ['#ef4444', '#f97316', '#eab308', '#22c55e', '#7c3aed'];
|
| 12 |
+
return { width: widths[score] || '0%', color: colors[score - 1] || '#334155' };
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export default function AuthModal({ open, onClose, onAuthed }) {
|
| 16 |
+
const [mode, setMode] = useState("login"); // "login" | "register"
|
| 17 |
+
const [identifier, setIdentifier] = useState("");
|
| 18 |
+
const [username, setUsername] = useState("");
|
| 19 |
+
const [email, setEmail] = useState("");
|
| 20 |
+
const [password, setPassword] = useState("");
|
| 21 |
+
const [error, setError] = useState("");
|
| 22 |
+
const [busy, setBusy] = useState(false);
|
| 23 |
+
const [pwStrength, setPwStrength] = useState({ width: '0%', color: '#334155' });
|
| 24 |
+
|
| 25 |
+
// Reset state + close on Escape.
|
| 26 |
+
useEffect(() => {
|
| 27 |
+
if (!open) return;
|
| 28 |
+
setError("");
|
| 29 |
+
setBusy(false);
|
| 30 |
+
setPassword("");
|
| 31 |
+
const onKey = (e) => e.key === "Escape" && onClose();
|
| 32 |
+
window.addEventListener("keydown", onKey);
|
| 33 |
+
return () => window.removeEventListener("keydown", onKey);
|
| 34 |
+
}, [open, onClose]);
|
| 35 |
+
|
| 36 |
+
if (!open) return null;
|
| 37 |
+
|
| 38 |
+
async function handleSubmit(e) {
|
| 39 |
+
e.preventDefault();
|
| 40 |
+
setError("");
|
| 41 |
+
if (mode === "login") {
|
| 42 |
+
if (!identifier.trim() || !password) {
|
| 43 |
+
setError("Enter your username/email and password.");
|
| 44 |
+
return;
|
| 45 |
+
}
|
| 46 |
+
if (hasSQLI(identifier) || hasSQLI(password)) {
|
| 47 |
+
setError("Invalid input detected.");
|
| 48 |
+
return;
|
| 49 |
+
}
|
| 50 |
+
} else {
|
| 51 |
+
if (username.trim().length < 3) {
|
| 52 |
+
setError("Username must be at least 3 characters.");
|
| 53 |
+
return;
|
| 54 |
+
}
|
| 55 |
+
if (username.trim().length > 15) {
|
| 56 |
+
setError("Username must be 15 characters or less.");
|
| 57 |
+
return;
|
| 58 |
+
}
|
| 59 |
+
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email.trim())) {
|
| 60 |
+
setError("Enter a valid email address.");
|
| 61 |
+
return;
|
| 62 |
+
}
|
| 63 |
+
if (email.trim().length > 20) {
|
| 64 |
+
setError("Email must be 20 characters or less.");
|
| 65 |
+
return;
|
| 66 |
+
}
|
| 67 |
+
if (password.length < 6) {
|
| 68 |
+
setError("Password must be at least 6 characters.");
|
| 69 |
+
return;
|
| 70 |
+
}
|
| 71 |
+
if (hasSQLI(username) || hasSQLI(email) || hasSQLI(password)) {
|
| 72 |
+
setError("Invalid input detected.");
|
| 73 |
+
return;
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
setBusy(true);
|
| 77 |
+
try {
|
| 78 |
+
const res =
|
| 79 |
+
mode === "login"
|
| 80 |
+
? await api.authLogin(identifier.trim(), password)
|
| 81 |
+
: await api.authRegister(username.trim(), email.trim(), password);
|
| 82 |
+
setSession(res.token, res.user);
|
| 83 |
+
onAuthed?.(res.user);
|
| 84 |
+
onClose();
|
| 85 |
+
} catch (err) {
|
| 86 |
+
setError(err.message || "Something went wrong. Try again.");
|
| 87 |
+
} finally {
|
| 88 |
+
setBusy(false);
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
return (
|
| 93 |
+
<div className="auth-overlay" onClick={onClose}>
|
| 94 |
+
<div className="auth-modal" onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true">
|
| 95 |
+
<button className="auth-modal-close" onClick={onClose} aria-label="Close">
|
| 96 |
+
✕
|
| 97 |
+
</button>
|
| 98 |
+
|
| 99 |
+
<div className="auth-modal-logo"><span className="auth-modal-logo-text">ANICOVE</span></div>
|
| 100 |
+
|
| 101 |
+
<div className="auth-tabs">
|
| 102 |
+
<button
|
| 103 |
+
className={`auth-tab${mode === "login" ? " active" : ""}`}
|
| 104 |
+
onClick={() => { setMode("login"); setError(""); }}
|
| 105 |
+
>
|
| 106 |
+
Sign in
|
| 107 |
+
</button>
|
| 108 |
+
<button
|
| 109 |
+
className={`auth-tab${mode === "register" ? " active" : ""}`}
|
| 110 |
+
onClick={() => { setMode("register"); setError(""); }}
|
| 111 |
+
>
|
| 112 |
+
Create account
|
| 113 |
+
</button>
|
| 114 |
+
</div>
|
| 115 |
+
|
| 116 |
+
<form className="auth-form" onSubmit={handleSubmit}>
|
| 117 |
+
{mode === "login" ? (
|
| 118 |
+
<label className="auth-field">
|
| 119 |
+
<span>Username or email</span>
|
| 120 |
+
<input
|
| 121 |
+
type="text"
|
| 122 |
+
value={identifier}
|
| 123 |
+
onChange={(e) => setIdentifier(e.target.value)}
|
| 124 |
+
placeholder="username@example.com"
|
| 125 |
+
autoComplete="username"
|
| 126 |
+
autoFocus
|
| 127 |
+
/>
|
| 128 |
+
</label>
|
| 129 |
+
) : (
|
| 130 |
+
<>
|
| 131 |
+
<label className="auth-field">
|
| 132 |
+
<span>Username</span>
|
| 133 |
+
<input
|
| 134 |
+
type="text"
|
| 135 |
+
value={username}
|
| 136 |
+
onChange={(e) => setUsername(e.target.value)}
|
| 137 |
+
placeholder="Your username"
|
| 138 |
+
autoComplete="username"
|
| 139 |
+
autoFocus
|
| 140 |
+
/>
|
| 141 |
+
</label>
|
| 142 |
+
<label className="auth-field">
|
| 143 |
+
<span>Email</span>
|
| 144 |
+
<input
|
| 145 |
+
type="email"
|
| 146 |
+
value={email}
|
| 147 |
+
onChange={(e) => setEmail(e.target.value)}
|
| 148 |
+
placeholder="you@example.com"
|
| 149 |
+
autoComplete="email"
|
| 150 |
+
/>
|
| 151 |
+
</label>
|
| 152 |
+
</>
|
| 153 |
+
)}
|
| 154 |
+
|
| 155 |
+
<label className="auth-field">
|
| 156 |
+
<span>Password {mode === "register" && <span style={{color:'#475569',fontWeight:400,textTransform:'none',letterSpacing:0}}>(min 6 characters)</span>}</span>
|
| 157 |
+
<input
|
| 158 |
+
type="password"
|
| 159 |
+
value={password}
|
| 160 |
+
onChange={(e) => {
|
| 161 |
+
setPassword(e.target.value);
|
| 162 |
+
if (mode === "register") setPwStrength(getPasswordStrength(e.target.value));
|
| 163 |
+
}}
|
| 164 |
+
placeholder="••••••••"
|
| 165 |
+
autoComplete={mode === "login" ? "current-password" : "new-password"}
|
| 166 |
+
maxLength={18}
|
| 167 |
+
/>
|
| 168 |
+
{mode === "register" && (
|
| 169 |
+
<div className="auth-pw-bar"><div className="auth-pw-fill" style={{width: pwStrength.width, background: pwStrength.color}}></div></div>
|
| 170 |
+
)}
|
| 171 |
+
</label>
|
| 172 |
+
|
| 173 |
+
{error ? <div className="auth-error show">{error}</div> : null}
|
| 174 |
+
|
| 175 |
+
<button type="submit" className="btn btn-primary auth-submit" disabled={busy}>
|
| 176 |
+
<span className="auth-submit-text">{busy ? "Please wait…" : mode === "login" ? "Sign in" : "Create account"}</span>
|
| 177 |
+
<span className="auth-submit-spinner"></span>
|
| 178 |
+
</button>
|
| 179 |
+
</form>
|
| 180 |
+
|
| 181 |
+
<div className="auth-divider"><span>OR</span></div>
|
| 182 |
+
|
| 183 |
+
<button
|
| 184 |
+
className="auth-anilist-btn"
|
| 185 |
+
onClick={() => { window.location.href = "/api/auth/anilist/login"; }}
|
| 186 |
+
title="Sign in with your AniList account"
|
| 187 |
+
>
|
| 188 |
+
<img src="https://anilist.co/img/icons/icon.svg" style={{width:20,height:20}} alt="AniList" />
|
| 189 |
+
<span>Continue with AniList</span>
|
| 190 |
+
</button>
|
| 191 |
+
|
| 192 |
+
{mode === "login" ? (
|
| 193 |
+
<p className="auth-switch">No account? <a onClick={() => { setMode("register"); setError(""); }}>Create one</a></p>
|
| 194 |
+
) : (
|
| 195 |
+
<p className="auth-switch">Already have an account? <a onClick={() => { setMode("login"); setError(""); }}>Sign in</a></p>
|
| 196 |
+
)}
|
| 197 |
+
|
| 198 |
+
<div className="auth-recaptcha-terms">
|
| 199 |
+
This site is protected by reCAPTCHA and the Google
|
| 200 |
+
<a href="https://policies.google.com/privacy" target="_blank" rel="noopener noreferrer">Privacy Policy</a> and
|
| 201 |
+
<a href="https://policies.google.com/terms" target="_blank" rel="noopener noreferrer">Terms of Service</a> apply.
|
| 202 |
+
</div>
|
| 203 |
+
</div>
|
| 204 |
+
</div>
|
| 205 |
+
);
|
| 206 |
+
}
|
frontend/src/components/ContinueWatching.jsx
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useRef, useState } from "react";
|
| 2 |
+
import { Link } from "react-router-dom";
|
| 3 |
+
import { api } from "../api.js";
|
| 4 |
+
import { getContinueWatching, removeProgress } from "../utils/watchProgress.js";
|
| 5 |
+
|
| 6 |
+
function timeAgo(ts) {
|
| 7 |
+
if (!ts) return "";
|
| 8 |
+
const s = Math.max(1, Math.floor((Date.now() - ts) / 1000));
|
| 9 |
+
if (s < 60) return "just now";
|
| 10 |
+
const m = Math.floor(s / 60);
|
| 11 |
+
if (m < 60) return `${m}m ago`;
|
| 12 |
+
const h = Math.floor(m / 60);
|
| 13 |
+
if (h < 24) return `${h}h ago`;
|
| 14 |
+
const d = Math.floor(h / 24);
|
| 15 |
+
if (d < 30) return `${d}d ago`;
|
| 16 |
+
return new Date(ts).toLocaleDateString();
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
function timeLeft(position, duration) {
|
| 20 |
+
const left = Math.max(0, (duration || 0) - (position || 0));
|
| 21 |
+
const m = Math.floor(left / 60);
|
| 22 |
+
const s = Math.floor(left % 60);
|
| 23 |
+
return `${m}:${String(s).padStart(2, "0")}`;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export default function ContinueWatching() {
|
| 27 |
+
const [items, setItems] = useState([]);
|
| 28 |
+
const rowRef = useRef(null);
|
| 29 |
+
|
| 30 |
+
function refresh() {
|
| 31 |
+
setItems(getContinueWatching());
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
useEffect(() => {
|
| 35 |
+
refresh();
|
| 36 |
+
window.addEventListener("storage", refresh);
|
| 37 |
+
return () => window.removeEventListener("storage", refresh);
|
| 38 |
+
}, []);
|
| 39 |
+
|
| 40 |
+
function handleRemove(key, e) {
|
| 41 |
+
e.preventDefault();
|
| 42 |
+
e.stopPropagation();
|
| 43 |
+
removeProgress(key);
|
| 44 |
+
refresh();
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
function scroll(dir) {
|
| 48 |
+
if (!rowRef.current) return;
|
| 49 |
+
rowRef.current.scrollBy({ left: dir === "left" ? -600 : 600, behavior: "smooth" });
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
if (items.length === 0) return null;
|
| 53 |
+
|
| 54 |
+
return (
|
| 55 |
+
<section className="content-section">
|
| 56 |
+
<div className="container">
|
| 57 |
+
<div className="section-header">
|
| 58 |
+
<div className="section-title">Continue Watching</div>
|
| 59 |
+
<span className="cw-subtitle">
|
| 60 |
+
{items.length} {items.length === 1 ? "title" : "titles"} in progress
|
| 61 |
+
</span>
|
| 62 |
+
</div>
|
| 63 |
+
<div className="genre-nav-wrap">
|
| 64 |
+
<button className="row-nav-btn prev" onClick={() => scroll("left")} aria-label="Scroll left">
|
| 65 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
| 66 |
+
<path d="M15 18l-6-6 6-6" />
|
| 67 |
+
</svg>
|
| 68 |
+
</button>
|
| 69 |
+
<div className="scroll-row cw-row" ref={rowRef}>
|
| 70 |
+
{items.map((item) => {
|
| 71 |
+
const pct = item.duration ? Math.min(100, (item.position / item.duration) * 100) : 0;
|
| 72 |
+
return (
|
| 73 |
+
<Link key={item.key} to={item.href} className="cw-card" title={item.title}>
|
| 74 |
+
<div className="cw-poster">
|
| 75 |
+
{item.image ? (
|
| 76 |
+
<img src={api.img(item.image)} alt={item.title} loading="lazy" />
|
| 77 |
+
) : (
|
| 78 |
+
<div className="skeleton" style={{ width: "100%", height: "100%" }} />
|
| 79 |
+
)}
|
| 80 |
+
<span className="cw-badge">{item.label}</span>
|
| 81 |
+
<div className="cw-overlay">
|
| 82 |
+
<svg viewBox="0 0 24 24" fill="currentColor">
|
| 83 |
+
<path d="M8 5v14l11-7z" />
|
| 84 |
+
</svg>
|
| 85 |
+
<span>Resume</span>
|
| 86 |
+
</div>
|
| 87 |
+
<div className="cw-progress">
|
| 88 |
+
<div className="cw-progress-fill" style={{ width: `${pct}%` }} />
|
| 89 |
+
</div>
|
| 90 |
+
<button
|
| 91 |
+
className="cw-remove"
|
| 92 |
+
onClick={(e) => handleRemove(item.key, e)}
|
| 93 |
+
aria-label="Remove from Continue Watching"
|
| 94 |
+
title="Remove"
|
| 95 |
+
>
|
| 96 |
+
×
|
| 97 |
+
</button>
|
| 98 |
+
</div>
|
| 99 |
+
<div className="cw-body">
|
| 100 |
+
<h3>{item.title}</h3>
|
| 101 |
+
<div className="cw-meta">
|
| 102 |
+
<span className="cw-meta-left">
|
| 103 |
+
{item.category ? item.category.toUpperCase() : null}
|
| 104 |
+
{item.category ? " · " : ""}
|
| 105 |
+
{timeAgo(item.updatedAt)}
|
| 106 |
+
</span>
|
| 107 |
+
<span className="cw-left">⏱ {timeLeft(item.position, item.duration)} left</span>
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
</Link>
|
| 111 |
+
);
|
| 112 |
+
})}
|
| 113 |
+
</div>
|
| 114 |
+
<button className="row-nav-btn next" onClick={() => scroll("right")} aria-label="Scroll right">
|
| 115 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
| 116 |
+
<path d="M9 18l6-6-6-6" />
|
| 117 |
+
</svg>
|
| 118 |
+
</button>
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
</section>
|
| 122 |
+
);
|
| 123 |
+
}
|
frontend/src/components/ErrorBoundary.jsx
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Component } from "react";
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Catches render/effect errors so a single crashing component (e.g. the
|
| 5 |
+
* player) can never unmount the whole app and leave a black screen.
|
| 6 |
+
*/
|
| 7 |
+
export default class ErrorBoundary extends Component {
|
| 8 |
+
constructor(props) {
|
| 9 |
+
super(props);
|
| 10 |
+
this.state = { error: null };
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
static getDerivedStateFromError(error) {
|
| 14 |
+
return { error };
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
componentDidCatch(error, info) {
|
| 18 |
+
console.error("[ErrorBoundary]", error, info);
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
render() {
|
| 22 |
+
if (this.state.error) {
|
| 23 |
+
const message = String(
|
| 24 |
+
this.state.error?.message || this.state.error || "Unknown error"
|
| 25 |
+
).slice(0, 240);
|
| 26 |
+
return (
|
| 27 |
+
<div
|
| 28 |
+
className="player-error"
|
| 29 |
+
style={{
|
| 30 |
+
position: "static",
|
| 31 |
+
minHeight: 220,
|
| 32 |
+
margin: "24px auto",
|
| 33 |
+
maxWidth: 560,
|
| 34 |
+
}}
|
| 35 |
+
>
|
| 36 |
+
<div className="big">⚠️</div>
|
| 37 |
+
<p>Something went wrong rendering this page.</p>
|
| 38 |
+
{message ? <p style={{ fontSize: "0.8rem", opacity: 0.7 }}>{message}</p> : null}
|
| 39 |
+
<button
|
| 40 |
+
className="btn btn-primary btn-sm"
|
| 41 |
+
style={{ marginTop: 14 }}
|
| 42 |
+
onClick={() => this.setState({ error: null })}
|
| 43 |
+
>
|
| 44 |
+
Try again
|
| 45 |
+
</button>
|
| 46 |
+
</div>
|
| 47 |
+
);
|
| 48 |
+
}
|
| 49 |
+
return this.props.children;
|
| 50 |
+
}
|
| 51 |
+
}
|
frontend/src/components/Header.jsx
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from "react";
|
| 2 |
+
import { Link, useLocation, useNavigate } from "react-router-dom";
|
| 3 |
+
import { api } from "../api.js";
|
| 4 |
+
import { clearSession, getToken, getUser, setSession } from "../utils/auth.js";
|
| 5 |
+
import AuthModal from "./AuthModal.jsx";
|
| 6 |
+
|
| 7 |
+
const NAV = [
|
| 8 |
+
{ to: "/home", label: "Home" },
|
| 9 |
+
{ to: "/browse", label: "Browse" },
|
| 10 |
+
{ to: "/schedule", label: "Schedule" },
|
| 11 |
+
{ to: "/movies", label: "Movies & TV" },
|
| 12 |
+
{ to: "/manga", label: "Manga" },
|
| 13 |
+
{ to: "/music", label: "Music" },
|
| 14 |
+
];
|
| 15 |
+
|
| 16 |
+
export default function Header() {
|
| 17 |
+
const [scrolled, setScrolled] = useState(false);
|
| 18 |
+
const [q, setQ] = useState("");
|
| 19 |
+
const [user, setUser] = useState(() => getUser());
|
| 20 |
+
const [menuOpen, setMenuOpen] = useState(false);
|
| 21 |
+
const [modalOpen, setModalOpen] = useState(false);
|
| 22 |
+
const location = useLocation();
|
| 23 |
+
const navigate = useNavigate();
|
| 24 |
+
|
| 25 |
+
useEffect(() => {
|
| 26 |
+
const fn = () => setScrolled(window.scrollY > 8);
|
| 27 |
+
window.addEventListener("scroll", fn, { passive: true });
|
| 28 |
+
return () => window.removeEventListener("scroll", fn);
|
| 29 |
+
}, []);
|
| 30 |
+
|
| 31 |
+
// Hydrate a stored token into the full user profile on load.
|
| 32 |
+
useEffect(() => {
|
| 33 |
+
if (getToken() && !getUser()) {
|
| 34 |
+
api
|
| 35 |
+
.authMe()
|
| 36 |
+
.then((u) => setSession(getToken(), u))
|
| 37 |
+
.then(() => setUser(getUser()))
|
| 38 |
+
.catch(() => clearSession());
|
| 39 |
+
}
|
| 40 |
+
}, []);
|
| 41 |
+
|
| 42 |
+
// Close the dropdown when clicking outside / navigating.
|
| 43 |
+
useEffect(() => setMenuOpen(false), [location.pathname]);
|
| 44 |
+
useEffect(() => {
|
| 45 |
+
if (!menuOpen) return;
|
| 46 |
+
const close = (e) => {
|
| 47 |
+
if (!e.target.closest?.(".auth-nav")) setMenuOpen(false);
|
| 48 |
+
};
|
| 49 |
+
document.addEventListener("click", close);
|
| 50 |
+
return () => document.removeEventListener("click", close);
|
| 51 |
+
}, [menuOpen]);
|
| 52 |
+
|
| 53 |
+
useEffect(() => {
|
| 54 |
+
const openAuth = () => {
|
| 55 |
+
setMenuOpen(false);
|
| 56 |
+
setModalOpen(true);
|
| 57 |
+
};
|
| 58 |
+
window.addEventListener("open-auth-modal", openAuth);
|
| 59 |
+
return () => window.removeEventListener("open-auth-modal", openAuth);
|
| 60 |
+
}, []);
|
| 61 |
+
|
| 62 |
+
function handleSearch(e) {
|
| 63 |
+
e.preventDefault();
|
| 64 |
+
if (q.trim()) navigate(`/search?q=${encodeURIComponent(q.trim())}`);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
async function handleLogout() {
|
| 68 |
+
try {
|
| 69 |
+
await api.authLogout();
|
| 70 |
+
} catch {
|
| 71 |
+
/* token already invalid — clear locally anyway */
|
| 72 |
+
}
|
| 73 |
+
clearSession();
|
| 74 |
+
setUser(null);
|
| 75 |
+
setMenuOpen(false);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
return (
|
| 79 |
+
<header id="header" className={scrolled ? "scrolled" : ""}>
|
| 80 |
+
<div className="container">
|
| 81 |
+
<Link to="/home" className="logo">
|
| 82 |
+
<div className="logo-wordmark">
|
| 83 |
+
{"ANICOVE".split("").map((ch, i) => (
|
| 84 |
+
<span key={i} className="l">{ch}</span>
|
| 85 |
+
))}
|
| 86 |
+
</div>
|
| 87 |
+
</Link>
|
| 88 |
+
|
| 89 |
+
<nav id="main-nav">
|
| 90 |
+
{NAV.map(({ to, label }) => (
|
| 91 |
+
<Link
|
| 92 |
+
key={to}
|
| 93 |
+
to={to}
|
| 94 |
+
className={location.pathname === to ? "active" : ""}
|
| 95 |
+
>
|
| 96 |
+
{label}
|
| 97 |
+
</Link>
|
| 98 |
+
))}
|
| 99 |
+
</nav>
|
| 100 |
+
|
| 101 |
+
<div className="search-wrap">
|
| 102 |
+
<span className="search-icon">
|
| 103 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
| 104 |
+
<circle cx="11" cy="11" r="8" />
|
| 105 |
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
| 106 |
+
</svg>
|
| 107 |
+
</span>
|
| 108 |
+
<form onSubmit={handleSearch}>
|
| 109 |
+
<input
|
| 110 |
+
id="header-search"
|
| 111 |
+
type="text"
|
| 112 |
+
value={q}
|
| 113 |
+
onChange={e => setQ(e.target.value)}
|
| 114 |
+
className="search-input"
|
| 115 |
+
placeholder="Search anime..."
|
| 116 |
+
autoComplete="off"
|
| 117 |
+
/>
|
| 118 |
+
</form>
|
| 119 |
+
</div>
|
| 120 |
+
|
| 121 |
+
<div className="auth-nav">
|
| 122 |
+
<button
|
| 123 |
+
className={`auth-avatar-btn${user ? "" : " guest"}${menuOpen ? " open" : ""}`}
|
| 124 |
+
onClick={() => setMenuOpen((v) => !v)}
|
| 125 |
+
aria-label={user ? `${user.username} menu` : "Account menu"}
|
| 126 |
+
aria-expanded={menuOpen}
|
| 127 |
+
>
|
| 128 |
+
{user?.avatar ? (
|
| 129 |
+
<img className="auth-avatar-circle auth-avatar-img" src={user.avatar} alt="" />
|
| 130 |
+
) : (
|
| 131 |
+
<div className="auth-avatar-circle auth-avatar-icon">
|
| 132 |
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
| 133 |
+
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
| 134 |
+
<circle cx="12" cy="7" r="4" />
|
| 135 |
+
</svg>
|
| 136 |
+
</div>
|
| 137 |
+
)}
|
| 138 |
+
<span>{user ? user.username : "Guest"}</span>
|
| 139 |
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
| 140 |
+
<path d="M6 9l6 6 6-6" />
|
| 141 |
+
</svg>
|
| 142 |
+
</button>
|
| 143 |
+
|
| 144 |
+
{menuOpen ? (
|
| 145 |
+
<div className="auth-dropdown">
|
| 146 |
+
{user ? (
|
| 147 |
+
<>
|
| 148 |
+
<div className="auth-dropdown-head">
|
| 149 |
+
<div className="auth-dropdown-name">{user.username}</div>
|
| 150 |
+
<div className="auth-dropdown-sub">{user.email || user.provider}</div>
|
| 151 |
+
</div>
|
| 152 |
+
<button
|
| 153 |
+
className="auth-dropdown-item"
|
| 154 |
+
onClick={() => { setMenuOpen(false); navigate("/home"); }}
|
| 155 |
+
>
|
| 156 |
+
Home
|
| 157 |
+
</button>
|
| 158 |
+
<button className="auth-dropdown-item danger" onClick={handleLogout}>
|
| 159 |
+
Sign out
|
| 160 |
+
</button>
|
| 161 |
+
</>
|
| 162 |
+
) : (
|
| 163 |
+
<>
|
| 164 |
+
<button
|
| 165 |
+
className="auth-dropdown-item"
|
| 166 |
+
onClick={() => { setMenuOpen(false); setModalOpen(true); }}
|
| 167 |
+
>
|
| 168 |
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
| 169 |
+
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
|
| 170 |
+
<path d="M10 17l5-5-5-5M15 12H3" />
|
| 171 |
+
</svg>
|
| 172 |
+
Sign in
|
| 173 |
+
</button>
|
| 174 |
+
<button
|
| 175 |
+
className="auth-dropdown-item"
|
| 176 |
+
onClick={() => { window.location.href = "/api/auth/anilist/login"; }}
|
| 177 |
+
>
|
| 178 |
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor">
|
| 179 |
+
<path d="M2 3h20v18H2V3zm5.5 5.5h-1.2v7h1.2v-7zm2.6 0h1.2l2.1 5.1 2.1-5.1h1.2v7h-1.15v-4.6L12 15.6l-1.75-4.25v4.15H9.1v-7z" />
|
| 180 |
+
</svg>
|
| 181 |
+
Sign in with AniList
|
| 182 |
+
</button>
|
| 183 |
+
</>
|
| 184 |
+
)}
|
| 185 |
+
</div>
|
| 186 |
+
) : null}
|
| 187 |
+
</div>
|
| 188 |
+
<AuthModal
|
| 189 |
+
open={modalOpen}
|
| 190 |
+
onClose={() => setModalOpen(false)}
|
| 191 |
+
onAuthed={(u) => setUser(u)}
|
| 192 |
+
/>
|
| 193 |
+
</div>
|
| 194 |
+
</header>
|
| 195 |
+
);
|
| 196 |
+
}
|
frontend/src/components/HeroCarousel.jsx
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useCallback, useEffect, useRef, useState } from "react";
|
| 2 |
+
import { Link } from "react-router-dom";
|
| 3 |
+
import { api, titleOf } from "../api.js";
|
| 4 |
+
|
| 5 |
+
export default function HeroCarousel({ items = [] }) {
|
| 6 |
+
const [index, setIndex] = useState(0);
|
| 7 |
+
const timer = useRef(null);
|
| 8 |
+
|
| 9 |
+
const count = items.length;
|
| 10 |
+
const go = useCallback(
|
| 11 |
+
(i) => setIndex(((i % count) + count) % count),
|
| 12 |
+
[count]
|
| 13 |
+
);
|
| 14 |
+
|
| 15 |
+
useEffect(() => {
|
| 16 |
+
if (count <= 1) return undefined;
|
| 17 |
+
timer.current = setInterval(() => setIndex((i) => (i + 1) % count), 6000);
|
| 18 |
+
return () => clearInterval(timer.current);
|
| 19 |
+
}, [count]);
|
| 20 |
+
|
| 21 |
+
if (!count) return null;
|
| 22 |
+
const media = items[index];
|
| 23 |
+
const title = titleOf(media);
|
| 24 |
+
const native = media.title?.native;
|
| 25 |
+
const genres = (media.genres || []).slice(0, 3);
|
| 26 |
+
const banner = media.bannerImage || media.coverImage;
|
| 27 |
+
const epRef = media.id;
|
| 28 |
+
|
| 29 |
+
return (
|
| 30 |
+
<div className="hero">
|
| 31 |
+
{banner ? (
|
| 32 |
+
<div
|
| 33 |
+
className="hero-bg"
|
| 34 |
+
style={{ backgroundImage: `url(${api.img(banner)})` }}
|
| 35 |
+
/>
|
| 36 |
+
) : null}
|
| 37 |
+
<div className="hero-scrim" />
|
| 38 |
+
<button
|
| 39 |
+
className="hero-nav prev"
|
| 40 |
+
aria-label="Previous"
|
| 41 |
+
onClick={() => go(index - 1)}
|
| 42 |
+
>
|
| 43 |
+
‹
|
| 44 |
+
</button>
|
| 45 |
+
<button
|
| 46 |
+
className="hero-nav next"
|
| 47 |
+
aria-label="Next"
|
| 48 |
+
onClick={() => go(index + 1)}
|
| 49 |
+
>
|
| 50 |
+
›
|
| 51 |
+
</button>
|
| 52 |
+
<div className="hero-content">
|
| 53 |
+
<div className="hero-kicker">Trending now</div>
|
| 54 |
+
<h1>{title}</h1>
|
| 55 |
+
{native && native !== title ? <div className="detail-native">{native}</div> : null}
|
| 56 |
+
<div className="hero-meta">
|
| 57 |
+
{media.score ? <span className="chip score">★ {media.score}%</span> : null}
|
| 58 |
+
{media.format ? <span className="chip">{media.format}</span> : null}
|
| 59 |
+
{media.seasonYear ? <span className="chip dim">{media.seasonYear}</span> : null}
|
| 60 |
+
{media.episodes ? <span className="chip dim">{media.episodes} eps</span> : null}
|
| 61 |
+
</div>
|
| 62 |
+
{media.synopsis ? <p className="hero-desc">{media.synopsis}</p> : null}
|
| 63 |
+
<div className="hero-actions">
|
| 64 |
+
<Link to={`/anime/${epRef}`} className="btn btn-primary">
|
| 65 |
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor">
|
| 66 |
+
<path d="M8 5v14l11-7z" />
|
| 67 |
+
</svg>
|
| 68 |
+
Watch
|
| 69 |
+
</Link>
|
| 70 |
+
<Link to={`/anime/${epRef}`} className="btn btn-ghost">
|
| 71 |
+
Details
|
| 72 |
+
</Link>
|
| 73 |
+
</div>
|
| 74 |
+
{genres.length ? (
|
| 75 |
+
<div className="genre-pills">
|
| 76 |
+
{genres.map((g) => (
|
| 77 |
+
<Link key={g} to={`/search?q=${encodeURIComponent(g)}`}>
|
| 78 |
+
{g}
|
| 79 |
+
</Link>
|
| 80 |
+
))}
|
| 81 |
+
</div>
|
| 82 |
+
) : null}
|
| 83 |
+
</div>
|
| 84 |
+
{count > 1 ? (
|
| 85 |
+
<div className="hero-dots">
|
| 86 |
+
{items.map((_, i) => (
|
| 87 |
+
<button
|
| 88 |
+
key={i}
|
| 89 |
+
className={i === index ? "active" : ""}
|
| 90 |
+
aria-label={`Slide ${i + 1}`}
|
| 91 |
+
onClick={() => setIndex(i)}
|
| 92 |
+
/>
|
| 93 |
+
))}
|
| 94 |
+
</div>
|
| 95 |
+
) : null}
|
| 96 |
+
</div>
|
| 97 |
+
);
|
| 98 |
+
}
|
frontend/src/components/HeroSection.jsx
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from "react";
|
| 2 |
+
import { useNavigate } from "react-router-dom";
|
| 3 |
+
|
| 4 |
+
const POPULAR_SEARCHES = ["One Piece", "Solo Leveling", "Demon Slayer", "Jujutsu Kaisen", "Frieren", "Attack on Titan"];
|
| 5 |
+
|
| 6 |
+
export default function HeroSection() {
|
| 7 |
+
const navigate = useNavigate();
|
| 8 |
+
const [searchQuery, setSearchQuery] = useState("");
|
| 9 |
+
const [activeCategory, setActiveCategory] = useState("All");
|
| 10 |
+
|
| 11 |
+
function handleSearch(e) {
|
| 12 |
+
e.preventDefault();
|
| 13 |
+
if (searchQuery.trim()) {
|
| 14 |
+
navigate(`/search?q=${encodeURIComponent(searchQuery.trim())}`);
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
function handleFilterClick(cat) {
|
| 19 |
+
setActiveCategory(cat);
|
| 20 |
+
if (cat === "Anime") {
|
| 21 |
+
navigate("/search");
|
| 22 |
+
} else if (cat === "Manga") {
|
| 23 |
+
navigate("/manga");
|
| 24 |
+
} else if (cat === "Movies & TV") {
|
| 25 |
+
navigate("/search?q=movie");
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
return (
|
| 30 |
+
<section className="home-hero">
|
| 31 |
+
<div className="hero-glow" />
|
| 32 |
+
<div className="hero-badge">
|
| 33 |
+
<span className="hero-badge-dot" />
|
| 34 |
+
<span>ANIME • MANGA • MOVIES • MUSIC</span>
|
| 35 |
+
</div>
|
| 36 |
+
|
| 37 |
+
<h1 className="hero-logo-text">
|
| 38 |
+
ANI<span className="hero-logo-e">C</span>OVE
|
| 39 |
+
</h1>
|
| 40 |
+
|
| 41 |
+
<div className="hero-subtitle">YOUR STREAMING UNIVERSE</div>
|
| 42 |
+
<p className="hero-desc">
|
| 43 |
+
Watch, read and listen without losing your place. <strong>Zero friction.</strong><br />
|
| 44 |
+
<strong>Everything connected.</strong>
|
| 45 |
+
</p>
|
| 46 |
+
|
| 47 |
+
{/* Filter Pills */}
|
| 48 |
+
<div className="hero-filters">
|
| 49 |
+
{["All", "Anime", "Manga", "Movies & TV", "Music"].map(cat => (
|
| 50 |
+
<button
|
| 51 |
+
key={cat}
|
| 52 |
+
className={`hero-filter-pill ${activeCategory === cat ? "active" : ""}`}
|
| 53 |
+
onClick={() => handleFilterClick(cat)}
|
| 54 |
+
>
|
| 55 |
+
{cat}
|
| 56 |
+
</button>
|
| 57 |
+
))}
|
| 58 |
+
</div>
|
| 59 |
+
|
| 60 |
+
{/* Search Bar */}
|
| 61 |
+
<form className="hero-search-wrap" onSubmit={handleSearch}>
|
| 62 |
+
<input
|
| 63 |
+
type="text"
|
| 64 |
+
className="hero-search-input"
|
| 65 |
+
placeholder="Search anime, manga, movies, music..."
|
| 66 |
+
value={searchQuery}
|
| 67 |
+
onChange={e => setSearchQuery(e.target.value)}
|
| 68 |
+
/>
|
| 69 |
+
<button type="submit" className="hero-search-btn" aria-label="Search">
|
| 70 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
| 71 |
+
<circle cx="11" cy="11" r="8" />
|
| 72 |
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
| 73 |
+
</svg>
|
| 74 |
+
</button>
|
| 75 |
+
</form>
|
| 76 |
+
|
| 77 |
+
{/* Popular Searches */}
|
| 78 |
+
<div className="hero-popular">
|
| 79 |
+
<div className="hero-popular-label">POPULAR SEARCHES</div>
|
| 80 |
+
<div className="hero-popular-tags">
|
| 81 |
+
{POPULAR_SEARCHES.map(query => (
|
| 82 |
+
<span
|
| 83 |
+
key={query}
|
| 84 |
+
className="hero-popular-tag"
|
| 85 |
+
onClick={() => navigate(`/search?q=${encodeURIComponent(query)}`)}
|
| 86 |
+
>
|
| 87 |
+
{query}
|
| 88 |
+
</span>
|
| 89 |
+
))}
|
| 90 |
+
</div>
|
| 91 |
+
</div>
|
| 92 |
+
|
| 93 |
+
{/* Action Buttons */}
|
| 94 |
+
<div className="hero-ctas">
|
| 95 |
+
<button className="hero-cta hero-cta-anime" onClick={() => navigate("/home")}>
|
| 96 |
+
<span>Enter anicove</span>
|
| 97 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
| 98 |
+
<path d="M5 12h14M12 5l7 7-7 7" />
|
| 99 |
+
</svg>
|
| 100 |
+
</button>
|
| 101 |
+
<button className="hero-cta hero-cta-manga" onClick={() => navigate("/manga")}>
|
| 102 |
+
<span>Read Manga</span>
|
| 103 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
| 104 |
+
<path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
| 105 |
+
</svg>
|
| 106 |
+
</button>
|
| 107 |
+
<button className="hero-cta hero-cta-movies" onClick={() => navigate("/movies")}>
|
| 108 |
+
<span>Movies & TV</span>
|
| 109 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
| 110 |
+
<polygon points="5 3 19 12 5 21 5 3" />
|
| 111 |
+
</svg>
|
| 112 |
+
</button>
|
| 113 |
+
<button className="hero-cta hero-cta-music" onClick={() => navigate("/music")}>
|
| 114 |
+
<span>Play Music</span>
|
| 115 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
| 116 |
+
<path d="M9 18V5l12-2v13M9 9l12-2" />
|
| 117 |
+
</svg>
|
| 118 |
+
</button>
|
| 119 |
+
</div>
|
| 120 |
+
|
| 121 |
+
{/* Stats */}
|
| 122 |
+
<div className="hero-stats">
|
| 123 |
+
<div className="hero-stat">
|
| 124 |
+
<div className="hero-stat-val">10K+</div>
|
| 125 |
+
<div className="hero-stat-label">SERIES & MOVIES</div>
|
| 126 |
+
</div>
|
| 127 |
+
<div className="hero-stat">
|
| 128 |
+
<div className="hero-stat-val">SUB+DUB</div>
|
| 129 |
+
<div className="hero-stat-label">AUDIO CHOICES</div>
|
| 130 |
+
</div>
|
| 131 |
+
<div className="hero-stat">
|
| 132 |
+
<div className="hero-stat-val">HD</div>
|
| 133 |
+
<div className="hero-stat-label">ADAPTIVE PLAYBACK</div>
|
| 134 |
+
</div>
|
| 135 |
+
<div className="hero-stat">
|
| 136 |
+
<div className="hero-stat-val">4-IN-1</div>
|
| 137 |
+
<div className="hero-stat-label">ANIME • MANGA • MOVIES • MUSIC</div>
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
|
| 141 |
+
<div className="scroll-indicator">
|
| 142 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
| 143 |
+
<path d="M19 9l-7 7-7-7" />
|
| 144 |
+
</svg>
|
| 145 |
+
</div>
|
| 146 |
+
</section>
|
| 147 |
+
);
|
| 148 |
+
}
|
frontend/src/components/HlsPlayer.jsx
ADDED
|
@@ -0,0 +1,1012 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Hls from "hls.js";
|
| 2 |
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
| 3 |
+
import { proxiedStreamUrl, streamHeaders } from "../api.js";
|
| 4 |
+
|
| 5 |
+
/** blob:/data: URLs are already local — never route them through the proxy. */
|
| 6 |
+
function subtitleSrc(file, referer) {
|
| 7 |
+
if (/^(blob|data):/i.test(file || "")) return file;
|
| 8 |
+
return proxiedStreamUrl(file, referer);
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
function fmt(t) {
|
| 12 |
+
if (!Number.isFinite(t) || t < 0) t = 0;
|
| 13 |
+
const h = Math.floor(t / 3600);
|
| 14 |
+
const m = Math.floor((t % 3600) / 60);
|
| 15 |
+
const s = Math.floor(t % 60);
|
| 16 |
+
const mm = h ? String(m).padStart(2, "0") : String(m);
|
| 17 |
+
return h ? `${h}:${mm}:${String(s).padStart(2, "0")}` : `${mm}:${String(s).padStart(2, "0")}`;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
const S = { fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" };
|
| 21 |
+
|
| 22 |
+
const IconPlay = (p) => (
|
| 23 |
+
<svg viewBox="0 0 24 24" width={p.s || 20} height={p.s || 20} fill="currentColor">
|
| 24 |
+
<path d="M8 5.14v13.72c0 .8.87 1.3 1.56.9l11-6.86a1.05 1.05 0 000-1.8l-11-6.86c-.69-.4-1.56.1-1.56.9z" />
|
| 25 |
+
</svg>
|
| 26 |
+
);
|
| 27 |
+
const IconPause = (p) => (
|
| 28 |
+
<svg viewBox="0 0 24 24" width={p.s || 20} height={p.s || 20} fill="currentColor">
|
| 29 |
+
<rect x="6" y="5" width="4" height="14" rx="1.2" />
|
| 30 |
+
<rect x="14" y="5" width="4" height="14" rx="1.2" />
|
| 31 |
+
</svg>
|
| 32 |
+
);
|
| 33 |
+
const IconReplay = (p) => (
|
| 34 |
+
<svg viewBox="0 0 24 24" width={p.s || 20} height={p.s || 20} {...S}>
|
| 35 |
+
<path d="M3 12a9 9 0 109-9 9.75 9.75 0 00-6.74 2.74L3 8" />
|
| 36 |
+
<path d="M3 3v5h5" />
|
| 37 |
+
</svg>
|
| 38 |
+
);
|
| 39 |
+
const IconVol = (p) => (
|
| 40 |
+
<svg viewBox="0 0 24 24" width={p.s || 20} height={p.s || 20} {...S}>
|
| 41 |
+
<path d="M11 5L6 9H2v6h4l5 4V5z" />
|
| 42 |
+
<path d="M15.5 8.5a5 5 0 010 7" />
|
| 43 |
+
<path d="M18.5 5.5a9 9 0 010 13" />
|
| 44 |
+
</svg>
|
| 45 |
+
);
|
| 46 |
+
const IconMuted = (p) => (
|
| 47 |
+
<svg viewBox="0 0 24 24" width={p.s || 20} height={p.s || 20} {...S}>
|
| 48 |
+
<path d="M11 5L6 9H2v6h4l5 4V5z" />
|
| 49 |
+
<path d="M22 9l-6 6M16 9l6 6" />
|
| 50 |
+
</svg>
|
| 51 |
+
);
|
| 52 |
+
const IconGear = (p) => (
|
| 53 |
+
<svg viewBox="0 0 24 24" width={p.s || 20} height={p.s || 20} {...S}>
|
| 54 |
+
<circle cx="12" cy="12" r="3" />
|
| 55 |
+
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 11-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 11-4 0v-.09a1.65 1.65 0 00-1-1.51 1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 11-2.83-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 110-4h.09a1.65 1.65 0 001.51-1 1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 112.83-2.83l.06.06a1.65 1.65 0 001.82.33h0a1.65 1.65 0 001-1.51V3a2 2 0 114 0v.09a1.65 1.65 0 001 1.51h0a1.65 1.65 0 001.82-.33l.06-.06a2 2 0 112.83 2.83l-.06.06a1.65 1.65 0 00-.33 1.82v0a1.65 1.65 0 001.51 1H21a2 2 0 110 4h-.09a1.65 1.65 0 00-1.51 1z" />
|
| 56 |
+
</svg>
|
| 57 |
+
);
|
| 58 |
+
const IconCast = (p) => (
|
| 59 |
+
<svg viewBox="0 0 24 24" width={p.s || 20} height={p.s || 20} {...S}>
|
| 60 |
+
<path d="M2 16.1A5.9 5.9 0 017.9 22" />
|
| 61 |
+
<path d="M2 12.05A9.95 9.95 0 0111.95 22" />
|
| 62 |
+
<path d="M2 8V6a2 2 0 012-2h16a2 2 0 012 2v12a2 2 0 01-2 2h-6" />
|
| 63 |
+
<circle cx="2" cy="20" r="1.4" fill="currentColor" stroke="none" />
|
| 64 |
+
</svg>
|
| 65 |
+
);
|
| 66 |
+
const IconPip = (p) => (
|
| 67 |
+
<svg viewBox="0 0 24 24" width={p.s || 20} height={p.s || 20} {...S}>
|
| 68 |
+
<rect x="2" y="4" width="20" height="16" rx="2" />
|
| 69 |
+
<rect x="12" y="12" width="8" height="5" rx="1" fill="currentColor" stroke="none" />
|
| 70 |
+
</svg>
|
| 71 |
+
);
|
| 72 |
+
const IconFs = (p) => (
|
| 73 |
+
<svg viewBox="0 0 24 24" width={p.s || 20} height={p.s || 20} {...S}>
|
| 74 |
+
<path d="M8 3H5a2 2 0 00-2 2v3M16 3h3a2 2 0 012 2v3M8 21H5a2 2 0 01-2-2v-3M16 21h3a2 2 0 002-2v-3" />
|
| 75 |
+
</svg>
|
| 76 |
+
);
|
| 77 |
+
const IconShrink = (p) => (
|
| 78 |
+
<svg viewBox="0 0 24 24" width={p.s || 20} height={p.s || 20} {...S}>
|
| 79 |
+
<path d="M8 3v3a2 2 0 01-2 2H3M21 8h-3a2 2 0 01-2-2V3M3 16h3a2 2 0 012 2v3M16 21v-3a2 2 0 012-2h3" />
|
| 80 |
+
</svg>
|
| 81 |
+
);
|
| 82 |
+
const IconTheater = (p) => (
|
| 83 |
+
<svg viewBox="0 0 24 24" width={p.s || 20} height={p.s || 20} {...S}>
|
| 84 |
+
<rect x="2" y="3" width="20" height="14" rx="2" />
|
| 85 |
+
<path d="M7 21h10" />
|
| 86 |
+
<path d="M10 17v4" />
|
| 87 |
+
<path d="M14 17v4" />
|
| 88 |
+
</svg>
|
| 89 |
+
);
|
| 90 |
+
const IconScreenshot = (p) => (
|
| 91 |
+
<svg viewBox="0 0 24 24" width={p.s || 20} height={p.s || 20} {...S}>
|
| 92 |
+
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
|
| 93 |
+
<circle cx="12" cy="13" r="4" />
|
| 94 |
+
</svg>
|
| 95 |
+
);
|
| 96 |
+
const IconRewind = (p) => (
|
| 97 |
+
<svg viewBox="0 0 24 24" width={p.s || 20} height={p.s || 20} {...S}>
|
| 98 |
+
<polygon points="11 19 2 12 11 5 11 19" />
|
| 99 |
+
<polygon points="22 19 13 12 22 5 22 19" />
|
| 100 |
+
</svg>
|
| 101 |
+
);
|
| 102 |
+
const IconForward = (p) => (
|
| 103 |
+
<svg viewBox="0 0 24 24" width={p.s || 20} height={p.s || 20} {...S}>
|
| 104 |
+
<polygon points="13 19 22 12 13 5 13 19" />
|
| 105 |
+
<polygon points="2 19 11 12 2 5 2 19" />
|
| 106 |
+
</svg>
|
| 107 |
+
);
|
| 108 |
+
const IconLoop = (p) => (
|
| 109 |
+
<svg viewBox="0 0 24 24" width={p.s || 20} height={p.s || 20} {...S}>
|
| 110 |
+
<path d="M17 1l4 4-4 4" />
|
| 111 |
+
<path d="M3 11V9a4 4 0 0 1 4-4h14" />
|
| 112 |
+
<path d="M7 23l-4-4 4-4" />
|
| 113 |
+
<path d="M21 13v2a4 4 0 0 1-4 4H3" />
|
| 114 |
+
</svg>
|
| 115 |
+
);
|
| 116 |
+
|
| 117 |
+
export default function HlsPlayer({ sources, autoPlay = true, onFatal = null, onProgress = null, startAt = 0 }) {
|
| 118 |
+
const wrapRef = useRef(null);
|
| 119 |
+
const videoRef = useRef(null);
|
| 120 |
+
const seekBarRef = useRef(null);
|
| 121 |
+
const hlsRef = useRef(null);
|
| 122 |
+
const onFatalRef = useRef(onFatal);
|
| 123 |
+
const onProgressRef = useRef(onProgress);
|
| 124 |
+
const startAtRef = useRef(startAt);
|
| 125 |
+
const hideTimer = useRef(null);
|
| 126 |
+
const seeking = useRef(false);
|
| 127 |
+
const resumeAt = useRef(0);
|
| 128 |
+
const playingRef = useRef(false);
|
| 129 |
+
const timeRef = useRef(0);
|
| 130 |
+
const durationRef = useRef(0);
|
| 131 |
+
|
| 132 |
+
useEffect(() => {
|
| 133 |
+
onFatalRef.current = onFatal;
|
| 134 |
+
onProgressRef.current = onProgress;
|
| 135 |
+
startAtRef.current = startAt;
|
| 136 |
+
}, [onFatal, onProgress, startAt]);
|
| 137 |
+
|
| 138 |
+
const streams = useMemo(
|
| 139 |
+
() => (sources?.streams || []).filter((s) => s && s.url),
|
| 140 |
+
[sources]
|
| 141 |
+
);
|
| 142 |
+
|
| 143 |
+
const [streamIdx, setStreamIdx] = useState(0);
|
| 144 |
+
const stream = streams[Math.min(streamIdx, streams.length - 1)] || streams[0];
|
| 145 |
+
|
| 146 |
+
const [playing, setPlaying] = useState(false);
|
| 147 |
+
const [currentTime, setCurrentTime] = useState(0);
|
| 148 |
+
const [duration, setDuration] = useState(0);
|
| 149 |
+
const [buffered, setBuffered] = useState(0);
|
| 150 |
+
const [volume, setVolume] = useState(1);
|
| 151 |
+
const [muted, setMuted] = useState(false);
|
| 152 |
+
const [rate, setRate] = useState(1);
|
| 153 |
+
const [fullscreen, setFullscreen] = useState(false);
|
| 154 |
+
const [ui, setUi] = useState(true);
|
| 155 |
+
const [settings, setSettings] = useState(null); // "quality" | "speed" | null
|
| 156 |
+
const [levels, setLevels] = useState([]);
|
| 157 |
+
const [hlsLevel, setHlsLevel] = useState(-1);
|
| 158 |
+
const [castOk, setCastOk] = useState(false);
|
| 159 |
+
const [casting, setCasting] = useState(false);
|
| 160 |
+
const [fatal, setFatal] = useState(null);
|
| 161 |
+
const [showSkip, setShowSkip] = useState(null);
|
| 162 |
+
const [ended, setEnded] = useState(false);
|
| 163 |
+
const [hover, setHover] = useState(null); // { pct, time }
|
| 164 |
+
const [theaterMode, setTheaterMode] = useState(false);
|
| 165 |
+
const [loop, setLoop] = useState(false);
|
| 166 |
+
const [volumeBoost, setVolumeBoost] = useState(1);
|
| 167 |
+
|
| 168 |
+
const intro = sources?.intro || { start: 0, end: 0 };
|
| 169 |
+
const outro = sources?.outro || { start: 0, end: 0 };
|
| 170 |
+
const subtitles = sources?.subtitles || [];
|
| 171 |
+
|
| 172 |
+
const isHls =
|
| 173 |
+
stream?.type === "hls" ||
|
| 174 |
+
(stream?.url || "").split("?")[0].toLowerCase().endsWith(".m3u8");
|
| 175 |
+
|
| 176 |
+
useEffect(() => {
|
| 177 |
+
playingRef.current = playing;
|
| 178 |
+
timeRef.current = currentTime;
|
| 179 |
+
durationRef.current = duration;
|
| 180 |
+
}, [playing, currentTime, duration]);
|
| 181 |
+
|
| 182 |
+
// ── player attach (hls.js for m3u8, native for mp4) ────────────
|
| 183 |
+
useEffect(() => {
|
| 184 |
+
const video = videoRef.current;
|
| 185 |
+
if (!video || !stream?.url) return undefined;
|
| 186 |
+
|
| 187 |
+
// Resume from a saved position (startAt), or from a position carried over
|
| 188 |
+
// when the parent swaps the stream (e.g. movie quality change).
|
| 189 |
+
const resume = startAtRef.current || resumeAt.current;
|
| 190 |
+
startAtRef.current = 0;
|
| 191 |
+
resumeAt.current = 0;
|
| 192 |
+
|
| 193 |
+
// Capture the current position before teardown so the next stream can
|
| 194 |
+
// resume seamlessly. Runs before hls.destroy() resets the video element.
|
| 195 |
+
const carryPosition = () => {
|
| 196 |
+
const v = videoRef.current;
|
| 197 |
+
if (v && v.readyState >= 1 && v.currentTime > 5) resumeAt.current = v.currentTime;
|
| 198 |
+
};
|
| 199 |
+
|
| 200 |
+
setFatal(null);
|
| 201 |
+
setEnded(false);
|
| 202 |
+
|
| 203 |
+
// Streams marked `direct` are already proxied (e.g. the MovieBox sidecar's
|
| 204 |
+
// own /api/stream endpoint) — play them as-is instead of routing them
|
| 205 |
+
// through the HLS worker again.
|
| 206 |
+
const resolveSrc = (st) => (st?.direct ? st.url : proxiedStreamUrl(st.url, st.referer));
|
| 207 |
+
|
| 208 |
+
// MP4 path: when the browser rejects the source, probe the resolved URL to
|
| 209 |
+
// tell apart a server refusal (429/403/HTML — the usual cause) from a real
|
| 210 |
+
// codec problem, so the message is honest instead of a generic "codec".
|
| 211 |
+
let retryCount = 0;
|
| 212 |
+
let retryTimer = null;
|
| 213 |
+
let alive = true;
|
| 214 |
+
let diagnosing = false;
|
| 215 |
+
const diagnoseStream = (st) => {
|
| 216 |
+
// The error event and the play().catch fallback can both fire for one
|
| 217 |
+
// failure — only run the probe once per failure to keep the retry count honest.
|
| 218 |
+
if (diagnosing) return;
|
| 219 |
+
diagnosing = true;
|
| 220 |
+
const probeUrl = resolveSrc(st);
|
| 221 |
+
fetch(probeUrl, {
|
| 222 |
+
headers: { ...streamHeaders(), Range: "bytes=0-63" },
|
| 223 |
+
signal: AbortSignal.timeout(12000),
|
| 224 |
+
})
|
| 225 |
+
.then((res) => {
|
| 226 |
+
if (!alive) return;
|
| 227 |
+
const ct = res.headers.get("content-type") || "";
|
| 228 |
+
if (res.status === 429) {
|
| 229 |
+
if (retryCount < 1) {
|
| 230 |
+
retryCount += 1;
|
| 231 |
+
setFatal("Stream server is busy (HTTP 429). Retrying…");
|
| 232 |
+
retryTimer = setTimeout(reloadSrc, 3000);
|
| 233 |
+
} else {
|
| 234 |
+
const msg =
|
| 235 |
+
"The stream server is rate-limiting requests right now. Try again in a few minutes.";
|
| 236 |
+
setFatal(msg);
|
| 237 |
+
onFatalRef.current?.(msg);
|
| 238 |
+
}
|
| 239 |
+
return;
|
| 240 |
+
}
|
| 241 |
+
if (res.status >= 400 || !/video|mp4|octet-stream|quicktime/i.test(ct)) {
|
| 242 |
+
const msg = `The stream server refused the video (HTTP ${res.status}).`;
|
| 243 |
+
setFatal(msg);
|
| 244 |
+
onFatalRef.current?.(msg);
|
| 245 |
+
return;
|
| 246 |
+
}
|
| 247 |
+
// Bytes look like video — the container/codec is genuinely unsupported.
|
| 248 |
+
const msg = "This video codec is not supported by your browser.";
|
| 249 |
+
setFatal(msg);
|
| 250 |
+
onFatalRef.current?.(msg);
|
| 251 |
+
})
|
| 252 |
+
.catch(() => {
|
| 253 |
+
if (!alive) return;
|
| 254 |
+
const msg = "This stream could not be played.";
|
| 255 |
+
setFatal(msg);
|
| 256 |
+
onFatalRef.current?.(msg);
|
| 257 |
+
});
|
| 258 |
+
};
|
| 259 |
+
|
| 260 |
+
const reloadSrc = () => {
|
| 261 |
+
const v = videoRef.current;
|
| 262 |
+
if (!alive || !v || v.getAttribute("data-stream-url") !== stream.url) return;
|
| 263 |
+
diagnosing = false; // a fresh failure after the reload may be diagnosed again
|
| 264 |
+
v.removeAttribute("src");
|
| 265 |
+
v.load();
|
| 266 |
+
v.src = resolveSrc(stream);
|
| 267 |
+
v.load();
|
| 268 |
+
if (autoPlay) v.play().catch(() => {});
|
| 269 |
+
};
|
| 270 |
+
|
| 271 |
+
const onVideoError = () => {
|
| 272 |
+
const code = video.error?.code;
|
| 273 |
+
if (!isHls && code === 4 && stream?.url) {
|
| 274 |
+
diagnoseStream(stream);
|
| 275 |
+
return;
|
| 276 |
+
}
|
| 277 |
+
const msg =
|
| 278 |
+
code === 4
|
| 279 |
+
? "This video codec is not supported by your browser."
|
| 280 |
+
: "This stream could not be played.";
|
| 281 |
+
setFatal(msg);
|
| 282 |
+
onFatalRef.current?.(msg);
|
| 283 |
+
};
|
| 284 |
+
video.setAttribute("data-stream-url", stream.url);
|
| 285 |
+
video.addEventListener("error", onVideoError);
|
| 286 |
+
|
| 287 |
+
if (!isHls) {
|
| 288 |
+
setLevels([]);
|
| 289 |
+
video.src = resolveSrc(stream);
|
| 290 |
+
const onMeta = () => {
|
| 291 |
+
try {
|
| 292 |
+
video.currentTime = Math.min(resume, video.duration || resume);
|
| 293 |
+
} catch {
|
| 294 |
+
/* ignore */
|
| 295 |
+
}
|
| 296 |
+
};
|
| 297 |
+
if (resume > 0) video.addEventListener("loadedmetadata", onMeta, { once: true });
|
| 298 |
+
if (autoPlay) {
|
| 299 |
+
video.play().catch(() => {
|
| 300 |
+
if (video.error) onVideoError();
|
| 301 |
+
});
|
| 302 |
+
}
|
| 303 |
+
return () => {
|
| 304 |
+
alive = false;
|
| 305 |
+
if (retryTimer) clearTimeout(retryTimer);
|
| 306 |
+
carryPosition();
|
| 307 |
+
video.removeEventListener("error", onVideoError);
|
| 308 |
+
video.removeEventListener("loadedmetadata", onMeta);
|
| 309 |
+
};
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
let hls = null;
|
| 313 |
+
let manifestTimer = null;
|
| 314 |
+
let stuckTimer = null;
|
| 315 |
+
|
| 316 |
+
try {
|
| 317 |
+
if (Hls.isSupported()) {
|
| 318 |
+
hls = new Hls({
|
| 319 |
+
enableWorker: true,
|
| 320 |
+
maxBufferLength: 40,
|
| 321 |
+
maxMaxBufferLength: 120,
|
| 322 |
+
xhrSetup: (xhr) => {
|
| 323 |
+
for (const [k, v] of Object.entries(streamHeaders())) {
|
| 324 |
+
xhr.setRequestHeader(k, v);
|
| 325 |
+
}
|
| 326 |
+
},
|
| 327 |
+
});
|
| 328 |
+
let manifestLoaded = false;
|
| 329 |
+
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
| 330 |
+
manifestLoaded = true;
|
| 331 |
+
if (!hls.levels?.length) {
|
| 332 |
+
const msg = "This server returned no playable quality levels.";
|
| 333 |
+
setFatal(msg);
|
| 334 |
+
onFatalRef.current?.(msg);
|
| 335 |
+
return;
|
| 336 |
+
}
|
| 337 |
+
setLevels(
|
| 338 |
+
hls.levels.map((l, i) => ({
|
| 339 |
+
i,
|
| 340 |
+
height: l.height,
|
| 341 |
+
name: l.height ? `${l.height}p` : `Level ${i + 1}`,
|
| 342 |
+
}))
|
| 343 |
+
);
|
| 344 |
+
if (resume > 0) {
|
| 345 |
+
try {
|
| 346 |
+
video.currentTime = Math.min(resume, video.duration || resume);
|
| 347 |
+
} catch {
|
| 348 |
+
/* ignore */
|
| 349 |
+
}
|
| 350 |
+
}
|
| 351 |
+
if (autoPlay) video.play().catch(() => {});
|
| 352 |
+
stuckTimer = setTimeout(() => {
|
| 353 |
+
if (video.readyState < 2) {
|
| 354 |
+
const msg = "This stream is not delivering data.";
|
| 355 |
+
setFatal(msg);
|
| 356 |
+
onFatalRef.current?.(msg);
|
| 357 |
+
}
|
| 358 |
+
}, 20000);
|
| 359 |
+
video.addEventListener("loadeddata", () => clearTimeout(stuckTimer), {
|
| 360 |
+
once: true,
|
| 361 |
+
});
|
| 362 |
+
});
|
| 363 |
+
let retries = 0;
|
| 364 |
+
hls.on(Hls.Events.ERROR, (_e, data) => {
|
| 365 |
+
if (!data.fatal) return;
|
| 366 |
+
if (data.type === Hls.ErrorTypes.NETWORK_ERROR && retries < 3) {
|
| 367 |
+
retries += 1;
|
| 368 |
+
hls.startLoad();
|
| 369 |
+
} else {
|
| 370 |
+
const msg = data.details || "This stream could not be loaded.";
|
| 371 |
+
setFatal(msg);
|
| 372 |
+
onFatalRef.current?.(msg);
|
| 373 |
+
}
|
| 374 |
+
});
|
| 375 |
+
manifestTimer = setTimeout(() => {
|
| 376 |
+
if (!manifestLoaded) {
|
| 377 |
+
const msg = "Timed out loading this stream.";
|
| 378 |
+
setFatal(msg);
|
| 379 |
+
onFatalRef.current?.(msg);
|
| 380 |
+
}
|
| 381 |
+
}, 12000);
|
| 382 |
+
|
| 383 |
+
hls.loadSource(proxiedStreamUrl(stream.url, stream.referer));
|
| 384 |
+
hls.attachMedia(video);
|
| 385 |
+
hlsRef.current = hls;
|
| 386 |
+
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
| 387 |
+
video.src = proxiedStreamUrl(stream.url, stream.referer); // native HLS (Safari)
|
| 388 |
+
}
|
| 389 |
+
} catch (err) {
|
| 390 |
+
const msg = err?.message || "Player error";
|
| 391 |
+
console.error("[HlsPlayer]", err);
|
| 392 |
+
setFatal(msg);
|
| 393 |
+
onFatalRef.current?.(msg);
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
return () => {
|
| 397 |
+
alive = false;
|
| 398 |
+
if (retryTimer) clearTimeout(retryTimer);
|
| 399 |
+
carryPosition();
|
| 400 |
+
if (manifestTimer) clearTimeout(manifestTimer);
|
| 401 |
+
if (stuckTimer) clearTimeout(stuckTimer);
|
| 402 |
+
video.removeEventListener("error", onVideoError);
|
| 403 |
+
if (hls) hls.destroy();
|
| 404 |
+
hlsRef.current = null;
|
| 405 |
+
};
|
| 406 |
+
}, [stream?.url, autoPlay, isHls]);
|
| 407 |
+
|
| 408 |
+
// ── media element events ────────────────────────────────────────
|
| 409 |
+
useEffect(() => {
|
| 410 |
+
const video = videoRef.current;
|
| 411 |
+
if (!video) return undefined;
|
| 412 |
+
let lastSave = 0;
|
| 413 |
+
const save = (ended = false) => {
|
| 414 |
+
const v = videoRef.current;
|
| 415 |
+
if (!v || !Number.isFinite(v.currentTime) || !Number.isFinite(v.duration) || v.duration <= 0) return;
|
| 416 |
+
onProgressRef.current?.({ position: v.currentTime, duration: v.duration, ended });
|
| 417 |
+
};
|
| 418 |
+
const onPlay = () => setPlaying(true);
|
| 419 |
+
const onPause = () => {
|
| 420 |
+
setPlaying(false);
|
| 421 |
+
setUi(true);
|
| 422 |
+
save();
|
| 423 |
+
};
|
| 424 |
+
const onTime = () => {
|
| 425 |
+
setCurrentTime(video.currentTime);
|
| 426 |
+
const now = Date.now();
|
| 427 |
+
if (now - lastSave >= 5000) {
|
| 428 |
+
lastSave = now;
|
| 429 |
+
save();
|
| 430 |
+
}
|
| 431 |
+
};
|
| 432 |
+
const onDur = () => setDuration(video.duration || 0);
|
| 433 |
+
const onProg = () => {
|
| 434 |
+
const b = video.buffered;
|
| 435 |
+
setBuffered(b.length ? b.end(b.length - 1) : 0);
|
| 436 |
+
};
|
| 437 |
+
const onVol = () => {
|
| 438 |
+
setVolume(video.volume);
|
| 439 |
+
setMuted(video.muted);
|
| 440 |
+
};
|
| 441 |
+
const onEnd = () => {
|
| 442 |
+
setPlaying(false);
|
| 443 |
+
setEnded(true);
|
| 444 |
+
setUi(true);
|
| 445 |
+
save(true);
|
| 446 |
+
};
|
| 447 |
+
const onFs = () =>
|
| 448 |
+
setFullscreen(Boolean(document.fullscreenElement || document.webkitFullscreenElement));
|
| 449 |
+
const onCastState = () => {
|
| 450 |
+
try {
|
| 451 |
+
setCasting(video.remote?.state === "connected");
|
| 452 |
+
} catch {
|
| 453 |
+
/* ignore */
|
| 454 |
+
}
|
| 455 |
+
};
|
| 456 |
+
video.addEventListener("play", onPlay);
|
| 457 |
+
video.addEventListener("pause", onPause);
|
| 458 |
+
video.addEventListener("timeupdate", onTime);
|
| 459 |
+
video.addEventListener("durationchange", onDur);
|
| 460 |
+
video.addEventListener("progress", onProg);
|
| 461 |
+
video.addEventListener("volumechange", onVol);
|
| 462 |
+
video.addEventListener("ended", onEnd);
|
| 463 |
+
document.addEventListener("fullscreenchange", onFs);
|
| 464 |
+
try {
|
| 465 |
+
video.remote?.addEventListener?.("statechange", onCastState);
|
| 466 |
+
} catch {
|
| 467 |
+
/* ignore */
|
| 468 |
+
}
|
| 469 |
+
return () => {
|
| 470 |
+
save();
|
| 471 |
+
video.removeEventListener("play", onPlay);
|
| 472 |
+
video.removeEventListener("pause", onPause);
|
| 473 |
+
video.removeEventListener("timeupdate", onTime);
|
| 474 |
+
video.removeEventListener("durationchange", onDur);
|
| 475 |
+
video.removeEventListener("progress", onProg);
|
| 476 |
+
video.removeEventListener("volumechange", onVol);
|
| 477 |
+
video.removeEventListener("ended", onEnd);
|
| 478 |
+
document.removeEventListener("fullscreenchange", onFs);
|
| 479 |
+
try {
|
| 480 |
+
video.remote?.removeEventListener?.("statechange", onCastState);
|
| 481 |
+
} catch {
|
| 482 |
+
/* ignore */
|
| 483 |
+
}
|
| 484 |
+
};
|
| 485 |
+
}, []);
|
| 486 |
+
|
| 487 |
+
// ── cast / airplay availability ─────────────────────────────────
|
| 488 |
+
useEffect(() => {
|
| 489 |
+
const video = videoRef.current;
|
| 490 |
+
if (!video) return undefined;
|
| 491 |
+
let alive = true;
|
| 492 |
+
const hasPicker = typeof video.webkitShowPlaybackTargetPicker === "function";
|
| 493 |
+
try {
|
| 494 |
+
if (video.remote?.watchAvailability) {
|
| 495 |
+
video.remote
|
| 496 |
+
.watchAvailability((avail) => {
|
| 497 |
+
if (alive) setCastOk(Boolean(avail) || hasPicker);
|
| 498 |
+
})
|
| 499 |
+
.catch(() => {
|
| 500 |
+
if (alive) setCastOk(hasPicker);
|
| 501 |
+
});
|
| 502 |
+
} else {
|
| 503 |
+
setCastOk(hasPicker);
|
| 504 |
+
}
|
| 505 |
+
} catch {
|
| 506 |
+
setCastOk(hasPicker);
|
| 507 |
+
}
|
| 508 |
+
return () => {
|
| 509 |
+
alive = false;
|
| 510 |
+
};
|
| 511 |
+
}, [stream?.url]);
|
| 512 |
+
|
| 513 |
+
// ── auto-hide controls ──────────────────────────────────────────
|
| 514 |
+
useEffect(() => {
|
| 515 |
+
clearTimeout(hideTimer.current);
|
| 516 |
+
if (!playing) {
|
| 517 |
+
setUi(true);
|
| 518 |
+
return undefined;
|
| 519 |
+
}
|
| 520 |
+
hideTimer.current = setTimeout(() => {
|
| 521 |
+
if (!seeking.current) setUi(false);
|
| 522 |
+
}, 2600);
|
| 523 |
+
return () => clearTimeout(hideTimer.current);
|
| 524 |
+
}, [playing]);
|
| 525 |
+
|
| 526 |
+
const poke = useCallback(() => {
|
| 527 |
+
setUi(true);
|
| 528 |
+
clearTimeout(hideTimer.current);
|
| 529 |
+
if (playingRef.current) {
|
| 530 |
+
hideTimer.current = setTimeout(() => {
|
| 531 |
+
if (!seeking.current) setUi(false);
|
| 532 |
+
}, 2600);
|
| 533 |
+
}
|
| 534 |
+
}, []);
|
| 535 |
+
|
| 536 |
+
// ── controls ────────────────────────────────────────────────────
|
| 537 |
+
const togglePlay = useCallback(() => {
|
| 538 |
+
const video = videoRef.current;
|
| 539 |
+
if (!video) return;
|
| 540 |
+
if (video.paused) video.play().catch(() => {});
|
| 541 |
+
else video.pause();
|
| 542 |
+
}, []);
|
| 543 |
+
|
| 544 |
+
const seekTo = useCallback((sec) => {
|
| 545 |
+
const video = videoRef.current;
|
| 546 |
+
if (!video) return;
|
| 547 |
+
const d = durationRef.current || video.duration || 0;
|
| 548 |
+
video.currentTime = Math.min(Math.max(sec, 0), d);
|
| 549 |
+
}, []);
|
| 550 |
+
|
| 551 |
+
const seekFromEvent = useCallback(
|
| 552 |
+
(clientX) => {
|
| 553 |
+
const bar = seekBarRef.current;
|
| 554 |
+
if (!bar) return;
|
| 555 |
+
const r = bar.getBoundingClientRect();
|
| 556 |
+
const ratio = Math.min(Math.max((clientX - r.left) / r.width, 0), 1);
|
| 557 |
+
seekTo(ratio * (durationRef.current || 0));
|
| 558 |
+
},
|
| 559 |
+
[seekTo]
|
| 560 |
+
);
|
| 561 |
+
|
| 562 |
+
function toggleMute() {
|
| 563 |
+
const video = videoRef.current;
|
| 564 |
+
if (!video) return;
|
| 565 |
+
video.muted = !video.muted;
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
function setVol(v) {
|
| 569 |
+
const video = videoRef.current;
|
| 570 |
+
if (!video) return;
|
| 571 |
+
const boosted = v * volumeBoost;
|
| 572 |
+
video.volume = Math.min(boosted, 1);
|
| 573 |
+
if (v > 0) video.muted = false;
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
function toggleVolumeBoost() {
|
| 577 |
+
const next = volumeBoost === 1 ? 2 : 1;
|
| 578 |
+
setVolumeBoost(next);
|
| 579 |
+
const video = videoRef.current;
|
| 580 |
+
if (video) {
|
| 581 |
+
video.volume = Math.min(video.volume * (next / volumeBoost), 1);
|
| 582 |
+
}
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
function toggleFullscreen() {
|
| 586 |
+
const el = wrapRef.current;
|
| 587 |
+
if (!el) return;
|
| 588 |
+
try {
|
| 589 |
+
if (document.fullscreenElement || document.webkitFullscreenElement) {
|
| 590 |
+
(document.exitFullscreen || document.webkitExitFullscreen).call(document);
|
| 591 |
+
} else {
|
| 592 |
+
(el.requestFullscreen || el.webkitRequestFullscreen).call(el);
|
| 593 |
+
}
|
| 594 |
+
} catch {
|
| 595 |
+
/* ignore */
|
| 596 |
+
}
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
async function togglePip() {
|
| 600 |
+
const video = videoRef.current;
|
| 601 |
+
if (!video) return;
|
| 602 |
+
try {
|
| 603 |
+
if (document.pictureInPictureElement) {
|
| 604 |
+
await document.exitPictureInPicture();
|
| 605 |
+
} else if (video.requestPictureInPicture) {
|
| 606 |
+
await video.requestPictureInPicture();
|
| 607 |
+
}
|
| 608 |
+
} catch {
|
| 609 |
+
/* ignore */
|
| 610 |
+
}
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
async function handleCast() {
|
| 614 |
+
const video = videoRef.current;
|
| 615 |
+
if (!video) return;
|
| 616 |
+
try {
|
| 617 |
+
if (video.remote?.prompt) {
|
| 618 |
+
await video.remote.prompt();
|
| 619 |
+
return;
|
| 620 |
+
}
|
| 621 |
+
} catch {
|
| 622 |
+
/* fall through */
|
| 623 |
+
}
|
| 624 |
+
try {
|
| 625 |
+
video.webkitShowPlaybackTargetPicker?.();
|
| 626 |
+
} catch {
|
| 627 |
+
/* ignore */
|
| 628 |
+
}
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
function setSpeed(r) {
|
| 632 |
+
const video = videoRef.current;
|
| 633 |
+
if (!video) return;
|
| 634 |
+
video.playbackRate = r;
|
| 635 |
+
setRate(r);
|
| 636 |
+
setSettings(null);
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
function switchHlsQuality(i) {
|
| 640 |
+
const hls = hlsRef.current;
|
| 641 |
+
if (hls) hls.currentLevel = i;
|
| 642 |
+
setHlsLevel(i);
|
| 643 |
+
setSettings(null);
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
function pickStream(i) {
|
| 647 |
+
if (i === streamIdx) {
|
| 648 |
+
setSettings(null);
|
| 649 |
+
return;
|
| 650 |
+
}
|
| 651 |
+
resumeAt.current = videoRef.current?.currentTime || 0;
|
| 652 |
+
setStreamIdx(i);
|
| 653 |
+
setSettings(null);
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
function replay() {
|
| 657 |
+
const video = videoRef.current;
|
| 658 |
+
if (!video) return;
|
| 659 |
+
video.currentTime = 0;
|
| 660 |
+
video.play().catch(() => {});
|
| 661 |
+
}
|
| 662 |
+
|
| 663 |
+
function takeScreenshot() {
|
| 664 |
+
const video = videoRef.current;
|
| 665 |
+
if (!video) return;
|
| 666 |
+
try {
|
| 667 |
+
const canvas = document.createElement('canvas');
|
| 668 |
+
canvas.width = video.videoWidth;
|
| 669 |
+
canvas.height = video.videoHeight;
|
| 670 |
+
const ctx = canvas.getContext('2d');
|
| 671 |
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
| 672 |
+
const link = document.createElement('a');
|
| 673 |
+
link.download = `anicove-screenshot-${Date.now()}.png`;
|
| 674 |
+
link.href = canvas.toDataURL('image/png');
|
| 675 |
+
link.click();
|
| 676 |
+
} catch (err) {
|
| 677 |
+
console.error('Screenshot failed:', err);
|
| 678 |
+
}
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
+
function toggleTheaterMode() {
|
| 682 |
+
setTheaterMode(!theaterMode);
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
function toggleLoop() {
|
| 686 |
+
const video = videoRef.current;
|
| 687 |
+
if (!video) return;
|
| 688 |
+
video.loop = !loop;
|
| 689 |
+
setLoop(!loop);
|
| 690 |
+
}
|
| 691 |
+
|
| 692 |
+
function skip() {
|
| 693 |
+
const video = videoRef.current;
|
| 694 |
+
if (!video) return;
|
| 695 |
+
const target = showSkip === "intro" ? intro.end + 0.5 : outro.end + 0.5;
|
| 696 |
+
video.currentTime = target;
|
| 697 |
+
setShowSkip(null);
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
// ── skip intro/outro detection ──────────────────────────────────
|
| 701 |
+
useEffect(() => {
|
| 702 |
+
const video = videoRef.current;
|
| 703 |
+
if (!video) return undefined;
|
| 704 |
+
const onTime = () => {
|
| 705 |
+
const t = video.currentTime;
|
| 706 |
+
const inIntro = intro.end > intro.start + 3 && t >= intro.start && t < intro.end - 2;
|
| 707 |
+
const inOutro = outro.end > outro.start + 3 && t >= outro.start && t < outro.end - 2;
|
| 708 |
+
setShowSkip(inIntro ? "intro" : inOutro ? "outro" : null);
|
| 709 |
+
};
|
| 710 |
+
video.addEventListener("timeupdate", onTime);
|
| 711 |
+
return () => video.removeEventListener("timeupdate", onTime);
|
| 712 |
+
}, [intro.start, intro.end, outro.start, outro.end]);
|
| 713 |
+
|
| 714 |
+
// ── keyboard shortcuts ──────────────────────────────────────────
|
| 715 |
+
useEffect(() => {
|
| 716 |
+
const el = wrapRef.current;
|
| 717 |
+
if (!el) return undefined;
|
| 718 |
+
const onKey = (e) => {
|
| 719 |
+
const tag = (e.target.tagName || "").toLowerCase();
|
| 720 |
+
if (tag === "input" || tag === "textarea" || tag === "select") return;
|
| 721 |
+
switch (e.key.toLowerCase()) {
|
| 722 |
+
case " ":
|
| 723 |
+
case "k":
|
| 724 |
+
e.preventDefault();
|
| 725 |
+
togglePlay();
|
| 726 |
+
break;
|
| 727 |
+
case "arrowright":
|
| 728 |
+
e.preventDefault();
|
| 729 |
+
seekTo(timeRef.current + (e.shiftKey ? 10 : 5));
|
| 730 |
+
break;
|
| 731 |
+
case "arrowleft":
|
| 732 |
+
e.preventDefault();
|
| 733 |
+
seekTo(timeRef.current - (e.shiftKey ? 10 : 5));
|
| 734 |
+
break;
|
| 735 |
+
case "arrowup":
|
| 736 |
+
e.preventDefault();
|
| 737 |
+
setVol(Math.min((videoRef.current?.volume || 0) + 0.1, 1));
|
| 738 |
+
break;
|
| 739 |
+
case "arrowdown":
|
| 740 |
+
e.preventDefault();
|
| 741 |
+
setVol(Math.max((videoRef.current?.volume || 0) - 0.1, 0));
|
| 742 |
+
break;
|
| 743 |
+
case "m":
|
| 744 |
+
toggleMute();
|
| 745 |
+
break;
|
| 746 |
+
case "f":
|
| 747 |
+
toggleFullscreen();
|
| 748 |
+
break;
|
| 749 |
+
case "p":
|
| 750 |
+
togglePip();
|
| 751 |
+
break;
|
| 752 |
+
case "l":
|
| 753 |
+
toggleLoop();
|
| 754 |
+
break;
|
| 755 |
+
case "t":
|
| 756 |
+
toggleTheaterMode();
|
| 757 |
+
break;
|
| 758 |
+
case "s":
|
| 759 |
+
takeScreenshot();
|
| 760 |
+
break;
|
| 761 |
+
default:
|
| 762 |
+
break;
|
| 763 |
+
}
|
| 764 |
+
};
|
| 765 |
+
el.addEventListener("keydown", onKey);
|
| 766 |
+
return () => el.removeEventListener("keydown", onKey);
|
| 767 |
+
}, [togglePlay, seekTo]);
|
| 768 |
+
|
| 769 |
+
// ── quality options ─────────────────────────────────────────────
|
| 770 |
+
const qualityOptions = useMemo(() => {
|
| 771 |
+
if (isHls) {
|
| 772 |
+
return levels.map((l) => ({ key: l.i, label: l.name }));
|
| 773 |
+
}
|
| 774 |
+
return streams.map((s, i) => ({
|
| 775 |
+
key: i,
|
| 776 |
+
label:
|
| 777 |
+
s.resolution || s.format || s.server || (s.type === "hls" ? "HLS" : `Quality ${i + 1}`),
|
| 778 |
+
}));
|
| 779 |
+
}, [isHls, levels, streams]);
|
| 780 |
+
|
| 781 |
+
const currentQualityLabel = useMemo(() => {
|
| 782 |
+
if (isHls) return hlsLevel === -1 ? "Auto" : levels.find((l) => l.i === hlsLevel)?.name || "Auto";
|
| 783 |
+
return stream?.resolution || stream?.format || stream?.server || "Auto";
|
| 784 |
+
}, [isHls, levels, hlsLevel, stream]);
|
| 785 |
+
|
| 786 |
+
const pipOk =
|
| 787 |
+
typeof document !== "undefined" &&
|
| 788 |
+
document.pictureInPictureEnabled &&
|
| 789 |
+
typeof HTMLVideoElement !== "undefined" &&
|
| 790 |
+
"requestPictureInPicture" in HTMLVideoElement.prototype;
|
| 791 |
+
|
| 792 |
+
const pct = (v) => `${Math.min(Math.max((v / (durationRef.current || duration || 1)) * 100, 0), 100)}%`;
|
| 793 |
+
|
| 794 |
+
const skipLabel = showSkip === "intro" ? "Skip Intro" : "Skip Outro";
|
| 795 |
+
|
| 796 |
+
return (
|
| 797 |
+
<div className={`player-wrap${theaterMode ? " theater-mode" : ""}`}>
|
| 798 |
+
<div
|
| 799 |
+
className="custom-player"
|
| 800 |
+
ref={wrapRef}
|
| 801 |
+
tabIndex={0}
|
| 802 |
+
onMouseMove={poke}
|
| 803 |
+
onPointerDown={poke}
|
| 804 |
+
>
|
| 805 |
+
<video ref={videoRef} playsInline onClick={togglePlay} onDoubleClick={(e) => {
|
| 806 |
+
const rect = videoRef.current.getBoundingClientRect();
|
| 807 |
+
const x = e.clientX - rect.left;
|
| 808 |
+
if (x < rect.width / 3) {
|
| 809 |
+
seekTo(timeRef.current - 10);
|
| 810 |
+
} else if (x > (rect.width / 3) * 2) {
|
| 811 |
+
seekTo(timeRef.current + 10);
|
| 812 |
+
} else {
|
| 813 |
+
togglePlay();
|
| 814 |
+
}
|
| 815 |
+
}}>
|
| 816 |
+
{subtitles.map((s, i) => (
|
| 817 |
+
<track
|
| 818 |
+
key={`${s.file}-${i}`}
|
| 819 |
+
kind={s.kind || "captions"}
|
| 820 |
+
src={subtitleSrc(s.file, stream?.referer)}
|
| 821 |
+
label={s.label || "Subtitles"}
|
| 822 |
+
srcLang={s.srcLang || "en"}
|
| 823 |
+
default={i === 0}
|
| 824 |
+
/>
|
| 825 |
+
))}
|
| 826 |
+
</video>
|
| 827 |
+
|
| 828 |
+
{fatal ? (
|
| 829 |
+
<div className="player-error">
|
| 830 |
+
<div className="big">📺</div>
|
| 831 |
+
<p>{fatal}</p>
|
| 832 |
+
</div>
|
| 833 |
+
) : null}
|
| 834 |
+
|
| 835 |
+
{/* center play / replay */}
|
| 836 |
+
{!fatal && ended ? (
|
| 837 |
+
<button className="cp-center-play" onClick={replay} aria-label="Replay">
|
| 838 |
+
<IconReplay s={34} />
|
| 839 |
+
</button>
|
| 840 |
+
) : !fatal && !playing ? (
|
| 841 |
+
<button className="cp-center-play" onClick={togglePlay} aria-label="Play">
|
| 842 |
+
<IconPlay s={34} />
|
| 843 |
+
</button>
|
| 844 |
+
) : null}
|
| 845 |
+
|
| 846 |
+
{/* skip intro/outro */}
|
| 847 |
+
<button className={`skip-btn ${showSkip ? "show" : ""}`} onClick={skip}>
|
| 848 |
+
⏭ {skipLabel}
|
| 849 |
+
</button>
|
| 850 |
+
|
| 851 |
+
{/* bottom gradient + controls */}
|
| 852 |
+
<div className={`cp-fade ${ui ? "" : "hide"}`} />
|
| 853 |
+
<div className={`cp-controls ${ui ? "" : "hide"}`} onClick={(e) => e.stopPropagation()}>
|
| 854 |
+
<div
|
| 855 |
+
className="cp-seek"
|
| 856 |
+
ref={seekBarRef}
|
| 857 |
+
onPointerDown={(e) => {
|
| 858 |
+
seeking.current = true;
|
| 859 |
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
| 860 |
+
seekFromEvent(e.clientX);
|
| 861 |
+
}}
|
| 862 |
+
onPointerMove={(e) => {
|
| 863 |
+
if (seeking.current) {
|
| 864 |
+
seekFromEvent(e.clientX);
|
| 865 |
+
} else {
|
| 866 |
+
const r = e.currentTarget.getBoundingClientRect();
|
| 867 |
+
const ratio = Math.min(Math.max((e.clientX - r.left) / r.width, 0), 1);
|
| 868 |
+
setHover({ pct: `${ratio * 100}%`, time: ratio * (durationRef.current || 0) });
|
| 869 |
+
}
|
| 870 |
+
}}
|
| 871 |
+
onPointerUp={() => {
|
| 872 |
+
seeking.current = false;
|
| 873 |
+
setHover(null);
|
| 874 |
+
}}
|
| 875 |
+
onPointerLeave={() => {
|
| 876 |
+
if (!seeking.current) setHover(null);
|
| 877 |
+
}}
|
| 878 |
+
>
|
| 879 |
+
<div className="cp-seek-track" />
|
| 880 |
+
<div className="cp-buffered" style={{ width: pct(buffered) }} />
|
| 881 |
+
<div className="cp-progress" style={{ width: pct(currentTime) }} />
|
| 882 |
+
<div className="cp-thumb" style={{ left: pct(currentTime) }} />
|
| 883 |
+
{hover ? (
|
| 884 |
+
<div className="cp-hover-time" style={{ left: hover.pct }}>
|
| 885 |
+
{fmt(hover.time)}
|
| 886 |
+
</div>
|
| 887 |
+
) : null}
|
| 888 |
+
</div>
|
| 889 |
+
|
| 890 |
+
<div className="cp-row">
|
| 891 |
+
<button className="cp-btn" onClick={togglePlay} aria-label={playing ? "Pause" : "Play"}>
|
| 892 |
+
{playing ? <IconPause /> : <IconPlay />}
|
| 893 |
+
</button>
|
| 894 |
+
<button className="cp-btn" onClick={() => seekTo(timeRef.current - 10)} aria-label="Rewind 10s">
|
| 895 |
+
<IconRewind />
|
| 896 |
+
</button>
|
| 897 |
+
<button className="cp-btn" onClick={() => seekTo(timeRef.current + 10)} aria-label="Forward 10s">
|
| 898 |
+
<IconForward />
|
| 899 |
+
</button>
|
| 900 |
+
<button className="cp-btn" onClick={toggleMute} aria-label={muted ? "Unmute" : "Mute"}>
|
| 901 |
+
{muted || volume === 0 ? <IconMuted /> : <IconVol />}
|
| 902 |
+
</button>
|
| 903 |
+
<input
|
| 904 |
+
className="cp-vol-slider"
|
| 905 |
+
type="range"
|
| 906 |
+
min="0"
|
| 907 |
+
max="1"
|
| 908 |
+
step="0.05"
|
| 909 |
+
value={muted ? 0 : volume}
|
| 910 |
+
onChange={(e) => setVol(Number(e.target.value))}
|
| 911 |
+
aria-label="Volume"
|
| 912 |
+
/>
|
| 913 |
+
<span className="cp-time">
|
| 914 |
+
{fmt(currentTime)} <em>/</em> {fmt(duration)}
|
| 915 |
+
</span>
|
| 916 |
+
<div className="cp-spacer" />
|
| 917 |
+
|
| 918 |
+
<button
|
| 919 |
+
className={`cp-btn cp-gear ${settings ? "active" : ""}`}
|
| 920 |
+
onClick={() => setSettings(settings ? null : "quality")}
|
| 921 |
+
title="Quality & speed"
|
| 922 |
+
aria-label="Settings"
|
| 923 |
+
>
|
| 924 |
+
<IconGear />
|
| 925 |
+
</button>
|
| 926 |
+
|
| 927 |
+
{pipOk ? (
|
| 928 |
+
<button className="cp-btn" onClick={togglePip} title="Picture-in-Picture" aria-label="Picture in picture">
|
| 929 |
+
<IconPip />
|
| 930 |
+
</button>
|
| 931 |
+
) : null}
|
| 932 |
+
|
| 933 |
+
{castOk ? (
|
| 934 |
+
<button
|
| 935 |
+
className={`cp-btn ${casting ? "active" : ""}`}
|
| 936 |
+
onClick={handleCast}
|
| 937 |
+
title={casting ? "Connected to a cast device" : "Cast / AirPlay"}
|
| 938 |
+
aria-label="Cast"
|
| 939 |
+
>
|
| 940 |
+
<IconCast />
|
| 941 |
+
</button>
|
| 942 |
+
) : null}
|
| 943 |
+
|
| 944 |
+
<button className="cp-btn" onClick={toggleTheaterMode} title="Theater Mode" aria-label="Theater mode">
|
| 945 |
+
<IconTheater />
|
| 946 |
+
</button>
|
| 947 |
+
<button className="cp-btn" onClick={takeScreenshot} title="Screenshot" aria-label="Screenshot">
|
| 948 |
+
<IconScreenshot />
|
| 949 |
+
</button>
|
| 950 |
+
<button className={`cp-btn${loop ? " active" : ""}`} onClick={toggleLoop} title="Loop" aria-label="Loop">
|
| 951 |
+
<IconLoop />
|
| 952 |
+
</button>
|
| 953 |
+
<button className="cp-btn" onClick={toggleFullscreen} title="Fullscreen" aria-label="Fullscreen">
|
| 954 |
+
{fullscreen ? <IconShrink /> : <IconFs />}
|
| 955 |
+
</button>
|
| 956 |
+
</div>
|
| 957 |
+
|
| 958 |
+
{settings ? (
|
| 959 |
+
<div className="cp-menu">
|
| 960 |
+
{qualityOptions.length > 1 ? (
|
| 961 |
+
<>
|
| 962 |
+
<div className="cp-menu-title">Quality</div>
|
| 963 |
+
{isHls && hlsLevel !== -1 ? (
|
| 964 |
+
<button
|
| 965 |
+
className="cp-menu-item"
|
| 966 |
+
onClick={() => switchHlsQuality(-1)}
|
| 967 |
+
>
|
| 968 |
+
Auto
|
| 969 |
+
</button>
|
| 970 |
+
) : null}
|
| 971 |
+
{qualityOptions.map((q) => {
|
| 972 |
+
const active = isHls ? q.key === hlsLevel : q.key === streamIdx;
|
| 973 |
+
return (
|
| 974 |
+
<button
|
| 975 |
+
key={q.key}
|
| 976 |
+
className={`cp-menu-item ${active ? "active" : ""}`}
|
| 977 |
+
onClick={() => (isHls ? switchHlsQuality(q.key) : pickStream(q.key))}
|
| 978 |
+
>
|
| 979 |
+
{q.label}
|
| 980 |
+
</button>
|
| 981 |
+
);
|
| 982 |
+
})}
|
| 983 |
+
</>
|
| 984 |
+
) : null}
|
| 985 |
+
<div className="cp-menu-title">Speed</div>
|
| 986 |
+
{[0.5, 0.75, 1, 1.25, 1.5, 2].map((r) => (
|
| 987 |
+
<button
|
| 988 |
+
key={r}
|
| 989 |
+
className={`cp-menu-item ${rate === r ? "active" : ""}`}
|
| 990 |
+
onClick={() => setSpeed(r)}
|
| 991 |
+
>
|
| 992 |
+
{r}x
|
| 993 |
+
</button>
|
| 994 |
+
))}
|
| 995 |
+
<div className="cp-menu-title">Volume Boost</div>
|
| 996 |
+
{[1, 1.5, 2].map((boost) => (
|
| 997 |
+
<button
|
| 998 |
+
key={boost}
|
| 999 |
+
className={`cp-menu-item ${volumeBoost === boost ? "active" : ""}`}
|
| 1000 |
+
onClick={() => toggleVolumeBoost()}
|
| 1001 |
+
>
|
| 1002 |
+
{boost}x
|
| 1003 |
+
</button>
|
| 1004 |
+
))}
|
| 1005 |
+
<div className="cp-menu-note">Current: {currentQualityLabel}</div>
|
| 1006 |
+
</div>
|
| 1007 |
+
) : null}
|
| 1008 |
+
</div>
|
| 1009 |
+
</div>
|
| 1010 |
+
</div>
|
| 1011 |
+
);
|
| 1012 |
+
}
|
frontend/src/components/LibraryMarquee.jsx
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useNavigate } from "react-router-dom";
|
| 2 |
+
import { api } from "../api.js";
|
| 3 |
+
|
| 4 |
+
export default function LibraryMarquee({ items = [] }) {
|
| 5 |
+
const navigate = useNavigate();
|
| 6 |
+
const list = items.filter((x) => x && x.coverImage);
|
| 7 |
+
if (!list.length) return null;
|
| 8 |
+
|
| 9 |
+
const rowA = list.slice(0, 15);
|
| 10 |
+
// Second row usually offsets by 10; fall back to row A when the library is
|
| 11 |
+
// too small so the reverse row never renders empty.
|
| 12 |
+
const rowB = list.length > 10 ? list.slice(10, 25) : rowA;
|
| 13 |
+
|
| 14 |
+
return (
|
| 15 |
+
<section className="library-section">
|
| 16 |
+
<div className="library-header">
|
| 17 |
+
<div className="library-header-line" />
|
| 18 |
+
<div className="library-header-text">A LIBRARY THAT KEEPS MOVING</div>
|
| 19 |
+
<div className="library-header-line right" />
|
| 20 |
+
</div>
|
| 21 |
+
<div className="marquee-track">
|
| 22 |
+
<div className="marquee-row">
|
| 23 |
+
{rowA.concat(rowA).map((item, idx) => (
|
| 24 |
+
<div key={idx} className="marquee-poster" onClick={() => navigate(`/anime/${item.id}`)}>
|
| 25 |
+
<img src={api.img(item.coverImage)} alt={item.id} loading="lazy" />
|
| 26 |
+
</div>
|
| 27 |
+
))}
|
| 28 |
+
</div>
|
| 29 |
+
<div className="marquee-row reverse">
|
| 30 |
+
{rowB.concat(rowB).map((item, idx) => (
|
| 31 |
+
<div key={idx} className="marquee-poster" onClick={() => navigate(`/anime/${item.id}`)}>
|
| 32 |
+
<img src={api.img(item.coverImage)} alt={item.id} loading="lazy" />
|
| 33 |
+
</div>
|
| 34 |
+
))}
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
</section>
|
| 38 |
+
);
|
| 39 |
+
}
|
frontend/src/components/MangaCard.jsx
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Link } from "react-router-dom";
|
| 2 |
+
import { api } from "../api.js";
|
| 3 |
+
|
| 4 |
+
export default function MangaCard({ manga }) {
|
| 5 |
+
if (!manga?.id) return null;
|
| 6 |
+
const title = manga.title || "Untitled";
|
| 7 |
+
|
| 8 |
+
return (
|
| 9 |
+
<Link
|
| 10 |
+
to={`/manga/${manga.source}/${encodeURIComponent(manga.id)}`}
|
| 11 |
+
className="anime-card manga-card"
|
| 12 |
+
title={title}
|
| 13 |
+
>
|
| 14 |
+
<div className="card-poster">
|
| 15 |
+
{manga.cover ? (
|
| 16 |
+
<img
|
| 17 |
+
src={api.mangaImg(manga.cover, manga.source)}
|
| 18 |
+
alt={title}
|
| 19 |
+
loading="lazy"
|
| 20 |
+
/>
|
| 21 |
+
) : (
|
| 22 |
+
<div className="skeleton" style={{ width: "100%", height: "100%" }} />
|
| 23 |
+
)}
|
| 24 |
+
<div className="card-overlay" />
|
| 25 |
+
{manga.latest ? (
|
| 26 |
+
<div className="card-ep-badge">CH {manga.latest}</div>
|
| 27 |
+
) : null}
|
| 28 |
+
<div className="card-quick">
|
| 29 |
+
<div className="card-quick-btn">
|
| 30 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
| 31 |
+
<path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
| 32 |
+
</svg>
|
| 33 |
+
Read Now
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
<div className="card-body">
|
| 38 |
+
<div className="card-title">{title}</div>
|
| 39 |
+
<div className="card-meta">
|
| 40 |
+
{manga.type ? <span className="card-meta-item">{manga.type}</span> : null}
|
| 41 |
+
{manga.status ? <span className="card-meta-item">• {manga.status}</span> : null}
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
</Link>
|
| 45 |
+
);
|
| 46 |
+
}
|
frontend/src/components/MediaCard.jsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Link } from "react-router-dom";
|
| 2 |
+
import { api, titleOf } from "../api.js";
|
| 3 |
+
|
| 4 |
+
export default function MediaCard({ media }) {
|
| 5 |
+
const title = titleOf(media);
|
| 6 |
+
const sub = [media.seasonYear, media.format].filter(Boolean).join(" · ");
|
| 7 |
+
|
| 8 |
+
return (
|
| 9 |
+
<Link to={`/anime/${media.id}`} className="card" title={title}>
|
| 10 |
+
<div className="card-poster">
|
| 11 |
+
{media.score ? <span className="card-score">{media.score}%</span> : null}
|
| 12 |
+
{media.coverImage ? (
|
| 13 |
+
<img src={api.img(media.coverImage)} alt={title} loading="lazy" />
|
| 14 |
+
) : (
|
| 15 |
+
<div className="skeleton" style={{ width: "100%", height: "100%" }} />
|
| 16 |
+
)}
|
| 17 |
+
<div className="card-overlay">
|
| 18 |
+
<span className="card-play">
|
| 19 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
| 20 |
+
<path d="M8 5v14l11-7z" />
|
| 21 |
+
</svg>
|
| 22 |
+
</span>
|
| 23 |
+
<span className="card-eps">
|
| 24 |
+
{media.episodes ? `${media.episodes} ep` : media.status || ""}
|
| 25 |
+
</span>
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
<div className="card-body">
|
| 29 |
+
<h3>{title}</h3>
|
| 30 |
+
{sub ? <div className="sub">{sub}</div> : null}
|
| 31 |
+
</div>
|
| 32 |
+
</Link>
|
| 33 |
+
);
|
| 34 |
+
}
|
frontend/src/components/MediaRow.jsx
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import MediaCard from "./MediaCard.jsx";
|
| 2 |
+
|
| 3 |
+
export default function MediaRow({ title, items = [], to }) {
|
| 4 |
+
if (!items.length) return null;
|
| 5 |
+
return (
|
| 6 |
+
<section className="section-reveal">
|
| 7 |
+
<div className="section-head">
|
| 8 |
+
<h2>{title}</h2>
|
| 9 |
+
{to ? (
|
| 10 |
+
<a className="link" href={to}>
|
| 11 |
+
See all →
|
| 12 |
+
</a>
|
| 13 |
+
) : null}
|
| 14 |
+
</div>
|
| 15 |
+
<div className="row">
|
| 16 |
+
{items.map((m) => (
|
| 17 |
+
<MediaCard key={m.id} media={m} />
|
| 18 |
+
))}
|
| 19 |
+
</div>
|
| 20 |
+
</section>
|
| 21 |
+
);
|
| 22 |
+
}
|
frontend/src/components/MobileNav.jsx
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NavLink, useLocation, useNavigate } from "react-router-dom";
|
| 2 |
+
|
| 3 |
+
const ICON = {
|
| 4 |
+
home: (
|
| 5 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
| 6 |
+
<path d="M3 10.5 12 3l9 7.5" />
|
| 7 |
+
<path d="M5 9.5V21h14V9.5" />
|
| 8 |
+
</svg>
|
| 9 |
+
),
|
| 10 |
+
browse: (
|
| 11 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
| 12 |
+
<circle cx="11" cy="11" r="7" />
|
| 13 |
+
<path d="m20 20-3.5-3.5" />
|
| 14 |
+
</svg>
|
| 15 |
+
),
|
| 16 |
+
search: (
|
| 17 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
| 18 |
+
<circle cx="11" cy="11" r="7" />
|
| 19 |
+
<path d="m20 20-3.5-3.5" />
|
| 20 |
+
</svg>
|
| 21 |
+
),
|
| 22 |
+
schedule: (
|
| 23 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
| 24 |
+
<rect x="3" y="4" width="18" height="18" rx="2" />
|
| 25 |
+
<path d="M16 2v4M8 2v4M3 10h18" />
|
| 26 |
+
</svg>
|
| 27 |
+
),
|
| 28 |
+
movies: (
|
| 29 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
| 30 |
+
<rect x="2" y="4" width="20" height="16" rx="2" />
|
| 31 |
+
<path d="M7 4v16M17 4v16M2 9h5M2 15h5M17 9h5M17 15h5" />
|
| 32 |
+
</svg>
|
| 33 |
+
),
|
| 34 |
+
manga: (
|
| 35 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
| 36 |
+
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
|
| 37 |
+
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
|
| 38 |
+
</svg>
|
| 39 |
+
),
|
| 40 |
+
music: (
|
| 41 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
| 42 |
+
<path d="M9 18V5l12-2v13" />
|
| 43 |
+
<circle cx="6" cy="18" r="3" />
|
| 44 |
+
<circle cx="18" cy="16" r="3" />
|
| 45 |
+
</svg>
|
| 46 |
+
),
|
| 47 |
+
profile: (
|
| 48 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
| 49 |
+
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
| 50 |
+
<circle cx="12" cy="7" r="4" />
|
| 51 |
+
</svg>
|
| 52 |
+
),
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
const ITEMS = [
|
| 56 |
+
{ to: "/home", label: "Home", icon: ICON.home },
|
| 57 |
+
{ to: "/browse", label: "Browse", icon: ICON.browse },
|
| 58 |
+
{ to: "/movies", label: "Movies", icon: ICON.movies },
|
| 59 |
+
{ to: "/schedule", label: "Schedule", icon: ICON.schedule },
|
| 60 |
+
{ to: "/manga", label: "Manga", icon: ICON.manga },
|
| 61 |
+
{ to: "/music", label: "Music", icon: ICON.music },
|
| 62 |
+
{ to: "/home", label: "Profile", icon: ICON.profile, action: "profile" },
|
| 63 |
+
];
|
| 64 |
+
|
| 65 |
+
export default function MobileNav() {
|
| 66 |
+
const { pathname } = useLocation();
|
| 67 |
+
const navigate = useNavigate();
|
| 68 |
+
|
| 69 |
+
const isFullscreen =
|
| 70 |
+
pathname.startsWith("/watch") ||
|
| 71 |
+
pathname === "/manga/read" ||
|
| 72 |
+
pathname.startsWith("/manga/read/") ||
|
| 73 |
+
/^\/movies\/.+\/watch$/.test(pathname);
|
| 74 |
+
if (isFullscreen) return null;
|
| 75 |
+
|
| 76 |
+
const isActive = (to, action) => {
|
| 77 |
+
if (action === "profile") return pathname === "/home" || pathname === "/";
|
| 78 |
+
if (to === "/browse") return pathname === "/browse" || pathname === "/search";
|
| 79 |
+
return pathname === to;
|
| 80 |
+
};
|
| 81 |
+
|
| 82 |
+
const handleClick = (item) => {
|
| 83 |
+
if (item.action === "profile") {
|
| 84 |
+
window.dispatchEvent(new CustomEvent("open-auth-modal"));
|
| 85 |
+
return;
|
| 86 |
+
}
|
| 87 |
+
if (item.to) navigate(item.to);
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
return (
|
| 91 |
+
<nav className="mobile-nav" aria-label="Mobile navigation">
|
| 92 |
+
{ITEMS.map(({ to, label, icon, action }) => (
|
| 93 |
+
<button
|
| 94 |
+
key={`${label}-${to}`}
|
| 95 |
+
type="button"
|
| 96 |
+
onClick={() => handleClick({ to, action })}
|
| 97 |
+
className={`mobile-nav-item${isActive(to, action) ? " active" : ""}`}
|
| 98 |
+
aria-label={label}
|
| 99 |
+
>
|
| 100 |
+
{icon}
|
| 101 |
+
<span className="mobile-nav-label">{label}</span>
|
| 102 |
+
</button>
|
| 103 |
+
))}
|
| 104 |
+
</nav>
|
| 105 |
+
);
|
| 106 |
+
}
|
frontend/src/components/MovieCard.jsx
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useNavigate } from "react-router-dom";
|
| 2 |
+
import { api } from "../api.js";
|
| 3 |
+
|
| 4 |
+
export default function MovieCard({ item, rank = null }) {
|
| 5 |
+
const navigate = useNavigate();
|
| 6 |
+
if (!item?.slug) return null;
|
| 7 |
+
const title = item.title || "Untitled";
|
| 8 |
+
const comingSoon = item.upcoming || item.badge === "Coming Soon";
|
| 9 |
+
|
| 10 |
+
return (
|
| 11 |
+
<div className="anime-card" onClick={() => navigate(`/movies/${item.slug}`)} title={title}>
|
| 12 |
+
<div className="card-poster">
|
| 13 |
+
{item.cover ? (
|
| 14 |
+
<img src={api.img(item.cover)} alt={title} loading="lazy" />
|
| 15 |
+
) : (
|
| 16 |
+
<div className="skeleton" style={{ width: "100%", height: "100%" }} />
|
| 17 |
+
)}
|
| 18 |
+
<div className="card-overlay" />
|
| 19 |
+
{rank ? <div className="card-rank">{rank}</div> : null}
|
| 20 |
+
{item.badge ? (
|
| 21 |
+
<div className={`card-type-badge${comingSoon ? " coming-soon" : ""}`}>{item.badge}</div>
|
| 22 |
+
) : null}
|
| 23 |
+
<div className="card-quick">
|
| 24 |
+
<div className={`card-quick-btn${comingSoon ? " coming-soon" : ""}`}>
|
| 25 |
+
{comingSoon ? (
|
| 26 |
+
<>
|
| 27 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
| 28 |
+
<path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm1 10.59 3.66 2.2-.67 1.11L11 13.02V7h2z" />
|
| 29 |
+
</svg>
|
| 30 |
+
Coming Soon
|
| 31 |
+
</>
|
| 32 |
+
) : (
|
| 33 |
+
<>
|
| 34 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
| 35 |
+
<polygon points="5 3 19 12 5 21 5 3" />
|
| 36 |
+
</svg>
|
| 37 |
+
Watch Now
|
| 38 |
+
</>
|
| 39 |
+
)}
|
| 40 |
+
</div>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
<div className="card-body">
|
| 44 |
+
<div className="card-title" title={title}>{title}</div>
|
| 45 |
+
<div className="card-meta">
|
| 46 |
+
{item.rating ? (
|
| 47 |
+
<span className="card-score">
|
| 48 |
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor">
|
| 49 |
+
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
|
| 50 |
+
</svg>
|
| 51 |
+
{item.rating}
|
| 52 |
+
</span>
|
| 53 |
+
) : null}
|
| 54 |
+
{item.year ? <span className="card-meta-item">• {item.year}</span> : null}
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
);
|
| 59 |
+
}
|
frontend/src/components/MovieRow.jsx
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useRef } from "react";
|
| 2 |
+
import MovieCard from "./MovieCard.jsx";
|
| 3 |
+
|
| 4 |
+
export default function MovieRow({ title, items = [], isRanked = false }) {
|
| 5 |
+
const rowRef = useRef(null);
|
| 6 |
+
|
| 7 |
+
function scroll(dir) {
|
| 8 |
+
if (!rowRef.current) return;
|
| 9 |
+
const scrollAmount = dir === "left" ? -600 : 600;
|
| 10 |
+
rowRef.current.scrollBy({ left: scrollAmount, behavior: "smooth" });
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
if (!items || items.length === 0) return null;
|
| 14 |
+
|
| 15 |
+
return (
|
| 16 |
+
<section className="content-section">
|
| 17 |
+
<div className="container">
|
| 18 |
+
<div className="section-header">
|
| 19 |
+
<div className="section-title">{title}</div>
|
| 20 |
+
</div>
|
| 21 |
+
<div className="genre-nav-wrap">
|
| 22 |
+
<button className="row-nav-btn prev" onClick={() => scroll("left")} aria-label="Scroll left">
|
| 23 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
| 24 |
+
<path d="M15 18l-6-6 6-6" />
|
| 25 |
+
</svg>
|
| 26 |
+
</button>
|
| 27 |
+
<div className="scroll-row" ref={rowRef}>
|
| 28 |
+
{items.map((item, index) => (
|
| 29 |
+
<MovieCard key={item.slug || index} item={item} rank={isRanked ? index + 1 : null} />
|
| 30 |
+
))}
|
| 31 |
+
</div>
|
| 32 |
+
<button className="row-nav-btn next" onClick={() => scroll("right")} aria-label="Scroll right">
|
| 33 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
| 34 |
+
<path d="M9 18l6-6-6-6" />
|
| 35 |
+
</svg>
|
| 36 |
+
</button>
|
| 37 |
+
</div>
|
| 38 |
+
</div>
|
| 39 |
+
</section>
|
| 40 |
+
);
|
| 41 |
+
}
|
frontend/src/components/Sidebar.jsx
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from "react";
|
| 2 |
+
import { Link, NavLink, useNavigate } from "react-router-dom";
|
| 3 |
+
|
| 4 |
+
const ICONS = {
|
| 5 |
+
home: (
|
| 6 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
| 7 |
+
<path d="M3 10.5 12 3l9 7.5" />
|
| 8 |
+
<path d="M5 9.5V21h14V9.5" />
|
| 9 |
+
</svg>
|
| 10 |
+
),
|
| 11 |
+
search: (
|
| 12 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
| 13 |
+
<circle cx="11" cy="11" r="7" />
|
| 14 |
+
<path d="m20 20-3.5-3.5" />
|
| 15 |
+
</svg>
|
| 16 |
+
),
|
| 17 |
+
book: (
|
| 18 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
| 19 |
+
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
|
| 20 |
+
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
|
| 21 |
+
</svg>
|
| 22 |
+
),
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
function NavItem({ to, end, label, icon, onClick }) {
|
| 26 |
+
return (
|
| 27 |
+
<NavLink
|
| 28 |
+
to={to}
|
| 29 |
+
end={end}
|
| 30 |
+
className={({ isActive }) => `nav-item${isActive ? " active" : ""}`}
|
| 31 |
+
onClick={onClick}
|
| 32 |
+
>
|
| 33 |
+
<span className="nav-icon">{ICONS[icon]}</span>
|
| 34 |
+
{label}
|
| 35 |
+
</NavLink>
|
| 36 |
+
);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
export default function Sidebar() {
|
| 40 |
+
const navigate = useNavigate();
|
| 41 |
+
const [open, setOpen] = useState(false);
|
| 42 |
+
const [q, setQ] = useState("");
|
| 43 |
+
|
| 44 |
+
useEffect(() => {
|
| 45 |
+
const onKey = (e) => {
|
| 46 |
+
if (e.key === "Escape") setOpen(false);
|
| 47 |
+
};
|
| 48 |
+
window.addEventListener("keydown", onKey);
|
| 49 |
+
return () => window.removeEventListener("keydown", onKey);
|
| 50 |
+
}, []);
|
| 51 |
+
|
| 52 |
+
function submit(e) {
|
| 53 |
+
e.preventDefault();
|
| 54 |
+
if (!q.trim()) return;
|
| 55 |
+
close();
|
| 56 |
+
navigate(`/search?q=${encodeURIComponent(q.trim())}`);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
return (
|
| 60 |
+
<>
|
| 61 |
+
<header className="topbar">
|
| 62 |
+
<span className="mark">◈</span>
|
| 63 |
+
<span className="topbar-title">anicove</span>
|
| 64 |
+
<button
|
| 65 |
+
className={`menu-btn${open ? " active" : ""}`}
|
| 66 |
+
onClick={() => setOpen((v) => !v)}
|
| 67 |
+
aria-label="Toggle menu"
|
| 68 |
+
>
|
| 69 |
+
<span />
|
| 70 |
+
<span />
|
| 71 |
+
<span />
|
| 72 |
+
</button>
|
| 73 |
+
</header>
|
| 74 |
+
|
| 75 |
+
<div className={`overlay${open ? " open" : ""}`} onClick={close} />
|
| 76 |
+
|
| 77 |
+
<aside className={`sidebar${open ? " open" : ""}`}>
|
| 78 |
+
<div className="logo">
|
| 79 |
+
<Link to="/home" onClick={close} style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
| 80 |
+
<span className="mark">◈</span>
|
| 81 |
+
<span className="logo-text">anicove</span>
|
| 82 |
+
</Link>
|
| 83 |
+
<span className="logo-version">v0.1</span>
|
| 84 |
+
</div>
|
| 85 |
+
|
| 86 |
+
<form className="sidebar-search" onSubmit={submit}>
|
| 87 |
+
{ICONS.search}
|
| 88 |
+
<input
|
| 89 |
+
value={q}
|
| 90 |
+
onChange={(e) => setQ(e.target.value)}
|
| 91 |
+
placeholder="Search anime…"
|
| 92 |
+
aria-label="Search anime"
|
| 93 |
+
/>
|
| 94 |
+
</form>
|
| 95 |
+
|
| 96 |
+
<nav className="nav">
|
| 97 |
+
<div className="nav-section">
|
| 98 |
+
<div className="nav-label">Browse</div>
|
| 99 |
+
<NavItem to="/home" label="Home" icon="home" onClick={close} />
|
| 100 |
+
<NavItem to="/search" label="Search" icon="search" onClick={close} />
|
| 101 |
+
</div>
|
| 102 |
+
<div className="nav-section">
|
| 103 |
+
<div className="nav-label">Library</div>
|
| 104 |
+
<NavItem to="/manga" label="Manga" icon="book" onClick={close} />
|
| 105 |
+
</div>
|
| 106 |
+
</nav>
|
| 107 |
+
|
| 108 |
+
<div className="sidebar-foot">
|
| 109 |
+
Streaming via <b>Anicove</b> · 13 providers
|
| 110 |
+
<br />
|
| 111 |
+
Fallback: <b>Aniraku</b>
|
| 112 |
+
</div>
|
| 113 |
+
</aside>
|
| 114 |
+
</>
|
| 115 |
+
);
|
| 116 |
+
}
|
frontend/src/components/Toast.jsx
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from "react";
|
| 2 |
+
|
| 3 |
+
export default function Toast({ message, duration = 3500, onClose }) {
|
| 4 |
+
const [visible, setVisible] = useState(true);
|
| 5 |
+
|
| 6 |
+
useEffect(() => {
|
| 7 |
+
const timer = setTimeout(() => {
|
| 8 |
+
setVisible(false);
|
| 9 |
+
setTimeout(onClose, 300);
|
| 10 |
+
}, duration);
|
| 11 |
+
return () => clearTimeout(timer);
|
| 12 |
+
}, [duration, onClose]);
|
| 13 |
+
|
| 14 |
+
if (!visible) return null;
|
| 15 |
+
|
| 16 |
+
return (
|
| 17 |
+
<div className="auth-toast">
|
| 18 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
| 19 |
+
<polyline points="20,6 9,17 4,12" />
|
| 20 |
+
</svg>
|
| 21 |
+
{message}
|
| 22 |
+
</div>
|
| 23 |
+
);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export function showToast(message, duration = 3500) {
|
| 27 |
+
const container = document.getElementById('toast-container') || createToastContainer();
|
| 28 |
+
const toastEl = document.createElement('div');
|
| 29 |
+
toastEl.className = 'auth-toast';
|
| 30 |
+
toastEl.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,6 9,17 4,12"/></svg> ${message}`;
|
| 31 |
+
container.appendChild(toastEl);
|
| 32 |
+
setTimeout(() => {
|
| 33 |
+
toastEl.style.opacity = '0';
|
| 34 |
+
toastEl.style.transform = 'translateY(16px)';
|
| 35 |
+
setTimeout(() => toastEl.remove(), 300);
|
| 36 |
+
}, duration);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
function createToastContainer() {
|
| 40 |
+
const container = document.createElement('div');
|
| 41 |
+
container.id = 'toast-container';
|
| 42 |
+
container.style.cssText = 'position:fixed;bottom:24px;right:24px;z-index:99999;display:flex;flex-direction:column;gap:10px;';
|
| 43 |
+
document.body.appendChild(container);
|
| 44 |
+
return container;
|
| 45 |
+
}
|
frontend/src/index.css
ADDED
|
@@ -0,0 +1,1072 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ANICOVE UI � Complete Design System */
|
| 2 |
+
:root {
|
| 3 |
+
--bg-base: #080808;
|
| 4 |
+
--bg-surface: #0d0d0d;
|
| 5 |
+
--bg-elevated: #111111;
|
| 6 |
+
--bg-card: #0f0f0f;
|
| 7 |
+
--bg-card-hover: #1a1a1a;
|
| 8 |
+
--accent-v: #7c3aed;
|
| 9 |
+
--accent-v2: #9d6ef8;
|
| 10 |
+
--accent-c: #5b21b6;
|
| 11 |
+
--accent-c2: #c4b5fd;
|
| 12 |
+
--accent-green: #22c55e;
|
| 13 |
+
--accent-amber: #f59e0b;
|
| 14 |
+
--grad-accent: linear-gradient(135deg, #7c3aed, #4c1d95);
|
| 15 |
+
--grad-card: linear-gradient(180deg, transparent 35%, rgba(8,8,8,0.99) 100%);
|
| 16 |
+
--text-primary: #ffffff;
|
| 17 |
+
--text-secondary: #777790;
|
| 18 |
+
--text-muted: #404050;
|
| 19 |
+
--border: rgba(255,255,255,0.07);
|
| 20 |
+
--border-bright: rgba(124,58,237,0.5);
|
| 21 |
+
--glass-border: rgba(255,255,255,0.06);
|
| 22 |
+
--radius-sm: 6px;
|
| 23 |
+
--radius-md: 10px;
|
| 24 |
+
--radius-lg: 16px;
|
| 25 |
+
--radius-pill: 999px;
|
| 26 |
+
--shadow-hover: 0 8px 40px rgba(124,58,237,0.35);
|
| 27 |
+
--transition: all 0.22s cubic-bezier(0.4,0,0.2,1);
|
| 28 |
+
--header-h: 62px;
|
| 29 |
+
/* Legacy layout cap keeps the 158px cards visually dense instead of stretching across ultra-wide screens. */
|
| 30 |
+
--max-w: 1380px;
|
| 31 |
+
}
|
| 32 |
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
| 33 |
+
html{scroll-behavior:smooth;scrollbar-width:none}
|
| 34 |
+
::-webkit-scrollbar{display:none}
|
| 35 |
+
body{font-family:'Outfit','Inter',sans-serif;background:
|
| 36 |
+
radial-gradient(circle at top, rgba(124,58,237,0.22), transparent 32%),
|
| 37 |
+
linear-gradient(180deg, #06070a 0%, #0b0b12 36%, #08090d 100%);
|
| 38 |
+
color:var(--text-primary);min-height:100vh;line-height:1.6;overflow-x:hidden}
|
| 39 |
+
#root{min-height:100vh}
|
| 40 |
+
img{display:block;max-width:100%}
|
| 41 |
+
a{color:inherit;text-decoration:none}
|
| 42 |
+
button{cursor:pointer;border:none;background:none;font-family:inherit;color:inherit}
|
| 43 |
+
ul,ol{list-style:none}
|
| 44 |
+
input{font-family:inherit}
|
| 45 |
+
.container{max-width:var(--max-w);margin:0 auto;padding:0 28px}
|
| 46 |
+
.main-content{position:relative;min-height:100vh;overflow-x:hidden}
|
| 47 |
+
.page-shell{padding-top:calc(var(--header-h) + 30px);padding-bottom:80px}
|
| 48 |
+
.catalog-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:20px 16px}
|
| 49 |
+
.search-shell{display:flex;flex-direction:column;gap:18px}
|
| 50 |
+
.section-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:22px}
|
| 51 |
+
.section-header.compact{margin-bottom:16px}
|
| 52 |
+
|
| 53 |
+
/* HEADER */
|
| 54 |
+
#header{position:fixed;top:0;left:0;right:0;z-index:100;height:var(--header-h);transition:var(--transition)}
|
| 55 |
+
#header::before{content:'';position:absolute;inset:0;background:linear-gradient(180deg, rgba(10,10,18,0.82), rgba(10,10,18,0.38));backdrop-filter:blur(14px);opacity:0;transition:var(--transition);pointer-events:none}
|
| 56 |
+
#header.scrolled::before,#header:not(.scrolled){opacity:1}
|
| 57 |
+
#header.scrolled{border-bottom:1px solid var(--border);box-shadow:0 12px 28px rgba(0,0,0,0.18)}
|
| 58 |
+
#header .container{height:100%;display:flex;align-items:center;gap:24px;position:relative;z-index:1}
|
| 59 |
+
.logo{display:flex;align-items:center;flex-shrink:0}
|
| 60 |
+
.logo-wordmark{display:flex;align-items:flex-end;line-height:1}
|
| 61 |
+
.logo-wordmark .l{font-family:'Bebas Neue','Outfit',sans-serif;font-size:2.2rem;font-weight:400;line-height:1;color:#fff;letter-spacing:0.02em}
|
| 62 |
+
nav{display:flex;align-items:center;gap:2px}
|
| 63 |
+
nav a{padding:6px 14px;border-radius:var(--radius-pill);font-size:0.88rem;font-weight:500;color:var(--text-secondary);transition:var(--transition)}
|
| 64 |
+
nav a:hover,nav a.active{color:var(--text-primary);background:rgba(139,92,246,0.15)}
|
| 65 |
+
.search-wrap{flex:1;max-width:360px;position:relative}
|
| 66 |
+
.search-input{width:100%;padding:9px 16px 9px 42px;border-radius:var(--radius-pill);background:rgba(255,255,255,0.06);border:1px solid var(--border);color:var(--text-primary);font-size:0.88rem;outline:none;transition:var(--transition)}
|
| 67 |
+
.search-input::placeholder{color:var(--text-muted)}
|
| 68 |
+
.search-input:focus{background:rgba(255,255,255,0.1);border-color:var(--accent-v);box-shadow:0 0 0 3px rgba(139,92,246,0.12)}
|
| 69 |
+
.search-icon{position:absolute;left:14px;top:50%;transform:translateY(-50%);pointer-events:none;color:var(--text-muted)}
|
| 70 |
+
.search-icon svg{width:16px;height:16px}
|
| 71 |
+
.auth-nav{margin-left:auto;flex-shrink:0}
|
| 72 |
+
.auth-avatar-btn{display:flex;align-items:center;gap:8px;padding:7px 14px 7px 8px;border-radius:var(--radius-pill);background:rgba(255,255,255,0.06);border:1px solid var(--border);transition:var(--transition);font-size:0.85rem;font-weight:700;letter-spacing:0.01em;color:var(--text-primary);box-shadow:0 8px 18px rgba(0,0,0,0.12)}
|
| 73 |
+
.auth-avatar-btn:hover{background:rgba(255,255,255,0.1);border-color:var(--border-bright);transform:translateY(-1px)}
|
| 74 |
+
.auth-avatar-btn.open{border-color:var(--border-bright);background:linear-gradient(180deg, rgba(124,58,237,0.18), rgba(255,255,255,0.08));box-shadow:0 12px 30px rgba(124,58,237,0.18)}
|
| 75 |
+
.auth-avatar-btn.guest{background:linear-gradient(135deg, rgba(124,58,237,0.18), rgba(255,255,255,0.08));border-color:rgba(124,58,237,0.42)}
|
| 76 |
+
.auth-avatar-btn.guest:hover{border-color:rgba(124,58,237,0.7);box-shadow:0 12px 32px rgba(124,58,237,0.22)}
|
| 77 |
+
.auth-avatar-circle{width:28px;height:28px;border-radius:50%;background:rgba(255,255,255,0.08);display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
| 78 |
+
.auth-avatar-icon{background:linear-gradient(135deg, rgba(124,58,237,0.26), rgba(255,255,255,0.08));color:var(--accent-v2)}
|
| 79 |
+
.auth-avatar-img{object-fit:cover}
|
| 80 |
+
.auth-nav{position:relative}
|
| 81 |
+
|
| 82 |
+
@media (max-width: 1024px) {
|
| 83 |
+
.container {
|
| 84 |
+
padding: 0 20px;
|
| 85 |
+
}
|
| 86 |
+
nav {
|
| 87 |
+
display: none;
|
| 88 |
+
}
|
| 89 |
+
.search-wrap {
|
| 90 |
+
max-width: 260px;
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
@media (max-width: 768px) {
|
| 95 |
+
.container {
|
| 96 |
+
padding: 0 16px;
|
| 97 |
+
}
|
| 98 |
+
.search-wrap {
|
| 99 |
+
display: none;
|
| 100 |
+
}
|
| 101 |
+
.anime-card {
|
| 102 |
+
width: 130px;
|
| 103 |
+
}
|
| 104 |
+
.hero-next,
|
| 105 |
+
.hero-prev {
|
| 106 |
+
display: none !important;
|
| 107 |
+
}
|
| 108 |
+
.hero-cover-wrap {
|
| 109 |
+
display: none;
|
| 110 |
+
}
|
| 111 |
+
.home-layout {
|
| 112 |
+
grid-template-columns: 1fr;
|
| 113 |
+
gap: 14px;
|
| 114 |
+
}
|
| 115 |
+
.home-sidebar {
|
| 116 |
+
position: static;
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
@media (max-width: 680px){
|
| 121 |
+
.container {
|
| 122 |
+
padding: 0 16px;
|
| 123 |
+
}
|
| 124 |
+
.auth-avatar-btn {
|
| 125 |
+
gap: 0;
|
| 126 |
+
padding: 5px 8px 5px 6px;
|
| 127 |
+
min-width: 42px;
|
| 128 |
+
justify-content: center;
|
| 129 |
+
}
|
| 130 |
+
.auth-avatar-btn span,
|
| 131 |
+
.auth-avatar-btn svg:last-child{
|
| 132 |
+
display: none;
|
| 133 |
+
}
|
| 134 |
+
.auth-avatar-circle {
|
| 135 |
+
width: 26px;
|
| 136 |
+
height: 26px;
|
| 137 |
+
}
|
| 138 |
+
.auth-avatar-icon svg {
|
| 139 |
+
width: 14px;
|
| 140 |
+
height: 14px;
|
| 141 |
+
}
|
| 142 |
+
.main-content {
|
| 143 |
+
padding-bottom: calc(66px + env(safe-area-inset-bottom));
|
| 144 |
+
}
|
| 145 |
+
.home-layout {
|
| 146 |
+
grid-template-columns: 1fr;
|
| 147 |
+
gap: 14px;
|
| 148 |
+
}
|
| 149 |
+
.spotlight-featured {
|
| 150 |
+
min-height: 262px;
|
| 151 |
+
border-radius: 14px;
|
| 152 |
+
}
|
| 153 |
+
.spotlight-featured-body {
|
| 154 |
+
padding: 12px 12px 38px;
|
| 155 |
+
}
|
| 156 |
+
.spotlight-featured-title {
|
| 157 |
+
font-size: 1.28rem;
|
| 158 |
+
margin-bottom: 4px;
|
| 159 |
+
}
|
| 160 |
+
.spotlight-nav-btn {
|
| 161 |
+
width: 30px;
|
| 162 |
+
height: 30px;
|
| 163 |
+
opacity: 1;
|
| 164 |
+
}
|
| 165 |
+
.spotlight-nav-btn.prev {
|
| 166 |
+
left: 8px;
|
| 167 |
+
}
|
| 168 |
+
.spotlight-nav-btn.next {
|
| 169 |
+
right: 8px;
|
| 170 |
+
}
|
| 171 |
+
.genre-row-wrap {
|
| 172 |
+
gap: 8px;
|
| 173 |
+
}
|
| 174 |
+
.genre-row {
|
| 175 |
+
gap: 8px;
|
| 176 |
+
}
|
| 177 |
+
.genre-pill {
|
| 178 |
+
padding: 7px 12px;
|
| 179 |
+
font-size: 0.74rem;
|
| 180 |
+
}
|
| 181 |
+
.section-header {
|
| 182 |
+
margin-bottom: 14px;
|
| 183 |
+
}
|
| 184 |
+
.section-title {
|
| 185 |
+
font-size: 1.02rem;
|
| 186 |
+
}
|
| 187 |
+
.section-title::before {
|
| 188 |
+
height: 19px;
|
| 189 |
+
}
|
| 190 |
+
.scroll-row {
|
| 191 |
+
gap: 10px;
|
| 192 |
+
}
|
| 193 |
+
.anime-card {
|
| 194 |
+
width: 138px;
|
| 195 |
+
}
|
| 196 |
+
.card-body {
|
| 197 |
+
padding: 8px 9px 10px;
|
| 198 |
+
}
|
| 199 |
+
.card-title {
|
| 200 |
+
font-size: 0.78rem;
|
| 201 |
+
}
|
| 202 |
+
.btn {
|
| 203 |
+
padding: 10px 18px;
|
| 204 |
+
font-size: 0.85rem;
|
| 205 |
+
}
|
| 206 |
+
.btn-sm {
|
| 207 |
+
padding: 6px 13px;
|
| 208 |
+
font-size: 0.78rem;
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
@media (max-width: 420px) {
|
| 213 |
+
.anime-card {
|
| 214 |
+
width: 126px;
|
| 215 |
+
}
|
| 216 |
+
.hero-ctas {
|
| 217 |
+
flex-direction: column;
|
| 218 |
+
}
|
| 219 |
+
.hero-cta {
|
| 220 |
+
width: 100%;
|
| 221 |
+
justify-content: center;
|
| 222 |
+
}
|
| 223 |
+
.spotlight-featured {
|
| 224 |
+
min-height: 240px;
|
| 225 |
+
}
|
| 226 |
+
.auth-dropdown {
|
| 227 |
+
left: 12px;
|
| 228 |
+
right: 12px;
|
| 229 |
+
min-width: 0;
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
/* ACCOUNT DROPDOWN */
|
| 234 |
+
.auth-dropdown{position:absolute;top:calc(100% + 10px);right:0;z-index:130;min-width:240px;background:rgba(12,12,20,0.96);backdrop-filter:blur(18px);border:1px solid var(--border-bright);border-radius:var(--radius-lg);box-shadow:0 22px 52px rgba(0,0,0,0.7);padding:10px;animation:fadeUp .18s ease both;overflow:hidden}
|
| 235 |
+
.auth-dropdown-head{padding:12px 12px 10px;border-bottom:1px solid var(--border);margin-bottom:8px;background:linear-gradient(180deg, rgba(124,58,237,0.1), transparent);border-radius:8px 8px 0 0}
|
| 236 |
+
.auth-dropdown-name{font-size:0.9rem;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
| 237 |
+
.auth-dropdown-sub{font-size:0.72rem;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
| 238 |
+
.auth-dropdown-item{display:flex;align-items:center;gap:9px;width:100%;padding:10px 12px;border-radius:var(--radius-md);font-size:0.85rem;font-weight:600;color:var(--text-primary);transition:var(--transition);cursor:pointer;background:none;border:none;text-align:left;margin-top:2px}
|
| 239 |
+
.auth-dropdown-item svg{color:var(--accent-v2);flex-shrink:0}
|
| 240 |
+
.auth-dropdown-item:hover{background:linear-gradient(90deg, rgba(124,58,237,0.16), rgba(255,255,255,0.03));border-color:transparent}
|
| 241 |
+
.auth-dropdown-item.danger{color:#fda4af}
|
| 242 |
+
.auth-dropdown-item.danger svg{color:#fda4af}
|
| 243 |
+
.auth-dropdown-item.danger:hover{background:rgba(225,29,72,0.12)}
|
| 244 |
+
|
| 245 |
+
/* AUTH MODAL */
|
| 246 |
+
.auth-overlay{position:fixed;inset:0;z-index:200;background:rgba(5,5,10,0.72);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;padding:20px;animation:fadeIn .2s ease both}
|
| 247 |
+
.auth-modal{position:relative;width:100%;max-width:400px;background:var(--bg-surface);border:1px solid var(--border-bright);border-radius:var(--radius-lg);padding:30px 28px 26px;box-shadow:0 30px 90px rgba(0,0,0,0.7);animation:fadeUp .22s ease both}
|
| 248 |
+
.auth-modal-close{position:absolute;top:14px;right:14px;width:30px;height:30px;border-radius:50%;background:rgba(255,255,255,0.06);border:1px solid var(--border);color:var(--text-secondary);font-size:0.85rem;display:flex;align-items:center;justify-content:center;transition:var(--transition);cursor:pointer}
|
| 249 |
+
.auth-modal-close:hover{background:rgba(255,255,255,0.12);color:#fff}
|
| 250 |
+
.auth-modal-logo{font-family:'Bebas Neue','Outfit',sans-serif;font-size:1.9rem;letter-spacing:0.05em;text-align:center;margin-bottom:20px;color:#fff}
|
| 251 |
+
.auth-tabs{display:flex;background:rgba(255,255,255,0.05);border:1px solid var(--border);border-radius:var(--radius-pill);padding:4px;margin-bottom:20px}
|
| 252 |
+
.auth-tab{flex:1;padding:8px 0;border-radius:var(--radius-pill);font-size:0.84rem;font-weight:700;color:var(--text-secondary);transition:var(--transition);cursor:pointer;background:none;border:none}
|
| 253 |
+
.auth-tab.active{background:var(--grad-accent);color:#fff;box-shadow:0 0 16px rgba(124,58,237,0.4)}
|
| 254 |
+
.auth-form{display:flex;flex-direction:column;gap:14px}
|
| 255 |
+
.auth-field{display:flex;flex-direction:column;gap:6px}
|
| 256 |
+
.auth-field span{font-size:0.72rem;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:var(--text-muted)}
|
| 257 |
+
.auth-field input{padding:11px 14px;border-radius:var(--radius-md);background:rgba(255,255,255,0.05);border:1px solid var(--border);color:var(--text-primary);font-size:0.9rem;outline:none;transition:var(--transition)}
|
| 258 |
+
.auth-field input:focus{background:rgba(255,255,255,0.08);border-color:var(--accent-v);box-shadow:0 0 0 3px rgba(139,92,246,0.12)}
|
| 259 |
+
.auth-field input::placeholder{color:var(--text-muted)}
|
| 260 |
+
.auth-error{font-size:0.8rem;font-weight:600;color:#fda4af;background:rgba(225,29,72,0.1);border:1px solid rgba(225,29,72,0.25);border-radius:var(--radius-sm);padding:8px 12px}
|
| 261 |
+
.auth-submit{width:100%;justify-content:center;margin-top:4px}
|
| 262 |
+
.auth-submit:disabled{opacity:0.5;cursor:not-allowed}
|
| 263 |
+
.auth-divider{display:flex;align-items:center;gap:12px;margin:18px 0 14px;color:var(--text-muted);font-size:0.72rem;font-weight:700;letter-spacing:0.1em;text-transform:uppercase}
|
| 264 |
+
.auth-divider::before,.auth-divider::after{content:'';flex:1;height:1px;background:var(--border)}
|
| 265 |
+
.auth-anilist-btn{width:100%;display:flex;align-items:center;justify-content:center;gap:9px;padding:11px;border-radius:var(--radius-md);background:rgba(6,182,212,0.12);border:1px solid rgba(6,182,212,0.35);color:#67e8f9;font-size:0.86rem;font-weight:700;transition:var(--transition);cursor:pointer}
|
| 266 |
+
.auth-anilist-btn:hover{background:rgba(6,182,212,0.22);border-color:rgba(6,182,212,0.6)}
|
| 267 |
+
|
| 268 |
+
/* ENHANCED AUTH STYLES */
|
| 269 |
+
.auth-modal-logo-text{font-size:1.4rem;font-weight:900;letter-spacing:.15em;background:linear-gradient(90deg,#fff 60%,#9d5bf0);-webkit-background-clip:text;background-clip:text;color:transparent}
|
| 270 |
+
.auth-input.error{border-color:rgba(248,113,113,.6)}
|
| 271 |
+
.auth-pw-bar{height:3px;border-radius:2px;background:#1e293b;overflow:hidden;margin-top:4px}
|
| 272 |
+
.auth-pw-fill{height:100%;width:0;border-radius:2px;transition:width .3s,background .3s}
|
| 273 |
+
.auth-error{display:none}
|
| 274 |
+
.auth-error.show{display:block}
|
| 275 |
+
.auth-submit-spinner{display:none;width:18px;height:18px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite;margin:0 auto}
|
| 276 |
+
.auth-submit.loading .auth-submit-text{display:none}
|
| 277 |
+
.auth-submit.loading .auth-submit-spinner{display:block}
|
| 278 |
+
.auth-switch{text-align:center;font-size:0.8rem;color:#475569;margin-top:4px}
|
| 279 |
+
.auth-switch a{color:#9d5bf0;font-weight:600;cursor:pointer;text-decoration:none}
|
| 280 |
+
.auth-switch a:hover{text-decoration:underline}
|
| 281 |
+
.auth-toast{position:fixed;bottom:24px;right:24px;z-index:99999;background:#0f172a;border:1px solid rgba(74,222,128,.3);border-radius:12px;padding:14px 18px;display:flex;align-items:center;gap:10px;color:#86efac;font-size:0.85rem;font-weight:600;box-shadow:0 10px 40px rgba(0,0,0,.5);animation:slideUp .3s ease}
|
| 282 |
+
@keyframes slideUp{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}
|
| 283 |
+
.auth-recaptcha-terms{margin-top:24px;text-align:center;font-size:0.65rem;color:#475569;line-height:1.4}
|
| 284 |
+
.auth-recaptcha-terms a{color:#64748b;text-decoration:underline;transition:color .2s}
|
| 285 |
+
.auth-recaptcha-terms a:hover{color:#9d5bf0}
|
| 286 |
+
.grecaptcha-badge{visibility:hidden!important}
|
| 287 |
+
|
| 288 |
+
/* BUTTONS */
|
| 289 |
+
.btn{display:inline-flex;align-items:center;gap:8px;padding:11px 24px;border-radius:var(--radius-pill);font-size:0.9rem;font-weight:600;transition:var(--transition);white-space:nowrap;cursor:pointer;border:none}
|
| 290 |
+
.btn-primary{background:var(--grad-accent);color:#fff;box-shadow:0 0 22px rgba(124,58,237,0.35)}
|
| 291 |
+
.btn-primary:hover{transform:translateY(-2px);box-shadow:0 4px 30px rgba(124,58,237,0.55)}
|
| 292 |
+
.btn-primary:disabled{opacity:0.4;cursor:not-allowed;transform:none}
|
| 293 |
+
.btn-ghost{background:rgba(255,255,255,0.07);color:var(--text-primary);border:1px solid var(--border)}
|
| 294 |
+
.btn-ghost:hover{background:rgba(255,255,255,0.12);border-color:var(--border-bright)}
|
| 295 |
+
.btn-ghost:disabled{opacity:0.4;cursor:not-allowed}
|
| 296 |
+
.btn-sm{padding:7px 16px;font-size:0.82rem}
|
| 297 |
+
|
| 298 |
+
/* BADGES */
|
| 299 |
+
.badge{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border-radius:var(--radius-pill);font-size:0.72rem;font-weight:600;letter-spacing:0.04em;text-transform:uppercase}
|
| 300 |
+
.badge-score{background:rgba(245,158,11,0.15);color:#fcd34d;border:1px solid rgba(245,158,11,0.3)}
|
| 301 |
+
.badge-green{background:rgba(34,197,94,0.15);color:#4ade80;border:1px solid rgba(34,197,94,0.3)}
|
| 302 |
+
.badge-dark{background:rgba(255,255,255,0.07);color:var(--text-secondary);border:1px solid var(--border)}
|
| 303 |
+
|
| 304 |
+
/* SECTION HEADERS */
|
| 305 |
+
.section-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:22px}
|
| 306 |
+
.section-title{display:flex;align-items:center;gap:10px;font-size:1.2rem;font-weight:700}
|
| 307 |
+
.section-title::before{content:'';display:block;width:4px;height:22px;background:var(--grad-accent);border-radius:2px}
|
| 308 |
+
.section-link{font-size:0.84rem;font-weight:500;color:var(--accent-v2);display:flex;align-items:center;gap:4px;transition:var(--transition)}
|
| 309 |
+
.section-link:hover{color:var(--accent-c2);gap:8px}
|
| 310 |
+
|
| 311 |
+
/* ANIME CARD */
|
| 312 |
+
.anime-card{position:relative;flex-shrink:0;width:158px;border-radius:var(--radius-md);overflow:hidden;background:var(--bg-card);cursor:pointer;transition:var(--transition);border:1px solid var(--border)}
|
| 313 |
+
.anime-card:hover{transform:translateY(-6px) scale(1.02);border-color:var(--border-bright);box-shadow:var(--shadow-hover)}
|
| 314 |
+
.card-poster{position:relative;aspect-ratio:2/3;overflow:hidden}
|
| 315 |
+
.card-poster img{width:100%;height:100%;object-fit:cover;transition:transform 0.45s ease}
|
| 316 |
+
.anime-card:hover .card-poster img{transform:scale(1.05)}
|
| 317 |
+
.card-overlay{position:absolute;inset:0;background:var(--grad-card);opacity:0;transition:var(--transition)}
|
| 318 |
+
.anime-card:hover .card-overlay{opacity:1}
|
| 319 |
+
.card-quick{position:absolute;bottom:8px;left:8px;right:8px;opacity:0;transform:translateY(8px);transition:var(--transition)}
|
| 320 |
+
.anime-card:hover .card-quick{opacity:1;transform:translateY(0)}
|
| 321 |
+
.card-quick-btn{width:100%;padding:7px;border-radius:var(--radius-sm);background:var(--grad-accent);color:#fff;font-size:0.78rem;font-weight:600;text-align:center}
|
| 322 |
+
.card-body{padding:10px 10px 12px}
|
| 323 |
+
.card-title{font-size:0.84rem;font-weight:600;line-height:1.35;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;margin-bottom:6px}
|
| 324 |
+
.card-meta{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
|
| 325 |
+
.card-score{font-size:0.72rem;font-weight:700;color:#6ee7b7}
|
| 326 |
+
.card-meta-item{font-size:0.7rem;color:var(--text-muted)}
|
| 327 |
+
|
| 328 |
+
/* SCROLL ROW */
|
| 329 |
+
.scroll-row{display:flex;gap:14px;overflow-x:auto;scroll-snap-type:x mandatory;padding:12px 0;scrollbar-width:thin;scrollbar-color:var(--accent-v) transparent}
|
| 330 |
+
.scroll-row::-webkit-scrollbar{height:4px;display:block}
|
| 331 |
+
.scroll-row::-webkit-scrollbar-thumb{background:var(--accent-v);border-radius:99px}
|
| 332 |
+
.scroll-row .anime-card{scroll-snap-align:start}
|
| 333 |
+
|
| 334 |
+
/* SKELETON */
|
| 335 |
+
.skeleton{background:linear-gradient(90deg,var(--bg-elevated) 25%,var(--bg-card-hover) 50%,var(--bg-elevated) 75%);background-size:200% 100%;animation:shimmer 1.5s infinite;border-radius:var(--radius-md)}
|
| 336 |
+
.skel-card{width:158px;flex-shrink:0}
|
| 337 |
+
.skel-poster{width:100%;aspect-ratio:2/3;border-radius:var(--radius-md) var(--radius-md) 0 0}
|
| 338 |
+
.skel-line{height:11px;margin:8px 10px;border-radius:6px}
|
| 339 |
+
.skel-line-sm{width:55%}
|
| 340 |
+
@keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
|
| 341 |
+
@keyframes fadeUp{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:translateY(0)}}
|
| 342 |
+
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
|
| 343 |
+
@keyframes spin{to{transform:rotate(360deg)}}
|
| 344 |
+
@keyframes marquee{0%{transform:translateX(0)}100%{transform:translateX(-50%)}}
|
| 345 |
+
@keyframes bounce{0%,100%{transform:translateX(-50%) translateY(0)}50%{transform:translateX(-50%) translateY(6px)}}
|
| 346 |
+
.spinner{width:34px;height:34px;border:3px solid var(--border);border-top-color:var(--accent-v);border-radius:50%;animation:spin 0.7s linear infinite}
|
| 347 |
+
.loader{display:flex;justify-content:center;align-items:center;padding:60px;flex-direction:column;gap:12px;color:var(--text-muted)}
|
| 348 |
+
|
| 349 |
+
/* HOME HERO */
|
| 350 |
+
.home-hero{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;position:relative;overflow:hidden;padding:calc(var(--header-h) + 40px) 20px 60px;text-align:center;background:radial-gradient(circle at 20% 15%, rgba(124,58,237,0.26), transparent 22%), radial-gradient(circle at 80% 18%, rgba(139,92,246,0.18), transparent 18%);}
|
| 351 |
+
.hero-glow{position:absolute;inset:0;background:radial-gradient(ellipse 70% 70% at 15% 50%,rgba(88,28,235,0.35) 0%,transparent 60%),radial-gradient(ellipse 40% 40% at 85% 50%,rgba(88,28,235,0.15) 0%,transparent 60%);pointer-events:none}
|
| 352 |
+
.hero-badge{display:inline-flex;align-items:center;gap:8px;padding:6px 16px;border-radius:var(--radius-pill);background:rgba(124,58,237,0.12);border:1px solid rgba(124,58,237,0.3);font-size:0.75rem;font-weight:700;letter-spacing:0.1em;color:var(--accent-v2);margin-bottom:32px;animation:fadeUp 0.5s ease both;box-shadow:0 14px 32px rgba(124,58,237,0.12)}
|
| 353 |
+
.hero-badge-dot{width:6px;height:6px;border-radius:50%;background:var(--accent-v)}
|
| 354 |
+
.hero-logo-text{font-family:'Rubik Dirt','Bebas Neue',sans-serif;font-size:clamp(5rem,16vw,14rem);font-weight:400;line-height:0.9;margin-bottom:16px;background:linear-gradient(180deg,#fff 40%,rgba(200,180,255,0.72) 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;animation:fadeUp 0.6s ease 0.1s both;text-shadow:0 0 26px rgba(124,58,237,0.18)}
|
| 355 |
+
.hero-logo-e{display:inline-block}
|
| 356 |
+
.hero-subtitle{font-size:clamp(1.4rem,3.5vw,2.4rem);font-weight:700;color:var(--text-secondary);margin-bottom:14px;animation:fadeUp 0.6s ease 0.2s both}
|
| 357 |
+
.hero-desc{font-size:1rem;color:var(--text-secondary);max-width:500px;margin:0 auto 32px;line-height:1.7;animation:fadeUp 0.6s ease 0.25s both}
|
| 358 |
+
.hero-desc strong{color:var(--text-primary)}
|
| 359 |
+
.hero-filters{display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:24px;animation:fadeUp 0.6s ease 0.3s both;flex-wrap:wrap}
|
| 360 |
+
.hero-filter-pill{padding:7px 20px;border-radius:var(--radius-pill);background:rgba(255,255,255,0.06);border:1px solid var(--border);font-size:0.86rem;font-weight:500;color:var(--text-secondary);transition:var(--transition);cursor:pointer}
|
| 361 |
+
.hero-filter-pill.active{background:var(--accent-v);border-color:var(--accent-v);color:#fff}
|
| 362 |
+
.hero-filter-pill:hover:not(.active){border-color:var(--border-bright);color:var(--text-primary)}
|
| 363 |
+
.hero-search-wrap{position:relative;width:min(620px,100%);margin:0 auto 28px;animation:fadeUp 0.6s ease 0.35s both}
|
| 364 |
+
.hero-search-input{width:100%;padding:16px 60px 16px 22px;border-radius:var(--radius-pill);background:rgba(255,255,255,0.07);border:1px solid rgba(255,255,255,0.1);color:var(--text-primary);font-size:0.95rem;outline:none;transition:var(--transition)}
|
| 365 |
+
.hero-search-input::placeholder{color:var(--text-muted)}
|
| 366 |
+
.hero-search-input:focus{background:rgba(255,255,255,0.1);border-color:var(--accent-v);box-shadow:0 0 0 4px rgba(124,58,237,0.15)}
|
| 367 |
+
.hero-search-btn{position:absolute;right:6px;top:50%;transform:translateY(-50%);width:42px;height:42px;border-radius:50%;background:var(--accent-v);color:#fff;display:flex;align-items:center;justify-content:center;transition:var(--transition)}
|
| 368 |
+
.hero-search-btn:hover{background:var(--accent-v2);transform:translateY(-50%) scale(1.08)}
|
| 369 |
+
.hero-search-btn svg{width:18px;height:18px}
|
| 370 |
+
.hero-popular{margin-bottom:36px;animation:fadeUp 0.6s ease 0.4s both}
|
| 371 |
+
.hero-popular-label{font-size:0.72rem;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--text-muted);margin-bottom:12px}
|
| 372 |
+
.hero-popular-tags{display:flex;justify-content:center;flex-wrap:wrap;gap:8px}
|
| 373 |
+
.hero-popular-tag{padding:6px 16px;border-radius:var(--radius-pill);background:rgba(255,255,255,0.05);border:1px solid var(--border);font-size:0.82rem;font-weight:500;color:var(--text-secondary);transition:var(--transition);cursor:pointer}
|
| 374 |
+
.hero-popular-tag:hover{background:rgba(124,58,237,0.12);border-color:var(--border-bright);color:var(--accent-v2)}
|
| 375 |
+
.hero-ctas{display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:12px;margin-bottom:52px;animation:fadeUp 0.6s ease 0.45s both}
|
| 376 |
+
.hero-cta{display:inline-flex;align-items:center;gap:10px;padding:13px 26px;border-radius:var(--radius-pill);font-size:0.9rem;font-weight:700;transition:var(--transition);cursor:pointer;border:1px solid transparent}
|
| 377 |
+
.hero-cta-anime{background:var(--grad-accent);color:#fff;box-shadow:0 0 28px rgba(124,58,237,0.4)}
|
| 378 |
+
.hero-cta-anime:hover{transform:translateY(-2px);box-shadow:0 6px 36px rgba(124,58,237,0.55)}
|
| 379 |
+
.hero-cta-manga{background:rgba(255,255,255,0.07);border-color:var(--border);color:var(--text-primary)}
|
| 380 |
+
.hero-cta-manga:hover{background:rgba(255,255,255,0.12);border-color:var(--border-bright)}
|
| 381 |
+
.hero-cta-movies{background:rgba(6,182,212,0.15);border-color:rgba(6,182,212,0.4);color:#67e8f9}
|
| 382 |
+
.hero-cta-movies:hover{background:rgba(6,182,212,0.25);transform:translateY(-2px)}
|
| 383 |
+
.hero-cta-music{background:rgba(139,92,246,0.18);border-color:rgba(139,92,246,0.4);color:#a78bfa}
|
| 384 |
+
.hero-cta-music:hover{background:rgba(139,92,246,0.28);transform:translateY(-2px)}
|
| 385 |
+
.hero-stats{display:flex;align-items:center;justify-content:center;animation:fadeUp 0.6s ease 0.5s both}
|
| 386 |
+
.hero-stat{padding:0 36px;text-align:center;position:relative}
|
| 387 |
+
.hero-stat+.hero-stat::before{content:'';position:absolute;left:0;top:15%;bottom:15%;width:1px;background:var(--border)}
|
| 388 |
+
.hero-stat-val{font-size:1.6rem;font-weight:800;letter-spacing:-0.02em;margin-bottom:2px}
|
| 389 |
+
.hero-stat-label{font-size:0.68rem;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--text-muted)}
|
| 390 |
+
.scroll-indicator{position:absolute;bottom:24px;left:50%;transform:translateX(-50%);color:var(--text-muted);animation:bounce 2s ease infinite}
|
| 391 |
+
|
| 392 |
+
/* LIBRARY MARQUEE */
|
| 393 |
+
.library-section{padding:60px 0;overflow:hidden;border-top:1px solid var(--border);border-bottom:1px solid var(--border)}
|
| 394 |
+
.library-header{display:flex;align-items:center;justify-content:center;gap:20px;margin-bottom:28px}
|
| 395 |
+
.library-header-line{flex:1;height:1px;background:linear-gradient(to right,transparent,var(--border-bright));max-width:200px}
|
| 396 |
+
.library-header-line.right{background:linear-gradient(to left,transparent,var(--border-bright))}
|
| 397 |
+
.library-header-text{font-size:0.7rem;font-weight:700;letter-spacing:0.15em;text-transform:uppercase;color:var(--text-muted)}
|
| 398 |
+
.marquee-track{overflow:hidden}
|
| 399 |
+
.marquee-row{display:flex;gap:10px;width:max-content;animation:marquee 30s linear infinite;margin-bottom:10px}
|
| 400 |
+
.marquee-row.reverse{animation:marquee 35s linear infinite reverse}
|
| 401 |
+
.marquee-row:last-child{margin-bottom:0}
|
| 402 |
+
.marquee-poster{width:100px;height:140px;flex-shrink:0;border-radius:var(--radius-md);overflow:hidden;border:1px solid var(--border);transition:var(--transition);cursor:pointer}
|
| 403 |
+
.marquee-poster:hover{border-color:var(--border-bright);transform:scale(1.04)}
|
| 404 |
+
.marquee-poster img{width:100%;height:100%;object-fit:cover}
|
| 405 |
+
|
| 406 |
+
/* FEATURE SECTION */
|
| 407 |
+
.feature-section{padding:80px 0}
|
| 408 |
+
.feature-eyebrow{font-size:0.75rem;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--accent-v2);margin-bottom:16px}
|
| 409 |
+
.feature-heading{font-size:clamp(2rem,4vw,3.2rem);font-weight:800;line-height:1.15;margin-bottom:20px}
|
| 410 |
+
.feature-heading .accent{color:var(--accent-v2)}
|
| 411 |
+
.feature-desc{font-size:1rem;color:var(--text-secondary);line-height:1.75;max-width:480px;margin-bottom:40px}
|
| 412 |
+
.feature-card{background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:24px;max-width:560px}
|
| 413 |
+
.feature-card-icon{width:44px;height:44px;border-radius:var(--radius-md);background:rgba(124,58,237,0.15);border:1px solid rgba(124,58,237,0.3);display:flex;align-items:center;justify-content:center;color:var(--accent-v2);margin-bottom:16px}
|
| 414 |
+
.feature-card-eyebrow{font-size:0.72rem;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--accent-v2);margin-bottom:6px}
|
| 415 |
+
.feature-card-title{font-size:1.1rem;font-weight:800;margin-bottom:8px}
|
| 416 |
+
.feature-card-desc{font-size:0.88rem;color:var(--text-secondary);line-height:1.7;margin-bottom:16px}
|
| 417 |
+
.feature-track-item{display:flex;align-items:center;gap:12px;padding:12px 14px;background:var(--bg-elevated);border-radius:var(--radius-md);margin-bottom:8px}
|
| 418 |
+
.feature-track-name{font-size:0.88rem;font-weight:600}
|
| 419 |
+
.feature-track-sub{font-size:0.75rem;color:var(--text-muted)}
|
| 420 |
+
.feature-track-badge{margin-left:auto;font-size:0.72rem;font-weight:700;color:var(--accent-green)}
|
| 421 |
+
.content-section{padding:40px 0}
|
| 422 |
+
/* Pages whose first section sits directly under the fixed header (Home) */
|
| 423 |
+
.content-section:first-child{padding-top:calc(var(--header-h) + 26px)}
|
| 424 |
+
.section-sub-label{font-size:0.7rem;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:var(--text-muted)}
|
| 425 |
+
|
| 426 |
+
/* DETAILS PAGE */
|
| 427 |
+
.info-hero{position:relative;min-height:520px;overflow:hidden;display:flex;align-items:flex-end;padding-top:var(--header-h);background:#0a0a0a}
|
| 428 |
+
.info-hero::before,
|
| 429 |
+
.info-hero::after{content:'';position:absolute;inset:0;pointer-events:none}
|
| 430 |
+
.info-hero::before{background:radial-gradient(120% 80% at 30% 30%, rgba(0,0,0,0.25), rgba(0,0,0,0.85) 70%, #0a0a0a 100%)}
|
| 431 |
+
.info-hero::after{background:linear-gradient(180deg, rgba(10,10,10,0) 40%, rgba(10,10,10,0.85) 80%, var(--bg-base) 100%)}
|
| 432 |
+
.info-hero-bg{position:absolute;inset:0;background-size:cover;background-position:center;background-repeat:no-repeat;transform:scale(1.05);filter:brightness(0.55)}
|
| 433 |
+
.info-hero-inner{position:relative;z-index:2;display:flex;gap:36px;padding:96px 28px 56px;align-items:flex-end}
|
| 434 |
+
.info-poster{flex:0 0 240px;width:240px;aspect-ratio:2/3;border-radius:14px;overflow:hidden;box-shadow:0 24px 60px rgba(0,0,0,0.6),0 0 0 1px rgba(255,255,255,0.06);background:#111;border:1px solid rgba(255,255,255,0.06)}
|
| 435 |
+
.info-poster img{width:100%;height:100%;object-fit:cover;display:block}
|
| 436 |
+
.info-head{flex:1;min-width:0;display:flex;flex-direction:column;gap:18px}
|
| 437 |
+
.info-eyebrow{display:inline-flex;align-items:center;gap:10px;color:rgba(255,255,255,0.7);font-size:0.78rem;font-weight:600;letter-spacing:0.18em;text-transform:uppercase}
|
| 438 |
+
.info-eyebrow .dot{width:6px;height:6px;border-radius:50%;background:var(--accent-v);box-shadow:0 0 12px var(--accent-v)}
|
| 439 |
+
.info-title{font-family:'Poppins','Inter',sans-serif;font-size:clamp(1.8rem,3.6vw,3.2rem);font-weight:900;line-height:1.05;color:#fff;margin:0;text-shadow:0 6px 28px rgba(0,0,0,0.6)}
|
| 440 |
+
.info-subtitle{font-size:0.95rem;color:rgba(255,255,255,0.65);font-weight:500;margin-top:-6px}
|
| 441 |
+
.info-meta-row{display:flex;flex-wrap:wrap;gap:16px;align-items:center;color:rgba(255,255,255,0.85);font-size:0.85rem;font-weight:600}
|
| 442 |
+
.meta-pill{background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.08);padding:6px 12px;border-radius:999px;backdrop-filter:blur(6px)}
|
| 443 |
+
.meta-score{color:#fbbf24}
|
| 444 |
+
.meta-status-airing{color:#22c55e}
|
| 445 |
+
.info-genres{display:flex;gap:8px;flex-wrap:wrap}
|
| 446 |
+
.info-genre-tag{background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);padding:5px 12px;border-radius:999px;font-size:0.72rem;font-weight:600;color:rgba(255,255,255,0.85);cursor:pointer;transition:all 0.2s ease}
|
| 447 |
+
.info-genre-tag:hover{background:rgba(255,255,255,0.12);transform:translateY(-1px)}
|
| 448 |
+
.info-cta{display:flex;gap:12px;margin-top:4px;flex-wrap:wrap}
|
| 449 |
+
.btn-watch{display:inline-flex;align-items:center;gap:10px;padding:14px 28px;border-radius:12px;background:linear-gradient(135deg, var(--accent-v) 0%, var(--accent-v2) 100%);color:#fff;font-weight:800;font-size:0.95rem;letter-spacing:0.04em;text-decoration:none;border:none;cursor:pointer;transition:transform 0.2s ease, box-shadow 0.2s ease;box-shadow:0 12px 30px rgba(124,58,237,0.35)}
|
| 450 |
+
.btn-watch:hover{transform:translateY(-2px);box-shadow:0 18px 40px rgba(124,58,237,0.5)}
|
| 451 |
+
.btn-secondary{display:inline-flex;align-items:center;gap:8px;padding:14px 22px;border-radius:12px;background:rgba(255,255,255,0.08);color:#fff;font-weight:700;font-size:0.9rem;text-decoration:none;border:1px solid rgba(255,255,255,0.1);cursor:pointer;transition:background 0.2s ease}
|
| 452 |
+
.btn-secondary:hover{background:rgba(255,255,255,0.14)}
|
| 453 |
+
.info-body{padding:40px 28px 80px}
|
| 454 |
+
.info-section{margin-bottom:48px}
|
| 455 |
+
.info-section-title{display:flex;align-items:center;gap:12px;font-family:'Poppins','Inter',sans-serif;font-size:1.3rem;font-weight:800;color:#fff;margin:0 0 18px}
|
| 456 |
+
.info-section-title::before{content:'';width:4px;height:22px;border-radius:4px;background:var(--accent-v)}
|
| 457 |
+
.info-grid-2{display:grid;grid-template-columns:minmax(0, 2fr) minmax(0, 1fr);gap:32px;align-items:flex-start}
|
| 458 |
+
.info-desc{font-size:0.96rem;line-height:1.75;color:rgba(255,255,255,0.78);margin:0;position:relative;max-height:22em;overflow:hidden;transition:max-height .3s ease}
|
| 459 |
+
.info-desc.expanded{max-height:200em}
|
| 460 |
+
.info-desc::after{content:'';position:absolute;inset:auto 0 0 0;height:3em;background:linear-gradient(180deg, rgba(10, 10, 10, 0) 0%, var(--bg-base) 100%);pointer-events:none;transition:opacity 0.3s ease}
|
| 461 |
+
.info-desc.expanded::after{opacity:0}
|
| 462 |
+
.info-desc-toggle{display:inline-block;margin-top:8px;color:var(--accent-v2);font-weight:700;font-size:0.85rem;cursor:pointer;background:none;border:none;padding:0}
|
| 463 |
+
.info-stats{display:grid;grid-template-columns:1fr 1fr;gap:14px;background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.06);border-radius:14px;padding:22px}
|
| 464 |
+
.info-stat-row{display:flex;flex-direction:column;gap:4px}
|
| 465 |
+
.info-stat-row .label{font-size:0.7rem;font-weight:700;text-transform:uppercase;letter-spacing:0.12em;color:rgba(255,255,255,0.45)}
|
| 466 |
+
.info-stat-row .value{font-size:0.95rem;font-weight:700;color:#fff}
|
| 467 |
+
.info-tags{display:flex;flex-wrap:wrap;gap:6px}
|
| 468 |
+
.info-tag{background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.06);padding:4px 10px;border-radius:6px;font-size:0.72rem;font-weight:600;color:rgba(255,255,255,0.7)}
|
| 469 |
+
@media (max-width: 880px) {
|
| 470 |
+
.info-hero-inner{flex-direction:column;align-items:center;text-align:center;padding:80px 22px 40px;gap:24px}
|
| 471 |
+
.info-poster{flex:0 0 180px;width:180px}
|
| 472 |
+
.info-meta-row,.info-genres,.info-cta{justify-content:center}
|
| 473 |
+
.info-grid-2{grid-template-columns:1fr}
|
| 474 |
+
.info-hero{min-height:auto}
|
| 475 |
+
.info-body{padding:32px 22px 80px}
|
| 476 |
+
}
|
| 477 |
+
@media (max-width: 540px) {
|
| 478 |
+
.info-hero-inner{padding:72px 20px 36px}
|
| 479 |
+
.info-poster{flex:0 0 150px;width:150px}
|
| 480 |
+
.btn-watch{padding:12px 22px;font-size:0.88rem}
|
| 481 |
+
.info-body{padding:28px 20px 80px}
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
/* NEXT-EPISODE COUNTDOWN */
|
| 485 |
+
.countdown-wrap{display:inline-flex;flex-direction:column;gap:10px;padding:14px 18px;margin-bottom:22px;border-radius:var(--radius-md);background:rgba(124,58,237,0.08);border:1px solid rgba(124,58,237,0.3);backdrop-filter:blur(8px);width:fit-content}
|
| 486 |
+
.countdown-label{font-size:0.72rem;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;color:var(--accent-v2)}
|
| 487 |
+
.countdown-cards{display:flex;gap:8px}
|
| 488 |
+
.countdown-cell{display:flex;flex-direction:column;align-items:center;gap:2px;min-width:58px;padding:8px 6px;border-radius:var(--radius-sm);background:rgba(8,8,8,0.6);border:1px solid var(--border-bright)}
|
| 489 |
+
.countdown-num{font-size:1.35rem;font-weight:800;font-variant-numeric:tabular-nums;line-height:1;color:#fff}
|
| 490 |
+
.countdown-unit{font-size:0.6rem;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--text-muted)}
|
| 491 |
+
.info-genre-pill{padding:5px 14px;background:rgba(255,255,255,0.07);border:1px solid var(--border);border-radius:var(--radius-pill);font-size:0.8rem;font-weight:500;color:var(--text-secondary);transition:var(--transition);cursor:pointer}
|
| 492 |
+
.info-genre-pill:hover{border-color:var(--border-bright);color:var(--accent-v2)}
|
| 493 |
+
.info-actions{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
| 494 |
+
.info-btn-watch{background:var(--grad-accent);color:#fff;box-shadow:0 0 24px rgba(124,58,237,0.35)}
|
| 495 |
+
.info-btn-watch:hover{transform:translateY(-2px);box-shadow:0 6px 32px rgba(124,58,237,0.55)}
|
| 496 |
+
.info-btn-ghost{background:rgba(255,255,255,0.08);color:var(--text-primary);border:1px solid var(--border)}
|
| 497 |
+
.info-btn-ghost:hover{background:rgba(255,255,255,0.14);border-color:var(--border-bright)}
|
| 498 |
+
.info-body{padding:44px 0 80px}
|
| 499 |
+
.info-layout{display:grid;grid-template-columns:1fr 300px;gap:48px;align-items:start}
|
| 500 |
+
.info-section-label{font-size:1.15rem;font-weight:700;display:flex;align-items:center;gap:10px;margin-bottom:16px}
|
| 501 |
+
.info-section-label::before{content:'';width:4px;height:20px;background:var(--grad-accent);border-radius:2px}
|
| 502 |
+
.info-synopsis{font-size:0.93rem;color:var(--text-secondary);line-height:1.8;margin-bottom:8px}
|
| 503 |
+
.info-synopsis.clamped{display:-webkit-box;-webkit-line-clamp:5;-webkit-box-orient:vertical;overflow:hidden}
|
| 504 |
+
.show-more-btn{color:var(--accent-v2);font-size:0.88rem;font-weight:600;display:inline-flex;align-items:center;gap:4px;transition:var(--transition);margin-top:8px;cursor:pointer;background:none;border:none}
|
| 505 |
+
.show-more-btn:hover{color:var(--accent-c2)}
|
| 506 |
+
.info-tags{display:flex;flex-wrap:wrap;gap:8px;margin:20px 0 32px}
|
| 507 |
+
.info-tag{padding:5px 14px;background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-pill);font-size:0.78rem;font-weight:500;color:var(--text-secondary);transition:var(--transition);cursor:pointer}
|
| 508 |
+
.info-tag:hover{border-color:var(--border-bright);color:var(--accent-v2)}
|
| 509 |
+
.info-stats-grid{background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;display:grid;grid-template-columns:1fr 1fr}
|
| 510 |
+
.info-stat-cell{padding:18px 20px;border-right:1px solid var(--border);border-bottom:1px solid var(--border)}
|
| 511 |
+
.info-stat-cell:nth-child(2n){border-right:none}
|
| 512 |
+
.info-stat-cell:nth-last-child(-n+2){border-bottom:none}
|
| 513 |
+
.info-stat-label{font-size:0.68rem;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:var(--text-muted);margin-bottom:5px}
|
| 514 |
+
.info-stat-value{font-size:0.95rem;font-weight:700}
|
| 515 |
+
.char-row{display:flex;gap:16px;overflow-x:auto;padding:4px 0 12px}
|
| 516 |
+
.char-card{display:flex;flex-direction:column;align-items:center;gap:8px;flex-shrink:0;cursor:pointer}
|
| 517 |
+
.char-avatar{width:80px;height:80px;border-radius:50%;overflow:hidden;border:2px solid var(--border);transition:var(--transition)}
|
| 518 |
+
.char-avatar img{width:100%;height:100%;object-fit:cover}
|
| 519 |
+
.char-card:hover .char-avatar{border-color:var(--border-bright);transform:scale(1.06)}
|
| 520 |
+
.char-name{font-size:0.78rem;font-weight:600;text-align:center;max-width:84px;line-height:1.3}
|
| 521 |
+
.char-role{font-size:0.68rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.05em}
|
| 522 |
+
|
| 523 |
+
/* WATCH PAGE */
|
| 524 |
+
.watch-page{padding-top:var(--header-h)}
|
| 525 |
+
.watch-container{padding:0 28px 80px;max-width:var(--max-w);margin:0 auto}
|
| 526 |
+
.watch-layout{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:18px;align-items:start;padding-top:12px}
|
| 527 |
+
.watch-main{min-width:0}
|
| 528 |
+
.watch-sidebar-col{min-width:0}
|
| 529 |
+
.player-wrapper{position:relative;width:100%;aspect-ratio:16/9;background:#080a12;border-radius:14px;overflow:hidden;border:1px solid var(--border)}
|
| 530 |
+
.player-wrapper iframe,.player-wrapper video,.player-wrapper .custom-player{width:100%;height:100%;border:none;display:block}
|
| 531 |
+
.player-placeholder{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;color:var(--text-muted);background:rgba(7,10,18,0.95)}
|
| 532 |
+
.player-wrap.theater-mode{position:fixed;top:0;left:0;right:0;bottom:0;z-index:9999;background:#000;display:flex;align-items:center;justify-content:center}
|
| 533 |
+
.player-wrap.theater-mode .custom-player{width:100%;height:100%;max-width:100%;max-height:100%;border-radius:0}
|
| 534 |
+
@media(max-width:680px){.auth-nav{display:none}}
|
| 535 |
+
.player-placeholder svg{width:52px;height:52px;opacity:0.3}
|
| 536 |
+
.player-placeholder span{font-size:0.9rem;font-weight:500}
|
| 537 |
+
.player-error-box{color:#f43f5e}
|
| 538 |
+
.server-warning{display:flex;align-items:center;gap:10px;padding:10px 16px;background:rgba(245,158,11,0.07);border:1px solid rgba(245,158,11,0.2);border-radius:var(--radius-md);margin:10px 0;font-size:0.83rem;color:#fcd34d}
|
| 539 |
+
.server-warning-close{margin-left:auto;opacity:0.5;transition:var(--transition);font-size:1.1rem;line-height:1;cursor:pointer;border:none;background:none;color:inherit}
|
| 540 |
+
.server-warning-close:hover{opacity:1}
|
| 541 |
+
.watch-warn-banner{display:flex;align-items:center;gap:10px;padding:12px 14px;background:rgba(245,158,11,0.08);border:1px solid rgba(245,158,11,0.2);border-radius:12px;margin:10px 0 14px;font-size:0.83rem;color:#fcd34d}
|
| 542 |
+
.wwb-icon{width:16px;height:16px;flex-shrink:0}
|
| 543 |
+
.wwb-text{display:block;flex:1}
|
| 544 |
+
.wwb-close{margin-left:auto;border:none;background:none;color:inherit;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0}
|
| 545 |
+
.wwb-close svg{width:16px;height:16px}
|
| 546 |
+
.kv-prefs-bar{display:flex;align-items:center;gap:18px;padding:11px 14px;background:var(--bg-surface);border:1px solid var(--border);border-radius:12px;margin:10px 0;flex-wrap:wrap}
|
| 547 |
+
.kv-pref-btn{display:flex;align-items:center;gap:8px;padding:0;background:none;border:none;color:var(--text-secondary);font-size:0.82rem;font-weight:600;cursor:pointer}
|
| 548 |
+
.kv-pref-btn svg{opacity:0.8}
|
| 549 |
+
.kv-pref-pill{position:relative;display:inline-flex;align-items:center;width:36px;height:20px;border-radius:999px;background:rgba(255,255,255,0.08);border:1px solid var(--border);margin-left:4px}
|
| 550 |
+
.kv-pref-knob{position:absolute;left:3px;width:12px;height:12px;border-radius:50%;background:#fff;box-shadow:0 1px 4px rgba(0,0,0,0.4)}
|
| 551 |
+
.kv-pref-btn.on{color:var(--text-primary)}.kv-pref-btn.on svg{color:var(--accent-v2)}.kv-pref-pill.on{background:var(--accent-v);border-color:var(--accent-v)}.kv-pref-pill.on .kv-pref-knob{left:19px}.kv-pref-knob{transition:left .18s ease}.action-btn.is-active{color:var(--accent-v2);border-color:rgba(124,58,237,.45);background:rgba(124,58,237,.12)}
|
| 552 |
+
.kv-prefs-sep{width:1px;height:16px;background:rgba(255,255,255,0.08)}
|
| 553 |
+
.watch-player-info{padding-top:14px}
|
| 554 |
+
.watch-ep-label{font-size:0.72rem;font-weight:800;color:var(--accent-v2);letter-spacing:0.12em;text-transform:uppercase;margin:0 0 8px}
|
| 555 |
+
.watch-ep-title{font-size:1.45rem;font-weight:800;line-height:1.25;margin:0 0 8px}
|
| 556 |
+
.watch-filler-warn{color:#facc15;font-size:0.82rem;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;margin-top:8px}
|
| 557 |
+
.action-bar{display:flex;align-items:center;gap:12px;padding:14px 0 18px;border-bottom:1px solid var(--border);margin-bottom:18px}
|
| 558 |
+
.action-anime{display:flex;align-items:center;gap:12px;min-width:0;flex:1;text-decoration:none;color:inherit}
|
| 559 |
+
.aa-poster{width:52px;height:52px;border-radius:10px;overflow:hidden;flex-shrink:0;border:1px solid rgba(255,255,255,0.08)}
|
| 560 |
+
.aa-poster img{width:100%;height:100%;object-fit:cover;display:block}
|
| 561 |
+
.aa-info{min-width:0;display:flex;flex-direction:column;gap:4px}
|
| 562 |
+
.aa-name{font-size:0.94rem;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
| 563 |
+
.aa-meta{font-size:0.72rem;color:var(--text-muted)}
|
| 564 |
+
.action-buttons{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end;margin-left:auto}
|
| 565 |
+
.audio-toggle{display:flex;align-items:center;background:rgba(255,255,255,0.04);border:1px solid var(--border);border-radius:999px;padding:4px;gap:4px}
|
| 566 |
+
.audio-btn{padding:6px 12px;border-radius:999px;font-size:0.78rem;font-weight:700;background:transparent;border:none;color:var(--text-secondary);cursor:pointer;transition:var(--transition)}
|
| 567 |
+
.audio-btn.active{background:var(--accent-v);border-color:var(--accent-v);color:#fff}
|
| 568 |
+
.audio-btn:disabled{opacity:0.5;cursor:not-allowed}
|
| 569 |
+
.action-btn{display:inline-flex;align-items:center;gap:8px;padding:8px 14px;border-radius:999px;background:rgba(255,255,255,0.06);border:1px solid var(--border);color:var(--text-primary);font-weight:700;font-size:0.8rem;cursor:pointer;transition:var(--transition)}
|
| 570 |
+
.action-btn svg{width:15px;height:15px}
|
| 571 |
+
.action-btn:hover,.audio-btn:not(.active):hover{border-color:var(--border-bright)}
|
| 572 |
+
.audio-lang-selected{font-size:0.78rem;color:var(--text-muted)}
|
| 573 |
+
.server-panel{background:var(--bg-surface);border:1px solid var(--border);border-radius:14px;padding:14px;margin:10px 0 0;box-shadow:0 20px 40px rgba(0,0,0,0.24)}
|
| 574 |
+
.server-panel.is-hidden{display:none}
|
| 575 |
+
.server-panel-row{display:flex;align-items:flex-start;gap:14px;flex-wrap:wrap}
|
| 576 |
+
.server-group{display:flex;flex-direction:column;gap:10px;flex:1;min-width:180px}
|
| 577 |
+
.server-group-toggle{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:10px;background:rgba(255,255,255,0.04);border:1px solid var(--border);color:var(--text-primary);font-weight:700;cursor:pointer}
|
| 578 |
+
.sg-icon{flex-shrink:0}
|
| 579 |
+
.sg-label{flex:1;text-align:left}
|
| 580 |
+
.sg-count{font-size:0.72rem;padding:2px 7px;border-radius:999px;background:rgba(255,255,255,0.08);color:var(--text-muted)}
|
| 581 |
+
.server-btn-row{display:flex;gap:8px;flex-wrap:wrap;padding-left:2px}
|
| 582 |
+
.server-option{padding:7px 12px;border-radius:10px;border:1px solid var(--border);background:rgba(255,255,255,0.03);color:var(--text-secondary);font-size:0.78rem;font-weight:600;cursor:pointer}
|
| 583 |
+
.server-option.selected{background:rgba(124,58,237,0.16);border-color:rgba(124,58,237,0.42);color:var(--text-primary)}
|
| 584 |
+
.server-quality-wrap{margin-left:auto}
|
| 585 |
+
.server-quality-select{padding:8px 10px;border-radius:10px;border:1px solid var(--border);background:rgba(255,255,255,0.04);color:var(--text-primary)}
|
| 586 |
+
.server-loading,.server-error{display:flex;align-items:center;gap:8px;padding-top:12px;font-size:0.8rem;color:var(--text-muted)}
|
| 587 |
+
.server-loading-dot{width:8px;height:8px;border-radius:50%;background:var(--accent-v);display:inline-block;animation:pulse 1.3s ease-in-out infinite}
|
| 588 |
+
@keyframes pulse{0%,100%{opacity:0.45;transform:scale(0.9)}50%{opacity:1;transform:scale(1.1)}}
|
| 589 |
+
.watch-desc-card{background:var(--bg-surface);border:1px solid var(--border);border-radius:14px;padding:18px 20px;margin-top:18px}
|
| 590 |
+
.watch-sidebar{background:var(--bg-surface);border:1px solid var(--border);border-radius:14px;overflow:hidden;position:sticky;top:calc(var(--header-h) + 12px);max-height:calc(100vh - var(--header-h) - 24px);display:flex;flex-direction:column}
|
| 591 |
+
.up-next-header{padding:14px 14px 0}
|
| 592 |
+
.un-info{display:flex;flex-direction:column;gap:3px}
|
| 593 |
+
.un-title{font-size:1rem;font-weight:800}
|
| 594 |
+
.un-subtitle{font-size:0.76rem;color:var(--accent-v2)}
|
| 595 |
+
.next-ep-strip{padding:12px 14px 0}
|
| 596 |
+
.next-ep-pill{display:flex;flex-direction:column;gap:8px;padding:10px 12px;border-radius:12px;background:rgba(124,58,237,0.08);border:1px solid rgba(124,58,237,0.22);color:var(--text-primary)}
|
| 597 |
+
.nep-bell{width:15px;height:15px}
|
| 598 |
+
.nep-label{font-size:0.72rem;font-weight:700;color:var(--text-muted);letter-spacing:0.08em;text-transform:uppercase}
|
| 599 |
+
.nep-units{display:flex;align-items:flex-end;gap:8px}
|
| 600 |
+
.nec-unit{display:flex;align-items:flex-end;gap:2px;font-weight:800;color:#fff}
|
| 601 |
+
.nec-num{font-size:0.9rem}
|
| 602 |
+
.nec-u{font-size:0.6rem;color:var(--text-muted)}
|
| 603 |
+
.ep-search-wrap{padding:12px 14px 0}
|
| 604 |
+
.ep-search-input{width:100%;padding:9px 12px;background:var(--bg-elevated);border:1px solid var(--border);border-radius:10px;color:var(--text-primary);font-size:0.82rem;outline:none}
|
| 605 |
+
.ep-search-input::placeholder{color:var(--text-muted)}
|
| 606 |
+
.ep-view-bar{display:flex;align-items:center;justify-content:space-between;padding:12px 14px 8px}
|
| 607 |
+
.ep-view-bar-label{font-size:0.68rem;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:var(--text-muted)}
|
| 608 |
+
.ep-view-toggle-btn{padding:5px 9px;border:1px solid var(--border);border-radius:8px;color:var(--text-secondary);font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em}.ep-view-toggle-btn:hover{color:var(--text-primary);border-color:var(--border-bright)}
|
| 609 |
+
.ep-list{flex:1;overflow-y:auto;padding:0 8px 12px;scrollbar-width:thin;scrollbar-color:var(--accent-v) transparent}
|
| 610 |
+
.ep-list::-webkit-scrollbar{width:3px;display:block}
|
| 611 |
+
.ep-list::-webkit-scrollbar-thumb{background:var(--accent-v);border-radius:99px}
|
| 612 |
+
.ep-list.compact{display:grid;grid-template-columns:repeat(auto-fill,minmax(54px,1fr));gap:7px;align-content:start}.ep-list.compact .ep-item{justify-content:center;margin:0;padding:9px 5px}.ep-list.compact .ep-thumb{width:auto;height:auto;background:transparent;overflow:visible}.ep-list.compact .ep-thumb img,.ep-list.compact .ep-info{display:none}.ep-list.compact .ep-thumb-label{position:static;background:transparent;padding:0;color:var(--text-secondary);font-size:.76rem}.ep-list.compact .ep-item.active .ep-thumb-label{color:var(--accent-v2)}
|
| 613 |
+
.ep-item{display:flex;align-items:center;gap:10px;padding:7px 8px;border-radius:10px;cursor:pointer;transition:var(--transition);margin-bottom:2px;border:1px solid transparent}
|
| 614 |
+
.ep-item:hover{background:rgba(255,255,255,0.035)}
|
| 615 |
+
.ep-item.active{background:rgba(124,58,237,0.12);border-color:rgba(124,58,237,0.25)}
|
| 616 |
+
.ep-thumb{width:72px;height:46px;flex-shrink:0;background:var(--bg-elevated);border-radius:8px;overflow:hidden;display:flex;align-items:center;justify-content:center;font-size:0.68rem;font-weight:700;color:var(--text-muted);position:relative}
|
| 617 |
+
.ep-thumb img{width:100%;height:100%;object-fit:cover}
|
| 618 |
+
.ep-thumb-label{position:absolute;bottom:3px;left:3px;background:rgba(0,0,0,0.7);color:#fff;font-size:0.62rem;font-weight:700;padding:1px 5px;border-radius:4px}
|
| 619 |
+
.ep-info{flex:1;min-width:0}
|
| 620 |
+
.ep-info-title{font-size:0.8rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--text-primary)}
|
| 621 |
+
.ep-info-sub{font-size:0.7rem;color:var(--text-muted);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
| 622 |
+
.ep-watched-badge{position:absolute;top:3px;right:3px;width:18px;height:18px;border-radius:50%;background:#22c55e;color:#fff;font-size:0.65rem;font-weight:700;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(0,0,0,0.4)}
|
| 623 |
+
.ep-item.watched{opacity:0.7}
|
| 624 |
+
.ep-item.watched:hover{opacity:1}
|
| 625 |
+
.season-selector{display:flex;align-items:center;gap:10px;padding:12px 14px;border-bottom:1px solid var(--border)}
|
| 626 |
+
.season-label{font-size:0.68rem;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:var(--text-muted)}
|
| 627 |
+
.season-options{display:flex;gap:6px;flex-wrap:wrap}
|
| 628 |
+
.season-btn{padding:6px 12px;border-radius:8px;background:rgba(255,255,255,0.04);border:1px solid var(--border);color:var(--text-secondary);font-size:0.76rem;font-weight:600;cursor:pointer;transition:var(--transition)}
|
| 629 |
+
.season-btn:hover{border-color:var(--border-bright);color:var(--text-primary)}
|
| 630 |
+
.season-btn.active{background:var(--accent-v);border-color:var(--accent-v);color:#fff;box-shadow:0 0 12px rgba(124,58,237,0.35)}
|
| 631 |
+
.movie-quality-bar{display:flex;align-items:center;gap:12px;padding:12px 0;margin-top:8px}
|
| 632 |
+
.movie-quality-label{font-size:0.68rem;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:var(--text-muted)}
|
| 633 |
+
.movie-quality-options{display:flex;gap:8px;flex-wrap:wrap}
|
| 634 |
+
.movie-quality-btn{padding:6px 14px;border-radius:8px;background:rgba(255,255,255,0.04);border:1px solid var(--border);color:var(--text-secondary);font-size:0.76rem;font-weight:600;cursor:pointer;transition:var(--transition)}
|
| 635 |
+
.movie-quality-btn:hover{border-color:var(--border-bright);color:var(--text-primary)}
|
| 636 |
+
.movie-quality-btn.active{background:var(--accent-v);border-color:var(--accent-v);color:#fff;box-shadow:0 0 12px rgba(124,58,237,0.35)}
|
| 637 |
+
.watch-anime-row{display:flex;align-items:center;gap:16px;padding:16px;background:var(--bg-surface);border:1px solid var(--border);border-radius:12px;margin:16px 0}
|
| 638 |
+
.watch-anime-thumb{width:48px;height:48px;border-radius:8px;overflow:hidden;flex-shrink:0;background:var(--bg-elevated)}
|
| 639 |
+
.watch-anime-thumb img{width:100%;height:100%;object-fit:cover}
|
| 640 |
+
.watch-anime-name{font-size:0.9rem;font-weight:700;color:var(--text-primary)}
|
| 641 |
+
.watch-anime-sub-text{font-size:0.75rem;color:var(--text-muted);margin-top:2px}
|
| 642 |
+
.watch-audio-btns{display:flex;align-items:center;gap:8px;margin-left:auto;flex-wrap:wrap}
|
| 643 |
+
.audio-notice{display:flex;align-items:center;gap:10px;padding:9px 14px;background:rgba(124,58,237,0.08);border:1px solid rgba(124,58,237,0.28);border-radius:var(--radius-md);margin:12px 0;font-size:0.83rem;font-weight:500;color:var(--accent-c2);animation:fadeUp .2s ease both}
|
| 644 |
+
.audio-notice svg{flex-shrink:0;color:var(--accent-v2)}
|
| 645 |
+
.audio-notice .server-warning-close{margin-left:auto;color:var(--text-muted)}
|
| 646 |
+
|
| 647 |
+
@media (max-width: 1024px){
|
| 648 |
+
.watch-layout{grid-template-columns:1fr}
|
| 649 |
+
.watch-sidebar{position:static;max-height:540px}
|
| 650 |
+
}
|
| 651 |
+
@media (max-width: 680px){
|
| 652 |
+
.watch-container{padding:0 12px 60px}
|
| 653 |
+
.watch-layout{gap:12px;padding-top:8px}
|
| 654 |
+
.watch-warn-banner{padding:10px 12px}
|
| 655 |
+
.action-bar{flex-wrap:wrap;border-bottom:none;padding-bottom:8px}
|
| 656 |
+
.action-anime{width:100%}
|
| 657 |
+
.action-buttons{width:100%;justify-content:flex-start}
|
| 658 |
+
.watch-ep-title{font-size:1.15rem}
|
| 659 |
+
.watch-desc-card{padding:14px 16px}
|
| 660 |
+
.watch-sidebar{position:static;max-height:420px}
|
| 661 |
+
.ep-thumb{width:64px;height:40px}
|
| 662 |
+
.aa-poster{width:44px;height:44px}
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
/* MOVIES & TV */
|
| 666 |
+
.movie-search-wrap{position:relative}
|
| 667 |
+
.movie-tabs{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px}
|
| 668 |
+
.movie-tabs button{padding:8px 20px;border-radius:var(--radius-pill);background:rgba(255,255,255,0.06);border:1px solid var(--border);font-size:0.86rem;font-weight:600;color:var(--text-secondary);transition:var(--transition);cursor:pointer}
|
| 669 |
+
.movie-tabs button:hover{border-color:var(--border-bright);color:var(--text-primary)}
|
| 670 |
+
.movie-tabs button.active{background:var(--accent-v);border-color:var(--accent-v);color:#fff;box-shadow:0 0 18px rgba(124,58,237,0.35)}
|
| 671 |
+
.movie-sort{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:0 0 24px}
|
| 672 |
+
.movie-sort-label{font-size:0.7rem;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--text-muted);margin-right:6px}
|
| 673 |
+
.movie-sort button{padding:5px 13px;border-radius:var(--radius-pill);background:rgba(255,255,255,0.04);border:1px solid var(--border);font-size:0.76rem;font-weight:600;color:var(--text-secondary);transition:var(--transition);cursor:pointer}
|
| 674 |
+
.movie-sort button:hover{border-color:var(--border-bright);color:var(--text-primary)}
|
| 675 |
+
.movie-sort button.active{background:rgba(124,58,237,0.18);border-color:rgba(124,58,237,0.5);color:var(--accent-c2)}
|
| 676 |
+
.movie-suggest{position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:60;background:var(--bg-elevated);border:1px solid var(--border-bright);border-radius:var(--radius-md);box-shadow:0 18px 50px rgba(0,0,0,0.6);overflow:hidden;max-height:320px;overflow-y:auto}
|
| 677 |
+
.movie-suggest-item{display:flex;align-items:center;gap:10px;width:100%;padding:11px 16px;font-size:0.88rem;color:var(--text-secondary);transition:var(--transition);cursor:pointer;text-align:left;background:none;border:none;border-bottom:1px solid var(--border)}
|
| 678 |
+
.movie-suggest-item:last-child{border-bottom:none}
|
| 679 |
+
.movie-suggest-item:hover{background:rgba(124,58,237,0.12);color:var(--text-primary)}
|
| 680 |
+
.movie-suggest-item svg{color:var(--accent-v2);flex-shrink:0}
|
| 681 |
+
.load-more-wrap{display:flex;justify-content:center;margin-top:36px}
|
| 682 |
+
.load-more-btn{min-width:220px;justify-content:center}
|
| 683 |
+
|
| 684 |
+
/* MOVIE DETAILS */
|
| 685 |
+
.cast-row{display:flex;gap:16px;overflow-x:auto;padding:8px 0 20px;scrollbar-width:thin;scrollbar-color:var(--accent-v) transparent}
|
| 686 |
+
.cast-row::-webkit-scrollbar{height:4px}
|
| 687 |
+
.cast-row::-webkit-scrollbar-thumb{background:var(--accent-v);border-radius:99px}
|
| 688 |
+
.cast-chip{flex-shrink:0;width:110px;text-align:center;cursor:default;transition:var(--transition)}
|
| 689 |
+
.cast-chip:hover{transform:translateY(-4px)}
|
| 690 |
+
.cast-chip img{width:80px;height:80px;border-radius:50%;object-fit:cover;margin:0 auto 10px;border:2px solid var(--border);transition:var(--transition);box-shadow:0 4px 12px rgba(0,0,0,0.3)}
|
| 691 |
+
.cast-chip:hover img{border-color:var(--accent-v);box-shadow:0 6px 20px rgba(124,58,237,0.4)}
|
| 692 |
+
.cast-avatar-fallback{width:80px;height:80px;border-radius:50%;margin:0 auto 10px;background:var(--grad-accent);display:flex;align-items:center;justify-content:center;font-size:1.6rem;font-weight:800;color:#fff;box-shadow:0 4px 12px rgba(0,0,0,0.3)}
|
| 693 |
+
.cast-chip-name{font-size:0.82rem;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--text-primary)}
|
| 694 |
+
.cast-chip-role{font-size:0.7rem;color:var(--text-muted);margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.2}
|
| 695 |
+
.ep-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(80px,1fr));gap:12px}
|
| 696 |
+
.ep-grid-btn{position:relative;display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px;background:var(--bg-surface);border:1px solid var(--border);border-radius:12px;transition:var(--transition);cursor:pointer;color:var(--text-secondary);box-shadow:0 2px 8px rgba(0,0,0,0.15)}
|
| 697 |
+
.ep-grid-btn:hover{background:rgba(124,58,237,0.15);border-color:var(--accent-v);color:var(--text-primary);transform:translateY(-3px);box-shadow:0 6px 20px rgba(124,58,237,0.3)}
|
| 698 |
+
.ep-grid-btn.watched{background:rgba(34,197,94,0.08);border-color:rgba(34,197,94,0.25)}
|
| 699 |
+
.ep-grid-btn.watched:hover{background:rgba(34,197,94,0.15);border-color:rgba(34,197,94,0.4)}
|
| 700 |
+
.ep-grid-num{font-size:1rem;font-weight:700;color:var(--text-primary)}
|
| 701 |
+
.ep-grid-watched{position:absolute;top:8px;right:8px;width:20px;height:20px;border-radius:50%;background:#22c55e;color:#fff;font-size:0.7rem;font-weight:800;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,0.3)}
|
| 702 |
+
.ep-grid-play{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:50%;background:rgba(124,58,237,0.15);color:var(--accent-v2);transition:var(--transition)}
|
| 703 |
+
.ep-grid-btn:hover .ep-grid-play{background:var(--accent-v);color:#fff}
|
| 704 |
+
.ep-grid-btn.watched .ep-grid-play{background:rgba(34,197,94,0.15);color:#22c55e}
|
| 705 |
+
.ep-grid-btn.watched:hover .ep-grid-play{background:#22c55e;color:#fff}
|
| 706 |
+
|
| 707 |
+
/* MOVIE PLAYER */
|
| 708 |
+
.player-error{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;color:var(--text-muted);padding:20px;text-align:center}
|
| 709 |
+
.player-error .big{font-size:2.6rem}
|
| 710 |
+
.player-error p{font-size:0.9rem;max-width:420px}
|
| 711 |
+
|
| 712 |
+
/* CUSTOM PLAYER */
|
| 713 |
+
.custom-player{position:relative;width:100%;height:100%;background:#000;outline:none;overflow:hidden;cursor:default}
|
| 714 |
+
.custom-player video{width:100%;height:100%;object-fit:contain;background:#000}
|
| 715 |
+
.custom-player video::-webkit-media-controls{display:none !important}
|
| 716 |
+
.cp-fade{position:absolute;left:0;right:0;bottom:0;height:130px;background:linear-gradient(to top,rgba(0,0,0,0.88),transparent);pointer-events:none;transition:opacity .25s ease;z-index:3}
|
| 717 |
+
.cp-controls{position:absolute;left:0;right:0;bottom:0;padding:14px 16px 12px;display:flex;flex-direction:column;gap:8px;transition:opacity .25s ease,transform .25s ease;z-index:5}
|
| 718 |
+
.cp-controls.hide,.cp-fade.hide{opacity:0;pointer-events:none;transform:translateY(10px)}
|
| 719 |
+
.cp-seek{position:relative;height:18px;display:flex;align-items:center;cursor:pointer;touch-action:none}
|
| 720 |
+
.cp-seek-track{position:absolute;left:0;right:0;height:4px;border-radius:99px;background:rgba(255,255,255,0.22);transition:height .15s ease}
|
| 721 |
+
.cp-seek:hover .cp-seek-track{height:6px}
|
| 722 |
+
.cp-buffered{position:absolute;left:0;height:4px;border-radius:99px;background:rgba(255,255,255,0.32);transition:height .15s ease}
|
| 723 |
+
.cp-seek:hover .cp-buffered{height:6px}
|
| 724 |
+
.cp-progress{position:absolute;left:0;height:4px;border-radius:99px;background:var(--accent-v);box-shadow:0 0 12px rgba(124,58,237,0.65);transition:height .15s ease}
|
| 725 |
+
.cp-seek:hover .cp-progress{height:6px}
|
| 726 |
+
.cp-thumb{position:absolute;top:50%;width:13px;height:13px;border-radius:50%;background:#fff;transform:translate(-50%,-50%);opacity:0;transition:opacity .15s ease;box-shadow:0 1px 6px rgba(0,0,0,0.5);pointer-events:none}
|
| 727 |
+
.cp-seek:hover .cp-thumb{opacity:1}
|
| 728 |
+
.cp-hover-time{position:absolute;top:-28px;transform:translateX(-50%);background:rgba(10,10,18,0.92);border:1px solid var(--border-bright);color:#fff;font-size:0.72rem;font-weight:600;padding:2px 8px;border-radius:6px;pointer-events:none;white-space:nowrap}
|
| 729 |
+
.cp-row{display:flex;align-items:center;gap:4px}
|
| 730 |
+
.cp-btn{width:38px;height:38px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,0.92);transition:var(--transition);background:transparent;flex-shrink:0}
|
| 731 |
+
.cp-btn:hover{background:rgba(255,255,255,0.12);color:#fff}
|
| 732 |
+
.cp-btn.active{color:var(--accent-v2);background:rgba(124,58,237,0.22)}
|
| 733 |
+
@media(max-width:680px){.cp-row{gap:2px}.cp-btn{width:34px;height:34px}.cp-vol-slider{width:60px}.cp-btn:nth-child(2),.cp-btn:nth-child(3),.cp-btn:nth-child(11),.cp-btn:nth-child(12),.cp-btn:nth-child(13){display:none}.cp-time{display:none}}
|
| 734 |
+
.cp-time{font-size:0.8rem;font-weight:600;color:rgba(255,255,255,0.85);font-variant-numeric:tabular-nums;margin-left:6px;white-space:nowrap}
|
| 735 |
+
.cp-time em{font-style:normal;color:rgba(255,255,255,0.4);margin:0 4px}
|
| 736 |
+
.cp-spacer{flex:1}
|
| 737 |
+
.cp-vol-slider{width:74px;accent-color:var(--accent-v);cursor:pointer;flex-shrink:0}
|
| 738 |
+
.cp-menu{position:absolute;bottom:56px;right:12px;background:rgba(12,12,20,0.95);backdrop-filter:blur(14px);border:1px solid var(--border-bright);border-radius:var(--radius-md);padding:8px 10px 10px;min-width:172px;box-shadow:0 16px 44px rgba(0,0,0,0.6);z-index:10;animation:fadeUp .18s ease both;max-height:min(340px,60vh);overflow-y:auto}
|
| 739 |
+
.cp-menu-title{font-size:0.64rem;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--text-muted);padding:8px 10px 4px}
|
| 740 |
+
.cp-menu-title:first-child{padding-top:2px}
|
| 741 |
+
.cp-menu-item{display:block;width:100%;text-align:left;padding:7px 10px;border-radius:var(--radius-sm);font-size:0.82rem;font-weight:600;color:var(--text-secondary);transition:var(--transition);cursor:pointer;background:none;border:none}
|
| 742 |
+
.cp-menu-item:hover{background:rgba(255,255,255,0.06);color:var(--text-primary)}
|
| 743 |
+
.cp-menu-item.active{background:var(--accent-v);color:#fff}
|
| 744 |
+
.cp-menu-note{font-size:0.7rem;color:var(--text-muted);padding:8px 10px 2px}
|
| 745 |
+
.cp-center-play{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:76px;height:76px;border-radius:50%;background:rgba(10,10,18,0.6);backdrop-filter:blur(6px);border:1px solid rgba(255,255,255,0.18);color:#fff;display:flex;align-items:center;justify-content:center;transition:var(--transition);z-index:4}
|
| 746 |
+
.cp-center-play:hover{background:var(--accent-v);border-color:var(--accent-v);transform:translate(-50%,-50%) scale(1.08);box-shadow:0 0 34px rgba(124,58,237,0.6)}
|
| 747 |
+
.skip-btn{position:absolute;right:18px;bottom:78px;z-index:6;padding:9px 18px;border-radius:var(--radius-pill);background:rgba(12,12,20,0.85);backdrop-filter:blur(8px);border:1px solid var(--border-bright);color:#fff;font-size:0.84rem;font-weight:700;opacity:0;transform:translateY(10px);pointer-events:none;transition:var(--transition);display:flex;align-items:center;gap:6px}
|
| 748 |
+
.skip-btn.show{opacity:1;transform:translateY(0);pointer-events:auto}
|
| 749 |
+
.skip-btn:hover{background:var(--accent-v);border-color:var(--accent-v)}
|
| 750 |
+
.movie-quality-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:12px 16px;background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-md);margin:10px 0}
|
| 751 |
+
.movie-quality-label{font-size:0.7rem;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--text-muted);margin-right:4px}
|
| 752 |
+
.movie-quality-bar button{padding:5px 14px;border-radius:var(--radius-pill);font-size:0.78rem;font-weight:700;background:rgba(255,255,255,0.06);border:1px solid var(--border);color:var(--text-secondary);transition:var(--transition);cursor:pointer}
|
| 753 |
+
.movie-quality-bar button:hover{border-color:var(--border-bright);color:var(--text-primary)}
|
| 754 |
+
.movie-quality-bar button.active{background:var(--accent-v);border-color:var(--accent-v);color:#fff}
|
| 755 |
+
|
| 756 |
+
/* SCROLL ROW NAV */
|
| 757 |
+
.genre-nav-wrap{position:relative}
|
| 758 |
+
.row-nav-btn{position:absolute;top:50%;transform:translateY(-50%);z-index:6;width:38px;height:38px;border-radius:50%;background:rgba(10,10,18,0.85);backdrop-filter:blur(8px);border:1px solid var(--border-bright);color:var(--text-primary);display:flex;align-items:center;justify-content:center;opacity:0;transition:var(--transition);cursor:pointer;box-shadow:0 6px 20px rgba(0,0,0,0.5)}
|
| 759 |
+
.genre-nav-wrap:hover .row-nav-btn{opacity:1}
|
| 760 |
+
.row-nav-btn:hover{background:var(--accent-v);border-color:var(--accent-v);transform:translateY(-50%) scale(1.08)}
|
| 761 |
+
.row-nav-btn.prev{left:-14px}
|
| 762 |
+
.row-nav-btn.next{right:-14px}
|
| 763 |
+
|
| 764 |
+
/* CONTINUE WATCHING */
|
| 765 |
+
.cw-subtitle{font-size:0.78rem;font-weight:500;color:var(--text-muted)}
|
| 766 |
+
.cw-card{position:relative;flex-shrink:0;width:250px;border-radius:var(--radius-md);overflow:hidden;background:var(--bg-card);cursor:pointer;transition:var(--transition);border:1px solid var(--border)}
|
| 767 |
+
.cw-card:hover{transform:translateY(-4px);border-color:var(--border-bright);box-shadow:var(--shadow-hover)}
|
| 768 |
+
.cw-poster{position:relative;aspect-ratio:16/9;overflow:hidden;background:var(--bg-elevated)}
|
| 769 |
+
.cw-poster img{width:100%;height:100%;object-fit:cover;transition:transform .45s ease}
|
| 770 |
+
.cw-card:hover .cw-poster img{transform:scale(1.06)}
|
| 771 |
+
.cw-overlay{position:absolute;inset:0;background:rgba(8,8,12,0.55);backdrop-filter:blur(2px);opacity:0;transition:var(--transition);display:flex;align-items:center;justify-content:center;gap:8px;color:#fff;font-size:0.86rem;font-weight:700}
|
| 772 |
+
.cw-card:hover .cw-overlay{opacity:1}
|
| 773 |
+
.cw-overlay svg{width:20px;height:20px}
|
| 774 |
+
.cw-badge{position:absolute;top:8px;left:8px;padding:2px 8px;border-radius:var(--radius-pill);background:rgba(8,8,8,0.78);backdrop-filter:blur(6px);border:1px solid var(--border);color:#fff;font-size:0.64rem;font-weight:700;letter-spacing:0.04em}
|
| 775 |
+
.cw-progress{position:absolute;left:0;right:0;bottom:0;height:4px;background:rgba(255,255,255,0.16)}
|
| 776 |
+
.cw-progress-fill{height:100%;background:var(--grad-accent);box-shadow:0 0 8px rgba(124,58,237,0.6)}
|
| 777 |
+
.cw-remove{position:absolute;top:6px;right:6px;width:22px;height:22px;border-radius:50%;background:rgba(8,8,8,0.8);border:1px solid var(--border);color:var(--text-secondary);font-size:0.9rem;line-height:1;display:flex;align-items:center;justify-content:center;opacity:0;transition:var(--transition);cursor:pointer;padding:0}
|
| 778 |
+
.cw-card:hover .cw-remove{opacity:1}
|
| 779 |
+
.cw-remove:hover{background:#e11d48;border-color:#e11d48;color:#fff}
|
| 780 |
+
.cw-body{padding:10px 12px 12px}
|
| 781 |
+
.cw-body h3{font-size:0.88rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:4px}
|
| 782 |
+
.cw-meta{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:0.7rem;color:var(--text-muted)}
|
| 783 |
+
.cw-meta-left{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
| 784 |
+
.cw-left{color:var(--accent-v2);font-weight:600;font-variant-numeric:tabular-nums;white-space:nowrap}
|
| 785 |
+
|
| 786 |
+
/* TOP 10 SPOTLIGHT — full-width slideshow */
|
| 787 |
+
.spotlight-featured{position:relative;width:100%;min-height:500px;border-radius:16px;overflow:hidden;background:var(--bg-elevated) center/cover no-repeat;border:1px solid var(--border);cursor:pointer;transition:var(--transition)}
|
| 788 |
+
.spotlight-featured:hover{transform:translateY(-4px);border-color:var(--border-bright);box-shadow:var(--shadow-hover)}
|
| 789 |
+
.spotlight-featured-grad{position:absolute;inset:0;background:linear-gradient(180deg, rgba(8,8,12,0.08) 0%, rgba(8,8,12,0.24) 34%, rgba(8,8,12,0.72) 75%, rgba(8,8,12,0.9) 100%)}
|
| 790 |
+
.spotlight-featured-body{position:absolute;left:0;right:0;bottom:0;padding:26px 26px 24px;z-index:2}
|
| 791 |
+
.spotlight-featured-meta-row{display:flex;align-items:center;gap:10px;font-size:0.72rem;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;color:#f8d6d6;opacity:0.9;margin-bottom:12px}
|
| 792 |
+
.spotlight-featured-bubble{display:inline-flex;align-items:center;justify-content:center;margin-bottom:12px}
|
| 793 |
+
.spotlight-featured-bubble-inner{display:flex;flex-direction:column;align-items:flex-start;padding:11px 16px;border-radius:999px;background:rgba(18, 58, 130, 0.38);border:1px solid rgba(255,255,255,0.18);backdrop-filter:blur(8px)}
|
| 794 |
+
.spotlight-featured-bubble-kicker{font-size:0.68rem;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:#dbeafe}
|
| 795 |
+
.spotlight-featured-bubble-title{font-size:1rem;font-weight:800;color:#f2fbff;line-height:1.15}
|
| 796 |
+
.spotlight-featured-title{font-family:'Bebas Neue','Outfit',sans-serif;font-size:clamp(2.2rem,4.6vw,4.4rem);font-weight:400;line-height:0.9;margin:0 0 10px;text-shadow:0 8px 30px rgba(0,0,0,0.7);letter-spacing:0.02em;max-width:52%}
|
| 797 |
+
.spotlight-featured-caption{max-width:48%;font-size:0.9rem;color:rgba(255,255,255,0.8);line-height:1.55;margin-bottom:18px}
|
| 798 |
+
.spotlight-featured-actions{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
| 799 |
+
.spotlight-featured-btn{position:relative;z-index:2}
|
| 800 |
+
.spotlight-featured-btn.secondary{background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.12);color:#fff}
|
| 801 |
+
.spotlight-slide{animation:spotFade .5s ease both}
|
| 802 |
+
@keyframes spotFade{from{opacity:0;transform:scale(1.03)}to{opacity:1;transform:scale(1)}}
|
| 803 |
+
.spotlight-nav-btn{position:absolute;top:50%;transform:translateY(-50%);z-index:3;width:36px;height:36px;border-radius:50%;background:rgba(8,8,8,0.62);backdrop-filter:blur(6px);border:1px solid rgba(255,255,255,0.16);color:#fff;display:flex;align-items:center;justify-content:center;opacity:0.7;transition:var(--transition);cursor:pointer}
|
| 804 |
+
.spotlight-featured:hover .spotlight-nav-btn{opacity:1}
|
| 805 |
+
.spotlight-nav-btn:hover{background:var(--accent-v);border-color:var(--accent-v);transform:translateY(-50%) scale(1.08)}
|
| 806 |
+
.spotlight-nav-btn.prev{left:12px}
|
| 807 |
+
.spotlight-nav-btn.next{right:12px}
|
| 808 |
+
.spotlight-dots{position:absolute;bottom:14px;left:0;right:0;display:flex;justify-content:center;gap:6px;z-index:3}
|
| 809 |
+
.spotlight-dot{width:7px;height:7px;border-radius:50%;background:rgba(255,255,255,0.35);transition:var(--transition);cursor:pointer;padding:0}
|
| 810 |
+
.spotlight-dot.active{background:var(--accent-v2);width:20px;border-radius:99px;box-shadow:0 0 10px rgba(124,58,237,0.6)}
|
| 811 |
+
|
| 812 |
+
.genre-section{padding-top:8px;padding-bottom:12px}
|
| 813 |
+
.genre-row-wrap{position:relative;display:flex;align-items:center;gap:12px}
|
| 814 |
+
.genre-row{display:flex;align-items:center;gap:10px;overflow-x:auto;scrollbar-width:none;padding:6px 0}
|
| 815 |
+
.genre-row::-webkit-scrollbar{display:none}
|
| 816 |
+
.genre-pill{padding:8px 16px;border-radius:var(--radius-pill);background:rgba(255,255,255,0.035);border:1px solid var(--border);color:var(--text-secondary);font-size:0.82rem;font-weight:600;transition:var(--transition);cursor:pointer;white-space:nowrap}
|
| 817 |
+
.genre-pill:hover{background:rgba(124,58,237,0.12);border-color:var(--border-bright);color:var(--text-primary)}
|
| 818 |
+
|
| 819 |
+
.home-dashboard{padding-top:4px}
|
| 820 |
+
.home-layout{display:grid;grid-template-columns:minmax(0, 1.9fr) 300px;gap:22px;align-items:start}
|
| 821 |
+
.home-main-column{display:flex;flex-direction:column;gap:10px}
|
| 822 |
+
.home-sidebar{display:flex;flex-direction:column;gap:18px;position:sticky;top:88px}
|
| 823 |
+
.sidebar-panel{background:rgba(255,255,255,0.02);border:1px solid var(--border);border-radius:12px;padding:18px 14px}
|
| 824 |
+
.sidebar-panel h3{font-size:0.96rem;font-weight:800;letter-spacing:0.02em;margin:0 0 14px}
|
| 825 |
+
.sidebar-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
|
| 826 |
+
.sidebar-list li{display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid rgba(255,255,255,0.04);cursor:pointer}
|
| 827 |
+
.sidebar-list li:last-child{border-bottom:none}
|
| 828 |
+
.sidebar-rank{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:8px;background:var(--grad-accent);font-size:0.72rem;font-weight:800;color:#fff;flex-shrink:0}
|
| 829 |
+
.sidebar-rank.alt{background:linear-gradient(135deg, rgba(14,165,233,0.9), rgba(124,58,237,0.9))}
|
| 830 |
+
.sidebar-info{display:flex;flex-direction:column;min-width:0}
|
| 831 |
+
.sidebar-info strong{font-size:0.8rem;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
| 832 |
+
.sidebar-info small{font-size:0.67rem;color:var(--text-muted);margin-top:2px}
|
| 833 |
+
|
| 834 |
+
/* LANDING ENTER CTA */
|
| 835 |
+
.landing-enter{position:relative;padding:90px 24px;overflow:hidden;text-align:center;border-top:1px solid var(--border)}
|
| 836 |
+
.landing-enter-glow{position:absolute;inset:0;background:radial-gradient(ellipse 55% 80% at 50% 110%,rgba(124,58,237,0.3) 0%,transparent 65%);pointer-events:none}
|
| 837 |
+
.landing-enter-inner{position:relative;max-width:560px;margin:0 auto}
|
| 838 |
+
.landing-enter-label{font-size:0.72rem;font-weight:700;letter-spacing:0.16em;text-transform:uppercase;color:var(--accent-v2);margin-bottom:14px}
|
| 839 |
+
.landing-enter-title{font-family:'Bebas Neue','Outfit',sans-serif;font-size:clamp(2rem,4vw,3.2rem);font-weight:400;line-height:1.05;margin-bottom:12px}
|
| 840 |
+
.landing-enter-desc{font-size:0.95rem;color:var(--text-secondary);line-height:1.7;margin-bottom:26px}
|
| 841 |
+
|
| 842 |
+
/* SHARED EMPTY STATE + CARD EXTRAS */
|
| 843 |
+
.empty{padding:40px;text-align:center;color:var(--text-muted)}
|
| 844 |
+
.empty .big{font-size:2.6rem;margin-bottom:12px}
|
| 845 |
+
.empty p{font-size:0.9rem}
|
| 846 |
+
.card-rank{position:absolute;top:8px;left:8px;z-index:2;width:26px;height:26px;border-radius:8px;background:var(--grad-accent);color:#fff;font-size:0.78rem;font-weight:800;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 14px rgba(0,0,0,0.5)}
|
| 847 |
+
.card-type-badge{position:absolute;top:8px;right:8px;z-index:2;padding:3px 9px;border-radius:var(--radius-pill);background:rgba(8,8,8,0.72);backdrop-filter:blur(6px);border:1px solid var(--border-bright);color:var(--accent-c2);font-size:0.62rem;font-weight:700;letter-spacing:0.04em;text-transform:uppercase}
|
| 848 |
+
.card-type-badge.coming-soon{background:rgba(245,158,11,0.18);border-color:rgba(245,158,11,0.55);color:#fcd34d}
|
| 849 |
+
.card-quick-btn.coming-soon{background:rgba(245,158,11,0.9);color:#1a1205}
|
| 850 |
+
.card-ep-badge{position:absolute;top:8px;left:8px;z-index:2;padding:3px 9px;border-radius:var(--radius-pill);background:rgba(8,8,8,0.72);backdrop-filter:blur(6px);border:1px solid var(--border);color:#fff;font-size:0.62rem;font-weight:700;letter-spacing:0.04em}
|
| 851 |
+
|
| 852 |
+
/* MOBILE BOTTOM NAV (tab bar with every section) */
|
| 853 |
+
#root .mobile-nav{display:flex;position:fixed;left:0;right:0;bottom:0;z-index:120;height:calc(60px + env(safe-area-inset-bottom));padding-bottom:env(safe-area-inset-bottom);background:rgba(10,10,18,0.96);backdrop-filter:blur(18px);border-top:1px solid var(--border);justify-content:space-around;align-items:stretch;box-shadow:0 -16px 34px rgba(10,10,14,0.65)}
|
| 854 |
+
#root .mobile-nav-item{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;color:var(--text-muted);font-size:0.6rem;font-weight:600;letter-spacing:0.02em;transition:var(--transition)}
|
| 855 |
+
#root .mobile-nav-item.active{color:var(--accent-v2);background:linear-gradient(180deg, rgba(124,58,237,0.12), rgba(124,58,237,0.02));}
|
| 856 |
+
#root .mobile-nav-item.active svg{filter:drop-shadow(0 0 8px rgba(124,58,237,0.6))}
|
| 857 |
+
#root .mobile-nav-item:active{transform:scale(0.94)}
|
| 858 |
+
@media(max-width:1024px){
|
| 859 |
+
.search-wrap{max-width:260px}
|
| 860 |
+
nav{gap:0}
|
| 861 |
+
nav a{padding:6px 10px;font-size:0.8rem}
|
| 862 |
+
.container{padding:0 20px}
|
| 863 |
+
.page-shell{padding-top:calc(var(--header-h) + 24px);padding-bottom:64px}
|
| 864 |
+
.catalog-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:18px 14px}
|
| 865 |
+
.watch-grid,.info-layout{grid-template-columns:1fr}.watch-sidebar{position:static;max-height:500px}.info-hero-body{flex-direction:column;align-items:flex-start}.spotlight-featured{min-height:340px}
|
| 866 |
+
}
|
| 867 |
+
@media(max-width:900px){
|
| 868 |
+
.main-content{padding-bottom:calc(66px + env(safe-area-inset-bottom))}
|
| 869 |
+
.home-hero{padding-bottom:110px}
|
| 870 |
+
.scroll-indicator{bottom:calc(80px + env(safe-area-inset-bottom))}
|
| 871 |
+
.landing-enter{padding-bottom:120px}
|
| 872 |
+
}
|
| 873 |
+
|
| 874 |
+
/* ============ DESKTOP (>=681px) - Hide mobile nav ============ */
|
| 875 |
+
@media(min-width:681px){
|
| 876 |
+
.mobile-nav{
|
| 877 |
+
display: none !important;
|
| 878 |
+
}
|
| 879 |
+
}
|
| 880 |
+
|
| 881 |
+
/* ============ MOBILE (<=680px) ============ */
|
| 882 |
+
@media(max-width:680px){
|
| 883 |
+
.container{padding:0 16px}
|
| 884 |
+
nav{display:none}
|
| 885 |
+
.page-shell{padding-top:calc(var(--header-h) + 18px);padding-bottom:60px}
|
| 886 |
+
.catalog-grid{grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:16px 12px}
|
| 887 |
+
.search-shell{gap:12px}
|
| 888 |
+
|
| 889 |
+
/* Header: compact but still polished */
|
| 890 |
+
#header .container{gap:10px}
|
| 891 |
+
.logo-wordmark .l{font-size:1.6rem}
|
| 892 |
+
.search-wrap{display:none}
|
| 893 |
+
.auth-avatar-btn{padding:5px 7px;gap:0;border-radius:14px}
|
| 894 |
+
.auth-avatar-btn span,.auth-avatar-btn>svg:last-child{display:none}
|
| 895 |
+
.auth-avatar-circle{width:30px;height:30px}
|
| 896 |
+
.auth-dropdown{min-width:220px;max-width:calc(100vw - 24px)}
|
| 897 |
+
|
| 898 |
+
/* Home */
|
| 899 |
+
.content-section{padding:26px 0}
|
| 900 |
+
.section-header{margin-bottom:14px}
|
| 901 |
+
.section-title{font-size:1.02rem}
|
| 902 |
+
.section-title::before{height:19px}
|
| 903 |
+
.scroll-row{gap:10px}
|
| 904 |
+
.anime-card{width:138px;border-radius:var(--radius-sm)}
|
| 905 |
+
.skel-card{width:138px}
|
| 906 |
+
.card-body{padding:8px 9px 10px}
|
| 907 |
+
.card-title{font-size:0.78rem}
|
| 908 |
+
.cw-card{width:198px}
|
| 909 |
+
|
| 910 |
+
/* Spotlight slideshow */
|
| 911 |
+
.spotlight-featured{min-height:262px;border-radius:14px}
|
| 912 |
+
.spotlight-featured-body{padding:12px 12px 38px}
|
| 913 |
+
.spotlight-featured-score{font-size:0.68rem;margin-bottom:4px}
|
| 914 |
+
.spotlight-featured-title{font-size:1.28rem;margin-bottom:4px}
|
| 915 |
+
.spotlight-featured-meta{font-size:0.7rem;margin-bottom:10px}
|
| 916 |
+
.spotlight-featured-btn{padding:8px 15px;font-size:0.76rem}
|
| 917 |
+
.spotlight-nav-btn{opacity:1;width:30px;height:30px}
|
| 918 |
+
.spotlight-nav-btn.prev{left:8px}
|
| 919 |
+
.spotlight-nav-btn.next{right:8px}
|
| 920 |
+
.spotlight-dots{bottom:9px;gap:5px}
|
| 921 |
+
.spotlight-dot{width:6px;height:6px}
|
| 922 |
+
.spotlight-dot.active{width:16px}
|
| 923 |
+
|
| 924 |
+
.genre-row-wrap{gap:8px}
|
| 925 |
+
.genre-row{gap:8px}
|
| 926 |
+
.genre-pill{padding:7px 12px;font-size:0.74rem}
|
| 927 |
+
.home-layout{grid-template-columns:1fr;gap:14px}
|
| 928 |
+
.home-sidebar{position:static}
|
| 929 |
+
.sidebar-panel{padding:14px 12px}
|
| 930 |
+
|
| 931 |
+
/* Buttons + shared */
|
| 932 |
+
.btn{padding:10px 18px;font-size:0.85rem}
|
| 933 |
+
.btn-sm{padding:6px 13px;font-size:0.78rem}
|
| 934 |
+
.section-link{font-size:0.78rem}
|
| 935 |
+
|
| 936 |
+
/* Landing hero */
|
| 937 |
+
.home-hero{padding:calc(var(--header-h) + 30px) 14px 0}
|
| 938 |
+
.home-hero{padding-bottom:110px}
|
| 939 |
+
.hero-badge{padding:5px 13px;font-size:0.66rem;margin-bottom:22px}
|
| 940 |
+
.hero-logo-text{font-size:clamp(3.2rem,17vw,5.6rem);margin-bottom:12px}
|
| 941 |
+
.hero-subtitle{font-size:clamp(1.05rem,3.6vw,1.45rem);margin-bottom:10px}
|
| 942 |
+
.hero-desc{font-size:0.88rem;margin-bottom:22px}
|
| 943 |
+
.hero-filters{gap:6px;margin-bottom:18px}
|
| 944 |
+
.hero-filter-pill{padding:6px 14px;font-size:0.8rem}
|
| 945 |
+
.hero-search-input{padding:12px 52px 12px 16px;font-size:0.9rem}
|
| 946 |
+
.hero-search-btn{width:38px;height:38px}
|
| 947 |
+
.hero-ctas{gap:9px;margin-bottom:32px}
|
| 948 |
+
.hero-cta{padding:11px 17px;font-size:0.84rem;gap:7px}
|
| 949 |
+
.hero-stat{padding:0 14px}
|
| 950 |
+
.hero-stat-val{font-size:1.15rem}
|
| 951 |
+
.hero-stat-label{font-size:0.6rem}
|
| 952 |
+
|
| 953 |
+
/* Library marquee */
|
| 954 |
+
.library-section{padding:44px 0}
|
| 955 |
+
.library-header{gap:12px;margin-bottom:20px}
|
| 956 |
+
.library-header-text{font-size:0.62rem}
|
| 957 |
+
.library-header-line{max-width:90px}
|
| 958 |
+
.marquee-poster{width:86px;height:120px}
|
| 959 |
+
.marquee-row{gap:8px}
|
| 960 |
+
|
| 961 |
+
/* Feature section */
|
| 962 |
+
.feature-section{padding:48px 0}
|
| 963 |
+
.feature-eyebrow{font-size:0.7rem;margin-bottom:12px}
|
| 964 |
+
.feature-heading{font-size:1.7rem;line-height:1.2;margin-bottom:14px}
|
| 965 |
+
.feature-desc{font-size:0.9rem;margin-bottom:26px}
|
| 966 |
+
.feature-card{padding:18px}
|
| 967 |
+
.landing-enter{padding:56px 18px 118px}
|
| 968 |
+
.landing-enter-title{font-size:1.9rem}
|
| 969 |
+
.landing-enter-desc{font-size:0.88rem;margin-bottom:20px}
|
| 970 |
+
|
| 971 |
+
/* Details pages */
|
| 972 |
+
.info-hero{min-height:320px}
|
| 973 |
+
.info-hero-content{padding:20px 0}
|
| 974 |
+
.info-hero-body{gap:16px}
|
| 975 |
+
.info-poster{width:108px}
|
| 976 |
+
.info-title{font-size:1.7rem;margin-bottom:10px}
|
| 977 |
+
.info-type-tag{font-size:0.72rem;margin-bottom:8px}
|
| 978 |
+
.info-badges{margin-bottom:12px}
|
| 979 |
+
.info-genres{gap:6px;margin-bottom:16px}
|
| 980 |
+
.info-genre-pill{padding:4px 11px;font-size:0.74rem}
|
| 981 |
+
.info-actions{gap:8px}
|
| 982 |
+
.countdown-wrap{padding:10px 14px;gap:8px;margin-bottom:16px}
|
| 983 |
+
.countdown-cell{min-width:50px;padding:6px 4px}
|
| 984 |
+
.countdown-num{font-size:1.05rem}
|
| 985 |
+
.countdown-unit{font-size:0.56rem}
|
| 986 |
+
.info-body{padding:26px 0 60px}
|
| 987 |
+
.info-layout{gap:30px}
|
| 988 |
+
.info-section-label{font-size:1.02rem}
|
| 989 |
+
.info-synopsis{font-size:0.88rem}
|
| 990 |
+
.info-stat-cell{padding:13px 14px}
|
| 991 |
+
.char-avatar{width:64px;height:64px}
|
| 992 |
+
.char-name{font-size:0.72rem;max-width:72px}
|
| 993 |
+
.info-btn-watch{padding:10px 20px}
|
| 994 |
+
|
| 995 |
+
/* Watch page */
|
| 996 |
+
.watch-container{padding:0 12px 60px}
|
| 997 |
+
.watch-grid{gap:12px;padding-top:8px}
|
| 998 |
+
.watch-anime-row{flex-wrap:wrap}
|
| 999 |
+
.watch-info-card{padding:14px 16px}
|
| 1000 |
+
.watch-info-title{font-size:0.95rem}
|
| 1001 |
+
.ep-title{font-size:1.1rem;margin-bottom:12px}
|
| 1002 |
+
.prefs-bar{padding:10px 12px;gap:12px}
|
| 1003 |
+
.pref-item{font-size:0.78rem}
|
| 1004 |
+
.audio-btn,.server-btn{padding:4px 10px;font-size:0.74rem}
|
| 1005 |
+
.audio-notice{margin:10px 0;font-size:0.78rem;padding:8px 12px}
|
| 1006 |
+
.watch-anime-name{font-size:0.85rem}
|
| 1007 |
+
.sidebar-upnext{font-size:0.92rem}
|
| 1008 |
+
.ep-thumb{width:64px;height:40px}
|
| 1009 |
+
.ep-item{padding:6px}
|
| 1010 |
+
.ep-info-title{font-size:0.76rem}
|
| 1011 |
+
.ep-grid{grid-template-columns:repeat(auto-fill,minmax(64px,1fr));gap:8px}
|
| 1012 |
+
.movie-quality-bar{padding:10px 12px;gap:6px}
|
| 1013 |
+
.movie-quality-bar button{padding:4px 11px;font-size:0.72rem}
|
| 1014 |
+
|
| 1015 |
+
/* Movies & TV */
|
| 1016 |
+
.movie-tabs{flex-wrap:nowrap;overflow-x:auto;padding-bottom:4px;scrollbar-width:none}
|
| 1017 |
+
.movie-tabs::-webkit-scrollbar{display:none}
|
| 1018 |
+
.movie-tabs button{white-space:nowrap;padding:7px 16px;font-size:0.8rem}
|
| 1019 |
+
.movie-sort button{padding:5px 11px;font-size:0.72rem}
|
| 1020 |
+
.load-more-btn{min-width:170px}
|
| 1021 |
+
|
| 1022 |
+
/* Custom player */
|
| 1023 |
+
.cp-controls{padding:10px 8px 8px}
|
| 1024 |
+
.cp-btn{width:34px;height:34px}
|
| 1025 |
+
.cp-vol-slider{width:52px}
|
| 1026 |
+
.cp-time{font-size:0.72rem}
|
| 1027 |
+
.cp-menu{right:6px;bottom:52px;min-width:150px}
|
| 1028 |
+
.cp-center-play{width:60px;height:60px}
|
| 1029 |
+
.skip-btn{bottom:72px;right:12px;padding:8px 14px;font-size:0.8rem}
|
| 1030 |
+
|
| 1031 |
+
/* Auth modal -> bottom sheet */
|
| 1032 |
+
.auth-overlay{padding:0;align-items:flex-end}
|
| 1033 |
+
.auth-modal{max-width:100%;border-radius:20px 20px 0 0;padding:24px 20px calc(20px + env(safe-area-inset-bottom));animation:sheetUp .25s ease both}
|
| 1034 |
+
@keyframes sheetUp{from{transform:translateY(44px);opacity:0}to{transform:translateY(0);opacity:1}}
|
| 1035 |
+
}
|
| 1036 |
+
|
| 1037 |
+
/* ============ SMALL PHONES (<=420px) ============ */
|
| 1038 |
+
@media(max-width:420px){
|
| 1039 |
+
.anime-card{width:126px}
|
| 1040 |
+
.skel-card{width:126px}
|
| 1041 |
+
.cw-card{width:172px}
|
| 1042 |
+
.marquee-poster{width:74px;height:104px}
|
| 1043 |
+
.info-poster{width:94px}
|
| 1044 |
+
.hero-logo-text{font-size:3rem}
|
| 1045 |
+
.hero-ctas{flex-direction:column}
|
| 1046 |
+
.hero-cta{width:100%;justify-content:center}
|
| 1047 |
+
.spotlight-featured{min-height:240px}
|
| 1048 |
+
.auth-dropdown{left:12px;right:12px;min-width:0}
|
| 1049 |
+
}
|
| 1050 |
+
|
| 1051 |
+
/* React Home discovery view (migrated from the legacy project, deliberately scoped). */
|
| 1052 |
+
#hero{position:relative;height:56vh;min-height:600px;max-height:720px;overflow:hidden;background:var(--bg-base)}
|
| 1053 |
+
#hero .hero-slide{position:absolute;inset:0;opacity:0;transition:opacity .7s ease;pointer-events:none;will-change:opacity}#hero .hero-slide.active{opacity:1;pointer-events:auto}
|
| 1054 |
+
#hero .hero-bg{position:absolute;inset:0}#hero .hero-bg img{width:100%;height:100%;object-fit:cover;object-position:center top;transform:scale(1.025);transition:transform 8s ease;filter:saturate(.9) contrast(1.04)}#hero .hero-slide.active .hero-bg img{transform:scale(1)}
|
| 1055 |
+
#hero .hero-gradient{position:absolute;inset:0;background:linear-gradient(90deg,rgba(6,7,10,.96) 0%,rgba(6,7,10,.64) 40%,rgba(6,7,10,.1) 72%),linear-gradient(0deg,rgba(6,7,10,.98) 0%,transparent 52%)}
|
| 1056 |
+
#hero .hero-content{position:relative;z-index:2;height:100%;display:flex;align-items:flex-end;padding:calc(var(--header-h) + 20px) 0 72px}#hero .hero-content .container{width:100%}#hero .hero-layout{display:flex;align-items:center;justify-content:space-between;gap:40px;width:100%}#hero .hero-info{max-width:560px;animation:fadeUp .6s ease both}
|
| 1057 |
+
#hero .hero-title-block{display:flex;flex-direction:column;align-items:flex-start;gap:10px}#hero .hero-badges{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:6px}#hero .hero-title{font-size:clamp(2rem,4vw,3.35rem);font-weight:800;line-height:1.12;letter-spacing:-.035em;margin-bottom:14px;text-wrap:balance}
|
| 1058 |
+
#hero .hero-desc{font-size:.95rem;color:var(--text-secondary);line-height:1.7;margin:0 0 18px;max-width:470px;display:-webkit-box;-webkit-line-clamp:3;line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;text-align:left}#hero .hero-genres{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:24px}#hero .hero-genre-tag{padding:4px 10px;border-radius:var(--radius-pill);background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.1);font-size:.75rem;font-weight:600;color:var(--text-secondary);transition:var(--transition)}#hero .hero-genre-tag:hover{background:rgba(124,58,237,.2);border-color:var(--accent-v);color:var(--accent-v2)}
|
| 1059 |
+
#hero .hero-actions{display:flex;gap:12px;flex-wrap:wrap}#hero .hero-stats{display:flex;gap:20px;margin-bottom:14px;flex-wrap:wrap}#hero .hero-stat{display:flex;align-items:center;gap:6px;font-size:.82rem;color:var(--text-secondary)}#hero .hero-stat svg{color:var(--accent-v2);flex-shrink:0}
|
| 1060 |
+
#hero .hero-cover-wrap{position:relative;flex-shrink:0;animation:fadeIn .8s ease .3s both}#hero .hero-cover{width:190px;aspect-ratio:2/3;object-fit:cover;border-radius:var(--radius-lg);box-shadow:0 8px 48px rgba(0,0,0,.6),0 0 0 1px var(--border);display:block}#hero .hero-cover-glow{position:absolute;inset:-14px;border-radius:32px;background:radial-gradient(ellipse at center,rgba(139,92,246,.28),transparent 70%);z-index:-1;animation:glow-pulse 3s ease infinite}
|
| 1061 |
+
#hero .hero-prev,#hero .hero-next{position:absolute;top:50%;z-index:5;width:48px;height:48px;border-radius:50%;background:rgba(10,10,18,.72);border:1px solid var(--border);display:flex;align-items:center;justify-content:center;color:var(--text-primary);transition:var(--transition);backdrop-filter:blur(8px);opacity:0;pointer-events:none}#hero .hero-prev{left:40px;transform:translateY(-50%)}#hero .hero-next{right:40px;transform:translateY(-50%)}#hero:hover .hero-next,#hero:hover .hero-prev{opacity:1;pointer-events:auto}#hero .hero-prev:hover{background:rgba(124,58,237,.35);border-color:var(--accent-v);transform:translate(-4px,-50%)}#hero .hero-next:hover{background:rgba(124,58,237,.35);border-color:var(--accent-v);transform:translate(4px,-50%)}
|
| 1062 |
+
#hero .hero-indicators{position:absolute;bottom:25px;left:50%;transform:translateX(-50%);display:flex;gap:8px;z-index:5}#hero .hero-dot{width:8px;height:8px;border-radius:99px;background:rgba(255,255,255,.3);transition:var(--transition)}#hero .hero-dot.active{width:28px;background:var(--accent-v)}
|
| 1063 |
+
.genre-bar{display:flex;align-items:center;gap:8px;overflow-x:auto;padding:14px 0 12px;border-bottom:1px solid var(--border);scrollbar-width:none;margin-bottom:24px}.genre-bar::-webkit-scrollbar{display:none}.genre-pill{flex-shrink:0;padding:6px 14px;border-radius:var(--radius-pill);background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.08);font-size:.8rem;font-weight:500;color:var(--text-secondary);transition:var(--transition);white-space:nowrap}.genre-pill:hover{background:rgba(124,58,237,.18);border-color:var(--accent-v);color:var(--accent-v2);transform:translateY(-1px)}
|
| 1064 |
+
.home-grid{display:grid;grid-template-columns:minmax(0,1fr) 260px;gap:28px;align-items:start}.home-sections{min-width:0}.home-grid .sidebar-widget{position:sticky;top:calc(var(--header-h) + 16px);background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-md);overflow:hidden}.home-grid .sidebar-tabs{display:flex;gap:16px;padding:14px 16px 0;border-bottom:1px solid var(--border);background:rgba(255,255,255,.02)}.home-grid .sidebar-tab{padding:0 0 10px;font-size:.82rem;font-weight:700;text-transform:uppercase;color:var(--text-secondary);border-bottom:2px solid transparent;transition:var(--transition)}.home-grid .sidebar-tab:hover{color:var(--text-primary)}.home-grid .sidebar-tab.active{color:var(--accent-v2);border-bottom-color:var(--accent-v)}.home-grid .sidebar-item{display:flex;align-items:center;gap:12px;width:100%;padding:12px 16px;transition:var(--transition);border-bottom:1px solid var(--border);color:inherit;text-align:left}.home-grid .sidebar-item:last-child{border-bottom:none}.home-grid .sidebar-item:hover{background:var(--bg-elevated)}.home-grid .sidebar-thumb{width:50px;height:68px;object-fit:cover;border-radius:6px;flex-shrink:0}.home-grid .sidebar-rank{font-size:.72rem;font-weight:800;color:var(--text-muted);width:18px;flex-shrink:0}.home-grid .sidebar-title{font-size:.86rem;font-weight:600;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.home-grid .sidebar-meta{font-size:.7rem;color:var(--text-muted);margin-top:4px}
|
| 1065 |
+
/* Legacy browse sizing: roomy grid cards, compact two-column cards on phones. */
|
| 1066 |
+
.catalog-grid{grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:22px}
|
| 1067 |
+
@media(max-width:1024px){.home-grid{grid-template-columns:1fr}.home-grid .sidebar-widget{position:static;margin-top:20px}}@media(max-width:680px){#hero{height:620px;min-height:0}#hero .hero-content{align-items:flex-end;padding:calc(var(--header-h) + 18px) 0 58px}#hero .hero-cover-wrap,#hero .hero-prev,#hero .hero-next{display:none}#hero .hero-title{font-size:clamp(1.8rem,9vw,2.5rem)}#hero .hero-desc{-webkit-line-clamp:4;line-clamp:4}#hero .hero-indicators{bottom:22px}.genre-bar{margin-bottom:10px}}
|
| 1068 |
+
@media(max-width:600px){.catalog-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.catalog-grid .anime-card{width:100%}}
|
| 1069 |
+
@media(min-width:601px) and (max-width:768px){.anime-card{width:130px}}@media(max-width:420px){.anime-card{width:126px}}
|
| 1070 |
+
|
| 1071 |
+
/* Watch-page anime information card: an intentional media summary instead of loose text. */
|
| 1072 |
+
.watch-desc-card{display:flex;gap:18px;align-items:flex-start;position:relative;isolation:isolate;overflow:hidden;background:linear-gradient(135deg,rgba(124,58,237,.16),rgba(13,13,20,.96) 38%,rgba(11,11,17,.98));border-color:rgba(139,92,246,.26);box-shadow:0 18px 42px rgba(0,0,0,.2)}.watch-desc-card::before{content:"";position:absolute;inset:0;background:radial-gradient(circle at 0 0,rgba(167,139,250,.15),transparent 42%);z-index:-1}.watch-info-poster{width:92px;aspect-ratio:2/3;object-fit:cover;border-radius:10px;box-shadow:0 10px 24px rgba(0,0,0,.42);flex-shrink:0}.watch-info-body{min-width:0;flex:1}.watch-info-kicker{font-size:.66rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--accent-v2);margin-bottom:5px}.watch-info-title{font-size:1.22rem;font-weight:800;line-height:1.22;letter-spacing:-.02em;margin-bottom:10px}.watch-info-meta{display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin-bottom:12px}.watch-meta-item,.watch-meta-score{display:inline-flex;align-items:center;min-height:23px;padding:3px 8px;border-radius:999px;background:rgba(255,255,255,.055);border:1px solid rgba(255,255,255,.075);font-size:.7rem;font-weight:700;color:var(--text-secondary)}.watch-meta-score{color:#fcd34d;background:rgba(245,158,11,.1);border-color:rgba(245,158,11,.2)}.watch-meta-dot{display:none}.watch-genres{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 11px}.watch-genre-pill{padding:3px 8px;border-radius:999px;background:rgba(124,58,237,.13);border:1px solid rgba(124,58,237,.22);color:#c4b5fd;font-size:.68rem;font-weight:700}.watch-synopsis{margin:0;color:var(--text-secondary);font-size:.86rem;line-height:1.62;display:-webkit-box;-webkit-line-clamp:3;line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}@media(max-width:600px){.watch-desc-card{gap:12px;padding:14px}.watch-info-poster{width:72px;border-radius:8px}.watch-info-title{font-size:1.04rem}.watch-synopsis{-webkit-line-clamp:4;line-clamp:4}}
|
frontend/src/main.jsx
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from "react";
|
| 2 |
+
import ReactDOM from "react-dom/client";
|
| 3 |
+
import { BrowserRouter } from "react-router-dom";
|
| 4 |
+
import App from "./App.jsx";
|
| 5 |
+
import "./index.css";
|
| 6 |
+
|
| 7 |
+
ReactDOM.createRoot(document.getElementById("root")).render(
|
| 8 |
+
<React.StrictMode>
|
| 9 |
+
<BrowserRouter>
|
| 10 |
+
<App />
|
| 11 |
+
</BrowserRouter>
|
| 12 |
+
</React.StrictMode>
|
| 13 |
+
);
|
frontend/src/pages/Anime.jsx
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from "react";
|
| 2 |
+
import { Link, useNavigate, useParams } from "react-router-dom";
|
| 3 |
+
import { api, titleOf } from "../api.js";
|
| 4 |
+
import AnimeRow from "../components/AnimeRow.jsx";
|
| 5 |
+
|
| 6 |
+
export default function Anime() {
|
| 7 |
+
const { id } = useParams();
|
| 8 |
+
const navigate = useNavigate();
|
| 9 |
+
const [media, setMedia] = useState(null);
|
| 10 |
+
const [epData, setEpData] = useState(null);
|
| 11 |
+
const [loading, setLoading] = useState(true);
|
| 12 |
+
const [showFullSynopsis, setShowFullSynopsis] = useState(false);
|
| 13 |
+
// Tick every second while the next episode's air time is in the future.
|
| 14 |
+
const [now, setNow] = useState(() => Date.now());
|
| 15 |
+
|
| 16 |
+
useEffect(() => {
|
| 17 |
+
let alive = true;
|
| 18 |
+
setLoading(true);
|
| 19 |
+
(async () => {
|
| 20 |
+
try {
|
| 21 |
+
const [m, eps] = await Promise.all([
|
| 22 |
+
api.details(id),
|
| 23 |
+
api.episodes(id).catch(() => null)
|
| 24 |
+
]);
|
| 25 |
+
if (!alive) return;
|
| 26 |
+
setMedia(m);
|
| 27 |
+
setEpData(eps);
|
| 28 |
+
} catch (e) {
|
| 29 |
+
console.error(e);
|
| 30 |
+
} finally {
|
| 31 |
+
if (alive) setLoading(false);
|
| 32 |
+
}
|
| 33 |
+
})();
|
| 34 |
+
return () => { alive = false; };
|
| 35 |
+
}, [id]);
|
| 36 |
+
|
| 37 |
+
// Live countdown to the next airing episode (absolute timestamp from AniList).
|
| 38 |
+
const nextAiring = media?.nextAiringEpisode;
|
| 39 |
+
const airTargetMs = nextAiring?.airingAt ? nextAiring.airingAt * 1000 : 0;
|
| 40 |
+
useEffect(() => {
|
| 41 |
+
if (!airTargetMs) return;
|
| 42 |
+
setNow(Date.now());
|
| 43 |
+
const t = setInterval(() => {
|
| 44 |
+
// Stop ticking once the episode airs — no need to re-render forever.
|
| 45 |
+
if (airTargetMs - Date.now() <= 0) clearInterval(t);
|
| 46 |
+
setNow(Date.now());
|
| 47 |
+
}, 1000);
|
| 48 |
+
return () => clearInterval(t);
|
| 49 |
+
}, [airTargetMs]);
|
| 50 |
+
|
| 51 |
+
const remainingMs = airTargetMs ? Math.max(0, airTargetMs - now) : 0;
|
| 52 |
+
const showCountdown = airTargetMs > 0 && remainingMs > 0;
|
| 53 |
+
const totalSecs = Math.floor(remainingMs / 1000);
|
| 54 |
+
const cdDays = Math.floor(totalSecs / 86400);
|
| 55 |
+
const cdHours = Math.floor((totalSecs % 86400) / 3600);
|
| 56 |
+
const cdMins = Math.floor((totalSecs % 3600) / 60);
|
| 57 |
+
const cdSecs = totalSecs % 60;
|
| 58 |
+
const pad = (n) => String(n).padStart(2, "0");
|
| 59 |
+
|
| 60 |
+
if (loading || !media) {
|
| 61 |
+
return (
|
| 62 |
+
<div className="loader" style={{ minHeight: "80vh" }}>
|
| 63 |
+
<div className="spinner" />
|
| 64 |
+
<span>Loading anime details...</span>
|
| 65 |
+
</div>
|
| 66 |
+
);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
const title = titleOf(media);
|
| 70 |
+
const banner = media.bannerImage || media.coverImage;
|
| 71 |
+
const score = media.score ? (media.score > 10 ? (media.score / 10).toFixed(1) : media.score) : "N/A";
|
| 72 |
+
|
| 73 |
+
function handleWatchNow() {
|
| 74 |
+
const provider = (epData?.providers || [])[0];
|
| 75 |
+
const firstEp = provider?.sub?.[0] || provider?.dub?.[0];
|
| 76 |
+
if (firstEp) {
|
| 77 |
+
navigate(`/watch/${firstEp.id}?animeId=${media.id}`);
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
return (
|
| 82 |
+
<div className="info-page">
|
| 83 |
+
<section className="info-hero">
|
| 84 |
+
{banner ? (
|
| 85 |
+
<div className="info-hero-bg" style={{ backgroundImage: `url(${api.img(banner)})` }} />
|
| 86 |
+
) : null}
|
| 87 |
+
|
| 88 |
+
<div className="container info-hero-inner">
|
| 89 |
+
<div className="info-poster">
|
| 90 |
+
<img src={api.img(media.coverImage)} alt={title} />
|
| 91 |
+
</div>
|
| 92 |
+
|
| 93 |
+
<div className="info-head">
|
| 94 |
+
<div className="info-eyebrow">
|
| 95 |
+
<span className="dot" />
|
| 96 |
+
<span>{media.format || "TV"}</span>
|
| 97 |
+
</div>
|
| 98 |
+
|
| 99 |
+
<div>
|
| 100 |
+
<h1 className="info-title">{title}</h1>
|
| 101 |
+
<div className="info-subtitle">
|
| 102 |
+
{media.seasonYear || "Ongoing"} • {media.status || "Airing"}
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
<div className="info-meta-row">
|
| 107 |
+
<span className="meta-pill meta-score">★ {score}</span>
|
| 108 |
+
<span className="meta-pill">{media.episodes || "?"} EP</span>
|
| 109 |
+
<span className="meta-pill">{media.duration ? `${media.duration}m` : "24m"}</span>
|
| 110 |
+
<span className={`meta-pill ${media.status === "RELEASING" ? "meta-status-airing" : ""}`}>
|
| 111 |
+
{media.status || "Airing"}
|
| 112 |
+
</span>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
{media.genres ? (
|
| 116 |
+
<div className="info-genres">
|
| 117 |
+
{media.genres.map(g => (
|
| 118 |
+
<span key={g} className="info-genre-tag">{g}</span>
|
| 119 |
+
))}
|
| 120 |
+
</div>
|
| 121 |
+
) : null}
|
| 122 |
+
|
| 123 |
+
<div className="info-cta">
|
| 124 |
+
<button className="btn-watch" type="button" onClick={handleWatchNow}>▶ Watch Now</button>
|
| 125 |
+
<button className="btn-secondary" type="button">Share</button>
|
| 126 |
+
<button className="btn-secondary" type="button">My List</button>
|
| 127 |
+
</div>
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
</section>
|
| 131 |
+
|
| 132 |
+
<main className="info-body">
|
| 133 |
+
<div className="info-grid-2">
|
| 134 |
+
<section className="info-section">
|
| 135 |
+
<h2 className="info-section-title">Synopsis</h2>
|
| 136 |
+
<p className={`info-desc ${showFullSynopsis ? "expanded" : ""}`}>
|
| 137 |
+
{media.synopsis || "No description available for this title."}
|
| 138 |
+
</p>
|
| 139 |
+
{media.synopsis && media.synopsis.length > 200 ? (
|
| 140 |
+
<button className="info-desc-toggle" onClick={() => setShowFullSynopsis(v => !v)}>
|
| 141 |
+
{showFullSynopsis ? "Show Less" : "Read More"}
|
| 142 |
+
</button>
|
| 143 |
+
) : null}
|
| 144 |
+
</section>
|
| 145 |
+
|
| 146 |
+
<aside className="info-section">
|
| 147 |
+
<h2 className="info-section-title">Details</h2>
|
| 148 |
+
<div className="info-stats">
|
| 149 |
+
<div className="info-stat-row">
|
| 150 |
+
<span className="label">Format</span>
|
| 151 |
+
<span className="value">{media.format || "TV"}</span>
|
| 152 |
+
</div>
|
| 153 |
+
<div className="info-stat-row">
|
| 154 |
+
<span className="label">Status</span>
|
| 155 |
+
<span className="value">{media.status || "Airing"}</span>
|
| 156 |
+
</div>
|
| 157 |
+
<div className="info-stat-row">
|
| 158 |
+
<span className="label">Episodes</span>
|
| 159 |
+
<span className="value">{media.episodes || "Ongoing"}</span>
|
| 160 |
+
</div>
|
| 161 |
+
<div className="info-stat-row">
|
| 162 |
+
<span className="label">Duration</span>
|
| 163 |
+
<span className="value">{media.duration ? `${media.duration}m` : "24m"}</span>
|
| 164 |
+
</div>
|
| 165 |
+
<div className="info-stat-row">
|
| 166 |
+
<span className="label">Year</span>
|
| 167 |
+
<span className="value">{media.seasonYear || "1999"}</span>
|
| 168 |
+
</div>
|
| 169 |
+
<div className="info-stat-row">
|
| 170 |
+
<span className="label">Score</span>
|
| 171 |
+
<span className="value">{score}</span>
|
| 172 |
+
</div>
|
| 173 |
+
</div>
|
| 174 |
+
</aside>
|
| 175 |
+
</div>
|
| 176 |
+
|
| 177 |
+
{media.genres ? (
|
| 178 |
+
<section className="info-section">
|
| 179 |
+
<h2 className="info-section-title">Tags</h2>
|
| 180 |
+
<div className="info-tags">
|
| 181 |
+
{media.genres.map(g => (
|
| 182 |
+
<span key={g} className="info-tag">{g}</span>
|
| 183 |
+
))}
|
| 184 |
+
</div>
|
| 185 |
+
</section>
|
| 186 |
+
) : null}
|
| 187 |
+
|
| 188 |
+
{media.relations && media.relations.length > 0 ? (
|
| 189 |
+
<div className="info-section" style={{ marginTop: 12 }}>
|
| 190 |
+
<AnimeRow title="You Might Also Like" items={media.relations} />
|
| 191 |
+
</div>
|
| 192 |
+
) : null}
|
| 193 |
+
</main>
|
| 194 |
+
</div>
|
| 195 |
+
);
|
| 196 |
+
}
|
frontend/src/pages/AuthCallback.jsx
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from "react";
|
| 2 |
+
import { useNavigate, useSearchParams } from "react-router-dom";
|
| 3 |
+
import { api } from "../api.js";
|
| 4 |
+
import { getToken, setSession } from "../utils/auth.js";
|
| 5 |
+
|
| 6 |
+
export default function AuthCallback() {
|
| 7 |
+
const [searchParams] = useSearchParams();
|
| 8 |
+
const navigate = useNavigate();
|
| 9 |
+
const [msg, setMsg] = useState("Finishing sign in…");
|
| 10 |
+
const [error, setError] = useState("");
|
| 11 |
+
|
| 12 |
+
useEffect(() => {
|
| 13 |
+
const token = searchParams.get("token");
|
| 14 |
+
const err = searchParams.get("error");
|
| 15 |
+
if (err) {
|
| 16 |
+
setError(decodeURIComponent(err));
|
| 17 |
+
setMsg("");
|
| 18 |
+
const t = setTimeout(() => navigate("/home", { replace: true }), 3500);
|
| 19 |
+
return () => clearTimeout(t);
|
| 20 |
+
}
|
| 21 |
+
if (!token) {
|
| 22 |
+
setError("No sign-in result received.");
|
| 23 |
+
setMsg("");
|
| 24 |
+
const t = setTimeout(() => navigate("/home", { replace: true }), 3500);
|
| 25 |
+
return () => clearTimeout(t);
|
| 26 |
+
}
|
| 27 |
+
(async () => {
|
| 28 |
+
try {
|
| 29 |
+
setSession(token, null);
|
| 30 |
+
const user = await api.authMe();
|
| 31 |
+
setSession(getToken(), user);
|
| 32 |
+
navigate("/home", { replace: true });
|
| 33 |
+
} catch (e) {
|
| 34 |
+
setError(e.message || "Sign-in failed.");
|
| 35 |
+
setMsg("");
|
| 36 |
+
setTimeout(() => navigate("/home", { replace: true }), 3500);
|
| 37 |
+
}
|
| 38 |
+
})();
|
| 39 |
+
}, [searchParams, navigate]);
|
| 40 |
+
|
| 41 |
+
return (
|
| 42 |
+
<div className="loader" style={{ minHeight: "80vh", paddingTop: 120 }}>
|
| 43 |
+
<div className="spinner" />
|
| 44 |
+
{error ? (
|
| 45 |
+
<span style={{ color: "#fda4af", maxWidth: 420, textAlign: "center" }}>{error}</span>
|
| 46 |
+
) : (
|
| 47 |
+
<span>{msg}</span>
|
| 48 |
+
)}
|
| 49 |
+
</div>
|
| 50 |
+
);
|
| 51 |
+
}
|
frontend/src/pages/Home.jsx
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useMemo, useState } from "react";
|
| 2 |
+
import { useNavigate } from "react-router-dom";
|
| 3 |
+
import { api, titleOf } from "../api.js";
|
| 4 |
+
import AnimeRow from "../components/AnimeRow.jsx";
|
| 5 |
+
|
| 6 |
+
export default function Home() {
|
| 7 |
+
const navigate = useNavigate();
|
| 8 |
+
const [trending, setTrending] = useState([]);
|
| 9 |
+
const [popular, setPopular] = useState([]);
|
| 10 |
+
const [recent, setRecent] = useState([]);
|
| 11 |
+
|
| 12 |
+
useEffect(() => {
|
| 13 |
+
let alive = true;
|
| 14 |
+
(async () => {
|
| 15 |
+
try {
|
| 16 |
+
const [t, p, r] = await Promise.all([
|
| 17 |
+
api.trending(1),
|
| 18 |
+
api.popular(1),
|
| 19 |
+
api.recent(1),
|
| 20 |
+
]);
|
| 21 |
+
if (!alive) return;
|
| 22 |
+
setTrending(t.results || []);
|
| 23 |
+
setPopular(p.results || []);
|
| 24 |
+
setRecent(r.results || []);
|
| 25 |
+
} catch (e) {
|
| 26 |
+
console.error(e);
|
| 27 |
+
}
|
| 28 |
+
})();
|
| 29 |
+
return () => { alive = false; };
|
| 30 |
+
}, []);
|
| 31 |
+
|
| 32 |
+
const top10 = (trending || []).slice(0, 10);
|
| 33 |
+
const [slide, setSlide] = useState(0);
|
| 34 |
+
const [paused, setPaused] = useState(false);
|
| 35 |
+
|
| 36 |
+
useEffect(() => {
|
| 37 |
+
if (paused || top10.length < 2) return;
|
| 38 |
+
const t = setInterval(() => setSlide((s) => (s + 1) % top10.length), 5000);
|
| 39 |
+
return () => clearInterval(t);
|
| 40 |
+
}, [paused, top10.length]);
|
| 41 |
+
|
| 42 |
+
const safeSlide = Math.min(slide, Math.max(0, top10.length - 1));
|
| 43 |
+
const current = top10[safeSlide] || top10[0];
|
| 44 |
+
const go = (dir) => setSlide((s) => (s + dir + top10.length) % top10.length);
|
| 45 |
+
const jump = (i) => setSlide(i);
|
| 46 |
+
const fmtScore = (sc) => (sc != null ? (sc > 10 ? sc / 10 : sc).toFixed(1) : "");
|
| 47 |
+
|
| 48 |
+
const genres = useMemo(() => [
|
| 49 |
+
"Action", "Adventure", "Comedy", "Drama", "Ecchi", "Fantasy", "Horror",
|
| 50 |
+
"Mahou Shoujo", "Mecha", "Music", "Mystery", "Psychological", "Romance",
|
| 51 |
+
"Sci-Fi", "Slice of Life", "Sports", "Supernatural", "Thriller",
|
| 52 |
+
], []);
|
| 53 |
+
|
| 54 |
+
return (
|
| 55 |
+
<>
|
| 56 |
+
{current ? (
|
| 57 |
+
<section id="hero" onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
|
| 58 |
+
<button className="hero-prev" type="button" aria-label="Previous" onClick={(e) => { e.stopPropagation(); go(-1); }}>
|
| 59 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
| 60 |
+
<path d="M15 18l-6-6 6-6" />
|
| 61 |
+
</svg>
|
| 62 |
+
</button>
|
| 63 |
+
<button className="hero-next" type="button" aria-label="Next" onClick={(e) => { e.stopPropagation(); go(1); }}>
|
| 64 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
| 65 |
+
<path d="M9 18l6-6-6-6" />
|
| 66 |
+
</svg>
|
| 67 |
+
</button>
|
| 68 |
+
|
| 69 |
+
<div className={`hero-slide ${top10.length ? "active" : ""}`}>
|
| 70 |
+
<div className="hero-bg">
|
| 71 |
+
<img src={current.coverImage ? api.img(current.coverImage) : "https://images.unsplash.com/..."} alt={titleOf(current)} />
|
| 72 |
+
</div>
|
| 73 |
+
<div className="hero-gradient" />
|
| 74 |
+
<div className="hero-content">
|
| 75 |
+
<div className="container">
|
| 76 |
+
<div className="hero-layout">
|
| 77 |
+
<div className="hero-info">
|
| 78 |
+
<div className="hero-title-block">
|
| 79 |
+
<div className="hero-badges">
|
| 80 |
+
<span className="badge badge-dark">{current.format || "TV"}</span>
|
| 81 |
+
<span className="badge badge-score">★ {fmtScore(current.score)}</span>
|
| 82 |
+
<span className="badge badge-dark">{current.episodes ? `${current.episodes} eps` : "24 min"}</span>
|
| 83 |
+
</div>
|
| 84 |
+
<h1 className="hero-title">{titleOf(current)}</h1>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<p className="hero-desc">{current.description || "A new adventure is waiting. Dive into the latest episodes and discover your next obsession."}</p>
|
| 88 |
+
|
| 89 |
+
<div className="hero-genres">
|
| 90 |
+
{(current.genres || genres.slice(0, 4)).map((g) => (
|
| 91 |
+
<span key={g} className="hero-genre-tag">{g}</span>
|
| 92 |
+
))}
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
<div className="hero-actions">
|
| 96 |
+
<button className="btn btn-primary" type="button" onClick={() => navigate(`/anime/${current.id}`)}>
|
| 97 |
+
Watch Now
|
| 98 |
+
</button>
|
| 99 |
+
<button className="btn btn-ghost" type="button" onClick={() => navigate(`/search?q=${encodeURIComponent(titleOf(current))}`)}>
|
| 100 |
+
More Info
|
| 101 |
+
</button>
|
| 102 |
+
</div>
|
| 103 |
+
|
| 104 |
+
<div className="hero-stats">
|
| 105 |
+
<div className="hero-stat">
|
| 106 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" /></svg>
|
| 107 |
+
{current.score != null ? fmtScore(current.score) : "9.4"}
|
| 108 |
+
</div>
|
| 109 |
+
<div className="hero-stat">
|
| 110 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2Zm1 15h-2v-2h2Zm0-4h-2V7h2Z" /></svg>
|
| 111 |
+
{current.episodes ? `${current.episodes} episodes` : "Fresh"}
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
|
| 116 |
+
<div className="hero-cover-wrap">
|
| 117 |
+
<div className="hero-cover-glow" />
|
| 118 |
+
<img className="hero-cover" src={current.coverImage ? api.img(current.coverImage) : ""} alt={titleOf(current)} />
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
|
| 125 |
+
<div className="hero-indicators">
|
| 126 |
+
{top10.map((m, i) => (
|
| 127 |
+
<button
|
| 128 |
+
key={m.id}
|
| 129 |
+
type="button"
|
| 130 |
+
className={`hero-dot${i === safeSlide ? " active" : ""}`}
|
| 131 |
+
aria-label={`Go to slide ${i + 1}`}
|
| 132 |
+
onClick={() => jump(i)}
|
| 133 |
+
/>
|
| 134 |
+
))}
|
| 135 |
+
</div>
|
| 136 |
+
</section>
|
| 137 |
+
) : null}
|
| 138 |
+
|
| 139 |
+
<section className="content-section" style={{ paddingTop: 24 }}>
|
| 140 |
+
<div className="container">
|
| 141 |
+
<div className="genre-bar">
|
| 142 |
+
{genres.map((genre) => (
|
| 143 |
+
<button key={genre} className="genre-pill" type="button" onClick={() => navigate('/search')}>{genre}</button>
|
| 144 |
+
))}
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
</section>
|
| 148 |
+
|
| 149 |
+
<section className="content-section">
|
| 150 |
+
<div className="container home-grid">
|
| 151 |
+
<div className="home-sections">
|
| 152 |
+
<AnimeRow title="Trending Now" items={trending} isRanked={true} viewAllUrl="/search" />
|
| 153 |
+
<AnimeRow title="Most Popular" items={popular} viewAllUrl="/search" />
|
| 154 |
+
<AnimeRow title="Recently Added" items={recent} viewAllUrl="/search" />
|
| 155 |
+
<AnimeRow title="Movies" items={popular.slice(0, 12)} viewAllUrl="/movies" />
|
| 156 |
+
</div>
|
| 157 |
+
|
| 158 |
+
<aside className="sidebar-widget">
|
| 159 |
+
<div className="sidebar-tabs">
|
| 160 |
+
<button type="button" className="sidebar-tab active">Top Airing</button>
|
| 161 |
+
<button type="button" className="sidebar-tab">New</button>
|
| 162 |
+
</div>
|
| 163 |
+
<div>
|
| 164 |
+
{trending.slice(0, 5).map((item, idx) => (
|
| 165 |
+
<button key={item.id || idx} type="button" className="sidebar-item" onClick={() => navigate(`/anime/${item.id}`)}>
|
| 166 |
+
<span className="sidebar-rank">{idx + 1}</span>
|
| 167 |
+
<img className="sidebar-thumb" src={item.coverImage ? api.img(item.coverImage) : ""} alt={titleOf(item)} />
|
| 168 |
+
<div style={{ flex: 1, textAlign: "left" }}>
|
| 169 |
+
<div className="sidebar-title">{titleOf(item)}</div>
|
| 170 |
+
<div className="sidebar-meta">{item.format || "TV"}</div>
|
| 171 |
+
</div>
|
| 172 |
+
</button>
|
| 173 |
+
))}
|
| 174 |
+
</div>
|
| 175 |
+
</aside>
|
| 176 |
+
</div>
|
| 177 |
+
</section>
|
| 178 |
+
</>
|
| 179 |
+
);
|
| 180 |
+
}
|
frontend/src/pages/Landing.jsx
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from "react";
|
| 2 |
+
import { useNavigate } from "react-router-dom";
|
| 3 |
+
import { api } from "../api.js";
|
| 4 |
+
import HeroSection from "../components/HeroSection.jsx";
|
| 5 |
+
import LibraryMarquee from "../components/LibraryMarquee.jsx";
|
| 6 |
+
|
| 7 |
+
export default function Landing() {
|
| 8 |
+
const navigate = useNavigate();
|
| 9 |
+
const [marqueeItems, setMarqueeItems] = useState([]);
|
| 10 |
+
|
| 11 |
+
useEffect(() => {
|
| 12 |
+
let alive = true;
|
| 13 |
+
(async () => {
|
| 14 |
+
try {
|
| 15 |
+
const [t, p, r] = await Promise.all([
|
| 16 |
+
api.trending(1),
|
| 17 |
+
api.popular(1),
|
| 18 |
+
api.recent(1),
|
| 19 |
+
]);
|
| 20 |
+
if (!alive) return;
|
| 21 |
+
setMarqueeItems([...(t.results || []), ...(p.results || []), ...(r.results || [])]);
|
| 22 |
+
} catch (e) {
|
| 23 |
+
console.error(e);
|
| 24 |
+
}
|
| 25 |
+
})();
|
| 26 |
+
return () => { alive = false; };
|
| 27 |
+
}, []);
|
| 28 |
+
|
| 29 |
+
return (
|
| 30 |
+
<>
|
| 31 |
+
{/* HERO — the anicove logo through the stats */}
|
| 32 |
+
<HeroSection />
|
| 33 |
+
|
| 34 |
+
{/* LIBRARY THAT KEEPS MOVING */}
|
| 35 |
+
<LibraryMarquee items={marqueeItems} />
|
| 36 |
+
|
| 37 |
+
{/* FEATURE — EVERYTHING STAYS CONNECTED */}
|
| 38 |
+
<section className="feature-section">
|
| 39 |
+
<div className="container">
|
| 40 |
+
<div className="feature-eyebrow">EVERYTHING STAYS CONNECTED</div>
|
| 41 |
+
<h2 className="feature-heading">
|
| 42 |
+
One place for every <span className="accent">kind of story.</span>
|
| 43 |
+
</h2>
|
| 44 |
+
<p className="feature-desc">
|
| 45 |
+
Jump between a weekly anime episode, a manga chapter, a movie, or an opening theme — all without rebuilding your library each time.
|
| 46 |
+
</p>
|
| 47 |
+
|
| 48 |
+
<div className="feature-card">
|
| 49 |
+
<div className="feature-card-icon">
|
| 50 |
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
| 51 |
+
<path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
| 52 |
+
</svg>
|
| 53 |
+
</div>
|
| 54 |
+
<div className="feature-card-eyebrow">YOUR RULES, YOUR LISTS</div>
|
| 55 |
+
<h3 className="feature-card-title">Connected Tracking</h3>
|
| 56 |
+
<p className="feature-card-desc">
|
| 57 |
+
Link AniList, MAL, or both. anidoom records completed episodes using the tracking rules you choose.
|
| 58 |
+
</p>
|
| 59 |
+
|
| 60 |
+
<div className="feature-track-item">
|
| 61 |
+
<div className="feature-track-name">AniList</div>
|
| 62 |
+
<div className="feature-track-sub">Progress & status sync</div>
|
| 63 |
+
<div className="feature-track-badge">Synced</div>
|
| 64 |
+
</div>
|
| 65 |
+
<div className="feature-track-item">
|
| 66 |
+
<div className="feature-track-name">MyAnimeList</div>
|
| 67 |
+
<div className="feature-track-sub">Auto score & list sync</div>
|
| 68 |
+
<div className="feature-track-badge">Synced</div>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
| 72 |
+
</section>
|
| 73 |
+
|
| 74 |
+
{/* ENTER CTA */}
|
| 75 |
+
<section className="landing-enter">
|
| 76 |
+
<div className="landing-enter-glow" />
|
| 77 |
+
<div className="landing-enter-inner">
|
| 78 |
+
<div className="landing-enter-label">EVERYTHING. CONNECTED.</div>
|
| 79 |
+
<h2 className="landing-enter-title">Ready to dive in?</h2>
|
| 80 |
+
<p className="landing-enter-desc">
|
| 81 |
+
Anime, manga, movies, TV and music — with progress that follows you everywhere.
|
| 82 |
+
</p>
|
| 83 |
+
<button className="btn btn-primary" onClick={() => navigate("/home")}>
|
| 84 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
| 85 |
+
<path d="M8 5v14l11-7z" />
|
| 86 |
+
</svg>
|
| 87 |
+
Enter anicove
|
| 88 |
+
</button>
|
| 89 |
+
</div>
|
| 90 |
+
</section>
|
| 91 |
+
</>
|
| 92 |
+
);
|
| 93 |
+
}
|
frontend/src/pages/Manga.jsx
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useRef, useState } from "react";
|
| 2 |
+
import { api } from "../api.js";
|
| 3 |
+
import MangaCard from "../components/MangaCard.jsx";
|
| 4 |
+
|
| 5 |
+
export default function Manga() {
|
| 6 |
+
const [sections, setSections] = useState([]);
|
| 7 |
+
const [q, setQ] = useState("");
|
| 8 |
+
const [results, setResults] = useState([]);
|
| 9 |
+
const [loading, setLoading] = useState(true);
|
| 10 |
+
const [searching, setSearching] = useState(false);
|
| 11 |
+
const [error, setError] = useState(null);
|
| 12 |
+
const debounce = useRef(null);
|
| 13 |
+
|
| 14 |
+
useEffect(() => {
|
| 15 |
+
let alive = true;
|
| 16 |
+
(async () => {
|
| 17 |
+
try {
|
| 18 |
+
const data = await api.mangaHome();
|
| 19 |
+
if (!alive) return;
|
| 20 |
+
setSections(data || []);
|
| 21 |
+
} catch (e) {
|
| 22 |
+
if (alive) setError(e.message);
|
| 23 |
+
} finally {
|
| 24 |
+
if (alive) setLoading(false);
|
| 25 |
+
}
|
| 26 |
+
})();
|
| 27 |
+
return () => {
|
| 28 |
+
alive = false;
|
| 29 |
+
};
|
| 30 |
+
}, []);
|
| 31 |
+
|
| 32 |
+
useEffect(() => {
|
| 33 |
+
if (!q.trim()) {
|
| 34 |
+
setResults([]);
|
| 35 |
+
setSearching(false);
|
| 36 |
+
return undefined;
|
| 37 |
+
}
|
| 38 |
+
setSearching(true);
|
| 39 |
+
clearTimeout(debounce.current);
|
| 40 |
+
debounce.current = setTimeout(async () => {
|
| 41 |
+
try {
|
| 42 |
+
const data = await api.mangaSearch(q.trim());
|
| 43 |
+
setResults(data || []);
|
| 44 |
+
} catch (e) {
|
| 45 |
+
setError(e.message);
|
| 46 |
+
setResults([]);
|
| 47 |
+
} finally {
|
| 48 |
+
setSearching(false);
|
| 49 |
+
}
|
| 50 |
+
}, 350);
|
| 51 |
+
return () => clearTimeout(debounce.current);
|
| 52 |
+
}, [q]);
|
| 53 |
+
|
| 54 |
+
const showSearch = q.trim().length > 0;
|
| 55 |
+
|
| 56 |
+
return (
|
| 57 |
+
<div className="container" style={{ paddingTop: "calc(var(--header-h) + 30px)", paddingBottom: 80 }}>
|
| 58 |
+
{/* MANGA HERO BANNER */}
|
| 59 |
+
<div className="section-header" style={{ marginBottom: 12 }}>
|
| 60 |
+
<h1 className="section-title" style={{ fontSize: "1.8rem" }}>
|
| 61 |
+
Manga Vault
|
| 62 |
+
</h1>
|
| 63 |
+
</div>
|
| 64 |
+
<p style={{ color: "var(--text-secondary)", marginBottom: 28, fontSize: "0.95rem" }}>
|
| 65 |
+
Read thousands of manga, manhwa & manhua online with zero ads.
|
| 66 |
+
</p>
|
| 67 |
+
|
| 68 |
+
{/* SEARCH BAR */}
|
| 69 |
+
<div className="hero-search-wrap" style={{ margin: "0 0 36px 0", maxWidth: 640 }}>
|
| 70 |
+
<input
|
| 71 |
+
type="text"
|
| 72 |
+
className="hero-search-input"
|
| 73 |
+
value={q}
|
| 74 |
+
onChange={(e) => setQ(e.target.value)}
|
| 75 |
+
placeholder="Search manga, manhwa, authors..."
|
| 76 |
+
/>
|
| 77 |
+
<div className="hero-search-btn">
|
| 78 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" width="18" height="18">
|
| 79 |
+
<circle cx="11" cy="11" r="8" />
|
| 80 |
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
| 81 |
+
</svg>
|
| 82 |
+
</div>
|
| 83 |
+
</div>
|
| 84 |
+
|
| 85 |
+
{error && !showSearch ? (
|
| 86 |
+
<div className="error-box">
|
| 87 |
+
{error}
|
| 88 |
+
<div style={{ marginTop: 8, fontSize: "0.82rem" }}>
|
| 89 |
+
Start the MangaVault sidecar with <code>manga-vault/run.bat</code>.
|
| 90 |
+
</div>
|
| 91 |
+
</div>
|
| 92 |
+
) : null}
|
| 93 |
+
|
| 94 |
+
{showSearch ? (
|
| 95 |
+
searching ? (
|
| 96 |
+
<div className="loader">
|
| 97 |
+
<div className="spinner" />
|
| 98 |
+
<span>Searching manga...</span>
|
| 99 |
+
</div>
|
| 100 |
+
) : results.length ? (
|
| 101 |
+
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(160px, 1fr))", gap: "20px 16px" }}>
|
| 102 |
+
{results.map((m, i) => (
|
| 103 |
+
<MangaCard key={`${m.source}-${m.id}-${i}`} manga={m} />
|
| 104 |
+
))}
|
| 105 |
+
</div>
|
| 106 |
+
) : (
|
| 107 |
+
<div className="empty-msg">
|
| 108 |
+
<p>No manga found for "{q}".</p>
|
| 109 |
+
</div>
|
| 110 |
+
)
|
| 111 |
+
) : loading ? (
|
| 112 |
+
<div className="scroll-row" style={{ flexWrap: "wrap", gap: 20, overflow: "visible" }}>
|
| 113 |
+
{Array.from({ length: 12 }).map((_, i) => (
|
| 114 |
+
<div key={i} className="anime-card skel-card">
|
| 115 |
+
<div className="skel-poster skeleton" />
|
| 116 |
+
<div className="skel-line skeleton" />
|
| 117 |
+
<div className="skel-line skel-line-sm skeleton" />
|
| 118 |
+
</div>
|
| 119 |
+
))}
|
| 120 |
+
</div>
|
| 121 |
+
) : sections.length ? (
|
| 122 |
+
sections.map((section) => (
|
| 123 |
+
<section key={section.key} className="content-section" style={{ padding: "20px 0" }}>
|
| 124 |
+
<div className="section-header">
|
| 125 |
+
<div className="section-title">{section.title}</div>
|
| 126 |
+
<span className="section-link">{section.key.split("_")[0]} →</span>
|
| 127 |
+
</div>
|
| 128 |
+
<div className="scroll-row">
|
| 129 |
+
{section.items.map((m, i) => (
|
| 130 |
+
<MangaCard key={`${section.key}-${m.id}-${i}`} manga={m} />
|
| 131 |
+
))}
|
| 132 |
+
</div>
|
| 133 |
+
</section>
|
| 134 |
+
))
|
| 135 |
+
) : (
|
| 136 |
+
<div className="empty-msg">
|
| 137 |
+
<p>No manga catalog loaded. Check sidecar connection.</p>
|
| 138 |
+
</div>
|
| 139 |
+
)}
|
| 140 |
+
</div>
|
| 141 |
+
);
|
| 142 |
+
}
|
frontend/src/pages/MangaDetails.jsx
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useMemo, useState } from "react";
|
| 2 |
+
import { Link, useNavigate, useParams } from "react-router-dom";
|
| 3 |
+
import { api } from "../api.js";
|
| 4 |
+
import { lastChapterIndex, saveLastRead } from "../utils/mangaProgress.js";
|
| 5 |
+
|
| 6 |
+
export default function MangaDetails() {
|
| 7 |
+
const { source, id } = useParams();
|
| 8 |
+
const navigate = useNavigate();
|
| 9 |
+
const [manga, setManga] = useState(null);
|
| 10 |
+
const [error, setError] = useState(null);
|
| 11 |
+
const [showFullDesc, setShowFullDesc] = useState(false);
|
| 12 |
+
|
| 13 |
+
useEffect(() => {
|
| 14 |
+
let alive = true;
|
| 15 |
+
setManga(null);
|
| 16 |
+
setError(null);
|
| 17 |
+
(async () => {
|
| 18 |
+
try {
|
| 19 |
+
const data = await api.mangaDetails(source, id);
|
| 20 |
+
if (alive) setManga(data);
|
| 21 |
+
} catch (e) {
|
| 22 |
+
if (alive) setError(e.message);
|
| 23 |
+
}
|
| 24 |
+
})();
|
| 25 |
+
return () => {
|
| 26 |
+
alive = false;
|
| 27 |
+
};
|
| 28 |
+
}, [source, id]);
|
| 29 |
+
|
| 30 |
+
const chapters = manga?.chapters || [];
|
| 31 |
+
const lastIdx = useMemo(() => lastChapterIndex(source, id), [source, id, manga]);
|
| 32 |
+
const continueIdx = Number.isInteger(lastIdx) && lastIdx < chapters.length ? lastIdx : 0;
|
| 33 |
+
|
| 34 |
+
function openChapter(idx) {
|
| 35 |
+
const chapter = chapters[idx];
|
| 36 |
+
if (!chapter) return;
|
| 37 |
+
saveLastRead(source, id, idx);
|
| 38 |
+
navigate(
|
| 39 |
+
`/manga/read?source=${source}&id=${encodeURIComponent(id)}&path=${encodeURIComponent(chapter.imagePath)}`,
|
| 40 |
+
{ state: { mangaTitle: manga?.title } }
|
| 41 |
+
);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
if (error) {
|
| 45 |
+
return (
|
| 46 |
+
<div className="container" style={{ paddingTop: "calc(var(--header-h) + 40px)" }}>
|
| 47 |
+
<div className="error-box">{error}</div>
|
| 48 |
+
</div>
|
| 49 |
+
);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
if (!manga) {
|
| 53 |
+
return (
|
| 54 |
+
<div className="loader" style={{ minHeight: "80vh" }}>
|
| 55 |
+
<div className="spinner" />
|
| 56 |
+
<span>Loading manga details...</span>
|
| 57 |
+
</div>
|
| 58 |
+
);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
const cover = manga.cover ? api.mangaImg(manga.cover, manga.source) : null;
|
| 62 |
+
|
| 63 |
+
return (
|
| 64 |
+
<div className="info-page">
|
| 65 |
+
{/* HERO BANNER */}
|
| 66 |
+
<div className="info-hero">
|
| 67 |
+
{cover ? (
|
| 68 |
+
<div className="info-hero-bg" style={{ backgroundImage: `url(${cover})` }} />
|
| 69 |
+
) : null}
|
| 70 |
+
<div className="info-hero-grad" />
|
| 71 |
+
<div className="container info-hero-content">
|
| 72 |
+
<div className="info-hero-body">
|
| 73 |
+
<div className="info-poster">
|
| 74 |
+
<img src={cover} alt={manga.title} />
|
| 75 |
+
</div>
|
| 76 |
+
<div className="info-meta-block">
|
| 77 |
+
<div className="info-type-tag">
|
| 78 |
+
<span className="info-type-tag-dot" />
|
| 79 |
+
<span>MANGA</span>
|
| 80 |
+
<span>•</span>
|
| 81 |
+
<span>{manga.source}</span>
|
| 82 |
+
</div>
|
| 83 |
+
<h1 className="info-title">{manga.title}</h1>
|
| 84 |
+
<div className="info-badges">
|
| 85 |
+
<span className="badge badge-green">{manga.status || "Ongoing"}</span>
|
| 86 |
+
<span className="badge badge-accent">{chapters.length} CHAPTERS</span>
|
| 87 |
+
{manga.views ? <span className="badge badge-dark">{manga.views} Views</span> : null}
|
| 88 |
+
</div>
|
| 89 |
+
{manga.genres ? (
|
| 90 |
+
<div className="info-genres">
|
| 91 |
+
{manga.genres.map((g) => (
|
| 92 |
+
<span key={g} className="info-genre-pill">{g}</span>
|
| 93 |
+
))}
|
| 94 |
+
</div>
|
| 95 |
+
) : null}
|
| 96 |
+
<div className="info-actions">
|
| 97 |
+
<button className="btn info-btn-watch" onClick={() => openChapter(continueIdx)}>
|
| 98 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
| 99 |
+
<path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
| 100 |
+
</svg>
|
| 101 |
+
{lastIdx != null ? "Continue Reading" : "Start Reading"}
|
| 102 |
+
</button>
|
| 103 |
+
<Link to="/manga" className="btn info-btn-ghost">
|
| 104 |
+
← Back to Vault
|
| 105 |
+
</Link>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
{/* DETAILS & CHAPTER LIST */}
|
| 113 |
+
<div className="container info-body">
|
| 114 |
+
<div className="info-layout">
|
| 115 |
+
<div className="info-main-col">
|
| 116 |
+
<div className="info-section-label">Synopsis</div>
|
| 117 |
+
<p className={`info-synopsis ${!showFullDesc ? "clamped" : ""}`}>
|
| 118 |
+
{manga.description || "No synopsis available for this title."}
|
| 119 |
+
</p>
|
| 120 |
+
{manga.description && manga.description.length > 200 ? (
|
| 121 |
+
<button className="show-more-btn" onClick={() => setShowFullDesc(!showFullDesc)}>
|
| 122 |
+
{showFullDesc ? "Show Less" : "Show More"}
|
| 123 |
+
</button>
|
| 124 |
+
) : null}
|
| 125 |
+
|
| 126 |
+
<div className="info-section-label" style={{ marginTop: 36 }}>
|
| 127 |
+
Chapters ({chapters.length})
|
| 128 |
+
</div>
|
| 129 |
+
{chapters.length ? (
|
| 130 |
+
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(130px, 1fr))", gap: 10 }}>
|
| 131 |
+
{chapters.map((ch, i) => (
|
| 132 |
+
<button
|
| 133 |
+
key={`${ch.id}-${i}`}
|
| 134 |
+
className={`ep-btn ${i === continueIdx && lastIdx != null ? "active" : ""}`}
|
| 135 |
+
onClick={() => openChapter(i)}
|
| 136 |
+
style={{
|
| 137 |
+
padding: "10px 12px",
|
| 138 |
+
borderRadius: "var(--radius-md)",
|
| 139 |
+
background: i === continueIdx && lastIdx != null ? "rgba(124,58,237,0.18)" : "var(--bg-surface)",
|
| 140 |
+
border: "1px solid " + (i === continueIdx && lastIdx != null ? "rgba(124,58,237,0.4)" : "var(--border)"),
|
| 141 |
+
color: "var(--text-primary)",
|
| 142 |
+
textAlign: "center",
|
| 143 |
+
fontWeight: 600,
|
| 144 |
+
fontSize: "0.83rem",
|
| 145 |
+
cursor: "pointer",
|
| 146 |
+
transition: "var(--transition)"
|
| 147 |
+
}}
|
| 148 |
+
>
|
| 149 |
+
{ch.number != null ? `Ch. ${ch.number}` : ch.title || `Ch. ${i + 1}`}
|
| 150 |
+
</button>
|
| 151 |
+
))}
|
| 152 |
+
</div>
|
| 153 |
+
) : (
|
| 154 |
+
<div className="empty-msg">
|
| 155 |
+
<p>No chapters available.</p>
|
| 156 |
+
</div>
|
| 157 |
+
)}
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
<div className="info-side-col">
|
| 161 |
+
<div className="info-section-label">Information</div>
|
| 162 |
+
<div className="info-stats-grid">
|
| 163 |
+
<div className="info-stat-cell">
|
| 164 |
+
<div className="info-stat-label">Source</div>
|
| 165 |
+
<div className="info-stat-value">{manga.source}</div>
|
| 166 |
+
</div>
|
| 167 |
+
<div className="info-stat-cell">
|
| 168 |
+
<div className="info-stat-label">Status</div>
|
| 169 |
+
<div className="info-stat-value">{manga.status || "Ongoing"}</div>
|
| 170 |
+
</div>
|
| 171 |
+
<div className="info-stat-cell">
|
| 172 |
+
<div className="info-stat-label">Authors</div>
|
| 173 |
+
<div className="info-stat-value">{manga.authors || "Unknown"}</div>
|
| 174 |
+
</div>
|
| 175 |
+
<div className="info-stat-cell">
|
| 176 |
+
<div className="info-stat-label">Total Chapters</div>
|
| 177 |
+
<div className="info-stat-value">{chapters.length}</div>
|
| 178 |
+
</div>
|
| 179 |
+
</div>
|
| 180 |
+
</div>
|
| 181 |
+
</div>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
);
|
| 185 |
+
}
|
frontend/src/pages/MovieDetails.jsx
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from "react";
|
| 2 |
+
import { useNavigate, useParams } from "react-router-dom";
|
| 3 |
+
import { api } from "../api.js";
|
| 4 |
+
import { getProgress, CW_KEY } from "../utils/watchProgress.js";
|
| 5 |
+
|
| 6 |
+
export default function MovieDetails() {
|
| 7 |
+
const { slug } = useParams();
|
| 8 |
+
const navigate = useNavigate();
|
| 9 |
+
const [media, setMedia] = useState(null);
|
| 10 |
+
const [loading, setLoading] = useState(true);
|
| 11 |
+
const [error, setError] = useState(null);
|
| 12 |
+
const [season, setSeason] = useState(null);
|
| 13 |
+
const [showFull, setShowFull] = useState(false);
|
| 14 |
+
const [watchedEps, setWatchedEps] = useState(new Set());
|
| 15 |
+
|
| 16 |
+
useEffect(() => {
|
| 17 |
+
let alive = true;
|
| 18 |
+
setLoading(true);
|
| 19 |
+
setError(null);
|
| 20 |
+
(async () => {
|
| 21 |
+
try {
|
| 22 |
+
const m = await api.movieDetails(slug);
|
| 23 |
+
if (!alive) return;
|
| 24 |
+
setMedia(m);
|
| 25 |
+
setSeason(m.seasons?.length ? m.seasons[0] : null);
|
| 26 |
+
|
| 27 |
+
// Load watched episodes
|
| 28 |
+
const progress = getProgress(CW_KEY.movie(slug));
|
| 29 |
+
if (progress) {
|
| 30 |
+
const watched = new Set();
|
| 31 |
+
if (progress.se && progress.ep) {
|
| 32 |
+
watched.add(`${progress.se}-${progress.ep}`);
|
| 33 |
+
}
|
| 34 |
+
setWatchedEps(watched);
|
| 35 |
+
}
|
| 36 |
+
} catch (e) {
|
| 37 |
+
if (alive) setError(e.message);
|
| 38 |
+
} finally {
|
| 39 |
+
if (alive) setLoading(false);
|
| 40 |
+
}
|
| 41 |
+
})();
|
| 42 |
+
return () => { alive = false; };
|
| 43 |
+
}, [slug]);
|
| 44 |
+
|
| 45 |
+
if (loading) {
|
| 46 |
+
return (
|
| 47 |
+
<div className="loader" style={{ minHeight: "80vh" }}>
|
| 48 |
+
<div className="spinner" />
|
| 49 |
+
<span>Loading title...</span>
|
| 50 |
+
</div>
|
| 51 |
+
);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
if (error || !media) {
|
| 55 |
+
const isMissing = error?.status === 404 || /not found|coming soon/i.test(error || "");
|
| 56 |
+
return (
|
| 57 |
+
<div className="empty" style={{ minHeight: "50vh", paddingTop: 80 }}>
|
| 58 |
+
<div className="big">🎬</div>
|
| 59 |
+
{isMissing ? (
|
| 60 |
+
<>
|
| 61 |
+
<h2 style={{ margin: "0 0 8px" }}>Not available yet</h2>
|
| 62 |
+
<p style={{ maxWidth: 420, margin: "0 auto" }}>
|
| 63 |
+
This title isn't watchable on MovieBox right now — it may be
|
| 64 |
+
coming soon or has been removed. Try searching for something else.
|
| 65 |
+
</p>
|
| 66 |
+
<button className="btn info-btn-ghost" onClick={() => navigate("/movies")}>
|
| 67 |
+
Browse Movies
|
| 68 |
+
</button>
|
| 69 |
+
</>
|
| 70 |
+
) : (
|
| 71 |
+
<p>{error || "Title not found."}</p>
|
| 72 |
+
)}
|
| 73 |
+
</div>
|
| 74 |
+
);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
const isSeries = media.type === 2 || media.seasons?.length > 0;
|
| 78 |
+
const title = media.title;
|
| 79 |
+
const genres = media.genres || [];
|
| 80 |
+
const cast = media.cast || [];
|
| 81 |
+
const rating = media.rating ? Number(media.rating).toFixed(1) : null;
|
| 82 |
+
|
| 83 |
+
function handleWatch(se = 0, ep = 0) {
|
| 84 |
+
if (isSeries) {
|
| 85 |
+
navigate(`/movies/${media.slug}/watch?se=${se}&ep=${ep}`);
|
| 86 |
+
} else {
|
| 87 |
+
navigate(`/movies/${media.slug}/watch`);
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
return (
|
| 92 |
+
<div className="info-page">
|
| 93 |
+
<div className="info-hero">
|
| 94 |
+
{media.banner ? (
|
| 95 |
+
<div className="info-hero-bg" style={{ backgroundImage: `url(${api.img(media.banner)})` }} />
|
| 96 |
+
) : null}
|
| 97 |
+
<div className="info-hero-grad" />
|
| 98 |
+
<div className="container info-hero-content">
|
| 99 |
+
<div className="info-hero-body">
|
| 100 |
+
<div className="info-poster">
|
| 101 |
+
<img src={api.img(media.cover)} alt={title} />
|
| 102 |
+
</div>
|
| 103 |
+
<div className="info-meta-block">
|
| 104 |
+
<div className="info-type-tag">
|
| 105 |
+
<span className="info-type-tag-dot" />
|
| 106 |
+
<span>{isSeries ? "TV Series" : "Movie"}</span>
|
| 107 |
+
<span>•</span>
|
| 108 |
+
<span>{media.year || media.country || "—"}</span>
|
| 109 |
+
{media.corner ? <span>•</span> : null}
|
| 110 |
+
{media.corner ? <span>{media.corner}</span> : null}
|
| 111 |
+
</div>
|
| 112 |
+
<h1 className="info-title">{title}</h1>
|
| 113 |
+
<div className="info-badges">
|
| 114 |
+
{rating ? <span className="badge badge-score">★ {rating}</span> : null}
|
| 115 |
+
<span className="badge badge-green">{isSeries ? "Series" : "Feature"}</span>
|
| 116 |
+
{media.duration ? <span className="badge badge-dark">{media.duration} min</span> : null}
|
| 117 |
+
{media.country ? <span className="badge badge-dark">{media.country}</span> : null}
|
| 118 |
+
</div>
|
| 119 |
+
{genres.length ? (
|
| 120 |
+
<div className="info-genres">
|
| 121 |
+
{genres.map((g) => (
|
| 122 |
+
<span key={g} className="info-genre-pill">{g}</span>
|
| 123 |
+
))}
|
| 124 |
+
</div>
|
| 125 |
+
) : null}
|
| 126 |
+
<div className="info-actions">
|
| 127 |
+
{media.hasResource ? (
|
| 128 |
+
<button
|
| 129 |
+
className="btn info-btn-watch"
|
| 130 |
+
onClick={() => handleWatch(season?.se ?? 0, isSeries ? 1 : 0)}
|
| 131 |
+
>
|
| 132 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
| 133 |
+
<path d="M8 5v14l11-7z" />
|
| 134 |
+
</svg>
|
| 135 |
+
{isSeries ? `Watch S${season?.se ?? 1}E1` : "Watch Now"}
|
| 136 |
+
</button>
|
| 137 |
+
) : (
|
| 138 |
+
<span className="badge badge-dark" style={{ fontSize: "0.85rem", padding: "10px 16px" }}>
|
| 139 |
+
Stream unavailable{media.note ? ` — ${media.note}` : ""}
|
| 140 |
+
</span>
|
| 141 |
+
)}
|
| 142 |
+
{media.trailer ? (
|
| 143 |
+
<a
|
| 144 |
+
className="btn info-btn-ghost"
|
| 145 |
+
href={media.trailer}
|
| 146 |
+
target="_blank"
|
| 147 |
+
rel="noreferrer"
|
| 148 |
+
style={{ textDecoration: "none" }}
|
| 149 |
+
>
|
| 150 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
| 151 |
+
<path d="M8 5v14l11-7z" />
|
| 152 |
+
</svg>
|
| 153 |
+
Trailer
|
| 154 |
+
</a>
|
| 155 |
+
) : null}
|
| 156 |
+
<button className="btn info-btn-ghost" onClick={() => {
|
| 157 |
+
navigator.clipboard.writeText(window.location.href);
|
| 158 |
+
}} title="Copy link">
|
| 159 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
| 160 |
+
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
| 161 |
+
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
| 162 |
+
</svg>
|
| 163 |
+
Share
|
| 164 |
+
</button>
|
| 165 |
+
</div>
|
| 166 |
+
</div>
|
| 167 |
+
</div>
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
|
| 171 |
+
<div className="container info-body">
|
| 172 |
+
<div className="info-layout">
|
| 173 |
+
<div className="info-main-col">
|
| 174 |
+
<div className="info-section-label">Synopsis</div>
|
| 175 |
+
<p className={`info-synopsis ${!showFull ? "clamped" : ""}`}>
|
| 176 |
+
{media.description || "No description available for this title."}
|
| 177 |
+
</p>
|
| 178 |
+
{media.description && media.description.length > 200 ? (
|
| 179 |
+
<button className="show-more-btn" onClick={() => setShowFull(!showFull)}>
|
| 180 |
+
{showFull ? "Show Less" : "Show More"}
|
| 181 |
+
</button>
|
| 182 |
+
) : null}
|
| 183 |
+
|
| 184 |
+
{cast.length ? (
|
| 185 |
+
<>
|
| 186 |
+
<div className="info-section-label" style={{ marginTop: 32 }}>Cast</div>
|
| 187 |
+
<div className="cast-row">
|
| 188 |
+
{cast.map((person, i) => (
|
| 189 |
+
<div key={`${person.name}-${i}`} className="cast-chip" title={person.character}>
|
| 190 |
+
{person.avatar ? (
|
| 191 |
+
<img src={api.img(person.avatar)} alt={person.name} loading="lazy" />
|
| 192 |
+
) : (
|
| 193 |
+
<div className="cast-avatar-fallback">{(person.name || "?").charAt(0)}</div>
|
| 194 |
+
)}
|
| 195 |
+
<div className="cast-chip-name">{person.name}</div>
|
| 196 |
+
<div className="cast-chip-role">{person.character || "—"}</div>
|
| 197 |
+
</div>
|
| 198 |
+
))}
|
| 199 |
+
</div>
|
| 200 |
+
</>
|
| 201 |
+
) : null}
|
| 202 |
+
</div>
|
| 203 |
+
|
| 204 |
+
<div className="info-side-col">
|
| 205 |
+
<div className="info-section-label">Information</div>
|
| 206 |
+
<div className="info-stats-grid">
|
| 207 |
+
<div className="info-stat-cell">
|
| 208 |
+
<div className="info-stat-label">Type</div>
|
| 209 |
+
<div className="info-stat-value">{isSeries ? "TV Series" : "Movie"}</div>
|
| 210 |
+
</div>
|
| 211 |
+
<div className="info-stat-cell">
|
| 212 |
+
<div className="info-stat-label">Released</div>
|
| 213 |
+
<div className="info-stat-value">{media.releaseDate || media.year || "—"}</div>
|
| 214 |
+
</div>
|
| 215 |
+
<div className="info-stat-cell">
|
| 216 |
+
<div className="info-stat-label">Rating</div>
|
| 217 |
+
<div className="info-stat-value">{rating ? `★ ${rating}` : "—"}</div>
|
| 218 |
+
</div>
|
| 219 |
+
<div className="info-stat-cell">
|
| 220 |
+
<div className="info-stat-label">Duration</div>
|
| 221 |
+
<div className="info-stat-value">{media.duration ? `${media.duration} min` : "—"}</div>
|
| 222 |
+
</div>
|
| 223 |
+
<div className="info-stat-cell">
|
| 224 |
+
<div className="info-stat-label">Audio</div>
|
| 225 |
+
<div className="info-stat-value">
|
| 226 |
+
{media.audioTracks?.length
|
| 227 |
+
? media.audioTracks.filter((t) => t?.original).map((t) => t.lanName).join(", ")
|
| 228 |
+
: "Original"}
|
| 229 |
+
</div>
|
| 230 |
+
</div>
|
| 231 |
+
<div className="info-stat-cell">
|
| 232 |
+
<div className="info-stat-label">IMDb votes</div>
|
| 233 |
+
<div className="info-stat-value">
|
| 234 |
+
{media.imdbCount ? media.imdbCount.toLocaleString?.() || media.imdbCount : "—"}
|
| 235 |
+
</div>
|
| 236 |
+
</div>
|
| 237 |
+
</div>
|
| 238 |
+
</div>
|
| 239 |
+
</div>
|
| 240 |
+
|
| 241 |
+
{/* Season + episode picker for series */}
|
| 242 |
+
{isSeries && media.seasons?.length ? (
|
| 243 |
+
<div style={{ marginTop: 40 }}>
|
| 244 |
+
<div className="info-section-label">Episodes</div>
|
| 245 |
+
{media.seasons.length > 1 ? (
|
| 246 |
+
<div className="movie-tabs" style={{ marginBottom: 18 }}>
|
| 247 |
+
{media.seasons.map((s) => (
|
| 248 |
+
<button
|
| 249 |
+
key={s.se}
|
| 250 |
+
type="button"
|
| 251 |
+
className={season?.se === s.se ? "active" : ""}
|
| 252 |
+
onClick={() => setSeason(s)}
|
| 253 |
+
>
|
| 254 |
+
Season {s.se}
|
| 255 |
+
</button>
|
| 256 |
+
))}
|
| 257 |
+
</div>
|
| 258 |
+
) : null}
|
| 259 |
+
<div className="ep-grid">
|
| 260 |
+
{Array.from({ length: season?.maxEp || 0 }).map((_, i) => {
|
| 261 |
+
const ep = i + 1;
|
| 262 |
+
const isWatched = watchedEps.has(`${season.se}-${ep}`);
|
| 263 |
+
return (
|
| 264 |
+
<button
|
| 265 |
+
key={ep}
|
| 266 |
+
className={`ep-grid-btn ${isWatched ? "watched" : ""}`}
|
| 267 |
+
onClick={() => handleWatch(season.se, ep)}
|
| 268 |
+
>
|
| 269 |
+
<span className="ep-grid-num">{ep}</span>
|
| 270 |
+
{isWatched && <span className="ep-grid-watched">✓</span>}
|
| 271 |
+
<span className="ep-grid-play">
|
| 272 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
| 273 |
+
<path d="M8 5v14l11-7z" />
|
| 274 |
+
</svg>
|
| 275 |
+
</span>
|
| 276 |
+
</button>
|
| 277 |
+
);
|
| 278 |
+
})}
|
| 279 |
+
</div>
|
| 280 |
+
</div>
|
| 281 |
+
) : null}
|
| 282 |
+
</div>
|
| 283 |
+
</div>
|
| 284 |
+
);
|
| 285 |
+
}
|
frontend/src/pages/MovieWatch.jsx
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
| 2 |
+
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
| 3 |
+
import { api, proxiedStreamUrl, streamHeaders } from "../api.js";
|
| 4 |
+
import HlsPlayer from "../components/HlsPlayer.jsx";
|
| 5 |
+
import { CW_KEY, getProgress, removeProgress, saveProgress } from "../utils/watchProgress.js";
|
| 6 |
+
|
| 7 |
+
/** Fetch an .srt via the HLS worker and return a WebVTT blob URL (or null). */
|
| 8 |
+
async function srtToVttBlob(url, ref) {
|
| 9 |
+
try {
|
| 10 |
+
const res = await fetch(proxiedStreamUrl(url, ref), { headers: streamHeaders() });
|
| 11 |
+
if (!res.ok) return null;
|
| 12 |
+
const srt = await res.text();
|
| 13 |
+
// SRT timestamps "00:00:01,000" → WebVTT "00:00:01.000"
|
| 14 |
+
const vtt = "WEBVTT\n\n" + srt.replace(/(\d{2}:\d{2}:\d{2}),(\d{3})/g, "$1.$2");
|
| 15 |
+
return URL.createObjectURL(new Blob([vtt], { type: "text/vtt" }));
|
| 16 |
+
} catch {
|
| 17 |
+
return null;
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export default function MovieWatch() {
|
| 22 |
+
const { slug } = useParams();
|
| 23 |
+
const [searchParams] = useSearchParams();
|
| 24 |
+
const se = Number(searchParams.get("se") || 0);
|
| 25 |
+
const ep = Number(searchParams.get("ep") || 0);
|
| 26 |
+
const navigate = useNavigate();
|
| 27 |
+
|
| 28 |
+
const [data, setData] = useState(null);
|
| 29 |
+
const [media, setMedia] = useState(null);
|
| 30 |
+
const [quality, setQuality] = useState(0);
|
| 31 |
+
const [captions, setCaptions] = useState([]);
|
| 32 |
+
const [loading, setLoading] = useState(true);
|
| 33 |
+
const [error, setError] = useState(null);
|
| 34 |
+
const [selectedSeason, setSelectedSeason] = useState(se || 1);
|
| 35 |
+
|
| 36 |
+
useEffect(() => {
|
| 37 |
+
let alive = true;
|
| 38 |
+
setLoading(true);
|
| 39 |
+
setError(null);
|
| 40 |
+
setCaptions([]);
|
| 41 |
+
setQuality(0);
|
| 42 |
+
(async () => {
|
| 43 |
+
try {
|
| 44 |
+
const [s, m] = await Promise.all([
|
| 45 |
+
api.movieStream(slug, se, ep),
|
| 46 |
+
api.movieDetails(slug).catch(() => null),
|
| 47 |
+
]);
|
| 48 |
+
if (!alive) return;
|
| 49 |
+
setData(s);
|
| 50 |
+
setMedia(m);
|
| 51 |
+
// Convert SRT captions to WebVTT blobs (through the HLS worker for CORS).
|
| 52 |
+
const ref = s.streams?.[0]?.referer || "";
|
| 53 |
+
const tracks = await Promise.all(
|
| 54 |
+
(s.captions || []).map(async (c) => ({
|
| 55 |
+
...c,
|
| 56 |
+
vtt: await srtToVttBlob(c.url, ref),
|
| 57 |
+
}))
|
| 58 |
+
);
|
| 59 |
+
if (alive) setCaptions(tracks.filter((t) => t.vtt));
|
| 60 |
+
} catch (e) {
|
| 61 |
+
if (alive) setError(e.message);
|
| 62 |
+
} finally {
|
| 63 |
+
if (alive) setLoading(false);
|
| 64 |
+
}
|
| 65 |
+
})();
|
| 66 |
+
return () => { alive = false; };
|
| 67 |
+
}, [slug, se, ep]);
|
| 68 |
+
|
| 69 |
+
const streams = useMemo(() => data?.streams || [], [data]);
|
| 70 |
+
const selected = streams[quality] || streams[0];
|
| 71 |
+
|
| 72 |
+
const isSeries = media?.type === 2 || media?.seasons?.length > 0;
|
| 73 |
+
const currentSeasonData = media?.seasons?.find((s) => s.se === selectedSeason);
|
| 74 |
+
const maxEp = currentSeasonData?.maxEp;
|
| 75 |
+
const totalSeasons = media?.seasons?.length || 1;
|
| 76 |
+
const seasons = media?.seasons || [];
|
| 77 |
+
|
| 78 |
+
function goTo(nse, nep) {
|
| 79 |
+
const params = new URLSearchParams();
|
| 80 |
+
if (nse > 0) params.set("se", nse);
|
| 81 |
+
if (nep > 0) params.set("ep", nep);
|
| 82 |
+
navigate(`/movies/${slug}/watch${params.toString() ? `?${params}` : ""}`);
|
| 83 |
+
setSelectedSeason(nse);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
function nextEp() {
|
| 87 |
+
if (!isSeries) return;
|
| 88 |
+
if (ep < (maxEp || 1)) goTo(selectedSeason, ep + 1);
|
| 89 |
+
else if (selectedSeason < totalSeasons) goTo(selectedSeason + 1, 1);
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
function prevEp() {
|
| 93 |
+
if (!isSeries) return;
|
| 94 |
+
if (ep > 1) goTo(selectedSeason, ep - 1);
|
| 95 |
+
else if (selectedSeason > 1) goTo(selectedSeason - 1, media?.seasons?.find((s) => s.se === selectedSeason - 1)?.maxEp || 1);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
const title = media?.title || "Title";
|
| 99 |
+
const watchLabel = isSeries ? `S${se || 1} E${ep || 1}` : "Movie";
|
| 100 |
+
|
| 101 |
+
const progressKey = CW_KEY.movie(slug);
|
| 102 |
+
|
| 103 |
+
// Resume position for this episode, from the Continue Watching store.
|
| 104 |
+
const startAt = useMemo(() => {
|
| 105 |
+
const p = getProgress(progressKey);
|
| 106 |
+
return p && p.se === se && p.ep === ep ? p.position || 0 : 0;
|
| 107 |
+
}, [progressKey, se, ep]);
|
| 108 |
+
|
| 109 |
+
// Record playback progress (throttled by the player). Finished titles drop
|
| 110 |
+
// out of Continue Watching automatically.
|
| 111 |
+
const handleProgress = useCallback(
|
| 112 |
+
({ position, duration, ended }) => {
|
| 113 |
+
if (ended || (duration > 0 && position / duration > 0.92)) {
|
| 114 |
+
removeProgress(progressKey);
|
| 115 |
+
return;
|
| 116 |
+
}
|
| 117 |
+
saveProgress({
|
| 118 |
+
key: progressKey,
|
| 119 |
+
type: "movie",
|
| 120 |
+
mediaId: slug,
|
| 121 |
+
title,
|
| 122 |
+
image: media?.cover || "",
|
| 123 |
+
href: se > 0 || ep > 0 ? `/movies/${slug}/watch?se=${se}&ep=${ep}` : `/movies/${slug}/watch`,
|
| 124 |
+
label: watchLabel,
|
| 125 |
+
category: "",
|
| 126 |
+
se,
|
| 127 |
+
ep,
|
| 128 |
+
position: Math.floor(position),
|
| 129 |
+
duration: Math.floor(duration),
|
| 130 |
+
updatedAt: Date.now(),
|
| 131 |
+
});
|
| 132 |
+
},
|
| 133 |
+
[progressKey, slug, se, ep, title, watchLabel, media]
|
| 134 |
+
);
|
| 135 |
+
|
| 136 |
+
return (
|
| 137 |
+
<div className="watch-page">
|
| 138 |
+
<div className="watch-container">
|
| 139 |
+
<div className="watch-layout">
|
| 140 |
+
<div className="watch-main">
|
| 141 |
+
<div className="player-wrap">
|
| 142 |
+
{loading ? (
|
| 143 |
+
<div className="player-loading">
|
| 144 |
+
<div className="spinner" />
|
| 145 |
+
<span>Loading Player...</span>
|
| 146 |
+
</div>
|
| 147 |
+
) : error ? (
|
| 148 |
+
<div className="player-error">
|
| 149 |
+
<div className="big">🎬</div>
|
| 150 |
+
<p>{error}</p>
|
| 151 |
+
</div>
|
| 152 |
+
) : selected ? (
|
| 153 |
+
<HlsPlayer
|
| 154 |
+
key={`${slug}-${se}-${ep}`}
|
| 155 |
+
sources={{
|
| 156 |
+
streams: [selected],
|
| 157 |
+
subtitles: captions.map((c) => ({
|
| 158 |
+
file: c.vtt,
|
| 159 |
+
label: c.label || c.lang || "English",
|
| 160 |
+
kind: "captions",
|
| 161 |
+
srcLang: c.lang || "en",
|
| 162 |
+
})),
|
| 163 |
+
}}
|
| 164 |
+
onProgress={handleProgress}
|
| 165 |
+
startAt={startAt}
|
| 166 |
+
/>
|
| 167 |
+
) : (
|
| 168 |
+
<div className="player-error">
|
| 169 |
+
<div className="big">📺</div>
|
| 170 |
+
<p>{data?.note || "No stream available for this title yet."}</p>
|
| 171 |
+
</div>
|
| 172 |
+
)}
|
| 173 |
+
</div>
|
| 174 |
+
|
| 175 |
+
{streams.length > 1 ? (
|
| 176 |
+
<div className="movie-quality-bar">
|
| 177 |
+
<span className="movie-quality-label">Quality</span>
|
| 178 |
+
<div className="movie-quality-options">
|
| 179 |
+
{streams.map((s, i) => (
|
| 180 |
+
<button
|
| 181 |
+
key={`${s.resolution}-${i}`}
|
| 182 |
+
className={`movie-quality-btn ${quality === i ? "active" : ""}`}
|
| 183 |
+
onClick={() => setQuality(i)}
|
| 184 |
+
>
|
| 185 |
+
{s.resolution || s.format || "HD"}
|
| 186 |
+
</button>
|
| 187 |
+
))}
|
| 188 |
+
</div>
|
| 189 |
+
</div>
|
| 190 |
+
) : null}
|
| 191 |
+
|
| 192 |
+
<div className="ep-label">{watchLabel}</div>
|
| 193 |
+
<h1 className="ep-title">{title}</h1>
|
| 194 |
+
|
| 195 |
+
<div className="watch-anime-row">
|
| 196 |
+
{media?.cover ? (
|
| 197 |
+
<div className="watch-anime-thumb">
|
| 198 |
+
<img src={api.img(media.cover)} alt={title} />
|
| 199 |
+
</div>
|
| 200 |
+
) : null}
|
| 201 |
+
<div>
|
| 202 |
+
<div className="watch-anime-name">{title}</div>
|
| 203 |
+
<div className="watch-anime-sub-text">
|
| 204 |
+
{isSeries ? "TV Series" : "Movie"} • {media?.year || media?.country || "—"}
|
| 205 |
+
</div>
|
| 206 |
+
</div>
|
| 207 |
+
|
| 208 |
+
<div className="watch-audio-btns">
|
| 209 |
+
{isSeries ? (
|
| 210 |
+
<>
|
| 211 |
+
<button className="audio-btn" onClick={prevEp} disabled={se <= 1 && ep <= 1}>
|
| 212 |
+
← Prev
|
| 213 |
+
</button>
|
| 214 |
+
<button className="audio-btn" onClick={nextEp}>
|
| 215 |
+
Next →
|
| 216 |
+
</button>
|
| 217 |
+
</>
|
| 218 |
+
) : null}
|
| 219 |
+
{selected?.downloadUrl ? (
|
| 220 |
+
<a
|
| 221 |
+
className="server-btn download-btn"
|
| 222 |
+
href={selected.downloadUrl}
|
| 223 |
+
download
|
| 224 |
+
title={`Download ${selected.resolution || selected.format || "video"}`}
|
| 225 |
+
>
|
| 226 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
| 227 |
+
<path d="M12 3v10.17l-3.59-3.58L7 11l5 5 5-5-1.41-1.41L13 13.17V3h-1zM5 19h14v2H5z" />
|
| 228 |
+
</svg>
|
| 229 |
+
Download
|
| 230 |
+
</a>
|
| 231 |
+
) : null}
|
| 232 |
+
{media?.trailer ? (
|
| 233 |
+
<a className="server-btn" href={media.trailer} target="_blank" rel="noreferrer">
|
| 234 |
+
Trailer
|
| 235 |
+
</a>
|
| 236 |
+
) : null}
|
| 237 |
+
<button className="server-btn" onClick={() => navigate(`/movies/${slug}`)}>
|
| 238 |
+
Details
|
| 239 |
+
</button>
|
| 240 |
+
</div>
|
| 241 |
+
</div>
|
| 242 |
+
|
| 243 |
+
<div className="watch-info-card">
|
| 244 |
+
<div className="watch-info-title">{title}</div>
|
| 245 |
+
<div className="watch-info-meta">
|
| 246 |
+
{media?.rating ? <span className="watch-meta-score">★ {Number(media.rating).toFixed(1)}</span> : null}
|
| 247 |
+
<span className="watch-meta-dot" />
|
| 248 |
+
<span className="watch-meta-item">{isSeries ? "TV Series" : "Movie"}</span>
|
| 249 |
+
<span className="watch-meta-dot" />
|
| 250 |
+
<span className="watch-meta-item">{media?.year || "—"}</span>
|
| 251 |
+
{media?.duration ? (
|
| 252 |
+
<>
|
| 253 |
+
<span className="watch-meta-dot" />
|
| 254 |
+
<span className="watch-meta-item">{media.duration}m</span>
|
| 255 |
+
</>
|
| 256 |
+
) : null}
|
| 257 |
+
</div>
|
| 258 |
+
{media?.genres?.length ? (
|
| 259 |
+
<div className="watch-genres">
|
| 260 |
+
{media.genres.map((g) => (
|
| 261 |
+
<span key={g} className="watch-genre-pill">{g}</span>
|
| 262 |
+
))}
|
| 263 |
+
</div>
|
| 264 |
+
) : null}
|
| 265 |
+
<p className="watch-synopsis">{media?.description || "No description available."}</p>
|
| 266 |
+
</div>
|
| 267 |
+
</div>
|
| 268 |
+
|
| 269 |
+
<div className="watch-sidebar">
|
| 270 |
+
<div className="sidebar-head">
|
| 271 |
+
<div className="sidebar-upnext">{isSeries ? "Episodes" : "Now Playing"}</div>
|
| 272 |
+
<div className="sidebar-subtitle">
|
| 273 |
+
{isSeries ? `Season ${selectedSeason || 1}` : media?.title || " "}
|
| 274 |
+
</div>
|
| 275 |
+
</div>
|
| 276 |
+
|
| 277 |
+
{isSeries && totalSeasons > 1 ? (
|
| 278 |
+
<div className="season-selector">
|
| 279 |
+
<span className="season-label">Season</span>
|
| 280 |
+
<div className="season-options">
|
| 281 |
+
{seasons.map((s) => (
|
| 282 |
+
<button
|
| 283 |
+
key={s.se}
|
| 284 |
+
className={`season-btn ${selectedSeason === s.se ? "active" : ""}`}
|
| 285 |
+
onClick={() => goTo(s.se, 1)}
|
| 286 |
+
>
|
| 287 |
+
S{s.se}
|
| 288 |
+
</button>
|
| 289 |
+
))}
|
| 290 |
+
</div>
|
| 291 |
+
</div>
|
| 292 |
+
) : null}
|
| 293 |
+
|
| 294 |
+
{isSeries ? (
|
| 295 |
+
<div className="ep-list">
|
| 296 |
+
{Array.from({ length: maxEp || 0 }).map((_, i) => {
|
| 297 |
+
const n = i + 1;
|
| 298 |
+
const active = n === ep;
|
| 299 |
+
const progress = getProgress(CW_KEY.movie(slug));
|
| 300 |
+
const isWatched = progress && progress.se === selectedSeason && progress.ep === n && (progress.position / progress.duration) > 0.92;
|
| 301 |
+
return (
|
| 302 |
+
<div key={n} className={`ep-item ${active ? "active" : ""} ${isWatched ? "watched" : ""}`} onClick={() => goTo(selectedSeason, n)}>
|
| 303 |
+
<div className="ep-thumb">
|
| 304 |
+
{media?.cover ? <img src={api.img(media.cover)} alt="" /> : null}
|
| 305 |
+
<div className="ep-thumb-label">EP {n}</div>
|
| 306 |
+
{isWatched && <div className="ep-watched-badge">✓</div>}
|
| 307 |
+
</div>
|
| 308 |
+
<div className="ep-info">
|
| 309 |
+
<div className="ep-info-title">Episode {n}</div>
|
| 310 |
+
<div className="ep-info-sub">{title}</div>
|
| 311 |
+
</div>
|
| 312 |
+
</div>
|
| 313 |
+
);
|
| 314 |
+
})}
|
| 315 |
+
</div>
|
| 316 |
+
) : (
|
| 317 |
+
<div className="empty" style={{ padding: "24px 8px" }}>
|
| 318 |
+
<p>Feature film — sit back and enjoy.</p>
|
| 319 |
+
</div>
|
| 320 |
+
)}
|
| 321 |
+
</div>
|
| 322 |
+
</div>
|
| 323 |
+
</div>
|
| 324 |
+
</div>
|
| 325 |
+
);
|
| 326 |
+
}
|
frontend/src/pages/Movies.jsx
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useRef, useState } from "react";
|
| 2 |
+
import { api } from "../api.js";
|
| 3 |
+
import MovieCard from "../components/MovieCard.jsx";
|
| 4 |
+
import MovieRow from "../components/MovieRow.jsx";
|
| 5 |
+
|
| 6 |
+
const TABS = [
|
| 7 |
+
{ key: "home", label: "Home" },
|
| 8 |
+
{ key: "movie", label: "Movies" },
|
| 9 |
+
{ key: "tv", label: "TV Series" },
|
| 10 |
+
{ key: "animation", label: "Animation" },
|
| 11 |
+
];
|
| 12 |
+
|
| 13 |
+
const SORTS = ["RECOMMEND", "HOT", "NEW", "RATING", "POPULAR", "LATEST"];
|
| 14 |
+
|
| 15 |
+
const GRID_STYLE = {
|
| 16 |
+
display: "grid",
|
| 17 |
+
gridTemplateColumns: "repeat(auto-fill, minmax(160px, 1fr))",
|
| 18 |
+
gap: "20px 16px",
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
export default function Movies() {
|
| 22 |
+
const [tab, setTab] = useState("home");
|
| 23 |
+
const [sections, setSections] = useState([]);
|
| 24 |
+
const [catalog, setCatalog] = useState([]);
|
| 25 |
+
const [page, setPage] = useState(1);
|
| 26 |
+
const [hasMore, setHasMore] = useState(false);
|
| 27 |
+
const [sort, setSort] = useState("RECOMMEND");
|
| 28 |
+
const [q, setQ] = useState("");
|
| 29 |
+
const [results, setResults] = useState([]);
|
| 30 |
+
const [searching, setSearching] = useState(false);
|
| 31 |
+
const [suggestions, setSuggestions] = useState([]);
|
| 32 |
+
const [loading, setLoading] = useState(true);
|
| 33 |
+
const [error, setError] = useState(null);
|
| 34 |
+
const debounce = useRef(null);
|
| 35 |
+
const suggestRef = useRef(null);
|
| 36 |
+
|
| 37 |
+
// ── home sections (once) ─────────────────────────────────────────
|
| 38 |
+
useEffect(() => {
|
| 39 |
+
let alive = true;
|
| 40 |
+
(async () => {
|
| 41 |
+
try {
|
| 42 |
+
const data = await api.movieHome();
|
| 43 |
+
if (alive) setSections(data || []);
|
| 44 |
+
} catch (e) {
|
| 45 |
+
if (alive) setError(e.message);
|
| 46 |
+
} finally {
|
| 47 |
+
if (alive) setLoading(false);
|
| 48 |
+
}
|
| 49 |
+
})();
|
| 50 |
+
return () => { alive = false; };
|
| 51 |
+
}, []);
|
| 52 |
+
|
| 53 |
+
// ── catalog load (per tab/sort/page) ─────────────────────────────
|
| 54 |
+
useEffect(() => {
|
| 55 |
+
if (tab === "home" || q.trim()) return undefined;
|
| 56 |
+
let alive = true;
|
| 57 |
+
setLoading(true);
|
| 58 |
+
(async () => {
|
| 59 |
+
try {
|
| 60 |
+
const data = await api.movieCatalog(tab, page, sort);
|
| 61 |
+
if (!alive) return;
|
| 62 |
+
setCatalog((prev) => (page === 1 ? data.results : [...prev, ...data.results]));
|
| 63 |
+
setHasMore(data.hasNextPage);
|
| 64 |
+
} catch (e) {
|
| 65 |
+
if (alive) setError(e.message);
|
| 66 |
+
} finally {
|
| 67 |
+
if (alive) setLoading(false);
|
| 68 |
+
}
|
| 69 |
+
})();
|
| 70 |
+
return () => { alive = false; };
|
| 71 |
+
}, [tab, page, sort, q]);
|
| 72 |
+
|
| 73 |
+
// ── search + suggestions (debounced) ─────────────────────────────
|
| 74 |
+
useEffect(() => {
|
| 75 |
+
if (!q.trim()) {
|
| 76 |
+
setResults([]);
|
| 77 |
+
setSuggestions([]);
|
| 78 |
+
setSearching(false);
|
| 79 |
+
return undefined;
|
| 80 |
+
}
|
| 81 |
+
setSearching(true);
|
| 82 |
+
clearTimeout(debounce.current);
|
| 83 |
+
debounce.current = setTimeout(async () => {
|
| 84 |
+
try {
|
| 85 |
+
const [data, sug] = await Promise.all([
|
| 86 |
+
api.movieSearch(q.trim(), 1),
|
| 87 |
+
api.movieSuggest(q.trim()).catch(() => []),
|
| 88 |
+
]);
|
| 89 |
+
setResults(data.results || []);
|
| 90 |
+
setSuggestions(sug || []);
|
| 91 |
+
} catch (e) {
|
| 92 |
+
setError(e.message);
|
| 93 |
+
setResults([]);
|
| 94 |
+
} finally {
|
| 95 |
+
setSearching(false);
|
| 96 |
+
}
|
| 97 |
+
}, 350);
|
| 98 |
+
return () => clearTimeout(debounce.current);
|
| 99 |
+
}, [q]);
|
| 100 |
+
|
| 101 |
+
// close suggestions on outside click
|
| 102 |
+
useEffect(() => {
|
| 103 |
+
function onClick(e) {
|
| 104 |
+
if (suggestRef.current && !suggestRef.current.contains(e.target)) {
|
| 105 |
+
setSuggestions([]);
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
document.addEventListener("mousedown", onClick);
|
| 109 |
+
return () => document.removeEventListener("mousedown", onClick);
|
| 110 |
+
}, []);
|
| 111 |
+
|
| 112 |
+
function switchTab(key) {
|
| 113 |
+
setTab(key);
|
| 114 |
+
setPage(1);
|
| 115 |
+
setCatalog([]);
|
| 116 |
+
setError(null);
|
| 117 |
+
setSuggestions([]);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
function switchSort(value) {
|
| 121 |
+
setSort(value);
|
| 122 |
+
setPage(1);
|
| 123 |
+
setCatalog([]);
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
function loadMore() {
|
| 127 |
+
setPage((p) => p + 1);
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
function pickSuggestion(s) {
|
| 131 |
+
setSuggestions([]);
|
| 132 |
+
setQ(s);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
const showSearch = q.trim().length > 0;
|
| 136 |
+
const showHome = tab === "home" && !showSearch;
|
| 137 |
+
|
| 138 |
+
return (
|
| 139 |
+
<div className="container page-shell">
|
| 140 |
+
<div className="section-header compact">
|
| 141 |
+
<h1 className="section-title">Movies & TV</h1>
|
| 142 |
+
</div>
|
| 143 |
+
|
| 144 |
+
{/* Search + suggestions */}
|
| 145 |
+
<div className="movie-search-wrap" ref={suggestRef}>
|
| 146 |
+
<div className="hero-search-wrap" style={{ margin: "0 0 22px 0", maxWidth: 640 }}>
|
| 147 |
+
<input
|
| 148 |
+
type="text"
|
| 149 |
+
className="hero-search-input"
|
| 150 |
+
value={q}
|
| 151 |
+
onChange={(e) => setQ(e.target.value)}
|
| 152 |
+
placeholder="Search movies & TV…"
|
| 153 |
+
/>
|
| 154 |
+
<div className="hero-search-btn">
|
| 155 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" width="18" height="18">
|
| 156 |
+
<circle cx="11" cy="11" r="8" />
|
| 157 |
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
| 158 |
+
</svg>
|
| 159 |
+
</div>
|
| 160 |
+
{suggestions.length > 0 ? (
|
| 161 |
+
<div className="movie-suggest">
|
| 162 |
+
{suggestions.map((s, i) => (
|
| 163 |
+
<button key={`${s.title}-${i}`} className="movie-suggest-item" onClick={() => pickSuggestion(s.title)}>
|
| 164 |
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
| 165 |
+
<circle cx="11" cy="11" r="7" />
|
| 166 |
+
<path d="m20 20-3.5-3.5" />
|
| 167 |
+
</svg>
|
| 168 |
+
{s.title}
|
| 169 |
+
</button>
|
| 170 |
+
))}
|
| 171 |
+
</div>
|
| 172 |
+
) : null}
|
| 173 |
+
</div>
|
| 174 |
+
</div>
|
| 175 |
+
|
| 176 |
+
{/* Tabs */}
|
| 177 |
+
<div className="movie-tabs">
|
| 178 |
+
{TABS.map((t) => (
|
| 179 |
+
<button
|
| 180 |
+
key={t.key}
|
| 181 |
+
type="button"
|
| 182 |
+
className={tab === t.key && !showSearch ? "active" : ""}
|
| 183 |
+
onClick={() => switchTab(t.key)}
|
| 184 |
+
>
|
| 185 |
+
{t.label}
|
| 186 |
+
</button>
|
| 187 |
+
))}
|
| 188 |
+
</div>
|
| 189 |
+
|
| 190 |
+
{/* Sort (catalog tabs only) */}
|
| 191 |
+
{!showHome && !showSearch ? (
|
| 192 |
+
<div className="movie-sort">
|
| 193 |
+
<span className="movie-sort-label">Sort</span>
|
| 194 |
+
{SORTS.map((s) => (
|
| 195 |
+
<button key={s} className={sort === s ? "active" : ""} onClick={() => switchSort(s)}>
|
| 196 |
+
{s}
|
| 197 |
+
</button>
|
| 198 |
+
))}
|
| 199 |
+
</div>
|
| 200 |
+
) : null}
|
| 201 |
+
|
| 202 |
+
{error ? (
|
| 203 |
+
<div className="error-box">
|
| 204 |
+
{error}
|
| 205 |
+
<div style={{ marginTop: 8, fontSize: "0.82rem" }}>
|
| 206 |
+
MovieBox is rate-limiting requests right now — try again in a few minutes.
|
| 207 |
+
</div>
|
| 208 |
+
</div>
|
| 209 |
+
) : null}
|
| 210 |
+
|
| 211 |
+
{/* Search results */}
|
| 212 |
+
{showSearch ? (
|
| 213 |
+
searching ? (
|
| 214 |
+
<div style={GRID_STYLE}>
|
| 215 |
+
{Array.from({ length: 12 }).map((_, i) => (
|
| 216 |
+
<div key={i} className="anime-card skel-card">
|
| 217 |
+
<div className="skel-poster skeleton" />
|
| 218 |
+
<div className="skel-line skeleton" />
|
| 219 |
+
<div className="skel-line skel-line-sm skeleton" />
|
| 220 |
+
</div>
|
| 221 |
+
))}
|
| 222 |
+
</div>
|
| 223 |
+
) : results.length ? (
|
| 224 |
+
<div style={GRID_STYLE}>
|
| 225 |
+
{results.map((m) => (
|
| 226 |
+
<MovieCard key={m.slug || m.id} item={m} />
|
| 227 |
+
))}
|
| 228 |
+
</div>
|
| 229 |
+
) : (
|
| 230 |
+
<div className="empty">
|
| 231 |
+
<div className="big">🎬</div>
|
| 232 |
+
<p>No movies found for “{q}”.</p>
|
| 233 |
+
</div>
|
| 234 |
+
)
|
| 235 |
+
) : null}
|
| 236 |
+
|
| 237 |
+
{/* Home rows */}
|
| 238 |
+
{showHome ? (
|
| 239 |
+
loading ? (
|
| 240 |
+
<div style={GRID_STYLE}>
|
| 241 |
+
{Array.from({ length: 12 }).map((_, i) => (
|
| 242 |
+
<div key={i} className="anime-card skel-card">
|
| 243 |
+
<div className="skel-poster skeleton" />
|
| 244 |
+
<div className="skel-line skeleton" />
|
| 245 |
+
<div className="skel-line skel-line-sm skeleton" />
|
| 246 |
+
</div>
|
| 247 |
+
))}
|
| 248 |
+
</div>
|
| 249 |
+
) : sections.length ? (
|
| 250 |
+
sections.map((section) => (
|
| 251 |
+
<MovieRow key={section.key} title={section.title} items={section.items} />
|
| 252 |
+
))
|
| 253 |
+
) : (
|
| 254 |
+
<div className="empty">
|
| 255 |
+
<div className="big">🎬</div>
|
| 256 |
+
<p>No sections loaded from MovieBox.</p>
|
| 257 |
+
</div>
|
| 258 |
+
)
|
| 259 |
+
) : null}
|
| 260 |
+
|
| 261 |
+
{/* Catalog grid */}
|
| 262 |
+
{!showHome && !showSearch ? (
|
| 263 |
+
<>
|
| 264 |
+
<div style={GRID_STYLE}>
|
| 265 |
+
{catalog.map((m) => (
|
| 266 |
+
<MovieCard key={m.slug || m.id} item={m} />
|
| 267 |
+
))}
|
| 268 |
+
</div>
|
| 269 |
+
{loading ? (
|
| 270 |
+
<div className="loader">
|
| 271 |
+
<div className="spinner" />
|
| 272 |
+
</div>
|
| 273 |
+
) : null}
|
| 274 |
+
{!loading && catalog.length === 0 && !error ? (
|
| 275 |
+
<div className="empty">
|
| 276 |
+
<div className="big">📺</div>
|
| 277 |
+
<p>No titles in this tab yet.</p>
|
| 278 |
+
</div>
|
| 279 |
+
) : null}
|
| 280 |
+
{hasMore ? (
|
| 281 |
+
<div className="load-more-wrap">
|
| 282 |
+
<button className="btn btn-primary load-more-btn" onClick={loadMore}>
|
| 283 |
+
Load More
|
| 284 |
+
</button>
|
| 285 |
+
</div>
|
| 286 |
+
) : null}
|
| 287 |
+
</>
|
| 288 |
+
) : null}
|
| 289 |
+
</div>
|
| 290 |
+
);
|
| 291 |
+
}
|