diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index fa5347e10..033a47a8c 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -387,6 +387,8 @@ BA4AB0AE2A63B9270070A32A /* AddEmojiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4AB0AD2A63B9270070A32A /* AddEmojiView.swift */; }; BA4AB0B02A63B94D0070A32A /* EmojiListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4AB0AF2A63B94D0070A32A /* EmojiListItemView.swift */; }; BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; }; + BA8A4F0F2A2D95F70045C48C /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8A4F0E2A2D95F70045C48C /* CameraView.swift */; }; + BA8A4F132A2D96AD0045C48C /* CameraPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8A4F122A2D96AD0045C48C /* CameraPreview.swift */; }; BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; }; D2277EEA2A089BD5006C3807 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; }; D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */; }; @@ -945,6 +947,8 @@ BA4AB0AD2A63B9270070A32A /* AddEmojiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEmojiView.swift; sourceTree = ""; }; BA4AB0AF2A63B94D0070A32A /* EmojiListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiListItemView.swift; sourceTree = ""; }; BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = ""; }; + BA8A4F0E2A2D95F70045C48C /* CameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = ""; }; + BA8A4F122A2D96AD0045C48C /* CameraPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = ""; }; BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = ""; }; D2277EE92A089BD5006C3807 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContentViewTests.swift; sourceTree = ""; }; @@ -1252,6 +1256,7 @@ BA3759952ABCCF360018D73B /* Camera */, F71694E82A66221E001F4053 /* Onboarding */, 4C190F232A547D1700027FD5 /* NostrScript */, + BA8A4F0D2A2D95880045C48C /* Camera */, 4C7D09692A0AEA0400943473 /* CodeScanner */, 4C7D095A2A098C5C00943473 /* Wallet */, 4C8D1A6D29F31E4100ACDF75 /* Buttons */, @@ -2147,6 +2152,7 @@ 4C190F252A547D2000027FD5 /* LoadScript.swift in Sources */, 4C59B98C2A76C2550032FFEB /* ProfileUpdatedNotify.swift in Sources */, 4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */, + BA3BF28A2A7F156B00600232 /* ImageResizer.swift in Sources */, 4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */, 4C12535C2A76CA540004F4B8 /* LoginNotify.swift in Sources */, 4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */, @@ -2222,6 +2228,7 @@ 4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */, BA3759972ABCCF360018D73B /* CameraPreview.swift in Sources */, 4C86F7C42A76C44C00EC0817 /* ZappingNotify.swift in Sources */, + BA27222E2A806E39004CDF52 /* VideoCaptureProcessor.swift in Sources */, 4C363A8428233689006E126D /* Parser.swift in Sources */, 3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */, 4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */, @@ -2316,6 +2323,7 @@ 4C4E137D2A76D63600BDD832 /* UnmuteThreadNotify.swift in Sources */, 4CE4F0F829DB7399005914DB /* ThiccDivider.swift in Sources */, 4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */, + BA3BF28C2A7F156B00600232 /* CameraService.swift in Sources */, 4C363A8828236948006E126D /* BlocksView.swift in Sources */, 4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */, 3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */, @@ -2323,6 +2331,7 @@ 4C1D4FB12A7958E60024F453 /* VersionInfo.swift in Sources */, 5053ACA72A56DF3B00851AE3 /* DeveloperSettingsView.swift in Sources */, F79C7FAD29D5E9620000F946 /* EditPictureControl.swift in Sources */, + BA3BF2892A7F156B00600232 /* PhotoCaptureProcessor.swift in Sources */, 4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */, 4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */, 4C1A9A2A29DDF54400516EAC /* DamusVideoPlayer.swift in Sources */, @@ -2397,6 +2406,7 @@ 4C3EA66028FF5E7700C48A62 /* node_id.c in Sources */, 4C687C212A5F7ED00092C550 /* DamusBackground.swift in Sources */, 4CA352A02A76AE80003BB08B /* Notify.swift in Sources */, + BA3BF28B2A7F156B00600232 /* CameraService+Extensions.swift in Sources */, 4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */, 4C1253582A76C9060004F4B8 /* PresentSheetNotify.swift in Sources */, 4C363A962827096D006E126D /* PostBlock.swift in Sources */, @@ -2442,6 +2452,7 @@ 5C513FBA297F72980072348F /* CustomPicker.swift in Sources */, 4C1253622A76D00B0004F4B8 /* PostNotify.swift in Sources */, 4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */, + BA3BF28F2A7F1B2D00600232 /* CameraModel.swift in Sources */, F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */, 4C9147002A2A891E00DDEA40 /* error.c in Sources */, 4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */, @@ -2451,6 +2462,7 @@ 4CF0ABD42980996B00D66079 /* Report.swift in Sources */, 4C06670B28FDE64700038D2A /* damus.c in Sources */, 4C1253642A76D08F0004F4B8 /* ReportNotify.swift in Sources */, + BA8A4F0F2A2D95F70045C48C /* CameraView.swift in Sources */, 4C1A9A2529DDDF2600516EAC /* ZapSettingsView.swift in Sources */, 4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */, 3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */, @@ -2476,6 +2488,7 @@ 3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */, 3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */, 4C9AA14A2A4587A6003F49FD /* NotificationStatusModel.swift in Sources */, + BA8A4F132A2D96AD0045C48C /* CameraPreview.swift in Sources */, 4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */, 4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */, BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */, @@ -2788,7 +2801,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; - DEVELOPMENT_TEAM = XK7H4JAB3D; + DEVELOPMENT_TEAM = XL4476DR2X; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = damus/Info.plist; @@ -2814,7 +2827,7 @@ "$(PROJECT_DIR)", ); MARKETING_VERSION = 1.6; - PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2; + PRODUCT_BUNDLE_IDENTIFIER = com.suhail.damus2; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; @@ -2837,7 +2850,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; - DEVELOPMENT_TEAM = XK7H4JAB3D; + DEVELOPMENT_TEAM = XL4476DR2X; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = damus/Info.plist; diff --git a/damus/Views/Camera/CameraView.swift b/damus/Views/Camera/CameraView.swift new file mode 100644 index 000000000..c021bb710 --- /dev/null +++ b/damus/Views/Camera/CameraView.swift @@ -0,0 +1,212 @@ +// +// 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: Text(model.mediaItems.map { $0.url.absoluteString }.joined(separator: ", "))) { + 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 8c7897087..d2fde5597 100644 --- a/damus/Views/PostView.swift +++ b/damus/Views/PostView.swift @@ -56,7 +56,7 @@ struct PostView: View { @State var newCursorIndex: Int? @State var postTextViewCanScroll: Bool = true - @State var mediaToUpload: MediaUpload? = nil + @State var mediaToUpload: [MediaUpload] = [] @StateObject var image_upload: ImageUploadModel = ImageUploadModel() @StateObject var tagModel: TagModel = TagModel() @@ -325,6 +325,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 @@ -363,36 +372,29 @@ struct PostView: View { } .sheet(isPresented: $attach_media) { ImagePicker(uploader: damus_state.settings.default_media_uploader, sourceType: .photoLibrary, pubkey: damus_state.pubkey, image_upload_confirm: $image_upload_confirm) { img in - self.mediaToUpload = .image(img) + self.mediaToUpload.append(.image(img)) } onVideoPicked: { url in - self.mediaToUpload = .video(url) + self.mediaToUpload.append(.video(url)) } .alert(NSLocalizedString("Are you sure you want to upload this media?", comment: "Alert message asking if the user wants to upload media."), isPresented: $image_upload_confirm) { Button(NSLocalizedString("Upload", comment: "Button to proceed with uploading."), role: .none) { - if let mediaToUpload { - self.handle_upload(media: mediaToUpload) + if !mediaToUpload.isEmpty { + self.handle_upload(media: mediaToUpload[0]) self.attach_media = false } } Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {} } } - .sheet(isPresented: $attach_camera) { - - ImagePicker(uploader: damus_state.settings.default_media_uploader, sourceType: .camera, pubkey: damus_state.pubkey, image_upload_confirm: $image_upload_confirm) { img in - self.mediaToUpload = .image(img) - } onVideoPicked: { url in - self.mediaToUpload = .video(url) - } - .alert(NSLocalizedString("Are you sure you want to upload this media?", comment: "Alert message asking if the user wants to upload media."), isPresented: $image_upload_confirm) { - Button(NSLocalizedString("Upload", comment: "Button to proceed with uploading."), role: .none) { - if let mediaToUpload { - self.handle_upload(media: mediaToUpload) - self.attach_camera = false - } + .fullScreenCover(isPresented: $attach_camera) { + CameraView(damus_state: damus_state, action: { items in + for item in items { + addToMediaToUpload(mediaItem: item) } - Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {} - } + for media in mediaToUpload { + self.handle_upload(media: media) + } + }) } .onAppear() { let loaded_draft = load_draft()