-
Notifications
You must be signed in to change notification settings - Fork 449
Scroll templates better #4584
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
christian-byrne
merged 11 commits into
Comfy-Org:main
from
Myestery:scroll-templates-better
Aug 6, 2025
Merged
Scroll templates better #4584
Changes from 10 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
939b94f
feat: add lazy loading image component and skeleton for workflow cards
Myestery 0b2d985
feat: implement LazyImage component for thumbnails and search bar
Myestery 6b69225
feat: add useIntersectionObserver, useLazyPagination, useTemplateFilt…
Myestery d6074cd
feat: add clearFilters, loadingMore, and searchPlaceholder translatio…
Myestery 7e6a3cd
Merge branch 'main' into scroll-templates-better
Myestery bfdad0e
Merge branch 'main' into scroll-templates-better
Myestery e46f682
[fix] Improve lazy loading logic in LazyImage component
Myestery cc67ee0
Merge branch 'main' into scroll-templates-better
Myestery 650e9d0
Merge branch 'main' into scroll-templates-better
Myestery fbc44b3
fix unit tests
Myestery 6316dde
fix PR comments
Myestery File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| <template> | ||
| <div | ||
| ref="containerRef" | ||
| class="relative overflow-hidden w-full h-full flex items-center justify-center" | ||
| > | ||
| <Skeleton | ||
| v-if="!isImageLoaded" | ||
| width="100%" | ||
| height="100%" | ||
| class="absolute inset-0" | ||
| /> | ||
| <img | ||
| v-show="isImageLoaded" | ||
Myestery marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ref="imageRef" | ||
| :src="cachedSrc" | ||
| :alt="alt" | ||
| draggable="false" | ||
| :class="imageClass" | ||
| :style="imageStyle" | ||
| @load="onImageLoad" | ||
| @error="onImageError" | ||
| /> | ||
| <div | ||
| v-if="hasError" | ||
| class="absolute inset-0 flex items-center justify-center bg-surface-50 dark-theme:bg-surface-800 text-muted" | ||
| > | ||
| <i class="pi pi-image text-2xl" /> | ||
| </div> | ||
| </div> | ||
| </template> | ||
|
|
||
| <script setup lang="ts"> | ||
| import Skeleton from 'primevue/skeleton' | ||
| import { computed, ref, watch } from 'vue' | ||
| import { useIntersectionObserver } from '@/composables/useIntersectionObserver' | ||
| import { useMediaCache } from '@/services/mediaCacheService' | ||
| const { | ||
| src, | ||
| alt = '', | ||
| imageClass = '', | ||
| imageStyle, | ||
| rootMargin = '50px' | ||
Myestery marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } = defineProps<{ | ||
| src: string | ||
| alt?: string | ||
| imageClass?: string | string[] | Record<string, boolean> | ||
| imageStyle?: Record<string, any> | ||
| rootMargin?: string | ||
| }>() | ||
| const containerRef = ref<HTMLElement | null>(null) | ||
| const imageRef = ref<HTMLImageElement | null>(null) | ||
| const isIntersecting = ref(false) | ||
| const isImageLoaded = ref(false) | ||
| const hasError = ref(false) | ||
| const cachedSrc = ref<string | undefined>(undefined) | ||
| const { getCachedMedia } = useMediaCache() | ||
| // Use intersection observer to detect when the image container comes into view | ||
| useIntersectionObserver( | ||
| containerRef, | ||
| (entries) => { | ||
| const entry = entries[0] | ||
| isIntersecting.value = entry?.isIntersecting ?? false | ||
| }, | ||
| { | ||
| rootMargin, | ||
| threshold: 0.1 | ||
| } | ||
| ) | ||
| // Only start loading the image when it's in view | ||
| const shouldLoad = computed(() => isIntersecting.value) | ||
| // Watch for when we should load and handle caching | ||
| watch( | ||
| shouldLoad, | ||
| async (shouldLoad) => { | ||
| if (shouldLoad && src && !cachedSrc.value && !hasError.value) { | ||
| try { | ||
| const cachedMedia = await getCachedMedia(src) | ||
| if (cachedMedia.error) { | ||
| hasError.value = true | ||
| } else if (cachedMedia.objectUrl) { | ||
| cachedSrc.value = cachedMedia.objectUrl | ||
| } else { | ||
| cachedSrc.value = src | ||
| } | ||
| } catch (error) { | ||
| console.warn('Failed to load cached media:', error) | ||
| // Fallback to original src | ||
| cachedSrc.value = src | ||
| } | ||
| } else if (!shouldLoad) { | ||
| // Hide image when out of view | ||
| isImageLoaded.value = false | ||
| cachedSrc.value = undefined | ||
| hasError.value = false | ||
| } | ||
| }, | ||
| { immediate: true } | ||
| ) | ||
| const onImageLoad = () => { | ||
| isImageLoaded.value = true | ||
| hasError.value = false | ||
| } | ||
| const onImageError = () => { | ||
| hasError.value = true | ||
| isImageLoaded.value = false | ||
| } | ||
| </script> | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| <template> | ||
| <div class="relative w-full p-4"> | ||
| <div class="h-12 flex items-center gap-4 justify-between"> | ||
| <div class="flex-1 max-w-md"> | ||
| <AutoComplete | ||
| v-model.lazy="searchQuery" | ||
| :placeholder="$t('templateWorkflows.searchPlaceholder')" | ||
| :complete-on-focus="false" | ||
| :delay="200" | ||
| class="w-full" | ||
| :pt="{ | ||
| pcInputText: { | ||
| root: { | ||
| class: 'w-full rounded-2xl' | ||
| } | ||
| }, | ||
| loader: { | ||
| style: 'display: none' | ||
| } | ||
| }" | ||
| :show-empty-message="false" | ||
| @complete="() => {}" | ||
| /> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div class="flex items-center gap-4 mt-2"> | ||
| <small | ||
| v-if="searchQuery && filteredCount !== null" | ||
| class="text-color-secondary" | ||
| > | ||
| {{ $t('g.resultsCount', { count: filteredCount }) }} | ||
| </small> | ||
| <Button | ||
Myestery marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| v-if="searchQuery" | ||
| text | ||
| size="small" | ||
| icon="pi pi-times" | ||
| :label="$t('g.clearFilters')" | ||
| @click="clearFilters" | ||
| /> | ||
| </div> | ||
| </div> | ||
| </template> | ||
|
|
||
| <script setup lang="ts"> | ||
| import AutoComplete from 'primevue/autocomplete' | ||
| import Button from 'primevue/button' | ||
| const { filteredCount } = defineProps<{ | ||
| filteredCount?: number | null | ||
| }>() | ||
| const searchQuery = defineModel<string>('searchQuery', { default: '' }) | ||
| const emit = defineEmits<{ | ||
| clearFilters: [] | ||
| }>() | ||
| const clearFilters = () => { | ||
| searchQuery.value = '' | ||
| emit('clearFilters') | ||
| } | ||
| </script> | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| <template> | ||
| <Card | ||
| class="w-64 template-card rounded-2xl overflow-hidden shadow-elevation-2 dark-theme:bg-dark-elevation-1.5 h-full" | ||
| :pt="{ | ||
| body: { class: 'p-0 h-full flex flex-col' } | ||
| }" | ||
| > | ||
| <template #header> | ||
| <div class="flex items-center justify-center"> | ||
| <div class="relative overflow-hidden rounded-t-lg"> | ||
| <Skeleton width="16rem" height="12rem" /> | ||
| </div> | ||
| </div> | ||
| </template> | ||
| <template #content> | ||
| <div class="flex items-center px-4 py-3"> | ||
| <div class="flex-1 flex flex-col"> | ||
| <Skeleton width="80%" height="1.25rem" class="mb-2" /> | ||
| <Skeleton width="100%" height="0.875rem" class="mb-1" /> | ||
| <Skeleton width="90%" height="0.875rem" /> | ||
| </div> | ||
| </div> | ||
| </template> | ||
| </Card> | ||
| </template> | ||
|
|
||
| <script setup lang="ts"> | ||
| import Card from 'primevue/card' | ||
| import Skeleton from 'primevue/skeleton' | ||
| </script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.