WallpaperItem.swift

3.1 KB · raw

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  // Transient thumbnail failures (flaky network) are common; retry a few12  // times with growing backoff before giving up quietly.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          // Favorite Button41          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  func retryAfterFailure() async {83    guard attempt < maxAttempts else { return }84    let delay = UInt64(attempt + 1) * retryDelayNanoseconds85    try? await Task.sleep(nanoseconds: delay)86    guard !Task.isCancelled else { return }87    attempt += 188    retryID = UUID()89  }9091  func toggleFavorite() { favoritesManager.toggleFavorite(wallpaper) }92}