summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2025-02-22 06:35:52 -0800
committerFuwn <[email protected]>2025-02-22 06:35:52 -0800
commit570a49d5c8ab326f3207e95538559b2a6f03fe59 (patch)
treec668329a79337352dd1a730a9e055af83e7b0a0a
parentfeat: Development commit (diff)
downloadsora-testing-570a49d5c8ab326f3207e95538559b2a6f03fe59.tar.xz
sora-testing-570a49d5c8ab326f3207e95538559b2a6f03fe59.zip
feat: Development commit
-rw-r--r--Sora/Other/AsyncImageWithPreview.swift168
-rw-r--r--Sora/Views/ZoomableImageView.swift89
2 files changed, 133 insertions, 124 deletions
diff --git a/Sora/Other/AsyncImageWithPreview.swift b/Sora/Other/AsyncImageWithPreview.swift
index 2e29052..9cae976 100644
--- a/Sora/Other/AsyncImageWithPreview.swift
+++ b/Sora/Other/AsyncImageWithPreview.swift
@@ -12,138 +12,58 @@ struct AsyncImageWithPreview<Placeholder: View>: View {
@State private var finalOffset: CGSize = .zero
var body: some View {
- GeometryReader { geometry in
- AsyncImage(url: url) { image in
- image
- .resizable()
- .scaledToFit()
- .onAppear {
- 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)
+ AsyncImage(url: url) { image in
+ ZoomableImageView(image: image)
+ .onAppear {
+ loadingState = finalLoadingState
+ }
+ .contextMenu {
+ #if os(iOS)
+ Button {
+ guard let url else { return }
- 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 else { return }
-
- URLSession.shared.dataTask(with: url) { data, _, _ in
- guard let data, let uiImage = UIImage(data: data) else { return }
+ URLSession.shared.dataTask(with: url) { data, _, _ in
+ guard let data, let uiImage = UIImage(data: data) else { return }
- UIImageWriteToSavedPhotosAlbum(uiImage, nil, nil, nil)
- }
- .resume()
- } label: {
- Label("Save Image", systemImage: "square.and.arrow.down")
+ UIImageWriteToSavedPhotosAlbum(uiImage, nil, nil, nil)
}
- #endif
+ .resume()
+ } label: {
+ Label("Save Image", systemImage: "square.and.arrow.down")
+ }
+ #endif
- #if os(iOS)
- Button {
- let activityViewController = UIActivityViewController(
- activityItems: [url ?? URL(string: "")!], applicationActivities: nil
- )
+ #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
+ 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")
- }
+ 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 {
- loadingState = .loadingPreview
- }
- }
+ }
+ } placeholder: {
+ placeholder()
+ .onAppear {
+ loadingState = .loadingPreview
+ }
}
}
diff --git a/Sora/Views/ZoomableImageView.swift b/Sora/Views/ZoomableImageView.swift
new file mode 100644
index 0000000..8304c6e
--- /dev/null
+++ b/Sora/Views/ZoomableImageView.swift
@@ -0,0 +1,89 @@
+import SwiftUI
+
+struct ZoomableImageView: View {
+ let image: Image
+ @State private var screenWidth = 0.0
+ @State private var screenHeight = 0.0
+ @State private var currentScale = 1.0
+ @State private var previousScale = 0.0
+ @State private var currentOffset: CGSize = .zero
+ @State private var previousOffset: CGSize = .zero
+
+ var body: some View {
+ GeometryReader { geometry in
+ VStack {
+ image
+ .resizable()
+ .scaledToFit()
+ .scaleEffect(currentScale)
+ .offset(currentOffset)
+ .frame(width: screenWidth, height: screenHeight)
+ .clipped()
+ .gesture(
+ MagnifyGesture()
+ .onChanged { gesture in
+ withAnimation(.interactiveSpring()) {
+ currentScale =
+ previousScale + gesture.magnification - (previousScale == 0 ? 0 : 1)
+ }
+ }
+ .onEnded { _ in
+ previousScale = currentScale
+ }
+ .simultaneously(
+ with: DragGesture(minimumDistance: 0)
+ .onChanged { gesture in
+ withAnimation(.interactiveSpring()) {
+ var newOffset: CGSize = .zero
+ let offset = gesture.translation
+
+ newOffset.width = offset.width + previousOffset.width
+ newOffset.height = offset.height + previousOffset.height
+
+ currentOffset = clampOffset(offset: newOffset)
+ }
+ }
+ .onEnded { _ in
+ previousOffset = currentOffset
+ }
+ )
+ )
+ .highPriorityGesture(
+ TapGesture(count: 2)
+ .onEnded {
+ withAnimation {
+ currentScale = 1.0
+ previousScale = 0
+ currentOffset = .zero
+ previousOffset = .zero
+ }
+ }
+ )
+ }
+ .onAppear {
+ screenWidth = geometry.size.width
+ screenHeight = geometry.size.height
+ }
+ }
+ }
+
+ private func clampOffset(offset: CGSize = .zero) -> CGSize {
+ var newOffset = offset
+
+ if currentScale > 1 {
+ let maxX = ((screenWidth * currentScale) - screenWidth) / 2
+ let maxY = ((screenHeight * currentScale) - screenHeight) / 2
+
+ newOffset.width = min(max(-maxX, newOffset.width), maxX)
+ newOffset.height = min(max(-maxY, newOffset.height), maxY)
+ } else {
+ newOffset = .zero
+ }
+
+ return newOffset
+ }
+}
+
+#Preview {
+ ZoomableImageView(image: Image(systemName: "photo"))
+}