redesign: Ember — port mobile's Inter/mono type system and gradient accent to desktop
This commit is contained in:
parent
4df5a3dbf9
commit
89b7d61501
20 changed files with 144 additions and 115 deletions
|
|
@ -11,7 +11,7 @@
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Space+Mono:wght@400;700&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
|
|
@ -94,8 +94,8 @@ export default function AddToListButton({ releaseId }: { releaseId: number }) {
|
||||||
onClick={() => setOpen((v) => !v)}
|
onClick={() => setOpen((v) => !v)}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
whileTap={{ scale: 0.96 }}
|
whileTap={{ scale: 0.96 }}
|
||||||
className={`rounded-full px-4 py-1.5 text-sm font-semibold transition-colors ${
|
className={`rounded-full px-4 py-1.5 text-sm font-semibold transition-transform ${
|
||||||
collectionType ? 'bg-white/10 hover:bg-white/15' : 'bg-accent hover:bg-accent-hover'
|
collectionType ? 'bg-white/10 hover:bg-white/15' : 'bg-accent-gradient hover:scale-105'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{collectionType ? LABELS[collectionType] : '+ Добавить в список'}
|
{collectionType ? LABELS[collectionType] : '+ Добавить в список'}
|
||||||
|
|
@ -103,12 +103,12 @@ export default function AddToListButton({ releaseId }: { releaseId: number }) {
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{open && (
|
{open && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: -6, scale: 0.96 }}
|
initial={{ opacity: 0, y: -6, filter: 'blur(4px)' }}
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
|
||||||
exit={{ opacity: 0, y: -6, scale: 0.96 }}
|
exit={{ opacity: 0, y: -4, filter: 'blur(3px)' }}
|
||||||
transition={{ duration: 0.15 }}
|
transition={{ type: 'spring', duration: 0.3, bounce: 0 }}
|
||||||
onMouseLeave={() => setOpen(false)}
|
onMouseLeave={() => setOpen(false)}
|
||||||
className="absolute left-0 top-full z-10 mt-1 w-44 origin-top overflow-hidden rounded-lg bg-surface-card shadow-xl"
|
className="glass absolute left-0 top-full z-10 mt-1 w-44 origin-top overflow-hidden rounded-xl border border-white/10 shadow-xl"
|
||||||
>
|
>
|
||||||
{(Object.keys(LABELS) as CollectionType[]).map((t) => (
|
{(Object.keys(LABELS) as CollectionType[]).map((t) => (
|
||||||
<button
|
<button
|
||||||
|
|
@ -142,7 +142,7 @@ export default function AddToListButton({ releaseId }: { releaseId: number }) {
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
title={isFavorite ? 'Убрать из избранного' : 'В избранное'}
|
title={isFavorite ? 'Убрать из избранного' : 'В избранное'}
|
||||||
className={`flex h-8 w-8 items-center justify-center rounded-full transition-colors ${
|
className={`flex h-8 w-8 items-center justify-center rounded-full transition-colors ${
|
||||||
isFavorite ? 'bg-accent/20 text-accent' : 'bg-white/5 text-white/50 hover:bg-white/10'
|
isFavorite ? 'bg-accent-soft text-accent' : 'bg-white/5 text-white/50 hover:bg-white/10'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill={isFavorite ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill={isFavorite ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2">
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ export default function AnimeCard({ release }: { release: Release }) {
|
||||||
const title = release.name.main
|
const title = release.name.main
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div variants={cardVariants} whileHover={{ y: -6 }} transition={{ type: 'spring', stiffness: 300, damping: 22 }}>
|
<motion.div variants={cardVariants}>
|
||||||
<Link to={`/title/${release.alias || release.id}`} className="poster-card group block">
|
<Link to={`/title/${release.alias || release.id}`} className="poster-card group block">
|
||||||
<div className="aspect-[2/3] w-full overflow-hidden rounded-xl bg-surface-raised shadow-md shadow-black/30 ring-1 ring-white/5">
|
<div className="aspect-[2/3] w-full overflow-hidden rounded-xl bg-surface-raised ring-1 ring-white/5">
|
||||||
{poster && (
|
{poster && (
|
||||||
<motion.img
|
<motion.img
|
||||||
src={poster}
|
src={poster}
|
||||||
|
|
@ -33,19 +33,19 @@ export default function AnimeCard({ release }: { release: Release }) {
|
||||||
|
|
||||||
<div className="absolute left-1.5 top-1.5 flex flex-col gap-1">
|
<div className="absolute left-1.5 top-1.5 flex flex-col gap-1">
|
||||||
{release.is_ongoing && (
|
{release.is_ongoing && (
|
||||||
<span className="rounded bg-accent px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide shadow">
|
<span className="mono-label rounded bg-accent-gradient px-1.5 py-0.5 text-white shadow">
|
||||||
Онгоинг
|
Онгоинг
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{release.age_rating?.is_adult && (
|
{release.age_rating?.is_adult && (
|
||||||
<span className="rounded bg-black/70 px-1.5 py-0.5 text-[10px] font-semibold">
|
<span className="mono-label rounded bg-black/70 px-1.5 py-0.5">
|
||||||
{release.age_rating.label}
|
{release.age_rating.label}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{release.episodes_total != null && (
|
{release.episodes_total != null && (
|
||||||
<span className="absolute right-1.5 top-1.5 rounded bg-black/70 px-1.5 py-0.5 text-[10px] font-medium">
|
<span className="mono-label absolute right-1.5 top-1.5 rounded bg-black/70 px-1.5 py-0.5">
|
||||||
{release.episodes_total} эп.
|
{release.episodes_total} эп.
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, FormEvent } from 'react'
|
import { useState, FormEvent } from 'react'
|
||||||
import { Link, useNavigate } from 'react-router-dom'
|
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import Logo from './Logo'
|
import Logo from './Logo'
|
||||||
import LoginModal from './LoginModal'
|
import LoginModal from './LoginModal'
|
||||||
|
|
@ -19,6 +19,7 @@ export default function Header() {
|
||||||
const [showLogin, setShowLogin] = useState(false)
|
const [showLogin, setShowLogin] = useState(false)
|
||||||
const [menuOpen, setMenuOpen] = useState(false)
|
const [menuOpen, setMenuOpen] = useState(false)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
const { user, logout } = useAuth()
|
const { user, logout } = useAuth()
|
||||||
|
|
||||||
function onSubmit(e: FormEvent) {
|
function onSubmit(e: FormEvent) {
|
||||||
|
|
@ -28,27 +29,34 @@ export default function Header() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-40 border-b border-white/5 bg-surface/85 backdrop-blur">
|
<header className="glass sticky top-0 z-40 border-b border-white/5">
|
||||||
<div className="mx-auto flex max-w-7xl items-center gap-6 px-6 py-3">
|
<div className="mx-auto flex max-w-7xl items-center gap-6 px-6 py-3">
|
||||||
<Link to="/" className="flex items-center gap-2 shrink-0">
|
<Link to="/" className="flex shrink-0 items-center gap-2">
|
||||||
<Logo />
|
<Logo />
|
||||||
<span className="text-lg font-extrabold tracking-tight">
|
<span className="font-mono text-sm font-bold tracking-widest">
|
||||||
Ani<span className="text-accent">Sav</span>
|
ANI<span className="text-accent">SAV</span>
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<nav className="hidden gap-5 text-sm font-medium text-white/70 md:flex">
|
<nav className="hidden gap-1 text-sm font-medium text-white/60 md:flex">
|
||||||
{NAV.map((n) => (
|
{NAV.map((n) => (
|
||||||
<Link key={n.to} to={n.to} className="transition-colors hover:text-white">
|
<Link key={n.to} to={n.to} className="relative px-3 py-1.5 transition-colors hover:text-white">
|
||||||
{n.label}
|
{location.pathname === n.to && (
|
||||||
|
<motion.span
|
||||||
|
layoutId="nav-active"
|
||||||
|
transition={{ type: 'spring', stiffness: 380, damping: 32 }}
|
||||||
|
className="absolute inset-0 rounded-full bg-white/8"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span className={`relative ${location.pathname === n.to ? 'text-white' : ''}`}>{n.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
{user && (
|
{user && (
|
||||||
<>
|
<>
|
||||||
<Link to="/lists" className="transition-colors hover:text-white">
|
<Link to="/lists" className="px-3 py-1.5 transition-colors hover:text-white">
|
||||||
Мои списки
|
Мои списки
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/history" className="transition-colors hover:text-white">
|
<Link to="/history" className="px-3 py-1.5 transition-colors hover:text-white">
|
||||||
История
|
История
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
|
|
@ -77,7 +85,7 @@ export default function Header() {
|
||||||
className="h-6 w-6 rounded-full object-cover"
|
className="h-6 w-6 rounded-full object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-accent text-xs font-bold">
|
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-accent-gradient text-xs font-bold">
|
||||||
{user.nickname[0]?.toUpperCase()}
|
{user.nickname[0]?.toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -86,12 +94,12 @@ export default function Header() {
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{menuOpen && (
|
{menuOpen && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: -6, scale: 0.96 }}
|
initial={{ opacity: 0, y: -6, filter: 'blur(4px)' }}
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
|
||||||
exit={{ opacity: 0, y: -6, scale: 0.96 }}
|
exit={{ opacity: 0, y: -4, filter: 'blur(3px)' }}
|
||||||
transition={{ duration: 0.15 }}
|
transition={{ type: 'spring', duration: 0.35, bounce: 0 }}
|
||||||
onMouseLeave={() => setMenuOpen(false)}
|
onMouseLeave={() => setMenuOpen(false)}
|
||||||
className="absolute right-0 top-full z-10 mt-2 w-44 origin-top-right overflow-hidden rounded-lg bg-surface-card shadow-xl"
|
className="glass absolute right-0 top-full z-10 mt-2 w-44 origin-top-right overflow-hidden rounded-xl border border-white/10 shadow-2xl"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
to="/lists"
|
to="/lists"
|
||||||
|
|
@ -112,7 +120,7 @@ export default function Header() {
|
||||||
setMenuOpen(false)
|
setMenuOpen(false)
|
||||||
logout()
|
logout()
|
||||||
}}
|
}}
|
||||||
className="block w-full px-4 py-2 text-left text-sm text-red-400 hover:bg-white/5"
|
className="block w-full px-4 py-2 text-left text-sm text-accent hover:bg-white/5"
|
||||||
>
|
>
|
||||||
Выйти
|
Выйти
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -123,7 +131,7 @@ export default function Header() {
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowLogin(true)}
|
onClick={() => setShowLogin(true)}
|
||||||
className="shrink-0 rounded-full bg-accent px-4 py-1.5 text-sm font-semibold hover:bg-accent-hover"
|
className="shrink-0 rounded-full bg-accent-gradient px-4 py-1.5 text-sm font-semibold transition-transform hover:scale-105"
|
||||||
>
|
>
|
||||||
Войти
|
Войти
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ export default function LoginModal({ onClose }: { onClose: () => void }) {
|
||||||
await login(loginValue, password, remember)
|
await login(loginValue, password, remember)
|
||||||
onClose()
|
onClose()
|
||||||
} catch {
|
} catch {
|
||||||
// error is surfaced via context
|
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
}
|
}
|
||||||
|
|
@ -33,15 +32,15 @@ export default function LoginModal({ onClose }: { onClose: () => void }) {
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
<motion.form
|
<motion.form
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 10 }}
|
initial={{ opacity: 0, scale: 0.96, y: 10, filter: 'blur(6px)' }}
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
animate={{ opacity: 1, scale: 1, y: 0, filter: 'blur(0px)' }}
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 10 }}
|
exit={{ opacity: 0, scale: 0.97, y: 6, filter: 'blur(4px)' }}
|
||||||
transition={{ type: 'spring', duration: 0.35, bounce: 0.25 }}
|
transition={{ type: 'spring', duration: 0.4, bounce: 0.1 }}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
className="my-auto max-h-[90vh] w-full max-w-sm overflow-y-auto rounded-2xl bg-surface-raised p-6 shadow-2xl"
|
className="my-auto max-h-[90vh] w-full max-w-sm overflow-y-auto rounded-2xl border border-white/10 bg-surface-raised p-6 shadow-2xl"
|
||||||
>
|
>
|
||||||
<h2 className="text-lg font-bold">Вход в AniLiberty</h2>
|
<h2 className="text-lg font-bold tracking-tight">Вход в AniLiberty</h2>
|
||||||
<p className="mt-1 text-xs text-white/40">
|
<p className="mt-1 text-xs text-white/40">
|
||||||
Используй логин и пароль своего аккаунта на aniliberty.top
|
Используй логин и пароль своего аккаунта на aniliberty.top
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -72,13 +71,13 @@ export default function LoginModal({ onClose }: { onClose: () => void }) {
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={remember}
|
checked={remember}
|
||||||
onChange={(e) => setRemember(e.target.checked)}
|
onChange={(e) => setRemember(e.target.checked)}
|
||||||
className="h-3.5 w-3.5 accent-[#fe3635]"
|
className="h-3.5 w-3.5 accent-[#FF3D5A]"
|
||||||
/>
|
/>
|
||||||
Запомнить меня
|
Запомнить меня
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="mt-3 text-xs text-red-400">{error}</p>}
|
{error && <p className="mt-3 text-xs text-accent">{error}</p>}
|
||||||
|
|
||||||
<div className="mt-5 flex gap-2">
|
<div className="mt-5 flex gap-2">
|
||||||
<button
|
<button
|
||||||
|
|
@ -91,7 +90,7 @@ export default function LoginModal({ onClose }: { onClose: () => void }) {
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting || !loginValue || !password}
|
disabled={submitting || !loginValue || !password}
|
||||||
className="flex-1 rounded-full bg-accent py-2 text-sm font-semibold hover:bg-accent-hover disabled:opacity-40"
|
className="flex-1 rounded-full bg-accent-gradient py-2 text-sm font-semibold transition-transform hover:scale-[1.02] disabled:opacity-40 disabled:hover:scale-100"
|
||||||
>
|
>
|
||||||
{submitting ? '...' : 'Войти'}
|
{submitting ? '...' : 'Войти'}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
export default function Logo({ size = 28 }: { size?: number }) {
|
export default function Logo({ size = 28 }: { size?: number }) {
|
||||||
return (
|
return (
|
||||||
<svg width={size} height={size} viewBox="0 0 1174 1174" fill="#fe3635" xmlns="http://www.w3.org/2000/svg">
|
<svg width={size} height={size} viewBox="0 0 1174 1174" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="anisav-logo-grad" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#FF3D5A" />
|
||||||
|
<stop offset="100%" stopColor="#FF8A4C" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g fill="url(#anisav-logo-grad)">
|
||||||
<path d="M698 245c0-7 8-12 14-9l169 77c4 2 6 6 6 9v395c0 6 4 10 10 10h248c6 0 11 6 9 12-25.3 94.2-73.6 180.7-140.7 251.5-67.1 70.9-150.8 123.9-243.5 154.2-92.7 30.4-191.5 37.2-287.5 19.8-96-17.4-186.2-58.5-262.3-119.5-3-3-4-8-2-12l34-58c3-5 10-7 15-3 51.5 43.1 111.1 75.4 175.4 94.9 64.2 19.6 131.7 25.9 198.5 18.7 66.7-7.2 131.4-27.8 190-60.5 58.6-32.8 110-77 151.1-130.1 5-7 1-16-8-16h-93c-4 0-7-2-9-5L699 574l-1-5z" />
|
<path d="M698 245c0-7 8-12 14-9l169 77c4 2 6 6 6 9v395c0 6 4 10 10 10h248c6 0 11 6 9 12-25.3 94.2-73.6 180.7-140.7 251.5-67.1 70.9-150.8 123.9-243.5 154.2-92.7 30.4-191.5 37.2-287.5 19.8-96-17.4-186.2-58.5-262.3-119.5-3-3-4-8-2-12l34-58c3-5 10-7 15-3 51.5 43.1 111.1 75.4 175.4 94.9 64.2 19.6 131.7 25.9 198.5 18.7 66.7-7.2 131.4-27.8 190-60.5 58.6-32.8 110-77 151.1-130.1 5-7 1-16-8-16h-93c-4 0-7-2-9-5L699 574l-1-5z" />
|
||||||
<path d="M422 240c4-7 14-7 18 0l360 624c4 7-1 15-8 15H628c-4 0-7-2-9-5l-43-75c-2-3-5-5-9-5H373c-7 0-12-8-8-15l65-113c1-3 5-5 8-5h41c7 0 12-8 8-15l-47-83c-4-7-14-7-18 0L211 929v1l-23 39-1 1-10 19c-4 6-12 7-16 2-36-38-67-82-92-128-2-3-2-7 0-10l10-19 2-2v-1z" />
|
<path d="M422 240c4-7 14-7 18 0l360 624c4 7-1 15-8 15H628c-4 0-7-2-9-5l-43-75c-2-3-5-5-9-5H373c-7 0-12-8-8-15l65-113c1-3 5-5 8-5h41c7 0 12-8 8-15l-47-83c-4-7-14-7-18 0L211 929v1l-23 39-1 1-10 19c-4 6-12 7-16 2-36-38-67-82-92-128-2-3-2-7 0-10l10-19 2-2v-1z" />
|
||||||
<path d="M587 0c83.2.1 165.5 17.8 241.3 52.1 75.8 34.3 143.5 84.2 198.5 146.7 55.1 62.4 96.2 135.8 120.7 215.3s31.8 163.3 21.5 245.9c0 5-4 9-9 9h-67c-6 0-11-6-10-12 20.8-146.3-24-294.3-122.5-404.5C862.1 142.4 720 81.3 572.3 85.6 424.6 90 286.3 159.3 194.5 275 102.7 390.8 66.6 541.2 96 686l-1 7-47 81c-5 7-15 6-18-2C.5 683.8-7.6 589.8 6.4 497.8c14-91.9 49.6-179.2 104.1-254.7 54.4-75.4 126-136.8 208.9-179.1C402.2 21.7 494-.2 587 0z" />
|
<path d="M587 0c83.2.1 165.5 17.8 241.3 52.1 75.8 34.3 143.5 84.2 198.5 146.7 55.1 62.4 96.2 135.8 120.7 215.3s31.8 163.3 21.5 245.9c0 5-4 9-9 9h-67c-6 0-11-6-10-12 20.8-146.3-24-294.3-122.5-404.5C862.1 142.4 720 81.3 572.3 85.6 424.6 90 286.3 159.3 194.5 275 102.7 390.8 66.6 541.2 96 686l-1 7-47 81c-5 7-15 6-18-2C.5 683.8-7.6 589.8 6.4 497.8c14-91.9 49.6-179.2 104.1-254.7 54.4-75.4 126-136.8 208.9-179.1C402.2 21.7 494-.2 587 0z" />
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,6 @@ export default function Player({ episode, resumeAt, onProgress, onEnded }: Props
|
||||||
setQuality(available[0]?.key)
|
setQuality(available[0]?.key)
|
||||||
skippedRanges.current = new Set()
|
skippedRanges.current = new Set()
|
||||||
didAutoFullscreen.current = false
|
didAutoFullscreen.current = false
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [episode.id])
|
}, [episode.id])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -73,10 +72,6 @@ export default function Player({ episode, resumeAt, onProgress, onEnded }: Props
|
||||||
})
|
})
|
||||||
hls.loadSource(src)
|
hls.loadSource(src)
|
||||||
hls.attachMedia(video)
|
hls.attachMedia(video)
|
||||||
// Network/media hiccups (CDN edge blip, transient segment fetch failure)
|
|
||||||
// are fatal by default in hls.js unless the app recovers them — without
|
|
||||||
// this, a single dropped segment can stall playback or force a reload
|
|
||||||
// that jumps back to position 0 instead of resuming where it was.
|
|
||||||
hls.on(Hls.Events.ERROR, (_event, data) => {
|
hls.on(Hls.Events.ERROR, (_event, data) => {
|
||||||
if (!data.fatal) return
|
if (!data.fatal) return
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
|
|
@ -106,7 +101,6 @@ export default function Player({ episode, resumeAt, onProgress, onEnded }: Props
|
||||||
hlsRef.current?.destroy()
|
hlsRef.current?.destroy()
|
||||||
hlsRef.current = null
|
hlsRef.current = null
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [quality, episode.id])
|
}, [quality, episode.id])
|
||||||
|
|
||||||
const reportProgress = useCallback(() => {
|
const reportProgress = useCallback(() => {
|
||||||
|
|
@ -222,12 +216,10 @@ export default function Player({ episode, resumeAt, onProgress, onEnded }: Props
|
||||||
}
|
}
|
||||||
window.addEventListener('keydown', onKey)
|
window.addEventListener('keydown', onKey)
|
||||||
return () => window.removeEventListener('keydown', onKey)
|
return () => window.removeEventListener('keydown', onKey)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [duration])
|
}, [duration])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => reportProgress()
|
return () => reportProgress()
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [episode.id])
|
}, [episode.id])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -253,7 +245,7 @@ export default function Player({ episode, resumeAt, onProgress, onEnded }: Props
|
||||||
{showSkip && (
|
{showSkip && (
|
||||||
<button
|
<button
|
||||||
onClick={skip}
|
onClick={skip}
|
||||||
className="absolute bottom-20 right-4 flex items-center gap-1.5 rounded bg-black/80 px-4 py-2 text-sm font-semibold backdrop-blur transition-colors hover:bg-accent"
|
className="absolute bottom-20 right-4 flex items-center gap-1.5 rounded-lg bg-black/70 px-4 py-2 text-sm font-semibold backdrop-blur-md transition-colors hover:bg-accent-gradient"
|
||||||
>
|
>
|
||||||
<SkipIcon />
|
<SkipIcon />
|
||||||
Пропустить {showSkip === 'opening' ? 'опенинг' : 'эндинг'}
|
Пропустить {showSkip === 'opening' ? 'опенинг' : 'эндинг'}
|
||||||
|
|
@ -320,7 +312,7 @@ export default function Player({ episode, resumeAt, onProgress, onEnded }: Props
|
||||||
{showSettings && (
|
{showSettings && (
|
||||||
<div
|
<div
|
||||||
onMouseLeave={() => setShowSettings(false)}
|
onMouseLeave={() => setShowSettings(false)}
|
||||||
className="absolute bottom-full right-0 mb-2 w-64 space-y-3 rounded-xl bg-surface-raised p-3 text-sm shadow-2xl"
|
className="glass absolute bottom-full right-0 mb-2 w-64 space-y-3 rounded-xl border border-white/10 p-3 text-sm shadow-2xl"
|
||||||
>
|
>
|
||||||
{available.length > 1 && (
|
{available.length > 1 && (
|
||||||
<SettingsRow label="Качество">
|
<SettingsRow label="Качество">
|
||||||
|
|
@ -329,8 +321,8 @@ export default function Player({ episode, resumeAt, onProgress, onEnded }: Props
|
||||||
<button
|
<button
|
||||||
key={q.key}
|
key={q.key}
|
||||||
onClick={() => setQuality(q.key)}
|
onClick={() => setQuality(q.key)}
|
||||||
className={`rounded px-1.5 py-0.5 text-xs ${
|
className={`rounded px-1.5 py-0.5 text-xs font-semibold ${
|
||||||
quality === q.key ? 'bg-accent' : 'bg-white/10 hover:bg-white/20'
|
quality === q.key ? 'bg-accent-gradient' : 'bg-white/10 hover:bg-white/20'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{q.label}
|
{q.label}
|
||||||
|
|
@ -340,8 +332,8 @@ export default function Player({ episode, resumeAt, onProgress, onEnded }: Props
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="border-t border-white/10 pt-2 text-xs text-white/40">
|
<div className="border-t border-white/10 pt-2 font-mono text-[11px] text-white/40">
|
||||||
Горячие клавиши: Пробел — пауза, ←/→ — перемотка 10с, F — экран, M — звук
|
Пробел — пауза, ←/→ — перемотка 10с, F — экран, M — звук
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 border-t border-white/10 pt-2">
|
<div className="space-y-2 border-t border-white/10 pt-2">
|
||||||
|
|
@ -406,7 +398,7 @@ function ToggleRow({
|
||||||
<span className="text-white/70">{label}</span>
|
<span className="text-white/70">{label}</span>
|
||||||
<span
|
<span
|
||||||
className={`relative h-4 w-8 shrink-0 rounded-full transition-colors ${
|
className={`relative h-4 w-8 shrink-0 rounded-full transition-colors ${
|
||||||
checked ? 'bg-accent' : 'bg-white/15'
|
checked ? 'bg-accent-gradient' : 'bg-white/15'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export default function Row({ title, releases }: { title: string; releases: Rele
|
||||||
if (!releases.length) return null
|
if (!releases.length) return null
|
||||||
return (
|
return (
|
||||||
<section className="mx-auto max-w-7xl px-6 py-5">
|
<section className="mx-auto max-w-7xl px-6 py-5">
|
||||||
<h2 className="mb-3 text-lg font-bold">{title}</h2>
|
<h2 className="mb-3 text-lg font-bold tracking-tight">{title}</h2>
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={container}
|
variants={container}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
export function CardSkeleton() {
|
export function CardSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="animate-pulse">
|
<div className="animate-pulse">
|
||||||
<div className="aspect-[2/3] w-full rounded-lg bg-white/5" />
|
<div className="aspect-[2/3] w-full rounded-xl bg-white/5" />
|
||||||
<div className="mt-2 h-3 w-4/5 rounded bg-white/5" />
|
<div className="mt-2 h-3 w-4/5 rounded bg-white/5" />
|
||||||
<div className="mt-1.5 h-2.5 w-2/5 rounded bg-white/5" />
|
<div className="mt-1.5 h-2.5 w-2/5 rounded bg-white/5" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export default function WishlistButton({
|
||||||
transition={{ duration: 0.35 }}
|
transition={{ duration: 0.35 }}
|
||||||
title={saved ? 'Убрать из понравившегося' : 'Добавить в понравившееся'}
|
title={saved ? 'Убрать из понравившегося' : 'Добавить в понравившееся'}
|
||||||
className={`flex ${dims} items-center justify-center rounded-full transition-colors ${
|
className={`flex ${dims} items-center justify-center rounded-full transition-colors ${
|
||||||
saved ? 'bg-yellow-400/20 text-yellow-400' : 'bg-black/60 text-white/70 hover:bg-black/80'
|
saved ? 'bg-accent-2/20 text-accent-2' : 'bg-black/60 text-white/70 hover:bg-black/80'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|
|
||||||
|
|
@ -19,18 +19,27 @@ body {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #333;
|
background: #2a2a3a;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #444;
|
background: #383850;
|
||||||
}
|
}
|
||||||
|
|
||||||
.poster-card {
|
.poster-card {
|
||||||
@apply relative overflow-hidden rounded-lg bg-surface-card transition-transform duration-200 ease-out;
|
@apply relative overflow-hidden rounded-xl bg-surface-card transition-all duration-200 ease-out;
|
||||||
}
|
}
|
||||||
.poster-card:hover {
|
.poster-card:hover {
|
||||||
transform: translateY(-4px);
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 16px 32px -14px rgba(255, 61, 90, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mono-label {
|
||||||
|
@apply font-mono text-[10px] font-bold uppercase tracking-widest;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass {
|
||||||
|
@apply bg-surface/80 backdrop-blur-xl;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-clamp-2 {
|
.line-clamp-2 {
|
||||||
|
|
@ -47,7 +56,7 @@ body {
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
to right,
|
to right,
|
||||||
#fe3635 var(--progress, 0%),
|
#ff3d5a var(--progress, 0%),
|
||||||
rgba(255, 255, 255, 0.25) var(--progress, 0%)
|
rgba(255, 255, 255, 0.25) var(--progress, 0%)
|
||||||
);
|
);
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
@ -59,7 +68,7 @@ body {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #fe3635;
|
background: #ff3d5a;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.15s ease;
|
transition: transform 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
@ -71,11 +80,11 @@ body {
|
||||||
height: 12px;
|
height: 12px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #fe3635;
|
background: #ff3d5a;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.player-seek::-moz-range-progress {
|
.player-seek::-moz-range-progress {
|
||||||
background: #fe3635;
|
background: #ff3d5a;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,8 +87,8 @@ export default function Catalog() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-7xl px-6 py-6">
|
<div className="mx-auto max-w-7xl px-6 py-6">
|
||||||
<h1 className="mb-4 text-xl font-bold">
|
<h1 className="mb-4 text-xl font-bold tracking-tight">
|
||||||
{searchQ ? `Поиск: "${searchQ}"` : 'Каталог'}
|
{searchQ ? `Поиск: «${searchQ}»` : 'Каталог'}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className="flex gap-6">
|
<div className="flex gap-6">
|
||||||
|
|
@ -198,17 +198,17 @@ export default function Catalog() {
|
||||||
<button
|
<button
|
||||||
disabled={page <= 1}
|
disabled={page <= 1}
|
||||||
onClick={() => setPage((p) => p - 1)}
|
onClick={() => setPage((p) => p - 1)}
|
||||||
className="rounded bg-white/5 px-3 py-1.5 disabled:opacity-30"
|
className="rounded-full bg-white/5 px-4 py-1.5 transition-colors hover:bg-white/10 disabled:opacity-30"
|
||||||
>
|
>
|
||||||
Назад
|
Назад
|
||||||
</button>
|
</button>
|
||||||
<span className="text-white/50">
|
<span className="font-mono text-xs tabular-nums text-white/50">
|
||||||
{page} / {totalPages}
|
{page} / {totalPages}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
disabled={page >= totalPages}
|
disabled={page >= totalPages}
|
||||||
onClick={() => setPage((p) => p + 1)}
|
onClick={() => setPage((p) => p + 1)}
|
||||||
className="rounded bg-white/5 px-3 py-1.5 disabled:opacity-30"
|
className="rounded-full bg-white/5 px-4 py-1.5 transition-colors hover:bg-white/10 disabled:opacity-30"
|
||||||
>
|
>
|
||||||
Далее
|
Далее
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -225,7 +225,7 @@ export default function Catalog() {
|
||||||
function FilterGroup({ title, children }: { title: string; children: React.ReactNode }) {
|
function FilterGroup({ title, children }: { title: string; children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-2 text-xs font-bold uppercase tracking-wide text-white/40">{title}</h3>
|
<h3 className="mono-label mb-2 text-white/40">{title}</h3>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -246,7 +246,7 @@ function Check({
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
className="h-3.5 w-3.5 accent-[#fe3635]"
|
className="h-3.5 w-3.5 accent-[#FF3D5A]"
|
||||||
/>
|
/>
|
||||||
<span className="text-xs">{label}</span>
|
<span className="text-xs">{label}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export default function Genres() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-7xl px-6 py-6">
|
<div className="mx-auto max-w-7xl px-6 py-6">
|
||||||
<h1 className="mb-4 text-xl font-bold">Жанры</h1>
|
<h1 className="mb-4 text-xl font-bold tracking-tight">Жанры</h1>
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={container}
|
variants={container}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
|
|
@ -48,9 +48,9 @@ export default function Genres() {
|
||||||
)}
|
)}
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent" />
|
<div className="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent" />
|
||||||
<div className="relative flex h-full flex-col justify-end p-3">
|
<div className="relative flex h-full flex-col justify-end p-3">
|
||||||
<p className="font-semibold">{g.name}</p>
|
<p className="font-semibold tracking-tight">{g.name}</p>
|
||||||
{g.total_releases != null && (
|
{g.total_releases != null && (
|
||||||
<p className="text-xs text-white/50">{g.total_releases} тайтлов</p>
|
<p className="mono-label text-white/50">{g.total_releases} тайтлов</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -32,19 +32,19 @@ export default function History() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-3xl px-6 py-6">
|
<div className="mx-auto max-w-3xl px-6 py-6">
|
||||||
<h1 className="mb-4 text-xl font-bold">История просмотра</h1>
|
<h1 className="mb-4 text-xl font-bold tracking-tight">История просмотра</h1>
|
||||||
{items.length === 0 && <p className="py-16 text-center text-white/40">История пуста</p>}
|
{items.length === 0 && <p className="py-16 text-center text-white/40">История пуста</p>}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{items.filter((item) => item.release && item.release_episode).map((item) => (
|
{items.filter((item) => item.release && item.release_episode).map((item) => (
|
||||||
<Link
|
<Link
|
||||||
key={item.id}
|
key={item.id}
|
||||||
to={`/title/${item.release.alias || item.release.id}`}
|
to={`/title/${item.release.alias || item.release.id}`}
|
||||||
className="flex items-center gap-3 rounded-lg bg-white/5 p-2.5 hover:bg-white/10"
|
className="flex items-center gap-3 rounded-xl bg-white/5 p-2.5 transition-colors hover:bg-white/10"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={imageUrl(item.release.poster?.optimized?.thumbnail || item.release.poster?.thumbnail)}
|
src={imageUrl(item.release.poster?.optimized?.thumbnail || item.release.poster?.thumbnail)}
|
||||||
alt=""
|
alt=""
|
||||||
className="h-16 w-12 shrink-0 rounded object-cover"
|
className="h-16 w-12 shrink-0 rounded-lg object-cover"
|
||||||
/>
|
/>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="truncate text-sm font-medium">{item.release.name.main}</p>
|
<p className="truncate text-sm font-medium">{item.release.name.main}</p>
|
||||||
|
|
@ -52,14 +52,14 @@ export default function History() {
|
||||||
Серия {item.release_episode.ordinal}
|
Серия {item.release_episode.ordinal}
|
||||||
{item.release_episode.name ? ` — ${item.release_episode.name}` : ''}
|
{item.release_episode.name ? ` — ${item.release_episode.name}` : ''}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-white/30">{formatDate(item.updated_at)}</p>
|
<p className="mt-0.5 font-mono text-[10px] text-white/30">{formatDate(item.updated_at)}</p>
|
||||||
</div>
|
</div>
|
||||||
{item.is_watched ? (
|
{item.is_watched ? (
|
||||||
<span className="shrink-0 rounded bg-white/10 px-2 py-0.5 text-[10px] font-semibold text-white/50">
|
<span className="mono-label shrink-0 rounded bg-white/10 px-2 py-1 text-white/50">
|
||||||
Просмотрено
|
Просмотрено
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="shrink-0 rounded bg-accent/20 px-2 py-0.5 text-[10px] font-semibold text-accent">
|
<span className="mono-label shrink-0 rounded bg-accent-soft px-2 py-1 text-accent">
|
||||||
В процессе
|
В процессе
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -50,16 +50,16 @@ export default function Home() {
|
||||||
transition={{ duration: 0.5, delay: 0.15 }}
|
transition={{ duration: 0.5, delay: 0.15 }}
|
||||||
className="relative mx-auto flex h-full max-w-7xl flex-col justify-end px-6 pb-8"
|
className="relative mx-auto flex h-full max-w-7xl flex-col justify-end px-6 pb-8"
|
||||||
>
|
>
|
||||||
<span className="mb-2 w-fit rounded bg-accent px-2 py-0.5 text-xs font-bold uppercase">
|
<span className="mono-label mb-2 w-fit rounded bg-accent-gradient px-2 py-1 text-white">
|
||||||
Новинка
|
Новинка
|
||||||
</span>
|
</span>
|
||||||
<h1 className="max-w-xl text-3xl font-extrabold leading-tight drop-shadow">
|
<h1 className="max-w-xl text-3xl font-extrabold leading-tight tracking-tight drop-shadow">
|
||||||
{hero.name.main}
|
{hero.name.main}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-2 max-w-xl line-clamp-2 text-sm text-white/60">{hero.description}</p>
|
<p className="mt-2 max-w-xl line-clamp-2 text-sm text-white/60">{hero.description}</p>
|
||||||
<Link
|
<Link
|
||||||
to={`/title/${hero.alias || hero.id}`}
|
to={`/title/${hero.alias || hero.id}`}
|
||||||
className="mt-4 w-fit rounded-full bg-accent px-5 py-2 text-sm font-semibold transition-colors hover:bg-accent-hover"
|
className="mt-4 w-fit rounded-full bg-accent-gradient px-5 py-2 text-sm font-semibold transition-transform hover:scale-105"
|
||||||
>
|
>
|
||||||
Смотреть
|
Смотреть
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -90,13 +90,13 @@ export default function Home() {
|
||||||
/>
|
/>
|
||||||
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/90 via-black/10 to-transparent opacity-0 transition-opacity duration-200 group-hover:opacity-100" />
|
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/90 via-black/10 to-transparent opacity-0 transition-opacity duration-200 group-hover:opacity-100" />
|
||||||
<div className="absolute inset-x-0 bottom-0 p-2">
|
<div className="absolute inset-x-0 bottom-0 p-2">
|
||||||
<p className="mb-1 truncate text-[10px] font-medium text-white/70">
|
<p className="mono-label mb-1 truncate text-white/80">
|
||||||
Серия {c.episode.ordinal}
|
Серия {c.episode.ordinal}
|
||||||
</p>
|
</p>
|
||||||
{c.episode.duration && (
|
{c.episode.duration && (
|
||||||
<div className="h-0.5 w-full overflow-hidden rounded bg-black/40">
|
<div className="h-0.5 w-full overflow-hidden rounded bg-black/40">
|
||||||
<div
|
<div
|
||||||
className="h-full bg-accent"
|
className="h-full bg-accent-gradient"
|
||||||
style={{ width: `${Math.min(100, (c.time / c.episode.duration) * 100)}%` }}
|
style={{ width: `${Math.min(100, (c.time / c.episode.duration) * 100)}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export default function MyLists() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-7xl px-6 py-6">
|
<div className="mx-auto max-w-7xl px-6 py-6">
|
||||||
<h1 className="mb-4 text-xl font-bold">Мои списки</h1>
|
<h1 className="mb-4 text-xl font-bold tracking-tight">Мои списки</h1>
|
||||||
|
|
||||||
<div className="mb-6 flex flex-wrap gap-2 border-b border-white/5 pb-3">
|
<div className="mb-6 flex flex-wrap gap-2 border-b border-white/5 pb-3">
|
||||||
{TABS.map((t) => (
|
{TABS.map((t) => (
|
||||||
|
|
@ -49,7 +49,7 @@ export default function MyLists() {
|
||||||
<motion.span
|
<motion.span
|
||||||
layoutId="mylists-tab-pill"
|
layoutId="mylists-tab-pill"
|
||||||
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
|
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
|
||||||
className="absolute inset-0 rounded-full bg-accent"
|
className="absolute inset-0 rounded-full bg-accent-gradient"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="relative">{t.label}</span>
|
<span className="relative">{t.label}</span>
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,11 @@ export default function Schedule() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-7xl px-6 py-6">
|
<div className="mx-auto max-w-7xl px-6 py-6">
|
||||||
<h1 className="mb-4 text-xl font-bold">Расписание выхода серий</h1>
|
<h1 className="mb-4 text-xl font-bold tracking-tight">Расписание выхода серий</h1>
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{DAY_ORDER.filter((d) => byDay.has(d)).map((day) => (
|
{DAY_ORDER.filter((d) => byDay.has(d)).map((day) => (
|
||||||
<div key={day}>
|
<div key={day}>
|
||||||
<h2 className="mb-3 text-sm font-bold uppercase tracking-wide text-accent">
|
<h2 className="mono-label mb-3 text-accent-2">
|
||||||
{byDay.get(day)![0].release.publish_day?.description}
|
{byDay.get(day)![0].release.publish_day?.description}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-3 gap-4 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-7">
|
<div className="grid grid-cols-3 gap-4 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-7">
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,6 @@ export default function TitleDetail() {
|
||||||
}, [idOrAlias])
|
}, [idOrAlias])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Local progress always applies (no account needed) — account timecodes
|
|
||||||
// (when logged in) are layered on top since they may be synced elsewhere.
|
|
||||||
const local = getAllLocalProgress()
|
const local = getAllLocalProgress()
|
||||||
const merged = new Map<string, TimecodeItem>(
|
const merged = new Map<string, TimecodeItem>(
|
||||||
Object.entries(local).map(([id, p]) => [
|
Object.entries(local).map(([id, p]) => [
|
||||||
|
|
@ -116,18 +114,18 @@ export default function TitleDetail() {
|
||||||
transition={{ duration: 0.35 }}
|
transition={{ duration: 0.35 }}
|
||||||
className="min-w-0"
|
className="min-w-0"
|
||||||
>
|
>
|
||||||
<h1 className="text-2xl font-extrabold">{release.name.main}</h1>
|
<h1 className="text-2xl font-extrabold tracking-tight">{release.name.main}</h1>
|
||||||
{release.name.english && (
|
{release.name.english && (
|
||||||
<p className="mt-0.5 text-sm text-white/40">{release.name.english}</p>
|
<p className="mt-0.5 font-mono text-xs text-white/40">{release.name.english}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-3 flex flex-wrap gap-2 text-xs">
|
<div className="mt-3 flex flex-wrap items-center gap-2 text-xs">
|
||||||
<Chip>{release.type?.description}</Chip>
|
<Chip>{release.type?.description}</Chip>
|
||||||
<Chip>{release.year}</Chip>
|
<Chip>{release.year}</Chip>
|
||||||
<Chip>{release.season?.description}</Chip>
|
<Chip>{release.season?.description}</Chip>
|
||||||
<Chip>{release.age_rating?.label}</Chip>
|
<Chip>{release.age_rating?.label}</Chip>
|
||||||
{release.is_ongoing && <Chip accent>Онгоинг</Chip>}
|
{release.is_ongoing && <Chip accent mono>Онгоинг</Chip>}
|
||||||
{release.episodes_total != null && <Chip>{release.episodes_total} эп.</Chip>}
|
{release.episodes_total != null && <Chip mono>{release.episodes_total} эп.</Chip>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 flex items-center gap-2">
|
<div className="mt-3 flex items-center gap-2">
|
||||||
|
|
@ -203,7 +201,7 @@ export default function TitleDetail() {
|
||||||
<motion.div
|
<motion.div
|
||||||
layoutId="episode-active-bg"
|
layoutId="episode-active-bg"
|
||||||
transition={{ type: 'spring', stiffness: 400, damping: 32 }}
|
transition={{ type: 'spring', stiffness: 400, damping: 32 }}
|
||||||
className="absolute inset-0 bg-accent"
|
className="absolute inset-0 bg-accent-gradient"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="relative flex items-center gap-1.5 font-semibold">
|
<div className="relative flex items-center gap-1.5 font-semibold">
|
||||||
|
|
@ -249,7 +247,7 @@ export default function TitleDetail() {
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => window.anisav.openExternal(t.magnet)}
|
onClick={() => window.anisav.openExternal(t.magnet)}
|
||||||
className="shrink-0 rounded-full bg-accent px-3 py-1 text-xs font-semibold hover:bg-accent-hover"
|
className="shrink-0 rounded-full bg-accent-gradient px-3 py-1 text-xs font-semibold transition-transform hover:scale-105"
|
||||||
>
|
>
|
||||||
Magnet
|
Magnet
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -262,10 +260,20 @@ export default function TitleDetail() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Chip({ children, accent }: { children: React.ReactNode; accent?: boolean }) {
|
function Chip({
|
||||||
|
children,
|
||||||
|
accent,
|
||||||
|
mono,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
accent?: boolean
|
||||||
|
mono?: boolean
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`rounded px-2 py-0.5 font-semibold ${accent ? 'bg-accent' : 'bg-white/10 text-white/70'}`}
|
className={`rounded-full px-2.5 py-1 ${mono ? 'font-mono text-[10px] uppercase tracking-widest' : 'font-semibold'} ${
|
||||||
|
accent ? 'bg-accent-gradient text-white' : 'bg-white/10 text-white/70'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ export default function Wishlist() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-7xl px-6 py-6">
|
<div className="mx-auto max-w-7xl px-6 py-6">
|
||||||
<h1 className="mb-1 text-xl font-bold">Понравилось</h1>
|
<h1 className="mb-1 text-xl font-bold tracking-tight">Понравилось</h1>
|
||||||
<p className="mb-4 text-xs text-white/40">
|
<p className="mb-4 font-mono text-[11px] text-white/40">
|
||||||
Локальный список на этом устройстве — не требует входа в аккаунт AniLiberty
|
Локальный список на этом устройстве — не требует входа в аккаунт AniLiberty
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,22 @@ export default {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
accent: {
|
accent: {
|
||||||
DEFAULT: '#fe3635',
|
DEFAULT: '#FF3D5A',
|
||||||
hover: '#ff5352',
|
2: '#FF8A4C',
|
||||||
|
soft: 'rgba(255,61,90,0.16)',
|
||||||
},
|
},
|
||||||
surface: {
|
surface: {
|
||||||
DEFAULT: '#121212',
|
DEFAULT: '#0A0A13',
|
||||||
raised: '#1a1a1a',
|
raised: '#1C1C2C',
|
||||||
card: '#1e1e1e',
|
card: '#161623',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Inter', 'Roboto', 'sans-serif'],
|
sans: ['Inter', 'Roboto', 'sans-serif'],
|
||||||
|
mono: ['"Space Mono"', 'ui-monospace', 'monospace'],
|
||||||
|
},
|
||||||
|
backgroundImage: {
|
||||||
|
'accent-gradient': 'linear-gradient(135deg, #FF3D5A, #FF8A4C)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue