diff --git a/AppliverySDK/Applivery/Modules/Screenshoot/Screens/ScreenshootPreviewScreen.swift b/AppliverySDK/Applivery/Modules/ScreenshotReport/Screens/ScreenshootPreviewScreen.swift similarity index 72% rename from AppliverySDK/Applivery/Modules/Screenshoot/Screens/ScreenshootPreviewScreen.swift rename to AppliverySDK/Applivery/Modules/ScreenshotReport/Screens/ScreenshootPreviewScreen.swift index 757e4dd..9b0fcb6 100644 --- a/AppliverySDK/Applivery/Modules/Screenshoot/Screens/ScreenshootPreviewScreen.swift +++ b/AppliverySDK/Applivery/Modules/ScreenshotReport/Screens/ScreenshootPreviewScreen.swift @@ -8,13 +8,14 @@ import SwiftUI struct ScreenshootPreviewScreen: View { - let viewModel = ScreenshootViewModel() + @ObservedObject var viewModel = ScreenshootViewModel() @Environment(\.dismiss) var dismiss @State var screenshot: UIImage? @State var user: String = "" @State var description: String = "" @State var reportType: FeedbackType = .feedback @State var imageLines: [Line] = [] + @State var imageIsSelected: Bool = true @FocusState var focused: Bool var body: some View { @@ -38,18 +39,21 @@ struct ScreenshootPreviewScreen: View { .frame(maxHeight: .infinity) .lineLimit(0) .focused($focused) - #if DEBUG - if let screenshot { - Image(uiImage: screenshot) - .resizable() - .frame(width: 200, height: 300) - .aspectRatio(contentMode: .fit) - } - #endif - ScreenShootRowView(image: $screenshot, lines: $imageLines) + ScreenShootRowView( + image: $screenshot, + lines: $imageLines, + isSelected: $imageIsSelected + ) .frame(height: 64) Spacer() } + .alert(viewModel.isReportSended.title ?? "", isPresented: $viewModel.isAlertPresented, actions: { + Button(action: { + dismiss.callAsFunction() + }, label: { + Text("Ok") + }) + }) .padding() .navigationBarTitleDisplayMode(.inline) .navigationTitle("Send \(reportType.rawValue.capitalized)") @@ -71,7 +75,16 @@ struct ScreenshootPreviewScreen: View { image: screenshot, lines: imageLines ) - self.screenshot = newScreenShot + + if let newScreenShot { + viewModel.sendScreenshootFeedback( + feedback: .init( + feedbackType: reportType, + message: description, + screenshot: imageIsSelected ? .init(image: newScreenShot) : nil + ) + ) + } }, label: { Image(systemName: "location.fill") diff --git a/AppliverySDK/Applivery/Modules/Screenshoot/ViewModel/ScreenshootViewModel.swift b/AppliverySDK/Applivery/Modules/ScreenshotReport/ViewModel/ScreenshootViewModel.swift similarity index 51% rename from AppliverySDK/Applivery/Modules/Screenshoot/ViewModel/ScreenshootViewModel.swift rename to AppliverySDK/Applivery/Modules/ScreenshotReport/ViewModel/ScreenshootViewModel.swift index b701296..3a8cd3e 100644 --- a/AppliverySDK/Applivery/Modules/Screenshoot/ViewModel/ScreenshootViewModel.swift +++ b/AppliverySDK/Applivery/Modules/ScreenshotReport/ViewModel/ScreenshootViewModel.swift @@ -9,7 +9,28 @@ import Foundation import UIKit import SwiftUI -final class ScreenshootViewModel { +enum ReportRequestState { + case idle + case success + case failed + case loading + + var title: String? { + switch self { + case .idle, .loading: + nil + case .success: + "Feedback sended succesfully!" + case .failed: + "Feedback sended error" + } + } +} + +final class ScreenshootViewModel: ObservableObject { + + @Published var isReportSended: ReportRequestState = .idle + @Published var isAlertPresented: Bool = false private let feedbackService: PFeedbackService @@ -23,18 +44,24 @@ final class ScreenshootViewModel { self.feedbackService = feedbackService } + @MainActor func sendScreenshootFeedback(feedback: Feedback) { + isReportSended = .loading feedbackService.postFeedback(feedback) { result in switch result { - case .success(let successType): + case .success: + self.isReportSended = .success + self.isAlertPresented = true print("Feedback sended succesfully") - case .error(let errorType): + case .error: + self.isReportSended = .failed + self.isAlertPresented = true print("Feedback sended error:") } } } - func exportDrawing(image: UIImage, lines: [Line]) -> UIImage { + func exportDrawing(image: UIImage, lines: [Line]) -> UIImage? { // Create a context of the starting image size and set it as the current one UIGraphicsBeginImageContext(image.size) @@ -43,30 +70,32 @@ final class ScreenshootViewModel { image.draw(at: CGPoint.zero) // Get the current context - let context = UIGraphicsGetCurrentContext()! + let context = UIGraphicsGetCurrentContext() func toImagePoint(point: CGPoint) -> CGPoint { .init(x: point.x * image.size.width, y: point.y * image.size.height) } for line in lines { - context.setStrokeColor(line.color.cgColor ?? UIColor.systemPink.cgColor) - context.setLineWidth(line.lineWidth) + context?.setStrokeColor(line.color.cgColor ?? UIColor.systemPink.cgColor) + context?.setLineWidth(line.lineWidth) var points = line.points - let firstPoint = points.removeFirst() - - context.move(to: toImagePoint(point: firstPoint)) - for point in points { - context.addLine(to: toImagePoint(point: point)) + if !points.isEmpty { + let firstPoint = points.removeFirst() + + context?.move(to: toImagePoint(point: firstPoint)) + for point in points { + context?.addLine(to: toImagePoint(point: point)) + } + context?.strokePath() } - context.strokePath() } // Save the context as a new UIImage - let myImage = UIGraphicsGetImageFromCurrentImageContext() + let renderedImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() // Return modified image - return myImage! + return renderedImage } } diff --git a/AppliverySDK/Applivery/Modules/Screenshoot/Views/ColorPickerView.swift b/AppliverySDK/Applivery/Modules/ScreenshotReport/Views/ColorPickerView.swift similarity index 100% rename from AppliverySDK/Applivery/Modules/Screenshoot/Views/ColorPickerView.swift rename to AppliverySDK/Applivery/Modules/ScreenshotReport/Views/ColorPickerView.swift diff --git a/AppliverySDK/Applivery/Modules/Screenshoot/Views/EditScreenshotView.swift b/AppliverySDK/Applivery/Modules/ScreenshotReport/Views/EditScreenshotView.swift similarity index 94% rename from AppliverySDK/Applivery/Modules/Screenshoot/Views/EditScreenshotView.swift rename to AppliverySDK/Applivery/Modules/ScreenshotReport/Views/EditScreenshotView.swift index 1c2bc33..9621b59 100644 --- a/AppliverySDK/Applivery/Modules/Screenshoot/Views/EditScreenshotView.swift +++ b/AppliverySDK/Applivery/Modules/ScreenshotReport/Views/EditScreenshotView.swift @@ -167,15 +167,15 @@ struct EditScreenshotView: View { .padding(.horizontal, 100) } - func screenPoint(_ point: CGPoint) -> CGPoint { - // Convert 0->1 to view's coordinates - debugPrint("ViewSize \(viewSize)") - let vw = viewSize.width - let vh = viewSize.height - let nextX = min(1, max(0, point.x)) * vw - let nextY = min(1, max(0, point.y)) * vh - return CGPoint(x: nextX, y: nextY) - } +// func screenPoint(_ point: CGPoint) -> CGPoint { +// // Convert 0->1 to view's coordinates +// debugPrint("ViewSize \(viewSize)") +// let vw = viewSize.width +// let vh = viewSize.height +// let nextX = min(1, max(0, point.x)) * vw +// let nextY = min(1, max(0, point.y)) * vh +// return CGPoint(x: nextX, y: nextY) +// } func limitPoint(_ point: CGPoint) -> CGPoint { debugPrint("ViewSize \(viewSize)") diff --git a/AppliverySDK/Applivery/Modules/Screenshoot/Views/EmailTextFieldView.swift b/AppliverySDK/Applivery/Modules/ScreenshotReport/Views/EmailTextFieldView.swift similarity index 100% rename from AppliverySDK/Applivery/Modules/Screenshoot/Views/EmailTextFieldView.swift rename to AppliverySDK/Applivery/Modules/ScreenshotReport/Views/EmailTextFieldView.swift diff --git a/AppliverySDK/Applivery/Modules/Screenshoot/Views/ReportTypeView.swift b/AppliverySDK/Applivery/Modules/ScreenshotReport/Views/ReportTypeView.swift similarity index 100% rename from AppliverySDK/Applivery/Modules/Screenshoot/Views/ReportTypeView.swift rename to AppliverySDK/Applivery/Modules/ScreenshotReport/Views/ReportTypeView.swift diff --git a/AppliverySDK/Applivery/Modules/Screenshoot/Views/Rows/ScreenShootRowView.swift b/AppliverySDK/Applivery/Modules/ScreenshotReport/Views/Rows/ScreenShootRowView.swift similarity index 88% rename from AppliverySDK/Applivery/Modules/Screenshoot/Views/Rows/ScreenShootRowView.swift rename to AppliverySDK/Applivery/Modules/ScreenshotReport/Views/Rows/ScreenShootRowView.swift index 4b7a313..be91858 100644 --- a/AppliverySDK/Applivery/Modules/Screenshoot/Views/Rows/ScreenShootRowView.swift +++ b/AppliverySDK/Applivery/Modules/ScreenshotReport/Views/Rows/ScreenShootRowView.swift @@ -10,7 +10,7 @@ import SwiftUI struct ScreenShootRowView: View { @Binding var image: UIImage? @Binding var lines: [Line] - @State var isSelected: Bool = true + @Binding var isSelected: Bool @State var editScreenshootSheetIsPresented = false var body: some View { @@ -55,5 +55,9 @@ struct ScreenShootRowView: View { } #Preview { - ScreenShootRowView(image: .constant(UIImage(systemName: "checkmark.circle.fill")), lines: .constant([])) + ScreenShootRowView( + image: .constant(UIImage(systemName: "checkmark.circle.fill")), + lines: .constant([]), + isSelected: .constant(false) + ) } diff --git a/AppliverySDK/Applivery/Modules/VideoReport/TimedButton.swift b/AppliverySDK/Applivery/Modules/VideoReport/TimedButton.swift new file mode 100644 index 0000000..d2e03f9 --- /dev/null +++ b/AppliverySDK/Applivery/Modules/VideoReport/TimedButton.swift @@ -0,0 +1,31 @@ +// +// TimedButton.swift +// +// +// Created by Fran Alarza on 16/9/24. +// + +import SwiftUI + +struct TimedButton: View { + let action: () -> Void + + var body: some View { + Button { + action() + } label: { + Image(systemName: "play.circle.fill") + .resizable() + .frame(width: 60, height: 60) + } + .clipShape(Circle()) + .overlay { + Circle() + .stroke(lineWidth: 4) + } + } +} + +#Preview { + TimedButton(action: {}) +} diff --git a/AppliverySDK/Applivery/Modules/Screenshoot/Views/Rows/VideoPreviewRow.swift b/AppliverySDK/Applivery/Modules/VideoReport/VideoPreviewRow.swift similarity index 100% rename from AppliverySDK/Applivery/Modules/Screenshoot/Views/Rows/VideoPreviewRow.swift rename to AppliverySDK/Applivery/Modules/VideoReport/VideoPreviewRow.swift diff --git a/AppliverySDK/Applivery/Modules/Screenshoot/Screens/VideoPreviewScreen.swift b/AppliverySDK/Applivery/Modules/VideoReport/VideoPreviewScreen.swift similarity index 100% rename from AppliverySDK/Applivery/Modules/Screenshoot/Screens/VideoPreviewScreen.swift rename to AppliverySDK/Applivery/Modules/VideoReport/VideoPreviewScreen.swift diff --git a/AppliverySDK/Applivery/Modules/Screenshoot/Views/ViedeoPlayerView.swift b/AppliverySDK/Applivery/Modules/VideoReport/ViedeoPlayerView.swift similarity index 100% rename from AppliverySDK/Applivery/Modules/Screenshoot/Views/ViedeoPlayerView.swift rename to AppliverySDK/Applivery/Modules/VideoReport/ViedeoPlayerView.swift diff --git a/AppliverySDK/Applivery/Views/RecordingViewController.swift b/AppliverySDK/Applivery/Views/RecordingViewController.swift index 340f8b4..67528e4 100644 --- a/AppliverySDK/Applivery/Views/RecordingViewController.swift +++ b/AppliverySDK/Applivery/Views/RecordingViewController.swift @@ -10,11 +10,11 @@ import UIKit class RecordingViewController: UIViewController { private var recordButton: UIButton = UIButton(type: .system) + private let borderLayer = CAShapeLayer() private var actionSheet: UIAlertController = UIAlertController() var buttonAction: (() -> Void)? private var feedbackCoordinator: PFeedbackCoordinator - private let shapeLayer = CAShapeLayer() init(feedbackCoordinator: PFeedbackCoordinator) { self.feedbackCoordinator = feedbackCoordinator @@ -29,7 +29,16 @@ class RecordingViewController: UIViewController { super.viewDidLoad() view.backgroundColor = .clear addRecordButton() - setupProgressBorder() + } + + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + recordButton.layer.cornerRadius = recordButton.bounds.size.width / 2 + + borderLayer.frame = recordButton.bounds + borderLayer.path = UIBezierPath(ovalIn: borderLayer.bounds).cgPath } deinit { @@ -41,23 +50,43 @@ class RecordingViewController: UIViewController { } private func addRecordButton() { - let configuration = UIImage.SymbolConfiguration(pointSize: 32, weight: .heavy) + let configuration = UIImage.SymbolConfiguration(pointSize: 48, weight: .heavy) let symbolImage = UIImage(systemName: "stop.circle.fill", withConfiguration: configuration) recordButton.setImage(symbolImage, for: .normal) recordButton.tintColor = .white recordButton.addTarget(self, action: #selector(recordButtonTapped), for: .touchUpInside) recordButton.translatesAutoresizingMaskIntoConstraints = false + recordButton.clipsToBounds = true recordButton.isHidden = true view.addSubview(self.recordButton) - let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) - recordButton.addGestureRecognizer(panGesture) - + // Set up constraints NSLayoutConstraint.activate([ - recordButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), - recordButton.centerYAnchor.constraint(equalTo: view.centerYAnchor) + recordButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 48), + recordButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -48), + recordButton.widthAnchor.constraint(equalToConstant: 48), + recordButton.heightAnchor.constraint(equalToConstant: 48), ]) + + borderLayer.strokeColor = UIColor.red.cgColor + borderLayer.lineWidth = 8 + borderLayer.fillColor = nil + + recordButton.layer.addSublayer(borderLayer) + + let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) + recordButton.addGestureRecognizer(panGesture) + } + + private func animateBorder() { + let animation = CABasicAnimation(keyPath: "strokeEnd") + animation.duration = 30 + animation.fromValue = 0 + animation.toValue = 1 + animation.timingFunction = CAMediaTimingFunction(name: .linear) + + borderLayer.add(animation, forKey: "borderAnimation") } @objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) { @@ -70,44 +99,9 @@ class RecordingViewController: UIViewController { gesture.setTranslation(.zero, in: view) } - private func setupProgressBorder() { - let circularPath = UIBezierPath( - arcCenter: .init( - x: recordButton.frame.midX, - y: recordButton.frame.midY - ), - radius: recordButton.bounds.width + 10, - startAngle: -CGFloat.pi / 2, - endAngle: 1.5 * CGFloat.pi, - clockwise: true - ) - - shapeLayer.path = circularPath.cgPath - shapeLayer.strokeColor = UIColor.red.cgColor - shapeLayer.lineWidth = 4 - shapeLayer.fillColor = UIColor.clear.cgColor - shapeLayer.lineCap = .round - shapeLayer.strokeEnd = 0 - - //view.layer.addSublayer(shapeLayer) - - } - - private func startProgressAnimation() { - shapeLayer.strokeEnd = 0 - - let basicAnimation = CABasicAnimation(keyPath: "strokeEnd") - basicAnimation.toValue = 1 - basicAnimation.duration = 30 - basicAnimation.fillMode = .forwards - basicAnimation.isRemovedOnCompletion = false - - shapeLayer.add(basicAnimation, forKey: "progressAnimation") - } - func showRecordButton() { self.recordButton.isHidden = false - startProgressAnimation() + animateBorder() } func hideRecordButton() { diff --git a/AppliverySDK/Applivery/Wrappers/App.swift b/AppliverySDK/Applivery/Wrappers/App.swift index 39f129b..340b3b1 100644 --- a/AppliverySDK/Applivery/Wrappers/App.swift +++ b/AppliverySDK/Applivery/Wrappers/App.swift @@ -156,7 +156,7 @@ class App: AppProtocol { } func presentModal(_ viewController: UIViewController, animated: Bool) { - viewController.modalPresentationStyle = .overFullScreen + viewController.modalPresentationStyle = .fullScreen let topVC = self.topViewController() topVC?.present(viewController, animated: animated, completion: nil) } @@ -216,14 +216,20 @@ class App: AppProtocol { } private func topViewController() -> UIViewController? { - var rootVC = UIApplication.shared - .keyWindow? - .rootViewController - while let presentedController = rootVC?.presentedViewController { - rootVC = presentedController - } - - return rootVC + + if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = scene.windows.first { + + if var topController = window.rootViewController { + while let presentedController = topController.presentedViewController { + topController = presentedController + } + + return topController + } + } + + return nil } } diff --git a/AppliverySDK/Applivery/Wrappers/ScreenRecorderManager.swift b/AppliverySDK/Applivery/Wrappers/ScreenRecorderManager.swift index 35aa36b..3bd772e 100644 --- a/AppliverySDK/Applivery/Wrappers/ScreenRecorderManager.swift +++ b/AppliverySDK/Applivery/Wrappers/ScreenRecorderManager.swift @@ -15,6 +15,7 @@ public final class ScreenRecorderManager: NSObject, RPScreenRecorderDelegate, RP public static let shared = ScreenRecorderManager() private let recorder = RPScreenRecorder.shared() + private var timer: Timer? var isRecording = false private var isShowingSheet = false @@ -22,9 +23,13 @@ public final class ScreenRecorderManager: NSObject, RPScreenRecorderDelegate, RP var recordViewController: RecordingViewController? - init(recordViewController: RecordingViewController? = RecordingViewController(feedbackCoordinator: FeedbackCoordinator())) { + init( + recordViewController: RecordingViewController? = RecordingViewController(feedbackCoordinator: FeedbackCoordinator()), + timer: Timer? = nil + ) { super.init() self.recordViewController = recordViewController + self.timer = timer recorder.delegate = self } @@ -33,19 +38,20 @@ public final class ScreenRecorderManager: NSObject, RPScreenRecorderDelegate, RP } func startClipBuffering() { - recorder.startClipBuffering { (error) in + recorder.startClipBuffering { [weak self] (error) in if error != nil { - print("Error attempting to start Clip Buffering: \(error?.localizedDescription)") + print("Error attempting to start Clip Buffering: \(String(describing: error?.localizedDescription))") } else { - self.isRecording = true - self.recordViewController?.showRecordButton() + self?.isRecording = true + self?.setTimer() + self?.recordViewController?.showRecordButton() } } } func exportClip() async { let clipURL = getDirectory() - let interval = TimeInterval(15) + let interval = TimeInterval(30) print("Generating clip at URL: ", clipURL) do { @@ -67,6 +73,8 @@ public final class ScreenRecorderManager: NSObject, RPScreenRecorderDelegate, RP await exportClip() try await recorder.stopClipBuffering() await self.recordViewController?.hideRecordButton() + self.timer?.invalidate() + self.timer = nil } catch { print("Error attempting to stop Clip Buffering: \(error.localizedDescription)") } @@ -87,7 +95,7 @@ public final class ScreenRecorderManager: NSObject, RPScreenRecorderDelegate, RP DispatchQueue.main.async { let view = ScreenshootPreviewScreen(screenshot: screenshot) let hosting = UIHostingController(rootView: view) - hosting.modalPresentationStyle = .fullScreen + hosting.modalPresentationStyle = .overFullScreen topController.present(hosting, animated: true) } } @@ -106,12 +114,18 @@ public final class ScreenRecorderManager: NSObject, RPScreenRecorderDelegate, RP DispatchQueue.main.async { let view = VideoPreviewScreen(url: clipURL) let hosting = UIHostingController(rootView: view) - hosting.modalPresentationStyle = .fullScreen + hosting.modalPresentationStyle = .overFullScreen topController.present(hosting, animated: true) } } } } + + func setTimer() { + self.timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: false, block: { [weak self] _ in + self?.stopClipBuffering() + }) + } } private extension ScreenRecorderManager {