mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-10 07:30:08 +00:00
Queue tab infinite scroll (#501)
* Add vueuse * Infinite scroll queue tab * Set item per page to 8 * Handle sidebar resize * nit
This commit is contained in:
95
package-lock.json
generated
95
package-lock.json
generated
@@ -12,6 +12,7 @@
|
||||
"@comfyorg/litegraph": "^0.7.46",
|
||||
"@primevue/themes": "^4.0.0-rc.2",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"fuse.js": "^7.0.0",
|
||||
@@ -3773,6 +3774,12 @@
|
||||
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
|
||||
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
|
||||
@@ -4211,6 +4218,94 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.0.0.tgz",
|
||||
"integrity": "sha512-shibzNGjmRjZucEm97B8V0NO5J3vPHMCE/mltxQ3vHezbDoFQBMtK11XsfwfPionxSbo+buqPmsCljtYuXIBpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.20",
|
||||
"@vueuse/metadata": "11.0.0",
|
||||
"@vueuse/shared": "11.0.0",
|
||||
"vue-demi": ">=0.14.10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.0.0.tgz",
|
||||
"integrity": "sha512-0TKsAVT0iUOAPWyc9N79xWYfovJVPATiOPVKByG6jmAYdDiwvMVm9xXJ5hp4I8nZDxpCcYlLq/Rg9w1Z/jrGcg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.0.0.tgz",
|
||||
"integrity": "sha512-i4ZmOrIEjSsL94uAEt3hz88UCz93fMyP/fba9S+vypX90fKg3uYX9cThqvWc9aXxuTzR0UGhOKOTQd//Goh1nQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vue-demi": ">=0.14.10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/abab": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
"@comfyorg/litegraph": "^0.7.46",
|
||||
"@primevue/themes": "^4.0.0-rc.2",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"fuse.js": "^7.0.0",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:icon="isExpanded ? 'pi pi-chevron-up' : 'pi pi-chevron-down'"
|
||||
text
|
||||
severity="secondary"
|
||||
@click="isExpanded = !isExpanded"
|
||||
@click="toggleExpanded"
|
||||
class="toggle-expanded-button"
|
||||
v-tooltip="$t('sideToolbar.queueTab.showFlatList')"
|
||||
/>
|
||||
@@ -18,14 +18,21 @@
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<div v-if="tasks.length > 0" class="queue-grid">
|
||||
<TaskItem
|
||||
v-for="task in tasks"
|
||||
:key="task.key"
|
||||
:task="task"
|
||||
:isFlatTask="isExpanded"
|
||||
@contextmenu="handleContextMenu"
|
||||
/>
|
||||
<div
|
||||
v-if="visibleTasks.length > 0"
|
||||
ref="scrollContainer"
|
||||
class="scroll-container"
|
||||
>
|
||||
<div class="queue-grid">
|
||||
<TaskItem
|
||||
v-for="task in visibleTasks"
|
||||
:key="task.key"
|
||||
:task="task"
|
||||
:isFlatTask="isExpanded"
|
||||
@contextmenu="handleContextMenu"
|
||||
/>
|
||||
</div>
|
||||
<div ref="loadMoreTrigger" style="height: 1px" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<NoResultsPlaceholder
|
||||
@@ -41,18 +48,19 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import { useInfiniteScroll, useResizeObserver } from '@vueuse/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useConfirm } from 'primevue/useconfirm'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import Button from 'primevue/button'
|
||||
import ConfirmPopup from 'primevue/confirmpopup'
|
||||
import ContextMenu from 'primevue/contextmenu'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import TaskItem from './queue/TaskItem.vue'
|
||||
import SideBarTabTemplate from './SidebarTabTemplate.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import { useConfirm } from 'primevue/useconfirm'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { TaskItemImpl, useQueueStore } from '@/stores/queueStore'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
const confirm = useConfirm()
|
||||
@@ -60,10 +68,59 @@ const toast = useToast()
|
||||
const queueStore = useQueueStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const tasks = computed(() =>
|
||||
const isExpanded = ref(false)
|
||||
const visibleTasks = ref<TaskItemImpl[]>([])
|
||||
const scrollContainer = ref<HTMLElement | null>(null)
|
||||
const loadMoreTrigger = ref<HTMLElement | null>(null)
|
||||
|
||||
const ITEMS_PER_PAGE = 8
|
||||
const SCROLL_THRESHOLD = 100 // pixels from bottom to trigger load
|
||||
|
||||
const allTasks = computed(() =>
|
||||
isExpanded.value ? queueStore.flatTasks : queueStore.tasks
|
||||
)
|
||||
|
||||
const loadMoreItems = () => {
|
||||
const currentLength = visibleTasks.value.length
|
||||
const newTasks = allTasks.value.slice(
|
||||
currentLength,
|
||||
currentLength + ITEMS_PER_PAGE
|
||||
)
|
||||
visibleTasks.value.push(...newTasks)
|
||||
}
|
||||
|
||||
const checkAndLoadMore = () => {
|
||||
if (!scrollContainer.value) return
|
||||
|
||||
const { scrollHeight, scrollTop, clientHeight } = scrollContainer.value
|
||||
if (scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD) {
|
||||
loadMoreItems()
|
||||
}
|
||||
}
|
||||
|
||||
useInfiniteScroll(
|
||||
scrollContainer,
|
||||
() => {
|
||||
if (visibleTasks.value.length < allTasks.value.length) {
|
||||
loadMoreItems()
|
||||
}
|
||||
},
|
||||
{ distance: SCROLL_THRESHOLD }
|
||||
)
|
||||
|
||||
// Use ResizeObserver to detect container size changes
|
||||
// This is necessary as the sidebar tab can change size when user drags the splitter.
|
||||
useResizeObserver(scrollContainer, () => {
|
||||
nextTick(() => {
|
||||
checkAndLoadMore()
|
||||
})
|
||||
})
|
||||
|
||||
const toggleExpanded = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
visibleTasks.value = allTasks.value.slice(0, ITEMS_PER_PAGE)
|
||||
}
|
||||
|
||||
const removeTask = (task: TaskItemImpl) => {
|
||||
if (task.isRunning) {
|
||||
api.interrupt()
|
||||
@@ -75,9 +132,9 @@ const removeAllTasks = async () => {
|
||||
await queueStore.clear()
|
||||
}
|
||||
|
||||
const confirmRemoveAll = (event) => {
|
||||
const confirmRemoveAll = (event: Event) => {
|
||||
confirm.require({
|
||||
target: event.currentTarget,
|
||||
target: event.currentTarget as HTMLElement,
|
||||
message: 'Do you want to delete all tasks?',
|
||||
icon: 'pi pi-info-circle',
|
||||
rejectProps: {
|
||||
@@ -101,27 +158,35 @@ const confirmRemoveAll = (event) => {
|
||||
})
|
||||
}
|
||||
|
||||
const onStatus = () => queueStore.update()
|
||||
const onStatus = () => {
|
||||
queueStore.update()
|
||||
visibleTasks.value = allTasks.value.slice(0, ITEMS_PER_PAGE)
|
||||
}
|
||||
|
||||
const menu = ref(null)
|
||||
const menuTargetTask = ref<TaskItemImpl | null>(null)
|
||||
const menuItems = computed<MenuItem[]>(() => {
|
||||
return [
|
||||
{
|
||||
label: t('delete'),
|
||||
icon: 'pi pi-trash',
|
||||
command: () => removeTask(menuTargetTask.value!)
|
||||
},
|
||||
{
|
||||
label: t('loadWorkflow'),
|
||||
icon: 'pi pi-file-export',
|
||||
command: () => menuTargetTask.value?.loadWorkflow()
|
||||
}
|
||||
]
|
||||
})
|
||||
const handleContextMenu = ({ task, event }) => {
|
||||
const menuItems = computed<MenuItem[]>(() => [
|
||||
{
|
||||
label: t('delete'),
|
||||
icon: 'pi pi-trash',
|
||||
command: () => menuTargetTask.value && removeTask(menuTargetTask.value)
|
||||
},
|
||||
{
|
||||
label: t('loadWorkflow'),
|
||||
icon: 'pi pi-file-export',
|
||||
command: () => menuTargetTask.value?.loadWorkflow()
|
||||
}
|
||||
])
|
||||
|
||||
const handleContextMenu = ({
|
||||
task,
|
||||
event
|
||||
}: {
|
||||
task: TaskItemImpl
|
||||
event: Event
|
||||
}) => {
|
||||
menuTargetTask.value = task
|
||||
menu.value.show(event)
|
||||
menu.value?.show(event)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -133,10 +198,31 @@ onUnmounted(() => {
|
||||
api.removeEventListener('status', onStatus)
|
||||
})
|
||||
|
||||
const isExpanded = ref(false)
|
||||
// Watch for changes in allTasks and reset visibleTasks if necessary
|
||||
watch(
|
||||
allTasks,
|
||||
(newTasks) => {
|
||||
if (
|
||||
visibleTasks.value.length === 0 ||
|
||||
visibleTasks.value.length > newTasks.length
|
||||
) {
|
||||
visibleTasks.value = newTasks.slice(0, ITEMS_PER_PAGE)
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
checkAndLoadMore()
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.scroll-container {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.queue-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
|
||||
Reference in New Issue
Block a user