1import SwiftUI23struct WallpaperItem: View {4 let wallpaper: Wallpaper5 let action: () -> Void6 @ObservedObject var favoritesManager = FavoritesManager.shared7 @State private var retryID = UUID()8 @State private var attempt = 09 @State private var isHovering = false1011 12 13 let maxAttempts: Int14 let retryDelayNanoseconds: UInt641516 init(17 wallpaper: Wallpaper, action: @escaping () -> Void, attempt: Int = 0,18 maxAttempts: Int = 4, retryDelayNanoseconds: UInt64 = 800_000_00019 ) {20 self.wallpaper = wallpaper21 self.action = action22 self.maxAttempts = maxAttempts23 self.retryDelayNanoseconds = retryDelayNanoseconds24 _attempt = State(initialValue: attempt)25 }2627 var body: some View {28 Button(action: action) {29 AsyncImage(url: URL(string: wallpaper.thumbs.small)) { thumbnail(for: $0) }30 .id(retryID)31 .frame(height: 100)32 .clipped()33 .cornerRadius(8)34 .overlay(35 RoundedRectangle(cornerRadius: 8)36 .stroke(isHovering ? Color.accentColor : Color.clear, lineWidth: 2)37 )38 .shadow(radius: isHovering ? 4 : 0)39 .overlay(40 41 Group {42 if isHovering || favoritesManager.isFavorite(wallpaper) {43 Button(action: toggleFavorite) {44 Image(systemName: favoritesManager.isFavorite(wallpaper) ? "heart.fill" : "heart")45 .foregroundColor(favoritesManager.isFavorite(wallpaper) ? .red : .white)46 .shadow(radius: 2)47 .font(.system(size: 16, weight: .bold))48 .padding(6)49 .background(Color.black.opacity(0.3))50 .clipShape(Circle())51 }52 .buttonStyle(.plain)53 .padding(6)54 .transition(.opacity)55 }56 },57 alignment: .topTrailing58 )59 .scaleEffect(isHovering ? 1.02 : 1.0)60 .animation(.spring(response: 0.3), value: isHovering)61 }62 .buttonStyle(.plain)63 .onHover { isHovering = $0 }64 .help("\(wallpaper.resolution)\n\(wallpaper.purity)")65 }6667 func thumbnail(for phase: AsyncImagePhase) -> AnyView {68 if case .success(let image) = phase {69 return AnyView(image.resizable().aspectRatio(contentMode: .fill))70 }71 if case .failure = phase {72 return AnyView(73 Rectangle().fill(Color.gray.opacity(0.2)).overlay(74 attempt < maxAttempts75 ? AnyView(ProgressView())76 : AnyView(Image(systemName: "photo").foregroundColor(.gray.opacity(0.6)))77 ).task { await retryAfterFailure() })78 }79 return AnyView(Rectangle().fill(Color.gray.opacity(0.2)).overlay(ProgressView()))80 }8182 83 84 85 86 87 88 89 90 91 92 93 func backoffDelay(forAttempt attempt: Int) -> UInt64 {94 let safe = max(0, attempt)95 let multiplier = UInt64(1) << safe96 return multiplier &* retryDelayNanoseconds97 }9899 func retryAfterFailure() async {100 guard attempt < maxAttempts else { return }101 if Task.isCancelled { return }102103 104 105 let multiplier = UInt64(1) << attempt106 let delay = multiplier &* retryDelayNanoseconds107 do {108 try await Task.sleep(nanoseconds: delay)109 } catch {110 return111 }112 guard !Task.isCancelled else { return }113 attempt += 1114 retryID = UUID()115 }116117 118 var exposedAttempt: Int { attempt }119120 func toggleFavorite() { favoritesManager.toggleFavorite(wallpaper) }121}