diff --git a/IndieMusic/IndieMusic.xcodeproj/project.pbxproj b/IndieMusic/IndieMusic.xcodeproj/project.pbxproj index 015df65..8959ec4 100644 --- a/IndieMusic/IndieMusic.xcodeproj/project.pbxproj +++ b/IndieMusic/IndieMusic.xcodeproj/project.pbxproj @@ -20,6 +20,11 @@ 770228F82B5A224500E07F7A /* NSLayoutConstraint+Multiplier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 770228F52B5A224500E07F7A /* NSLayoutConstraint+Multiplier.swift */; }; 770228F92B5A224500E07F7A /* SliderControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 770228F62B5A224500E07F7A /* SliderControl.swift */; }; 770F6CA22B64F1DE0082F53A /* Carousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 770F6CA12B64F1DE0082F53A /* Carousel.swift */; }; + 770FC3EF2B92C8130023DE28 /* Array+IndieMusic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 770FC3EE2B92C8130023DE28 /* Array+IndieMusic.swift */; }; + 770FC3F52B92F4380023DE28 /* AVAsynchronousKeyValueLoading+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 770FC3F12B92F4380023DE28 /* AVAsynchronousKeyValueLoading+Rx.swift */; }; + 770FC3F62B92F4380023DE28 /* AVPlayer+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 770FC3F22B92F4380023DE28 /* AVPlayer+Rx.swift */; }; + 770FC3F72B92F4380023DE28 /* AVPlayerItem+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 770FC3F32B92F4380023DE28 /* AVPlayerItem+Rx.swift */; }; + 770FC3F82B92F4380023DE28 /* AVPlayerLayer+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 770FC3F42B92F4380023DE28 /* AVPlayerLayer+Rx.swift */; }; 771233EC2B6B6476009FAF01 /* UIButton+IndieMusic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771233EB2B6B6476009FAF01 /* UIButton+IndieMusic.swift */; }; 771233EE2B6B8CE6009FAF01 /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771233ED2B6B8CE6009FAF01 /* NavigationBar.swift */; }; 77165D742B464493002AE0A5 /* BarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77165D732B464493002AE0A5 /* BarButtonItem.swift */; }; @@ -283,6 +288,11 @@ 770228F52B5A224500E07F7A /* NSLayoutConstraint+Multiplier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSLayoutConstraint+Multiplier.swift"; sourceTree = ""; }; 770228F62B5A224500E07F7A /* SliderControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderControl.swift; sourceTree = ""; }; 770F6CA12B64F1DE0082F53A /* Carousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Carousel.swift; sourceTree = ""; }; + 770FC3EE2B92C8130023DE28 /* Array+IndieMusic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+IndieMusic.swift"; sourceTree = ""; }; + 770FC3F12B92F4380023DE28 /* AVAsynchronousKeyValueLoading+Rx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVAsynchronousKeyValueLoading+Rx.swift"; sourceTree = ""; }; + 770FC3F22B92F4380023DE28 /* AVPlayer+Rx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVPlayer+Rx.swift"; sourceTree = ""; }; + 770FC3F32B92F4380023DE28 /* AVPlayerItem+Rx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVPlayerItem+Rx.swift"; sourceTree = ""; }; + 770FC3F42B92F4380023DE28 /* AVPlayerLayer+Rx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVPlayerLayer+Rx.swift"; sourceTree = ""; }; 771233EB2B6B6476009FAF01 /* UIButton+IndieMusic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+IndieMusic.swift"; sourceTree = ""; }; 771233ED2B6B8CE6009FAF01 /* NavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBar.swift; sourceTree = ""; }; 77165D732B464493002AE0A5 /* BarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarButtonItem.swift; sourceTree = ""; }; @@ -577,6 +587,17 @@ path = Combine; sourceTree = ""; }; + 770FC3F02B92F42D0023DE28 /* RxAVFoundation */ = { + isa = PBXGroup; + children = ( + 770FC3F12B92F4380023DE28 /* AVAsynchronousKeyValueLoading+Rx.swift */, + 770FC3F22B92F4380023DE28 /* AVPlayer+Rx.swift */, + 770FC3F32B92F4380023DE28 /* AVPlayerItem+Rx.swift */, + 770FC3F42B92F4380023DE28 /* AVPlayerLayer+Rx.swift */, + ); + path = RxAVFoundation; + sourceTree = ""; + }; 7743999C2AFA18B0006F8EEA /* Player */ = { isa = PBXGroup; children = ( @@ -821,6 +842,7 @@ 77AC35742B6E3D8300D046C2 /* UIView+IndieMusic.swift */, 7751D3852B45409000F1F2BD /* NSNotification+IndieMusic.swift */, 7751009B2B62065800F46109 /* Date+IndieMusic.swift */, + 770FC3EE2B92C8130023DE28 /* Array+IndieMusic.swift */, ); path = Extensions; sourceTree = ""; @@ -910,6 +932,7 @@ 778B8A5A2AF8EAFD0034AFD4 /* Third Party */ = { isa = PBXGroup; children = ( + 770FC3F02B92F42D0023DE28 /* RxAVFoundation */, 775D07562B5E47C4009270D3 /* ETPopupView */, 77A60D7A2B5B975600D4E435 /* ResourceLoader */, 770228F22B5A224500E07F7A /* SliderControl */, @@ -1442,6 +1465,7 @@ 77C9B9DF2B4BCE300006C83F /* Message.swift in Sources */, 77FA0B2C2B0C480B00404C5E /* AudioMoreActionViewModel.swift in Sources */, 778B8AC22AF8ED280034AFD4 /* TableViewController.swift in Sources */, + 770FC3F52B92F4380023DE28 /* AVAsynchronousKeyValueLoading+Rx.swift in Sources */, 77620D8D2B68844E00798861 /* Followers.swift in Sources */, 77FA0B562B0F4ABF00404C5E /* MineSingleController.swift in Sources */, 778B8A9D2AF8ECFC0034AFD4 /* LibsManager.swift in Sources */, @@ -1504,6 +1528,7 @@ 778B8A862AF8ECE50034AFD4 /* SearchViewModel.swift in Sources */, 77C9B9DD2B4BC5D20006C83F /* FollowersViewModel.swift in Sources */, 778B8A802AF8ECE50034AFD4 /* MineViewController.swift in Sources */, + 770FC3EF2B92C8130023DE28 /* Array+IndieMusic.swift in Sources */, 778B8A8F2AF8ECF20034AFD4 /* EmptyModel.swift in Sources */, 778B8ABD2AF8ED280034AFD4 /* View.swift in Sources */, 778B8A912AF8ECF20034AFD4 /* Album.swift in Sources */, @@ -1570,6 +1595,7 @@ 7751D3762B43E8D200F1F2BD /* MusicStyle.swift in Sources */, 77C9B9D72B4BBD780006C83F /* FollowingViewModel.swift in Sources */, 7736FF442B4CECF2008D5DAD /* CommentViewModel.swift in Sources */, + 770FC3F62B92F4380023DE28 /* AVPlayer+Rx.swift in Sources */, 77620D962B68E96C00798861 /* DateItem.swift in Sources */, 778B8AA92AF8ED0E0034AFD4 /* UIImage+IndieMusic.swift in Sources */, 77C9B9D12B4B99600006C83F /* BadgeButton.swift in Sources */, @@ -1596,6 +1622,7 @@ 7751D37C2B4516BE00F1F2BD /* UILabel+IndieMusic.swift in Sources */, 778B8AAC2AF8ED0E0034AFD4 /* UIViewController+Rx.swift in Sources */, 778B8A8D2AF8ECF20034AFD4 /* APIImage.swift in Sources */, + 770FC3F72B92F4380023DE28 /* AVPlayerItem+Rx.swift in Sources */, 770228F92B5A224500E07F7A /* SliderControl.swift in Sources */, 7751D36A2B42ED6C00F1F2BD /* TimingViewController.swift in Sources */, 77FA0B462B0DFAFD00404C5E /* BindPhoneViewController.swift in Sources */, @@ -1610,6 +1637,7 @@ 77FA0B3E2B0C573600404C5E /* Filter.swift in Sources */, 77165D742B464493002AE0A5 /* BarButtonItem.swift in Sources */, 77CEFEFE2B82F18A0071B671 /* CommentToolView.swift in Sources */, + 770FC3F82B92F4380023DE28 /* AVPlayerLayer+Rx.swift in Sources */, 7751D3662B42BE7F00F1F2BD /* FeedbackViewController.swift in Sources */, 77A60D822B5B97A100D4E435 /* AssetDataManager.swift in Sources */, 77FB7A7B2B4A4FC900B64030 /* MessageViewController.swift in Sources */, diff --git a/IndieMusic/IndieMusic/Application/Application.swift b/IndieMusic/IndieMusic/Application/Application.swift index 7f124d3..6dfb59a 100644 --- a/IndieMusic/IndieMusic/Application/Application.swift +++ b/IndieMusic/IndieMusic/Application/Application.swift @@ -55,7 +55,7 @@ final class Application: NSObject { if let currentTrackData = PINCache.shared.object(forKey: "CurrentTrack") as? Data, let currentTrack = try? JSONDecoder().decode(AudioTrack.self, from: currentTrackData) { - AudioManager.sharedInstance.currentTrack = currentTrack + AudioManager.sharedInstance.currentTrack.accept(currentTrack) } diff --git a/IndieMusic/IndieMusic/Extensions/Array+IndieMusic.swift b/IndieMusic/IndieMusic/Extensions/Array+IndieMusic.swift new file mode 100644 index 0000000..f8a4600 --- /dev/null +++ b/IndieMusic/IndieMusic/Extensions/Array+IndieMusic.swift @@ -0,0 +1,49 @@ +// +// Array+IndieMusic.swift +// IndieMusic +// +// Created by WenLei on 2024/3/2. +// + +import Foundation + +extension Array where Element == AudioTrack { + mutating func updateTracks(by trackId: String, with properties: [PartialKeyPath: Any]) { + guard let index = self.firstIndex(where: { $0.id == trackId }) else { return } + + for (keyPath, newValue) in properties { + if let keyPath = keyPath as? WritableKeyPath, let newValue = newValue as? Bool? { + self[index][keyPath: keyPath] = newValue + } else if let keyPath = keyPath as? WritableKeyPath, let newValue = newValue as? String? { + self[index][keyPath: keyPath] = newValue + } + } + } + + mutating func replaceOrAdd(track newTrack: AudioTrack) { + if let index = self.firstIndex(where: { $0.id == newTrack.id }) { + self[index] = newTrack + } else { + self.append(newTrack) + } + } + + + + func isIdentical(to otherArray: [AudioTrack]) -> Bool { + guard self.count == otherArray.count else { return false } + + // 使用sorted方法确保id比较是顺序无关的 + let sortedSelf = self.sorted(by: { $0.id < $1.id }) + let sortedOtherArray = otherArray.sorted(by: { $0.id < $1.id }) + + for (trackFromSelf, trackFromOther) in zip(sortedSelf, sortedOtherArray) { + if trackFromSelf.id != trackFromOther.id { + return false + } + } + + return true + } + +} diff --git a/IndieMusic/IndieMusic/Managers/AudioManager.swift b/IndieMusic/IndieMusic/Managers/AudioManager.swift index bbe5e56..76d0421 100644 --- a/IndieMusic/IndieMusic/Managers/AudioManager.swift +++ b/IndieMusic/IndieMusic/Managers/AudioManager.swift @@ -21,6 +21,11 @@ class AudioManager { let commandCenter = MPRemoteCommandCenter.shared() + + let currentTrack: BehaviorRelay = .init(value: nil) + let playlist: BehaviorRelay<[AudioTrack]> = .init(value: []) + + init() { NotificationCenter.default .addObserver(self, @@ -48,17 +53,11 @@ class AudioManager { var player: AVPlayer? - var currentTrack: AudioTrack? var currentTrackIndex: Int { - return playlist?.firstIndex(where: {$0.id == currentTrack?.id}) ?? 0 + return playlist.value.firstIndex(where: {$0.id == currentTrack.value?.id}) ?? 0 } - var playlist: [AudioTrack]? - -// var repeatActive = false -// var shuffleActive = false - var playerShuffleType: PlayerShuffleType = .sequential var isSeekInProgress = false @@ -66,10 +65,12 @@ class AudioManager { var isPlayingState = BehaviorSubject(value: false) var disposeBag = DisposeBag() + + var progressObserver = BehaviorRelay<(time: CMTime, duration: CMTime, progress: Float)>.init(value: (time: .zero, duration: .zero, progress: 0)) func setPlaylist(list: [AudioTrack]) { - self.playlist = list + self.playlist.accept(list) // 缓存当前播放列表 do { @@ -95,15 +96,17 @@ class AudioManager { let playerItem: AVPlayerItem = AVPlayerItem.init(asset: CachingAVURLAsset(url: url)) + self.disposeBag = DisposeBag.init() player?.pause() player = AVPlayer(playerItem: playerItem) player?.play() player?.volume = 1 - self.currentTrack = track + self.currentTrack.accept(track) NotificationCenter.default.post(name: .notiPlayAudioTrack, object: track) -// updateLockScreenDisplay(track: track) + + player?.rx.observe(AVPlayer.TimeControlStatus.self, "timeControlStatus") .map { $0 == .playing } @@ -118,6 +121,20 @@ class AudioManager { }) .disposed(by: disposeBag) + + + let interval = CMTime(seconds: 0.05, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + self.player?.rx.periodicTimeObserver(interval: interval) + .subscribe(onNext: { [weak self] time in + let currentTime = time + let duration = playerItem.duration + let progress = self?.progress(currentTime: time, duration: duration) ?? 0 + + self?.progressObserver.accept((time: currentTime, duration: duration, progress: progress)) + + }).disposed(by: disposeBag) + + // 缓存当前歌曲 do { let trackData = try JSONEncoder().encode(track) @@ -133,8 +150,19 @@ class AudioManager { } } + func changePlaylistAndPlay(audioTrack: AudioTrack, audioTracks: [AudioTrack]) { + pause() + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + AudioManager.sharedInstance.setPlaylist(list: audioTracks) + AudioManager.sharedInstance.playTrack(track: audioTrack) + } + + + } + public func nextTrack() { - guard let playlist = playlist else { return } + let playlist = playlist.value switch playerShuffleType { case .random: @@ -164,7 +192,8 @@ class AudioManager { } public func previousTrack() { - if currentTrack != nil, let playlist = playlist { + let playlist = playlist.value + if currentTrack.value != nil { let index = currentTrackIndex - 1 if index >= 0 && index < playlist.count { playTrack(track: playlist[index]) @@ -211,7 +240,7 @@ class AudioManager { } else { self.trySeekToChaseTime() } - if let currentTrack = self.currentTrack { + if let currentTrack = self.currentTrack.value { self.updateLockScreenDisplay(track: currentTrack) } } @@ -277,7 +306,7 @@ class AudioManager { commandCenter.playCommand.isEnabled = true commandCenter.playCommand.addTarget { [unowned self] event in - if let currentTrack = currentTrack, !self.isPlaying() { + if let currentTrack = currentTrack.value, !self.isPlaying() { self.player?.play() // updateLockScreenDisplay(track: currentTrack) return .success @@ -287,7 +316,7 @@ class AudioManager { commandCenter.pauseCommand.isEnabled = true commandCenter.pauseCommand.addTarget { [unowned self] event in - if let currentTrack = currentTrack, self.isPlaying() { + if let currentTrack = currentTrack.value, self.isPlaying() { self.player?.pause() // updateLockScreenDisplay(track: currentTrack) return .success @@ -326,4 +355,33 @@ class AudioManager { } + + private func progress(currentTime: CMTime, duration: CMTime) -> Float { + if !duration.isValid || !currentTime.isValid { + return 0 + } + + let totalSeconds = duration.seconds + let currentSeconds = currentTime.seconds + + if !totalSeconds.isFinite || !currentSeconds.isFinite { + return 0 + } + + let p = Float(min(currentSeconds/totalSeconds, 1)) + return p + } + +} + + +extension AudioManager { + func updateTrack(in tracks: inout [AudioTrack], by trackId: String, property: WritableKeyPath, to newValue: T) { + // 查找特定id的元素索引 + if let index = tracks.firstIndex(where: { $0.id == trackId }) { + // 使用KeyPath直接在原数组上修改该属性 + tracks[index][keyPath: property] = newValue + } + } + } diff --git a/IndieMusic/IndieMusic/Models/Journal.swift b/IndieMusic/IndieMusic/Models/Journal.swift index bb20d48..625ae14 100644 --- a/IndieMusic/IndieMusic/Models/Journal.swift +++ b/IndieMusic/IndieMusic/Models/Journal.swift @@ -71,3 +71,23 @@ extension JournalSection: SectionModelType { } } + +extension JournalSection { + var allAudioTracks: [AudioTrack] { + let items: [JournalItem] = { + switch self { + case .audioSection(_, let items), .journalSection(_, let items): + return items + } + }() + + return items.compactMap { item -> AudioTrack? in + switch item { + case .audioItem(let model): + return model + case .journaItem: + return nil + } + } + } +} diff --git a/IndieMusic/IndieMusic/Modules/Home/HomeTabBarController.swift b/IndieMusic/IndieMusic/Modules/Home/HomeTabBarController.swift index 339eaf2..4e0644a 100644 --- a/IndieMusic/IndieMusic/Modules/Home/HomeTabBarController.swift +++ b/IndieMusic/IndieMusic/Modules/Home/HomeTabBarController.swift @@ -189,7 +189,7 @@ class HomeTabBarController: UITabBarController, Navigatable { playerTabBar.rx.controlEvent(.touchUpInside).subscribe { [weak self] _ in guard let self = self else { return } - guard let track = AudioManager.sharedInstance.currentTrack else { return } + guard let track = AudioManager.sharedInstance.currentTrack.value else { return } let playerViewModel = PlayerViewModel.init(track: track, provider: viewModel.provider) self.navigator.show(segue: .player(viewModel: playerViewModel), sender: self, transition: .modal) @@ -198,7 +198,7 @@ class HomeTabBarController: UITabBarController, Navigatable { output.notiPlayAudioTrack.subscribe { [weak self] noti in - self?.playerTabBar.audioTrack = AudioManager.sharedInstance.currentTrack + self?.playerTabBar.audioTrack = AudioManager.sharedInstance.currentTrack.value self?.playerTabBar.playButton.isSelected = true }.disposed(by: rx.disposeBag) @@ -208,14 +208,18 @@ class HomeTabBarController: UITabBarController, Navigatable { output.notiPlayPause.subscribe { [weak self] noti in self?.playerTabBar.playButton.isSelected = false - }.disposed(by: rx.disposeBag) - output.progress.subscribe { [weak self] progress in - self?.playerTabBar.progressView.progress = progress - }.disposed(by: rx.disposeBag) +// output.progress.subscribe { [weak self] progress in +// self?.playerTabBar.progressView.progress = progress +// }.disposed(by: rx.disposeBag) + + AudioManager.sharedInstance.progressObserver.subscribe { [weak self] (time, duration, progress) in + self?.playerTabBar.progressView.progress = Float(progress) + + }.disposed(by: rx.disposeBag) } diff --git a/IndieMusic/IndieMusic/Modules/Home/HomeTabBarViewModel.swift b/IndieMusic/IndieMusic/Modules/Home/HomeTabBarViewModel.swift index 6c0e061..e90c25e 100644 --- a/IndieMusic/IndieMusic/Modules/Home/HomeTabBarViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/Home/HomeTabBarViewModel.swift @@ -57,9 +57,17 @@ class HomeTabBarViewModel: ViewModel, ViewModelType { checkVersion() + + AudioManager.sharedInstance.progressObserver.subscribe { (time, duration, progress) in + + }.disposed(by: rx.disposeBag) + + input.notiPlayAudioTrack.subscribe { [weak self] noti in guard let track = noti.element?.object as? AudioTrack else { return } + + // AudioManager.sharedInstance.player?.addProgressObserver { currentTime, duration, progress in // self?.progress.accept(Float(progress)) // self?.duration.accept(Float(duration)) diff --git a/IndieMusic/IndieMusic/Modules/Home/HomeViewController.swift b/IndieMusic/IndieMusic/Modules/Home/HomeViewController.swift index ff7d286..541343e 100644 --- a/IndieMusic/IndieMusic/Modules/Home/HomeViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Home/HomeViewController.swift @@ -56,7 +56,7 @@ class HomeViewController: TableViewController, ScrollableNavBar { if tabbar.customeTabBar.isHidden == true && tabbar.playerTabBar.isHidden == true { tabbar.showAllBar(true, animated: true) } else if tabbar.playerTabBar.isHidden == true { - tabbar.playerTabBar.audioTrack = AudioManager.sharedInstance.currentTrack + tabbar.playerTabBar.audioTrack = AudioManager.sharedInstance.currentTrack.value tabbar.showPlayerBar(true, animated: true) } } @@ -65,7 +65,7 @@ class HomeViewController: TableViewController, ScrollableNavBar { navigationBar.passthroughView = homePagerView } - if AudioManager.sharedInstance.playlist?.count ?? 0 > 0 { + if AudioManager.sharedInstance.playlist.value.count > 0 { tableView.contentInset = .init(top: 0, left: 0, bottom: BaseDimensions.tabBarHeight + 66, right: 0) } else { tableView.contentInset = .init(top: 0, left: 0, bottom: BaseDimensions.tabBarHeight, right: 0) @@ -192,12 +192,7 @@ class HomeViewController: TableViewController, ScrollableNavBar { let playerViewModel = PlayerViewModel.init(track: audioTrack, provider: viewModel.provider) self?.navigator.show(segue: .player(viewModel: playerViewModel), sender: self, transition: .modal) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - // 这里放置延迟执行的代码 - AudioManager.sharedInstance.setPlaylist(list: audioTrackArray) - AudioManager.sharedInstance.playTrack(track: audioTrack) - } + AudioManager.sharedInstance.changePlaylistAndPlay(audioTrack: audioTrack, audioTracks: audioTrackArray) }.disposed(by: rx.disposeBag) diff --git a/IndieMusic/IndieMusic/Modules/Home/SongViewCell.swift b/IndieMusic/IndieMusic/Modules/Home/SongViewCell.swift index f7bae8b..f96105b 100644 --- a/IndieMusic/IndieMusic/Modules/Home/SongViewCell.swift +++ b/IndieMusic/IndieMusic/Modules/Home/SongViewCell.swift @@ -79,7 +79,7 @@ class SongViewCell: UITableViewCell { coverView.kf.setImage(with: URL.init(string: audioTrack.pic ?? "")) - if AudioManager.sharedInstance.currentTrack?.id == audioTrack.id { + if AudioManager.sharedInstance.currentTrack.value?.id == audioTrack.id { musicIndicator.state = AudioManager.sharedInstance.isPlaying() ? .playing : .paused } else { @@ -87,7 +87,7 @@ class SongViewCell: UITableViewCell { } AudioManager.sharedInstance.isPlayingState.subscribe { [weak self] isPlaying in - if AudioManager.sharedInstance.currentTrack?.id == audioTrack.id { + if AudioManager.sharedInstance.currentTrack.value?.id == audioTrack.id { self?.musicIndicator.state = AudioManager.sharedInstance.isPlaying() ? .playing : .paused } else { self?.musicIndicator.state = .stopped diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailController.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailController.swift index aa85fcd..70d3510 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailController.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailController.swift @@ -38,6 +38,7 @@ class JournalDetailController: TableViewController { super.viewWillAppear(animated) updateNav(navBarBgAlpha: self.currentNavBarBgAlpha) + } override func viewWillDisappear(_ animated: Bool) { @@ -118,6 +119,23 @@ class JournalDetailController: TableViewController { output.items.bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag) + output.items.subscribe { journalSections in + guard let currentAudioTracks = journalSections.first?.allAudioTracks else { return } + let audioTracks = AudioManager.sharedInstance.playlist.value + + if currentAudioTracks.isIdentical(to: audioTracks) { + if AudioManager.sharedInstance.isPlaying() { + self.journalCollapsibleHeaderView.playButton.isSelected = true + + } else { + self.journalCollapsibleHeaderView.playButton.isSelected = false + } + + } else { + self.journalCollapsibleHeaderView.playButton.isSelected = false + } + + }.disposed(by: rx.disposeBag) output.modelSelected.drive {[weak self] journalItem in self?.deselectSelectedRow() @@ -142,12 +160,7 @@ class JournalDetailController: TableViewController { let playerViewModel = PlayerViewModel.init(track: track, provider: viewModel.provider) self?.navigator.show(segue: .player(viewModel: playerViewModel), sender: self, transition: .modal) - - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - AudioManager.sharedInstance.setPlaylist(list: audioTracks) - AudioManager.sharedInstance.playTrack(track: track) - } + AudioManager.sharedInstance.changePlaylistAndPlay(audioTrack: track, audioTracks: audioTracks) case .journaItem(_): break } @@ -170,6 +183,8 @@ class JournalDetailController: TableViewController { guard let self = self else { return } self.commentToolView.commentCountButton.count = Int(journal.totalCommentReply ?? "") + + self.journalCollapsibleHeaderView.journal = journal self.updateHeader() @@ -202,9 +217,24 @@ class JournalDetailController: TableViewController { }.disposed(by: rx.disposeBag) journalCollapsibleHeaderView.playButton.rx.tap.subscribe { [weak self] _ in + guard let audioTracks = output.items.value.first?.allAudioTracks, + let audioTrack = audioTracks.first else { return } - SVProgressHUD.showText(withStatus: "播放分模式") + if AudioManager.sharedInstance.playlist.value.isIdentical(to: audioTracks) { + if AudioManager.sharedInstance.isPlaying() { + AudioManager.sharedInstance.pause() + } else { + let playerViewModel = PlayerViewModel.init(track: audioTrack, provider: viewModel.provider) + self?.navigator.show(segue: .player(viewModel: playerViewModel), sender: self, transition: .modal) + AudioManager.sharedInstance.changePlaylistAndPlay(audioTrack: audioTrack, audioTracks: audioTracks) + } + + } else { + let playerViewModel = PlayerViewModel.init(track: audioTrack, provider: viewModel.provider) + self?.navigator.show(segue: .player(viewModel: playerViewModel), sender: self, transition: .modal) + AudioManager.sharedInstance.changePlaylistAndPlay(audioTrack: audioTrack, audioTracks: audioTracks) + } }.disposed(by: rx.disposeBag) @@ -234,7 +264,20 @@ class JournalDetailController: TableViewController { }.disposed(by: rx.disposeBag) -// AudioManager.sharedInstance. + AudioManager.sharedInstance.isPlayingState.subscribe { isPlaying in + guard let currentListAudioTracks = output.items.value.first?.allAudioTracks else { return } + let audioTracks = AudioManager.sharedInstance.playlist.value + + if currentListAudioTracks.isIdentical(to: audioTracks) && isPlaying { + self.journalCollapsibleHeaderView.playButton.isSelected = true + } else { + self.journalCollapsibleHeaderView.playButton.isSelected = false + } + + + + }.disposed(by: rx.disposeBag) + tableView.rx.willDisplayCell .subscribe { [weak self] cell, indexPath in diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailView.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailView.swift index 71a7d56..f51af85 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailView.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailView.swift @@ -209,7 +209,7 @@ class JournalCollapsibleHeaderView: UIView { - playButton.isSelected = AudioManager.sharedInstance.currentTrack?.journalNo == journal.journalNo +// playButton.isSelected = AudioManager.sharedInstance.currentTrack.value?.journalNo == journal.journalNo } } @@ -619,7 +619,7 @@ class JournalAudioViewCell: UITableViewCell { coverView.kf.setImage(with: URL.init(string: audioTrack.pic ?? "")) - if AudioManager.sharedInstance.currentTrack?.id == audioTrack.id { + if AudioManager.sharedInstance.currentTrack.value?.id == audioTrack.id { musicIndicator.state = AudioManager.sharedInstance.isPlaying() ? .playing : .paused } else { @@ -627,7 +627,7 @@ class JournalAudioViewCell: UITableViewCell { } AudioManager.sharedInstance.isPlayingState.subscribe { [weak self] isPlaying in - if AudioManager.sharedInstance.currentTrack?.id == audioTrack.id { + if AudioManager.sharedInstance.currentTrack.value?.id == audioTrack.id { self?.musicIndicator.state = AudioManager.sharedInstance.isPlaying() ? .playing : .paused } else { self?.musicIndicator.state = .stopped diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailViewModel.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailViewModel.swift index 0e63e62..7091863 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailViewModel.swift @@ -66,6 +66,9 @@ class JournalDetailViewModel: ViewModel, ViewModelType { input.viewWillAppear.subscribe { _ in + + + }.disposed(by: rx.disposeBag) diff --git a/IndieMusic/IndieMusic/Modules/Mine/MineDownloadViewController.swift b/IndieMusic/IndieMusic/Modules/Mine/MineDownloadViewController.swift index df73a66..2ebdfdf 100644 --- a/IndieMusic/IndieMusic/Modules/Mine/MineDownloadViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Mine/MineDownloadViewController.swift @@ -123,13 +123,7 @@ class MineDownloadViewController: TableViewController { let playerViewModel = PlayerViewModel.init(track: audioTrack, provider: viewModel.provider) self?.navigator.show(segue: .player(viewModel: playerViewModel), sender: self, transition: .modal) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - // 这里放置延迟执行的代码 - AudioManager.sharedInstance.setPlaylist(list: audioTracks) - AudioManager.sharedInstance.playTrack(track: audioTrack) - } - + AudioManager.sharedInstance.changePlaylistAndPlay(audioTrack: audioTrack, audioTracks: audioTracks) }.disposed(by: rx.disposeBag) @@ -146,13 +140,7 @@ class MineDownloadViewController: TableViewController { let playerViewModel = PlayerViewModel.init(track: audioTrack, provider: viewModel.provider) self?.navigator.show(segue: .player(viewModel: playerViewModel), sender: self, transition: .modal) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - // 这里放置延迟执行的代码 - AudioManager.sharedInstance.setPlaylist(list: audioTracks) - AudioManager.sharedInstance.playTrack(track: audioTrack) - } - + AudioManager.sharedInstance.changePlaylistAndPlay(audioTrack: audioTrack, audioTracks: audioTracks) }.disposed(by: rx.disposeBag) @@ -337,7 +325,7 @@ class MineDownloadViewCell: UITableViewCell { selectButton.isSelected = mineDownload.isSelected - if AudioManager.sharedInstance.currentTrack?.id == mineDownload.audioTrack.id { + if AudioManager.sharedInstance.currentTrack.value?.id == mineDownload.audioTrack.id { musicIndicator.state = AudioManager.sharedInstance.isPlaying() ? .playing : .paused } else { @@ -345,7 +333,7 @@ class MineDownloadViewCell: UITableViewCell { } AudioManager.sharedInstance.isPlayingState.subscribe { [weak self] isPlaying in - if AudioManager.sharedInstance.currentTrack?.id == mineDownload.audioTrack.id { + if AudioManager.sharedInstance.currentTrack.value?.id == mineDownload.audioTrack.id { self?.musicIndicator.state = AudioManager.sharedInstance.isPlaying() ? .playing : .paused } else { self?.musicIndicator.state = .stopped diff --git a/IndieMusic/IndieMusic/Modules/Mine/MineJournalViewController.swift b/IndieMusic/IndieMusic/Modules/Mine/MineJournalViewController.swift index 3fefd5f..f0f7ffc 100644 --- a/IndieMusic/IndieMusic/Modules/Mine/MineJournalViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Mine/MineJournalViewController.swift @@ -11,6 +11,11 @@ import RxCocoa import RxDataSources class MineJournalViewController: CollectionViewController { + let headerView: MineSingleHeaderView = { + let headerView = MineSingleHeaderView.init() + + return headerView + }() override func viewDidLoad() { super.viewDidLoad() @@ -42,6 +47,7 @@ class MineJournalViewController: CollectionViewController { emptyDataSetDescription = "你还没收藏过期刊!" + view.addSubview(headerView) } override func bindViewModel() { @@ -54,7 +60,8 @@ class MineJournalViewController: CollectionViewController { let input = MineJournalViewModel.Input.init(viewWillAppear: rx.viewWillAppear, headerRefresh: refresh, footerRefresh: footerRefreshTrigger, - selection: collectionView.rx.itemSelected.asDriver()) + selection: collectionView.rx.itemSelected.asDriver(), + journalSongTrigger: headerView.playAllButton.rx.tap.asDriver()) let output = viewModel.transform(input: input) @@ -73,10 +80,16 @@ class MineJournalViewController: CollectionViewController { }.disposed(by: rx.disposeBag) - let updateEmptyDataSet = Observable.of(isLoading.mapToVoid().asObservable(), emptyDataSetImageTintColor.mapToVoid(), languageChanged.asObservable()).merge() - updateEmptyDataSet.subscribe(onNext: { [weak self] () in - self?.collectionView.reloadEmptyDataSet() - }).disposed(by: rx.disposeBag) + output.toPlay.subscribe { [weak self] audioTracks in + guard let audioTracks = audioTracks.element, + let audioTrack = audioTracks.first else { return } + + let playerViewModel = PlayerViewModel.init(track: audioTrack, provider: viewModel.provider) + self?.navigator.show(segue: .player(viewModel: playerViewModel), sender: self, transition: .modal) + AudioManager.sharedInstance.changePlaylistAndPlay(audioTrack: audioTrack, audioTracks: audioTracks) + + }.disposed(by: rx.disposeBag) + } @@ -85,14 +98,20 @@ class MineJournalViewController: CollectionViewController { override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - - - collectionView.snp.makeConstraints { make in + headerView.snp.remakeConstraints { make in make.left.equalTo(view) make.right.equalTo(view) make.top.equalTo(view) + make.height.equalTo(64) + } + + collectionView.snp.remakeConstraints { make in + make.left.equalTo(view) + make.right.equalTo(view) + make.top.equalTo(headerView.snp.bottom) make.bottom.equalTo(view) } + } } diff --git a/IndieMusic/IndieMusic/Modules/Mine/MineJournalViewModel.swift b/IndieMusic/IndieMusic/Modules/Mine/MineJournalViewModel.swift index df2d27d..68f4de7 100644 --- a/IndieMusic/IndieMusic/Modules/Mine/MineJournalViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/Mine/MineJournalViewModel.swift @@ -16,19 +16,21 @@ class MineJournalViewModel: ViewModel, ViewModelType { let headerRefresh: Observable let footerRefresh: Observable let selection: Driver + let journalSongTrigger: Driver } struct Output { let items: BehaviorRelay<[MineJournalSection]> let selection: Driver let itemSelected: PublishSubject - + let toPlay: PublishRelay<[AudioTrack]> } let itemSelected = PublishSubject() let items = BehaviorRelay<[MineJournalSection]>.init(value: []) // let item = BehaviorRelay.init(value: nil) - + let toPlay = PublishRelay<[AudioTrack]>.init() + func transform(input: Input) -> Output { input.viewWillAppear.subscribe { (_) in @@ -69,11 +71,24 @@ class MineJournalViewModel: ViewModel, ViewModelType { }.disposed(by: rx.disposeBag) + input.journalSongTrigger.drive { _ in + self.requestCollectJournalSongList(page: 0, size: 0) + .subscribe(onNext: { audioTracks in + + self.toPlay.accept(audioTracks) + + }, onError: { error in + + }).disposed(by: self.rx.disposeBag) + + }.disposed(by: rx.disposeBag) + return Output.init(items: items, selection: input.selection, - itemSelected: itemSelected + itemSelected: itemSelected, + toPlay: self.toPlay ) } @@ -85,5 +100,11 @@ class MineJournalViewModel: ViewModel, ViewModelType { } + func requestCollectJournalSongList( page: Int, size: Int) -> Observable<[AudioTrack]> { + return self.provider.collectJournalSongList(userId: UserDefaults.AccountInfo.string(forKey: .userID) ?? "", page: page, size: size) + .trackActivity(loading) + .trackError(error) + } + } diff --git a/IndieMusic/IndieMusic/Modules/Mine/MineSingleController.swift b/IndieMusic/IndieMusic/Modules/Mine/MineSingleController.swift index dccd212..a5db5fb 100644 --- a/IndieMusic/IndieMusic/Modules/Mine/MineSingleController.swift +++ b/IndieMusic/IndieMusic/Modules/Mine/MineSingleController.swift @@ -96,13 +96,7 @@ class MineSingleController: TableViewController { let playerViewModel = PlayerViewModel.init(track: track, provider: viewModel.provider) self?.navigator.show(segue: .player(viewModel: playerViewModel), sender: self, transition: .modal) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - // 这里放置延迟执行的代码 - AudioManager.sharedInstance.setPlaylist(list: audioModels) - AudioManager.sharedInstance.playTrack(track: track) - } - + AudioManager.sharedInstance.changePlaylistAndPlay(audioTrack: track, audioTracks: audioModels) }.disposed(by: rx.disposeBag) @@ -117,13 +111,7 @@ class MineSingleController: TableViewController { let playerViewModel = PlayerViewModel.init(track: audioTrack, provider: viewModel.provider) self?.navigator.show(segue: .player(viewModel: playerViewModel), sender: self, transition: .modal) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - // 这里放置延迟执行的代码 - AudioManager.sharedInstance.setPlaylist(list: audioTracks) - AudioManager.sharedInstance.playTrack(track: audioTrack) - } - + AudioManager.sharedInstance.changePlaylistAndPlay(audioTrack: audioTrack, audioTracks: audioTracks) }.disposed(by: rx.disposeBag) diff --git a/IndieMusic/IndieMusic/Modules/Mine/MineViewController.swift b/IndieMusic/IndieMusic/Modules/Mine/MineViewController.swift index ebc6581..19d82dc 100644 --- a/IndieMusic/IndieMusic/Modules/Mine/MineViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Mine/MineViewController.swift @@ -45,7 +45,7 @@ class MineViewController: TableViewController { tabbar.showAllBar(true, animated: true) } - if AudioManager.sharedInstance.playlist?.count ?? 0 > 0 { + if AudioManager.sharedInstance.playlist.value.count > 0 { tableView.contentInset = .init(top: 0, left: 0, bottom: BaseDimensions.tabBarHeight + 66, right: 0) } else { tableView.contentInset = .init(top: 0, left: 0, bottom: BaseDimensions.tabBarHeight, right: 0) @@ -233,13 +233,7 @@ class MineViewController: TableViewController { let playerViewModel = PlayerViewModel.init(track: audioTrack, provider: viewModel.provider) self.navigator.show(segue: .player(viewModel: playerViewModel), sender: self, transition: .modal) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - // 这里放置延迟执行的代码 - AudioManager.sharedInstance.setPlaylist(list: audioTracks) - AudioManager.sharedInstance.playTrack(track: audioTrack) - } - + AudioManager.sharedInstance.changePlaylistAndPlay(audioTrack: audioTrack, audioTracks: audioTracks) }.disposed(by: rx.disposeBag) diff --git a/IndieMusic/IndieMusic/Modules/Personal/PersonalDetail/PersonalSongViewController.swift b/IndieMusic/IndieMusic/Modules/Personal/PersonalDetail/PersonalSongViewController.swift index ee58575..268261b 100644 --- a/IndieMusic/IndieMusic/Modules/Personal/PersonalDetail/PersonalSongViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Personal/PersonalDetail/PersonalSongViewController.swift @@ -228,12 +228,7 @@ class PersonalSongViewController: CollectionViewController, UIScrollViewDelegate let playerViewModel = PlayerViewModel.init(track: audioTrack, provider: viewModel.provider) self?.navigator.show(segue: .player(viewModel: playerViewModel), sender: self, transition: .modal) - - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - AudioManager.sharedInstance.setPlaylist(list: [audioTrack]) - AudioManager.sharedInstance.playTrack(track: audioTrack) - } + AudioManager.sharedInstance.changePlaylistAndPlay(audioTrack: audioTrack, audioTracks: [audioTrack]) default: break } diff --git a/IndieMusic/IndieMusic/Modules/Player/AudioTrackListViewController.swift b/IndieMusic/IndieMusic/Modules/Player/AudioTrackListViewController.swift index aa55f5d..690b16e 100644 --- a/IndieMusic/IndieMusic/Modules/Player/AudioTrackListViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Player/AudioTrackListViewController.swift @@ -23,6 +23,7 @@ class AudioTrackListViewController: ViewController { tableView.register(SongViewCell.self , forCellReuseIdentifier: "SongViewCell") tableView.separatorColor = .clear + tableView.backgroundColor = .clear return tableView }() @@ -48,11 +49,6 @@ class AudioTrackListViewController: ViewController { view.addSubview(tableView) view.addSubview(audioMoreActionBottomView) - let haederView = UIView.init(frame: CGRect.init(x: 0, y: 0, width: BaseDimensions.screenWidth, height: 9)) - haederView.backgroundColor = .white - tableView.tableHeaderView = haederView - - view.backgroundColor = .white } @@ -144,7 +140,8 @@ extension AudioTrackListViewController { return RxTableViewSectionedReloadDataSource( configureCell: { dataSource, tableView, indexPath, item in let cell: SongViewCell = tableView.dequeueReusableCell(withIdentifier: "SongViewCell", for: indexPath) as! SongViewCell - + cell.backgroundColor = .clear + if case let .audioItem(model) = item { cell.audioTrack = model } diff --git a/IndieMusic/IndieMusic/Modules/Player/AudioTrackListViewModel.swift b/IndieMusic/IndieMusic/Modules/Player/AudioTrackListViewModel.swift index deb6221..daa7190 100644 --- a/IndieMusic/IndieMusic/Modules/Player/AudioTrackListViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/Player/AudioTrackListViewModel.swift @@ -37,12 +37,12 @@ class AudioTrackListViewModel: ViewModel, ViewModelType { input.viewWillAppear.subscribe { [weak self] isViewWillAppear in guard let self = self else { return } - if let playlist = AudioManager.sharedInstance.playlist { - let arr = playlist.map { audioTrack in - return JournalItem.audioItem(model: audioTrack) - } - self.items.accept([JournalSection.audioSection(header: nil, items: arr)]) + let playlist = AudioManager.sharedInstance.playlist.value + let arr = playlist.map { audioTrack in + return JournalItem.audioItem(model: audioTrack) } + self.items.accept([JournalSection.audioSection(header: nil, items: arr)]) + }.disposed(by: rx.disposeBag) diff --git a/IndieMusic/IndieMusic/Modules/Player/PlayerTabBar.swift b/IndieMusic/IndieMusic/Modules/Player/PlayerTabBar.swift index 0dee34b..25f8451 100644 --- a/IndieMusic/IndieMusic/Modules/Player/PlayerTabBar.swift +++ b/IndieMusic/IndieMusic/Modules/Player/PlayerTabBar.swift @@ -218,10 +218,12 @@ class PlayerTabBar: UIControl { -// AudioManager.sharedInstance.player?.addProgressObserver { time, duration, progress in -// self.progressView.setProgress(Float(progress), animated: false) -// } + AudioManager.sharedInstance.progressObserver.subscribe { (time, duration, progress) in + self.progressView.setProgress(Float(progress), animated: false) + + }.disposed(by: rx.disposeBag) + // self.playButton.image = UIImage(named: "pause") } diff --git a/IndieMusic/IndieMusic/Modules/Player/PlayerViewController.swift b/IndieMusic/IndieMusic/Modules/Player/PlayerViewController.swift index 11c8caa..fcdc1c8 100644 --- a/IndieMusic/IndieMusic/Modules/Player/PlayerViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Player/PlayerViewController.swift @@ -162,25 +162,25 @@ class PlayerViewController: ViewController { }.disposed(by: rx.disposeBag) - output.progress.subscribe { [weak self] progress in - guard let self = self else { return } - - if !self.playerScrollView.playerInfoView.playerSlider.isDrop { - self.playerScrollView.playerInfoView.playerSlider.slider.value = progress - } - }.disposed(by: rx.disposeBag) - - output.duration.subscribe { [weak self] duration in - guard let self = self else { return } - - self.playerScrollView.playerInfoView.playerSlider.totalSec = duration - }.disposed(by: rx.disposeBag) - - output.time.subscribe { [weak self] time in - guard let self = self else { return } - - self.playerScrollView.playerInfoView.playerSlider.updateTimeLabel(currentTime: time) - }.disposed(by: rx.disposeBag) +// output.progress.subscribe { [weak self] progress in +// guard let self = self else { return } +// +// if !self.playerScrollView.playerInfoView.playerSlider.isDrop { +// self.playerScrollView.playerInfoView.playerSlider.slider.value = progress +// } +// }.disposed(by: rx.disposeBag) +// +// output.duration.subscribe { [weak self] duration in +// guard let self = self else { return } +// +// self.playerScrollView.playerInfoView.playerSlider.totalSec = duration +// }.disposed(by: rx.disposeBag) +// +// output.time.subscribe { [weak self] time in +// guard let self = self else { return } +// +// self.playerScrollView.playerInfoView.playerSlider.updateTimeLabel(currentTime: time) +// }.disposed(by: rx.disposeBag) output.isPlaying.bind(to: self.playerControlView.playButton.rx.isSelected).disposed(by: rx.disposeBag) @@ -207,6 +207,17 @@ class PlayerViewController: ViewController { }.disposed(by: rx.disposeBag) + AudioManager.sharedInstance.progressObserver.subscribe { (time, duration, progress) in + if !self.playerScrollView.playerInfoView.playerSlider.isDrop { + self.playerScrollView.playerInfoView.playerSlider.slider.value = Float(progress) + } + + self.playerScrollView.playerInfoView.playerSlider.totalSec = Float(duration.seconds) + self.playerScrollView.playerInfoView.playerSlider.updateTimeLabel(currentTime: Float(time.seconds)) + + }.disposed(by: rx.disposeBag) + + } override func viewDidLayoutSubviews() { diff --git a/IndieMusic/IndieMusic/Modules/Player/PlayerViewModel.swift b/IndieMusic/IndieMusic/Modules/Player/PlayerViewModel.swift index 57a56ff..1f61dd2 100644 --- a/IndieMusic/IndieMusic/Modules/Player/PlayerViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/Player/PlayerViewModel.swift @@ -219,11 +219,10 @@ class PlayerViewModel: ViewModel, ViewModelType { func updateAudioTrack(audioTrack: AudioTrack) { self.currentAudioTrack.accept(audioTrack) + var playlist = AudioManager.sharedInstance.playlist.value + playlist.replaceOrAdd(track: audioTrack) - if let index = AudioManager.sharedInstance.playlist?.firstIndex(where: { $0.id == audioTrack.id }) { - // 使用新的 AudioTrack 实例替换旧的 - AudioManager.sharedInstance.playlist?[index] = audioTrack - } + AudioManager.sharedInstance.playlist.accept(playlist) } diff --git a/IndieMusic/IndieMusic/Modules/Search/SearchResults/SongResultsViewController.swift b/IndieMusic/IndieMusic/Modules/Search/SearchResults/SongResultsViewController.swift index 03b1f02..cf8f34c 100644 --- a/IndieMusic/IndieMusic/Modules/Search/SearchResults/SongResultsViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Search/SearchResults/SongResultsViewController.swift @@ -225,12 +225,7 @@ class SongResultsViewController: CollectionViewController { let playerViewModel = PlayerViewModel.init(track: audioTrack, provider: viewModel.provider) self?.navigator.show(segue: .player(viewModel: playerViewModel), sender: self, transition: .modal) - - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - AudioManager.sharedInstance.setPlaylist(list: [audioTrack]) - AudioManager.sharedInstance.playTrack(track: audioTrack) - } + AudioManager.sharedInstance.changePlaylistAndPlay(audioTrack: audioTrack, audioTracks: [audioTrack]) default: break } diff --git a/IndieMusic/IndieMusic/Modules/Search/SearchViewController.swift b/IndieMusic/IndieMusic/Modules/Search/SearchViewController.swift index c32f479..11c1587 100644 --- a/IndieMusic/IndieMusic/Modules/Search/SearchViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Search/SearchViewController.swift @@ -59,7 +59,7 @@ class SearchViewController: ViewController, UIScrollViewDelegate { self.navigationController?.setNavigationBarHidden(true, animated: false) - if AudioManager.sharedInstance.playlist?.count ?? 0 > 0 { + if AudioManager.sharedInstance.playlist.value.count > 0 { collectionView.contentInset = .init(top: 0, left: 0, bottom: BaseDimensions.tabBarHeight + 66, right: 0) } else { collectionView.contentInset = .init(top: 0, left: 0, bottom: BaseDimensions.tabBarHeight, right: 0) diff --git a/IndieMusic/IndieMusic/Networking/Api.swift b/IndieMusic/IndieMusic/Networking/Api.swift index 2048154..e46badc 100644 --- a/IndieMusic/IndieMusic/Networking/Api.swift +++ b/IndieMusic/IndieMusic/Networking/Api.swift @@ -53,6 +53,9 @@ protocol IndieMusicAPI { func messageList() -> Single /// 用户收藏歌曲列表 func collectSongList(userId: String, page: Int, size: Int) -> Single<[AudioTrack]> + /// 用户收藏期刊所有歌曲 + func collectJournalSongList(userId: String, page: Int, size: Int) -> Single<[AudioTrack]> + /// 用户收藏期刊列表 func journalCollectList(userId: String, page: Int, size: Int) -> Single<[Journal]> /// 用户关注列表 diff --git a/IndieMusic/IndieMusic/Networking/Rest/APIConfig.swift b/IndieMusic/IndieMusic/Networking/Rest/APIConfig.swift index 258199c..19040fb 100644 --- a/IndieMusic/IndieMusic/Networking/Rest/APIConfig.swift +++ b/IndieMusic/IndieMusic/Networking/Rest/APIConfig.swift @@ -36,6 +36,7 @@ enum APIConfig { case messageList case collectSongList([String: Any]) + case collectJournalSongList([String: Any]) case journalCollectList([String: Any]) case followingList(String, Int, Int) case followerList(String, Int, Int) @@ -110,6 +111,8 @@ extension APIConfig: TargetType { return "luoo-user/userMessage/list/" case .collectSongList: return "luoo-music/song/collect" + case .collectJournalSongList: + return "luoo-music/journal/collect/song" case .journalCollectList: return "luoo-music/journal/collect" case .followingList(let userId, let page, let size): @@ -166,7 +169,7 @@ extension APIConfig: TargetType { var method: Moya.Method { switch self { - case .wechatAccessToken, .journalList, .journalMusic, .countryCode, .imageCheckCode, .getUserInfo, .carousel, .otherUserInfo, .single, .journal, .messageList, .collectSongList, .journalCollectList, .followingList, .followerList, .blackList, .hotCommentList, .latestCommentList, .subCommentList, .filterMenu, .journalRecommend, .searchCategory, .searchSong, .searchJournal, .randomAudioTrack, .myThumbupList, .myCommentReplyList, .suggestions, .thanks: + case .wechatAccessToken, .journalList, .journalMusic, .countryCode, .imageCheckCode, .getUserInfo, .carousel, .otherUserInfo, .single, .journal, .messageList, .collectSongList, .journalCollectList, .followingList, .followerList, .blackList, .hotCommentList, .latestCommentList, .subCommentList, .filterMenu, .journalRecommend, .searchCategory, .searchSong, .searchJournal, .randomAudioTrack, .myThumbupList, .myCommentReplyList, .suggestions, .thanks, .collectJournalSongList: return .get case .sendsms, .login, .autoLogin, .editAvatar, .like, .checkVersion, .logout, .sendComment, .feedback, .commentReport: return .post @@ -180,7 +183,7 @@ extension APIConfig: TargetType { var parameterEncoding: ParameterEncoding { switch self { - case .wechatAccessToken, .journalList, .journalMusic, .countryCode, .sendsms, .imageCheckCode, .login, .getUserInfo, .carousel, .otherUserInfo, .single, .journal, .messageList, .like, .cancelLike, .collectSongList, .journalCollectList, .followingList, .followerList, .blackList, .hotCommentList, .latestCommentList, .subCommentList, .filterMenu, .journalRecommend, .searchCategory, .searchSong, .searchJournal, .randomAudioTrack, .myThumbupList, .myCommentReplyList, .suggestions, .commentReport, .deleteComment, .thanks: + case .wechatAccessToken, .journalList, .journalMusic, .countryCode, .sendsms, .imageCheckCode, .login, .getUserInfo, .carousel, .otherUserInfo, .single, .journal, .messageList, .like, .cancelLike, .collectSongList, .journalCollectList, .followingList, .followerList, .blackList, .hotCommentList, .latestCommentList, .subCommentList, .filterMenu, .journalRecommend, .searchCategory, .searchSong, .searchJournal, .randomAudioTrack, .myThumbupList, .myCommentReplyList, .suggestions, .commentReport, .deleteComment, .thanks, .collectJournalSongList: return URLEncoding.default case .autoLogin, .editUserInfo, .editAvatar, .checkVersion, .logout, .commentLike, .sendComment, .feedback: @@ -195,7 +198,7 @@ extension APIConfig: TargetType { case .wechatAccessToken, .countryCode, .journalMusic, .imageCheckCode, .getUserInfo, .carousel, .otherUserInfo, .single, .journal, .messageList, .followingList, .followerList, .blackList, .hotCommentList, .latestCommentList, .subCommentList, .filterMenu, .journalRecommend, .searchCategory, .randomAudioTrack, .myThumbupList, .myCommentReplyList, .commentReport, .deleteComment, .thanks: return .requestPlain - case .login(let dic), .journalList(let dic), .sendsms(let dic), .autoLogin(let dic), .editUserInfo(let dic), .like(let dic), .cancelLike(let dic), .logout(let dic), .checkVersion(let dic), .commentLike(_, let dic), .sendComment(let dic), .collectSongList(let dic), .journalCollectList(let dic), .suggestions(let dic), .searchSong(let dic), .searchJournal(let dic): + case .login(let dic), .journalList(let dic), .sendsms(let dic), .autoLogin(let dic), .editUserInfo(let dic), .like(let dic), .cancelLike(let dic), .logout(let dic), .checkVersion(let dic), .commentLike(_, let dic), .sendComment(let dic), .collectSongList(let dic), .journalCollectList(let dic), .suggestions(let dic), .searchSong(let dic), .searchJournal(let dic), .collectJournalSongList(let dic): parameters = dic return .requestParameters(parameters: parameters, encoding: parameterEncoding) @@ -238,7 +241,7 @@ extension APIConfig: TargetType { var headers : [String : String]? { switch self { - case .autoLogin, .getUserInfo, .journalList, .journalMusic, .otherUserInfo, .like, .cancelLike, .single, .journal, .collectSongList, .journalCollectList, .followingList, .messageList, .followerList, .blackList, .editUserInfo, .logout, .editAvatar, .hotCommentList, .latestCommentList, .subCommentList, .commentLike, .filterMenu, .journalRecommend, .searchSong, .searchJournal, .randomAudioTrack, .sendComment, .myThumbupList, .myCommentReplyList, .feedback, .suggestions, .commentReport, .deleteComment: + case .autoLogin, .getUserInfo, .journalList, .journalMusic, .otherUserInfo, .like, .cancelLike, .single, .journal, .collectSongList, .journalCollectList, .followingList, .messageList, .followerList, .blackList, .editUserInfo, .logout, .editAvatar, .hotCommentList, .latestCommentList, .subCommentList, .commentLike, .filterMenu, .journalRecommend, .searchSong, .searchJournal, .randomAudioTrack, .sendComment, .myThumbupList, .myCommentReplyList, .feedback, .suggestions, .commentReport, .deleteComment, .collectJournalSongList: return ["Authorization": AuthManager.shared.token?.basicToken ?? ""] default: return nil diff --git a/IndieMusic/IndieMusic/Networking/Rest/RestApi.swift b/IndieMusic/IndieMusic/Networking/Rest/RestApi.swift index 848c37c..9f337dd 100644 --- a/IndieMusic/IndieMusic/Networking/Rest/RestApi.swift +++ b/IndieMusic/IndieMusic/Networking/Rest/RestApi.swift @@ -215,6 +215,16 @@ extension RestApi { return requestObject(.collectSongList(dic), with: "data.rows", type: [AudioTrack].self) } + func collectJournalSongList(userId: String, page: Int, size: Int) -> Single<[AudioTrack]> { + let dic = ["userId": userId, + "pageNum": page, + "pageSize": size + ] as [String : Any] + return requestObject(.collectJournalSongList(dic), with: "data.rows", type: [AudioTrack].self) + } + + + func journalCollectList(userId: String, page: Int, size: Int) -> Single<[Journal]> { let dic = ["userId": userId, "pageNum": page, diff --git a/IndieMusic/IndieMusic/Third Party/RxAVFoundation/AVAsynchronousKeyValueLoading+Rx.swift b/IndieMusic/IndieMusic/Third Party/RxAVFoundation/AVAsynchronousKeyValueLoading+Rx.swift new file mode 100644 index 0000000..68eeb13 --- /dev/null +++ b/IndieMusic/IndieMusic/Third Party/RxAVFoundation/AVAsynchronousKeyValueLoading+Rx.swift @@ -0,0 +1,28 @@ +// +// AVAsynchronousKeyValueLoading+Rx.swift +// RxAVPlayer +// +// Created by Patrick Mick on 4/1/16. +// Copyright © 2016 YayNext. All rights reserved. +// + +import Foundation +import AVFoundation +import RxSwift +import RxCocoa + +extension Reactive where Base: AVAsynchronousKeyValueLoading { + public func loadValuesForKeys(_ keys: [String]) -> Observable { + return Observable.create { observer in + self.base.loadValuesAsynchronously(forKeys: keys) { + // TODO: Test statusOfValueForKey for every key that was loaded and + // return some kind of error model if any keys failed to load + + observer.onNext(()) + observer.onCompleted() + } + + return Disposables.create() + } + } +} diff --git a/IndieMusic/IndieMusic/Third Party/RxAVFoundation/AVPlayer+Rx.swift b/IndieMusic/IndieMusic/Third Party/RxAVFoundation/AVPlayer+Rx.swift new file mode 100644 index 0000000..27c71ec --- /dev/null +++ b/IndieMusic/IndieMusic/Third Party/RxAVFoundation/AVPlayer+Rx.swift @@ -0,0 +1,64 @@ +// +// AVPlayer+Rx.swift +// RxAVPlayer +// +// Created by Patrick Mick on 3/30/16. +// Copyright © 2016 YayNext. All rights reserved. +// + +import Foundation +import AVFoundation +import RxSwift +import RxCocoa + +extension Reactive where Base: AVPlayer { + public var rate: Observable { + return self.observe(Float.self, #keyPath(AVPlayer.rate)) + .map { $0 ?? 0 } + } + + public var currentItem: Observable { + return observe(AVPlayerItem.self, #keyPath(AVPlayer.currentItem)) + } + + public var status: Observable { + return self.observe(AVPlayer.Status.self, #keyPath(AVPlayer.status)) + .map { $0 ?? .unknown } + } + + public var error: Observable { + return self.observe(NSError.self, #keyPath(AVPlayer.error)) + } + + @available(iOS 10.0, tvOS 10.0, macOS 10.12, *) + public var reasonForWaitingToPlay: Observable { + return self.observe(AVPlayer.WaitingReason.self, #keyPath(AVPlayer.reasonForWaitingToPlay)) + } + + @available(iOS 10.0, tvOS 10.0, *, OSX 10.12, *) + public var timeControlStatus: Observable { + return self.observe(AVPlayer.TimeControlStatus.self, #keyPath(AVPlayer.timeControlStatus)) + .map { $0 ?? .waitingToPlayAtSpecifiedRate } + } + + public func periodicTimeObserver(interval: CMTime) -> Observable { + return Observable.create { observer in + let t = self.base.addPeriodicTimeObserver(forInterval: interval, queue: nil) { time in + observer.onNext(time) + } + + return Disposables.create { self.base.removeTimeObserver(t) } + } + } + + public func boundaryTimeObserver(times: [CMTime]) -> Observable { + return Observable.create { observer in + let timeValues = times.map() { NSValue(time: $0) } + let t = self.base.addBoundaryTimeObserver(forTimes: timeValues, queue: nil) { + observer.onNext(()) + } + + return Disposables.create { self.base.removeTimeObserver(t) } + } + } +} diff --git a/IndieMusic/IndieMusic/Third Party/RxAVFoundation/AVPlayerItem+Rx.swift b/IndieMusic/IndieMusic/Third Party/RxAVFoundation/AVPlayerItem+Rx.swift new file mode 100644 index 0000000..d1d67bd --- /dev/null +++ b/IndieMusic/IndieMusic/Third Party/RxAVFoundation/AVPlayerItem+Rx.swift @@ -0,0 +1,54 @@ +// +// AVPlayerItem+Rx.swift +// RxAVPlayer +// +// Created by Patrick Mick on 4/1/16. +// Copyright © 2016 YayNext. All rights reserved. +// + +import Foundation +import AVFoundation +import RxSwift +import RxCocoa + +extension Reactive where Base: AVPlayerItem { + public var status: Observable { + return self.observe(AVPlayerItem.Status.self, #keyPath(AVPlayerItem.status)) + .map { $0 ?? .unknown } + } + + public var error: Observable { + return self.observe(NSError.self, #keyPath(AVPlayerItem.error)) + } + + public var duration: Observable { + return self.observe(CMTime.self, #keyPath(AVPlayerItem.duration)) + .map { $0 ?? .zero } + } + + public var playbackLikelyToKeepUp: Observable { + return self.observe(Bool.self, #keyPath(AVPlayerItem.isPlaybackLikelyToKeepUp)) + .map { $0 ?? false } + } + + public var playbackBufferFull: Observable { + return self.observe(Bool.self, #keyPath(AVPlayerItem.isPlaybackBufferFull)) + .map { $0 ?? false } + } + + public var playbackBufferEmpty: Observable { + return self.observe(Bool.self, #keyPath(AVPlayerItem.isPlaybackBufferEmpty)) + .map { $0 ?? false } + } + + public var didPlayToEnd: Observable { + let ns = NotificationCenter.default + return ns.rx.notification(.AVPlayerItemDidPlayToEndTime, object: base) + } + + public var loadedTimeRanges: Observable<[CMTimeRange]> { + return self.observe([NSValue].self, #keyPath(AVPlayerItem.loadedTimeRanges)) + .map { $0 ?? [] } + .map { values in values.map { $0.timeRangeValue } } + } +} diff --git a/IndieMusic/IndieMusic/Third Party/RxAVFoundation/AVPlayerLayer+Rx.swift b/IndieMusic/IndieMusic/Third Party/RxAVFoundation/AVPlayerLayer+Rx.swift new file mode 100644 index 0000000..a3d0833 --- /dev/null +++ b/IndieMusic/IndieMusic/Third Party/RxAVFoundation/AVPlayerLayer+Rx.swift @@ -0,0 +1,24 @@ +// +// AVPlayerLayer+Rx.swift +// RxAVPlayer +// +// Created by Patrick Mick on 4/4/16. +// Copyright © 2016 YayNext. All rights reserved. +// + +import Foundation +import AVFoundation +import RxSwift +import RxCocoa + +extension Reactive where Base: AVPlayerLayer { + public var isReadyForDisplay: Observable { + return self.observe(Bool.self, #keyPath(AVPlayerLayer.isReadyForDisplay)) + .map { $0 ?? false } + } + + @available(*, deprecated, renamed: "isReadyForDisplay") + public var readyForDisplay: Observable { + isReadyForDisplay + } +}