diff options
Diffstat (limited to 'Sora/Other/AsyncImageWithPreview.swift')
| -rw-r--r-- | Sora/Other/AsyncImageWithPreview.swift | 163 |
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 + } + } + } + } +} |