diff --git a/package.json b/package.json index 4319d66..d9b34ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ui", - "version": "6.3.22", + "version": "6.3.23", "license": "BUSL-1.1", "private": true, "packageManager": "pnpm@9.14.4", diff --git a/src/routes/app/search/+page.svelte b/src/routes/app/search/+page.svelte index 001ef47..4041f23 100644 --- a/src/routes/app/search/+page.svelte +++ b/src/routes/app/search/+page.svelte @@ -2,7 +2,7 @@ import FileImage from 'lucide-svelte/icons/file-image' import Trash from 'lucide-svelte/icons/trash' import X from 'lucide-svelte/icons/x' - import { tick } from 'svelte' + import { onDestroy, tick } from 'svelte' import MagnifyingGlass from 'svelte-radix/MagnifyingGlass.svelte' import { toast } from 'svelte-sonner' @@ -20,7 +20,7 @@ import { Input, type FormInputEvent } from '$lib/components/ui/input' import { client } from '$lib/modules/anilist' import { click, dragScroll } from '$lib/modules/navigate' - import { cn, debounce, traceAnime } from '$lib/utils' + import { cn, debounce, sleep, traceAnime } from '$lib/utils' // util @@ -112,16 +112,25 @@ } inputText = '' pageNumber = 1 + for (const unsub of mediaSubscriptions) { + unsub() + } + mediaSubscriptions = [] } let media: Array> = [] + let mediaSubscriptions: Array<() => void> = [] + + onDestroy(clear) + // handlers function searchChanged (s: typeof search) { const filter = filterEmpty(s) - media = [searchQuery(filter, pageNumber)] + pageNumber = 1 + media = [searchQuery(filter, 1)] } function searchQuery (filter: Partial, page: number) { @@ -131,7 +140,7 @@ search: filter.name, onList: filter.onList?.[0]?.value === 'true' ? true : filter.onList?.[0]?.value === 'false' ? false : undefined, genre: filter.genres?.map(g => g.value), - seasonYear: filter.years ? parseInt(filter.years[0]!.value) : undefined, + seasonYear: filter.years?.length ? parseInt(filter.years[0]!.value) : undefined, season: filter.seasons?.[0]!.value as 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL' | undefined, format: filter.formats?.map(f => f.value) as Array<'MUSIC' | 'MANGA' | 'TV' | 'TV_SHORT' | 'MOVIE' | 'SPECIAL' | 'OVA' | 'ONA' | 'NOVEL' | 'ONE_SHOT'>, status: filter.status?.map(s => s.value) as Array<'FINISHED' | 'RELEASING' | 'NOT_YET_RELEASED' | 'CANCELLED' | 'HIATUS' | null>, @@ -140,7 +149,10 @@ tick().then(() => replaceState('', { search })) - return client.search(search) + const query = client.search(search) + + mediaSubscriptions.push(query.subscribe(() => {})) + return query } const updateText = debounce((e: FormInputEvent) => { @@ -150,12 +162,10 @@ $: searchChanged(search) - // function nextPage () { - // page = page + 1 - // media = [...media, searchQuery(filterEmpty(search), page)] - // } // TODO: selects should turn into modals on mobile! like anilist - // TODO: infinite scroll + + $: lastestQuery = media[media.length - 1] + $: hasNextPage = $lastestQuery?.data?.Page?.pageInfo?.hasNextPage async function imagePicker (e: Event) { const target = e.target as HTMLInputElement @@ -182,10 +192,35 @@ } } + function infiniteScroll (div: HTMLDivElement) { + const ctrl = new AbortController() + let ticking = false + div.addEventListener('scrollend', async () => { + if (!ticking && hasNextPage) { + const scrollTop = div.scrollTop + const scrollable = div.scrollHeight - div.clientHeight + const remaining = scrollable - scrollTop + if (remaining < 800) { + pageNumber = pageNumber + 1 + media = [...media, searchQuery(filterEmpty(search), pageNumber)] + ticking = true + await sleep(100) + ticking = false + } + } + }, { signal: ctrl.signal }) + + return { + destroy () { + ctrl.abort() + } + } + } + const viewer = client.viewer -
+