summaryrefslogtreecommitdiff
path: root/Sora/Other/AsyncImageWithPreview.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Sora/Other/AsyncImageWithPreview.swift')
-rw-r--r--Sora/Other/AsyncImageWithPreview.swift163
1 files changed, 163 insertions, 0 deletions
diff --git a/Sora/Other/AsyncImageWithPreview.swift b/Sora/Other/AsyncImageWithPreview.swift
new file mode 100644
index 0000000..8d93ee5
--- /dev/null
+++ b/Sora/Other/AsyncImageWithPreview.swift
@@ -0,0 +1,163 @@
+import SwiftUI
+
+struct AsyncImageWithPreview<Placeholder: View>: View {
+ var url: URL?
+ @Binding var loadingState: PostLoadingState
+ var finalLoadingState: PostLoadingState
+ var postURL: URL?
+ let placeholder: () -> Placeholder
+ @State private var currentScale: CGFloat = 1.0
+ @State private var finalScale: CGFloat = 1.0
+ @State private var currentOffset: CGSize = .zero
+ @State private var finalOffset: CGSize = .zero
+
+ init(
+ url: URL?,
+ loadingStage: Binding<PostLoadingState>,
+ finalLoadingState: PostLoadingState = .loadingFile,
+ postURL: URL? = nil,
+ @ViewBuilder placeholder: @escaping () -> Placeholder = {
+ GeometryReader { geometry in
+ ProgressView()
+ .frame(width: geometry.size.width, height: geometry.size.height)
+ .position(x: geometry.size.width / 2, y: geometry.size.height / 2)
+ .padding()
+ }
+ }
+ ) {
+ self.url = url
+ _loadingState = loadingStage
+ self.finalLoadingState = finalLoadingState
+ self.postURL = postURL
+ self.placeholder = placeholder
+ }
+
+ var body: some View {
+ GeometryReader { geometry in
+ AsyncImage(url: url) { image in
+ image
+ .resizable()
+ .scaledToFit()
+ .onAppear {
+ self.loadingState = finalLoadingState
+ }
+ .scaleEffect(finalScale * currentScale)
+ .offset(x: finalOffset.width + currentOffset.width,
+ y: finalOffset.height + currentOffset.height)
+ .frame(width: geometry.size.width, height: geometry.size.height)
+ .position(x: geometry.size.width / 2, y: geometry.size.height / 2)
+ .gesture(
+ DragGesture()
+ .onChanged { value in
+ let translation = value.translation
+ let newOffset = CGSize(
+ width: finalOffset.width + translation.width,
+ height: finalOffset.height + translation.height
+ )
+ let scale = finalScale * currentScale
+ let imageWidth = geometry.size.width * scale
+ let imageHeight = geometry.size.height * scale
+ let maxX = max((imageWidth - geometry.size.width) / 2, 0)
+ let maxY = max((imageHeight - geometry.size.height) / 2, 0)
+ let clampedX = min(max(newOffset.width, -maxX), maxX)
+ let clampedY = min(max(newOffset.height, -maxY), maxY)
+
+ currentOffset = CGSize(
+ width: clampedX - finalOffset.width,
+ height: clampedY - finalOffset.height
+ )
+ }
+ .onEnded { value in
+ let translation = value.translation
+ var newOffset = CGSize(
+ width: finalOffset.width + translation.width,
+ height: finalOffset.height + translation.height
+ )
+ let scale = finalScale * currentScale
+ let imageWidth = geometry.size.width * scale
+ let imageHeight = geometry.size.height * scale
+ let maxX = max((imageWidth - geometry.size.width) / 2, 0)
+ let maxY = max((imageHeight - geometry.size.height) / 2, 0)
+
+ newOffset.width = min(max(newOffset.width, -maxX), maxX)
+ newOffset.height = min(max(newOffset.height, -maxY), maxY)
+ finalOffset = newOffset
+ currentOffset = .zero
+ }
+ )
+ .simultaneousGesture(
+ MagnificationGesture()
+ .onChanged { value in
+ currentScale = value
+ }
+ .onEnded { _ in
+ finalScale *= currentScale
+ currentScale = 1.0
+
+ let scale = finalScale
+ let imageWidth = geometry.size.width * scale
+ let imageHeight = geometry.size.height * scale
+ let maxX = max((imageWidth - geometry.size.width) / 2, 0)
+ let maxY = max((imageHeight - geometry.size.height) / 2, 0)
+
+ finalOffset.width = min(max(finalOffset.width, -maxX), maxX)
+ finalOffset.height = min(max(finalOffset.height, -maxY), maxY)
+ }
+ )
+ .highPriorityGesture(
+ TapGesture(count: 2)
+ .onEnded {
+ withAnimation {
+ finalScale = 1.0
+ currentScale = 1.0
+ finalOffset = .zero
+ currentOffset = .zero
+ }
+ }
+ )
+ .contextMenu {
+ #if os(iOS)
+ Button {
+ guard let url = url else { return }
+
+ URLSession.shared.dataTask(with: url) { data, _, _ in
+ guard let data = data, let uiImage = UIImage(data: data) else { return }
+
+ UIImageWriteToSavedPhotosAlbum(uiImage, nil, nil, nil)
+ }.resume()
+ } label: {
+ Label("Save Image", systemImage: "square.and.arrow.down")
+ }
+ #endif
+
+ #if os(iOS)
+ Button {
+ let activityViewController = UIActivityViewController(activityItems: [url ?? URL(string: "")!], applicationActivities: nil)
+
+ UIApplication.shared.windows.first?.rootViewController?.present(activityViewController, animated: true)
+ } label: {
+ Label("Share Image", systemImage: "square.and.arrow.up")
+ }
+ #endif
+
+ if let url = postURL {
+ Button {
+ #if os(iOS)
+ UIApplication.shared.open(url)
+ #else
+ NSWorkspace.shared.open(url)
+ #endif
+ } label: {
+ Label("Open in Safari", systemImage: "safari")
+ }
+ }
+ }
+ } placeholder: {
+ placeholder()
+ .onAppear {
+ self.loadingState = .loadingPreview
+ }
+ }
+ }
+ }
+}