feat: cycling home screen banner #404

This commit is contained in:
ThaUnknown 2024-03-19 22:37:08 +01:00
parent 01e3bb7810
commit d760761bba
3 changed files with 92 additions and 27 deletions

View file

@ -2,13 +2,32 @@
import FullBanner from './FullBanner.svelte' import FullBanner from './FullBanner.svelte'
import SkeletonBanner from './SkeletonBanner.svelte' import SkeletonBanner from './SkeletonBanner.svelte'
export let data export let data
/**
* Shuffles array in place.
* @template T
* @param {T[]} array items An array containing the items.
* @returns {T[]}
*/
function shuffle (array) {
let currentIndex = array.length
let randomIndex
while (currentIndex > 0) {
randomIndex = Math.floor(Math.random() * currentIndex--);
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]
}
return array
}
</script> </script>
<div class='w-full h-450 position-relative gradient'> <div class='w-full h-450 position-relative gradient'>
{#await data} {#await data}
<SkeletonBanner /> <SkeletonBanner />
{:then { data }} {:then { data }}
<FullBanner media={data.Page.media[0]} /> <FullBanner mediaList={shuffle(data.Page.media).slice(0, 5)} />
{/await} {/await}
</div> </div>

View file

@ -2,60 +2,83 @@
import { formatMap, setStatus, playMedia } from '@/modules/anime.js' import { formatMap, setStatus, playMedia } from '@/modules/anime.js'
import { anilistClient } from '@/modules/anilist.js' import { anilistClient } from '@/modules/anilist.js'
import { click } from '@/modules/click.js' import { click } from '@/modules/click.js'
export let media export let mediaList
let current = mediaList[0]
async function toggleStatus () { async function toggleStatus () {
if (!media.mediaListEntry) { if (!current.mediaListEntry) {
// add // add
const res = await setStatus('PLANNING', {}, media) const res = await setStatus('PLANNING', {}, current)
media.mediaListEntry = res.data.SaveMediaListEntry current.mediaListEntry = res.data.SaveMediaListEntry
} else { } else {
// delete // delete
anilistClient.delete({ id: media.mediaListEntry.id }) anilistClient.delete({ id: current.mediaListEntry.id })
media.mediaListEntry = undefined current.mediaListEntry = undefined
} }
} }
function toggleFavourite () { function toggleFavourite () {
anilistClient.favourite({ id: media.id }) anilistClient.favourite({ id: current.id })
media.isFavourite = !media.isFavourite current.isFavourite = !current.isFavourite
}
function currentIndex () {
return mediaList.indexOf(current)
}
function schedule (index) {
return setTimeout(() => {
current = mediaList[index % mediaList.length]
timeout = schedule(index + 1)
}, 15000)
}
let timeout = schedule(currentIndex() + 1)
function setCurrent (media) {
clearTimeout(timeout)
current = media
timeout = schedule(currentIndex() + 1)
} }
</script> </script>
<img src={media.bannerImage || ''} alt='banner' class='img-cover w-full h-full position-absolute' /> {#key current}
<img src={current.bannerImage || ''} alt='banner' class='img-cover w-full h-full position-absolute' />
{/key}
<div class='pl-20 pb-20 justify-content-end d-flex flex-column h-full banner mw-full '> <div class='pl-20 pb-20 justify-content-end d-flex flex-column h-full banner mw-full '>
<div class='text-white font-weight-bold font-size-40 title w-700 mw-full overflow-hidden'> <div class='text-white font-weight-bold font-size-40 title w-700 mw-full overflow-hidden'>
{media.title.userPreferred} {current.title.userPreferred}
</div> </div>
<div class='details text-white text-capitalize pt-15 pb-10 d-flex w-600 mw-full'> <div class='details text-white text-capitalize pt-15 pb-10 d-flex w-600 mw-full'>
<span class='text-nowrap d-flex align-items-center'> <span class='text-nowrap d-flex align-items-center'>
{#if media.format} {#if current.format}
{formatMap[media.format]} {formatMap[current.format]}
{/if} {/if}
</span> </span>
{#if media.episodes && media.episodes !== 1} {#if current.episodes && current.episodes !== 1}
<span class='text-nowrap d-flex align-items-center'> <span class='text-nowrap d-flex align-items-center'>
{#if media.mediaListEntry?.status === 'CURRENT' && media.mediaListEntry?.progress } {#if current.mediaListEntry?.status === 'CURRENT' && current.mediaListEntry?.progress }
{media.mediaListEntry.progress} / {media.episodes} Episodes {current.mediaListEntry.progress} / {current.episodes} Episodes
{:else} {:else}
{media.episodes} Episodes {current.episodes} Episodes
{/if} {/if}
</span> </span>
{:else if media.duration} {:else if current.duration}
<span class='text-nowrap d-flex align-items-center'> <span class='text-nowrap d-flex align-items-center'>
{media.duration + ' Minutes'} {current.duration + ' Minutes'}
</span> </span>
{/if} {/if}
{#if media.season || media.seasonYear} {#if current.season || current.seasonYear}
<span class='text-nowrap d-flex align-items-center'> <span class='text-nowrap d-flex align-items-center'>
{[media.season?.toLowerCase(), media.seasonYear].filter(s => s).join(' ')} {[current.season?.toLowerCase(), current.seasonYear].filter(s => s).join(' ')}
</span> </span>
{/if} {/if}
</div> </div>
<div class='text-muted description overflow-hidden w-600 mw-full'> <div class='text-muted description overflow-hidden w-600 mw-full'>
{media.description?.replace(/<[^>]*>/g, '')} {current.description?.replace(/<[^>]*>/g, '')}
</div> </div>
<div class='details text-white text-capitalize pt-15 pb-10 d-flex w-600 mw-full'> <div class='details text-white text-capitalize pt-15 pb-10 d-flex w-600 mw-full'>
{#each media.genres as genre} {#each current.genres as genre}
<span class='text-nowrap d-flex align-items-center'> <span class='text-nowrap d-flex align-items-center'>
{genre} {genre}
</span> </span>
@ -63,19 +86,42 @@
</div> </div>
<div class='d-flex flex-row pb-20 w-600 mw-full'> <div class='d-flex flex-row pb-20 w-600 mw-full'>
<button class='btn bg-dark-light px-20 shadow-none border-0' <button class='btn bg-dark-light px-20 shadow-none border-0'
use:click={() => playMedia(media)}> use:click={() => playMedia(current)}>
Watch Now Watch Now
</button> </button>
<button class='btn bg-dark-light btn-square ml-10 material-symbols-outlined font-size-16 shadow-none border-0' class:filled={media.isFavourite} use:click={toggleFavourite}> <button class='btn bg-dark-light btn-square ml-10 material-symbols-outlined font-size-16 shadow-none border-0' class:filled={current.isFavourite} use:click={toggleFavourite}>
favorite favorite
</button> </button>
<button class='btn bg-dark-light btn-square ml-10 material-symbols-outlined font-size-16 shadow-none border-0' class:filled={media.mediaListEntry} use:click={toggleStatus}> <button class='btn bg-dark-light btn-square ml-10 material-symbols-outlined font-size-16 shadow-none border-0' class:filled={current.mediaListEntry} use:click={toggleStatus}>
bookmark bookmark
</button> </button>
</div> </div>
<div class='d-flex'>
{#each mediaList as media}
{@const active = current === media}
<div class='rounded bg-dark-light mr-10 progress-badge overflow-hidden pointer' class:active style='height: 3px;' style:width={active ? '5rem' : '2.7rem'} use:click={() => setCurrent(media)}>
<div class='progress-content h-full' class:bg-white={active} />
</div>
{/each}
</div>
</div> </div>
<style> <style>
.progress-badge {
transition: width .8s ease;
}
.progress-badge.active .progress-content {
animation: fill 15s linear;
}
@keyframes fill {
from {
width: 0;
}
to {
width: 100%;
}
}
.w-700 { .w-700 {
width: 70rem width: 70rem
} }

View file

@ -3,7 +3,7 @@
import { alToken, settings } from '@/modules/settings.js' import { alToken, settings } from '@/modules/settings.js'
import { anilistClient, currentSeason, currentYear } from '@/modules/anilist.js' import { anilistClient, currentSeason, currentYear } from '@/modules/anilist.js'
const bannerData = anilistClient.search({ method: 'Search', sort: 'POPULARITY_DESC', perPage: 1, onList: false, season: currentSeason, year: currentYear }) const bannerData = anilistClient.search({ method: 'Search', sort: 'POPULARITY_DESC', perPage: 15, onList: false, season: currentSeason, year: currentYear })
const manager = new SectionsManager() const manager = new SectionsManager()