구간반복 기능 추가
This commit is contained in:
		
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_loop_segment_active.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_loop_segment_active.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "ic_loop_segment_active.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_loop_segment_active.imageset/ic_loop_segment_active.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_loop_segment_active.imageset/ic_loop_segment_active.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.1 KiB | 
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_loop_segment_idle.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_loop_segment_idle.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "ic_loop_segment_idle.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_loop_segment_idle.imageset/ic_loop_segment_idle.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_loop_segment_idle.imageset/ic_loop_segment_idle.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.0 KiB | 
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_loop_segment_start_set.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_loop_segment_start_set.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "ic_loop_segment_start_set.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_loop_segment_start_set.imageset/ic_loop_segment_start_set.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_loop_segment_start_set.imageset/ic_loop_segment_start_set.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.1 KiB | 
| @@ -13,6 +13,12 @@ import Combine | ||||
| import Kingfisher | ||||
|  | ||||
| final class ContentPlayerPlayManager: NSObject, ObservableObject { | ||||
|     enum LoopState { | ||||
|         case idle | ||||
|         case waitingForEnd(start: CMTime) | ||||
|         case looping(start: CMTime, end: CMTime) | ||||
|     } | ||||
|      | ||||
|     static let shared = ContentPlayerPlayManager() | ||||
|      | ||||
|     private let repository = ContentGenerateUrlRepository() | ||||
| @@ -23,8 +29,8 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject { | ||||
|     @Published var coverImageUrl = "" | ||||
|     @Published var creatorProfileUrl = "" | ||||
|      | ||||
|     @Published private (set) var isShowingMiniPlayer = false | ||||
|     @Published private (set) var isPlaying = false | ||||
|     @Published private(set) var isShowingMiniPlayer = false | ||||
|     @Published private(set) var isPlaying = false | ||||
|      | ||||
|     @Published var isLoading = false | ||||
|     @Published var errorMessage = "" | ||||
| @@ -32,9 +38,12 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject { | ||||
|     @Published var isShowPlaylist = false | ||||
|     @Published var playlist: [AudioContentPlaylistContent] = [] | ||||
|     @Published var currentTime: Double = 0.0 | ||||
|     @Published private (set) var duration: Double = 0.0 | ||||
|     @Published private(set) var duration: Double = 0.0 | ||||
|     @Published var isEditing = false | ||||
|      | ||||
|     @Published private(set) var loopState: LoopState = .idle | ||||
|     @Published private var timeObserver: Any? | ||||
|      | ||||
|     var player: AVPlayer? | ||||
|     private var cancellables = Set<AnyCancellable>() | ||||
|      | ||||
| @@ -70,6 +79,8 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject { | ||||
|     } | ||||
|      | ||||
|     private func setupPlayer(with url: URL) { | ||||
|         stopLooping() | ||||
|         loopState = .idle | ||||
|         // 기존 playerItem 관련 Combine 구독 해제 | ||||
|         cancellables.removeAll() | ||||
|          | ||||
| @@ -152,6 +163,8 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject { | ||||
|      | ||||
|     func playNextContent() { | ||||
|         stop() | ||||
|         stopLooping() | ||||
|         loopState = .idle | ||||
|          | ||||
|         if let content = playlistManager?.moveToNext() { | ||||
|             generateUrl(contentId: content.id) { [unowned self] url in | ||||
| @@ -164,6 +177,8 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject { | ||||
|      | ||||
|     func playPreviousContent() { | ||||
|         stop() | ||||
|         stopLooping() | ||||
|         loopState = .idle | ||||
|          | ||||
|         if let content = playlistManager?.moveToPrevious() { | ||||
|             generateUrl(contentId: content.id) { [unowned self] url in | ||||
| @@ -213,6 +228,8 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject { | ||||
|      | ||||
|     func resetPlayer() { | ||||
|         stop() | ||||
|         stopLooping() | ||||
|         loopState = .idle | ||||
|         duration = 0 | ||||
|         isShowingMiniPlayer = false | ||||
|          | ||||
| @@ -227,6 +244,8 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject { | ||||
|     } | ||||
|      | ||||
|     func seek(to time: Double) { | ||||
|         stopLooping() | ||||
|         loopState = .idle | ||||
|         let cmTime = CMTime(seconds: time, preferredTimescale: 600) | ||||
|         player?.seek(to: cmTime) | ||||
|     } | ||||
| @@ -244,6 +263,41 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject { | ||||
|         seek(to: min(newTimeInSeconds, durationInSeconds)) | ||||
|     } | ||||
|      | ||||
|     func toggleLoop() { | ||||
|         guard let currentTime = player?.currentTime() else { return } | ||||
|          | ||||
|         switch loopState { | ||||
|         case .idle: | ||||
|             loopState = .waitingForEnd(start: currentTime) | ||||
|              | ||||
|         case .waitingForEnd(let start): | ||||
|             let end = currentTime | ||||
|             loopState = .looping(start: start, end: end) | ||||
|             startLooping(from: start, to: end) | ||||
|              | ||||
|         case .looping: | ||||
|             stopLooping() | ||||
|             loopState = .idle | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func startLooping(from start: CMTime, to end: CMTime) { | ||||
|         let interval = CMTime(seconds: 0.1, preferredTimescale: 600) | ||||
|          | ||||
|         timeObserver = player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] currentTime in | ||||
|             if currentTime >= end { | ||||
|                 self?.player?.seek(to: start, toleranceBefore: .zero, toleranceAfter: .zero) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func stopLooping() { | ||||
|         if let observer = timeObserver { | ||||
|             player?.removeTimeObserver(observer) | ||||
|             timeObserver = nil | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func generateUrl(contentId: Int, onSuccess: @escaping (String) -> Void, onFailure: @escaping () -> Void) { | ||||
|         if contentId < 0 { | ||||
|             onFailure() | ||||
|   | ||||
| @@ -163,6 +163,21 @@ struct ContentPlayerView: View { | ||||
|                 .padding(.vertical, 21) | ||||
|                  | ||||
|                 HStack(spacing: 0) { | ||||
|                     Image({ | ||||
|                         switch playerManager.loopState { | ||||
|                         case .waitingForEnd: | ||||
|                             "ic_loop_segment_start_set" | ||||
|                              | ||||
|                         case .looping: | ||||
|                             "ic_loop_segment_active" | ||||
|                              | ||||
|                         default: | ||||
|                             "ic_loop_segment_idle" | ||||
|                         } | ||||
|                     }()) | ||||
|                     .padding(5) | ||||
|                     .onTapGesture { playerManager.toggleLoop() } | ||||
|                      | ||||
|                     Spacer() | ||||
|                      | ||||
|                     Image("ic_playlist") | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Yu Sung
					Yu Sung