-
Notifications
You must be signed in to change notification settings - Fork 289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Redesign In-App Camera. #1254
base: master
Are you sure you want to change the base?
Redesign In-App Camera. #1254
Conversation
I found a better way to do this. Will hopefully fix the long delay issue too. |
71a7ba1
to
804a24e
Compare
@jb55 this is ready for review. |
On Mon, Jun 05, 2023 at 11:05:10PM -0500, Suhail Saqan wrote:
@jb55 this is ready for review.
It's 1444 lines in a single commit with no description.
I feel like I'm just beating the horse at this point but... I can't
review this in one sitting and its impossible to merge in its current
state. Please split up your work as described before. This will be much
more mergable if I can merge it bit by bit. A simple example is the
image resizer, if that was in its own commit I could merge that right
now and then review will move along much quicker. Help me and I'll help
you!
You keep doing *lots of work* that is *impossible* to merge. The
contributor guidelines about formatting commits still apply, just
because its through PRs instead of email now doesn't change that.
I imagine this this is frustrating for both of us. I already laid out
how to ensure your code will get merged, but since you keep ignoring it
you will have to continue waiting until the commits are properly
formatted, sorry.
|
This PR is properly formatted but never got any review.
https://github.com/damus-io/damus/pull/816/commits
…On Sun, Sep 17, 2023 at 8:23 AM William Casarin ***@***.***> wrote:
On Mon, Jun 05, 2023 at 11:05:10PM -0500, Suhail Saqan wrote:
***@***.*** this is ready for review.
It's 1444 lines in a single commit with no description.
I feel like I'm just beating the horse at this point but... I can't
review this in one sitting and its impossible to merge in its current
state. Please split up your work as described before. This will be much
more mergable if I can merge it bit by bit. A simple example is the
image resizer, if that was in its own commit I could merge that right
now and then review will move along much quicker. Help me and I'll help
you!
You keep doing *lots of work* that is *impossible* to merge. The
contributor guidelines about formatting commits still apply, just
because its through PRs instead of email now doesn't change that.
I imagine this this is frustrating for both of us. I already laid out
how to ensure your code will get merged, but since you keep ignoring it
you will have to continue waiting until the commits are properly
formatted, sorry.
--
Suhail
|
On Sun, Sep 17, 2023 at 02:24:09PM -0700, Suhail Saqan wrote:
This PR is properly formatted but never got any review.
https://github.com/damus-io/damus/pull/816/commits
I see that it was properly formatted but you never pinged anyone on the
PR for review after it was ready to review again. The only possible way
I can keep track of what to review and what not to review if its in my
inbox. I remember you pinging me on various chat apps a few times but
that will just get lost and forgotten until we have proper asyncronous
comms on nostr.
I will manually add the DM one to my inbox but next time a ping will
help me a lot.
|
f82a9bb
to
2ee2693
Compare
This is ready for review again @jb55! |
On Thu, Sep 21, 2023 at 12:45:11PM -0700, Suhail Saqan wrote:
This is ready for review again @jb55!
thanks, will do another pass soon
|
Reviewed-by: William Casarin ***@***.***>
…On Tue, Sep 19, 2023 at 01:24:11PM -0700, Suhail Saqan wrote:
---
damus.xcodeproj/project.pbxproj | 12 ++++++++
damus/Models/Camera/ImageResizer.swift | 40 ++++++++++++++++++++++++++
2 files changed, 52 insertions(+)
create mode 100644 damus/Models/Camera/ImageResizer.swift
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
index c1686283d..9759c819b 100644
--- a/damus.xcodeproj/project.pbxproj
+++ b/damus.xcodeproj/project.pbxproj
@@ -377,6 +377,7 @@
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; };
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C83F89229A937B900136C08 /* TextViewWrapper.swift */; };
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; };
+ BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; };
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 */; };
@@ -928,6 +929,7 @@
9609F057296E220800069BF3 /* BannerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerImageView.swift; sourceTree = "<group>"; };
9C83F89229A937B900136C08 /* TextViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewWrapper.swift; sourceTree = "<group>"; };
9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachMediaUtility.swift; sourceTree = "<group>"; };
+ BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = "<group>"; };
BA4AB0AD2A63B9270070A32A /* AddEmojiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEmojiView.swift; sourceTree = "<group>"; };
BA4AB0AF2A63B94D0070A32A /* EmojiListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiListItemView.swift; sourceTree = "<group>"; };
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; };
@@ -1071,6 +1073,7 @@
4C0A3F8D280F63FF000448DE /* Models */ = {
isa = PBXGroup;
children = (
+ BA3759882ABCCDE30018D73B /* Camera */,
4C190F1E2A535FC200027FD5 /* Zaps */,
4C54AA0829A55416003E4487 /* Notifications */,
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */,
@@ -1860,6 +1863,14 @@
path = Extensions;
sourceTree = "<group>";
};
+ BA3759882ABCCDE30018D73B /* Camera */ = {
+ isa = PBXGroup;
+ children = (
+ BA3759892ABCCDE30018D73B /* ImageResizer.swift */,
+ );
+ path = Camera;
+ sourceTree = "<group>";
+ };
F71694E82A66221E001F4053 /* Onboarding */ = {
isa = PBXGroup;
children = (
@@ -2370,6 +2381,7 @@
4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */,
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */,
50A60D142A28BEEE00186190 /* RelayLog.swift in Sources */,
+ BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */,
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
diff --git a/damus/Models/Camera/ImageResizer.swift b/damus/Models/Camera/ImageResizer.swift
new file mode 100644
index 000000000..2130d354e
--- /dev/null
+++ b/damus/Models/Camera/ImageResizer.swift
@@ -0,0 +1,40 @@
+//
+// ImageResizer.swift
+// damus
+//
+// Created by Suhail Saqan on 8/5/23.
+//
+
+import Foundation
+import UIKit
+
+public enum ImageResizingError: Error {
+ case cannotRetrieveFromURL
+ case cannotRetrieveFromData
+}
+
+public struct ImageResizer {
+ public var targetWidth: CGFloat
+
+ public init(targetWidth: CGFloat) {
+ self.targetWidth = targetWidth
+ }
+
+ public func resize(at url: URL) -> UIImage? {
+ guard let image = UIImage(contentsOfFile: url.path) else {
+ return nil
+ }
+
+ return self.resize(image: image)
+ }
+
+ public func resize(image: UIImage) -> UIImage {
+ let originalSize = image.size
+ let targetSize = CGSize(width: targetWidth, height: targetWidth*originalSize.height/originalSize.width)
+ let renderer = UIGraphicsImageRenderer(size: targetSize)
+ return renderer.image { (context) in
+ image.draw(in: CGRect(origin: .zero, size: targetSize))
+ }
+ }
+}
+
|
Reviewed-by: William Casarin ***@***.***>
…On Tue, Sep 19, 2023 at 01:29:40PM -0700, Suhail Saqan wrote:
---
damus.xcodeproj/project.pbxproj | 8 ++
.../Models/Camera/PhotoCaptureProcessor.swift | 91 +++++++++++++++++++
.../Models/Camera/VideoCaptureProcessor.swift | 77 ++++++++++++++++
3 files changed, 176 insertions(+)
create mode 100644 damus/Models/Camera/PhotoCaptureProcessor.swift
create mode 100644 damus/Models/Camera/VideoCaptureProcessor.swift
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
index 9759c819b..18e32e922 100644
--- a/damus.xcodeproj/project.pbxproj
+++ b/damus.xcodeproj/project.pbxproj
@@ -378,6 +378,8 @@
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C83F89229A937B900136C08 /* TextViewWrapper.swift */; };
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.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 */; };
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 */; };
@@ -930,6 +932,8 @@
9C83F89229A937B900136C08 /* TextViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewWrapper.swift; sourceTree = "<group>"; };
9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachMediaUtility.swift; sourceTree = "<group>"; };
BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = "<group>"; };
+ BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureProcessor.swift; sourceTree = "<group>"; };
+ BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCaptureProcessor.swift; sourceTree = "<group>"; };
BA4AB0AD2A63B9270070A32A /* AddEmojiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEmojiView.swift; sourceTree = "<group>"; };
BA4AB0AF2A63B94D0070A32A /* EmojiListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiListItemView.swift; sourceTree = "<group>"; };
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; };
@@ -1867,6 +1871,8 @@
isa = PBXGroup;
children = (
BA3759892ABCCDE30018D73B /* ImageResizer.swift */,
+ BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */,
+ BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */,
);
path = Camera;
sourceTree = "<group>";
@@ -2298,6 +2304,7 @@
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
4C1A9A2A29DDF54400516EAC /* DamusVideoPlayer.swift in Sources */,
4CA352A22A76AEC5003BB08B /* LikedNotify.swift in Sources */,
+ BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */,
4C9146FD2A2A87C200DDEA40 /* wasm.c in Sources */,
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
@@ -2447,6 +2454,7 @@
4C9AA14A2A4587A6003F49FD /* NotificationStatusModel.swift in Sources */,
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */,
+ BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */,
4C9B0DF32A65C46800CBDA21 /* ProfileEditButton.swift in Sources */,
4C2859622A12A7F0004746F7 /* GoldSupportGradient.swift in Sources */,
);
diff --git a/damus/Models/Camera/PhotoCaptureProcessor.swift b/damus/Models/Camera/PhotoCaptureProcessor.swift
new file mode 100644
index 000000000..9d5956daa
--- /dev/null
+++ b/damus/Models/Camera/PhotoCaptureProcessor.swift
@@ -0,0 +1,91 @@
+//
+// PhotoCaptureProcessor.swift
+// damus
+//
+// Created by Suhail Saqan on 8/5/23.
+//
+
+import Foundation
+import Photos
+
+class PhotoCaptureProcessor: NSObject {
+ private(set) var requestedPhotoSettings: AVCapturePhotoSettings
+ private(set) var photoOutput: AVCapturePhotoOutput?
+
+ lazy var context = CIContext()
+ var photoData: Data?
+ private var maxPhotoProcessingTime: CMTime?
+
+ private let willCapturePhotoAnimation: () -> Void
+ private let completionHandler: (PhotoCaptureProcessor) -> Void
+ private let photoProcessingHandler: (Bool) -> Void
+
+ init(with requestedPhotoSettings: AVCapturePhotoSettings,
+ photoOutput: AVCapturePhotoOutput?,
+ willCapturePhotoAnimation: @escaping () -> Void,
+ completionHandler: @escaping (PhotoCaptureProcessor) -> Void,
+ photoProcessingHandler: @escaping (Bool) -> Void) {
+ self.requestedPhotoSettings = requestedPhotoSettings
+ self.willCapturePhotoAnimation = willCapturePhotoAnimation
+ self.completionHandler = completionHandler
+ self.photoProcessingHandler = photoProcessingHandler
+ self.photoOutput = photoOutput
+ }
+
+ func capturePhoto(settings: AVCapturePhotoSettings) {
+ if let photoOutput = self.photoOutput {
+ photoOutput.capturePhoto(with: settings, delegate: self)
+ }
+ }
+}
+
+extension PhotoCaptureProcessor: AVCapturePhotoCaptureDelegate {
+ func photoOutput(_ output: AVCapturePhotoOutput, willBeginCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings) {
+ maxPhotoProcessingTime = resolvedSettings.photoProcessingTimeRange.start + resolvedSettings.photoProcessingTimeRange.duration
+ }
+
+ func photoOutput(_ output: AVCapturePhotoOutput, willCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings) {
+ DispatchQueue.main.async {
+ self.willCapturePhotoAnimation()
+ }
+
+ guard let maxPhotoProcessingTime = maxPhotoProcessingTime else {
+ return
+ }
+
+ DispatchQueue.main.async {
+ self.photoProcessingHandler(true)
+ }
+
+ let oneSecond = CMTime(seconds: 2, preferredTimescale: 1)
+ if maxPhotoProcessingTime > oneSecond {
+ DispatchQueue.main.async {
+ self.photoProcessingHandler(true)
+ }
+ }
+ }
+
+ func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
+ DispatchQueue.main.async {
+ self.photoProcessingHandler(false)
+ }
+
+ if let error = error {
+ print("Error capturing photo: \(error)")
+ } else {
+ photoData = photo.fileDataRepresentation()
+
+ }
+ }
+
+ func photoOutput(_ output: AVCapturePhotoOutput, didFinishCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) {
+ if let error = error {
+ print("Error capturing photo: \(error)")
+ return
+ }
+
+ DispatchQueue.main.async {
+ self.completionHandler(self)
+ }
+ }
+}
diff --git a/damus/Models/Camera/VideoCaptureProcessor.swift b/damus/Models/Camera/VideoCaptureProcessor.swift
new file mode 100644
index 000000000..7e9e614e8
--- /dev/null
+++ b/damus/Models/Camera/VideoCaptureProcessor.swift
@@ -0,0 +1,77 @@
+//
+// VideoCaptureProcessor.swift
+// damus
+//
+// Created by Suhail Saqan on 8/5/23.
+//
+
+import Foundation
+import AVFoundation
+import Photos
+
+class VideoCaptureProcessor: NSObject {
+ private(set) var movieOutput: AVCaptureMovieFileOutput?
+
+ private let beginHandler: () -> Void
+ private let completionHandler: (VideoCaptureProcessor, URL) -> Void
+ private let videoProcessingHandler: (Bool) -> Void
+ private var session: AVCaptureSession?
+
+ init(movieOutput: AVCaptureMovieFileOutput?,
+ beginHandler: @escaping () -> Void,
+ completionHandler: @escaping (VideoCaptureProcessor, URL) -> Void,
+ videoProcessingHandler: @escaping (Bool) -> Void) {
+ self.beginHandler = beginHandler
+ self.completionHandler = completionHandler
+ self.videoProcessingHandler = videoProcessingHandler
+ self.movieOutput = movieOutput
+ }
+
+ func startCapture(session: AVCaptureSession) {
+ if let movieOutput = self.movieOutput, session.isRunning {
+ let outputFileURL = uniqueOutputFileURL()
+ movieOutput.startRecording(to: outputFileURL, recordingDelegate: self)
+ }
+ }
+
+ func stopCapture() {
+ if let movieOutput = self.movieOutput {
+ if movieOutput.isRecording {
+ movieOutput.stopRecording()
+ }
+ }
+ }
+
+ private func uniqueOutputFileURL() -> URL {
+ let tempDirectory = FileManager.default.temporaryDirectory
+ let fileName = UUID().uuidString + ".mov"
+ return tempDirectory.appendingPathComponent(fileName)
+ }
+}
+
+extension VideoCaptureProcessor: AVCaptureFileOutputRecordingDelegate {
+
+ func fileOutput(_ output: AVCaptureFileOutput, didStartRecordingTo fileURL: URL, from connections: [AVCaptureConnection]) {
+ DispatchQueue.main.async {
+ self.beginHandler()
+ }
+ }
+
+ func fileOutput(_ output: AVCaptureFileOutput, willFinishRecordingTo fileURL: URL, from connections: [AVCaptureConnection]) {
+ DispatchQueue.main.async {
+ self.videoProcessingHandler(true)
+ }
+ }
+
+ func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
+ if let error = error {
+ print("Error capturing video: \(error)")
+ return
+ }
+
+ DispatchQueue.main.async {
+ self.completionHandler(self, outputFileURL)
+ self.videoProcessingHandler(false)
+ }
+ }
+}
|
On Tue, Sep 19, 2023 at 01:33:41PM -0700, Suhail Saqan wrote:
+ service.$shouldShowAlertView.sink { [weak self] (val) in
+ self?.alertError = self?.service.alertError
+ self?.showAlertError = val
+ }
+ .store(in: &self.subscriptions)
+
+ service.$flashMode.sink { [weak self] (mode) in
+ self?.isFlashOn = mode == .on
+ }
+ .store(in: &self.subscriptions)
+
+ service.$willCapturePhoto.sink { [weak self] (val) in
+ self?.willCapturePhoto = val
+ }
+ .store(in: &self.subscriptions)
+
+ service.$isCameraButtonDisabled.sink { [weak self] (val) in
+ self?.isCameraButtonDisabled = val
+ }
+ .store(in: &self.subscriptions)
+
+ service.$isPhotoProcessing.sink { [weak self] (val) in
+ self?.isPhotoProcessing = val
+ }
+ .store(in: &self.subscriptions)
+
+ service.$isRecording.sink { [weak self] (val) in
+ self?.isRecording = val
+ }
+ .store(in: &self.subscriptions)
+
+ service.$captureMode.sink { [weak self] (mode) in
+ self?.captureMode = mode
+ }
+ .store(in: &self.subscriptions)
+
+ service.$mediaItems.sink { [weak self] (mode) in
+ self?.mediaItems = mode
+ }
+ .store(in: &self.subscriptions)
+
+ service.$thumbnail.sink { [weak self] (thumbnail) in
+ guard let pic = thumbnail else { return }
+ self?.thumbnail = pic
+ }
+ .store(in: &self.subscriptions)
This is a new pattern I've never seen before. What's this for and is
there a reason this is done this way?
|
re: [PATCH damus 3/6] add CameraModel and CameraService for interacting with the camera
Looks complicated and I have no idea what all this is for because there are no commit descriptions but I'll merge it for now.
Reviewed-by: William Casarin ***@***.***>
…On Tue, Sep 19, 2023 at 01:33:41PM -0700, Suhail Saqan wrote:
Closes: #1254
---
damus.xcodeproj/project.pbxproj | 12 +
damus/Models/Camera/CameraModel.swift | 122 +++
.../Camera/CameraService+Extensions.swift | 32 +
damus/Models/Camera/CameraService.swift | 693 ++++++++++++++++++
4 files changed, 859 insertions(+)
create mode 100644 damus/Models/Camera/CameraModel.swift
create mode 100644 damus/Models/Camera/CameraService+Extensions.swift
create mode 100644 damus/Models/Camera/CameraService.swift
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
index 18e32e922..b5b5bf6c9 100644
--- a/damus.xcodeproj/project.pbxproj
+++ b/damus.xcodeproj/project.pbxproj
@@ -380,6 +380,9 @@
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 */; };
+ BA3759922ABCCEBA0018D73B /* CameraService+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */; };
+ BA3759932ABCCEBA0018D73B /* CameraModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759902ABCCEBA0018D73B /* CameraModel.swift */; };
+ BA3759942ABCCEBA0018D73B /* CameraService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759912ABCCEBA0018D73B /* CameraService.swift */; };
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 */; };
@@ -934,6 +937,9 @@
BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = "<group>"; };
BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureProcessor.swift; sourceTree = "<group>"; };
BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCaptureProcessor.swift; sourceTree = "<group>"; };
+ BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CameraService+Extensions.swift"; sourceTree = "<group>"; };
+ BA3759902ABCCEBA0018D73B /* CameraModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraModel.swift; sourceTree = "<group>"; };
+ BA3759912ABCCEBA0018D73B /* CameraService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraService.swift; sourceTree = "<group>"; };
BA4AB0AD2A63B9270070A32A /* AddEmojiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEmojiView.swift; sourceTree = "<group>"; };
BA4AB0AF2A63B94D0070A32A /* EmojiListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiListItemView.swift; sourceTree = "<group>"; };
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; };
@@ -1870,6 +1876,9 @@
BA3759882ABCCDE30018D73B /* Camera */ = {
isa = PBXGroup;
children = (
+ BA3759902ABCCEBA0018D73B /* CameraModel.swift */,
+ BA3759912ABCCEBA0018D73B /* CameraService.swift */,
+ BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */,
BA3759892ABCCDE30018D73B /* ImageResizer.swift */,
BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */,
BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */,
@@ -2204,6 +2213,7 @@
4C363A8428233689006E126D /* Parser.swift in Sources */,
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */,
4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */,
+ BA3759922ABCCEBA0018D73B /* CameraService+Extensions.swift in Sources */,
4C363A9A28283854006E126D /* Reply.swift in Sources */,
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */,
@@ -2240,6 +2250,7 @@
4C198DF529F88D2E004C165C /* ImageMetadata.swift in Sources */,
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */,
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */,
+ BA3759932ABCCEBA0018D73B /* CameraModel.swift in Sources */,
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */,
4C4E137B2A76D5FB00BDD832 /* MuteThreadNotify.swift in Sources */,
@@ -2339,6 +2350,7 @@
4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */,
4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */,
4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */,
+ BA3759942ABCCEBA0018D73B /* CameraService.swift in Sources */,
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
F71694EA2A662232001F4053 /* SuggestedUsersView.swift in Sources */,
diff --git a/damus/Models/Camera/CameraModel.swift b/damus/Models/Camera/CameraModel.swift
new file mode 100644
index 000000000..73def5730
--- /dev/null
+++ b/damus/Models/Camera/CameraModel.swift
@@ -0,0 +1,122 @@
+//
+// CameraModel.swift
+// damus
+//
+// Created by Suhail Saqan on 8/5/23.
+//
+
+import Foundation
+import AVFoundation
+import Combine
+
+final class CameraModel: ObservableObject {
+ private let service = CameraService()
+
+ @published var showAlertError = false
+
+ @published var isFlashOn = false
+
+ @published var willCapturePhoto = false
+
+ @published var isCameraButtonDisabled = false
+
+ @published var isPhotoProcessing = false
+
+ @published var isRecording = false
+
+ @published var captureMode: CameraMediaType = .image
+
+ @published public var mediaItems: [MediaItem] = []
+
+ @published var thumbnail: Thumbnail!
+
+ var alertError: AlertError!
+
+ var session: AVCaptureSession
+
+ private var subscriptions = Set<AnyCancellable>()
+
+ init() {
+ self.session = service.session
+
+ service.$shouldShowAlertView.sink { [weak self] (val) in
+ self?.alertError = self?.service.alertError
+ self?.showAlertError = val
+ }
+ .store(in: &self.subscriptions)
+
+ service.$flashMode.sink { [weak self] (mode) in
+ self?.isFlashOn = mode == .on
+ }
+ .store(in: &self.subscriptions)
+
+ service.$willCapturePhoto.sink { [weak self] (val) in
+ self?.willCapturePhoto = val
+ }
+ .store(in: &self.subscriptions)
+
+ service.$isCameraButtonDisabled.sink { [weak self] (val) in
+ self?.isCameraButtonDisabled = val
+ }
+ .store(in: &self.subscriptions)
+
+ service.$isPhotoProcessing.sink { [weak self] (val) in
+ self?.isPhotoProcessing = val
+ }
+ .store(in: &self.subscriptions)
+
+ service.$isRecording.sink { [weak self] (val) in
+ self?.isRecording = val
+ }
+ .store(in: &self.subscriptions)
+
+ service.$captureMode.sink { [weak self] (mode) in
+ self?.captureMode = mode
+ }
+ .store(in: &self.subscriptions)
+
+ service.$mediaItems.sink { [weak self] (mode) in
+ self?.mediaItems = mode
+ }
+ .store(in: &self.subscriptions)
+
+ service.$thumbnail.sink { [weak self] (thumbnail) in
+ guard let pic = thumbnail else { return }
+ self?.thumbnail = pic
+ }
+ .store(in: &self.subscriptions)
+ }
+
+ func configure() {
+ service.checkForPermissions()
+ service.configure()
+ }
+
+ func stop() {
+ service.stop()
+ }
+
+ func capturePhoto() {
+ service.capturePhoto()
+ }
+
+ func startRecording() {
+ service.startRecording()
+ }
+
+ func stopRecording() {
+ service.stopRecording()
+ }
+
+ func flipCamera() {
+ service.changeCamera()
+ }
+
+ func zoom(with factor: CGFloat) {
+ service.set(zoom: factor)
+ }
+
+ func switchFlash() {
+ service.flashMode = service.flashMode == .on ? .off : .on
+ }
+}
diff --git a/damus/Models/Camera/CameraService+Extensions.swift b/damus/Models/Camera/CameraService+Extensions.swift
new file mode 100644
index 000000000..2c31c4884
--- /dev/null
+++ b/damus/Models/Camera/CameraService+Extensions.swift
@@ -0,0 +1,32 @@
+//
+// CameraService+Extensions.swift
+// damus
+//
+// Created by Suhail Saqan on 8/5/23.
+//
+
+import Foundation
+import UIKit
+import AVFoundation
+
+extension AVCaptureVideoOrientation {
+ init?(deviceOrientation: UIDeviceOrientation) {
+ switch deviceOrientation {
+ case .portrait: self = .portrait
+ case .portraitUpsideDown: self = .portraitUpsideDown
+ case .landscapeLeft: self = .landscapeRight
+ case .landscapeRight: self = .landscapeLeft
+ default: return nil
+ }
+ }
+
+ init?(interfaceOrientation: UIInterfaceOrientation) {
+ switch interfaceOrientation {
+ case .portrait: self = .portrait
+ case .portraitUpsideDown: self = .portraitUpsideDown
+ case .landscapeLeft: self = .landscapeLeft
+ case .landscapeRight: self = .landscapeRight
+ default: return nil
+ }
+ }
+}
diff --git a/damus/Models/Camera/CameraService.swift b/damus/Models/Camera/CameraService.swift
new file mode 100644
index 000000000..e5a617163
--- /dev/null
+++ b/damus/Models/Camera/CameraService.swift
@@ -0,0 +1,693 @@
+//
+// CameraService.swift
+// Campus
+//
+// Created by Suhail Saqan on 8/5/23.
+//
+
+import Foundation
+import Combine
+import AVFoundation
+import Photos
+import UIKit
+
+public struct Thumbnail: Identifiable, Equatable {
+ public var id: String
+ public var type: CameraMediaType
+ public var url: URL
+
+ public init(id: String = UUID().uuidString, type: CameraMediaType, url: URL) {
+ self.id = id
+ self.type = type
+ self.url = url
+ }
+
+ public var thumbnailImage: UIImage? {
+ switch type {
+ case .image:
+ return ImageResizer(targetWidth: 100).resize(at: url)
+ case .video:
+ return generateVideoThumbnail(for: url)
+ }
+ }
+}
+
+public struct AlertError {
+ public var title: String = ""
+ public var message: String = ""
+ public var primaryButtonTitle = "Accept"
+ public var secondaryButtonTitle: String?
+ public var primaryAction: (() -> ())?
+ public var secondaryAction: (() -> ())?
+
+ public init(title: String = "", message: String = "", primaryButtonTitle: String = "Accept", secondaryButtonTitle: String? = nil, primaryAction: (() -> ())? = nil, secondaryAction: (() -> ())? = nil) {
+ self.title = title
+ self.message = message
+ self.primaryAction = primaryAction
+ self.primaryButtonTitle = primaryButtonTitle
+ self.secondaryAction = secondaryAction
+ }
+}
+
+func generateVideoThumbnail(for videoURL: URL) -> UIImage? {
+ let asset = AVAsset(url: videoURL)
+ let imageGenerator = AVAssetImageGenerator(asset: asset)
+ imageGenerator.appliesPreferredTrackTransform = true
+
+ do {
+ let cgImage = try imageGenerator.copyCGImage(at: .zero, actualTime: nil)
+ return UIImage(cgImage: cgImage)
+ } catch {
+ print("Error generating thumbnail: \(error)")
+ return nil
+ }
+}
+
+public enum CameraMediaType {
+ case image
+ case video
+}
+
+public struct MediaItem {
+ let url: URL
+ let type: CameraMediaType
+}
+
+public class CameraService: NSObject, Identifiable {
+ public let session = AVCaptureSession()
+
+ public var isSessionRunning = false
+ public var isConfigured = false
+ var setupResult: SessionSetupResult = .success
+
+ public var alertError: AlertError = AlertError()
+
+ @published public var flashMode: AVCaptureDevice.FlashMode = .off
+ @published public var shouldShowAlertView = false
+ @published public var isPhotoProcessing = false
+ @published public var captureMode: CameraMediaType = .image
+ @published public var isRecording: Bool = false
+
+ @published public var willCapturePhoto = false
+ @published public var isCameraButtonDisabled = false
+ @published public var isCameraUnavailable = false
+ @published public var thumbnail: Thumbnail?
+ @published public var mediaItems: [MediaItem] = []
+
+ public let sessionQueue = DispatchQueue(label: "io.damus.camera")
+
+ @objc dynamic public var videoDeviceInput: AVCaptureDeviceInput!
+ @objc dynamic public var audioDeviceInput: AVCaptureDeviceInput!
+
+ public let videoDeviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera, .builtInDualCamera, .builtInTrueDepthCamera], mediaType: .video, position: .unspecified)
+
+ public let photoOutput = AVCapturePhotoOutput()
+
+ public let movieOutput = AVCaptureMovieFileOutput()
+
+ var videoCaptureProcessor: VideoCaptureProcessor?
+ var photoCaptureProcessor: PhotoCaptureProcessor?
+
+ public var keyValueObservations = [NSKeyValueObservation]()
+
+ override public init() {
+ super.init()
+
+ DispatchQueue.main.async {
+ self.isCameraButtonDisabled = true
+ self.isCameraUnavailable = true
+ }
+ }
+
+ enum SessionSetupResult {
+ case success
+ case notAuthorized
+ case configurationFailed
+ }
+
+ public func configure() {
+ if !self.isSessionRunning && !self.isConfigured {
+ sessionQueue.async {
+ self.configureSession()
+ }
+ }
+ }
+
+ public func checkForPermissions() {
+ switch AVCaptureDevice.authorizationStatus(for: .video) {
+ case .authorized:
+ break
+ case .notDetermined:
+ sessionQueue.suspend()
+ AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in
+ if !granted {
+ self.setupResult = .notAuthorized
+ }
+ self.sessionQueue.resume()
+ })
+
+ default:
+ setupResult = .notAuthorized
+
+ DispatchQueue.main.async {
+ self.alertError = AlertError(title: "Camera Access", message: "Damus needs camera and microphone access. Enable in settings.", primaryButtonTitle: "Go to settings", secondaryButtonTitle: nil, primaryAction: {
+ UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!,
+ options: [:], completionHandler: nil)
+
+ }, secondaryAction: nil)
+ self.shouldShowAlertView = true
+ self.isCameraUnavailable = true
+ self.isCameraButtonDisabled = true
+ }
+ }
+ }
+
+ private func configureSession() {
+ if setupResult != .success {
+ return
+ }
+
+ session.beginConfiguration()
+
+ session.sessionPreset = .high
+
+ // Add video input.
+ do {
+ var defaultVideoDevice: AVCaptureDevice?
+
+ if let backCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) {
+ // If a rear dual camera is not available, default to the rear wide angle camera.
+ defaultVideoDevice = backCameraDevice
+ } else if let frontCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) {
+ // If the rear wide angle camera isn't available, default to the front wide angle camera.
+ defaultVideoDevice = frontCameraDevice
+ }
+
+ guard let videoDevice = defaultVideoDevice else {
+ print("Default video device is unavailable.")
+ setupResult = .configurationFailed
+ session.commitConfiguration()
+ return
+ }
+
+ let videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice)
+
+ if session.canAddInput(videoDeviceInput) {
+ session.addInput(videoDeviceInput)
+ self.videoDeviceInput = videoDeviceInput
+ } else {
+ print("Couldn't add video device input to the session.")
+ setupResult = .configurationFailed
+ session.commitConfiguration()
+ return
+ }
+
+ let audioDevice = AVCaptureDevice.default(for: .audio)
+ let audioDeviceInput = try AVCaptureDeviceInput(device: audioDevice!)
+
+ if session.canAddInput(audioDeviceInput) {
+ session.addInput(audioDeviceInput)
+ self.audioDeviceInput = audioDeviceInput
+ } else {
+ print("Couldn't add audio device input to the session.")
+ setupResult = .configurationFailed
+ session.commitConfiguration()
+ return
+ }
+
+ // Add video output
+ if session.canAddOutput(movieOutput) {
+ session.addOutput(movieOutput)
+ } else {
+ print("Could not add movie output to the session")
+ setupResult = .configurationFailed
+ session.commitConfiguration()
+ return
+ }
+ } catch {
+ print("Couldn't create video device input: \(error)")
+ setupResult = .configurationFailed
+ session.commitConfiguration()
+ return
+ }
+
+ // Add the photo output.
+ if session.canAddOutput(photoOutput) {
+ session.addOutput(photoOutput)
+
+ photoOutput.maxPhotoQualityPrioritization = .quality
+
+ } else {
+ print("Could not add photo output to the session")
+ setupResult = .configurationFailed
+ session.commitConfiguration()
+ return
+ }
+
+ session.commitConfiguration()
+ self.isConfigured = true
+
+ self.start()
+ }
+
+ private func resumeInterruptedSession() {
+ sessionQueue.async {
+ self.session.startRunning()
+ self.isSessionRunning = self.session.isRunning
+ if !self.session.isRunning {
+ DispatchQueue.main.async {
+ self.alertError = AlertError(title: "Camera Error", message: "Unable to resume camera", primaryButtonTitle: "Accept", secondaryButtonTitle: nil, primaryAction: nil, secondaryAction: nil)
+ self.shouldShowAlertView = true
+ self.isCameraUnavailable = true
+ self.isCameraButtonDisabled = true
+ }
+ } else {
+ DispatchQueue.main.async {
+ self.isCameraUnavailable = false
+ self.isCameraButtonDisabled = false
+ }
+ }
+ }
+ }
+
+ public func changeCamera() {
+ DispatchQueue.main.async {
+ self.isCameraButtonDisabled = true
+ }
+
+ sessionQueue.async {
+ let currentVideoDevice = self.videoDeviceInput.device
+ let currentPosition = currentVideoDevice.position
+
+ let preferredPosition: AVCaptureDevice.Position
+ let preferredDeviceType: AVCaptureDevice.DeviceType
+
+ switch currentPosition {
+ case .unspecified, .front:
+ preferredPosition = .back
+ preferredDeviceType = .builtInWideAngleCamera
+
+ case .back:
+ preferredPosition = .front
+ preferredDeviceType = .builtInWideAngleCamera
+
+ @unknown default:
+ print("Unknown capture position. Defaulting to back, dual-camera.")
+ preferredPosition = .back
+ preferredDeviceType = .builtInWideAngleCamera
+ }
+ let devices = self.videoDeviceDiscoverySession.devices
+ var newVideoDevice: AVCaptureDevice? = nil
+
+ if let device = devices.first(where: { $0.position == preferredPosition && $0.deviceType == preferredDeviceType }) {
+ newVideoDevice = device
+ } else if let device = devices.first(where: { $0.position == preferredPosition }) {
+ newVideoDevice = device
+ }
+
+ if let videoDevice = newVideoDevice {
+ do {
+ let videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice)
+
+ self.session.beginConfiguration()
+
+ self.session.removeInput(self.videoDeviceInput)
+
+ if self.session.canAddInput(videoDeviceInput) {
+ NotificationCenter.default.removeObserver(self, name: .AVCaptureDeviceSubjectAreaDidChange, object: currentVideoDevice)
+ NotificationCenter.default.addObserver(self, selector: #selector(self.subjectAreaDidChange), name: .AVCaptureDeviceSubjectAreaDidChange, object: videoDeviceInput.device)
+
+ self.session.addInput(videoDeviceInput)
+ self.videoDeviceInput = videoDeviceInput
+ } else {
+ self.session.addInput(self.videoDeviceInput)
+ }
+
+ if let connection = self.photoOutput.connection(with: .video) {
+ if connection.isVideoStabilizationSupported {
+ connection.preferredVideoStabilizationMode = .auto
+ }
+ }
+
+ self.photoOutput.maxPhotoQualityPrioritization = .quality
+
+ self.session.commitConfiguration()
+ } catch {
+ print("Error occurred while creating video device input: \(error)")
+ }
+ }
+
+ DispatchQueue.main.async {
+ self.isCameraButtonDisabled = false
+ }
+ }
+ }
+
+ public func focus(with focusMode: AVCaptureDevice.FocusMode, exposureMode: AVCaptureDevice.ExposureMode, at devicePoint: CGPoint, monitorSubjectAreaChange: Bool) {
+ sessionQueue.async {
+ guard let device = self.videoDeviceInput?.device else { return }
+ do {
+ try device.lockForConfiguration()
+
+ if device.isFocusPointOfInterestSupported && device.isFocusModeSupported(focusMode) {
+ device.focusPointOfInterest = devicePoint
+ device.focusMode = focusMode
+ }
+
+ if device.isExposurePointOfInterestSupported && device.isExposureModeSupported(exposureMode) {
+ device.exposurePointOfInterest = devicePoint
+ device.exposureMode = exposureMode
+ }
+
+ device.isSubjectAreaChangeMonitoringEnabled = monitorSubjectAreaChange
+ device.unlockForConfiguration()
+ } catch {
+ print("Could not lock device for configuration: \(error)")
+ }
+ }
+ }
+
+
+ public func focus(at focusPoint: CGPoint) {
+ let device = self.videoDeviceInput.device
+ do {
+ try device.lockForConfiguration()
+ if device.isFocusPointOfInterestSupported {
+ device.focusPointOfInterest = focusPoint
+ device.exposurePointOfInterest = focusPoint
+ device.exposureMode = .continuousAutoExposure
+ device.focusMode = .continuousAutoFocus
+ device.unlockForConfiguration()
+ }
+ }
+ catch {
+ print(error.localizedDescription)
+ }
+ }
+
+ @objc public func stop(completion: (() -> ())? = nil) {
+ sessionQueue.async {
+ if self.isSessionRunning {
+ if self.setupResult == .success {
+ self.session.stopRunning()
+ self.isSessionRunning = self.session.isRunning
+ print("CAMERA STOPPED")
+ self.removeObservers()
+
+ if !self.session.isRunning {
+ DispatchQueue.main.async {
+ self.isCameraButtonDisabled = true
+ self.isCameraUnavailable = true
+ completion?()
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @objc public func start() {
+ sessionQueue.async {
+ if !self.isSessionRunning && self.isConfigured {
+ switch self.setupResult {
+ case .success:
+ self.addObservers()
+ self.session.startRunning()
+ print("CAMERA RUNNING")
+ self.isSessionRunning = self.session.isRunning
+
+ if self.session.isRunning {
+ DispatchQueue.main.async {
+ self.isCameraButtonDisabled = false
+ self.isCameraUnavailable = false
+ }
+ }
+
+ case .notAuthorized:
+ print("Application not authorized to use camera")
+ DispatchQueue.main.async {
+ self.isCameraButtonDisabled = true
+ self.isCameraUnavailable = true
+ }
+
+ case .configurationFailed:
+ DispatchQueue.main.async {
+ self.alertError = AlertError(title: "Camera Error", message: "Camera configuration failed. Either your device camera is not available or other application is using it", primaryButtonTitle: "Accept", secondaryButtonTitle: nil, primaryAction: nil, secondaryAction: nil)
+ self.shouldShowAlertView = true
+ self.isCameraButtonDisabled = true
+ self.isCameraUnavailable = true
+ }
+ }
+ }
+ }
+ }
+
+ public func set(zoom: CGFloat) {
+ let factor = zoom < 1 ? 1 : zoom
+ let device = self.videoDeviceInput.device
+
+ do {
+ try device.lockForConfiguration()
+ device.videoZoomFactor = factor
+ device.unlockForConfiguration()
+ }
+ catch {
+ print(error.localizedDescription)
+ }
+ }
+
+ public func capturePhoto() {
+ if self.setupResult != .configurationFailed {
+ let videoPreviewLayerOrientation: AVCaptureVideoOrientation = .portrait
+ self.isCameraButtonDisabled = true
+
+ sessionQueue.async {
+ if let photoOutputConnection = self.photoOutput.connection(with: .video) {
+ photoOutputConnection.videoOrientation = videoPreviewLayerOrientation
+ }
+ var photoSettings = AVCapturePhotoSettings()
+
+ // Capture HEIF photos when supported. Enable according to user settings and high-resolution photos.
+ if (self.photoOutput.availablePhotoCodecTypes.contains(.hevc)) {
+ photoSettings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.hevc])
+ }
+
+ if self.videoDeviceInput.device.isFlashAvailable {
+ photoSettings.flashMode = self.flashMode
+ }
+
+ if !photoSettings.__availablePreviewPhotoPixelFormatTypes.isEmpty {
+ photoSettings.previewPhotoFormat = [kCVPixelBufferPixelFormatTypeKey as String: photoSettings.__availablePreviewPhotoPixelFormatTypes.first!]
+ }
+
+ photoSettings.photoQualityPrioritization = .speed
+
+ if self.photoCaptureProcessor == nil {
+ self.photoCaptureProcessor = PhotoCaptureProcessor(with: photoSettings, photoOutput: self.photoOutput, willCapturePhotoAnimation: {
+ DispatchQueue.main.async {
+ self.willCapturePhoto.toggle()
+ self.willCapturePhoto.toggle()
+ }
+ }, completionHandler: { (photoCaptureProcessor) in
+ if let data = photoCaptureProcessor.photoData {
+ let url = self.savePhoto(data: data)
+ if let unwrappedURL = url {
+ self.thumbnail = Thumbnail(type: .image, url: unwrappedURL)
+ }
+ } else {
+ print("Data for photo not found")
+ }
+
+ self.isCameraButtonDisabled = false
+ }, photoProcessingHandler: { animate in
+ self.isPhotoProcessing = animate
+ })
+ }
+
+ self.photoCaptureProcessor?.capturePhoto(settings: photoSettings)
+ }
+ }
+ }
+
+ public func startRecording() {
+ if self.setupResult != .configurationFailed {
+ let videoPreviewLayerOrientation: AVCaptureVideoOrientation = .portrait
+ self.isCameraButtonDisabled = true
+
+ sessionQueue.async {
+ if let videoOutputConnection = self.movieOutput.connection(with: .video) {
+ videoOutputConnection.videoOrientation = videoPreviewLayerOrientation
+
+ var videoSettings = [String: Any]()
+
+ if self.movieOutput.availableVideoCodecTypes.contains(.hevc) == true {
+ videoSettings[AVVideoCodecKey] = AVVideoCodecType.hevc
+ self.movieOutput.setOutputSettings(videoSettings, for: videoOutputConnection)
+ }
+ }
+
+ if self.videoCaptureProcessor == nil {
+ self.videoCaptureProcessor = VideoCaptureProcessor(movieOutput: self.movieOutput, beginHandler: {
+ self.isRecording = true
+ }, completionHandler: { (videoCaptureProcessor, outputFileURL) in
+ self.isCameraButtonDisabled = false
+ self.captureMode = .image
+
+ self.mediaItems.append(MediaItem(url: outputFileURL, type: .video))
+ self.thumbnail = Thumbnail(type: .video, url: outputFileURL)
+ }, videoProcessingHandler: { animate in
+ self.isPhotoProcessing = animate
+ })
+ }
+
+ self.videoCaptureProcessor?.startCapture(session: self.session)
+ }
+ }
+ }
+
+ func stopRecording() {
+ if let videoCaptureProcessor = self.videoCaptureProcessor {
+ isRecording = false
+ videoCaptureProcessor.stopCapture()
+ }
+ }
+
+ func savePhoto(imageType: String = "jpeg", data: Data) -> URL? {
+ guard let uiImage = UIImage(data: data) else {
+ print("Error converting media data to UIImage")
+ return nil
+ }
+
+ guard let compressedData = uiImage.jpegData(compressionQuality: 0.8) else {
+ print("Error converting UIImage to JPEG data")
+ return nil
+ }
+
+ let temporaryDirectory = NSTemporaryDirectory()
+ let tempFileName = "\(UUID().uuidString).\(imageType)"
+ let tempFileURL = URL(fileURLWithPath: temporaryDirectory).appendingPathComponent(tempFileName)
+
+ do {
+ try compressedData.write(to: tempFileURL)
+ self.mediaItems.append(MediaItem(url: tempFileURL, type: .image))
+ return tempFileURL
+ } catch {
+ print("Error saving image data to temporary URL: \(error.localizedDescription)")
+ }
+ return nil
+ }
+
+ private func addObservers() {
+ let systemPressureStateObservation = observe(\.videoDeviceInput.device.systemPressureState, options: .new) { _, change in
+ guard let systemPressureState = change.newValue else { return }
+ self.setRecommendedFrameRateRangeForPressureState(systemPressureState: systemPressureState)
+ }
+ keyValueObservations.append(systemPressureStateObservation)
+
+// NotificationCenter.default.addObserver(self, selector: #selector(self.onOrientationChange), name: UIDevice.orientationDidChangeNotification, object: nil)
+
+ NotificationCenter.default.addObserver(self,
+ selector: #selector(subjectAreaDidChange),
+ name: .AVCaptureDeviceSubjectAreaDidChange,
+ object: videoDeviceInput.device)
+
+ NotificationCenter.default.addObserver(self, selector: #selector(uiRequestedNewFocusArea), name: .init(rawValue: "UserDidRequestNewFocusPoint"), object: nil)
+
+ NotificationCenter.default.addObserver(self,
+ selector: #selector(sessionRuntimeError),
+ name: .AVCaptureSessionRuntimeError,
+ object: session)
+
+ NotificationCenter.default.addObserver(self,
+ selector: #selector(sessionWasInterrupted),
+ name: .AVCaptureSessionWasInterrupted,
+ object: session)
+
+ NotificationCenter.default.addObserver(self,
+ selector: #selector(sessionInterruptionEnded),
+ name: .AVCaptureSessionInterruptionEnded,
+ object: session)
+ }
+
+ private func removeObservers() {
+ NotificationCenter.default.removeObserver(self)
+
+ for keyValueObservation in keyValueObservations {
+ keyValueObservation.invalidate()
+ }
+ keyValueObservations.removeAll()
+ }
+
+ @objc private func uiRequestedNewFocusArea(notification: NSNotification) {
+ guard let userInfo = notification.userInfo as? [String: Any], let devicePoint = userInfo["devicePoint"] as? CGPoint else { return }
+ self.focus(at: devicePoint)
+ }
+
+ @objc
+ private func subjectAreaDidChange(notification: NSNotification) {
+ let devicePoint = CGPoint(x: 0.5, y: 0.5)
+ focus(with: .continuousAutoFocus, exposureMode: .continuousAutoExposure, at: devicePoint, monitorSubjectAreaChange: false)
+ }
+
+ @objc
+ private func sessionRuntimeError(notification: NSNotification) {
+ guard let error = notification.userInfo?[AVCaptureSessionErrorKey] as? AVError else { return }
+
+ print("Capture session runtime error: \(error)")
+
+ if error.code == .mediaServicesWereReset {
+ sessionQueue.async {
+ if self.isSessionRunning {
+ self.session.startRunning()
+ self.isSessionRunning = self.session.isRunning
+ }
+ }
+ }
+ }
+
+ private func setRecommendedFrameRateRangeForPressureState(systemPressureState: AVCaptureDevice.SystemPressureState) {
+ let pressureLevel = systemPressureState.level
+ if pressureLevel == .serious || pressureLevel == .critical {
+ do {
+ try self.videoDeviceInput.device.lockForConfiguration()
+ print("WARNING: Reached elevated system pressure level: \(pressureLevel). Throttling frame rate.")
+ self.videoDeviceInput.device.activeVideoMinFrameDuration = CMTime(value: 1, timescale: 20)
+ self.videoDeviceInput.device.activeVideoMaxFrameDuration = CMTime(value: 1, timescale: 15)
+ self.videoDeviceInput.device.unlockForConfiguration()
+ } catch {
+ print("Could not lock device for configuration: \(error)")
+ }
+ } else if pressureLevel == .shutdown {
+ print("Session stopped running due to shutdown system pressure level.")
+ }
+ }
+
+ @objc
+ private func sessionWasInterrupted(notification: NSNotification) {
+ DispatchQueue.main.async {
+ self.isCameraUnavailable = true
+ }
+
+ if let userInfoValue = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as AnyObject?,
+ let reasonIntegerValue = userInfoValue.integerValue,
+ let reason = AVCaptureSession.InterruptionReason(rawValue: reasonIntegerValue) {
+ print("Capture session was interrupted with reason \(reason)")
+
+ if reason == .audioDeviceInUseByAnotherClient || reason == .videoDeviceInUseByAnotherClient {
+ print("Session stopped running due to video devies in use by another client.")
+ } else if reason == .videoDeviceNotAvailableWithMultipleForegroundApps {
+ print("Session stopped running due to video devies is not available with multiple foreground apps.")
+ } else if reason == .videoDeviceNotAvailableDueToSystemPressure {
+ print("Session stopped running due to shutdown system pressure level.")
+ }
+ }
+ }
+
+ @objc
+ private func sessionInterruptionEnded(notification: NSNotification) {
+ print("Capture session interruption ended")
+ DispatchQueue.main.async {
+ self.isCameraUnavailable = false
+ }
+ }
+}
|
This is almost ready but
"change the camera view to the new custom camera"
Just needs rebased because I can't seem to apply the patch.
looks like due to conflicts in damus/Views/PostView.swift
|
372b147
to
c490a9a
Compare
2882679
to
2846b93
Compare
d130d6c
to
f574fb5
Compare
Closes: #1254 Reviewed-by: William Casarin <jb55@jb55.com> Signed-off-by: William Casarin <jb55@jb55.com>
Closes: #1254 Reviewed-by: William Casarin <jb55@jb55.com> Signed-off-by: William Casarin <jb55@jb55.com>
Closes: #1254 Reviewed-by: William Casarin <jb55@jb55.com> Signed-off-by: William Casarin <jb55@jb55.com>
Closes: #1254 Reviewed-by: William Casarin <jb55@jb55.com> Signed-off-by: William Casarin <jb55@jb55.com>
Closes: #1254 Reviewed-by: William Casarin <jb55@jb55.com> Signed-off-by: William Casarin <jb55@jb55.com> Changelog-Added: Added a custom camera view
Closes: #1254 Reviewed-by: William Casarin <jb55@jb55.com> Signed-off-by: William Casarin <jb55@jb55.com> Changelog-Added: Add ability to preview media taken with camera
this is in the merge queue, I just haven't had a chance to test it yet. |
Closes: #1254 Reviewed-by: William Casarin <jb55@jb55.com> Signed-off-by: William Casarin <jb55@jb55.com> Changelog-Added: Added a custom camera view
Closes: #1254 Reviewed-by: William Casarin <jb55@jb55.com> Signed-off-by: William Casarin <jb55@jb55.com> Changelog-Added: Add ability to preview media taken with camera
it's merged ! the media previews are broken though, it just shows text |
had to revert this due to regressions that were introduced: when you upload multiple media separately it only shows the first media item. |
I tried to merge this but it looks like its too out of sync
…On Sat, Feb 10, 2024 at 02:56:24PM -0800, Suhail Saqan wrote:
https://github.com/damus-io/damus/assets/43693074/da243dcb-1084-4165-af9f-dcff46bfeefe
You can view, comment on, or merge this pull request online at:
#1254
-- Commit Summary --
* redesign camera view
-- File Changes --
M damus.xcodeproj/project.pbxproj (28)
A damus/Models/CameraViewModel.swift (237)
A damus/Views/Camera/CameraPreview.swift (83)
A damus/Views/Camera/CameraView.swift (156)
M damus/Views/PostView.swift (28)
-- Patch Links --
https://github.com/damus-io/damus/pull/1254.patch
https://github.com/damus-io/damus/pull/1254.diff
--
Reply to this email directly or view it on GitHub:
#1254
You are receiving this because you are subscribed to this thread.
Message ID: ***@***.***>
|
Rebased! |
On Sat, Feb 10, 2024 at 02:56:24PM -0800, Suhail Saqan wrote:
Closes: #1254
---
This is great! The one fairly bad UX thing is that when you upload
multiple photos it seems to only show the progress on the first one?
This makes it seem like uploading the other ones have failed... but then
it randomly pops in.
Here's a video of this:
https://cdn.jb55.com/s/18a95d4889dab4fb.mp4
… damus.xcodeproj/project.pbxproj | 10 +-
.../DamusNotificationService.xcscheme | 1 -
damus/Views/Camera/CameraMediaView.swift | 89 +++++++
damus/Views/Camera/CameraView.swift | 221 ++++++++++++++++++
damus/Views/PostView.swift | 25 +-
5 files changed, 338 insertions(+), 8 deletions(-)
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 de4189afe..601de76a8 100644
--- a/damus.xcodeproj/project.pbxproj
+++ b/damus.xcodeproj/project.pbxproj
@@ -433,11 +433,12 @@
B59CAD4D2B688D1000677E8B /* MutelistManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B533694D2B66D791008A805E /* MutelistManager.swift */; };
B5A75C2A2B546D94007AFBC0 /* MuteItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A75C292B546D94007AFBC0 /* MuteItemTests.swift */; };
B5B4D1432B37D47600844320 /* NdbExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B4D1422B37D47600844320 /* NdbExtensions.swift */; };
- BA0F0A6F2B36207E001641B2 /* CameraMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0F0A6E2B36207E001641B2 /* CameraMediaView.swift */; };
BA10192F2B449556009C57DA /* CameraPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA10192E2B449556009C57DA /* CameraPreview.swift */; };
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 */; };
@@ -1355,10 +1356,11 @@
B57B4C652B312C3700A232C0 /* NostrAuth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NostrAuth.swift; sourceTree = "<group>"; };
B5A75C292B546D94007AFBC0 /* MuteItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteItemTests.swift; sourceTree = "<group>"; usesTabs = 0; };
B5B4D1422B37D47600844320 /* NdbExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NdbExtensions.swift; sourceTree = "<group>"; usesTabs = 0; };
- BA0F0A6E2B36207E001641B2 /* CameraMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraMediaView.swift; sourceTree = "<group>"; };
BA10192E2B449556009C57DA /* CameraPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = "<group>"; };
B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteItem.swift; sourceTree = "<group>"; usesTabs = 0; };
B5C60C222B532A8700C5ECA7 /* DamusDuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusDuration.swift; sourceTree = "<group>"; usesTabs = 0; };
+ BA15BB6A2B7833660045B913 /* CameraMediaView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraMediaView.swift; sourceTree = "<group>"; };
+ BA15BB6C2B78336D0045B913 /* CameraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = "<group>"; };
BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = "<group>"; };
BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureProcessor.swift; sourceTree = "<group>"; };
BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCaptureProcessor.swift; sourceTree = "<group>"; };
@@ -2692,8 +2694,10 @@
BA3759952ABCCF360018D73B /* Camera */ = {
isa = PBXGroup;
children = (
+ BA15BB6A2B7833660045B913 /* CameraMediaView.swift */,
BA3759962ABCCF360018D73B /* CameraPreview.swift */,
E02429942B7E97740088B16C /* CameraController.swift */,
+ BA15BB6C2B78336D0045B913 /* CameraView.swift */,
);
path = Camera;
sourceTree = "<group>";
@@ -3045,6 +3049,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 */,
@@ -3131,6 +3136,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/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">
<BuildableProductRunnable
runnableDebuggingMode = "0">
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()
|
Please do not back-merge master into your branch, rebase onto master.
You're also missing the Signed-off-by lines on your commits. See:
https://github.com/damus-io/damus/blob/68409f344092d82e18fc469dfc7bad7014042824/docs/CONTRIBUTING.md#sign-your-work---the-developers-certificate-of-origin
…On Sat, Feb 10, 2024 at 02:56:24PM -0800, Suhail Saqan wrote:
https://github.com/damus-io/damus/assets/43693074/da243dcb-1084-4165-af9f-dcff46bfeefe
You can view, comment on, or merge this pull request online at:
#1254
-- Commit Summary --
* redesign camera view
-- File Changes --
M damus.xcodeproj/project.pbxproj (28)
A damus/Models/CameraViewModel.swift (237)
A damus/Views/Camera/CameraPreview.swift (83)
A damus/Views/Camera/CameraView.swift (156)
M damus/Views/PostView.swift (28)
-- Patch Links --
https://github.com/damus-io/damus/pull/1254.patch
https://github.com/damus-io/damus/pull/1254.diff
--
Reply to this email directly or view it on GitHub:
#1254
You are receiving this because you are subscribed to this thread.
Message ID: ***@***.***>
|
95c0c2b
to
6db601f
Compare
trim.0669AB95-0BD8-4D10-BA9A-15899ACFB94B.MOV