Drastically improve image viewer

Changelog-Added: Drastically improved image viewer
Closes: #349
This commit is contained in:
OlegAba
2023-01-19 21:25:47 -05:00
committed by William Casarin
parent d1e7de5dcb
commit 98c24147e8
3 changed files with 185 additions and 111 deletions

View File

@@ -12,14 +12,14 @@ import Kingfisher
struct ShareSheet: UIViewControllerRepresentable { struct ShareSheet: UIViewControllerRepresentable {
typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
let activityItems: [URL] let activityItems: [URL?]
let callback: Callback? = nil let callback: Callback? = nil
let applicationActivities: [UIActivity]? = nil let applicationActivities: [UIActivity]? = nil
let excludedActivityTypes: [UIActivity.ActivityType]? = nil let excludedActivityTypes: [UIActivity.ActivityType]? = nil
func makeUIViewController(context: Context) -> UIActivityViewController { func makeUIViewController(context: Context) -> UIActivityViewController {
let controller = UIActivityViewController( let controller = UIActivityViewController(
activityItems: activityItems, activityItems: activityItems as [Any],
applicationActivities: applicationActivities) applicationActivities: applicationActivities)
controller.excludedActivityTypes = excludedActivityTypes controller.excludedActivityTypes = excludedActivityTypes
controller.completionWithItemsHandler = callback controller.completionWithItemsHandler = callback
@@ -32,7 +32,7 @@ struct ShareSheet: UIViewControllerRepresentable {
} }
struct ImageContextMenuModifier: ViewModifier { struct ImageContextMenuModifier: ViewModifier {
let url: URL let url: URL?
let image: UIImage? let image: UIImage?
@Binding var showShareSheet: Bool @Binding var showShareSheet: Bool
@@ -64,48 +64,20 @@ struct ImageContextMenuModifier: ViewModifier {
} }
} }
struct ImageView: View { private struct ImageContainerView: View {
let urls: [URL]
@Environment(\.presentationMode) var presentationMode @ObservedObject var imageModel: KFImageModel
//let pubkey: String
//let profiles: Profiles @State private var image: UIImage?
@State private var showShareSheet = false
@GestureState private var scaleState: CGFloat = 1
@GestureState private var offsetState = CGSize.zero init(url: URL?) {
self.imageModel = KFImageModel(
@State private var offset = CGSize.zero url: url,
@State private var scale: CGFloat = 1 fallbackUrl: nil,
maxByteSize: 2000000, // 2 MB
func resetStatus(){ downsampleSize: CGSize(width: 400, height: 400)
self.offset = CGSize.zero )
self.scale = 1
}
var zoomGesture: some Gesture {
MagnificationGesture()
.updating($scaleState) { currentState, gestureState, _ in
gestureState = currentState
}
.onEnded { value in
scale *= value
}
}
var dragGesture: some Gesture {
DragGesture()
.updating($offsetState) { currentState, gestureState, _ in
gestureState = currentState.translation
}.onEnded { value in
offset.height += value.translation.height
offset.width += value.translation.width
}
}
var doubleTapGesture : some Gesture {
TapGesture(count: 2).onEnded { value in
resetStatus()
}
} }
private struct ImageHandler: ImageModifier { private struct ImageHandler: ImageModifier {
@@ -116,79 +88,178 @@ struct ImageView: View {
return image return image
} }
} }
@State private var image: UIImage?
@State private var showShareSheet = false
func onShared(completed: Bool) -> Void { var body: some View {
if (completed) {
showShareSheet = false KFAnimatedImage(imageModel.url)
.callbackQueue(.dispatch(.global(qos: .background)))
.processingQueue(.dispatch(.global(qos: .background)))
.cacheOriginalImage()
.configure { view in
view.framePreloadCount = 1
}
.scaleFactor(UIScreen.main.scale)
.loadDiskFileSynchronously()
.fade(duration: 0.1)
.imageModifier(ImageHandler(handler: $image))
.onFailure { _ in
imageModel.downloadFailed()
}
.id(imageModel.refreshID)
.clipped()
.modifier(ImageContextMenuModifier(url: imageModel.url, image: image, showShareSheet: $showShareSheet))
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [imageModel.url])
}
// TODO: Update ImageCarousel with serializer and processor
// .serialize(by: imageModel.serializer)
// .setProcessor(imageModel.processor)
}
}
struct ZoomableScrollView<Content: View>: UIViewRepresentable {
private var content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator
scrollView.maximumZoomScale = 20
scrollView.minimumZoomScale = 1
scrollView.bouncesZoom = true
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
hostedView.backgroundColor = .clear
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: self.content))
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
context.coordinator.hostingController.rootView = self.content
assert(context.coordinator.hostingController.view.superview == uiView)
}
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
init(hostingController: UIHostingController<Content>) {
self.hostingController = hostingController
} }
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
}
}
struct ImageView: View {
let urls: [URL?]
@Environment(\.presentationMode) var presentationMode
@State private var selectedIndex = 0
@State var showMenu = true
var navBarView: some View {
VStack {
HStack {
Text(urls[selectedIndex]?.lastPathComponent ?? "")
.bold()
Spacer()
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Image(systemName: "xmark")
})
}
.padding()
Divider()
.ignoresSafeArea()
}
.background(.regularMaterial)
}
var tabViewIndicator: some View {
HStack(spacing: 10) {
ForEach(urls.indices, id: \.self) { index in
Capsule()
.fill(index == selectedIndex ? Color(UIColor.label) : Color.secondary)
.frame(width: 7, height: 7)
}
}
.padding()
.background(.regularMaterial)
.clipShape(Capsule())
} }
var body: some View { var body: some View {
ZStack(alignment: .topLeading) { ZStack {
Color("DamusDarkGrey") // Or Color("DamusBlack") Color(.systemBackground)
.edgesIgnoringSafeArea(.all) .ignoresSafeArea()
HStack() { TabView(selection: $selectedIndex) {
Button { ForEach(urls.indices, id: \.self) { index in
presentationMode.wrappedValue.dismiss() ZoomableScrollView {
} label: { ImageContainerView(url: urls[index])
Image(systemName: "xmark") .aspectRatio(contentMode: .fit)
.foregroundColor(.white) }
.font(.largeTitle) .ignoresSafeArea()
.frame(width: 40, height: 40) .tag(index)
.padding(20) .modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
presentationMode.wrappedValue.dismiss()
}))
} }
} }
.zIndex(1) .ignoresSafeArea()
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
VStack(alignment: .center) { .onChange(of: selectedIndex, perform: { _ in
//Spacer() showMenu = true
//.frame(height: 120) })
.onTapGesture {
TabView { showMenu.toggle()
ForEach(urls, id: \.absoluteString) { url in }
VStack{ .overlay(
//Color("DamusDarkGrey") VStack {
Text(url.lastPathComponent) if showMenu {
.foregroundColor(Color("DamusWhite")) navBarView
Spacer()
KFAnimatedImage(url)
.configure { view in if (urls.count > 1) {
view.framePreloadCount = 3 tabViewIndicator
} }
.cacheOriginalImage()
.imageModifier(ImageHandler(handler: $image))
.loadDiskFileSynchronously()
.scaleFactor(UIScreen.main.scale)
.fade(duration: 0.1)
.aspectRatio(contentMode: .fit)
.tabItem {
Text(url.absoluteString)
}
.id(url.absoluteString)
.modifier(ImageContextMenuModifier(url: url, image: image, showShareSheet: $showShareSheet))
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [url])
}
//.padding(100)
.scaledToFit()
.scaleEffect(self.scale * scaleState)
.offset(x: offset.width + offsetState.width, y: offset.height + offsetState.height)
.gesture(SimultaneousGesture(zoomGesture, dragGesture))
.gesture(doubleTapGesture)
.modifier(SwipeToDismissModifier(onDismiss: {
presentationMode.wrappedValue.dismiss()
}))
}.padding(.bottom, 50) // Ensure carousel appears beneath
} }
} }
} .animation(.easeInOut, value: showMenu)
.padding(
.bottom,
UIApplication
.shared
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.first { $0.isKeyWindow }?.safeAreaInsets.bottom
)
)
} }
.tabViewStyle(PageTabViewStyle())
} }
} }
@@ -205,13 +276,15 @@ struct ImageCarousel: View {
.foregroundColor(Color.clear) .foregroundColor(Color.clear)
.overlay { .overlay {
KFAnimatedImage(url) KFAnimatedImage(url)
.configure { view in .callbackQueue(.dispatch(.global(qos: .background)))
view.framePreloadCount = 3 .processingQueue(.dispatch(.global(qos: .background)))
}
.cacheOriginalImage() .cacheOriginalImage()
.loadDiskFileSynchronously() .loadDiskFileSynchronously()
.scaleFactor(UIScreen.main.scale) .scaleFactor(UIScreen.main.scale)
.fade(duration: 0.1) .fade(duration: 0.1)
.configure { view in
view.framePreloadCount = 3
}
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.tabItem { .tabItem {
Text(url.absoluteString) Text(url.absoluteString)

View File

@@ -8,6 +8,7 @@
import SwiftUI import SwiftUI
struct SwipeToDismissModifier: ViewModifier { struct SwipeToDismissModifier: ViewModifier {
let minDistance: CGFloat?
var onDismiss: () -> Void var onDismiss: () -> Void
@State private var offset: CGSize = .zero @State private var offset: CGSize = .zero
@@ -16,7 +17,7 @@ struct SwipeToDismissModifier: ViewModifier {
.offset(y: offset.height) .offset(y: offset.height)
.animation(.interactiveSpring(), value: offset) .animation(.interactiveSpring(), value: offset)
.simultaneousGesture( .simultaneousGesture(
DragGesture() DragGesture(minimumDistance: minDistance ?? 10)
.onChanged { gesture in .onChanged { gesture in
if gesture.translation.width < 50 { if gesture.translation.width < 50 {
offset = gesture.translation offset = gesture.translation

View File

@@ -78,7 +78,7 @@ struct ProfileZoomView: View {
.offset(x: offset.width + offsetState.width, y: offset.height + offsetState.height) .offset(x: offset.width + offsetState.width, y: offset.height + offsetState.height)
.gesture(SimultaneousGesture(zoomGesture, dragGesture)) .gesture(SimultaneousGesture(zoomGesture, dragGesture))
.gesture(doubleTapGesture) .gesture(doubleTapGesture)
.modifier(SwipeToDismissModifier(onDismiss: { .modifier(SwipeToDismissModifier(minDistance: nil, onDismiss: {
presentationMode.wrappedValue.dismiss() presentationMode.wrappedValue.dismiss()
})) }))