@@ -12,10 +12,12 @@ import UIKit
1212import Photos
1313import CoreImage
1414import CoreImage. CIFilterBuiltins
15+ import SwiftUI
1516
1617final class CameraViewModel : NSObject , ObservableObject {
1718 @Published var isAuthorized = false
1819 @Published var isSessionRunning = false
20+ @Published var isCameraReady = false
1921 @Published var isRecording = false
2022 @Published var frameRateLabel : String = " 60 fps "
2123 @Published var quickZoomIndex : Int = 1 // 0 -> 0.5x, 1 -> 1x, 2 -> 2x
@@ -28,6 +30,7 @@ final class CameraViewModel: NSObject, ObservableObject {
2830 @Published var teleprompterText : String = " Adicione seu roteiro aqui... "
2931 @Published var teleprompterSpeed : Double = 16.0 // points per second (default slower)
3032 @Published var teleprompterFontSize : CGFloat = 24.0 // default smaller
33+ @Published var isProcessingVideo : Bool = false
3134
3235 let controller = CaptureSessionController ( )
3336 private var recorder : SegmentedRecorder ?
@@ -38,17 +41,45 @@ final class CameraViewModel: NSObject, ObservableObject {
3841
3942 override init ( ) {
4043 super. init ( )
44+ loadTeleprompterSettingsFromUserDefaults ( )
45+ }
46+
47+ /// Load teleprompter text from UserDefaults (passed from Flutter)
48+ /// Auto-enables teleprompter if valid script is present
49+ private func loadTeleprompterSettingsFromUserDefaults( ) {
50+ if let savedText = UserDefaults . standard. string ( forKey: " teleprompter_initial_text " ) ,
51+ !savedText. isEmpty,
52+ savedText != " Adicione seu roteiro aqui... " {
53+ teleprompterText = savedText
54+ // Auto-enable teleprompter when a script is provided
55+ isTeleprompterOn = true
56+ }
4157 }
4258
4359 deinit {
44- // Ensure brightness is restored if we used screen torch
45- setScreenTorchEnabled ( false )
60+ // Stop recording if active
61+ if let recorder, recorder. isRecording {
62+ recorder. stopCurrentSegment ( )
63+ }
4664
47- // Remove orientation observer
48- if let observer = orientationObserver {
49- NotificationCenter . default. removeObserver ( observer)
65+ // Stop the camera session
66+ controller. stopSession ( )
67+
68+ // Restore screen brightness if we modified it
69+ if let originalBrightness = savedScreenBrightness {
70+ let brightness = originalBrightness
71+ DispatchQueue . main. async {
72+ UIScreen . main. brightness = brightness
73+ }
74+ }
75+
76+ // Clean up temporary segment files in background
77+ let segmentURLs = segments. map { $0. url }
78+ DispatchQueue . global ( qos: . background) . async {
79+ for url in segmentURLs {
80+ try ? FileManager . default. removeItem ( at: url)
81+ }
5082 }
51- UIDevice . current. endGeneratingDeviceOrientationNotifications ( )
5283 }
5384
5485 // MARK: - Grid
@@ -75,6 +106,12 @@ final class CameraViewModel: NSObject, ObservableObject {
75106 func start( ) {
76107 controller. startSession ( )
77108 isSessionRunning = true
109+ // Small delay to ensure session is fully running before showing preview
110+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.15 ) { [ weak self] in
111+ withAnimation ( . easeOut( duration: 0.3 ) ) {
112+ self ? . isCameraReady = true
113+ }
114+ }
78115 }
79116
80117 // MARK: - Filter
@@ -104,6 +141,13 @@ final class CameraViewModel: NSObject, ObservableObject {
104141 setScreenTorchEnabled ( false )
105142 }
106143
144+ /// Safely dismisses the camera and notifies Flutter of cancellation.
145+ func dismissCamera( ) {
146+ // Notify Flutter immediately - let the plugin handle the dismiss
147+ // This must happen BEFORE we start any cleanup to avoid race conditions
148+ NotificationCenter . default. post ( name: Notification . Name ( " conty.teleprompter.cancelled " ) , object: nil )
149+ }
150+
107151 func attachPreview( _ view: CameraPreviewView ) {
108152 view. attach ( session: controller. session)
109153 view. onTapToFocus = { [ weak self] devicePoint in
@@ -285,13 +329,16 @@ extension CameraViewModel {
285329 try ? FileManager . default. removeItem ( at: url)
286330 }
287331 }
288-
332+
289333 func nextAction( ) {
290334 print ( " Next pressed with \( segments. count) takes " )
291335
292336 guard !segments. isEmpty else { return }
337+ guard !isProcessingVideo else { return }
338+
339+ isProcessingVideo = true
293340
294- // Concatenate all segments into a single video and save to gallery
341+ // Concatenate all segments into a single video and notify Flutter
295342 concatenateAndSaveSegments ( )
296343 }
297344
@@ -353,10 +400,13 @@ extension CameraViewModel {
353400 private func exportComposition( _ composition: AVMutableComposition ) {
354401 guard let exporter = AVAssetExportSession ( asset: composition, presetName: AVAssetExportPresetHighestQuality) else {
355402 print ( " Failed to create export session " )
403+ DispatchQueue . main. async { self . isProcessingVideo = false }
356404 return
357405 }
358406
359- let outputURL = URL ( fileURLWithPath: NSTemporaryDirectory ( ) ) . appendingPathComponent ( " final_video_ \( UUID ( ) . uuidString) .mov " )
407+ // Use app documents directory so file persists for Flutter to use
408+ let docsDir = FileManager . default. urls ( for: . documentDirectory, in: . userDomainMask) . first!
409+ let outputURL = docsDir. appendingPathComponent ( " final_video_ \( UUID ( ) . uuidString) .mov " )
360410 exporter. outputURL = outputURL
361411 exporter. outputFileType = . mov
362412
@@ -370,71 +420,61 @@ extension CameraViewModel {
370420 case . completed:
371421 print ( " Video concatenation completed successfully " )
372422 print ( " Output URL: \( outputURL) " )
373- self . saveVideoToPhotos ( outputURL)
374-
375- // CLEANUP segments ONLY after successful export
376- for segment in self . segments {
377- try ? FileManager . default. removeItem ( at: segment. url)
378- }
379- DispatchQueue . main. async {
380- self . segments. removeAll ( )
381- }
382-
383- // Clean up temp file after a delay to ensure save completes
384- DispatchQueue . main. asyncAfter ( deadline: . now( ) + 2.0 ) {
385- try ? FileManager . default. removeItem ( at: outputURL)
386- }
423+ self . finishWithVideo ( at: outputURL)
387424 case . failed, . cancelled:
388425 print ( " Video concatenation failed: \( exporter. error? . localizedDescription ?? " Unknown error " ) " )
389426 if let error = exporter. error {
390427 print ( " Export error details: \( error) " )
391428 }
429+ DispatchQueue . main. async { self . isProcessingVideo = false }
392430 default :
393431 print ( " Export in progress or unknown status " )
394432 break
395433 }
396434 }
397435 } else {
398436 print ( " Applying filter ( \( selectedFilter. displayName) ) to concatenated video " )
399- applyFilter ( selectedFilter, to: composition) { result in
437+ applyFilter ( selectedFilter, to: composition) { [ weak self] result in
438+ guard let self = self else { return }
400439 switch result {
401440 case . success( let filteredURL) :
402- print ( " Filter applied successfully, saving filtered video " )
403- self . saveVideoToPhotos ( filteredURL)
404-
405- // CLEANUP segments ONLY after successful filtered export
406- for segment in self . segments {
407- try ? FileManager . default. removeItem ( at: segment. url)
408- }
409- DispatchQueue . main. async {
410- self . segments. removeAll ( )
411- }
412-
413- // Clean up temp files after a delay
414- DispatchQueue . main. asyncAfter ( deadline: . now( ) + 2.0 ) {
415- try ? FileManager . default. removeItem ( at: filteredURL)
416- try ? FileManager . default. removeItem ( at: outputURL)
417- }
441+ print ( " Filter applied successfully " )
442+ self . finishWithVideo ( at: filteredURL)
443+ // Clean up unfiltered temp file
444+ try ? FileManager . default. removeItem ( at: outputURL)
418445 case . failure( let error) :
419446 print ( " Filter application failed: \( error) " )
420- print ( " Saving unfiltered video instead " )
421- self . saveVideoToPhotos ( outputURL)
422-
423- // Even on failure, if we are saving the raw one, we cleanup segments after saving
424- for segment in self . segments {
425- try ? FileManager . default. removeItem ( at: segment. url)
426- }
427- DispatchQueue . main. async {
428- self . segments. removeAll ( )
429- }
430-
431- // Clean up temp file after a delay
432- DispatchQueue . main. asyncAfter ( deadline: . now( ) + 2.0 ) {
433- try ? FileManager . default. removeItem ( at: outputURL)
434- }
447+ print ( " Using unfiltered video instead " )
448+ self . finishWithVideo ( at: outputURL)
435449 }
436450 }
437451 }
452+
453+ // Clean up individual segment files
454+ for segment in segments {
455+ try ? FileManager . default. removeItem ( at: segment. url)
456+ }
457+
458+ // Clear segments after processing
459+ DispatchQueue . main. async {
460+ self . segments. removeAll ( )
461+ }
462+ }
463+
464+ /// Finishes the recording flow by notifying Flutter with the video path
465+ private func finishWithVideo( at url: URL ) {
466+ // Stop the camera session
467+ stop ( )
468+
469+ // Notify Flutter with the video path
470+ DispatchQueue . main. async {
471+ self . isProcessingVideo = false
472+ NotificationCenter . default. post (
473+ name: Notification . Name ( " conty.teleprompter.finished " ) ,
474+ object: nil ,
475+ userInfo: [ " path " : url. path, " savedToGallery " : false ]
476+ )
477+ }
438478 }
439479
440480 private func applyFilter( _ filterType: VideoFilter , to composition: AVMutableComposition , completion: @escaping ( Result < URL , Error > ) -> Void ) {
0 commit comments