From 459fbe9a34392605b7a9a04ca0a363f3386ab698 Mon Sep 17 00:00:00 2001 From: Dhrumil Shah Date: Wed, 1 Mar 2023 11:49:39 -0500 Subject: [PATCH 01/20] WIP: Open Full screen gallery view instead of the info page --- Photoview/Screens/Places/PlacesScreen.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Photoview/Screens/Places/PlacesScreen.swift b/Photoview/Screens/Places/PlacesScreen.swift index 0547abb..7f6bfa5 100644 --- a/Photoview/Screens/Places/PlacesScreen.swift +++ b/Photoview/Screens/Places/PlacesScreen.swift @@ -124,7 +124,7 @@ struct PlacesScreen: View { NavigationView { ZStack { PlacesMapView(markers: markers, selectedAnnotation: $selectedAnnotation) - .ignoresSafeArea() + .edgesIgnoringSafeArea(.top) NavigationLink(destination: ClusterDetailsView(markers: clusterMarkers, location: clusterLocation), isActive: clusterNavigationActive, label: { EmptyView() }) } } @@ -133,7 +133,7 @@ struct PlacesScreen: View { fetchGeoJson() } .sheet(isPresented: imageNavigationActive) { - MediaDetailsView() + FullScreenMediaGalleryView(showMedia: imageNavigationActive) // can crash without this .environmentObject(mediaEnv) } From 1315aa27ef849b7577b8b286e6eaac9bf226e5d9 Mon Sep 17 00:00:00 2001 From: Dhrumil Shah Date: Wed, 1 Mar 2023 11:50:24 -0500 Subject: [PATCH 02/20] WIP: Add isLoading Binding var --- Photoview/Views/AlbumThumbnailView.swift | 2 +- Photoview/Views/FaceThumbnailView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Photoview/Views/AlbumThumbnailView.swift b/Photoview/Views/AlbumThumbnailView.swift index 1490f39..7df75fc 100644 --- a/Photoview/Views/AlbumThumbnailView.swift +++ b/Photoview/Views/AlbumThumbnailView.swift @@ -17,7 +17,7 @@ struct AlbumThumbnailView: View { var body: some View { NavigationLink(destination: destination) { VStack(alignment: .leading) { - ProtectedImageView(url: thumbnail, blurhash: blurhash) { image in + ProtectedImageView(url: thumbnail, isLoading: nil, blurhash: blurhash) { image in AnyView( Image(uiImage: image) .resizable() diff --git a/Photoview/Views/FaceThumbnailView.swift b/Photoview/Views/FaceThumbnailView.swift index 8643333..17720ab 100644 --- a/Photoview/Views/FaceThumbnailView.swift +++ b/Photoview/Views/FaceThumbnailView.swift @@ -54,7 +54,7 @@ struct FaceThumbnailView: View { ZStack { Rectangle() .fill(Color("PlaceholderBackground")) - ProtectedImageView(url: face.imageFaces.first?.media.thumbnail?.url) { image in + ProtectedImageView(url: face.imageFaces.first?.media.thumbnail?.url, isLoading: nil) { image in AnyView( Image(uiImage: image) .resizable() From 3aa5debb41869671b1648834de952495d0cff4c3 Mon Sep 17 00:00:00 2001 From: Dhrumil Shah Date: Wed, 1 Mar 2023 11:52:15 -0500 Subject: [PATCH 03/20] WIP: ProtectedCache Moved for cleaner code and State variable updates for Image Loading indicator --- Photoview/ProtectedCache.swift | 63 ++++++++++++++++++++ Photoview/Views/ProtectedImageView.swift | 75 ++++++------------------ 2 files changed, 82 insertions(+), 56 deletions(-) create mode 100644 Photoview/ProtectedCache.swift diff --git a/Photoview/ProtectedCache.swift b/Photoview/ProtectedCache.swift new file mode 100644 index 0000000..1ffc78f --- /dev/null +++ b/Photoview/ProtectedCache.swift @@ -0,0 +1,63 @@ +// +// ProtectedCache.swift +// Photoview +// +// Created by Dhrumil Shah on 2/24/23. +// + +import Foundation +import SwiftUI + +protocol ProtectedCache { + associatedtype CacheData: AnyObject + var cache: NSCache { get } + func parseData(_ data: Data) -> CacheData? +} + +extension ProtectedCache { + + func fetch(url: String, callback: @escaping (_ data: CacheData) -> Void) -> URLSessionTask? { + if let cachedImage = self.cache.object(forKey: url as NSString) { + callback(cachedImage) + return nil + } + + let request = Network.shared.protectedURLRequest(url: url) + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + if error.localizedDescription != "cancelled" { + print("Error fetching protected image: \(error)") + } + return + } + + if let data = data, let cacheData = self.parseData(data) { + self.cache.setObject(cacheData, forKey: url as NSString) + DispatchQueue.main.async { + callback(cacheData) + } + } + } + + task.resume() + + return task + } + + func clearCache() { + cache.removeAllObjects() + } + +} + +class ProtectedImageCache: ProtectedCache { + internal var count = 0 + static let shared: ProtectedImageCache = ProtectedImageCache() + + internal let cache: NSCache = NSCache() + + func parseData(_ data: Data) -> UIImage? { + UIImage(data: data) + } +} diff --git a/Photoview/Views/ProtectedImageView.swift b/Photoview/Views/ProtectedImageView.swift index d4c4b17..0f04828 100644 --- a/Photoview/Views/ProtectedImageView.swift +++ b/Photoview/Views/ProtectedImageView.swift @@ -8,59 +8,6 @@ import SwiftUI import KeychainSwift -protocol ProtectedCache { - associatedtype CacheData: AnyObject - var cache: NSCache { get } - func parseData(_ data: Data) -> CacheData? -} - -extension ProtectedCache { - - func fetch(url: String, callback: @escaping (_ data: CacheData) -> Void) -> URLSessionTask? { - if let cachedImage = self.cache.object(forKey: url as NSString) { - callback(cachedImage) - return nil - } - - let request = Network.shared.protectedURLRequest(url: url) - - let task = URLSession.shared.dataTask(with: request) { data, response, error in - if let error = error { - if error.localizedDescription != "cancelled" { - print("Error fetching protected image: \(error)") - } - return - } - - if let data = data, let cacheData = self.parseData(data) { - self.cache.setObject(cacheData, forKey: url as NSString) - DispatchQueue.main.async { - callback(cacheData) - } - } - } - - task.resume() - - return task - } - - func clearCache() { - cache.removeAllObjects() - } - -} - -class ProtectedImageCache: ProtectedCache { - static let shared: ProtectedImageCache = ProtectedImageCache() - - internal let cache: NSCache = NSCache() - - func parseData(_ data: Data) -> UIImage? { - UIImage(data: data) - } -} - /// Shows a media thumbnail, used in grids where a lot of media is shown at once struct ProtectedImageView: View { @@ -68,14 +15,22 @@ struct ProtectedImageView: View { let blurhash: String? let imageView: (_ image: UIImage) -> AnyView - init(url: String?, blurhash: String? = nil, imageView: @escaping (_ image: UIImage) -> AnyView) { + @Binding var isLoading: Bool + + init(url: String?, isLoading: Binding?, blurhash: String? = nil, imageView: @escaping (_ image: UIImage) -> AnyView) { self.url = url self.blurhash = blurhash self.imageView = imageView + if isLoading?.wrappedValue != nil { + self._isLoading = isLoading! + } + else { + self._isLoading = .constant(false) + } } init(url: String?) { - self.init(url: url) { img in + self.init(url: url, isLoading: nil) { img in AnyView(Image(uiImage: img)) } } @@ -85,11 +40,13 @@ struct ProtectedImageView: View { @State var canceled: Bool = false @State var imageLoaded: Bool = false + @State var numOfRequests: Int = 0 + var presentImage: UIImage? { if self.image != nil { return self.image } - + if let blurhash = self.blurhash { return UIImage(blurHash: blurhash, size: CGSize(width: 4, height: 3)) } @@ -98,7 +55,9 @@ struct ProtectedImageView: View { } func fetchImage(url: String) { + numOfRequests += 1 self.task = ProtectedImageCache.shared.fetch(url: url) { image in + numOfRequests -= 1 self.image = image } } @@ -119,6 +78,9 @@ struct ProtectedImageView: View { self.image = nil } } + .onChange(of: numOfRequests) { num in + isLoading = num > 0 + } .onAppear { if image == nil, let url = url { canceled = false @@ -127,6 +89,7 @@ struct ProtectedImageView: View { } .onDisappear { canceled = true + numOfRequests = 0 self.task?.cancel() } } From 138d842a43fe61298913378702618dcf811f5907 Mon Sep 17 00:00:00 2001 From: Dhrumil Shah Date: Wed, 1 Mar 2023 11:52:52 -0500 Subject: [PATCH 04/20] WIP: Show full screen photo instead of details --- Photoview/Views/MediaThumbnailView.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Photoview/Views/MediaThumbnailView.swift b/Photoview/Views/MediaThumbnailView.swift index 7ded357..f736224 100644 --- a/Photoview/Views/MediaThumbnailView.swift +++ b/Photoview/Views/MediaThumbnailView.swift @@ -11,8 +11,8 @@ struct MediaThumbnailView: View { let media: MediaItem @EnvironmentObject var mediaEnv: MediaEnvironment - @State var showMediaDetailsSheet: Bool = false - + @State var showMedia: Bool = false + var mediaIndex: Int { return mediaEnv.mediaIndex(media) } @@ -30,16 +30,16 @@ struct MediaThumbnailView: View { return ZStack(alignment: .center) { GeometryReader { geo in - ProtectedImageView(url: media.thumbnail?.url, blurhash: media.blurhash) { image in + ProtectedImageView(url: media.thumbnail?.url, isLoading: nil, blurhash: media.blurhash) { image in AnyView( Image(uiImage: image) .resizable() .scaledToFill() + .frame(width: geo.size.width, height: geo.size.height, alignment: .center) + .clipped() ) } - .frame(height: geo.size.width) } - .clipped() .aspectRatio(1, contentMode: .fit) if media.type == .video { @@ -51,12 +51,12 @@ struct MediaThumbnailView: View { var body: some View { Button(action: { mediaEnv.activeMediaIndex = mediaIndex - showMediaDetailsSheet = true + showMedia = true }) { thumbnailView } - .sheet(isPresented: $showMediaDetailsSheet) { - MediaDetailsView() + .fullScreenCover(isPresented: $showMedia) { + FullScreenMediaGalleryView(showMedia: $showMedia) // can crash without this .environmentObject(mediaEnv) } From 649a23c01e1ef1fb9a120a5910ec511e74bcdc11 Mon Sep 17 00:00:00 2001 From: Dhrumil Shah Date: Wed, 1 Mar 2023 11:53:27 -0500 Subject: [PATCH 05/20] WIP: Full screen image redesign + details view cleanup --- Photoview/Views/ProtectedMediaView.swift | 92 +++++++++++++++++++----- 1 file changed, 73 insertions(+), 19 deletions(-) diff --git a/Photoview/Views/ProtectedMediaView.swift b/Photoview/Views/ProtectedMediaView.swift index fc88be7..b76baa2 100644 --- a/Photoview/Views/ProtectedMediaView.swift +++ b/Photoview/Views/ProtectedMediaView.swift @@ -10,43 +10,97 @@ import SwiftUI struct ProtectedMediaView: View { let mediaDetails: MediaDetailsQuery.Data.Medium? let mediaItem: MediaItem + typealias Download = MediaDetailsQuery.Data.Medium.Download + @Binding private var isVideo: Bool + + @State private var scale: CGFloat = 1.0 + @GestureState private var magnifyBy = CGFloat(1.0) + + @Binding private var isLoading: Bool + + @State private var showVideo: Bool = false + + private var geo: GeometryProxy + /// Show the fully loaded media view - init(mediaDetails: MediaDetailsQuery.Data.Medium) { + init(mediaDetails: MediaDetailsQuery.Data.Medium, isLoading: Binding, isVideo: Binding, geo: GeometryProxy) { self.mediaDetails = mediaDetails self.mediaItem = mediaDetails.fragments.mediaItem + self._isLoading = isLoading + self._isVideo = isVideo + self.geo = geo } /// Show a thumbnail like media view with less information - init(mediaItem: MediaItem) { + init(mediaItem: MediaItem, isVideo: Binding, geo: GeometryProxy) { self.mediaDetails = nil self.mediaItem = mediaItem + self._isLoading = .constant(false) + self._isVideo = isVideo + self.geo = geo } - var imageView: AnyView { - AnyView( - ProtectedImageView(url: self.mediaItem.thumbnail?.url, blurhash: self.mediaItem.blurhash) { image in - AnyView( - Image(uiImage: image) - .resizable() - .scaledToFit() - ) - } - ) + var imageView: ProtectedImageView { + let downloads: [Download] = (mediaDetails?.downloads ?? []).sorted { $0.mediaUrl.fileSize > $1.mediaUrl.fileSize } + self.isVideo = false + let url = (downloads.count > 0 ? downloads[0].mediaUrl.url : self.mediaItem.thumbnail?.url) + + return ProtectedImageView(url: url, isLoading: $isLoading, blurhash: self.mediaItem.blurhash) { image in + return AnyView( + SwiftUIZoomableImageViewer(image: Image(uiImage: image)) + ) + } } - func videoView(mediaDetails: MediaDetailsQuery.Data.Medium) -> AnyView { - AnyView( - ProtectedVideoView(url: mediaDetails.videoWeb?.url) - ) + func videoView(mediaDetails: MediaDetailsQuery.Data.Medium) -> some View { + self.isVideo = true + let media = mediaDetails.fragments.mediaItem + return GeometryReader { geo in + ZStack { + Button(action: { + self.showVideo = true + }) { + ZStack { + Rectangle() + .fill(Color.clear) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea() + VStack { + Spacer() + Image(systemName: "play.circle.fill") + .resizable() + .frame(width: min(geo.size.height, geo.size.width) / 5, height: min(geo.size.height, geo.size.width) / 5, alignment: .center) + .foregroundColor(.white) + .opacity(0.8) + Spacer() + } + } + } + .zIndex(1) + ProtectedImageView(url: media.thumbnail?.url, isLoading: nil, blurhash: media.blurhash) { image in + AnyView( + Image(uiImage: image) + .resizable() + .scaledToFit() + .clipped() + ) + } + } + .frame(width: geo.size.width, height: geo.size.height) + } + .fullScreenCover(isPresented: $showVideo) { + ProtectedVideoView(url: mediaDetails.videoWeb?.url, isLoading: $isLoading) + .ignoresSafeArea() + } } func detailedView(mediaDetails: MediaDetailsQuery.Data.Medium) -> AnyView { switch self.mediaItem.type { case .photo: - return imageView + return AnyView(imageView) case .video: - return videoView(mediaDetails: mediaDetails) + return AnyView(videoView(mediaDetails: mediaDetails).ignoresSafeArea()) case .__unknown(let raw): fatalError("Unknown media type: \(raw)") } @@ -56,7 +110,7 @@ struct ProtectedMediaView: View { if let mediaDetails = mediaDetails { return detailedView(mediaDetails: mediaDetails) } else { - return imageView + return AnyView(imageView) } } } From 96c5ef547af75a8dc9c8110a63b3dc9bb4746013 Mon Sep 17 00:00:00 2001 From: Dhrumil Shah Date: Wed, 1 Mar 2023 11:54:12 -0500 Subject: [PATCH 06/20] WIP: Made F.S. media view name more descriptive --- .../MediaDetails/FullScreenGalleryView.swift | 29 --- .../FullScreenMediaGalleryView.swift | 183 ++++++++++++++++++ 2 files changed, 183 insertions(+), 29 deletions(-) delete mode 100644 Photoview/Views/MediaDetails/FullScreenGalleryView.swift create mode 100644 Photoview/Views/MediaDetails/FullScreenMediaGalleryView.swift diff --git a/Photoview/Views/MediaDetails/FullScreenGalleryView.swift b/Photoview/Views/MediaDetails/FullScreenGalleryView.swift deleted file mode 100644 index 3ccead8..0000000 --- a/Photoview/Views/MediaDetails/FullScreenGalleryView.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// FullScreenGalleryView.swift -// Photoview -// -// Created by Viktor Strate Kløvedal on 26/07/2021. -// - -import SwiftUI - -struct FullScreenGalleryView: View { - let mediaDetails: MediaDetailsQuery.Data.Medium? - @EnvironmentObject var mediaEnv: MediaEnvironment - - var body: some View { - ZStack { - Rectangle() - .fill(Color.black) - ThumbnailDetailsView(mediaDetails: mediaDetails, fullscreenMode: true) - } - .ignoresSafeArea() - } -} - -struct FullScreenGalleryView_Previews: PreviewProvider { - static var previews: some View { - FullScreenGalleryView(mediaDetails: nil) - // .environmentObject(MediaDetailsView_Previews.mediaEnvironment) - } -} diff --git a/Photoview/Views/MediaDetails/FullScreenMediaGalleryView.swift b/Photoview/Views/MediaDetails/FullScreenMediaGalleryView.swift new file mode 100644 index 0000000..40a03ef --- /dev/null +++ b/Photoview/Views/MediaDetails/FullScreenMediaGalleryView.swift @@ -0,0 +1,183 @@ +// +// FullScreenMediaGalleryView.swift +// Photoview +// +// Created by Viktor Strate Kløvedal on 25/07/2021. +// + +import SwiftUI +import Apollo + +struct FullScreenMediaGalleryView: View { + + @EnvironmentObject var mediaEnv: MediaEnvironment + @EnvironmentObject var showWelcome: ShowWelcomeScreen + @Binding var showMedia: Bool + + @State var mediaDetails: MediaDetailsQuery.Data.Medium? = nil + + @State private var isShowingNavbarMenu = true + @State private var isZoomed = false + @State var isLoading = false + @State var isVideo = false + + func fetchMediaDetails() { + guard let activeMedia = mediaEnv.activeMedia else { return } + Network.shared.apollo?.fetch(query: MediaDetailsQuery(mediaID: activeMedia.id), cachePolicy: .returnCacheDataAndFetch) { data in + switch data { + case .success(let data): + DispatchQueue.main.async { + self.mediaDetails = data.data?.media + if let thumbnail = self.mediaDetails?.fragments.mediaItem.thumbnail { + mediaEnv.media?[mediaEnv.activeMediaIndex].thumbnail = thumbnail + } + } + case .failure(let error): + Network.shared.handleGraphqlError(error: NetworkError(message: "Failed to fetch media details", error: error), showWelcomeScreen: showWelcome) + } + } + } + + var mediaInformationView: some View { + VStack { + List { + if let exif = mediaDetails?.exif { + ExifDetailsView(exif: exif) + .listRowBackground(Color.clear) + } + DownloadDetailsView(downloads: (mediaDetails?.downloads ?? []).sorted { $0.mediaUrl.fileSize > $1.mediaUrl.fileSize }) + if let activeMedia = mediaEnv.activeMedia { + ShareDetailsView(mediaID: activeMedia.id, shares: mediaDetails?.shares ?? [], refreshMediaDetails: { fetchMediaDetails() }) + } + } + .listStyle(InsetGroupedListStyle()) + .redacted(reason: mediaDetails == nil ? .placeholder : []) + Spacer() + Text(mediaDetails?.title ?? "").font(.footnote) + } + .navigationBarTitle("Details", displayMode: .inline) + } + + var body: some View { + NavigationView { + ZStack { + ZStack { + Rectangle() + .fill(Color.black) + .opacity(isShowingNavbarMenu ? 0.0 : 1.0) + MediaGalleryView(mediaDetails: mediaDetails, isLoading: $isLoading, isVideo: $isVideo) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea() + .onTapGesture { + // Disable the onTap gesture if it's a video so they can tap to play the video + if !isVideo { + withAnimation(.easeIn(duration: 0.35)) { + isShowingNavbarMenu.toggle() + } + } + else { + isShowingNavbarMenu = true + } + } + VStack(spacing: 0) { + // Always show the nav bar if it's a video + // Do not want to conflict "hide navbar" tap with a "play video" tap + if isShowingNavbarMenu || isVideo { + ZStack { + HStack { + Spacer() + if isLoading { + ProgressView() + } + Spacer() + } + HStack { + Spacer() + Group { + // Info button + NavigationLink(destination: mediaInformationView) { + Image(systemName: "info.square") + .imageScale(.large) // Icon size + .padding(.trailing, 5) + } + + // Close button + Button(action: { + showMedia = false + isLoading = false + }) { + Image(systemName: "xmark") + .imageScale(.large) // Icon size + .padding(.trailing, 5) + } + .padding() + } + } + } +// .frame(height: 44) + .background(BlurBackgroundVisualEffectView(effect: UIBlurEffect(style: .systemMaterial)) + .ignoresSafeArea()) + Spacer() // Pushes the bar to the top of the screen + } + } + } + .navigationBarTitleDisplayMode(.inline) + } + .onAppear { + fetchMediaDetails() + } + .onChange(of: mediaEnv.activeMediaIndex, perform: { value in + fetchMediaDetails() + }) + .statusBarHidden(!isShowingNavbarMenu) + } + +} + +struct MediaDetailsView_Previews: PreviewProvider { + +// static let sampleMedia = MediaDetailsQuery.Data.Medium( +// id: "123", +// title: "Media title", +// thumbnail: nil, +// exif: MediaDetailsQuery.Data.Medium.Exif( +// camera: "Camera", +// maker: "Model 3000", +// lens: "300 mm", +// dateShot: Time(date: Date()), +// exposure: 0.01, +// aperture: 2.4, +// iso: 100, +// focalLength: 35, +// flash: 0, +// exposureProgram: 1), +// type: .photo, shares: [], downloads: [ +// MediaDetailsQuery.Data.Medium.Download(title: "Original", mediaUrl: MediaDetailsQuery.Data.Medium.Download.MediaUrl(url: "link", width: 1080, height: 720, fileSize: 20000)), +// MediaDetailsQuery.Data.Medium.Download(title: "High-res", mediaUrl: MediaDetailsQuery.Data.Medium.Download.MediaUrl(url: "link", width: 1080, height: 720, fileSize: 20000)), +// MediaDetailsQuery.Data.Medium.Download(title: "Thumbnail", mediaUrl: MediaDetailsQuery.Data.Medium.Download.MediaUrl(url: "link", width: 1080, height: 720, fileSize: 20000)) +// ]) +// +// static let mediaEnvironment = MediaEnvironment( +// media: [MediaEnvironment.Media(id: "123", thumbnail: nil, favorite: false)], +// activeMediaIndex: 0 +// ) + + static var previews: some View { + FullScreenMediaGalleryView(mediaEnv: EnvironmentObject(), showMedia: .constant(true), mediaDetails: nil) +// .environmentObject(mediaEnvironment) + } +} + +// Blurred Background +struct BlurBackgroundVisualEffectView: UIViewRepresentable { + var effect: UIVisualEffect? + + func makeUIView(context: UIViewRepresentableContext) -> UIVisualEffectView { + return UIVisualEffectView() + } + + func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext) { + uiView.effect = effect + } +} From 2e0a8807c2bb02d9c32ba012b962a1a01c9755dd Mon Sep 17 00:00:00 2001 From: Dhrumil Shah Date: Wed, 1 Mar 2023 11:54:48 -0500 Subject: [PATCH 07/20] WIP: Fix visual spacing issue if there is no exif data --- .../Views/MediaDetails/ExifDetailsView.swift | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/Photoview/Views/MediaDetails/ExifDetailsView.swift b/Photoview/Views/MediaDetails/ExifDetailsView.swift index ed8609b..8d3be48 100644 --- a/Photoview/Views/MediaDetails/ExifDetailsView.swift +++ b/Photoview/Views/MediaDetails/ExifDetailsView.swift @@ -28,6 +28,8 @@ struct ExifDetailsView: View { if let aperture = exif.aperture { result.append(ExifValue(label: "Aperture", value: Self.formatAperture(value: aperture))) } if let iso = exif.iso { result.append(ExifValue(label: "ISO", value: "\(iso)")) } if let focalLength = exif.focalLength { result.append(ExifValue(label: "Focal length", value: Self.formatFocalLength(value: focalLength))) } + + return result } @@ -72,22 +74,30 @@ struct ExifDetailsView: View { } var body: some View { - HStack { - VStack(alignment: .trailing) { - ForEach(values, id: \.label) { value in - Text(value.label) - .font(.caption) - .foregroundColor(.secondary) - .frame(height: 24) - } - } - VStack(alignment: .leading) { - ForEach(values, id: \.label) { value in - Text(value.value) - .frame(height: 24) - } + if !values.isEmpty { + VStack(alignment: .leading) { + HStack { + VStack(alignment: .trailing) { + ForEach(values, id: \.label) { value in + Text(value.label) + .font(.caption) + .foregroundColor(.secondary) + .frame(height: 24) + } + } + VStack(alignment: .leading) { + ForEach(values, id: \.label) { value in + Text(value.value) + .font(.caption) + .frame(height: 24) + } + } + } + } + .foregroundColor(.primary) + .textCase(.none) + .padding(.top, 0) } - } } } From a253402c2f9d15b5fb5940fb9606d536307f9659 Mon Sep 17 00:00:00 2001 From: Dhrumil Shah Date: Wed, 1 Mar 2023 11:55:14 -0500 Subject: [PATCH 08/20] WIP: Make Media view name more descriptive --- .../Views/MediaDetails/MediaGalleryView.swift | 159 ++++++++++++++++++ .../MediaDetails/ThumbnailDetailsView.swift | 146 ---------------- 2 files changed, 159 insertions(+), 146 deletions(-) create mode 100644 Photoview/Views/MediaDetails/MediaGalleryView.swift delete mode 100644 Photoview/Views/MediaDetails/ThumbnailDetailsView.swift diff --git a/Photoview/Views/MediaDetails/MediaGalleryView.swift b/Photoview/Views/MediaDetails/MediaGalleryView.swift new file mode 100644 index 0000000..cdab04e --- /dev/null +++ b/Photoview/Views/MediaDetails/MediaGalleryView.swift @@ -0,0 +1,159 @@ +// +// MediaGalleryView.swift +// Photoview +// +// Created by Viktor Strate Kløvedal on 25/07/2021. +// + +import SwiftUI + +struct MediaGalleryView: View { + + let mediaDetails: MediaDetailsQuery.Data.Medium? + + @EnvironmentObject var mediaEnv: MediaEnvironment + + @Binding var isLoading: Bool; + + @State var touchStarted: Bool = false + @State var currentOffset: CGSize = CGSize(width: 0, height: 0) + @State var previousOffset: CGSize = CGSize(width: 0, height: 0) + @State var currentScale: CGFloat = 1 + @State var previousScale: CGFloat = 1 + + @State var currentImageScrollIndex = 0 + @State private var loadedTabs = Set([0]) + + @State var totalNumberOfImages: ClosedRange = 0 ... 0 + @State private var imageRange: ClosedRange = 0 ... 1 + + @Binding var isVideo: Bool + + func imageView(index: Int, geo: GeometryProxy) -> some View { + if (mediaEnv.activeMediaIndex == index) { + let media = mediaEnv.media![index] + + if let mediaDetails = mediaDetails, index == mediaEnv.activeMediaIndex, media.id == mediaDetails.id { + return AnyView(ProtectedMediaView(mediaDetails: mediaDetails, isLoading: $isLoading, isVideo: $isVideo, geo: geo)) + } else { + return AnyView(ProtectedMediaView(mediaItem: media, isVideo: $isVideo, geo: geo)) + } + } + else { + return AnyView(ProgressView()) + } + } + + func imagePreloadRangeCalc() -> ClosedRange { + guard let media = mediaEnv.media else { return mediaEnv.activeMediaIndex ... mediaEnv.activeMediaIndex } + + currentImageScrollIndex = mediaEnv.activeMediaIndex + + let minVal = max(mediaEnv.activeMediaIndex - 2, 0) + let maxVal = min(mediaEnv.activeMediaIndex + 2, media.count - 1) + + // TODO: Could be removed + totalNumberOfImages = (0 ... media.count - 1) + + return (minVal ... maxVal) + } + + let IMAGE_PADDING: CGFloat = 50 + + var body: some View { + GeometryReader { geo in + TabView(selection: $currentImageScrollIndex) { + ForEach(totalNumberOfImages, id: \.self) { index in + imageView(index: index, geo: geo) + } + } + .tabViewStyle(.page(indexDisplayMode: .never)) + .onChange(of: currentImageScrollIndex) { newValue in + mediaEnv.activeMediaIndex = newValue + imageRange = imagePreloadRangeCalc() + } + } + .onAppear() { + imageRange = imagePreloadRangeCalc() + } + } +} + +//extension MediaGalleryView { +// struct ThumbnailGestures: ViewModifier { +// let fullscreenMode: Bool +// @Binding var touchStarted: Bool +// @Binding var previousOffset: CGSize +// @Binding var currentOffset: CGSize +// @Binding var previousScale: CGFloat +// @Binding var currentScale: CGFloat +// @EnvironmentObject var mediaEnv: MediaEnvironment +// +// func body(content: Content) -> some View { +// content +// .gesture( +// DragGesture(minimumDistance: 30, coordinateSpace: .local) +// .onChanged({ drag in +// var newOffset = CGSize(width: drag.translation.width + previousOffset.width, height: previousOffset.height) +// +// if currentScale > 1 { +// newOffset = CGSize(width: newOffset.width, height: newOffset.height + drag.translation.height) +// } +// +// if touchStarted { +// currentOffset = newOffset +// } else { +// touchStarted = true +// withAnimation(Animation.spring(response: 0, dampingFraction: 1, blendDuration: 0)) { +// currentOffset = newOffset +// } +// } +// }) +// .onEnded({ drag in +// let SHIFT_THRESHOLD: CGFloat = 200 +// touchStarted = false +// +// let newOffset = CGSize(width: drag.translation.width + previousOffset.width, height: drag.translation.height + previousOffset.height) +// +// withAnimation(Animation.spring(response: 0.3, dampingFraction: 0.9, blendDuration: 0)) { +// if currentScale == 1 { +// currentOffset = CGSize(width: 0, height: 0) +// } else { +// currentOffset = newOffset +// } +// +// if (drag.predictedEndTranslation.width - SHIFT_THRESHOLD > 0 && mediaEnv.activeMediaIndex > 0) { +// mediaEnv.activeMediaIndex -= 1 +// } +// if (drag.predictedEndTranslation.width + SHIFT_THRESHOLD < 0 && mediaEnv.activeMediaIndex + 1 < (mediaEnv.media?.count ?? 0)) { +// mediaEnv.activeMediaIndex += 1 +// } +// } +// +// previousOffset = currentOffset +// }) +// ) +// .gesture( +// MagnificationGesture() +// .onChanged({ newScale in +// guard fullscreenMode else { return } +// +// currentScale = newScale * previousScale +// }) +// .onEnded({ newScale in +// guard fullscreenMode else { return } +// +// withAnimation(.interactiveSpring()) { +// currentScale = max(1, newScale * previousScale) +// previousScale = currentScale +// +// if currentScale == 1 { +// currentOffset = CGSize(width: 0, height: 0) +// previousOffset = currentOffset +// } +// } +// }) +// ) +// } +// } +//} diff --git a/Photoview/Views/MediaDetails/ThumbnailDetailsView.swift b/Photoview/Views/MediaDetails/ThumbnailDetailsView.swift deleted file mode 100644 index 50187d7..0000000 --- a/Photoview/Views/MediaDetails/ThumbnailDetailsView.swift +++ /dev/null @@ -1,146 +0,0 @@ -// -// ThumbnailDetailsView.swift -// Photoview -// -// Created by Viktor Strate Kløvedal on 25/07/2021. -// - -import SwiftUI - -struct ThumbnailDetailsView: View { - - let mediaDetails: MediaDetailsQuery.Data.Medium? - let fullscreenMode: Bool - - @EnvironmentObject var mediaEnv: MediaEnvironment - - @State var touchStarted: Bool = false - @State var currentOffset: CGSize = CGSize(width: 0, height: 0) - @State var previousOffset: CGSize = CGSize(width: 0, height: 0) - @State var currentScale: CGFloat = 1 - @State var previousScale: CGFloat = 1 - - func imageView(index: Int) -> some View { - let media = mediaEnv.media![index] - - if let mediaDetails = mediaDetails, index == mediaEnv.activeMediaIndex, media.id == mediaDetails.id { - return ProtectedMediaView(mediaDetails: mediaDetails) - } else { - return ProtectedMediaView(mediaItem: media) - } - } - - var imageRange: ClosedRange { - guard let media = mediaEnv.media else { return mediaEnv.activeMediaIndex ... mediaEnv.activeMediaIndex } - - let minVal = max(mediaEnv.activeMediaIndex - 1, 0) - let maxVal = min(mediaEnv.activeMediaIndex + 1, media.count - 1) - - return (minVal ... maxVal) - } - - let IMAGE_PADDING: CGFloat = 50 - - func thumbnails(geo: GeometryProxy) -> some View { - ZStack { - ForEach(imageRange, id: \.self) { index in - imageView(index: index) - .offset(x: (currentOffset.width / currentScale + CGFloat(index-mediaEnv.activeMediaIndex) * (geo.size.width + IMAGE_PADDING)), y: currentOffset.height / currentScale) - // .scaleEffect(index == mediaEnv.activeMediaIndex ? currentScale : 1) - } - } - .frame(width: geo.size.width) - .scaleEffect(currentScale) - .modifier(Self.ThumbnailGestures(fullscreenMode: fullscreenMode, touchStarted: $touchStarted, previousOffset: $previousOffset, currentOffset: $currentOffset, previousScale: $previousScale, currentScale: $currentScale)) - } - - var body: some View { - GeometryReader { geo in - thumbnails(geo: geo) - } - .aspectRatio(CGSize(width: mediaEnv.activeMedia?.thumbnail?.width ?? 3, height: mediaEnv.activeMedia?.thumbnail?.height ?? 2), contentMode: .fit) - } -} - -extension ThumbnailDetailsView { - struct ThumbnailGestures: ViewModifier { - let fullscreenMode: Bool - @Binding var touchStarted: Bool - @Binding var previousOffset: CGSize - @Binding var currentOffset: CGSize - @Binding var previousScale: CGFloat - @Binding var currentScale: CGFloat - @EnvironmentObject var mediaEnv: MediaEnvironment - - func body(content: Content) -> some View { - content - .gesture( - DragGesture(minimumDistance: 30, coordinateSpace: .local) - .onChanged({ drag in - var newOffset = CGSize(width: drag.translation.width + previousOffset.width, height: previousOffset.height) - - if currentScale > 1 { - newOffset = CGSize(width: newOffset.width, height: newOffset.height + drag.translation.height) - } - - if touchStarted { - currentOffset = newOffset - } else { - touchStarted = true - withAnimation(Animation.spring(response: 0, dampingFraction: 1, blendDuration: 0)) { - currentOffset = newOffset - } - } - }) - .onEnded({ drag in - let SHIFT_THRESHOLD: CGFloat = 200 - touchStarted = false - - let newOffset = CGSize(width: drag.translation.width + previousOffset.width, height: drag.translation.height + previousOffset.height) - - withAnimation(Animation.spring(response: 0.3, dampingFraction: 0.9, blendDuration: 0)) { - if currentScale == 1 { - currentOffset = CGSize(width: 0, height: 0) - } else { - currentOffset = newOffset - } - - if (drag.predictedEndTranslation.width - SHIFT_THRESHOLD > 0 && mediaEnv.activeMediaIndex > 0) { - mediaEnv.activeMediaIndex -= 1 - } - if (drag.predictedEndTranslation.width + SHIFT_THRESHOLD < 0 && mediaEnv.activeMediaIndex + 1 < (mediaEnv.media?.count ?? 0)) { - mediaEnv.activeMediaIndex += 1 - } - } - - previousOffset = currentOffset - }) - ) - //.onTapGesture { - // Disabled until a better solution is made - // mediaEnv.fullScreen = !fullscreenMode - //} - .gesture( - MagnificationGesture() - .onChanged({ newScale in - guard fullscreenMode else { return } - - currentScale = newScale * previousScale - }) - .onEnded({ newScale in - guard fullscreenMode else { return } - - withAnimation(.interactiveSpring()) { - currentScale = max(1, newScale * previousScale) - previousScale = currentScale - - if currentScale == 1 { - currentOffset = CGSize(width: 0, height: 0) - previousOffset = currentOffset - } - } - }) - ) - } - } -} From 52c02f88dbac58515092d5b1f7b271b46598f861 Mon Sep 17 00:00:00 2001 From: Dhrumil Shah Date: Wed, 1 Mar 2023 11:55:47 -0500 Subject: [PATCH 09/20] WIP: New Full Screen video player --- .../FullscreenAVPlayerView.swift | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 Photoview/Views/Supporting Views/FullscreenAVPlayerView.swift diff --git a/Photoview/Views/Supporting Views/FullscreenAVPlayerView.swift b/Photoview/Views/Supporting Views/FullscreenAVPlayerView.swift new file mode 100644 index 0000000..65589c8 --- /dev/null +++ b/Photoview/Views/Supporting Views/FullscreenAVPlayerView.swift @@ -0,0 +1,68 @@ +// +// FullscreenAVPlayerView.swift +// Photoview +// +// Created by Dhrumil Shah on 3/1/23. +// + +import SwiftUI +import PDFKit +import AVFoundation +import AVKit + +struct FullscreenAVPlayerView: UIViewControllerRepresentable { + typealias UIViewControllerType = AVPlayerViewController + + var player: AVPlayer + + func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) { + playerController.modalPresentationStyle = .overFullScreen + playerController.player = player + playerController.player?.play() + playerController.showsPlaybackControls = true + playerController.allowsPictureInPicturePlayback = true + playerController.entersFullScreenWhenPlaybackBegins = true + if #available(iOS 16, *) { + playerController.allowsVideoFrameAnalysis = true + } + + // Add swipe down gesture recognizer + let swipeDown = UISwipeGestureRecognizer(target: context.coordinator, action: #selector(context.coordinator.handleSwipeGesture)) + swipeDown.direction = .down + playerController.view.addGestureRecognizer(swipeDown) + + + playerController.viewWillLayoutSubviews() + } + + func makeUIViewController(context: Context) -> AVPlayerViewController { + let playerController = AVPlayerViewController() + + // Set the coordinator as the gesture recognizer's target + playerController.view.isUserInteractionEnabled = true + playerController.view.addGestureRecognizer(UITapGestureRecognizer(target: context.coordinator, action: nil)) + + // Initialize the coordinator + context.coordinator.playerController = playerController + + return playerController + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + var parent: FullscreenAVPlayerView + var playerController: AVPlayerViewController? + + init(_ parent: FullscreenAVPlayerView) { + self.parent = parent + } + + // Handle swipe gesture by dismissing the player controller + @objc func handleSwipeGesture() { + playerController?.dismiss(animated: true, completion: nil) + } + } +} From 64d8d75c3aa50f14f1b8ddf3e7f499d458f7d9d0 Mon Sep 17 00:00:00 2001 From: Dhrumil Shah Date: Wed, 1 Mar 2023 11:56:02 -0500 Subject: [PATCH 10/20] WIP: New Image viewer --- .../SwiftUIZoomableImageViewer.swift | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 Photoview/Views/Supporting Views/SwiftUIZoomableImageViewer.swift diff --git a/Photoview/Views/Supporting Views/SwiftUIZoomableImageViewer.swift b/Photoview/Views/Supporting Views/SwiftUIZoomableImageViewer.swift new file mode 100644 index 0000000..9ba308a --- /dev/null +++ b/Photoview/Views/Supporting Views/SwiftUIZoomableImageViewer.swift @@ -0,0 +1,100 @@ +// +// SwiftUIZoomableImageViewer.swift +// Photoview +// Adapted from https://github.com/fuzzzlove/swiftui-image-viewer/ +// Created by Dhrumil Shah on 2/26/23. +// + +import SwiftUI + +public struct SwiftUIZoomableImageViewer: View { + let image: Image + + @State private var scale: CGFloat = 1 + @State private var lastScale: CGFloat = 1 + + @State private var offset: CGPoint = .zero + @State private var lastTranslation: CGSize = .zero + + public init(image: Image) { + self.image = image + } + + public var body: some View { + GeometryReader { proxy in + ZStack { + image + .resizable() + .aspectRatio(contentMode: .fit) + .scaleEffect(scale) + .offset(x: offset.x, y: offset.y) + .gesture(scale > 1 ? makeDragGesture(size: proxy.size) : nil) + .gesture(makeMagnificationGesture(size: proxy.size)) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .edgesIgnoringSafeArea(.all) + } + } + + private func makeMagnificationGesture(size: CGSize) -> some Gesture { + MagnificationGesture() + .onChanged { value in + let delta = value / lastScale + lastScale = value + + // To minimize jittering + if abs(1 - delta) > 0.01 { + scale *= delta + } + } + .onEnded { _ in + lastScale = 1 + if scale < 1 { + withAnimation { + scale = 1 + } + } + adjustMaxOffset(size: size) + } + } + + private func makeDragGesture(size: CGSize) -> some Gesture { + return DragGesture() + .onChanged { value in + if scale > 1 { + let diff = CGPoint( + x: value.translation.width - lastTranslation.width, + y: value.translation.height - lastTranslation.height + ) + offset = .init(x: offset.x + diff.x, y: offset.y + diff.y) + lastTranslation = value.translation + } + } + .onEnded { _ in + adjustMaxOffset(size: size) + } + } + + private func adjustMaxOffset(size: CGSize) { + let maxOffsetX = (size.width * (scale - 1)) / 2 + let maxOffsetY = (size.height * (scale - 1)) / 2 + + var newOffsetX = offset.x + var newOffsetY = offset.y + + if abs(newOffsetX) > maxOffsetX { + newOffsetX = maxOffsetX * (abs(newOffsetX) / newOffsetX) + } + if abs(newOffsetY) > maxOffsetY { + newOffsetY = maxOffsetY * (abs(newOffsetY) / newOffsetY) + } + + let newOffset = CGPoint(x: newOffsetX, y: newOffsetY) + if newOffset != offset { + withAnimation { + offset = newOffset + } + } + self.lastTranslation = .zero + } +} From 263005eb0416fff7746849936b1019814348b486 Mon Sep 17 00:00:00 2001 From: Dhrumil Shah Date: Wed, 1 Mar 2023 11:57:13 -0500 Subject: [PATCH 11/20] Revert "WIP: Made F.S. media view name more descriptive" This reverts commit 96c5ef547af75a8dc9c8110a63b3dc9bb4746013. --- .../MediaDetails/FullScreenGalleryView.swift | 29 +++ .../FullScreenMediaGalleryView.swift | 183 ------------------ 2 files changed, 29 insertions(+), 183 deletions(-) create mode 100644 Photoview/Views/MediaDetails/FullScreenGalleryView.swift delete mode 100644 Photoview/Views/MediaDetails/FullScreenMediaGalleryView.swift diff --git a/Photoview/Views/MediaDetails/FullScreenGalleryView.swift b/Photoview/Views/MediaDetails/FullScreenGalleryView.swift new file mode 100644 index 0000000..3ccead8 --- /dev/null +++ b/Photoview/Views/MediaDetails/FullScreenGalleryView.swift @@ -0,0 +1,29 @@ +// +// FullScreenGalleryView.swift +// Photoview +// +// Created by Viktor Strate Kløvedal on 26/07/2021. +// + +import SwiftUI + +struct FullScreenGalleryView: View { + let mediaDetails: MediaDetailsQuery.Data.Medium? + @EnvironmentObject var mediaEnv: MediaEnvironment + + var body: some View { + ZStack { + Rectangle() + .fill(Color.black) + ThumbnailDetailsView(mediaDetails: mediaDetails, fullscreenMode: true) + } + .ignoresSafeArea() + } +} + +struct FullScreenGalleryView_Previews: PreviewProvider { + static var previews: some View { + FullScreenGalleryView(mediaDetails: nil) + // .environmentObject(MediaDetailsView_Previews.mediaEnvironment) + } +} diff --git a/Photoview/Views/MediaDetails/FullScreenMediaGalleryView.swift b/Photoview/Views/MediaDetails/FullScreenMediaGalleryView.swift deleted file mode 100644 index 40a03ef..0000000 --- a/Photoview/Views/MediaDetails/FullScreenMediaGalleryView.swift +++ /dev/null @@ -1,183 +0,0 @@ -// -// FullScreenMediaGalleryView.swift -// Photoview -// -// Created by Viktor Strate Kløvedal on 25/07/2021. -// - -import SwiftUI -import Apollo - -struct FullScreenMediaGalleryView: View { - - @EnvironmentObject var mediaEnv: MediaEnvironment - @EnvironmentObject var showWelcome: ShowWelcomeScreen - @Binding var showMedia: Bool - - @State var mediaDetails: MediaDetailsQuery.Data.Medium? = nil - - @State private var isShowingNavbarMenu = true - @State private var isZoomed = false - @State var isLoading = false - @State var isVideo = false - - func fetchMediaDetails() { - guard let activeMedia = mediaEnv.activeMedia else { return } - Network.shared.apollo?.fetch(query: MediaDetailsQuery(mediaID: activeMedia.id), cachePolicy: .returnCacheDataAndFetch) { data in - switch data { - case .success(let data): - DispatchQueue.main.async { - self.mediaDetails = data.data?.media - if let thumbnail = self.mediaDetails?.fragments.mediaItem.thumbnail { - mediaEnv.media?[mediaEnv.activeMediaIndex].thumbnail = thumbnail - } - } - case .failure(let error): - Network.shared.handleGraphqlError(error: NetworkError(message: "Failed to fetch media details", error: error), showWelcomeScreen: showWelcome) - } - } - } - - var mediaInformationView: some View { - VStack { - List { - if let exif = mediaDetails?.exif { - ExifDetailsView(exif: exif) - .listRowBackground(Color.clear) - } - DownloadDetailsView(downloads: (mediaDetails?.downloads ?? []).sorted { $0.mediaUrl.fileSize > $1.mediaUrl.fileSize }) - if let activeMedia = mediaEnv.activeMedia { - ShareDetailsView(mediaID: activeMedia.id, shares: mediaDetails?.shares ?? [], refreshMediaDetails: { fetchMediaDetails() }) - } - } - .listStyle(InsetGroupedListStyle()) - .redacted(reason: mediaDetails == nil ? .placeholder : []) - Spacer() - Text(mediaDetails?.title ?? "").font(.footnote) - } - .navigationBarTitle("Details", displayMode: .inline) - } - - var body: some View { - NavigationView { - ZStack { - ZStack { - Rectangle() - .fill(Color.black) - .opacity(isShowingNavbarMenu ? 0.0 : 1.0) - MediaGalleryView(mediaDetails: mediaDetails, isLoading: $isLoading, isVideo: $isVideo) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .ignoresSafeArea() - .onTapGesture { - // Disable the onTap gesture if it's a video so they can tap to play the video - if !isVideo { - withAnimation(.easeIn(duration: 0.35)) { - isShowingNavbarMenu.toggle() - } - } - else { - isShowingNavbarMenu = true - } - } - VStack(spacing: 0) { - // Always show the nav bar if it's a video - // Do not want to conflict "hide navbar" tap with a "play video" tap - if isShowingNavbarMenu || isVideo { - ZStack { - HStack { - Spacer() - if isLoading { - ProgressView() - } - Spacer() - } - HStack { - Spacer() - Group { - // Info button - NavigationLink(destination: mediaInformationView) { - Image(systemName: "info.square") - .imageScale(.large) // Icon size - .padding(.trailing, 5) - } - - // Close button - Button(action: { - showMedia = false - isLoading = false - }) { - Image(systemName: "xmark") - .imageScale(.large) // Icon size - .padding(.trailing, 5) - } - .padding() - } - } - } -// .frame(height: 44) - .background(BlurBackgroundVisualEffectView(effect: UIBlurEffect(style: .systemMaterial)) - .ignoresSafeArea()) - Spacer() // Pushes the bar to the top of the screen - } - } - } - .navigationBarTitleDisplayMode(.inline) - } - .onAppear { - fetchMediaDetails() - } - .onChange(of: mediaEnv.activeMediaIndex, perform: { value in - fetchMediaDetails() - }) - .statusBarHidden(!isShowingNavbarMenu) - } - -} - -struct MediaDetailsView_Previews: PreviewProvider { - -// static let sampleMedia = MediaDetailsQuery.Data.Medium( -// id: "123", -// title: "Media title", -// thumbnail: nil, -// exif: MediaDetailsQuery.Data.Medium.Exif( -// camera: "Camera", -// maker: "Model 3000", -// lens: "300 mm", -// dateShot: Time(date: Date()), -// exposure: 0.01, -// aperture: 2.4, -// iso: 100, -// focalLength: 35, -// flash: 0, -// exposureProgram: 1), -// type: .photo, shares: [], downloads: [ -// MediaDetailsQuery.Data.Medium.Download(title: "Original", mediaUrl: MediaDetailsQuery.Data.Medium.Download.MediaUrl(url: "link", width: 1080, height: 720, fileSize: 20000)), -// MediaDetailsQuery.Data.Medium.Download(title: "High-res", mediaUrl: MediaDetailsQuery.Data.Medium.Download.MediaUrl(url: "link", width: 1080, height: 720, fileSize: 20000)), -// MediaDetailsQuery.Data.Medium.Download(title: "Thumbnail", mediaUrl: MediaDetailsQuery.Data.Medium.Download.MediaUrl(url: "link", width: 1080, height: 720, fileSize: 20000)) -// ]) -// -// static let mediaEnvironment = MediaEnvironment( -// media: [MediaEnvironment.Media(id: "123", thumbnail: nil, favorite: false)], -// activeMediaIndex: 0 -// ) - - static var previews: some View { - FullScreenMediaGalleryView(mediaEnv: EnvironmentObject(), showMedia: .constant(true), mediaDetails: nil) -// .environmentObject(mediaEnvironment) - } -} - -// Blurred Background -struct BlurBackgroundVisualEffectView: UIViewRepresentable { - var effect: UIVisualEffect? - - func makeUIView(context: UIViewRepresentableContext) -> UIVisualEffectView { - return UIVisualEffectView() - } - - func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext) { - uiView.effect = effect - } -} From 7e68afeeaef56f1bd19d17dde8a147d3213ee33d Mon Sep 17 00:00:00 2001 From: Dhrumil Shah Date: Wed, 1 Mar 2023 11:58:00 -0500 Subject: [PATCH 12/20] Revert "WIP: Made F.S. media view name more descriptive" This reverts commit 96c5ef547af75a8dc9c8110a63b3dc9bb4746013. --- .../Views/MediaDetails/MediaDetailsView.swift | 112 ------------------ 1 file changed, 112 deletions(-) delete mode 100644 Photoview/Views/MediaDetails/MediaDetailsView.swift diff --git a/Photoview/Views/MediaDetails/MediaDetailsView.swift b/Photoview/Views/MediaDetails/MediaDetailsView.swift deleted file mode 100644 index 15ac00e..0000000 --- a/Photoview/Views/MediaDetails/MediaDetailsView.swift +++ /dev/null @@ -1,112 +0,0 @@ -// -// MediaDetailsView.swift -// Photoview -// -// Created by Viktor Strate Kløvedal on 25/07/2021. -// - -import SwiftUI -import Apollo - -struct MediaDetailsView: View { - - @EnvironmentObject var mediaEnv: MediaEnvironment - @EnvironmentObject var showWelcome: ShowWelcomeScreen - - @State var mediaDetails: MediaDetailsQuery.Data.Medium? = nil - - func fetchMediaDetails() { - guard let activeMedia = mediaEnv.activeMedia else { return } - - Network.shared.apollo?.fetch(query: MediaDetailsQuery(mediaID: activeMedia.id), cachePolicy: .returnCacheDataAndFetch) { data in - switch data { - case .success(let data): - DispatchQueue.main.async { - self.mediaDetails = data.data?.media - if let thumbnail = self.mediaDetails?.fragments.mediaItem.thumbnail { - mediaEnv.media?[mediaEnv.activeMediaIndex].thumbnail = thumbnail - } - } - case .failure(let error): - Network.shared.handleGraphqlError(error: NetworkError(message: "Failed to fetch media details", error: error), showWelcomeScreen: showWelcome) - } - } - } - - var header: some View { - VStack { - ThumbnailDetailsView(mediaDetails: mediaDetails, fullscreenMode: false) - - Text(mediaDetails?.title ?? "Loading media...") - .font(.headline) - .padding(.horizontal) - - if let exif = mediaDetails?.exif { - ExifDetailsView(exif: exif) - .padding(.top) - } - } - .foregroundColor(.primary) - .textCase(.none) - .padding(0) - } - - var body: some View { - List { - Section(header: header) { - EmptyView() - } - DownloadDetailsView(downloads: (mediaDetails?.downloads ?? []).sorted { $0.mediaUrl.fileSize > $1.mediaUrl.fileSize }) - if let activeMedia = mediaEnv.activeMedia { - ShareDetailsView(mediaID: activeMedia.id, shares: mediaDetails?.shares ?? [], refreshMediaDetails: { fetchMediaDetails() }) - } - } - .listStyle(InsetGroupedListStyle()) - .redacted(reason: mediaDetails == nil ? .placeholder : []) - .onAppear { - fetchMediaDetails() - } - .onChange(of: mediaEnv.activeMediaIndex, perform: { value in - fetchMediaDetails() - }) - .fullScreenCover(isPresented: $mediaEnv.fullScreen, content: { - FullScreenGalleryView(mediaDetails: mediaDetails) - // can crash without this - .environmentObject(mediaEnv) - }) - } -} - -struct MediaDetailsView_Previews: PreviewProvider { - -// static let sampleMedia = MediaDetailsQuery.Data.Medium( -// id: "123", -// title: "Media title", -// thumbnail: nil, -// exif: MediaDetailsQuery.Data.Medium.Exif( -// camera: "Camera", -// maker: "Model 3000", -// lens: "300 mm", -// dateShot: Time(date: Date()), -// exposure: 0.01, -// aperture: 2.4, -// iso: 100, -// focalLength: 35, -// flash: 0, -// exposureProgram: 1), -// type: .photo, shares: [], downloads: [ -// MediaDetailsQuery.Data.Medium.Download(title: "Original", mediaUrl: MediaDetailsQuery.Data.Medium.Download.MediaUrl(url: "link", width: 1080, height: 720, fileSize: 20000)), -// MediaDetailsQuery.Data.Medium.Download(title: "High-res", mediaUrl: MediaDetailsQuery.Data.Medium.Download.MediaUrl(url: "link", width: 1080, height: 720, fileSize: 20000)), -// MediaDetailsQuery.Data.Medium.Download(title: "Thumbnail", mediaUrl: MediaDetailsQuery.Data.Medium.Download.MediaUrl(url: "link", width: 1080, height: 720, fileSize: 20000)) -// ]) -// -// static let mediaEnvironment = MediaEnvironment( -// media: [MediaEnvironment.Media(id: "123", thumbnail: nil, favorite: false)], -// activeMediaIndex: 0 -// ) - - static var previews: some View { - MediaDetailsView(mediaEnv: EnvironmentObject(), mediaDetails: nil) -// .environmentObject(mediaEnvironment) - } -} From f8f3069f03f508028f6c7e64c5a4aeab9cac80b9 Mon Sep 17 00:00:00 2001 From: Dhrumil Shah Date: Wed, 1 Mar 2023 12:05:22 -0500 Subject: [PATCH 13/20] WIP: Accidently removed in the revert --- .../FullScreenMediaGalleryView.swift | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 Photoview/Views/MediaDetails/FullScreenMediaGalleryView.swift diff --git a/Photoview/Views/MediaDetails/FullScreenMediaGalleryView.swift b/Photoview/Views/MediaDetails/FullScreenMediaGalleryView.swift new file mode 100644 index 0000000..40a03ef --- /dev/null +++ b/Photoview/Views/MediaDetails/FullScreenMediaGalleryView.swift @@ -0,0 +1,183 @@ +// +// FullScreenMediaGalleryView.swift +// Photoview +// +// Created by Viktor Strate Kløvedal on 25/07/2021. +// + +import SwiftUI +import Apollo + +struct FullScreenMediaGalleryView: View { + + @EnvironmentObject var mediaEnv: MediaEnvironment + @EnvironmentObject var showWelcome: ShowWelcomeScreen + @Binding var showMedia: Bool + + @State var mediaDetails: MediaDetailsQuery.Data.Medium? = nil + + @State private var isShowingNavbarMenu = true + @State private var isZoomed = false + @State var isLoading = false + @State var isVideo = false + + func fetchMediaDetails() { + guard let activeMedia = mediaEnv.activeMedia else { return } + Network.shared.apollo?.fetch(query: MediaDetailsQuery(mediaID: activeMedia.id), cachePolicy: .returnCacheDataAndFetch) { data in + switch data { + case .success(let data): + DispatchQueue.main.async { + self.mediaDetails = data.data?.media + if let thumbnail = self.mediaDetails?.fragments.mediaItem.thumbnail { + mediaEnv.media?[mediaEnv.activeMediaIndex].thumbnail = thumbnail + } + } + case .failure(let error): + Network.shared.handleGraphqlError(error: NetworkError(message: "Failed to fetch media details", error: error), showWelcomeScreen: showWelcome) + } + } + } + + var mediaInformationView: some View { + VStack { + List { + if let exif = mediaDetails?.exif { + ExifDetailsView(exif: exif) + .listRowBackground(Color.clear) + } + DownloadDetailsView(downloads: (mediaDetails?.downloads ?? []).sorted { $0.mediaUrl.fileSize > $1.mediaUrl.fileSize }) + if let activeMedia = mediaEnv.activeMedia { + ShareDetailsView(mediaID: activeMedia.id, shares: mediaDetails?.shares ?? [], refreshMediaDetails: { fetchMediaDetails() }) + } + } + .listStyle(InsetGroupedListStyle()) + .redacted(reason: mediaDetails == nil ? .placeholder : []) + Spacer() + Text(mediaDetails?.title ?? "").font(.footnote) + } + .navigationBarTitle("Details", displayMode: .inline) + } + + var body: some View { + NavigationView { + ZStack { + ZStack { + Rectangle() + .fill(Color.black) + .opacity(isShowingNavbarMenu ? 0.0 : 1.0) + MediaGalleryView(mediaDetails: mediaDetails, isLoading: $isLoading, isVideo: $isVideo) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea() + .onTapGesture { + // Disable the onTap gesture if it's a video so they can tap to play the video + if !isVideo { + withAnimation(.easeIn(duration: 0.35)) { + isShowingNavbarMenu.toggle() + } + } + else { + isShowingNavbarMenu = true + } + } + VStack(spacing: 0) { + // Always show the nav bar if it's a video + // Do not want to conflict "hide navbar" tap with a "play video" tap + if isShowingNavbarMenu || isVideo { + ZStack { + HStack { + Spacer() + if isLoading { + ProgressView() + } + Spacer() + } + HStack { + Spacer() + Group { + // Info button + NavigationLink(destination: mediaInformationView) { + Image(systemName: "info.square") + .imageScale(.large) // Icon size + .padding(.trailing, 5) + } + + // Close button + Button(action: { + showMedia = false + isLoading = false + }) { + Image(systemName: "xmark") + .imageScale(.large) // Icon size + .padding(.trailing, 5) + } + .padding() + } + } + } +// .frame(height: 44) + .background(BlurBackgroundVisualEffectView(effect: UIBlurEffect(style: .systemMaterial)) + .ignoresSafeArea()) + Spacer() // Pushes the bar to the top of the screen + } + } + } + .navigationBarTitleDisplayMode(.inline) + } + .onAppear { + fetchMediaDetails() + } + .onChange(of: mediaEnv.activeMediaIndex, perform: { value in + fetchMediaDetails() + }) + .statusBarHidden(!isShowingNavbarMenu) + } + +} + +struct MediaDetailsView_Previews: PreviewProvider { + +// static let sampleMedia = MediaDetailsQuery.Data.Medium( +// id: "123", +// title: "Media title", +// thumbnail: nil, +// exif: MediaDetailsQuery.Data.Medium.Exif( +// camera: "Camera", +// maker: "Model 3000", +// lens: "300 mm", +// dateShot: Time(date: Date()), +// exposure: 0.01, +// aperture: 2.4, +// iso: 100, +// focalLength: 35, +// flash: 0, +// exposureProgram: 1), +// type: .photo, shares: [], downloads: [ +// MediaDetailsQuery.Data.Medium.Download(title: "Original", mediaUrl: MediaDetailsQuery.Data.Medium.Download.MediaUrl(url: "link", width: 1080, height: 720, fileSize: 20000)), +// MediaDetailsQuery.Data.Medium.Download(title: "High-res", mediaUrl: MediaDetailsQuery.Data.Medium.Download.MediaUrl(url: "link", width: 1080, height: 720, fileSize: 20000)), +// MediaDetailsQuery.Data.Medium.Download(title: "Thumbnail", mediaUrl: MediaDetailsQuery.Data.Medium.Download.MediaUrl(url: "link", width: 1080, height: 720, fileSize: 20000)) +// ]) +// +// static let mediaEnvironment = MediaEnvironment( +// media: [MediaEnvironment.Media(id: "123", thumbnail: nil, favorite: false)], +// activeMediaIndex: 0 +// ) + + static var previews: some View { + FullScreenMediaGalleryView(mediaEnv: EnvironmentObject(), showMedia: .constant(true), mediaDetails: nil) +// .environmentObject(mediaEnvironment) + } +} + +// Blurred Background +struct BlurBackgroundVisualEffectView: UIViewRepresentable { + var effect: UIVisualEffect? + + func makeUIView(context: UIViewRepresentableContext) -> UIVisualEffectView { + return UIVisualEffectView() + } + + func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext) { + uiView.effect = effect + } +} From ba9a79e41e17c39b1225adba277351ebeab5f56c Mon Sep 17 00:00:00 2001 From: Dhrumil Shah Date: Wed, 1 Mar 2023 12:05:36 -0500 Subject: [PATCH 14/20] WIP: Add background audio capability for PIP (future) --- Photoview/Info.plist | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Photoview/Info.plist b/Photoview/Info.plist index 97b569e..d837eb8 100644 --- a/Photoview/Info.plist +++ b/Photoview/Info.plist @@ -36,6 +36,10 @@ UIApplicationSupportsIndirectInputEvents + UIBackgroundModes + + audio + UILaunchScreen UIRequiredDeviceCapabilities From fa5806004246a30e69bc70b462d9728cabcccf9f Mon Sep 17 00:00:00 2001 From: Dhrumil Shah Date: Wed, 1 Mar 2023 12:06:47 -0500 Subject: [PATCH 15/20] WIP: Push xcodeproj updates --- Photoview.xcodeproj/project.pbxproj | 40 ++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/Photoview.xcodeproj/project.pbxproj b/Photoview.xcodeproj/project.pbxproj index dd3920c..ecaa2af 100644 --- a/Photoview.xcodeproj/project.pbxproj +++ b/Photoview.xcodeproj/project.pbxproj @@ -17,7 +17,6 @@ 343EE59026A886A2002FC34B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 343EE58F26A886A2002FC34B /* Preview Assets.xcassets */; }; 343EE59B26A886A2002FC34B /* PhotoviewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 343EE59A26A886A2002FC34B /* PhotoviewTests.swift */; }; 343EE5A626A886A2002FC34B /* PhotoviewUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 343EE5A526A886A2002FC34B /* PhotoviewUITests.swift */; }; - 3470512D26AEB2DD007ECB91 /* FullScreenGalleryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3470512C26AEB2DD007ECB91 /* FullScreenGalleryView.swift */; }; 3470512F26AEBEAD007ECB91 /* View+if.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3470512E26AEBEAD007ECB91 /* View+if.swift */; }; 3472012D26A88EAE00293C9D /* schema.json in Resources */ = {isa = PBXBuildFile; fileRef = 3472012C26A88EAE00293C9D /* schema.json */; }; 348268C526A88FE9006A7D64 /* InitialSetupQuery.graphql in Resources */ = {isa = PBXBuildFile; fileRef = 348268C426A88FE9006A7D64 /* InitialSetupQuery.graphql */; }; @@ -40,13 +39,13 @@ 348268F526AC4A0A006A7D64 /* AlbumThumbnailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348268F426AC4A0A006A7D64 /* AlbumThumbnailView.swift */; }; 348268F726AC4A7A006A7D64 /* ProtectedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348268F626AC4A7A006A7D64 /* ProtectedImageView.swift */; }; 348268F926AC73BA006A7D64 /* MediaThumbnailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348268F826AC73BA006A7D64 /* MediaThumbnailView.swift */; }; - 348268FE26AD709F006A7D64 /* MediaDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348268FD26AD709F006A7D64 /* MediaDetailsView.swift */; }; + 348268FE26AD709F006A7D64 /* FullScreenMediaGalleryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348268FD26AD709F006A7D64 /* FullScreenMediaGalleryView.swift */; }; 3482690126AD72BE006A7D64 /* MediaDetails.graphql in Resources */ = {isa = PBXBuildFile; fileRef = 3482690026AD72BE006A7D64 /* MediaDetails.graphql */; }; 3482690426AD9129006A7D64 /* ExifDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3482690326AD9129006A7D64 /* ExifDetailsView.swift */; }; 3482690626ADABF6006A7D64 /* DownloadDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3482690526ADABF6006A7D64 /* DownloadDetailsView.swift */; }; 3482690826ADAD6B006A7D64 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3482690726ADAD6B006A7D64 /* ShareSheet.swift */; }; 3482690A26ADB56D006A7D64 /* ShareDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3482690926ADB56D006A7D64 /* ShareDetailsView.swift */; }; - 3482690C26ADBFF5006A7D64 /* ThumbnailDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3482690B26ADBFF5006A7D64 /* ThumbnailDetailsView.swift */; }; + 3482690C26ADBFF5006A7D64 /* MediaGalleryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3482690B26ADBFF5006A7D64 /* MediaGalleryView.swift */; }; 34A72B7126B17D7D003127B1 /* ClusterDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A72B7026B17D7D003127B1 /* ClusterDetailsView.swift */; }; 34A72B7326B17F69003127B1 /* MediaGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A72B7226B17F69003127B1 /* MediaGrid.swift */; }; 34A72B7526B17FC7003127B1 /* AlbumGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A72B7426B17FC7003127B1 /* AlbumGrid.swift */; }; @@ -65,6 +64,9 @@ 34F9C43026CECE7000D2D3A5 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F9C42F26CECE7000D2D3A5 /* TimelineView.swift */; }; 34FF398027BFEEE300321C10 /* SearchResultsOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34FF397F27BFEEE300321C10 /* SearchResultsOverlay.swift */; }; 34FF398227BFF26200321C10 /* MediaSearchQuery.graphql in Resources */ = {isa = PBXBuildFile; fileRef = 34FF398127BFF26200321C10 /* MediaSearchQuery.graphql */; }; + BF36D0B929AB4EB900BB2460 /* SwiftUIZoomableImageViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF36D0B829AB4EB900BB2460 /* SwiftUIZoomableImageViewer.swift */; }; + BF36D0BC29AFAA1500BB2460 /* FullscreenAVPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF36D0BB29AFAA1500BB2460 /* FullscreenAVPlayerView.swift */; }; + BFF036C229A8984100F3831D /* ProtectedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF036C129A8984100F3831D /* ProtectedCache.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -101,7 +103,6 @@ 343EE5A126A886A2002FC34B /* PhotoviewUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PhotoviewUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 343EE5A526A886A2002FC34B /* PhotoviewUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoviewUITests.swift; sourceTree = ""; }; 343EE5A726A886A2002FC34B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 3470512C26AEB2DD007ECB91 /* FullScreenGalleryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenGalleryView.swift; sourceTree = ""; }; 3470512E26AEBEAD007ECB91 /* View+if.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+if.swift"; sourceTree = ""; }; 3472012C26A88EAE00293C9D /* schema.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = schema.json; sourceTree = ""; }; 348268C426A88FE9006A7D64 /* InitialSetupQuery.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = InitialSetupQuery.graphql; sourceTree = ""; }; @@ -123,13 +124,13 @@ 348268F426AC4A0A006A7D64 /* AlbumThumbnailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumThumbnailView.swift; sourceTree = ""; }; 348268F626AC4A7A006A7D64 /* ProtectedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtectedImageView.swift; sourceTree = ""; }; 348268F826AC73BA006A7D64 /* MediaThumbnailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaThumbnailView.swift; sourceTree = ""; }; - 348268FD26AD709F006A7D64 /* MediaDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDetailsView.swift; sourceTree = ""; }; + 348268FD26AD709F006A7D64 /* FullScreenMediaGalleryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenMediaGalleryView.swift; sourceTree = ""; }; 3482690026AD72BE006A7D64 /* MediaDetails.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = MediaDetails.graphql; sourceTree = ""; }; 3482690326AD9129006A7D64 /* ExifDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExifDetailsView.swift; sourceTree = ""; }; 3482690526ADABF6006A7D64 /* DownloadDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadDetailsView.swift; sourceTree = ""; }; 3482690726ADAD6B006A7D64 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = ""; }; 3482690926ADB56D006A7D64 /* ShareDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareDetailsView.swift; sourceTree = ""; }; - 3482690B26ADBFF5006A7D64 /* ThumbnailDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailDetailsView.swift; sourceTree = ""; }; + 3482690B26ADBFF5006A7D64 /* MediaGalleryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryView.swift; sourceTree = ""; }; 34A72B7026B17D7D003127B1 /* ClusterDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClusterDetailsView.swift; sourceTree = ""; }; 34A72B7226B17F69003127B1 /* MediaGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGrid.swift; sourceTree = ""; }; 34A72B7426B17FC7003127B1 /* AlbumGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumGrid.swift; sourceTree = ""; }; @@ -147,6 +148,9 @@ 34F9C42F26CECE7000D2D3A5 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = ""; }; 34FF397F27BFEEE300321C10 /* SearchResultsOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsOverlay.swift; sourceTree = ""; }; 34FF398127BFF26200321C10 /* MediaSearchQuery.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = MediaSearchQuery.graphql; sourceTree = ""; }; + BF36D0B829AB4EB900BB2460 /* SwiftUIZoomableImageViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIZoomableImageViewer.swift; sourceTree = ""; }; + BF36D0BB29AFAA1500BB2460 /* FullscreenAVPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenAVPlayerView.swift; sourceTree = ""; }; + BFF036C129A8984100F3831D /* ProtectedCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtectedCache.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -216,6 +220,7 @@ 348268DC26A8CEC4006A7D64 /* Network+Authentication.swift */, 3482690726ADAD6B006A7D64 /* ShareSheet.swift */, 34FF398127BFF26200321C10 /* MediaSearchQuery.graphql */, + BFF036C129A8984100F3831D /* ProtectedCache.swift */, ); path = Photoview; sourceTree = ""; @@ -333,6 +338,7 @@ 348268FF26AD729A006A7D64 /* Views */ = { isa = PBXGroup; children = ( + BF36D0BA29ADCB7300BB2460 /* Supporting Views */, 3482690226AD910C006A7D64 /* MediaDetails */, 348268F626AC4A7A006A7D64 /* ProtectedImageView.swift */, 34F3EE7227AD854A0082624D /* ProtectedVideoView.swift */, @@ -351,13 +357,12 @@ 3482690226AD910C006A7D64 /* MediaDetails */ = { isa = PBXGroup; children = ( - 348268FD26AD709F006A7D64 /* MediaDetailsView.swift */, + 348268FD26AD709F006A7D64 /* FullScreenMediaGalleryView.swift */, 3482690026AD72BE006A7D64 /* MediaDetails.graphql */, 3482690326AD9129006A7D64 /* ExifDetailsView.swift */, 3482690526ADABF6006A7D64 /* DownloadDetailsView.swift */, 3482690926ADB56D006A7D64 /* ShareDetailsView.swift */, - 3482690B26ADBFF5006A7D64 /* ThumbnailDetailsView.swift */, - 3470512C26AEB2DD007ECB91 /* FullScreenGalleryView.swift */, + 3482690B26ADBFF5006A7D64 /* MediaGalleryView.swift */, ); path = MediaDetails; sourceTree = ""; @@ -380,6 +385,15 @@ path = Modifiers; sourceTree = ""; }; + BF36D0BA29ADCB7300BB2460 /* Supporting Views */ = { + isa = PBXGroup; + children = ( + BF36D0B829AB4EB900BB2460 /* SwiftUIZoomableImageViewer.swift */, + BF36D0BB29AFAA1500BB2460 /* FullscreenAVPlayerView.swift */, + ); + path = "Supporting Views"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -559,11 +573,13 @@ 34F3EE7127AD56110082624D /* BlurHash+UIImage.swift in Sources */, 34F3EE7327AD854A0082624D /* ProtectedVideoView.swift in Sources */, 348268D226A8A2F4006A7D64 /* PrimaryButtonStyle.swift in Sources */, + BF36D0BC29AFAA1500BB2460 /* FullscreenAVPlayerView.swift in Sources */, 348268EB26AC3E1F006A7D64 /* PlacesScreen.swift in Sources */, + BFF036C229A8984100F3831D /* ProtectedCache.swift in Sources */, 34F3EE7527AD8ECE0082624D /* ProtectedMediaView.swift in Sources */, - 3470512D26AEB2DD007ECB91 /* FullScreenGalleryView.swift in Sources */, 348268F526AC4A0A006A7D64 /* AlbumThumbnailView.swift in Sources */, 34FF398027BFEEE300321C10 /* SearchResultsOverlay.swift in Sources */, + BF36D0B929AB4EB900BB2460 /* SwiftUIZoomableImageViewer.swift in Sources */, 3482690626ADABF6006A7D64 /* DownloadDetailsView.swift in Sources */, 34A72B8226B2B6F7003127B1 /* FaceGrid.swift in Sources */, 342E892126AEE94C0094378B /* API+CustomScalars.swift in Sources */, @@ -576,7 +592,7 @@ 3482690426AD9129006A7D64 /* ExifDetailsView.swift in Sources */, 348268E026AC3D86006A7D64 /* AppView.swift in Sources */, 348268D826A8B243006A7D64 /* Network.swift in Sources */, - 348268FE26AD709F006A7D64 /* MediaDetailsView.swift in Sources */, + 348268FE26AD709F006A7D64 /* FullScreenMediaGalleryView.swift in Sources */, 3482690826ADAD6B006A7D64 /* ShareSheet.swift in Sources */, 348268F926AC73BA006A7D64 /* MediaThumbnailView.swift in Sources */, 348268DD26A8CEC4006A7D64 /* Network+Authentication.swift in Sources */, @@ -586,7 +602,7 @@ 34CB716C2724545D0089425D /* Apollo+Async.swift in Sources */, 348268F126AC4234006A7D64 /* AlbumView.swift in Sources */, 34A72B8026B2B4D3003127B1 /* FaceThumbnailView.swift in Sources */, - 3482690C26ADBFF5006A7D64 /* ThumbnailDetailsView.swift in Sources */, + 3482690C26ADBFF5006A7D64 /* MediaGalleryView.swift in Sources */, 343EE58926A8869F002FC34B /* PhotoviewApp.swift in Sources */, 3470512F26AEBEAD007ECB91 /* View+if.swift in Sources */, 3482690A26ADB56D006A7D64 /* ShareDetailsView.swift in Sources */, From 31a81285968703ee87d459e46bcf137680e9329f Mon Sep 17 00:00:00 2001 From: Dhrumil Shah Date: Wed, 1 Mar 2023 12:08:47 -0500 Subject: [PATCH 16/20] WIP: Updated for the new fullscreen player --- Photoview/Views/ProtectedVideoView.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Photoview/Views/ProtectedVideoView.swift b/Photoview/Views/ProtectedVideoView.swift index de64b67..fcd301e 100644 --- a/Photoview/Views/ProtectedVideoView.swift +++ b/Photoview/Views/ProtectedVideoView.swift @@ -12,6 +12,9 @@ import AVKit struct ProtectedVideoView: View { let url: String? + @Binding var isLoading: Bool + @State var willBeginFullScreenPresentation: Bool = false + var videoAsset: AVURLAsset? { guard let url = self.url else { return nil } @@ -29,7 +32,7 @@ struct ProtectedVideoView: View { } else { player = nil } - - return VideoPlayer(player: player) + return FullscreenAVPlayerView(player: player!) + .shadow(radius: 0) } } From eb02a4bbeebf3265b9a6cdba6a91fa52ef6406da Mon Sep 17 00:00:00 2001 From: Dhrumil Shah Date: Wed, 1 Mar 2023 12:09:11 -0500 Subject: [PATCH 17/20] WIP: dependency, misc updates --- .gitignore | 4 +- .../xcshareddata/swiftpm/Package.resolved | 150 +++++++++--------- 2 files changed, 77 insertions(+), 77 deletions(-) diff --git a/.gitignore b/.gitignore index 46612e4..d5c8af5 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,6 @@ DerivedData/ !default.perspectivev3 ## Gcc Patch -/*.gcno \ No newline at end of file +/*.gcno + +.DS_Store \ No newline at end of file diff --git a/Photoview.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Photoview.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e723890..b4f2e5c 100644 --- a/Photoview.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Photoview.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,79 +1,77 @@ { - "object": { - "pins": [ - { - "package": "Apollo", - "repositoryURL": "https://github.com/apollographql/apollo-ios.git", - "state": { - "branch": null, - "revision": "84e28febb0a6cc3b4b08968c0d7043afd288004e", - "version": "0.45.0" - } - }, - { - "package": "InflectorKit", - "repositoryURL": "https://github.com/apollographql/InflectorKit", - "state": { - "branch": null, - "revision": "b1d0099abe36facd198113633f502889842906af", - "version": "0.0.2" - } - }, - { - "package": "KeychainSwift", - "repositoryURL": "https://github.com/evgenyneu/keychain-swift.git", - "state": { - "branch": null, - "revision": "96fb84f45a96630e7583903bd7e08cf095c7a7ef", - "version": "19.0.0" - } - }, - { - "package": "PathKit", - "repositoryURL": "https://github.com/kylef/PathKit.git", - "state": { - "branch": null, - "revision": "73f8e9dca9b7a3078cb79128217dc8f2e585a511", - "version": "1.0.0" - } - }, - { - "package": "Spectre", - "repositoryURL": "https://github.com/kylef/Spectre.git", - "state": { - "branch": null, - "revision": "f79d4ecbf8bc4e1579fbd86c3e1d652fb6876c53", - "version": "0.9.2" - } - }, - { - "package": "SQLite.swift", - "repositoryURL": "https://github.com/stephencelis/SQLite.swift.git", - "state": { - "branch": null, - "revision": "0a9893ec030501a3956bee572d6b4fdd3ae158a1", - "version": "0.12.2" - } - }, - { - "package": "Starscream", - "repositoryURL": "https://github.com/apollographql/Starscream", - "state": { - "branch": null, - "revision": "8cf77babe5901693396436f4f418a6db0f328b78", - "version": "3.1.2" - } - }, - { - "package": "Stencil", - "repositoryURL": "https://github.com/stencilproject/Stencil.git", - "state": { - "branch": null, - "revision": "973e190edf5d09274e4a6bc2e636c86899ed84c3", - "version": "0.14.1" - } + "pins" : [ + { + "identity" : "apollo-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apollographql/apollo-ios.git", + "state" : { + "revision" : "84e28febb0a6cc3b4b08968c0d7043afd288004e", + "version" : "0.45.0" } - ] - }, - "version": 1 + }, + { + "identity" : "inflectorkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apollographql/InflectorKit", + "state" : { + "revision" : "b1d0099abe36facd198113633f502889842906af", + "version" : "0.0.2" + } + }, + { + "identity" : "keychain-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/evgenyneu/keychain-swift.git", + "state" : { + "revision" : "96fb84f45a96630e7583903bd7e08cf095c7a7ef", + "version" : "19.0.0" + } + }, + { + "identity" : "pathkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kylef/PathKit.git", + "state" : { + "revision" : "73f8e9dca9b7a3078cb79128217dc8f2e585a511", + "version" : "1.0.0" + } + }, + { + "identity" : "spectre", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kylef/Spectre.git", + "state" : { + "revision" : "f79d4ecbf8bc4e1579fbd86c3e1d652fb6876c53", + "version" : "0.9.2" + } + }, + { + "identity" : "sqlite.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stephencelis/SQLite.swift.git", + "state" : { + "revision" : "0a9893ec030501a3956bee572d6b4fdd3ae158a1", + "version" : "0.12.2" + } + }, + { + "identity" : "starscream", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apollographql/Starscream", + "state" : { + "revision" : "8cf77babe5901693396436f4f418a6db0f328b78", + "version" : "3.1.2" + } + }, + { + "identity" : "stencil", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stencilproject/Stencil.git", + "state" : { + "revision" : "973e190edf5d09274e4a6bc2e636c86899ed84c3", + "version" : "0.14.1" + } + } + ], + "version" : 2 } From bafed0c67f32f5ec6ffeda20aaa489902b1d65b4 Mon Sep 17 00:00:00 2001 From: Dhrumil Shah Date: Wed, 1 Mar 2023 13:03:14 -0500 Subject: [PATCH 18/20] WIP: Allow video audio playback while in silent mode --- .../Views/Supporting Views/FullscreenAVPlayerView.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Photoview/Views/Supporting Views/FullscreenAVPlayerView.swift b/Photoview/Views/Supporting Views/FullscreenAVPlayerView.swift index 65589c8..a578a9f 100644 --- a/Photoview/Views/Supporting Views/FullscreenAVPlayerView.swift +++ b/Photoview/Views/Supporting Views/FullscreenAVPlayerView.swift @@ -32,6 +32,14 @@ struct FullscreenAVPlayerView: UIViewControllerRepresentable { playerController.view.addGestureRecognizer(swipeDown) + // Set audio session category to playback + // Allows for audio even on silent mode + do { + try AVAudioSession.sharedInstance().setCategory(.playback) + } catch { + print("Failed to set audio session category: \(error)") + } + playerController.viewWillLayoutSubviews() } From b4fec5062052f35c783458c4b73a047513f03354 Mon Sep 17 00:00:00 2001 From: Dhrumil Shah Date: Thu, 2 Mar 2023 00:34:27 -0500 Subject: [PATCH 19/20] WIP: Fixed bug with < iOS 16 nav bar not hidden --- Photoview.xcodeproj/project.pbxproj | 4 ++++ .../View+hideDefaultNavigationBar.swift | 20 +++++++++++++++++++ .../FullScreenMediaGalleryView.swift | 5 ++--- 3 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 Photoview/Extensions/View+hideDefaultNavigationBar.swift diff --git a/Photoview.xcodeproj/project.pbxproj b/Photoview.xcodeproj/project.pbxproj index ecaa2af..a9fa5a8 100644 --- a/Photoview.xcodeproj/project.pbxproj +++ b/Photoview.xcodeproj/project.pbxproj @@ -66,6 +66,7 @@ 34FF398227BFF26200321C10 /* MediaSearchQuery.graphql in Resources */ = {isa = PBXBuildFile; fileRef = 34FF398127BFF26200321C10 /* MediaSearchQuery.graphql */; }; BF36D0B929AB4EB900BB2460 /* SwiftUIZoomableImageViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF36D0B829AB4EB900BB2460 /* SwiftUIZoomableImageViewer.swift */; }; BF36D0BC29AFAA1500BB2460 /* FullscreenAVPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF36D0BB29AFAA1500BB2460 /* FullscreenAVPlayerView.swift */; }; + BF36D0BE29B06C8D00BB2460 /* View+hideDefaultNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF36D0BD29B06C8D00BB2460 /* View+hideDefaultNavigationBar.swift */; }; BFF036C229A8984100F3831D /* ProtectedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF036C129A8984100F3831D /* ProtectedCache.swift */; }; /* End PBXBuildFile section */ @@ -150,6 +151,7 @@ 34FF398127BFF26200321C10 /* MediaSearchQuery.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = MediaSearchQuery.graphql; sourceTree = ""; }; BF36D0B829AB4EB900BB2460 /* SwiftUIZoomableImageViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIZoomableImageViewer.swift; sourceTree = ""; }; BF36D0BB29AFAA1500BB2460 /* FullscreenAVPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenAVPlayerView.swift; sourceTree = ""; }; + BF36D0BD29B06C8D00BB2460 /* View+hideDefaultNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+hideDefaultNavigationBar.swift"; sourceTree = ""; }; BFF036C129A8984100F3831D /* ProtectedCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtectedCache.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -374,6 +376,7 @@ 34A72B7726B185EC003127B1 /* Apollo+GraphqlID.swift */, 34A72B7926B1921D003127B1 /* Mapkit+Equatable.swift */, 34F3EE7027AD56110082624D /* BlurHash+UIImage.swift */, + BF36D0BD29B06C8D00BB2460 /* View+hideDefaultNavigationBar.swift */, ); path = Extensions; sourceTree = ""; @@ -568,6 +571,7 @@ 34A72B7526B17FC7003127B1 /* AlbumGrid.swift in Sources */, 34A72B7A26B1921D003127B1 /* Mapkit+Equatable.swift in Sources */, 348268D026A89801006A7D64 /* CredentialsModel.swift in Sources */, + BF36D0BE29B06C8D00BB2460 /* View+hideDefaultNavigationBar.swift in Sources */, 34A72B8526B2C639003127B1 /* PersonView.swift in Sources */, 348268CB26A890F3006A7D64 /* WelcomeScreen.swift in Sources */, 34F3EE7127AD56110082624D /* BlurHash+UIImage.swift in Sources */, diff --git a/Photoview/Extensions/View+hideDefaultNavigationBar.swift b/Photoview/Extensions/View+hideDefaultNavigationBar.swift new file mode 100644 index 0000000..cf9dddb --- /dev/null +++ b/Photoview/Extensions/View+hideDefaultNavigationBar.swift @@ -0,0 +1,20 @@ +// +// View+hideDefaultNavigationBar.swift +// Photoview +// +// Created by Dhrumil Shah on 3/2/23. +// + +import SwiftUI + +extension View { + @ViewBuilder + func hideDefaultNavigationBar() -> some View { + if #available(iOS 16, *) { + self.toolbar(.hidden) + } + else { + self.navigationBarHidden(true) + } + } +} diff --git a/Photoview/Views/MediaDetails/FullScreenMediaGalleryView.swift b/Photoview/Views/MediaDetails/FullScreenMediaGalleryView.swift index 40a03ef..cbf7d7d 100644 --- a/Photoview/Views/MediaDetails/FullScreenMediaGalleryView.swift +++ b/Photoview/Views/MediaDetails/FullScreenMediaGalleryView.swift @@ -97,7 +97,7 @@ struct FullScreenMediaGalleryView: View { Group { // Info button NavigationLink(destination: mediaInformationView) { - Image(systemName: "info.square") + Image(systemName: "info.circle") .imageScale(.large) // Icon size .padding(.trailing, 5) } @@ -115,14 +115,13 @@ struct FullScreenMediaGalleryView: View { } } } -// .frame(height: 44) .background(BlurBackgroundVisualEffectView(effect: UIBlurEffect(style: .systemMaterial)) .ignoresSafeArea()) Spacer() // Pushes the bar to the top of the screen } } } - .navigationBarTitleDisplayMode(.inline) + .hideDefaultNavigationBar() } .onAppear { fetchMediaDetails() From 2b49484900f45472284639232d4b332119a8afaa Mon Sep 17 00:00:00 2001 From: Dhrumil Shah Date: Thu, 2 Mar 2023 00:38:32 -0500 Subject: [PATCH 20/20] WIP: Removed unsused code --- Photoview/Views/ProtectedVideoView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Photoview/Views/ProtectedVideoView.swift b/Photoview/Views/ProtectedVideoView.swift index fcd301e..4fc587a 100644 --- a/Photoview/Views/ProtectedVideoView.swift +++ b/Photoview/Views/ProtectedVideoView.swift @@ -13,7 +13,6 @@ struct ProtectedVideoView: View { let url: String? @Binding var isLoading: Bool - @State var willBeginFullScreenPresentation: Bool = false var videoAsset: AVURLAsset? { guard let url = self.url else { return nil }