Add grid, camera focus
This commit is contained in:
79
Caps/Views/CachedCapImage.swift
Normal file
79
Caps/Views/CachedCapImage.swift
Normal file
@@ -0,0 +1,79 @@
|
||||
import SwiftUI
|
||||
|
||||
struct CachedCapImage<Content, T>: View where Content: View, T: Equatable {
|
||||
|
||||
@State private var phase: AsyncImagePhase
|
||||
|
||||
let id: T
|
||||
|
||||
let check: () -> UIImage?
|
||||
|
||||
let fetch: () async -> UIImage?
|
||||
|
||||
private let transaction: Transaction
|
||||
|
||||
private let content: (AsyncImagePhase) -> Content
|
||||
|
||||
var body: some View {
|
||||
content(phase)
|
||||
.task(id: id, load)
|
||||
}
|
||||
|
||||
init<I, P>(_ id: T, _ image: CapImage, cache: ImageCache, @ViewBuilder content: @escaping (Image) -> I, @ViewBuilder placeholder: @escaping () -> P) where Content == _ConditionalContent<I, P>, I : View, P : View {
|
||||
self.init(id, image: image, cache: cache) { phase in
|
||||
if let image = phase.image {
|
||||
content(image)
|
||||
} else {
|
||||
placeholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(_ id: T, image: CapImage, cache: ImageCache, transaction: Transaction = Transaction(), @ViewBuilder content: @escaping (AsyncImagePhase) -> Content) {
|
||||
self.init(id,
|
||||
check: { cache.cachedImage(image) },
|
||||
fetch: { await cache.image(image) },
|
||||
transaction: transaction,
|
||||
content: content)
|
||||
}
|
||||
|
||||
init<I, P>(_ id: T, check: @escaping () -> UIImage?, fetch: @escaping () async -> UIImage?, @ViewBuilder content: @escaping (Image) -> I, @ViewBuilder placeholder: @escaping () -> P) where Content == _ConditionalContent<I, P>, I : View, P : View {
|
||||
self.init(id, check: check, fetch: fetch) { phase in
|
||||
if let image = phase.image {
|
||||
content(image)
|
||||
} else {
|
||||
placeholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(_ id: T, check: @escaping () -> UIImage?, fetch: @escaping () async -> UIImage?, transaction: Transaction = Transaction(), @ViewBuilder content: @escaping (AsyncImagePhase) -> Content) {
|
||||
self.id = id
|
||||
self.check = check
|
||||
self.fetch = fetch
|
||||
self.transaction = transaction
|
||||
self.content = content
|
||||
|
||||
self._phase = State(wrappedValue: .empty)
|
||||
|
||||
guard let image = check() else {
|
||||
return
|
||||
}
|
||||
let wrapped = Image(uiImage: image)
|
||||
self._phase = State(wrappedValue: .success(wrapped))
|
||||
}
|
||||
|
||||
@Sendable
|
||||
private func load() async {
|
||||
guard let image = await fetch() else {
|
||||
withAnimation(transaction.animation) {
|
||||
phase = .empty
|
||||
}
|
||||
return
|
||||
}
|
||||
let wrapped = Image(uiImage: image)
|
||||
withAnimation(transaction.animation) {
|
||||
phase = .success(wrapped)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,4 @@
|
||||
import SwiftUI
|
||||
import CachedAsyncImage
|
||||
|
||||
struct CapRowView: View {
|
||||
|
||||
@@ -56,7 +55,7 @@ struct CapRowView: View {
|
||||
}
|
||||
}//.padding(.vertical)
|
||||
Spacer()
|
||||
CachedAsyncImage(url: imageUrl, urlCache: database.imageCache) { image in
|
||||
CachedCapImage(cap, cap.image, cache: database.images) { image in
|
||||
image.resizable()
|
||||
} placeholder: {
|
||||
ProgressView()
|
||||
|
@@ -1,13 +1,171 @@
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
struct GridView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
var database: Database
|
||||
|
||||
|
||||
@AppStorage("currentGridName")
|
||||
private(set) var currentGridName = "default"
|
||||
|
||||
private var defaultImageGrid: ImageGrid {
|
||||
.init(columns: 40, capPlacements: database.caps.keys.sorted())
|
||||
}
|
||||
|
||||
var imageSize: CGFloat {
|
||||
CapsApp.thumbnailImageSize
|
||||
}
|
||||
|
||||
private let verticalInsetFactor: CGFloat = cos(.pi / 6)
|
||||
|
||||
private let minScale: CGFloat = 1.0
|
||||
|
||||
private let maxScale: CGFloat = 0.5
|
||||
|
||||
private let cancelButtonSize: CGFloat = 75
|
||||
private let cancelIconSize: CGFloat = 25
|
||||
|
||||
@Binding
|
||||
var isPresented: Bool
|
||||
|
||||
var image: ImageGrid
|
||||
|
||||
@State var scale: CGFloat = 1.0
|
||||
|
||||
@State var lastScaleValue: CGFloat = 1.0
|
||||
|
||||
init(isPresented: Binding<Bool>) {
|
||||
self._isPresented = isPresented
|
||||
self.image = .init(columns: 1, capPlacements: [])
|
||||
|
||||
if let image = database.load(grid: currentGridName) {
|
||||
self.image = image
|
||||
} else {
|
||||
self.image = defaultImageGrid
|
||||
currentGridName = "default"
|
||||
}
|
||||
}
|
||||
|
||||
var columnCount: Int {
|
||||
image.columns
|
||||
}
|
||||
|
||||
var capCount: Int {
|
||||
image.capCount
|
||||
}
|
||||
|
||||
var imageHeight: CGFloat {
|
||||
(CGFloat(capCount) / CGFloat(columnCount)).rounded(.up) * verticalInsetFactor * imageSize + (1-verticalInsetFactor) * imageSize
|
||||
}
|
||||
|
||||
var imageWidth: CGFloat {
|
||||
imageSize * (CGFloat(columnCount) + 0.5)
|
||||
}
|
||||
|
||||
var magnificationGesture: some Gesture {
|
||||
MagnificationGesture()
|
||||
.onChanged { val in
|
||||
let delta = val / self.lastScaleValue
|
||||
self.lastScaleValue = val
|
||||
self.scale = max(min(self.scale * delta, minScale), maxScale)
|
||||
}
|
||||
.onEnded { val in
|
||||
// without this the next gesture will be broken
|
||||
self.lastScaleValue = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
var gridView: some View {
|
||||
let gridItems = Array(repeating: GridItem(.fixed(imageSize), spacing: 0), count: columnCount)
|
||||
|
||||
return LazyVGrid(columns: gridItems, alignment: .leading, spacing: 0) {
|
||||
ForEach(image.items) { item in
|
||||
CachedCapImage(
|
||||
item.id,
|
||||
check: { cachedImage(item.cap) },
|
||||
fetch: { await fetchImage(item.cap) },
|
||||
content: { $0.resizable() },
|
||||
placeholder: { ProgressView() })
|
||||
.frame(width: imageSize,
|
||||
height: imageSize)
|
||||
.clipShape(Circle())
|
||||
.offset(x: isEvenRow(item.id) ? 0 : imageSize / 2)
|
||||
.frame(width: imageSize,
|
||||
height: imageSize * verticalInsetFactor)
|
||||
}
|
||||
}
|
||||
.frame(width: imageWidth)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Text("Grid view")
|
||||
ZStack {
|
||||
ScrollView([.vertical, .horizontal]) {
|
||||
gridView
|
||||
.scaleEffect(scale)
|
||||
.frame(
|
||||
width: imageWidth * scale,
|
||||
height: imageHeight * scale
|
||||
)
|
||||
}
|
||||
.gesture(magnificationGesture)
|
||||
.onDisappear {
|
||||
database.save(image, named: currentGridName)
|
||||
}
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
Spacer()
|
||||
IconButton(action: saveScreenshot,
|
||||
icon: .squareAndArrowDown,
|
||||
iconSize: cancelIconSize,
|
||||
buttonSize: cancelButtonSize)
|
||||
.padding()
|
||||
IconButton(action: dismiss,
|
||||
icon: .xmark,
|
||||
iconSize: cancelIconSize,
|
||||
buttonSize: cancelButtonSize)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isEvenRow(_ idx: Int) -> Bool {
|
||||
(idx / columnCount) % 2 == 0
|
||||
}
|
||||
|
||||
private func cachedImage(_ cap: Int) -> UIImage? {
|
||||
let image = CapImage(
|
||||
cap: cap,
|
||||
version: database
|
||||
.mainImage(for: cap))
|
||||
return database.images.cachedImage(image)
|
||||
}
|
||||
|
||||
private func fetchImage(_ cap: Int) async -> UIImage? {
|
||||
let image = CapImage(
|
||||
cap: cap,
|
||||
version: database
|
||||
.mainImage(for: cap))
|
||||
return await database.images.thumbnail(for: image)
|
||||
}
|
||||
|
||||
private func dismiss() {
|
||||
isPresented = false
|
||||
}
|
||||
|
||||
private func saveScreenshot() {
|
||||
let image = gridView.snapshot()
|
||||
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
|
||||
}
|
||||
}
|
||||
|
||||
struct GridView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
GridView()
|
||||
GridView(isPresented: .constant(true))
|
||||
.environmentObject(Database.largeMock)
|
||||
}
|
||||
}
|
||||
|
41
Caps/Views/IconButton.swift
Normal file
41
Caps/Views/IconButton.swift
Normal file
@@ -0,0 +1,41 @@
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
struct IconButton: View {
|
||||
|
||||
let action: () -> Void
|
||||
|
||||
let icon: SFSymbol
|
||||
|
||||
let iconSize: CGFloat
|
||||
|
||||
let buttonSize: CGFloat
|
||||
|
||||
private var padding: CGFloat {
|
||||
(buttonSize - iconSize) / 2
|
||||
}
|
||||
|
||||
private var cornerRadius: CGFloat {
|
||||
buttonSize / 2
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
Image(systemSymbol: icon)
|
||||
.resizable()
|
||||
.frame(width: iconSize, height: iconSize)
|
||||
.padding(padding)
|
||||
.background(.thinMaterial)
|
||||
.cornerRadius(cornerRadius)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct IconButton_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
IconButton(action: { },
|
||||
icon: .xmark,
|
||||
iconSize: 20,
|
||||
buttonSize: 25)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user