fix: add quality of life

This commit is contained in:
WonderAliceUwU committed 2026-07-09 02:15:14 +02:00
1 parent c4220b8781
commit 238364146f
2 files changed
+192 -22

No files matched your search

+81 -19
View File
@@ -33,6 +33,7 @@ type EntryTransitionState = 'idle' | 'forward-enter' | 'forward-exit' | 'back-en
const MAX_CACHED_HUB_PAGES = 3
type ModalOriginRect = { left: number; top: number; width: number; height: number }
type SelectedCardState = { card: MediaCard; originRect: ModalOriginRect | null }
const CAROUSEL_SCROLL_DISTANCE_RATIO = 0.84
const EPISODE_SCROLL_DISTANCE_RATIO = 0.72
const SEASON_DOMINANCE_SWITCH_RATIO = 0.12
const SEASON_DOMINANCE_SWITCH_MIN_PX = 56
@@ -1147,17 +1148,77 @@ function SectionCarousel(props: {
onOpenCard: (card: MediaCard, originRect?: ModalOriginRect | null) => void
busy: boolean
}) {
const trackRef = useRef<HTMLDivElement | null>(null)
const [scrollState, setScrollState] = useState({ canScrollLeft: false, canScrollRight: false })
useEffect(() => {
const track = trackRef.current
if (!track) {
setScrollState({ canScrollLeft: false, canScrollRight: false })
return
}
const syncScrollState = () => {
const maxScrollLeft = track.scrollWidth - track.clientWidth
setScrollState({
canScrollLeft: track.scrollLeft > 8,
canScrollRight: maxScrollLeft - track.scrollLeft > 8,
})
}
syncScrollState()
track.addEventListener('scroll', syncScrollState, { passive: true })
window.addEventListener('resize', syncScrollState)
return () => {
track.removeEventListener('scroll', syncScrollState)
window.removeEventListener('resize', syncScrollState)
}
}, [props.section.items.length])
if (!props.section.items.length) return null
function scrollTrack(direction: 'left' | 'right') {
const track = trackRef.current
if (!track) return
const distance = Math.max(track.clientWidth * CAROUSEL_SCROLL_DISTANCE_RATIO, 320)
track.scrollBy({
left: direction === 'left' ? -distance : distance,
behavior: 'smooth',
})
}
return (
<section className={props.section.id === 'continue' ? 'carousel-row continue-row' : 'carousel-row'}>
<div className="carousel-title">{props.section.title}</div>
<div className="carousel-section">
<div className="carousel-track">
<div
className={[
'carousel-section',
scrollState.canScrollLeft ? 'show-left-fade has-left-nav' : '',
scrollState.canScrollRight ? 'show-right-fade' : '',
].filter(Boolean).join(' ')}
>
{scrollState.canScrollLeft ? (
<button
aria-label={`Scroll ${props.section.title} left`}
className="carousel-nav left"
onClick={() => scrollTrack('left')}
type="button"
/>
) : null}
<div className="carousel-track" ref={trackRef}>
{props.section.items.map((card) => (
<MediaTile busy={props.busy} card={card} isContinueWatching={props.section.id === 'continue'} key={card.id} onOpenCard={props.onOpenCard} onRequest={props.onRequest} styleType={props.section.style || 'landscape'} />
))}
</div>
<button
aria-label={`Scroll ${props.section.title} right`}
className="carousel-nav right"
disabled={!scrollState.canScrollRight}
onClick={() => scrollTrack('right')}
type="button"
/>
</div>
</section>
)
@@ -1807,14 +1868,13 @@ function TitleDetailsModal(props: {
<div
className="title-season-strip"
>
{hasPreviousSeason ? (
<button
aria-label="Previous season"
className="title-season-nav"
onClick={() => changeSeason(-1)}
type="button"
/>
) : null}
<button
aria-label="Previous season"
className="title-season-nav"
disabled={!hasPreviousSeason}
onClick={() => changeSeason(-1)}
type="button"
/>
{details.seasons.length ? (
<label className="title-season-selector">
<span className="title-season-kicker">Season</span>
@@ -1877,7 +1937,7 @@ function TitleDetailsModal(props: {
{canDeleteSeasonAction ? (
<div className="title-season-divider title-season-action-divider" aria-hidden="true" />
) : null}
{canDeleteSeasonAction ? (
{canDeleteSeasonAction ? (
<button
aria-label="Delete season files"
className="title-season-action title-season-action-delete"
@@ -1888,6 +1948,16 @@ function TitleDetailsModal(props: {
<TrashGlyph />
</button>
) : null}
{hasNextSeason ? (
<>
<button
aria-label="Next season"
className="title-season-nav next"
onClick={() => changeSeason(1)}
type="button"
/>
</>
) : null}
</div>
) : null}
<div className="title-season-strip-spacer" />
@@ -1901,14 +1971,6 @@ function TitleDetailsModal(props: {
<span>Play</span>
</ActionButton>
) : null}
{hasNextSeason ? (
<button
aria-label="Next season"
className="title-season-nav next"
onClick={() => changeSeason(1)}
type="button"
/>
) : null}
</div>
</div>
<div
+111 -3
View File
@@ -665,17 +665,50 @@ img {
}
.carousel-section {
position: relative;
display: grid;
min-width: 0;
margin: 0 1.25rem;
padding: 1rem 0 1.15rem;
padding: 1rem 0 1rem;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 1.9rem;
border-radius: 2.5rem;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.015));
box-shadow:
0 1.4rem 2.8rem rgba(0, 0, 0, 0.22),
inset 0 1px 0 rgba(255, 255, 255, 0.03);
overflow: hidden;
}
.carousel-section::before,
.carousel-section::after {
content: "";
position: absolute;
top: 0;
bottom: 0;
width: 5.4rem;
z-index: 5;
pointer-events: none;
opacity: 0;
transition: opacity 160ms ease;
}
.carousel-section::before {
left: 0;
background: linear-gradient(90deg, rgba(9, 11, 16, 0.98) 0%, rgba(9, 11, 16, 0.76) 48%, rgba(9, 11, 16, 0) 100%);
}
.carousel-section::after {
right: 0;
background: linear-gradient(270deg, rgba(9, 11, 16, 0.98) 0%, rgba(9, 11, 16, 0.76) 48%, rgba(9, 11, 16, 0) 100%);
}
.carousel-section.show-left-fade::before {
opacity: 1;
}
.carousel-section.show-right-fade::after {
opacity: 1;
}
.carousel-title {
@@ -690,14 +723,68 @@ img {
gap: 1rem;
overflow-x: auto;
overflow-y: visible;
padding: 0.45rem 1.25rem 0.7rem;
padding: 0.45rem 4.1rem 0.85rem 1.25rem;
scrollbar-width: none;
}
.carousel-section.has-left-nav .carousel-track {
padding-left: 4.1rem;
}
.carousel-track::-webkit-scrollbar {
display: none;
}
.carousel-nav {
position: absolute;
top: 50%;
z-index: 6;
width: 2rem;
height: 3.5rem;
border: 0;
padding: 0;
background: transparent;
color: inherit;
transform: translateY(-50%);
transition: transform 160ms ease, opacity 160ms ease;
}
.carousel-nav::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 0.78rem;
height: 0.78rem;
border-top: 2px solid rgba(244, 247, 255, 0.92);
border-right: 2px solid rgba(244, 247, 255, 0.92);
}
.carousel-nav.left {
left: 0.85rem;
}
.carousel-nav.left::before {
transform: translate(-35%, -50%) rotate(-135deg);
}
.carousel-nav.right {
right: 0.85rem;
}
.carousel-nav.right::before {
transform: translate(-65%, -50%) rotate(45deg);
}
.carousel-nav:hover,
.carousel-nav:focus-visible {
transform: translateY(-50%) scale(1.08);
}
.carousel-nav:disabled {
opacity: 0.24;
}
@media (max-width: 900px) {
.hero-showcase {
aspect-ratio: 16 / 10;
@@ -705,6 +792,27 @@ img {
max-height: none;
border-radius: 0 0 1.25rem 1.25rem;
}
.carousel-track {
padding-right: 3.35rem;
}
.carousel-section.has-left-nav .carousel-track {
padding-left: 3.35rem;
}
.carousel-nav {
width: 1.85rem;
height: 3.15rem;
}
.carousel-nav.left {
left: 0.7rem;
}
.carousel-nav.right {
right: 0.7rem;
}
}
.media-tile {