migration

This commit is contained in:
Daniel Arantes Loverde
2026-08-11 13:22:02 -03:00
parent c4d6998595
commit 7702836fe7
231 changed files with 4329 additions and 1958 deletions

View File

@@ -0,0 +1,146 @@
import Foundation
import SwiftUI
import UIKit
enum ImageFitMode {
/// Scale to cover the whole box, cropping whichever axis overflows.
/// Default matches historical behavior for every image in the app.
case fill
/// Scale so height always matches the box exactly; width follows the
/// source's aspect ratio and gets cropped/gapped on the sides. Opt-in
/// only used by the Store Detail cover header.
case heightFit
}
struct CachedRemoteImage<Placeholder: View>: View {
let imageURL: String?
let ttl: TimeInterval
let fitMode: ImageFitMode
let placeholder: Placeholder
@StateObject var loader = CachedRemoteImageLoader()
init(
imageURL: String?,
ttl: TimeInterval = AppCacheTTL.twoHours,
fitMode: ImageFitMode = .fill,
@ViewBuilder placeholder: () -> Placeholder
) {
self.imageURL = imageURL
self.ttl = ttl
self.fitMode = fitMode
self.placeholder = placeholder()
}
var body: some View {
GeometryReader { geometry in
Group {
if let uiImage = loader.uiImage {
rendered(image: Image(uiImage: uiImage), pixelSize: uiImage.size, in: geometry.size)
} else {
placeholder
}
}
.frame(width: geometry.size.width, height: geometry.size.height)
.clipped()
}
.onAppear {
loader.load(imageURL: imageURL, ttl: ttl)
}
.onChange(of: imageURL) { _, newValue in
loader.load(imageURL: newValue, ttl: ttl)
}
}
@ViewBuilder
private func rendered(image: Image, pixelSize: CGSize, in containerSize: CGSize) -> some View {
switch fitMode {
case .fill:
image
.resizable()
.scaledToFill()
case .heightFit:
let aspect = pixelSize.height > 0 ? pixelSize.width / pixelSize.height : 1
let renderWidth = containerSize.height * aspect
image
.resizable()
.frame(width: renderWidth, height: containerSize.height)
.frame(width: containerSize.width, height: containerSize.height)
}
}
}
@MainActor
final class CachedRemoteImageLoader: ObservableObject {
@Published var uiImage: UIImage?
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()
uiImage = nil
guard let normalized, normalized.isEmpty == false else { return }
let dataCacheKey = Self.dataURLCacheKey(normalized)
if let cachedDataImage: PlatformImage = AppContentCache.shared.value(for: dataCacheKey, as: PlatformImage.self) {
uiImage = cachedDataImage
return
}
if let image = Self.imageFromDataURL(normalized) {
AppContentCache.shared.set(image, for: dataCacheKey, ttl: ttl)
uiImage = image
return
}
guard let url = URL(string: normalized) else { return }
task = Task { [weak self] in
let image = await AppImageCache.shared.image(for: url, ttl: ttl)
guard Task.isCancelled == false else { return }
await MainActor.run {
self?.uiImage = image
}
}
}
private static func normalizeImageSource(_ value: String?) -> String? {
ImageSourceResolver.resolve(value)
}
private static func dataURLCacheKey(_ source: String) -> String {
let head = String(source.prefix(48))
let tail = String(source.suffix(48))
return "data-image:\(source.count):\(head):\(tail)"
}
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 }
return UIImage(data: data)
}
}