-
Notifications
You must be signed in to change notification settings - Fork 528
Feat/lazy image loading #998
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
Open
Ayyub2006
wants to merge
6
commits into
AOSSIE-Org:main
Choose a base branch
from
Ayyub2006:feat/lazy-image-loading
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
ce552d7
eat: add intersection observer hook for lazy loading
Ayyub2006 a1aeee5
feat: use LazyImage in ImageCard for performance
Ayyub2006 ac1342b
fix: allow nullable element refs in intersection observer hook
Ayyub2006 4d92e30
feat: add LazyImage component with skeleton and error handling
Ayyub2006 a992596
feat: lazy load thumbnails using LazyImage
Ayyub2006 9300088
fix: address review comments for accessibility and cleanup
Ayyub2006 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
Empty file.
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,60 @@ | ||
| import { useRef, useState } from "react"; | ||
| import { useIntersectionObserver } from "../../hooks/useIntersectionObserver"; | ||
|
|
||
| interface LazyImageProps { | ||
| src: string; | ||
| alt: string; | ||
| className?: string; | ||
| rootMargin?: string; | ||
| threshold?: number; | ||
| } | ||
|
|
||
| export function LazyImage({ | ||
| src, | ||
| alt, | ||
| className = "", | ||
| rootMargin = "200px", | ||
| threshold = 0.1, | ||
| }: LazyImageProps) { | ||
|
|
||
| const containerRef = useRef<HTMLDivElement>(null); | ||
| const isVisible = useIntersectionObserver(containerRef, { | ||
| rootMargin, | ||
| threshold, | ||
| }); | ||
|
|
||
| const [isLoaded, setIsLoaded] = useState(false); | ||
| const [hasError, setHasError] = useState(false); | ||
|
|
||
| return ( | ||
| <div | ||
| ref={containerRef} | ||
| className={`relative overflow-hidden ${className}`} | ||
| > | ||
| {/* Skeleton loader */} | ||
| {!isLoaded && !hasError && ( | ||
| <div className="absolute inset-0 animate-pulse bg-muted" /> | ||
| )} | ||
|
|
||
| {/* Image */} | ||
| {isVisible && !hasError && ( | ||
| <img | ||
| src={src} | ||
| alt={alt} | ||
| className={`h-full w-full object-cover transition-opacity duration-300 ${ | ||
| isLoaded ? "opacity-100" : "opacity-0" | ||
| }`} | ||
| onLoad={() => setIsLoaded(true)} | ||
| onError={() => setHasError(true)} | ||
| /> | ||
| )} | ||
|
|
||
| {/* Error state */} | ||
| {hasError && ( | ||
| <div className="flex h-full w-full items-center justify-center bg-muted text-xs text-muted-foreground"> | ||
| Failed to load image | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| } |
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
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
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,42 @@ | ||
| import { useEffect, useState, RefObject } from "react"; | ||
|
|
||
| interface IntersectionOptions { | ||
| root?: Element | null; | ||
| rootMargin?: string; | ||
| threshold?: number; | ||
| } | ||
|
|
||
| /** | ||
| * Hook to detect when an element enters the viewport | ||
| */ | ||
| export function useIntersectionObserver( | ||
| ref: RefObject<Element | null>, | ||
| options: IntersectionOptions = {} | ||
| ) { | ||
| const [isVisible, setIsVisible] = useState(false); | ||
|
|
||
| useEffect(() => { | ||
| const element = ref.current; | ||
| if (!element || isVisible) return; | ||
|
|
||
| const observer = new IntersectionObserver( | ||
| ([entry]) => { | ||
| if (entry.isIntersecting) { | ||
| setIsVisible(true); | ||
| observer.unobserve(element); | ||
| } | ||
| }, | ||
| { | ||
| root: options.root ?? null, | ||
| rootMargin: options.rootMargin ?? "200px", | ||
| threshold: options.threshold ?? 0.1, | ||
| } | ||
| ); | ||
|
|
||
| observer.observe(element); | ||
|
|
||
| return () => observer.disconnect(); | ||
| }, [ref, isVisible, options.root, options.rootMargin, options.threshold]); | ||
|
|
||
| return isVisible; | ||
| } |
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: AOSSIE-Org/PictoPy
Length of output: 267
🏁 Script executed:
Repository: AOSSIE-Org/PictoPy
Length of output: 4836
🏁 Script executed:
Repository: AOSSIE-Org/PictoPy
Length of output: 5878
🏁 Script executed:
Repository: AOSSIE-Org/PictoPy
Length of output: 1636
🌐 Web query:
Tauri convertFileSrc behavior empty string null invalid path return value💡 Result:
Short answer
Sources
References
[1] Tauri convertFileSrc (API docs).
[2] tauri convert_file_src (tauri_sys / tauri-wasm docs).
[3] tauri-apps/tauri issue: "Avoid calling convertFileSrc twice" (discussing double‑encoding).
Simplify the fallback logic for consistency and clarity.
The fallback pattern
convertFileSrc(image.thumbnailPath) || '/placeholder.svg'is ineffective becauseconvertFileSrcalways returns a truthy string value (a protocol URL likeasset://), even for invalid or empty paths. The|| '/placeholder.svg'branch will never execute.For consistency with the pattern used elsewhere in the codebase (e.g., ImageCard), place the fallback before converting:
This ensures that if
thumbnailPathis empty or invalid, the placeholder is used before conversion.Note: The error handling UX has changed—LazyImage now displays "Failed to load image" text on load errors rather than swapping to the placeholder image. Ensure this aligns with your design requirements.
🤖 Prompt for AI Agents