Drastically improve image viewer
Changelog-Added: Drastically improved image viewer Closes: #349
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user