mirror of
https://github.com/NoCrypt/migu.git
synced 2026-03-11 17:45:32 +00:00
feat(web): smooth scroll, about section, cards, scroll animations
This commit is contained in:
parent
2ef05e3d4e
commit
15beef8963
11 changed files with 817 additions and 5 deletions
|
|
@ -268,6 +268,9 @@ importers:
|
|||
quartermoon:
|
||||
specifier: ^1.2.3
|
||||
version: 1.2.3
|
||||
simple-store-svelte:
|
||||
specifier: ^1.0.1
|
||||
version: 1.0.1
|
||||
devDependencies:
|
||||
'@sveltejs/adapter-auto':
|
||||
specifier: ^2.1.1
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@
|
|||
"@fontsource-variable/material-symbols-outlined": "^5.0.16",
|
||||
"@fontsource-variable/nunito": "^5.0.16",
|
||||
"@fontsource/roboto": "^5.0.8",
|
||||
"quartermoon": "^1.2.3"
|
||||
"quartermoon": "^1.2.3",
|
||||
"simple-store-svelte": "^1.0.1"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,14 @@
|
|||
<div class='d-flex position-relative justify-content-center align-items-center hero flex-column'>
|
||||
<script>
|
||||
let hero
|
||||
function about () {
|
||||
document.querySelector('#about').scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
setTimeout(() => {
|
||||
hero.parentNode.dispatchEvent(new PointerEvent('pointerup'))
|
||||
}, 500)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class='d-flex position-relative justify-content-center align-items-center hero flex-column' bind:this={hero}>
|
||||
<div class='position-absolute ghost text-nowrap font-weight-semi-bold'>
|
||||
MIRU MIRU MIRU MIRU MIRU MIRU MIRU MIRU MIRU MIRU MIRU MIRU MIRU
|
||||
</div>
|
||||
|
|
@ -10,7 +20,7 @@
|
|||
<a href='/download' class='btn btn-danger btn-lg mr-20'>
|
||||
Download
|
||||
</a>
|
||||
<a class='btn bg-dark-light btn-lg ml-20' href='#about'>Learn More</a>
|
||||
<button class='btn bg-dark-light btn-lg ml-20' on:click={about}>Learn More</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
145
web/src/lib/components/PreviewCard.svelte
Normal file
145
web/src/lib/components/PreviewCard.svelte
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
<script>
|
||||
import { formatMap } from './anime.js'
|
||||
import { click } from '@/modules/click.js'
|
||||
export let media
|
||||
|
||||
let hide = true
|
||||
|
||||
function getPlayButtonText (media) {
|
||||
if (media.mediaListEntry) {
|
||||
const { status, progress } = media.mediaListEntry
|
||||
if (progress) {
|
||||
if (status === 'COMPLETED') {
|
||||
return 'Rewatch Now'
|
||||
} else {
|
||||
return 'Continue Now'
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'Watch Now'
|
||||
}
|
||||
const playButtonText = getPlayButtonText(media)
|
||||
|
||||
function volume (video) {
|
||||
video.volume = 0.1
|
||||
}
|
||||
let muted = true
|
||||
function toggleMute () {
|
||||
muted = !muted
|
||||
}
|
||||
const noop = () => {}
|
||||
</script>
|
||||
|
||||
<div class='position-absolute w-350 h-400 absolute-container top-0 bottom-0 m-auto bg-dark-light z-30 rounded overflow-hidden pointer'>
|
||||
<div class='banner position-relative overflow-hidden bg-black'>
|
||||
<img src={media.bannerImage || ' '} alt='banner' class='img-cover w-full h-full' />
|
||||
{#if media.trailer?.id}
|
||||
<div class='material-symbols-outlined filled position-absolute z-10 top-0 right-0 p-15 font-size-22' class:d-none={hide} use:click={toggleMute}>{muted ? 'volume_off' : 'volume_up'}</div>
|
||||
<!-- for now we use some invidious instance, would be nice to somehow get these links outselves, this redirects straight to some google endpoint -->
|
||||
<!-- eslint-disable-next-line svelte/valid-compile -->
|
||||
<video src={`https://yewtu.be/latest_version?id=${media.trailer.id}&itag=18`}
|
||||
class='w-full position-absolute left-0'
|
||||
class:d-none={hide}
|
||||
playsinline
|
||||
preload='none'
|
||||
loop
|
||||
use:volume
|
||||
bind:muted
|
||||
on:loadeddata={() => { hide = false }}
|
||||
autoplay />
|
||||
{/if}
|
||||
</div>
|
||||
<div class='w-full px-20'>
|
||||
<div class='font-size-24 font-weight-bold text-truncate d-inline-block w-full text-white' title={media.title.userPreferred}>
|
||||
{media.title.userPreferred}
|
||||
</div>
|
||||
<div class='d-flex flex-row pt-5'>
|
||||
<button class='btn btn-secondary flex-grow-1 text-dark font-weight-bold shadow-none border-0 d-flex align-items-center justify-content-center'
|
||||
use:click={noop}
|
||||
disabled={media.status === 'NOT_YET_RELEASED'}>
|
||||
<span class='material-symbols-outlined font-size-20 filled pr-10'>
|
||||
play_arrow
|
||||
</span>
|
||||
{playButtonText}
|
||||
</button>
|
||||
<button class='btn btn-square ml-10 material-symbols-outlined font-size-16 shadow-none border-0' class:filled={media.isFavourite} use:click={noop}>
|
||||
favorite
|
||||
</button>
|
||||
<button class='btn btn-square ml-10 material-symbols-outlined font-size-16 shadow-none border-0' class:filled={media.mediaListEntry} use:click={noop}>
|
||||
bookmark
|
||||
</button>
|
||||
</div>
|
||||
<div class='details text-white text-capitalize pt-15 pb-10 d-flex'>
|
||||
<span class='text-nowrap d-flex align-items-center'>
|
||||
{#if media.format}
|
||||
{formatMap[media.format]}
|
||||
{/if}
|
||||
</span>
|
||||
{#if media.episodes && media.episodes !== 1}
|
||||
<span class='text-nowrap d-flex align-items-center'>
|
||||
{#if media.mediaListEntry?.status === 'CURRENT' && media.mediaListEntry?.progress }
|
||||
{media.mediaListEntry.progress} / {media.episodes} Episodes
|
||||
{:else}
|
||||
{media.episodes} Episodes
|
||||
{/if}
|
||||
</span>
|
||||
{:else if media.duration}
|
||||
<span class='text-nowrap d-flex align-items-center'>
|
||||
{media.duration + ' Minutes'}
|
||||
</span>
|
||||
{/if}
|
||||
{#if media.season || media.seasonYear}
|
||||
<span class='text-nowrap d-flex align-items-center'>
|
||||
{[media.season?.toLowerCase(), media.seasonYear].filter(s => s).join(' ')}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class='w-full h-full text-muted description overflow-hidden'>
|
||||
{media.description?.replace(/<[^>]*>/g, '')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.description {
|
||||
display: -webkit-box !important;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.details span + span::before {
|
||||
content: '•';
|
||||
padding: 0 .5rem;
|
||||
font-size: .6rem;
|
||||
align-self: center;
|
||||
white-space: normal;
|
||||
color: var(--dm-muted-text-color) !important;
|
||||
}
|
||||
.banner {
|
||||
height: 45%
|
||||
}
|
||||
.banner::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0 ; bottom: 0;
|
||||
width: 100%; height: 100% ;
|
||||
background: linear-gradient(180deg, #0000 0%, #25292f00 80%, #25292fe3 95%, #25292f 100%);
|
||||
}
|
||||
@keyframes load-in {
|
||||
from {
|
||||
bottom: -1.2rem;
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
to {
|
||||
bottom: 0;
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.absolute-container {
|
||||
animation: 0.3s ease 0s 1 load-in;
|
||||
left: -100%;
|
||||
right: -100%;
|
||||
}
|
||||
</style>
|
||||
66
web/src/lib/components/SmallCard.svelte
Normal file
66
web/src/lib/components/SmallCard.svelte
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<script>
|
||||
import PreviewCard from './PreviewCard.svelte'
|
||||
import { formatMap, statusColorMap } from './anime.js'
|
||||
import { hoverClick } from '@/modules/click.js'
|
||||
|
||||
export let media
|
||||
export let preview = false
|
||||
|
||||
const noop = () => {}
|
||||
function setHoverState (state) {
|
||||
preview = state
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class='d-flex p-20 position-relative first-check' use:hoverClick={[noop, setHoverState]}>
|
||||
{#if preview}
|
||||
<PreviewCard {media} />
|
||||
{/if}
|
||||
<div class='item d-flex flex-column h-full pointer content-visibility-auto'>
|
||||
<img loading='lazy' src={media.coverImage.extraLarge || ''} alt='cover' class='cover-img w-full rounded' style:--color={media.coverImage.color || '#1890ff'} />
|
||||
<div class='text-white font-weight-very-bold font-size-16 pt-15 title overflow-hidden'>
|
||||
{#if media.mediaListEntry?.status}
|
||||
<div style:--statusColor={statusColorMap[media.mediaListEntry.status]} class='list-status-circle d-inline-flex overflow-hidden mr-5' title={media.mediaListEntry.status} />
|
||||
{/if}
|
||||
{media.title.userPreferred}
|
||||
</div>
|
||||
<div class='d-flex flex-row mt-auto pt-10 font-weight-medium justify-content-between w-full text-muted'>
|
||||
<div class='d-flex align-items-center' style='margin-left: -3px'>
|
||||
<span class='material-symbols-outlined font-size-24 pr-5'>calendar_month</span>
|
||||
{media.seasonYear || 'N/A'}
|
||||
</div>
|
||||
<div class='d-flex align-items-center'>
|
||||
{formatMap[media.format]}
|
||||
<span class='material-symbols-outlined font-size-24 pl-5'>monitor</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.first-check:first-child :global(.absolute-container) {
|
||||
left: -48% !important
|
||||
}
|
||||
.title {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
img {
|
||||
height: 27rem;
|
||||
}
|
||||
.item {
|
||||
animation: 0.3s ease 0s 1 load-in;
|
||||
width: 19rem;
|
||||
contain-intrinsic-height: 36.7rem;
|
||||
}
|
||||
.cover-img {
|
||||
background-color: var(--color) !important;
|
||||
}
|
||||
.list-status-circle {
|
||||
background: var(--statusColor);
|
||||
height: 1.1rem;
|
||||
width: 1.1rem;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
20
web/src/lib/components/anime.js
Normal file
20
web/src/lib/components/anime.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
export const formatMap = {
|
||||
TV: 'TV Series',
|
||||
TV_SHORT: 'TV Short',
|
||||
MOVIE: 'Movie',
|
||||
SPECIAL: 'Special',
|
||||
OVA: 'OVA',
|
||||
ONA: 'ONA',
|
||||
MUSIC: 'Music',
|
||||
undefined: 'N/A',
|
||||
null: 'N/A'
|
||||
}
|
||||
|
||||
export const statusColorMap = {
|
||||
CURRENT: 'rgb(61,180,242)',
|
||||
PLANNING: 'rgb(247,154,99)',
|
||||
COMPLETED: 'rgb(123,213,85)',
|
||||
PAUSED: 'rgb(250,122,122)',
|
||||
REPEATING: '#3baeea',
|
||||
DROPPED: 'rgb(232,93,117)'
|
||||
}
|
||||
|
|
@ -9,4 +9,8 @@ hr {
|
|||
|
||||
.h-vh-half {
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
:root {
|
||||
--container-xl-max-width: 150rem
|
||||
}
|
||||
461
web/src/lib/dummyData.js
Normal file
461
web/src/lib/dummyData.js
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
export const cards = [
|
||||
{
|
||||
id: 154587,
|
||||
idMal: 52991,
|
||||
title: {
|
||||
romaji: 'Sousou no Frieren',
|
||||
english: 'Frieren: Beyond Journey’s End',
|
||||
native: '葬送のフリーレン',
|
||||
userPreferred: 'Frieren: Beyond Journey’s End'
|
||||
},
|
||||
description: 'The adventure is over but life goes on for an elf mage just beginning to learn what living is all about. Elf mage Frieren and her courageous fellow adventurers have defeated the Demon King and brought peace to the land. But Frieren will long outlive the rest of her former party. How will she come to understand what life means to the people around her? Decades after their victory, the funeral of one her friends confronts Frieren with her own near immortality. Frieren sets out to fulfill the last wishes of her comrades and finds herself beginning a new adventure…\n<br><br>\n(Source: Crunchyroll)',
|
||||
season: 'FALL',
|
||||
seasonYear: 2023,
|
||||
format: 'TV',
|
||||
status: 'RELEASING',
|
||||
episodes: 28,
|
||||
duration: 24,
|
||||
averageScore: 89,
|
||||
genres: [
|
||||
'Adventure',
|
||||
'Drama',
|
||||
'Fantasy'
|
||||
],
|
||||
isFavourite: false,
|
||||
coverImage: {
|
||||
extraLarge: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx154587-n1fmjRv4JQUd.jpg',
|
||||
medium: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx154587-n1fmjRv4JQUd.jpg',
|
||||
color: '#d6f1c9'
|
||||
},
|
||||
countryOfOrigin: 'JP',
|
||||
isAdult: false,
|
||||
bannerImage: 'https://s4.anilist.co/file/anilistcdn/media/anime/banner/154587-ivXNJ23SM1xB.jpg',
|
||||
synonyms: [
|
||||
'Frieren at the Funeral',
|
||||
'장송의 프리렌',
|
||||
'Frieren: Oltre la Fine del Viaggio',
|
||||
'คำอธิษฐานในวันที่จากลา Frieren',
|
||||
'Frieren e a Jornada para o Além',
|
||||
'Frieren – Nach dem Ende der Reise',
|
||||
'葬送的芙莉蓮',
|
||||
'Frieren: Más allá del final del viaje',
|
||||
'Frieren en el funeral',
|
||||
'Sōsō no Furīren',
|
||||
'Frieren. U kresu drogi',
|
||||
'Frieren - Pháp sư tiễn táng',
|
||||
'Фрирен, провожающая в последний путь'
|
||||
],
|
||||
nextAiringEpisode: {
|
||||
timeUntilAiring: 317046,
|
||||
episode: 14
|
||||
},
|
||||
startDate: {
|
||||
year: 2023,
|
||||
month: 9,
|
||||
day: 29
|
||||
},
|
||||
trailer: {
|
||||
id: 'qgQunxD0qCk',
|
||||
site: 'youtube'
|
||||
},
|
||||
source: 'MANGA',
|
||||
studios: {
|
||||
nodes: [
|
||||
{
|
||||
name: 'MADHOUSE'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 145064,
|
||||
idMal: 51009,
|
||||
title: {
|
||||
romaji: 'Jujutsu Kaisen 2nd Season',
|
||||
english: 'JUJUTSU KAISEN Season 2',
|
||||
native: '呪術廻戦 第2期',
|
||||
userPreferred: 'JUJUTSU KAISEN Season 2'
|
||||
},
|
||||
description: 'The second season of <i>Jujutsu Kaisen</i>.<br>\n<br>\nThe past comes to light when second-year students Satoru Gojou and Suguru Getou are tasked with escorting young Riko Amanai to Master Tengen. But when a non-sorcerer user tries to kill them, their mission to protect the Star Plasma Vessel threatens to turn them into bitter enemies and cement their destinies—one as the world’s strongest sorcerer, and the other its most twisted curse user!<br>\n<br>\n(Source: Crunchyroll)',
|
||||
season: 'SUMMER',
|
||||
seasonYear: 2023,
|
||||
format: 'TV',
|
||||
status: 'RELEASING',
|
||||
episodes: 23,
|
||||
duration: 24,
|
||||
averageScore: 87,
|
||||
genres: [
|
||||
'Action',
|
||||
'Drama',
|
||||
'Supernatural'
|
||||
],
|
||||
isFavourite: true,
|
||||
coverImage: {
|
||||
extraLarge: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx145064-5fa4ZBbW4dqA.jpg',
|
||||
medium: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx145064-5fa4ZBbW4dqA.jpg',
|
||||
color: '#5d86e4'
|
||||
},
|
||||
countryOfOrigin: 'JP',
|
||||
isAdult: false,
|
||||
bannerImage: 'https://s4.anilist.co/file/anilistcdn/media/anime/banner/145064-S7qAgxf6kMrW.jpg',
|
||||
synonyms: [
|
||||
'呪術廻戦 懐玉・玉折/渋谷事変 ',
|
||||
'Jujutsu Kaisen: Kaigyoku Gyokusetsu / Shibuya Jihen',
|
||||
'Jujutsu Kaisen: Hidden Inventory / Premature Death',
|
||||
'JJK2',
|
||||
'咒術迴戰 第二季',
|
||||
'มหาเวทย์ผนึกมาร ภาค 2 ',
|
||||
'咒术回战 2',
|
||||
'2جوجوتسو كايسن '
|
||||
],
|
||||
nextAiringEpisode: {
|
||||
timeUntilAiring: 234006,
|
||||
episode: 20
|
||||
},
|
||||
startDate: {
|
||||
year: 2023,
|
||||
month: 7,
|
||||
day: 6
|
||||
},
|
||||
trailer: {
|
||||
id: 'O6qVieflwqs',
|
||||
site: 'youtube'
|
||||
},
|
||||
mediaListEntry: {
|
||||
progress: 19,
|
||||
repeat: 0,
|
||||
status: 'CURRENT',
|
||||
score: 0
|
||||
},
|
||||
source: 'MANGA',
|
||||
studios: {
|
||||
nodes: [
|
||||
{
|
||||
name: 'MAPPA'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 145139,
|
||||
idMal: 51019,
|
||||
title: {
|
||||
romaji: 'Kimetsu no Yaiba: Katanakaji no Sato-hen',
|
||||
english: 'Demon Slayer: Kimetsu no Yaiba Swordsmith Village Arc',
|
||||
native: '鬼滅の刃 刀鍛冶の里編',
|
||||
userPreferred: 'Demon Slayer: Kimetsu no Yaiba Swordsmith Village Arc'
|
||||
},
|
||||
description: 'Adaptation of the Swordsmith Village Arc.<br>\n<br>\nTanjiro’s journey leads him to the Swordsmith Village, where he reunites with two Hashira, members of the Demon Slayer Corps’ highest-ranking swordsmen - Mist Hashira Muichiro Tokito and Love Hashira Mitsuri Kanroji. With the shadows of demons lurking near, a new battle begins for Tanjiro and his comrades.\n<br><br>\n<i>Notes:<br>\n• The first episode has a runtime of ~49 minutes, and received an early premiere in cinemas worldwide as part of a special screening alongside the final two episodes of Kimetsu no Yaiba: Yuukaku-hen.<br>\n• The final episode has a runtime of ~52 minutes. </i>',
|
||||
season: 'SPRING',
|
||||
seasonYear: 2023,
|
||||
format: 'TV',
|
||||
status: 'FINISHED',
|
||||
episodes: 11,
|
||||
duration: 24,
|
||||
averageScore: 83,
|
||||
genres: [
|
||||
'Action',
|
||||
'Adventure',
|
||||
'Drama',
|
||||
'Fantasy',
|
||||
'Supernatural'
|
||||
],
|
||||
isFavourite: false,
|
||||
coverImage: {
|
||||
extraLarge: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx145139-rRimpHGWLhym.png',
|
||||
medium: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx145139-rRimpHGWLhym.png',
|
||||
color: '#1aaed6'
|
||||
},
|
||||
countryOfOrigin: 'JP',
|
||||
isAdult: false,
|
||||
bannerImage: 'https://s4.anilist.co/file/anilistcdn/media/anime/banner/145139-V01Prh6UzfRk.jpg',
|
||||
synonyms: [
|
||||
'KnY 3',
|
||||
'ดาบพิฆาตอสูร ภาค 3 บทหมู่บ้านช่างตีดาบ',
|
||||
'Demon Slayer: Kimetsu no Yaiba - Le village des forgerons',
|
||||
'Истребитель демонов: Kimetsu no Yaiba. Деревня кузнецов'
|
||||
],
|
||||
nextAiringEpisode: null,
|
||||
startDate: {
|
||||
year: 2023,
|
||||
month: 4,
|
||||
day: 9
|
||||
},
|
||||
trailer: {
|
||||
id: 'a9tq0aS5Zu8',
|
||||
site: 'youtube'
|
||||
},
|
||||
mediaListEntry: {
|
||||
progress: 11,
|
||||
repeat: 0,
|
||||
status: 'COMPLETED',
|
||||
score: 0
|
||||
},
|
||||
source: 'MANGA',
|
||||
studios: {
|
||||
nodes: [
|
||||
{
|
||||
name: 'ufotable'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 150672,
|
||||
idMal: 52034,
|
||||
title: {
|
||||
romaji: '[Oshi no Ko]',
|
||||
english: 'Oshi No Ko',
|
||||
native: '【推しの子】',
|
||||
userPreferred: 'Oshi No Ko'
|
||||
},
|
||||
description: 'When a pregnant young starlet appears in Gorou Amemiya’s countryside medical clinic, the doctor takes it upon himself to safely (and secretly) deliver Ai Hoshino’s child so she can make a scandal-free return to the stage. But no good deed goes unpunished, and on the eve of her delivery, he finds himself slain at the hands of Ai’s deluded stalker — and subsequently reborn as Ai’s child, Aquamarine Hoshino! The glitz and glamor of showbiz hide the dark underbelly of the entertainment industry, threatening to dull the shine of his favorite star. Can he help his new mother rise to the top of the charts? And what will he do when unthinkable disaster strikes? <br>\n<br>\n(Source: HIDIVE)\n<br><br>\n\n<i>Note: Episode 1<b>【推しの子】Mother and Children</b> was pre-screened in advance in Japanese theaters on March 17, 2023. The regular TV broadcast began on April 12, 2023. The first episode has an extended runtime of ~82 minutes.</i>',
|
||||
season: 'SPRING',
|
||||
seasonYear: 2023,
|
||||
format: 'TV',
|
||||
status: 'FINISHED',
|
||||
episodes: 11,
|
||||
duration: 24,
|
||||
averageScore: 86,
|
||||
genres: [
|
||||
'Drama',
|
||||
'Mystery',
|
||||
'Psychological',
|
||||
'Supernatural'
|
||||
],
|
||||
isFavourite: false,
|
||||
coverImage: {
|
||||
extraLarge: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx150672-2WWJVXIAOG11.png',
|
||||
medium: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx150672-2WWJVXIAOG11.png',
|
||||
color: '#ff35c9'
|
||||
},
|
||||
countryOfOrigin: 'JP',
|
||||
isAdult: false,
|
||||
bannerImage: 'https://s4.anilist.co/file/anilistcdn/media/anime/banner/150672-ISwoA0eS722H.jpg',
|
||||
synonyms: [
|
||||
'Favorite Girl',
|
||||
"My Idol's Child",
|
||||
'[Mein*Star]',
|
||||
'เกิดใหม่เป็นลูกโอชิ',
|
||||
'Anak Idola',
|
||||
'【OSHI NO KO】',
|
||||
'【推しの子】Mother and Children',
|
||||
'[Oshi no Ko] Mother and Children',
|
||||
'我推的孩子',
|
||||
'【최애의 아이】'
|
||||
],
|
||||
nextAiringEpisode: null,
|
||||
startDate: {
|
||||
year: 2023,
|
||||
month: 4,
|
||||
day: 12
|
||||
},
|
||||
trailer: {
|
||||
id: 'gKWEUJ4r5do',
|
||||
site: 'youtube'
|
||||
},
|
||||
streamingEpisodes: [],
|
||||
mediaListEntry: {
|
||||
progress: 11,
|
||||
repeat: 0,
|
||||
status: 'COMPLETED',
|
||||
score: 0
|
||||
},
|
||||
source: 'MANGA',
|
||||
studios: {
|
||||
nodes: [
|
||||
{
|
||||
name: 'Doga Kobo'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 151807,
|
||||
idMal: 52299,
|
||||
title: {
|
||||
romaji: 'Ore dake Level Up na Ken',
|
||||
english: 'Solo Leveling',
|
||||
native: '俺だけレベルアップな件',
|
||||
userPreferred: 'Solo Leveling'
|
||||
},
|
||||
description: "It's been over a decade since the sudden appearance of the \"gates\"—the paths that connect our world with a different dimension. Since then, certain humans have awakened to supernatural powers. We call these individuals \"hunters.\" Hunters make their living by using their powers to conquer dungeons inside the gates. In this world of tough customers, the low-ranked hunter Shun Mizushino is known as \"the weakest hunter of all mankind.\" One day, Shun gets fatally injured when he runs into high-rank double dungeons hidden within a low-rank dungeon. Just then, a mysterious quest window appears in front of him. On the verge of death, Shun decides to accept the quest and starts leveling up... while the others aren't. <br>\n<br>\n(Source: Anime News Network, edited) <br><br>\n\n<i> Note: Announced at the Crunchyroll Anime Expo 2022 Industry Panel.",
|
||||
season: 'WINTER',
|
||||
seasonYear: 2024,
|
||||
format: 'TV',
|
||||
status: 'NOT_YET_RELEASED',
|
||||
episodes: null,
|
||||
duration: null,
|
||||
averageScore: null,
|
||||
genres: [
|
||||
'Action',
|
||||
'Adventure',
|
||||
'Fantasy'
|
||||
],
|
||||
isFavourite: false,
|
||||
coverImage: {
|
||||
extraLarge: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx151807-m1gX3iwfIsLu.png',
|
||||
medium: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx151807-m1gX3iwfIsLu.png',
|
||||
color: '#28aef1'
|
||||
},
|
||||
countryOfOrigin: 'JP',
|
||||
isAdult: false,
|
||||
bannerImage: 'https://s4.anilist.co/file/anilistcdn/media/anime/banner/151807-37yfQA3ym8PA.jpg',
|
||||
synonyms: [
|
||||
'나 혼자만 레벨업',
|
||||
'Na Honjaman Level Up'
|
||||
],
|
||||
nextAiringEpisode: null,
|
||||
startDate: {
|
||||
year: 2024,
|
||||
month: 1,
|
||||
day: null
|
||||
},
|
||||
trailer: {
|
||||
id: 'NtssbUbxDDM',
|
||||
site: 'youtube'
|
||||
},
|
||||
mediaListEntry: {
|
||||
progress: 0,
|
||||
repeat: 0,
|
||||
status: 'PLANNING',
|
||||
score: 0
|
||||
},
|
||||
source: 'OTHER',
|
||||
studios: {
|
||||
nodes: [
|
||||
{
|
||||
name: 'A-1 Pictures'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 101165,
|
||||
idMal: 37349,
|
||||
title: {
|
||||
romaji: 'Goblin Slayer',
|
||||
english: 'GOBLIN SLAYER',
|
||||
native: 'ゴブリンスレイヤー',
|
||||
userPreferred: 'GOBLIN SLAYER'
|
||||
},
|
||||
description: "A young priestess has formed her first adventuring party, but almost immediately they find themselves in distress. It's the Goblin Slayer who comes to their rescue--a man who's dedicated his life to the extermination of all goblins, by any means necessary. And when rumors of his feats begin to circulate, there's no telling who might come calling next...<br><br>\n\n(Source: Yen Press)",
|
||||
season: 'FALL',
|
||||
seasonYear: 2018,
|
||||
format: 'TV',
|
||||
status: 'FINISHED',
|
||||
episodes: 12,
|
||||
duration: 24,
|
||||
averageScore: 71,
|
||||
genres: [
|
||||
'Action',
|
||||
'Adventure',
|
||||
'Fantasy'
|
||||
],
|
||||
isFavourite: false,
|
||||
coverImage: {
|
||||
extraLarge: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx101165-dVgOyGhEP4mB.jpg',
|
||||
medium: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx101165-dVgOyGhEP4mB.jpg',
|
||||
color: '#5d93e4'
|
||||
},
|
||||
countryOfOrigin: 'JP',
|
||||
isAdult: false,
|
||||
bannerImage: 'https://s4.anilist.co/file/anilistcdn/media/anime/banner/101165-bXPZrNiQGPHd.jpg',
|
||||
synonyms: [
|
||||
'ก็อบลิน สเลเยอร์'
|
||||
],
|
||||
nextAiringEpisode: null,
|
||||
startDate: {
|
||||
year: 2018,
|
||||
month: 10,
|
||||
day: 7
|
||||
},
|
||||
trailer: {
|
||||
id: '6sdxN30qNrw',
|
||||
site: 'youtube'
|
||||
},
|
||||
mediaListEntry: {
|
||||
id: 122792777,
|
||||
progress: 12,
|
||||
repeat: 1,
|
||||
status: 'COMPLETED',
|
||||
customLists: [
|
||||
{
|
||||
name: 'Watched using Miru',
|
||||
enabled: true
|
||||
}
|
||||
],
|
||||
score: 0
|
||||
},
|
||||
source: 'LIGHT_NOVEL',
|
||||
studios: {
|
||||
nodes: [
|
||||
{
|
||||
name: 'White Fox'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 21087,
|
||||
idMal: 30276,
|
||||
title: {
|
||||
romaji: 'One Punch Man',
|
||||
english: 'One-Punch Man',
|
||||
native: 'ワンパンマン',
|
||||
userPreferred: 'One Punch Man'
|
||||
},
|
||||
description: "Saitama has a rather peculiar hobby, being a superhero, but despite his heroic deeds and superhuman abilities, a shadow looms over his life. He's become much too powerful, to the point that every opponent ends up defeated with a single punch.\n<br><br>\nThe lack of challenge has driven him into a state of apathy, as he watches his life pass by having lost all enthusiasm, at least until he's unwillingly thrust in the role of being a mentor to the young and revenge-driven Genos. \n\n",
|
||||
season: 'FALL',
|
||||
seasonYear: 2015,
|
||||
format: 'TV',
|
||||
status: 'FINISHED',
|
||||
episodes: 12,
|
||||
duration: 24,
|
||||
averageScore: 83,
|
||||
genres: [
|
||||
'Action',
|
||||
'Comedy',
|
||||
'Sci-Fi',
|
||||
'Supernatural'
|
||||
],
|
||||
isFavourite: false,
|
||||
coverImage: {
|
||||
extraLarge: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx21087-UV2tu6exrfXz.jpg',
|
||||
medium: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx21087-UV2tu6exrfXz.jpg',
|
||||
color: '#e4ae5d'
|
||||
},
|
||||
countryOfOrigin: 'JP',
|
||||
isAdult: false,
|
||||
bannerImage: 'https://s4.anilist.co/file/anilistcdn/media/anime/banner/21087-sHb9zUZFsHe1.jpg',
|
||||
synonyms: [
|
||||
'OPM',
|
||||
'Wanpanman',
|
||||
'איש האגרוף הבודד',
|
||||
'一拳超人',
|
||||
'วันพันช์แมน',
|
||||
'Jagoan Sekali Pukul S1',
|
||||
'رجل اللكمة الواحدة',
|
||||
'ون بنش مان'
|
||||
],
|
||||
nextAiringEpisode: null,
|
||||
startDate: {
|
||||
year: 2015,
|
||||
month: 10,
|
||||
day: 5
|
||||
},
|
||||
trailer: {
|
||||
id: 'RzmFKUDOUgw',
|
||||
site: 'youtube'
|
||||
},
|
||||
mediaListEntry: null,
|
||||
source: 'MANGA',
|
||||
studios: {
|
||||
nodes: [
|
||||
{
|
||||
name: 'MADHOUSE'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -7,6 +7,7 @@ import '$lib/css.css'
|
|||
|
||||
export const prerender = false
|
||||
export const csr = true
|
||||
export const ssr = false
|
||||
export const trailingSlash = 'always'
|
||||
|
||||
/** @type {import('./$types').LayoutLoad} */
|
||||
|
|
|
|||
|
|
@ -2,12 +2,56 @@
|
|||
import Footer from '$lib/components/Footer.svelte'
|
||||
import Loader from '$lib/components/Loader.svelte'
|
||||
import Navbar from '$lib/components/Navbar.svelte'
|
||||
import { setContext } from 'svelte'
|
||||
import { writable } from 'simple-store-svelte'
|
||||
|
||||
const scrollPosition = writable(0)
|
||||
setContext('scroll-position', scrollPosition)
|
||||
function smoothScroll (t, { speed = 120, smooth = 10 } = {}) {
|
||||
let moving = false
|
||||
let pos = 0
|
||||
let scrollTop = 0
|
||||
let lastTime = null
|
||||
t.addEventListener('wheel', e => {
|
||||
e.preventDefault()
|
||||
// is trackpad
|
||||
const spd = (e.deltaY !== (e.deltaY | 0) || e.wheelDelta % 10 !== 0) ? speed / 10 : speed
|
||||
pos = Math.max(0, Math.min(pos - Math.max(-1, Math.min(1, e.deltaY * -1)) * spd, (t.scrollHeight - t.clientHeight) + (smooth * 2)))
|
||||
if (!moving) {
|
||||
lastTime = null
|
||||
update()
|
||||
}
|
||||
}, { capture: true, passive: false })
|
||||
|
||||
function getDeltaTime () {
|
||||
const now = performance.now()
|
||||
if (!lastTime) {
|
||||
lastTime = now
|
||||
return 1
|
||||
}
|
||||
const deltaTime = now - lastTime
|
||||
lastTime = now
|
||||
return deltaTime / 14
|
||||
}
|
||||
|
||||
t.addEventListener('pointerup', () => { pos = scrollTop = t.scrollTop })
|
||||
|
||||
function update () {
|
||||
const delta = pos - scrollTop === smooth * 2 ? 0 : ((pos - scrollTop) / smooth) * getDeltaTime()
|
||||
scrollTop += delta
|
||||
|
||||
scrollPosition.value = scrollTop < 1.3 ? 0 : scrollTop
|
||||
|
||||
t.scrollTo(0, scrollTop < 1.3 ? 0 : scrollTop)
|
||||
moving = Math.abs(delta) > 0.1 && !!requestAnimationFrame(update)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Loader />
|
||||
<div class='page-wrapper with-transitions position-relative' data-sidebar-type='overlayed-all'>
|
||||
<Navbar />
|
||||
<div class='overflow-x-hidden content-wrapper h-full overflow-y-scroll position-relative'>
|
||||
<div class='overflow-x-hidden content-wrapper h-full overflow-y-scroll position-relative' use:smoothScroll>
|
||||
<slot />
|
||||
<Footer />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,74 @@
|
|||
<script>
|
||||
import Hero from '$lib/components/Hero.svelte'
|
||||
import SmallCard from '$lib/components/SmallCard.svelte'
|
||||
import { cards } from '$lib/dummyData.js'
|
||||
import { getContext } from 'svelte'
|
||||
|
||||
const scrollPosition = getContext('scroll-position')
|
||||
</script>
|
||||
|
||||
<Hero />
|
||||
<div class='d-flex justify-content-center align-items-center'>
|
||||
<div class='container-xl'>
|
||||
<div class='position-relative'>
|
||||
<img src='app.png' alt='app' class='mw-full px-20' />
|
||||
<div class='overlay-gradient position-absolute top-0 left-0 w-full h-full' />
|
||||
</div>
|
||||
|
||||
<div class='content pb-20' id='about'>
|
||||
<h1 class='w-full font-weight-bold text-white px-20'>
|
||||
Torrenting made simple.
|
||||
</h1>
|
||||
<div class='row'>
|
||||
<div class='col-md-4 col-12'>
|
||||
<div class='content'>
|
||||
<h3>Quality meets speed</h3>
|
||||
<p class='text-muted font-size-16'>Stream torrents directly for playback, with no time wasted on looking for torrents or waiting for downloads to finish.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class='col-md-4 col-12'>
|
||||
<div class='content'>
|
||||
<h3>No expertise required</h3>
|
||||
<p class='text-muted font-size-16'>You don’t need to be a master of torrenting or have a deep understanding of technology, it simply works.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class='col-md-4 col-12'>
|
||||
<div class='content'>
|
||||
<h3>Configuration free</h3>
|
||||
<p class='text-muted font-size-16'>No need to look for setup guides, no need to worry about the ideal configuration, it’s perfect out of the box.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class='my-20' />
|
||||
<div class='content pt-20'>
|
||||
<div class='row'>
|
||||
<div class='col-md-7 col-12'>
|
||||
<div class='content'>
|
||||
<h1 class='w-full font-weight-bold text-white'>
|
||||
Tightly integrated.
|
||||
</h1>
|
||||
<p class='text-muted font-size-16'>Find and download torrents, watch trailers, manage your list, search, browse and discover anime, watch together with friends and more, all in the same interface. No need to open multiple apps, tabs, everything shipped in one package.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class='col-md-5 col-12'>
|
||||
<div class='pb-10 w-full position-relative d-flex flex-row justify-content-start gallery' style:--left={(300 - ($scrollPosition - 700)) / 5 + 'px'}>
|
||||
{#each cards as media, i}
|
||||
<SmallCard {media} preview={i === 1} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.overlay-gradient {
|
||||
background: linear-gradient(0deg, #0F1113 15.27%, rgba(15, 17, 19, 0.92) 41.28%, rgba(15, 17, 19, 0.25) 74.32%);
|
||||
}
|
||||
h1 {
|
||||
font-size: 5rem
|
||||
}
|
||||
.gallery {
|
||||
padding-left: clamp(0px, var(--left), 100px);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue