refactor(I4): extract useInfiniteScroll composable; migrate 6 consumers

The IntersectionObserver-on-a-sentinel infinite-scroll pattern was copy-
pasted in 7 places. New composables/useInfiniteScroll(sentinelRef, cb)
owns the observer lifecycle (attach on mount, re-attach when the ref
changes, disconnect on unmount). Migrated GalleryGrid, MasonryGrid
(gallery+showcase), ArtistPostsTab, ArtistsView, TagsView, SeriesManageView.
PostsView left manual on purpose — its anchored mode does bidirectional
scroll-position preservation that doesn't fit the simple composable.

I4(a) (timestamps) is effectively already done: the UTC displays were
converted earlier; the remaining toLocaleString uses already render local
time, so they're left as-is rather than churned for format-only consistency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 14:03:49 -04:00
parent 00e2608ba1
commit 8979e0e377
7 changed files with 57 additions and 79 deletions
+5 -13
View File
@@ -34,9 +34,10 @@
</template>
<script setup>
import { ref, watch, onMounted, onUnmounted } from 'vue'
import { ref, watch, onMounted } from 'vue'
import { useArtistDirectoryStore } from '../stores/artistDirectory.js'
import { usePlatformsStore } from '../stores/platforms.js'
import { useInfiniteScroll } from '../composables/useInfiniteScroll.js'
import ArtistCard from '../components/discovery/ArtistCard.vue'
const store = useArtistDirectoryStore()
@@ -55,16 +56,9 @@ watch(search, (v) => {
})
watch(platformModel, (v) => store.setPlatform(v || null))
let observer = null
function attachObserver() {
if (observer) observer.disconnect()
if (!sentinelEl.value) return
observer = new IntersectionObserver(([e]) => {
if (e.isIntersecting && store.hasMore && !store.loading) store.loadMore()
}, { rootMargin: '600px' })
observer.observe(sentinelEl.value)
}
watch(sentinelEl, attachObserver)
useInfiniteScroll(sentinelEl, () => {
if (store.hasMore && !store.loading) store.loadMore()
})
onMounted(async () => {
await platformsStore.loadAll()
@@ -72,9 +66,7 @@ onMounted(async () => {
title: p.key, value: p.key,
}))
store.reset()
attachObserver()
})
onUnmounted(() => observer && observer.disconnect())
</script>
<style scoped>
+4 -7
View File
@@ -61,15 +61,15 @@
</template>
<script setup>
import { onMounted, onUnmounted, ref } from 'vue'
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { useSeriesManageStore, moveItem } from '../stores/seriesManage.js'
import { useInfiniteScroll } from '../composables/useInfiniteScroll.js'
const route = useRoute()
const store = useSeriesManageStore()
const dragFrom = ref(null)
const sentinel = ref(null)
let observer = null
function onDrop(toIdx) {
if (dragFrom.value === null || dragFrom.value === toIdx) return
@@ -80,15 +80,12 @@ function onDrop(toIdx) {
store.reorder(ordered)
}
useInfiniteScroll(sentinel, () => store.loadPicker())
onMounted(async () => {
await store.load(parseInt(route.params.tagId, 10))
await store.loadPicker(true)
observer = new IntersectionObserver(([e]) => {
if (e.isIntersecting) store.loadPicker()
}, { rootMargin: '600px' })
if (sentinel.value) observer.observe(sentinel.value)
})
onUnmounted(() => observer && observer.disconnect())
</script>
<style scoped>
+6 -13
View File
@@ -89,11 +89,12 @@
</template>
<script setup>
import { ref, watch, onMounted, onUnmounted } from 'vue'
import { ref, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useTagDirectoryStore } from '../stores/tagDirectory.js'
import { useAdminStore } from '../stores/admin.js'
import { useApi } from '../composables/useApi.js'
import { useInfiniteScroll } from '../composables/useInfiniteScroll.js'
import TagCard from '../components/discovery/TagCard.vue'
import MergeConfirmDialog from '../components/discovery/MergeConfirmDialog.vue'
import DestructiveConfirmModal from '../components/modal/DestructiveConfirmModal.vue'
@@ -130,18 +131,10 @@ watch(search, (v) => {
})
watch(kind, (v) => store.setKind(v || null))
let observer = null
function attachObserver() {
if (observer) observer.disconnect()
if (!sentinelEl.value) return
observer = new IntersectionObserver(([e]) => {
if (e.isIntersecting && store.hasMore && !store.loading) store.loadMore()
}, { rootMargin: '600px' })
observer.observe(sentinelEl.value)
}
watch(sentinelEl, attachObserver)
onMounted(() => { store.reset(); attachObserver() })
onUnmounted(() => observer && observer.disconnect())
useInfiniteScroll(sentinelEl, () => {
if (store.hasMore && !store.loading) store.loadMore()
})
onMounted(() => store.reset())
function openTag(tagId) {
router.push({ name: 'gallery', query: { tag_id: tagId } })