Skip to content

Commit

Permalink
feat: virtualize posts
Browse files Browse the repository at this point in the history
  • Loading branch information
AlejandroAkbal committed Mar 10, 2024
1 parent 86d7cd8 commit 0e873e1
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 209 deletions.
26 changes: 0 additions & 26 deletions components/pages/posts/navigation/PostsPagination.vue

This file was deleted.

35 changes: 1 addition & 34 deletions components/pages/posts/post/PostMedia.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,35 +59,6 @@
localPosterSrc.value = props.mediaPosterSrc
}
function onMediaIntersectionObserver(entries: IntersectionObserverEntry[]) {
// Skip on fullscreen
if (document.fullscreenElement) {
return
}
// Smallest video & image possible - https://stackoverflow.com/a/36610159/11398632
const smallestImage =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII='
const smallestVideo =
'data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAAAtJtZGF0AAACrQYF//+p3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE2NCByMzEwMyA5NDFjYWU2IC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAyMiAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTMgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MzoweDExMyBtZT1oZXggc3VibWU9NyBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0xIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MSA4eDhkY3Q9MSBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0tMiB0aHJlYWRzPTEgbG9va2FoZWFkX3RocmVhZHM9MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRlcmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9MSBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRiPTEgb3Blbl9nb3A9MCB3ZWlnaHRwPTIga2V5aW50PTI1MCBrZXlpbnRfbWluPTEgc2NlbmVjdXQ9NDAgaW50cmFfcmVmcmVzaD0wIHJjX2xvb2thaGVhZD00MCByYz1jcmYgbWJ0cmVlPTEgY3JmPTIzLjAgcWNvbXA9MC42MCBxcG1pbj0wIHFwbWF4PTY5IHFwc3RlcD00IGlwX3JhdGlvPTEuNDAgYXE9MToxLjAwAIAAAAAVZYiEABX//vfJ78Cm6/X2tb9gAQD5AAADBm1vb3YAAABsbXZoZAAAAADgYBEw4GARMAAAA+gAAAPoAAEAAAEAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAIwdHJhawAAAFx0a2hkAAAAA+BgETDgYBEwAAAAAQAAAAAAAAPoAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAUAAAAFAAAAAAAJGVkdHMAAAAcZWxzdAAAAAAAAAABAAAD6AAAAAAAAQAAAAABqG1kaWEAAAAgbWRoZAAAAADgYBEw4GARMAAAQAAAAEAAVcQAAAAAAC1oZGxyAAAAAAAAAAB2aWRlAAAAAAAAAAAAAAAAVmlkZW9IYW5kbGVyAAAAAVNtaW5mAAAAFHZtaGQAAAABAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAAETc3RibAAAAK9zdHNkAAAAAAAAAAEAAACfYXZjMQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAUABQASAAAAEgAAAAAAAAAARVMYXZjNTkuNTYuMTAwIGxpYngyNjQAAAAAAAAAAAAAABj//wAAADVhdmNDAWQAM//hABhnZAAzrNlJeeeEAAADAAQAAAMACDxgxlgBAAZo6+PLIsD9+PgAAAAAFGJ0cnQAAAAAAAAWUAAAFlAAAAAYc3R0cwAAAAAAAAABAAAAAQAAQAAAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAEAAAABAAAAFHN0c3oAAAAAAAACygAAAAEAAAAUc3RjbwAAAAAAAAABAAAAMAAAAGJ1ZHRhAAAAWm1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAALWlsc3QAAAAlqXRvbwAAAB1kYXRhAAAAAQAAAABMYXZmNTkuMzUuMTAw'
const smallestMedia = isImage.value
? //
smallestImage
: smallestVideo
const entry = entries[0]
const mediaElement = entry.target.children[0] as HTMLImageElement | HTMLVideoElement
const newSrc = entry.isIntersecting
? //
localSrc.value
: smallestMedia
mediaElement.src = newSrc
}
/**
* Stops videos when they are out of the viewport
*/
Expand Down Expand Up @@ -167,7 +138,6 @@
<!-- TODO: Fix very large images not being on screen so not loaded -->
<div
v-else-if="isImage"
v-intersection-observer="[onMediaIntersectionObserver, { rootMargin: '1200px' }]"
class="transition-opacity duration-700 ease-in-out"
:class="mediaHasLoaded ? 'opacity-100' : 'opacity-0'"
>
Expand Down Expand Up @@ -208,10 +178,7 @@
</div>

<!-- Video -->
<div
v-else-if="isVideo"
v-intersection-observer="[onMediaIntersectionObserver, { rootMargin: '1200px' }]"
>
<div v-else-if="isVideo">
<!-- TODO: Add load animation -->
<!-- Fix(rounded borders): add the same rounded borders that the parent has -->
<video
Expand Down
206 changes: 141 additions & 65 deletions pages/posts/[domain].vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
<script lang="ts" setup>
import { useBooruList } from '~/composables/useBooruList'
import { ArrowPathIcon, ExclamationCircleIcon, QuestionMarkCircleIcon } from '@heroicons/vue/24/solid'
import { MagnifyingGlassIcon } from '@heroicons/vue/24/outline'
import { toast } from 'vue-sonner'
import Tag from '~/assets/js/tag.dto'
import { ArrowPathIcon, ExclamationCircleIcon, QuestionMarkCircleIcon } from '@heroicons/vue/24/solid'
import * as Sentry from '@sentry/vue'
import { useInfiniteQuery } from '@tanstack/vue-query'
import { useWindowVirtualizer } from '@tanstack/vue-virtual'
import { cloneDeep } from 'lodash-es'
import { FetchError } from 'ofetch'
import type { Ref } from 'vue'
import { toast } from 'vue-sonner'
import { generatePostsRoute } from '~/assets/js/RouterHelper'
import { tagArrayToTitle } from '~/assets/js/SeoHelper'
import { capitalize, startCase, cloneDeep } from 'lodash-es'
import type { Domain } from '~/assets/js/domain'
import type { IPostPage } from '~/assets/js/post'
import { useInfiniteQuery } from '@tanstack/vue-query'
import { FetchError } from 'ofetch'
import * as Sentry from '@sentry/vue'
import Tag from '~/assets/js/tag.dto'
import { useBooruList } from '~/composables/useBooruList'
const router = useRouter()
const route = useRoute()
Expand Down Expand Up @@ -186,7 +187,81 @@
await suspense()
})
// TODO: Virtualize posts or use a clever maxPages combination and scroll to last page end
const allRows = computed(() => {
if (!data.value) {
return []
}
// Flatten pages, but add `current_page` to each post
return data.value.pages.flatMap((page) => {
return page.data.map((post) => {
return {
...post,
current_page: page.meta.current_page
}
})
})
})
const parentRef = ref<HTMLElement | null>(null)
const parentOffsetRef = ref(0)
onMounted(() => {
parentOffsetRef.value = parentRef.value?.offsetTop ?? 0
})
const rowVirtualizerOptions = computed(() => {
return {
debug: false,
count: hasNextPage ? allRows.value.length + 1 : allRows.value.length,
estimateSize: () => 600,
scrollMargin: parentOffsetRef.value,
overscan: 3
}
})
const rowVirtualizer = useWindowVirtualizer(rowVirtualizerOptions)
const virtualRows = computed(() => rowVirtualizer.value.getVirtualItems())
const totalSize = computed(() => rowVirtualizer.value.getTotalSize())
// Next page loader
watchEffect(() => {
// Skip if there is no data
if (!data.value) {
return
}
const [lastItem] = [...virtualRows.value].reverse()
if (!lastItem) {
return
}
// IF last item is the last item in the list
// AND there is a next page
// AND it's not fetching
// THEN load next page
if (lastItem.index >= allRows.value.length - 1 && hasNextPage && !isFetchingNextPage.value) {
onLoadNextPostPage()
}
})
// FIX: Remove when this issue is fixed - https://github.com/TanStack/virtual/issues/619#issuecomment-1969516091
const measureElement = (el) => {
nextTick(() => {
if (!el) {
return
}
rowVirtualizer.value.measureElement(el)
})
}
/**
* `undefined` values mean that they will be replaced by default values
Expand Down Expand Up @@ -352,6 +427,11 @@
async function onPageIndicatorClick() {
const pagePrompt = prompt('To which page do you want to go?')
if (pagePrompt == null) {
return
}
const page = parseInt(pagePrompt, 10)
if (isNaN(page)) {
Expand Down Expand Up @@ -463,8 +543,6 @@
description += '. Free anime hentai here on R34.app'
return description
})
// TODO: Think about setting a real canonical URL
Expand Down Expand Up @@ -652,77 +730,75 @@
<!-- Posts -->
<div
v-else
class="space-y-4"
ref="parentRef"
>
<!-- TODO: Previous page -->
<!-- TODO: Previous pagination -->

<!-- TODO: Animate adding posts https://vuejs.org/guide/built-ins/transition-group.html#staggering-list-transitions -->

<!-- Pages -->
<div
v-for="(postsPage, postsPageIndex) in data.pages"
class="space-y-4"
data-testid="posts-list"
:style="{
height: `${totalSize}px`,
width: '100%',
position: 'relative'
}"
>
<!-- Page indicator -->
<button
v-if="postsPageIndex !== 0"
class="hover:hover-text-util hover:hover-bg-util focus-visible:focus-outline-util mx-auto block rounded-md px-1.5 py-1 text-sm"
type="button"
@click="onPageIndicatorClick"
<ol
:style="{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRows[0]?.start - rowVirtualizer.options.scrollMargin ?? 0}px)`
}"
class="space-y-4"
>
&dharl; Page {{ postsPage.meta.current_page }} &dharr;
</button>

<!-- TODO: Animate adding posts https://vuejs.org/guide/built-ins/transition-group.html#staggering-list-transitions -->
<ol class="space-y-4">
<template
v-for="(post, postIndex) in postsPage.data"
:key="`${selectedBooru.domain}-${post.id}`"
<li
v-for="virtualRow in virtualRows"
:key="virtualRow.key"
:data-index="virtualRow.index"
:ref="measureElement"
>
<li :data-testid="`${selectedBooru.domain}-${post.id}`">
<!-- Next Pagination -->
<div
v-if="virtualRow.index > allRows.length - 1"
class="flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium text-base-content"
>
<span class="block rounded-md px-1.5 py-1">
{{ hasNextPage ? 'Loading more...' : 'Nothing more to load' }}
</span>
</div>

<!-- Content -->
<template v-else>
<!-- Page indicator -->
<button
v-if="virtualRow.index !== 0 && virtualRow.index % userSettings.postsPerPage === 0"
class="hover:hover-text-util hover:hover-bg-util focus-visible:focus-outline-util mx-auto mb-4 block rounded-md px-1.5 py-1 text-sm"
type="button"
@click="onPageIndicatorClick"
>
&dharl; Page {{ allRows[virtualRow.index].current_page }} &dharl;
</button>

<!-- Post -->
<Post
:domain="selectedBooru.domain"
:post="post"
:post="allRows[virtualRow.index]"
:selected-tags="selectedTags"
@addTag="onPostAddTag"
@openTagInNewTab="onPostOpenTagInNewTab"
@setTag="onPostSetTag"
/>
</li>

<!-- Promoted content -->
<template v-if="!isPremium && postIndex !== 0 && postIndex % 7 === 0">
<li>
<PromotedContent />
</li>
<!-- Promoted content -->
<template v-if="!isPremium && virtualRow.index !== 0 && virtualRow.index % 7 === 0">
<PromotedContent class="mt-4" />
</template>
</template>
</template>
</li>
</ol>
</div>

<!-- Next Pagination -->
<PostsPagination @load-next-page="onLoadNextPostPage">
<span
v-if="isFetchingNextPage"
class="block rounded-md px-1.5 py-1"
>
Loading more&hellip;
</span>

<button
v-else-if="hasNextPage"
class="focus-visible:focus-outline-util hover:hover-bg-util hover:hover-text-util rounded-md px-1.5 py-1"
@click="onLoadNextPostPage"
>
Scroll here or click me to load more
</button>

<span
v-else
class="block rounded-md px-1.5 py-1"
>
Nothing more to load
</span>
</PostsPagination>
</div>
</section>
</main>
Expand Down
Loading

0 comments on commit 0e873e1

Please sign in to comment.