pref: Use virtual scroll load models

This commit is contained in:
hayden
2024-10-28 18:08:38 +08:00
parent 86e587eba2
commit 14a31a8ca8
7 changed files with 179 additions and 56 deletions

View File

@@ -35,6 +35,7 @@
"inplace", "inplace",
"contentcontainer", "contentcontainer",
"itemlist", "itemlist",
"virtualscroller"
], ],
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"files.associations": { "files.associations": {

View File

@@ -38,7 +38,7 @@
</ResponseSelect> </ResponseSelect>
</div> </div>
<ResponseScrollArea class="-mx-5 h-full"> <ResponseScroll class="-mx-5 h-full">
<div class="px-5"> <div class="px-5">
<ModelContent <ModelContent
v-for="{ item } in data" v-for="{ item } in data"
@@ -64,7 +64,7 @@
</div> </div>
</div> </div>
</div> </div>
</ResponseScrollArea> </ResponseScroll>
</div> </div>
<DialogResizer :min-width="390"></DialogResizer> <DialogResizer :min-width="390"></DialogResizer>
@@ -75,7 +75,7 @@
import DialogResizer from 'components/DialogResizer.vue' import DialogResizer from 'components/DialogResizer.vue'
import ResponseInput from 'components/ResponseInput.vue' import ResponseInput from 'components/ResponseInput.vue'
import ResponseSelect from 'components/ResponseSelect.vue' import ResponseSelect from 'components/ResponseSelect.vue'
import ResponseScrollArea from 'components/ResponseScrollArea.vue' import ResponseScroll from 'components/ResponseScroll.vue'
import ModelContent from 'components/ModelContent.vue' import ModelContent from 'components/ModelContent.vue'
import Button from 'primevue/button' import Button from 'primevue/button'
import Dialog from 'primevue/dialog' import Dialog from 'primevue/dialog'

View File

@@ -34,7 +34,7 @@
</div> </div>
</div> </div>
<ResponseScrollArea> <ResponseScroll>
<div class="w-full px-4"> <div class="w-full px-4">
<ul class="m-0 flex list-none flex-col gap-4 p-0"> <ul class="m-0 flex list-none flex-col gap-4 p-0">
<li <li
@@ -143,7 +143,7 @@
</div> </div>
</li> </li>
</ul> --> </ul> -->
</ResponseScrollArea> </ResponseScroll>
</div> </div>
<DialogResizer :min-width="390" :min-height="390"></DialogResizer> <DialogResizer :min-width="390" :min-height="390"></DialogResizer>
@@ -155,7 +155,7 @@
<script setup lang="ts"> <script setup lang="ts">
import DialogCreateTask from 'components/DialogCreateTask.vue' import DialogCreateTask from 'components/DialogCreateTask.vue'
import DialogResizer from 'components/DialogResizer.vue' import DialogResizer from 'components/DialogResizer.vue'
import ResponseScrollArea from 'components/ResponseScrollArea.vue' import ResponseScroll from 'components/ResponseScroll.vue'
import Button from 'primevue/button' import Button from 'primevue/button'
import Dialog from 'primevue/dialog' import Dialog from 'primevue/dialog'
import { useDownload } from 'hooks/download' import { useDownload } from 'hooks/download'

View File

@@ -7,7 +7,7 @@
minimizeIcon="pi pi-arrow-down-left-and-arrow-up-right-to-center" minimizeIcon="pi pi-arrow-down-left-and-arrow-up-right-to-center"
:pt:mask:class="['group', { open }]" :pt:mask:class="['group', { open }]"
pt:root:class="max-h-full group-[:not(.open)]:!hidden" pt:root:class="max-h-full group-[:not(.open)]:!hidden"
pt:content:class="px-0" pt:content:class="px-0 flex-1"
> >
<template #header> <template #header>
<div class="flex flex-1 items-center justify-between pr-2"> <div class="flex flex-1 items-center justify-between pr-2">
@@ -37,6 +37,7 @@
['--card-width']: `${cardWidth}px`, ['--card-width']: `${cardWidth}px`,
['--gutter']: `${gutter}px`, ['--gutter']: `${gutter}px`,
}" }"
v-resize="onContainerResize"
> >
<div <div
:class="[ :class="[
@@ -72,34 +73,42 @@
</div> </div>
</div> </div>
<ResponseScrollArea class="h-full"> <ResponseScroll
<div :items="list"
:class="[ :itemSize="cardWidth / aspect + gutter"
'-mt-8 grid grid-cols-1 justify-center gap-8 px-8', :row-key="(item) => item.map(genModelKey).join(',')"
'@lg/content:grid-cols-[repeat(auto-fit,var(--card-width))]', class="h-full flex-1"
'@lg/content:gap-[var(--gutter)]', >
'@lg/content:-mt-[var(--gutter)]', <template #item="{ item }">
'@lg/content:px-4', <div
]" :class="[
> 'grid grid-cols-1 justify-center gap-8 px-8',
<div class="col-span-full"></div> '@lg/content:grid-cols-[repeat(auto-fit,var(--card-width))]',
<div v-for="model in list" v-show="model.visible" :key="model.id"> '@lg/content:gap-[var(--gutter)]',
'@lg/content:px-4',
]"
>
<DialogModelCard <DialogModelCard
:key="`${model.type}:${model.pathIndex}:${model.fullname}`" v-for="model in item"
:key="genModelKey(model)"
:model="model" :model="model"
></DialogModelCard> ></DialogModelCard>
<div class="col-span-full"></div>
</div> </div>
</div> </template>
<div v-show="noneDisplayModel" class="flex justify-center pt-20"> <template #empty>
<div class="select-none text-lg font-bold">No models found</div> <div class="flex flex-col items-center gap-4 pt-20 opacity-70">
</div> <i class="pi pi-box text-4xl"></i>
</ResponseScrollArea> <div class="select-none text-lg font-bold">No models found</div>
</div>
</template>
</ResponseScroll>
</div> </div>
<DialogResizer <DialogResizer
:min-width="cardWidth * 2 + gutter + 42" :min-width="cardWidth * 2 + gutter + 42"
:min-height="cardWidth * aspect * 0.5 + 162" :min-height="(cardWidth / aspect) * 0.5 + 162"
></DialogResizer> ></DialogResizer>
</Dialog> </Dialog>
</template> </template>
@@ -112,13 +121,15 @@ import DialogResizer from 'components/DialogResizer.vue'
import DialogModelCard from 'components/DialogModelCard.vue' import DialogModelCard from 'components/DialogModelCard.vue'
import ResponseInput from 'components/ResponseInput.vue' import ResponseInput from 'components/ResponseInput.vue'
import ResponseSelect from 'components/ResponseSelect.vue' import ResponseSelect from 'components/ResponseSelect.vue'
import ResponseScrollArea from 'components/ResponseScrollArea.vue' import ResponseScroll from 'components/ResponseScroll.vue'
import Dialog from 'primevue/dialog' import Dialog from 'primevue/dialog'
import Button from 'primevue/button' import Button from 'primevue/button'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useToast } from 'hooks/toast' import { useToast } from 'hooks/toast'
import { useDownload } from 'hooks/download' import { useDownload } from 'hooks/download'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { chunk } from 'lodash'
import { defineResizeCallback } from 'hooks/resize'
const { isMobile, cardWidth, gutter, aspect, refreshSetting } = useConfig() const { isMobile, cardWidth, gutter, aspect, refreshSetting } = useConfig()
@@ -173,8 +184,11 @@ const sortOrderOptions = ref(
}), }),
) )
const colSpan = ref(1)
const colSpanWidth = ref(cardWidth)
const list = computed(() => { const list = computed(() => {
const filterList = data.value.map((model) => { const filterList = data.value.filter((model) => {
const showAllModel = currentType.value === 'all' const showAllModel = currentType.value === 'all'
const matchType = showAllModel || model.type === currentType.value const matchType = showAllModel || model.type === currentType.value
@@ -182,9 +196,7 @@ const list = computed(() => {
.toLowerCase() .toLowerCase()
.includes(searchContent.value?.toLowerCase() || '') .includes(searchContent.value?.toLowerCase() || '')
model.visible = matchType && matchName return matchType && matchName
return model
}) })
let sortStrategy = (a: Model, b: Model) => 0 let sortStrategy = (a: Model, b: Model) => 0
@@ -205,11 +217,9 @@ const list = computed(() => {
break break
} }
return filterList.sort(sortStrategy) const sortedList = filterList.sort(sortStrategy)
})
const noneDisplayModel = computed(() => { return chunk(sortedList, colSpan.value)
return !list.value.some((model) => model.visible)
}) })
const refreshModels = async () => { const refreshModels = async () => {
@@ -220,4 +230,19 @@ const refreshModels = async () => {
life: 2000, life: 2000,
}) })
} }
const onContainerResize = defineResizeCallback((entries) => {
const entry = entries[0]
if (isMobile.value) {
colSpan.value = 1
} else {
const containerWidth = entry.contentRect.width
colSpan.value = Math.floor((containerWidth - gutter) / (cardWidth + gutter))
colSpanWidth.value = colSpan.value * (cardWidth + gutter) - gutter
}
})
const genModelKey = (model: BaseModel) => {
return `${model.type}:${model.pathIndex}:${model.fullname}`
}
</script> </script>

View File

@@ -10,7 +10,7 @@
pt:content:class="px-0" pt:content:class="px-0"
@after-hide="handleCancel" @after-hide="handleCancel"
> >
<ResponseScrollArea class="h-full"> <ResponseScroll class="h-full">
<div class="px-8"> <div class="px-8">
<ModelContent <ModelContent
v-model:editable="editable" v-model:editable="editable"
@@ -49,7 +49,7 @@
</template> </template>
</ModelContent> </ModelContent>
</div> </div>
</ResponseScrollArea> </ResponseScroll>
<DialogResizer :min-width="390"></DialogResizer> <DialogResizer :min-width="390"></DialogResizer>
</Dialog> </Dialog>
</template> </template>
@@ -59,7 +59,7 @@ import Button from 'primevue/button'
import Dialog from 'primevue/dialog' import Dialog from 'primevue/dialog'
import ModelContent from 'components/ModelContent.vue' import ModelContent from 'components/ModelContent.vue'
import DialogResizer from 'components/DialogResizer.vue' import DialogResizer from 'components/DialogResizer.vue'
import ResponseScrollArea from 'components/ResponseScrollArea.vue' import ResponseScroll from 'components/ResponseScroll.vue'
import { useConfig } from 'hooks/config' import { useConfig } from 'hooks/config'
import { computed, ref, watchEffect } from 'vue' import { computed, ref, watchEffect } from 'vue'
import { useModelNodeAction, useModels } from 'hooks/model' import { useModelNodeAction, useModels } from 'hooks/model'

View File

@@ -4,12 +4,30 @@
ref="viewport" ref="viewport"
data-scroll-viewport data-scroll-viewport
class="h-full w-full overflow-auto scrollbar-none" class="h-full w-full overflow-auto scrollbar-none"
:style="{ contain: items ? 'strict' : undefined }"
@scroll="onContentScroll" @scroll="onContentScroll"
v-resize="onContainerResize" v-resize="onContainerResize"
> >
<div data-scroll-content style="min-width: 100%"> <div data-scroll-content class="relative min-w-full">
<slot name="default"></slot> <slot name="default">
<div
v-for="(item, index) in loadedItems"
:key="genRowKey(item, index)"
:style="{ height: `${itemSize}px` }"
>
<slot name="item" :item="item"></slot>
</div>
<slot v-if="loadedItems.length === 0" name="empty">
<div class="absolute w-full py-20 text-center">No Data</div>
</slot>
</slot>
</div> </div>
<div
data-scroll-space
class="pointer-events-none absolute left-0 top-0 h-px w-px"
:style="spaceStyle"
></div>
</div> </div>
<div <div
@@ -41,12 +59,15 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts" generic="T">
import { nextTick, onUnmounted, ref } from 'vue' import { nextTick, onUnmounted, ref, watch } from 'vue'
import { clamp, throttle } from 'lodash' import { clamp, throttle } from 'lodash'
interface ScrollAreaProps { interface ScrollAreaProps {
items?: T[][]
itemSize?: number
scrollbar?: boolean scrollbar?: boolean
rowKey?: string | ((item: T[]) => string)
} }
const props = withDefaults(defineProps<ScrollAreaProps>(), { const props = withDefaults(defineProps<ScrollAreaProps>(), {
@@ -105,25 +126,91 @@ const scrollbars = ref<Record<ScrollbarDirection, Scrollbar>>({
const isDragging = ref(false) const isDragging = ref(false)
const onContainerResize: ResizeObserverCallback = throttle((entries) => { const spaceStyle = ref({})
emit('resize', entries) const loadedItems = ref<T[][]>([])
if (isDragging.value) return
const entry = entries[0] const genRowKey = (item: any | any[], index: number) => {
const container = entry.target as HTMLElement if (typeof props.rowKey === 'function') {
const content = container.querySelector('[data-scroll-content]')! return props.rowKey(item)
}
return item[props.rowKey ?? 'key'] ?? index
}
const setSpacerSize = () => {
const items = props.items
if (items) {
const itemSize = props.itemSize ?? 0
spaceStyle.value = { height: `${itemSize * items.length}px` }
} else {
spaceStyle.value = {}
}
}
const getContainerContent = (raw?: boolean): HTMLElement => {
const container = viewport.value as HTMLElement
if (props.items && !raw) {
return container.querySelector('[data-scroll-space]')!
}
return container.querySelector('[data-scroll-content]')!
}
const init = () => {
const container = viewport.value as HTMLElement
container.scrollTop = 0
getContainerContent().style.transform = ''
}
const calculateLoadItems = () => {
let visibleItems: any[] = []
if (props.items) {
const container = viewport.value as HTMLElement
const content = getContainerContent(true)
const resolveVisibleItems = (items: any[], attr: ScrollbarAttribute) => {
const containerSize = container[attr.clientSize]
const itemSize = props.itemSize!
const viewCount = Math.ceil(containerSize / itemSize)
let start = Math.floor(container[attr.scrollOffset] / itemSize)
const offset = start * itemSize
let end = start + viewCount
end = Math.min(end + viewCount, items.length)
content.style.transform = `translateY(${offset}px)`
return items.slice(start, end)
}
visibleItems = resolveVisibleItems(props.items, scrollbarAttrs.vertical)
}
loadedItems.value = visibleItems
}
const calculateScrollThumbSize = () => {
const container = viewport.value as HTMLElement
const content = getContainerContent()
const resolveScrollbarSize = (item: Scrollbar, attr: ScrollbarAttribute) => { const resolveScrollbarSize = (item: Scrollbar, attr: ScrollbarAttribute) => {
const containerSize: number = container[attr.clientSize] const containerSize: number = container[attr.clientSize]
const contentSize: number = content[attr.clientSize] const contentSize: number = content[attr.clientSize]
item.visible = props.scrollbar && contentSize > containerSize item.visible = props.scrollbar && contentSize > containerSize
item.size = Math.pow(containerSize, 2) / contentSize item.size = Math.max(Math.pow(containerSize, 2) / contentSize, 16)
} }
nextTick(() => { nextTick(() => {
resolveScrollbarSize(scrollbars.value.horizontal, scrollbarAttrs.horizontal) resolveScrollbarSize(scrollbars.value.horizontal, scrollbarAttrs.horizontal)
resolveScrollbarSize(scrollbars.value.vertical, scrollbarAttrs.vertical) resolveScrollbarSize(scrollbars.value.vertical, scrollbarAttrs.vertical)
}) })
}
const onContainerResize: ResizeObserverCallback = throttle((entries) => {
emit('resize', entries)
if (isDragging.value) return
calculateScrollThumbSize()
}) })
const onContentScroll = throttle((event: Event) => { const onContentScroll = throttle((event: Event) => {
@@ -131,7 +218,7 @@ const onContentScroll = throttle((event: Event) => {
if (isDragging.value) return if (isDragging.value) return
const container = event.target as HTMLDivElement const container = event.target as HTMLDivElement
const content = container.querySelector('[data-scroll-content]')! const content = getContainerContent()
const resolveOffset = (item: Scrollbar, attr: ScrollbarAttribute) => { const resolveOffset = (item: Scrollbar, attr: ScrollbarAttribute) => {
const containerSize = container[attr.clientSize] const containerSize = container[attr.clientSize]
@@ -145,6 +232,8 @@ const onContentScroll = throttle((event: Event) => {
resolveOffset(scrollbars.value.horizontal, scrollbarAttrs.horizontal) resolveOffset(scrollbars.value.horizontal, scrollbarAttrs.horizontal)
resolveOffset(scrollbars.value.vertical, scrollbarAttrs.vertical) resolveOffset(scrollbars.value.vertical, scrollbarAttrs.vertical)
calculateLoadItems()
}) })
const viewport = ref<HTMLElement>() const viewport = ref<HTMLElement>()
@@ -154,7 +243,7 @@ const prevDraggingEvent = ref<MouseEvent>()
const moveThumb = throttle((event: MouseEvent) => { const moveThumb = throttle((event: MouseEvent) => {
if (isDragging.value) { if (isDragging.value) {
const container = viewport.value! const container = viewport.value!
const content = container.querySelector('[data-scroll-content]')! const content = getContainerContent()
const resolveOffset = (item: Scrollbar, attr: ScrollbarAttribute) => { const resolveOffset = (item: Scrollbar, attr: ScrollbarAttribute) => {
const containerSize = container[attr.clientSize] const containerSize = container[attr.clientSize]
@@ -180,6 +269,8 @@ const moveThumb = throttle((event: MouseEvent) => {
scrollbarAttrs[scrollDirection], scrollbarAttrs[scrollDirection],
) )
prevDraggingEvent.value = event prevDraggingEvent.value = event
calculateLoadItems()
} }
}) })
@@ -204,6 +295,16 @@ const startDragThumb = (event: MouseEvent) => {
document.body.style.cursor = 'default' document.body.style.cursor = 'default'
} }
watch(
() => props.items,
() => {
init()
setSpacerSize()
calculateScrollThumbSize()
calculateLoadItems()
},
)
onUnmounted(() => { onUnmounted(() => {
stopMoveThumb() stopMoveThumb()
}) })

View File

@@ -8,7 +8,6 @@ import { app } from 'scripts/comfyAPI'
import { bytesToSize, formatDate, previewUrlToFile } from 'utils/common' import { bytesToSize, formatDate, previewUrlToFile } from 'utils/common'
import { ModelGrid } from 'utils/legacy' import { ModelGrid } from 'utils/legacy'
import { resolveModelType } from 'utils/model' import { resolveModelType } from 'utils/model'
// import {}
import { import {
computed, computed,
inject, inject,
@@ -22,10 +21,7 @@ import {
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
export const useModels = defineStore('models', () => { export const useModels = defineStore('models', () => {
const { data, refresh } = useRequest<(Model & { visible?: boolean })[]>( const { data, refresh } = useRequest<Model[]>('/models', { defaultValue: [] })
'/models',
{ defaultValue: [] },
)
const { toast, confirm } = useToast() const { toast, confirm } = useToast()
const { t } = useI18n() const { t } = useI18n()
const loading = useLoading() const loading = useLoading()