import SwiftUI struct WallpaperItem: View { let wallpaper: Wallpaper let action: () -> Void @ObservedObject var favoritesManager = FavoritesManager.shared @State private var retryID = UUID() @State private var attempt = 0 @State private var isHovering = false // Transient thumbnail failures (flaky network) are common; retry a few // times with growing backoff before giving up quietly. let maxAttempts: Int let retryDelayNanoseconds: UInt64 init( wallpaper: Wallpaper, action: @escaping () -> Void, attempt: Int = 0, maxAttempts: Int = 4, retryDelayNanoseconds: UInt64 = 800_000_000 ) { self.wallpaper = wallpaper self.action = action self.maxAttempts = maxAttempts self.retryDelayNanoseconds = retryDelayNanoseconds _attempt = State(initialValue: attempt) } var body: some View { Button(action: action) { AsyncImage(url: URL(string: wallpaper.thumbs.small)) { thumbnail(for: $0) } .id(retryID) .frame(height: 100) .clipped() .cornerRadius(8) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(isHovering ? Color.accentColor : Color.clear, lineWidth: 2) ) .shadow(radius: isHovering ? 4 : 0) .overlay( // Favorite Button Group { if isHovering || favoritesManager.isFavorite(wallpaper) { Button(action: toggleFavorite) { Image(systemName: favoritesManager.isFavorite(wallpaper) ? "heart.fill" : "heart") .foregroundColor(favoritesManager.isFavorite(wallpaper) ? .red : .white) .shadow(radius: 2) .font(.system(size: 16, weight: .bold)) .padding(6) .background(Color.black.opacity(0.3)) .clipShape(Circle()) } .buttonStyle(.plain) .padding(6) .transition(.opacity) } }, alignment: .topTrailing ) .scaleEffect(isHovering ? 1.02 : 1.0) .animation(.spring(response: 0.3), value: isHovering) } .buttonStyle(.plain) .onHover { isHovering = $0 } .help("\(wallpaper.resolution)\n\(wallpaper.purity)") } func thumbnail(for phase: AsyncImagePhase) -> AnyView { if case .success(let image) = phase { return AnyView(image.resizable().aspectRatio(contentMode: .fill)) } if case .failure = phase { return AnyView( Rectangle().fill(Color.gray.opacity(0.2)).overlay( attempt < maxAttempts ? AnyView(ProgressView()) : AnyView(Image(systemName: "photo").foregroundColor(.gray.opacity(0.6))) ).task { await retryAfterFailure() }) } return AnyView(Rectangle().fill(Color.gray.opacity(0.2)).overlay(ProgressView())) } func retryAfterFailure() async { guard attempt < maxAttempts else { return } let delay = UInt64(attempt + 1) * retryDelayNanoseconds try? await Task.sleep(nanoseconds: delay) guard !Task.isCancelled else { return } attempt += 1 retryID = UUID() } func toggleFavorite() { favoritesManager.toggleFavorite(wallpaper) } }