This commit is contained in:
Daniel Arantes Loverde
2026-02-15 08:18:06 -03:00
parent d7b8b1864a
commit 0f5ad4c268
35 changed files with 4090 additions and 309 deletions

View File

@@ -0,0 +1,162 @@
import Foundation
import SwiftUI
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
struct CachedRemoteImage<Placeholder: View>: View {
let imageURL: String?
let ttl: TimeInterval
let placeholder: Placeholder
@StateObject var loader = CachedRemoteImageLoader()
init(
imageURL: String?,
ttl: TimeInterval = 6 * 60 * 60,
@ViewBuilder placeholder: () -> Placeholder
) {
self.imageURL = imageURL
self.ttl = ttl
self.placeholder = placeholder()
}
var body: some View {
Group {
#if canImport(UIKit)
if let uiImage = loader.uiImage {
Image(uiImage: uiImage)
.resizable()
.scaledToFill()
} else {
placeholder
}
#elseif canImport(AppKit)
if let nsImage = loader.nsImage {
Image(nsImage: nsImage)
.resizable()
.scaledToFill()
} else {
placeholder
}
#else
if let imageURL,
let url = URL(string: imageURL) {
AsyncImage(url: url) { phase in
switch phase {
case .success(let image):
image
.resizable()
.scaledToFill()
default:
placeholder
}
}
} else {
placeholder
}
#endif
}
.onAppear {
loader.load(imageURL: imageURL, ttl: ttl)
}
.onChange(of: imageURL) { _, newValue in
loader.load(imageURL: newValue, ttl: ttl)
}
}
}
@MainActor
final class CachedRemoteImageLoader: ObservableObject {
#if canImport(UIKit)
@Published var uiImage: UIImage?
#elseif canImport(AppKit)
@Published var nsImage: NSImage?
#endif
private var currentKey: String?
private var task: Task<Void, Never>?
deinit {
task?.cancel()
}
func load(imageURL: String?, ttl: TimeInterval) {
let normalized = Self.normalizeImageSource(imageURL)
let key = normalized ?? ""
guard currentKey != key else { return }
currentKey = key
task?.cancel()
#if canImport(UIKit)
uiImage = nil
#elseif canImport(AppKit)
nsImage = nil
#endif
guard let normalized, normalized.isEmpty == false else { return }
#if canImport(UIKit) || canImport(AppKit)
if let image = Self.imageFromDataURL(normalized) {
#if canImport(UIKit)
uiImage = image
#elseif canImport(AppKit)
nsImage = image
#endif
return
}
#endif
guard let url = URL(string: normalized) else { return }
task = Task { [weak self] in
#if canImport(UIKit) || canImport(AppKit)
let image = await AppImageCache.shared.image(for: url, ttl: ttl)
guard Task.isCancelled == false else { return }
await MainActor.run {
#if canImport(UIKit)
self?.uiImage = image
#elseif canImport(AppKit)
self?.nsImage = image
#endif
}
#endif
}
}
private static func normalizeImageSource(_ value: String?) -> String? {
guard var normalized = value?.trimmingCharacters(in: .whitespacesAndNewlines),
normalized.isEmpty == false else { return nil }
normalized = normalized.replacingOccurrences(of: "\\/", with: "/")
return normalized
}
#if canImport(UIKit) || canImport(AppKit)
private static func imageFromDataURL(_ source: String) -> PlatformImage? {
let lower = source.lowercased()
guard lower.hasPrefix("data:image"), let commaIndex = source.firstIndex(of: ",") else { return nil }
let header = String(source[..<commaIndex]).lowercased()
guard header.contains(";base64") else { return nil }
let payloadStart = source.index(after: commaIndex)
let payload = String(source[payloadStart...])
.replacingOccurrences(of: "\\/", with: "/")
.replacingOccurrences(of: "\n", with: "")
.replacingOccurrences(of: "\r", with: "")
.replacingOccurrences(of: " ", with: "")
guard let data = Data(base64Encoded: payload, options: [.ignoreUnknownCharacters]) else { return nil }
#if canImport(UIKit)
return UIImage(data: data)
#elseif canImport(AppKit)
return NSImage(data: data)
#else
return nil
#endif
}
#endif
}

View File

@@ -37,22 +37,28 @@ struct FeaturedStoreCard: View {
.font(AppTypography.heading3)
.foregroundStyle(AppColors.textPrimary)
HStack(spacing: 6) {
Image(systemName: "star.fill")
.font(.caption)
.foregroundStyle(Color(hex: "#F5B335"))
Text(String(format: "%.1f", store.rating))
.font(.caption)
if store.isOpen {
HStack(spacing: 6) {
Image(systemName: "star.fill")
.font(.caption)
.foregroundStyle(Color(hex: "#F5B335"))
Text(String(format: "%.1f", store.rating))
.font(.caption)
.foregroundStyle(AppColors.textPrimary)
Text("(\(store.reviews))")
.font(.caption)
.foregroundStyle(AppColors.textMuted)
Text("·")
.font(.caption)
.foregroundStyle(AppColors.textMuted)
Text(store.distance)
.font(.caption)
.foregroundStyle(AppColors.textMuted)
}
} else {
Text(store.statusLabel?.isEmpty == false ? (store.statusLabel ?? "Fechado") : "Fechado")
.font(AppTypography.heading3)
.foregroundStyle(AppColors.textPrimary)
Text("(\(store.reviews))")
.font(.caption)
.foregroundStyle(AppColors.textMuted)
Text("·")
.font(.caption)
.foregroundStyle(AppColors.textMuted)
Text(store.distance)
.font(.caption)
.foregroundStyle(AppColors.textMuted)
}
Text(store.category)
@@ -62,6 +68,8 @@ struct FeaturedStoreCard: View {
.padding(14)
.background(AppColors.surface)
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusXL, style: .continuous))
.saturation(store.isOpen ? 1 : 0)
.opacity(store.isOpen ? 1 : 0.9)
}
private var mediaBlock: some View {
@@ -77,23 +85,7 @@ struct FeaturedStoreCard: View {
@ViewBuilder
private var mediaImage: some View {
if let imageURL = store.imageURL,
let url = URL(string: imageURL) {
AsyncImage(url: url) { phase in
switch phase {
case .success(let image):
image
.resizable()
.scaledToFill()
default:
storeIconPlaceholder
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.clipped()
} else {
storeIconPlaceholder
}
AsyncStoreImage(imageURL: store.imageURL)
}
private var storeIconPlaceholder: some View {
@@ -130,6 +122,8 @@ struct FeaturedStoreCardModel: Identifiable {
let imageURL: String?
let logoURL: String?
let coverURL: String?
let isOpen: Bool
let statusLabel: String?
}
struct SpecialOfferCard: View {