Skip to content

Commit a7ee88c

Browse files
viva-jinyiclaude
andcommitted
feat: Add pagination support for media assets history
- Add offset parameter to history API endpoints - Implement loadMore functionality in assetsStore - Add approach-end handler in AssetsSidebarTab for infinite scroll - Update composables to support pagination state - Support both V1 and V2 history APIs with offset This enables efficient loading of large history lists by fetching items in batches as the user scrolls. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 6f068c8 commit a7ee88c

File tree

9 files changed

+170
-40
lines changed

9 files changed

+170
-40
lines changed

src/components/sidebar/tabs/AssetSidebarTemplate.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
<slot name="header" />
1515
</div>
1616
</div>
17-
<!-- h-0 to force scrollpanel to grow -->
18-
<ScrollPanel class="h-0 grow">
17+
<!-- min-h-0 to force scrollpanel to grow -->
18+
<ScrollPanel class="min-h-0 grow">
1919
<slot name="body" />
2020
</ScrollPanel>
2121
<div v-if="slots.footer">

src/components/sidebar/tabs/AssetsSidebarTab.vue

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,15 @@
4343
<template #body>
4444
<div v-if="displayAssets.length" class="relative size-full">
4545
<VirtualGrid
46-
v-if="displayAssets.length"
46+
v-if="!loading"
4747
:items="mediaAssetsWithKey"
4848
:grid-style="{
4949
display: 'grid',
5050
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
5151
padding: '0.5rem',
5252
gap: '0.5rem'
5353
}"
54+
@approach-end="handleApproachEnd"
5455
>
5556
<template #item="{ item }">
5657
<MediaAssetCard
@@ -66,24 +67,24 @@
6667
/>
6768
</template>
6869
</VirtualGrid>
69-
<div v-else-if="loading">
70+
<div v-else>
7071
<ProgressSpinner
7172
class="absolute left-1/2 w-[50px] -translate-x-1/2"
7273
/>
7374
</div>
74-
<div v-else>
75-
<NoResultsPlaceholder
76-
icon="pi pi-info-circle"
77-
:title="
78-
$t(
79-
activeTab === 'input'
80-
? 'sideToolbar.noImportedFiles'
81-
: 'sideToolbar.noGeneratedFiles'
82-
)
83-
"
84-
:message="$t('sideToolbar.noFilesFoundMessage')"
85-
/>
86-
</div>
75+
</div>
76+
<div v-else>
77+
<NoResultsPlaceholder
78+
icon="pi pi-info-circle"
79+
:title="
80+
$t(
81+
activeTab === 'input'
82+
? 'sideToolbar.noImportedFiles'
83+
: 'sideToolbar.noGeneratedFiles'
84+
)
85+
"
86+
:message="$t('sideToolbar.noFilesFoundMessage')"
87+
/>
8788
</div>
8889
</template>
8990
<template #footer>
@@ -291,6 +292,7 @@ watch(
291292
activeTab,
292293
() => {
293294
clearSelection()
295+
// Reset pagination state when tab changes
294296
void refreshAssets()
295297
},
296298
{ immediate: true }
@@ -395,4 +397,16 @@ const handleDeleteSelected = async () => {
395397
await deleteMultipleAssets(selectedAssets)
396398
clearSelection()
397399
}
400+
401+
const handleApproachEnd = async () => {
402+
if (
403+
activeTab.value === 'output' &&
404+
!isInFolderView.value &&
405+
outputAssets.loadMore &&
406+
outputAssets.hasMore?.value &&
407+
!outputAssets.isLoadingMore?.value
408+
) {
409+
await outputAssets.loadMore()
410+
}
411+
}
398412
</script>

src/platform/assets/composables/media/IAssetsProvider.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,19 @@ export interface IAssetsProvider {
2626
* Refresh the media list (alias for fetchMediaList)
2727
*/
2828
refresh: () => Promise<AssetItem[]>
29+
30+
/**
31+
* Load more items (for pagination)
32+
*/
33+
loadMore?: () => Promise<void>
34+
35+
/**
36+
* Whether there are more items to load
37+
*/
38+
hasMore?: Ref<boolean>
39+
40+
/**
41+
* Whether currently loading more items
42+
*/
43+
isLoadingMore?: Ref<boolean>
2944
}

src/platform/assets/composables/media/useAssetsApi.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,28 @@ export function useAssetsApi(directory: 'input' | 'output') {
3636

3737
const refresh = () => fetchMediaList()
3838

39+
const loadMore = async (): Promise<void> => {
40+
if (directory === 'output') {
41+
await assetsStore.loadMoreHistory()
42+
}
43+
}
44+
45+
const hasMore = computed(() => {
46+
return directory === 'output' ? assetsStore.hasMoreHistory : false
47+
})
48+
49+
const isLoadingMore = computed(() => {
50+
return directory === 'output' ? assetsStore.isLoadingMore : false
51+
})
52+
3953
return {
4054
media,
4155
loading,
4256
error,
4357
fetchMediaList,
44-
refresh
58+
refresh,
59+
loadMore,
60+
hasMore,
61+
isLoadingMore
4562
}
4663
}

src/platform/assets/composables/media/useInternalFilesApi.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,28 @@ export function useInternalFilesApi(directory: 'input' | 'output') {
3636

3737
const refresh = () => fetchMediaList()
3838

39+
const loadMore = async (): Promise<void> => {
40+
if (directory === 'output') {
41+
await assetsStore.loadMoreHistory()
42+
}
43+
}
44+
45+
const hasMore = computed(() => {
46+
return directory === 'output' ? assetsStore.hasMoreHistory : false
47+
})
48+
49+
const isLoadingMore = computed(() => {
50+
return directory === 'output' ? assetsStore.isLoadingMore : false
51+
})
52+
3953
return {
4054
media,
4155
loading,
4256
error,
4357
fetchMediaList,
44-
refresh
58+
refresh,
59+
loadMore,
60+
hasMore,
61+
isLoadingMore
4562
}
4663
}

src/platform/remote/comfyui/history/fetchers/fetchHistoryV1.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,14 @@ import type {
1919
*/
2020
export async function fetchHistoryV1(
2121
fetchApi: (url: string) => Promise<Response>,
22-
maxItems: number = 200
22+
maxItems: number = 200,
23+
offset?: number
2324
): Promise<HistoryV1Response> {
24-
const res = await fetchApi(`/history?max_items=${maxItems}`)
25+
let url = `/history?max_items=${maxItems}`
26+
if (offset !== undefined) {
27+
url += `&offset=${offset}`
28+
}
29+
const res = await fetchApi(url)
2530
const json: Record<
2631
string,
2732
Omit<HistoryTaskItem, 'taskType'>

src/platform/remote/comfyui/history/fetchers/fetchHistoryV2.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@ import type { HistoryResponseV2 } from '../types/historyV2Types'
1818
*/
1919
export async function fetchHistoryV2(
2020
fetchApi: (url: string) => Promise<Response>,
21-
maxItems: number = 200
21+
maxItems: number = 200,
22+
offset?: number
2223
): Promise<HistoryV1Response> {
23-
const res = await fetchApi(`/history_v2?max_items=${maxItems}`)
24+
let url = `/history_v2?max_items=${maxItems}`
25+
if (offset !== undefined) {
26+
url += `&offset=${offset}`
27+
}
28+
const res = await fetchApi(url)
2429
const rawData: HistoryResponseV2 = await res.json()
2530
const adaptedHistory = mapHistoryV2toHistory(rawData)
2631
return { History: adaptedHistory }

src/scripts/api.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -898,10 +898,15 @@ export class ComfyApi extends EventTarget {
898898
* @returns Prompt history including node outputs
899899
*/
900900
async getHistory(
901-
max_items: number = 200
901+
max_items: number = 200,
902+
options?: { offset?: number }
902903
): Promise<{ History: HistoryTaskItem[] }> {
903904
try {
904-
return await fetchHistory(this.fetchApi.bind(this), max_items)
905+
return await fetchHistory(
906+
this.fetchApi.bind(this),
907+
max_items,
908+
options?.offset
909+
)
905910
} catch (error) {
906911
console.error(error)
907912
return { History: [] }

src/stores/assetsStore.ts

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useAsyncState } from '@vueuse/core'
22
import { defineStore } from 'pinia'
3+
import { ref } from 'vue'
34

45
import {
56
mapInputFileToAssetItem,
@@ -79,8 +80,13 @@ function mapHistoryToAssets(historyItems: any[]): AssetItem[] {
7980
)
8081
}
8182

83+
const BATCH_SIZE = 200
84+
8285
export const useAssetsStore = defineStore('assets', () => {
83-
const maxHistoryItems = 200
86+
const historyOffset = ref(0)
87+
const hasMoreHistory = ref(true)
88+
const isLoadingMore = ref(false)
89+
const allHistoryItems = ref<AssetItem[]>([])
8490

8591
const fetchInputFiles = isCloud
8692
? fetchInputFilesFromCloud
@@ -99,23 +105,66 @@ export const useAssetsStore = defineStore('assets', () => {
99105
}
100106
})
101107

102-
const fetchHistoryAssets = async (): Promise<AssetItem[]> => {
103-
const history = await api.getHistory(maxHistoryItems)
104-
return mapHistoryToAssets(history.History)
108+
const fetchHistoryAssets = async (loadMore = false): Promise<AssetItem[]> => {
109+
if (!loadMore) {
110+
historyOffset.value = 0
111+
hasMoreHistory.value = true
112+
allHistoryItems.value = []
113+
}
114+
115+
const history = await api.getHistory(BATCH_SIZE, {
116+
offset: historyOffset.value
117+
})
118+
const newAssets = mapHistoryToAssets(history.History)
119+
120+
if (loadMore) {
121+
const existingIds = new Set(allHistoryItems.value.map((item) => item.id))
122+
const uniqueNewAssets = newAssets.filter(
123+
(item) => !existingIds.has(item.id)
124+
)
125+
allHistoryItems.value = [...allHistoryItems.value, ...uniqueNewAssets]
126+
} else {
127+
allHistoryItems.value = newAssets
128+
}
129+
130+
hasMoreHistory.value = newAssets.length === BATCH_SIZE
131+
historyOffset.value += newAssets.length
132+
133+
return allHistoryItems.value
105134
}
106135

107-
const {
108-
state: historyAssets,
109-
isLoading: historyLoading,
110-
error: historyError,
111-
execute: updateHistory
112-
} = useAsyncState(fetchHistoryAssets, [], {
113-
immediate: false,
114-
resetOnExecute: false,
115-
onError: (err) => {
136+
const historyAssets = ref<AssetItem[]>([])
137+
const historyLoading = ref(false)
138+
const historyError = ref<unknown>(null)
139+
140+
const updateHistory = async () => {
141+
historyLoading.value = true
142+
historyError.value = null
143+
try {
144+
const assets = await fetchHistoryAssets(false)
145+
historyAssets.value = assets
146+
} catch (err) {
116147
console.error('Error fetching history assets:', err)
148+
historyError.value = err
149+
} finally {
150+
historyLoading.value = false
117151
}
118-
})
152+
}
153+
154+
const loadMoreHistory = async () => {
155+
if (!hasMoreHistory.value || isLoadingMore.value) return
156+
157+
isLoadingMore.value = true
158+
try {
159+
const updatedAssets = await fetchHistoryAssets(true)
160+
historyAssets.value = updatedAssets
161+
} catch (err) {
162+
console.error('Error loading more history:', err)
163+
historyError.value = err
164+
} finally {
165+
isLoadingMore.value = false
166+
}
167+
}
119168

120169
return {
121170
// States
@@ -125,9 +174,12 @@ export const useAssetsStore = defineStore('assets', () => {
125174
historyLoading,
126175
inputError,
127176
historyError,
177+
hasMoreHistory,
178+
isLoadingMore,
128179

129180
// Actions
130181
updateInputs,
131-
updateHistory
182+
updateHistory,
183+
loadMoreHistory
132184
}
133185
})

0 commit comments

Comments
 (0)