Skip to content

Commit 9d3ca76

Browse files
authored
Queue tab infinite scroll (#501)
* Add vueuse * Infinite scroll queue tab * Set item per page to 8 * Handle sidebar resize * nit
1 parent a45851d commit 9d3ca76

File tree

3 files changed

+217
-35
lines changed

3 files changed

+217
-35
lines changed

package-lock.json

Lines changed: 95 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"@comfyorg/litegraph": "^0.7.46",
6060
"@primevue/themes": "^4.0.0-rc.2",
6161
"@vitejs/plugin-vue": "^5.0.5",
62+
"@vueuse/core": "^11.0.0",
6263
"class-transformer": "^0.5.1",
6364
"dotenv": "^16.4.5",
6465
"fuse.js": "^7.0.0",

src/components/sidebar/tabs/QueueSidebarTab.vue

Lines changed: 121 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
:icon="isExpanded ? 'pi pi-chevron-up' : 'pi pi-chevron-down'"
66
text
77
severity="secondary"
8-
@click="isExpanded = !isExpanded"
8+
@click="toggleExpanded"
99
class="toggle-expanded-button"
1010
v-tooltip="$t('sideToolbar.queueTab.showFlatList')"
1111
/>
@@ -18,14 +18,21 @@
1818
/>
1919
</template>
2020
<template #body>
21-
<div v-if="tasks.length > 0" class="queue-grid">
22-
<TaskItem
23-
v-for="task in tasks"
24-
:key="task.key"
25-
:task="task"
26-
:isFlatTask="isExpanded"
27-
@contextmenu="handleContextMenu"
28-
/>
21+
<div
22+
v-if="visibleTasks.length > 0"
23+
ref="scrollContainer"
24+
class="scroll-container"
25+
>
26+
<div class="queue-grid">
27+
<TaskItem
28+
v-for="task in visibleTasks"
29+
:key="task.key"
30+
:task="task"
31+
:isFlatTask="isExpanded"
32+
@contextmenu="handleContextMenu"
33+
/>
34+
</div>
35+
<div ref="loadMoreTrigger" style="height: 1px" />
2936
</div>
3037
<div v-else>
3138
<NoResultsPlaceholder
@@ -41,29 +48,79 @@
4148
</template>
4249

4350
<script setup lang="ts">
51+
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
52+
import { useInfiniteScroll, useResizeObserver } from '@vueuse/core'
53+
import { useI18n } from 'vue-i18n'
54+
import { useConfirm } from 'primevue/useconfirm'
55+
import { useToast } from 'primevue/usetoast'
4456
import Button from 'primevue/button'
4557
import ConfirmPopup from 'primevue/confirmpopup'
4658
import ContextMenu from 'primevue/contextmenu'
59+
import type { MenuItem } from 'primevue/menuitem'
4760
import TaskItem from './queue/TaskItem.vue'
4861
import SideBarTabTemplate from './SidebarTabTemplate.vue'
4962
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
50-
import { useConfirm } from 'primevue/useconfirm'
51-
import { useToast } from 'primevue/usetoast'
5263
import { TaskItemImpl, useQueueStore } from '@/stores/queueStore'
53-
import { computed, onMounted, onUnmounted, ref } from 'vue'
54-
import { useI18n } from 'vue-i18n'
55-
import type { MenuItem } from 'primevue/menuitem'
5664
import { api } from '@/scripts/api'
5765
5866
const confirm = useConfirm()
5967
const toast = useToast()
6068
const queueStore = useQueueStore()
6169
const { t } = useI18n()
6270
63-
const tasks = computed(() =>
71+
const isExpanded = ref(false)
72+
const visibleTasks = ref<TaskItemImpl[]>([])
73+
const scrollContainer = ref<HTMLElement | null>(null)
74+
const loadMoreTrigger = ref<HTMLElement | null>(null)
75+
76+
const ITEMS_PER_PAGE = 8
77+
const SCROLL_THRESHOLD = 100 // pixels from bottom to trigger load
78+
79+
const allTasks = computed(() =>
6480
isExpanded.value ? queueStore.flatTasks : queueStore.tasks
6581
)
6682
83+
const loadMoreItems = () => {
84+
const currentLength = visibleTasks.value.length
85+
const newTasks = allTasks.value.slice(
86+
currentLength,
87+
currentLength + ITEMS_PER_PAGE
88+
)
89+
visibleTasks.value.push(...newTasks)
90+
}
91+
92+
const checkAndLoadMore = () => {
93+
if (!scrollContainer.value) return
94+
95+
const { scrollHeight, scrollTop, clientHeight } = scrollContainer.value
96+
if (scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD) {
97+
loadMoreItems()
98+
}
99+
}
100+
101+
useInfiniteScroll(
102+
scrollContainer,
103+
() => {
104+
if (visibleTasks.value.length < allTasks.value.length) {
105+
loadMoreItems()
106+
}
107+
},
108+
{ distance: SCROLL_THRESHOLD }
109+
)
110+
111+
// Use ResizeObserver to detect container size changes
112+
// This is necessary as the sidebar tab can change size when user drags the splitter.
113+
useResizeObserver(scrollContainer, () => {
114+
nextTick(() => {
115+
checkAndLoadMore()
116+
})
117+
})
118+
119+
const toggleExpanded = () => {
120+
isExpanded.value = !isExpanded.value
121+
visibleTasks.value = allTasks.value.slice(0, ITEMS_PER_PAGE)
122+
}
123+
67124
const removeTask = (task: TaskItemImpl) => {
68125
if (task.isRunning) {
69126
api.interrupt()
@@ -75,9 +132,9 @@ const removeAllTasks = async () => {
75132
await queueStore.clear()
76133
}
77134
78-
const confirmRemoveAll = (event) => {
135+
const confirmRemoveAll = (event: Event) => {
79136
confirm.require({
80-
target: event.currentTarget,
137+
target: event.currentTarget as HTMLElement,
81138
message: 'Do you want to delete all tasks?',
82139
icon: 'pi pi-info-circle',
83140
rejectProps: {
@@ -101,27 +158,35 @@ const confirmRemoveAll = (event) => {
101158
})
102159
}
103160
104-
const onStatus = () => queueStore.update()
161+
const onStatus = () => {
162+
queueStore.update()
163+
visibleTasks.value = allTasks.value.slice(0, ITEMS_PER_PAGE)
164+
}
105165
106166
const menu = ref(null)
107167
const menuTargetTask = ref<TaskItemImpl | null>(null)
108-
const menuItems = computed<MenuItem[]>(() => {
109-
return [
110-
{
111-
label: t('delete'),
112-
icon: 'pi pi-trash',
113-
command: () => removeTask(menuTargetTask.value!)
114-
},
115-
{
116-
label: t('loadWorkflow'),
117-
icon: 'pi pi-file-export',
118-
command: () => menuTargetTask.value?.loadWorkflow()
119-
}
120-
]
121-
})
122-
const handleContextMenu = ({ task, event }) => {
168+
const menuItems = computed<MenuItem[]>(() => [
169+
{
170+
label: t('delete'),
171+
icon: 'pi pi-trash',
172+
command: () => menuTargetTask.value && removeTask(menuTargetTask.value)
173+
},
174+
{
175+
label: t('loadWorkflow'),
176+
icon: 'pi pi-file-export',
177+
command: () => menuTargetTask.value?.loadWorkflow()
178+
}
179+
])
180+
181+
const handleContextMenu = ({
182+
task,
183+
event
184+
}: {
185+
task: TaskItemImpl
186+
event: Event
187+
}) => {
123188
menuTargetTask.value = task
124-
menu.value.show(event)
189+
menu.value?.show(event)
125190
}
126191
127192
onMounted(() => {
@@ -133,10 +198,31 @@ onUnmounted(() => {
133198
api.removeEventListener('status', onStatus)
134199
})
135200
136-
const isExpanded = ref(false)
201+
// Watch for changes in allTasks and reset visibleTasks if necessary
202+
watch(
203+
allTasks,
204+
(newTasks) => {
205+
if (
206+
visibleTasks.value.length === 0 ||
207+
visibleTasks.value.length > newTasks.length
208+
) {
209+
visibleTasks.value = newTasks.slice(0, ITEMS_PER_PAGE)
210+
}
211+
212+
nextTick(() => {
213+
checkAndLoadMore()
214+
})
215+
},
216+
{ immediate: true }
217+
)
137218
</script>
138219

139220
<style scoped>
221+
.scroll-container {
222+
height: 100%;
223+
overflow-y: auto;
224+
}
225+
140226
.queue-grid {
141227
display: grid;
142228
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));

0 commit comments

Comments
 (0)