mirror of
https://github.com/WonderAliceUwU/OmniArr.git
synced 2026-11-11 04:01:01 +00:00
fix: add quality of life
This commit is contained in:
1 parent
c4220b8781
commit
238364146f
2 files changed
+192
-22
No files matched your search
+81
-19
@@ -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
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user