Skip to content
Merged
4 changes: 2 additions & 2 deletions src/components/sidebar/tabs/AssetSidebarTemplate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
<slot name="header" />
</div>
</div>
<!-- h-0 to force scrollpanel to grow -->
<ScrollPanel class="h-0 grow">
<!-- min-h-0 to force scrollpanel to grow -->
<ScrollPanel class="min-h-0 grow">
<slot name="body" />
</ScrollPanel>
<div v-if="slots.footer">
Expand Down
54 changes: 34 additions & 20 deletions src/components/sidebar/tabs/AssetsSidebarTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,35 @@
</TabList>
</template>
<template #body>
<div v-if="displayAssets.length" class="relative size-full">
<!-- Loading state -->
<div v-if="loading">
<ProgressSpinner class="absolute left-1/2 w-[50px] -translate-x-1/2" />
</div>
<!-- Empty state -->
<div v-else-if="!displayAssets.length">
<NoResultsPlaceholder
icon="pi pi-info-circle"
:title="
$t(
activeTab === 'input'
? 'sideToolbar.noImportedFiles'
: 'sideToolbar.noGeneratedFiles'
)
"
:message="$t('sideToolbar.noFilesFoundMessage')"
/>
</div>
<!-- Content -->
<div v-else class="relative size-full">
<VirtualGrid
v-if="displayAssets.length"
:items="mediaAssetsWithKey"
:grid-style="{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
padding: '0.5rem',
gap: '0.5rem'
}"
@approach-end="handleApproachEnd"
>
<template #item="{ item }">
<MediaAssetCard
Expand All @@ -66,24 +85,6 @@
/>
</template>
</VirtualGrid>
<div v-else-if="loading">
<ProgressSpinner
class="absolute left-1/2 w-[50px] -translate-x-1/2"
/>
</div>
<div v-else>
<NoResultsPlaceholder
icon="pi pi-info-circle"
:title="
$t(
activeTab === 'input'
? 'sideToolbar.noImportedFiles'
: 'sideToolbar.noGeneratedFiles'
)
"
:message="$t('sideToolbar.noFilesFoundMessage')"
/>
</div>
</div>
</template>
<template #footer>
Expand Down Expand Up @@ -147,6 +148,7 @@
</template>

<script setup lang="ts">
import { useDebounceFn } from '@vueuse/core'
import ProgressSpinner from 'primevue/progressspinner'
import { useToast } from 'primevue/usetoast'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
Expand Down Expand Up @@ -291,6 +293,7 @@ watch(
activeTab,
() => {
clearSelection()
// Reset pagination state when tab changes
void refreshAssets()
},
{ immediate: true }
Expand Down Expand Up @@ -395,4 +398,15 @@ const handleDeleteSelected = async () => {
await deleteMultipleAssets(selectedAssets)
clearSelection()
}

const handleApproachEnd = useDebounceFn(async () => {
if (
activeTab.value === 'output' &&
!isInFolderView.value &&
outputAssets.hasMore.value &&
!outputAssets.isLoadingMore.value
) {
await outputAssets.loadMore()
}
}, 300)
</script>
6 changes: 5 additions & 1 deletion src/platform/assets/components/Media3DBottom.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<template>
<div class="flex flex-col items-center gap-1">
<MediaTitle :file-name="fileName" />
<div class="flex items-center gap-2 text-xs text-zinc-400">
<!-- TBD: File size will be provided by backend history API -->
<div
v-if="asset.size"
class="flex items-center gap-2 text-xs text-zinc-400"
>
<span>{{ formatSize(asset.size) }}</span>
</div>
</div>
Expand Down
6 changes: 5 additions & 1 deletion src/platform/assets/components/MediaAudioBottom.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<template>
<div class="flex flex-col items-center gap-1">
<MediaTitle :file-name="fileName" />
<div class="flex items-center gap-2 text-xs text-zinc-400">
<!-- TBD: File size will be provided by backend history API -->
<div
v-if="asset.size"
class="flex items-center gap-2 text-xs text-zinc-400"
>
<span>{{ formatSize(asset.size) }}</span>
</div>
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/platform/assets/components/MediaVideoBottom.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<template>
<div class="flex flex-col items-center gap-1">
<MediaTitle :file-name="fileName" />
<div class="flex items-center text-xs text-zinc-400">
<!-- TBD: File size will be provided by backend history API -->
<div v-if="asset.size" class="flex items-center text-xs text-zinc-400">
<span>{{ formatSize(asset.size) }}</span>
</div>
</div>
Expand Down
6 changes: 5 additions & 1 deletion src/platform/assets/components/MediaVideoTop.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
<video
ref="videoRef"
:controls="shouldShowControls"
preload="none"
preload="metadata"
autoplay
muted
loop
playsinline
:poster="asset.preview_url"
class="relative size-full object-contain"
@click.stop
Expand Down
15 changes: 15 additions & 0 deletions src/platform/assets/composables/media/IAssetsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,19 @@ export interface IAssetsProvider {
* Refresh the media list (alias for fetchMediaList)
*/
refresh: () => Promise<AssetItem[]>

/**
* Load more items (for pagination)
*/
loadMore: () => Promise<void>

/**
* Whether there are more items to load
*/
hasMore: Ref<boolean>

/**
* Whether currently loading more items
*/
isLoadingMore: Ref<boolean>
}
19 changes: 18 additions & 1 deletion src/platform/assets/composables/media/useAssetsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,28 @@ export function useAssetsApi(directory: 'input' | 'output') {

const refresh = () => fetchMediaList()

const loadMore = async (): Promise<void> => {
if (directory === 'output') {
await assetsStore.loadMoreHistory()
}
}

const hasMore = computed(() => {
return directory === 'output' ? assetsStore.hasMoreHistory : false
})

const isLoadingMore = computed(() => {
return directory === 'output' ? assetsStore.isLoadingMore : false
})

return {
media,
loading,
error,
fetchMediaList,
refresh
refresh,
loadMore,
hasMore,
isLoadingMore
}
}
19 changes: 18 additions & 1 deletion src/platform/assets/composables/media/useInternalFilesApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,28 @@ export function useInternalFilesApi(directory: 'input' | 'output') {

const refresh = () => fetchMediaList()

const loadMore = async (): Promise<void> => {
if (directory === 'output') {
await assetsStore.loadMoreHistory()
}
}

const hasMore = computed(() => {
return directory === 'output' ? assetsStore.hasMoreHistory : false
})

const isLoadingMore = computed(() => {
return directory === 'output' ? assetsStore.isLoadingMore : false
})

return {
media,
loading,
error,
fetchMediaList,
refresh
refresh,
loadMore,
hasMore,
isLoadingMore
}
}
2 changes: 1 addition & 1 deletion src/platform/assets/schemas/assetSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const zAsset = z.object({
id: z.string(),
name: z.string(),
asset_hash: z.string().nullish(),
size: z.number(),
size: z.number().optional(), // TBD: Will be provided by history API in the future
mime_type: z.string().nullish(),
tags: z.array(z.string()).optional().default([]),
preview_id: z.string().nullable().optional(),
Expand Down
19 changes: 17 additions & 2 deletions src/platform/remote/comfyui/history/fetchers/fetchHistoryV1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,28 @@ import type {
* Fetches history from V1 API endpoint
* @param api - API instance with fetchApi method
* @param maxItems - Maximum number of history items to fetch
* @param offset - Offset for pagination (must be non-negative integer)
* @returns Promise resolving to V1 history response
* @throws Error if offset is invalid (negative or non-integer)
*/
export async function fetchHistoryV1(
fetchApi: (url: string) => Promise<Response>,
maxItems: number = 200
maxItems: number = 200,
offset?: number
): Promise<HistoryV1Response> {
const res = await fetchApi(`/history?max_items=${maxItems}`)
// Validate offset parameter
if (offset !== undefined && (offset < 0 || !Number.isInteger(offset))) {
throw new Error(
`Invalid offset parameter: ${offset}. Must be a non-negative integer.`
)
}

const params = new URLSearchParams({ max_items: maxItems.toString() })
if (offset !== undefined) {
params.set('offset', offset.toString())
}
const url = `/history?${params.toString()}`
const res = await fetchApi(url)
const json: Record<
string,
Omit<HistoryTaskItem, 'taskType'>
Expand Down
19 changes: 17 additions & 2 deletions src/platform/remote/comfyui/history/fetchers/fetchHistoryV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,28 @@ import type { HistoryResponseV2 } from '../types/historyV2Types'
* Fetches history from V2 API endpoint and adapts to V1 format
* @param fetchApi - API instance with fetchApi method
* @param maxItems - Maximum number of history items to fetch
* @param offset - Offset for pagination (must be non-negative integer)
* @returns Promise resolving to V1 history response (adapted from V2)
* @throws Error if offset is invalid (negative or non-integer)
*/
export async function fetchHistoryV2(
fetchApi: (url: string) => Promise<Response>,
maxItems: number = 200
maxItems: number = 200,
offset?: number
): Promise<HistoryV1Response> {
const res = await fetchApi(`/history_v2?max_items=${maxItems}`)
// Validate offset parameter
if (offset !== undefined && (offset < 0 || !Number.isInteger(offset))) {
throw new Error(
`Invalid offset parameter: ${offset}. Must be a non-negative integer.`
)
}

const params = new URLSearchParams({ max_items: maxItems.toString() })
if (offset !== undefined) {
params.set('offset', offset.toString())
}
const url = `/history_v2?${params.toString()}`
const res = await fetchApi(url)
const rawData: HistoryResponseV2 = await res.json()
const adaptedHistory = mapHistoryV2toHistory(rawData)
return { History: adaptedHistory }
Expand Down
9 changes: 7 additions & 2 deletions src/scripts/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -899,10 +899,15 @@ export class ComfyApi extends EventTarget {
* @returns Prompt history including node outputs
*/
async getHistory(
max_items: number = 200
max_items: number = 200,
options?: { offset?: number }
): Promise<{ History: HistoryTaskItem[] }> {
try {
return await fetchHistory(this.fetchApi.bind(this), max_items)
return await fetchHistory(
this.fetchApi.bind(this),
max_items,
options?.offset
)
} catch (error) {
console.error(error)
return { History: [] }
Expand Down
Loading