From 50a419076ab3aec20778f11aa75246f7790ac3bf Mon Sep 17 00:00:00 2001 From: Suhail Saqan Date: Sat, 10 Feb 2024 14:56:24 -0800 Subject: [PATCH] camera: add ability to preview media taken with camera --- damus.xcodeproj/project.pbxproj | 8 + .../xcshareddata/swiftpm/Package.resolved | 57 ----- .../DamusNotificationService.xcscheme | 1 - damus/Views/Camera/CameraMediaView.swift | 89 +++++++ damus/Views/Camera/CameraView.swift | 221 ++++++++++++++++++ damus/Views/PostView.swift | 25 +- 6 files changed, 338 insertions(+), 63 deletions(-) delete mode 100644 damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 damus/Views/Camera/CameraMediaView.swift create mode 100644 damus/Views/Camera/CameraView.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 9aa17a87c..97e2f6a7c 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -434,6 +434,8 @@ B5C60C202B530D5100C5ECA7 /* MuteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */; }; B5C60C212B530D5600C5ECA7 /* MuteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */; }; B5C60C232B532A8700C5ECA7 /* DamusDuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C60C222B532A8700C5ECA7 /* DamusDuration.swift */; }; + BA15BB6B2B7833660045B913 /* CameraMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA15BB6A2B7833660045B913 /* CameraMediaView.swift */; }; + BA15BB6D2B78336D0045B913 /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA15BB6C2B78336D0045B913 /* CameraView.swift */; }; BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; }; BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; }; BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; }; @@ -1353,6 +1355,8 @@ B5B4D1422B37D47600844320 /* NdbExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NdbExtensions.swift; sourceTree = ""; usesTabs = 0; }; B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteItem.swift; sourceTree = ""; usesTabs = 0; }; B5C60C222B532A8700C5ECA7 /* DamusDuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusDuration.swift; sourceTree = ""; usesTabs = 0; }; + BA15BB6A2B7833660045B913 /* CameraMediaView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraMediaView.swift; sourceTree = ""; }; + BA15BB6C2B78336D0045B913 /* CameraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = ""; }; BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = ""; }; BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureProcessor.swift; sourceTree = ""; }; BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCaptureProcessor.swift; sourceTree = ""; }; @@ -2688,8 +2692,10 @@ BA3759952ABCCF360018D73B /* Camera */ = { isa = PBXGroup; children = ( + BA15BB6A2B7833660045B913 /* CameraMediaView.swift */, BA3759962ABCCF360018D73B /* CameraPreview.swift */, E02429942B7E97740088B16C /* CameraController.swift */, + BA15BB6C2B78336D0045B913 /* CameraView.swift */, ); path = Camera; sourceTree = ""; @@ -3049,6 +3055,7 @@ 4C4793042A993DC000489948 /* midl.c in Sources */, 0E8A4BB72AE4359200065E81 /* NostrFilter+Hashable.swift in Sources */, 4C4793012A993CDA00489948 /* mdb.c in Sources */, + BA15BB6D2B78336D0045B913 /* CameraView.swift in Sources */, 4CE9FBBA2A6B3C63007E485C /* nostrdb.c in Sources */, ADFE73552AD4793100EC7326 /* QRScanNSECView.swift in Sources */, 4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */, @@ -3134,6 +3141,7 @@ 647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */, F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */, 4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */, + BA15BB6B2B7833660045B913 /* CameraMediaView.swift in Sources */, 4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */, B51C1CEA2B55A60A00E312A9 /* AddMuteItemView.swift in Sources */, 4C5D5C992A6AF8F80024563C /* NdbNote.swift in Sources */, diff --git a/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 1990dd350..000000000 --- a/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,57 +0,0 @@ -{ - "pins" : [ - { - "identity" : "gsplayer", - "kind" : "remoteSourceControl", - "location" : "https://github.com/wxxsw/GSPlayer", - "state" : { - "revision" : "aa6dad7943d52f5207f7fcc2ad3e4274583443b8", - "version" : "0.2.26" - } - }, - { - "identity" : "kingfisher", - "kind" : "remoteSourceControl", - "location" : "https://github.com/onevcat/Kingfisher", - "state" : { - "revision" : "415b1d97fb38bda1e5a6b2dde63354720832110b", - "version" : "7.6.1" - } - }, - { - "identity" : "secp256k1.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/jb55/secp256k1.swift", - "state" : { - "revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9" - } - }, - { - "identity" : "swift-markdown-ui", - "kind" : "remoteSourceControl", - "location" : "https://github.com/damus-io/swift-markdown-ui", - "state" : { - "revision" : "76bb7971da7fbf429de1c84f1244adf657242fee" - } - }, - { - "identity" : "swift-snapshot-testing", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-snapshot-testing", - "state" : { - "revision" : "5b356adceabff6ca027f6574aac79e9fee145d26", - "version" : "1.14.1" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", - "state" : { - "revision" : "74203046135342e4a4a627476dd6caf8b28fe11b", - "version" : "509.0.0" - } - } - ], - "version" : 2 -} diff --git a/damus.xcodeproj/xcshareddata/xcschemes/DamusNotificationService.xcscheme b/damus.xcodeproj/xcshareddata/xcschemes/DamusNotificationService.xcscheme index 4c06fd6b8..d2f574995 100644 --- a/damus.xcodeproj/xcshareddata/xcschemes/DamusNotificationService.xcscheme +++ b/damus.xcodeproj/xcshareddata/xcschemes/DamusNotificationService.xcscheme @@ -77,7 +77,6 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES" - askForAppToLaunch = "Yes" launchAutomaticallySubstyle = "2"> diff --git a/damus/Views/Camera/CameraMediaView.swift b/damus/Views/Camera/CameraMediaView.swift new file mode 100644 index 000000000..bce63083a --- /dev/null +++ b/damus/Views/Camera/CameraMediaView.swift @@ -0,0 +1,89 @@ +// +// MediaViewer.swift +// damus +// +// Created by Suhail Saqan on 12/22/23. +// + +import SwiftUI +import Kingfisher + +// MARK: - Camera Media Viewer +struct CameraMediaView: View { + let video_controller: VideoController + let urls: [MediaUrl] + + @Environment(\.presentationMode) var presentationMode + + @State private var selectedIndex = 0 + @State var showMenu = true + + let settings: UserSettingsStore + + 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) + .onTapGesture { + selectedIndex = index + } + } + } + .padding() + .background(.regularMaterial) + .clipShape(Capsule()) + } + + var body: some View { + ZStack { + Color(.systemBackground) + .ignoresSafeArea() + + TabView(selection: $selectedIndex) { + ForEach(urls.indices, id: \.self) { index in + ZoomableScrollView { + ImageContainerView(video_controller: video_controller, url: urls[index], settings: settings) + .aspectRatio(contentMode: .fit) + .padding(.top, Theme.safeAreaInsets?.top) + .padding(.bottom, Theme.safeAreaInsets?.bottom) + } + .ignoresSafeArea() + .tag(index) + } + } + .ignoresSafeArea() + .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) + .gesture(TapGesture(count: 2).onEnded { + // Prevents menu from hiding on double tap + }) + .gesture(TapGesture(count: 1).onEnded { + showMenu.toggle() + }) + .overlay( + GeometryReader { geo in + VStack { + if showMenu { + NavDismissBarView() + Spacer() + + if (urls.count > 1) { + tabViewIndicator + } + } + } + .animation(.easeInOut, value: showMenu) + .padding(.bottom, geo.safeAreaInsets.bottom == 0 ? 12 : 0) + } + ) + } + } +} + +struct CameraMediaView_Previews: PreviewProvider { + static var previews: some View { + let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!) + CameraMediaView(video_controller: test_damus_state.video, urls: [url], settings: test_damus_state.settings) + } +} diff --git a/damus/Views/Camera/CameraView.swift b/damus/Views/Camera/CameraView.swift new file mode 100644 index 000000000..4c4d07d44 --- /dev/null +++ b/damus/Views/Camera/CameraView.swift @@ -0,0 +1,221 @@ +// +// CameraView.swift +// damus +// +// Created by Suhail Saqan on 8/5/23. +// + +import SwiftUI +import Combine +import AVFoundation + +struct CameraView: View { + let damus_state: DamusState + let action: (([MediaItem]) -> Void) + + @Environment(\.presentationMode) var presentationMode + + @StateObject var model: CameraModel + + @State var currentZoomFactor: CGFloat = 1.0 + + public init(damus_state: DamusState, action: @escaping (([MediaItem]) -> Void)) { + self.damus_state = damus_state + self.action = action + _model = StateObject(wrappedValue: CameraModel()) + } + + var captureButton: some View { + Button { + if model.isRecording { + withAnimation { + model.stopRecording() + } + } else { + withAnimation { + model.capturePhoto() + } + } + UIImpactFeedbackGenerator(style: .medium).impactOccurred() + } label: { + ZStack { + Circle() + .fill( model.isRecording ? .red : DamusColors.black) + .frame(width: model.isRecording ? 85 : 65, height: model.isRecording ? 85 : 65, alignment: .center) + + Circle() + .stroke( model.isRecording ? .red : DamusColors.white, lineWidth: 4) + .frame(width: model.isRecording ? 95 : 75, height: model.isRecording ? 95 : 75, alignment: .center) + } + .frame(alignment: .center) + } + .simultaneousGesture( + LongPressGesture(minimumDuration: 0.5).onEnded({ value in + if (!model.isCameraButtonDisabled) { + withAnimation { + model.startRecording() + model.captureMode = .video + } + } + }) + ) + .buttonStyle(.plain) + } + + var capturedPhotoThumbnail: some View { + ZStack { + if model.thumbnail != nil { + Image(uiImage: model.thumbnail.thumbnailImage!) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 60, height: 60) + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + } + if model.isPhotoProcessing { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: DamusColors.white)) + } + } + } + + var closeButton: some View { + Button { + presentationMode.wrappedValue.dismiss() + model.stop() + } label: { + HStack { + Image(systemName: "xmark") + .font(.system(size: 24)) + } + .frame(minWidth: 40, minHeight: 40) + } + .accentColor(DamusColors.white) + } + + var flipCameraButton: some View { + Button(action: { + model.flipCamera() + }, label: { + HStack { + Image(systemName: "camera.rotate.fill") + .font(.system(size: 20)) + } + .frame(minWidth: 40, minHeight: 40) + }) + .accentColor(DamusColors.white) + } + + var toggleFlashButton: some View { + Button(action: { + model.switchFlash() + }, label: { + HStack { + Image(systemName: model.isFlashOn ? "bolt.fill" : "bolt.slash.fill") + .font(.system(size: 20)) + } + .frame(minWidth: 40, minHeight: 40) + }) + .accentColor(model.isFlashOn ? .yellow : DamusColors.white) + } + + var body: some View { + NavigationView { + GeometryReader { reader in + ZStack { + DamusColors.black.edgesIgnoringSafeArea(.all) + + CameraPreview(session: model.session) + .padding(.bottom, 175) + .edgesIgnoringSafeArea(.all) + .gesture( + DragGesture().onChanged({ (val) in + if abs(val.translation.height) > abs(val.translation.width) { + let percentage: CGFloat = -(val.translation.height / reader.size.height) + let calc = currentZoomFactor + percentage + let zoomFactor: CGFloat = min(max(calc, 1), 5) + + currentZoomFactor = zoomFactor + model.zoom(with: zoomFactor) + } + }) + ) + .onAppear { + model.configure() + } + .alert(isPresented: $model.showAlertError, content: { + Alert(title: Text(model.alertError.title), message: Text(model.alertError.message), dismissButton: .default(Text(model.alertError.primaryButtonTitle), action: { + model.alertError.primaryAction?() + })) + }) + .overlay( + Group { + if model.willCapturePhoto { + Color.black + } + } + ) + + VStack { + if !model.isRecording { + HStack { + closeButton + + Spacer() + + HStack { + flipCameraButton + toggleFlashButton + } + } + .padding(.horizontal, 20) + } + + Spacer() + + HStack(alignment: .center) { + if !model.mediaItems.isEmpty { + NavigationLink(destination: CameraMediaView(video_controller: damus_state.video, urls: model.mediaItems.map { mediaItem in + switch mediaItem.type { + case .image: + return .image(mediaItem.url) + case .video: + return .video(mediaItem.url) + } + }, settings: damus_state.settings) + .navigationBarBackButtonHidden(true) + ) { + capturedPhotoThumbnail + } + .frame(width: 100, alignment: .leading) + } + + Spacer() + + captureButton + + Spacer() + + if !model.mediaItems.isEmpty { + Button(action: { + action(model.mediaItems) + presentationMode.wrappedValue.dismiss() + model.stop() + }) { + Text("Upload") + .frame(width: 100, height: 40, alignment: .center) + .foregroundColor(DamusColors.white) + .overlay { + RoundedRectangle(cornerRadius: 24) + .stroke(DamusColors.white, lineWidth: 2) + } + } + } + } + .frame(height: 100) + .padding([.horizontal, .vertical], 20) + } + } + } + } + } +} diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift index 89846b27f..7450dd5a5 100644 --- a/damus/Views/PostView.swift +++ b/damus/Views/PostView.swift @@ -58,6 +58,7 @@ struct PostView: View { @State var textHeight: CGFloat? = nil @State var preUploadedMedia: PreUploadedMedia? = nil + @State var mediaToUpload: [MediaUpload] = [] @StateObject var image_upload: ImageUploadModel = ImageUploadModel() @StateObject var tagModel: TagModel = TagModel() @@ -379,6 +380,15 @@ struct PostView: View { pks.append(pk) } } + + func addToMediaToUpload(mediaItem: MediaItem) { + switch mediaItem.type { + case .image: + mediaToUpload.append(.image(mediaItem.url)) + case .video: + mediaToUpload.append(.video(mediaItem.url)) + } + } var body: some View { GeometryReader { (deviceSize: GeometryProxy) in @@ -433,11 +443,16 @@ struct PostView: View { Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {} } } - .sheet(isPresented: $attach_camera) { - CameraController(uploader: damus_state.settings.default_media_uploader) { - self.attach_camera = false - self.attach_media = true - } + .fullScreenCover(isPresented: $attach_camera) { + CameraView(damus_state: damus_state, action: { items in + for item in items { + addToMediaToUpload(mediaItem: item) + } + for media in mediaToUpload { + self.handle_upload(media: media) + } + mediaToUpload = [] + }) } .onAppear() { let loaded_draft = load_draft()