diff --git a/IndieMusic/IndieMusic.xcodeproj/project.pbxproj b/IndieMusic/IndieMusic.xcodeproj/project.pbxproj index f2d3ae8..542df5d 100644 --- a/IndieMusic/IndieMusic.xcodeproj/project.pbxproj +++ b/IndieMusic/IndieMusic.xcodeproj/project.pbxproj @@ -185,6 +185,12 @@ 77C9B9ED2B4BEA610006C83F /* MyCommentListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9B9EC2B4BEA610006C83F /* MyCommentListViewModel.swift */; }; 77C9B9EF2B4C2A910006C83F /* AudioTrackListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9B9EE2B4C2A910006C83F /* AudioTrackListViewController.swift */; }; 77C9B9F12B4C2B3A0006C83F /* AudioTrackListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9B9F02B4C2B3A0006C83F /* AudioTrackListViewModel.swift */; }; + 77C9C0722B845FF4000A277B /* RxTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9C0712B845FF3000A277B /* RxTimer.swift */; }; + 77C9C0742B8495A0000A277B /* 02.lyric in Resources */ = {isa = PBXBuildFile; fileRef = 77C9C0732B8495A0000A277B /* 02.lyric */; }; + 77C9C0762B84E513000A277B /* ReportMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9C0752B84E513000A277B /* ReportMenuViewController.swift */; }; + 77C9C0782B84EA19000A277B /* SheetViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9C0772B84EA19000A277B /* SheetViewCell.swift */; }; + 77C9C07A2B85B036000A277B /* PopoverListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9C0792B85B036000A277B /* PopoverListView.swift */; }; + 77C9C07C2B85C33A000A277B /* FeedbackMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9C07B2B85C33A000A277B /* FeedbackMenuViewController.swift */; }; 77CEFEFC2B81EC600071B671 /* PresentAndDismissTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CEFEFB2B81EC600071B671 /* PresentAndDismissTransition.swift */; }; 77CEFEFE2B82F18A0071B671 /* CommentToolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CEFEFD2B82F18A0071B671 /* CommentToolView.swift */; }; 77DFA9C52B4E8388005B8B13 /* MineDownloadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77DFA9C42B4E8388005B8B13 /* MineDownloadViewController.swift */; }; @@ -436,6 +442,12 @@ 77C9B9EC2B4BEA610006C83F /* MyCommentListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyCommentListViewModel.swift; sourceTree = ""; }; 77C9B9EE2B4C2A910006C83F /* AudioTrackListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioTrackListViewController.swift; sourceTree = ""; }; 77C9B9F02B4C2B3A0006C83F /* AudioTrackListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioTrackListViewModel.swift; sourceTree = ""; }; + 77C9C0712B845FF3000A277B /* RxTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RxTimer.swift; sourceTree = ""; }; + 77C9C0732B8495A0000A277B /* 02.lyric */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = 02.lyric; sourceTree = ""; }; + 77C9C0752B84E513000A277B /* ReportMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportMenuViewController.swift; sourceTree = ""; }; + 77C9C0772B84EA19000A277B /* SheetViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetViewCell.swift; sourceTree = ""; }; + 77C9C0792B85B036000A277B /* PopoverListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverListView.swift; sourceTree = ""; }; + 77C9C07B2B85C33A000A277B /* FeedbackMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackMenuViewController.swift; sourceTree = ""; }; 77CEFEFB2B81EC600071B671 /* PresentAndDismissTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentAndDismissTransition.swift; sourceTree = ""; }; 77CEFEFD2B82F18A0071B671 /* CommentToolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentToolView.swift; sourceTree = ""; }; 77DFA9C42B4E8388005B8B13 /* MineDownloadViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MineDownloadViewController.swift; sourceTree = ""; }; @@ -565,6 +577,7 @@ 7736FF432B4CECF2008D5DAD /* CommentViewModel.swift */, 7736FF452B4CF0E6008D5DAD /* CommentDetailViewController.swift */, 778638932B4D123D00B00AF9 /* CommentDetailViewModel.swift */, + 77C9C0752B84E513000A277B /* ReportMenuViewController.swift */, ); path = JournalDetail; sourceTree = ""; @@ -616,6 +629,7 @@ 77620D992B69DA1A00798861 /* EditSignatureViewController.swift */, 77620D932B68E65300798861 /* EditDateViewController.swift */, 77620D8F2B68DDA000798861 /* EditSexViewController.swift */, + 77C9C0772B84EA19000A277B /* SheetViewCell.swift */, ); path = EditInfo; sourceTree = ""; @@ -687,6 +701,7 @@ 778B8A512AF8EA2A0034AFD4 /* Resources */ = { isa = PBXGroup; children = ( + 77C9C0732B8495A0000A277B /* 02.lyric */, 776A1F712B7A165500F613EB /* Localizable */, 77620D7D2B67332600798861 /* font */, 775100A12B63442900F46109 /* Json */, @@ -735,6 +750,7 @@ 770228F02B57AD2C00E07F7A /* RefreshLoadingView.swift */, 775D075D2B5E5BCA009270D3 /* GradientLayerLabel.swift */, 77CEFEFD2B82F18A0071B671 /* CommentToolView.swift */, + 77C9C0792B85B036000A277B /* PopoverListView.swift */, ); path = Common; sourceTree = ""; @@ -856,6 +872,7 @@ 770228E32B54FA3600E07F7A /* RxActivityIndicator */, 778B8A602AF8ECC20034AFD4 /* RxErrorTracker */, 7751D3812B45324300F1F2BD /* IndieMusic-Bridging-Header.h */, + 77C9C0712B845FF3000A277B /* RxTimer.swift */, ); path = "Third Party"; sourceTree = ""; @@ -955,6 +972,7 @@ 7751D3632B42BC2E00F1F2BD /* AccountViewModel.swift */, 7751D3652B42BE7F00F1F2BD /* FeedbackViewController.swift */, 776A1F7D2B7B3EE600F613EB /* FeedbackViewModel.swift */, + 77C9C07B2B85C33A000A277B /* FeedbackMenuViewController.swift */, 7751D3672B42E96200F1F2BD /* ThanksViewController.swift */, 77FB7A7E2B4A630100B64030 /* ThanksViewModel.swift */, 7751D3692B42ED6C00F1F2BD /* TimingViewController.swift */, @@ -1184,6 +1202,7 @@ 77620D832B67332600798861 /* Alibaba PuHuiTi 2.0.ttf in Resources */, 778B8A2D2AF8E36E0034AFD4 /* LaunchScreen.storyboard in Resources */, 77620D822B67332600798861 /* Roboto Condensed.ttf in Resources */, + 77C9C0742B8495A0000A277B /* 02.lyric in Resources */, 77620D812B67332600798861 /* Fontquan-XinYiJiXiangSong.ttf in Resources */, 778B8A2A2AF8E36E0034AFD4 /* Assets.xcassets in Resources */, 778B8A282AF8E36D0034AFD4 /* Main.storyboard in Resources */, @@ -1317,6 +1336,7 @@ files = ( 77C9B9CF2B4B94020006C83F /* PersonalViewModel.swift in Sources */, 77FA0B422B0DFAA000404C5E /* LoginViewController.swift in Sources */, + 77C9C07C2B85C33A000A277B /* FeedbackMenuViewController.swift in Sources */, 7751D3702B43A4FC00F1F2BD /* CacheViewController.swift in Sources */, 7751D3642B42BC2E00F1F2BD /* AccountViewModel.swift in Sources */, 778B8A5D2AF8EC610034AFD4 /* Application.swift in Sources */, @@ -1348,6 +1368,7 @@ 77FB7A762B4A4B5600B64030 /* MineJournalViewController.swift in Sources */, 778B8AC32AF8ED280034AFD4 /* TabViewController.swift in Sources */, 77620D872B67446900798861 /* CustomFonts.swift in Sources */, + 77C9C07A2B85B036000A277B /* PopoverListView.swift in Sources */, 77FAF7622B07434A00FC2CA1 /* AudioMoreActionController.swift in Sources */, 774A18122B07327C00F56DF1 /* CommentCountButton.swift in Sources */, 7751D36C2B439F0000F1F2BD /* AboutViewController.swift in Sources */, @@ -1420,6 +1441,7 @@ 774A17F32B0459C900F56DF1 /* PlayerViewModel.swift in Sources */, 77C9B9EF2B4C2A910006C83F /* AudioTrackListViewController.swift in Sources */, 77FB7A782B4A4B6400B64030 /* MineJournalViewModel.swift in Sources */, + 77C9C0762B84E513000A277B /* ReportMenuViewController.swift in Sources */, 778B8A6D2AF8ECD30034AFD4 /* Observable+Operators.swift in Sources */, 77A659D12B4FDC5200B408C3 /* PullToDismissTransition.swift in Sources */, 77FA0B542B0F447400404C5E /* Mine.swift in Sources */, @@ -1428,6 +1450,7 @@ 778B8ABB2AF8ED280034AFD4 /* WebViewController.swift in Sources */, 774A17F72B04932100F56DF1 /* SegmentControl.swift in Sources */, 77FA0B282B0B3E1E00404C5E /* Journal.swift in Sources */, + 77C9C0722B845FF4000A277B /* RxTimer.swift in Sources */, 770228F82B5A224500E07F7A /* NSLayoutConstraint+Multiplier.swift in Sources */, 77FA0B5A2B147EC900404C5E /* CommentViewController.swift in Sources */, 77FA0B402B0D8E9300404C5E /* LayoutableButton.swift in Sources */, @@ -1443,6 +1466,7 @@ 77C9B9D92B4BBFA60006C83F /* Following.swift in Sources */, 77C9B9DB2B4BC40F0006C83F /* FollowersViewController.swift in Sources */, 775D075C2B5E49A6009270D3 /* VerificationCodeController.swift in Sources */, + 77C9C0782B84EA19000A277B /* SheetViewCell.swift in Sources */, 778B8AC02AF8ED280034AFD4 /* ViewController.swift in Sources */, 7751D3862B45409000F1F2BD /* NSNotification+IndieMusic.swift in Sources */, 774399A02AFA1968006F8EEA /* PlayerTabBar.swift in Sources */, diff --git a/IndieMusic/IndieMusic/Application/Navigator.swift b/IndieMusic/IndieMusic/Application/Navigator.swift index 6574a1b..3a84b72 100644 --- a/IndieMusic/IndieMusic/Application/Navigator.swift +++ b/IndieMusic/IndieMusic/Application/Navigator.swift @@ -41,6 +41,7 @@ class Navigator { case account(viewModel: AccountViewModel) case thanks(viewModel: ThanksViewModel) case feedback(viewModel: FeedbackViewModel) + case feedbackMenu(viewModel: FeedbackMenuViewModel) case timing(viewModel: TimingViewModel) case privacy(viewModel: PrivacyViewModel) case about(viewModel: AboutViewModel) @@ -53,6 +54,8 @@ class Navigator { case myCommentList(viewModel: MyCommentListViewModel) case comment(viewModel: CommentViewModel) case commentDetail(viewModel: CommentDetailViewModel) + + case reportMenu(viewModel: ReportMenuViewModel) case searchResults(viewModel: SearchResultsViewModel) case audioTrackList(viewModel: AudioTrackListViewModel) @@ -64,6 +67,7 @@ class Navigator { case internationalNumber(viewModel: InternationalNumberViewModel) case player(viewModel: PlayerViewModel) + case alert case test case safari(URL) @@ -92,40 +96,40 @@ class Navigator { return nav case .search(let viewModel): return SearchViewController(viewModel: viewModel, navigator: self) - case .journalDetail(viewModel: let viewModel): + case .journalDetail(let viewModel): return JournalDetailController.init(viewModel: viewModel, navigator: self) - case .filter(viewModel: let viewModel): + case .filter(let viewModel): return FilterViewController.init(viewModel: viewModel, navigator: self) - case .audioMore(viewModel: let viewModel): + case .audioMore(let viewModel): return AudioMoreActionController.init(viewModel: viewModel, navigator: self) - case .share(viewModel: let viewModel): + case .share(let viewModel): return ShareActionController.init(viewModel: viewModel, navigator: self) - case .shareCrad(viewModel: let viewModel): + case .shareCrad(let viewModel): return ShareCardViewController.init(viewModel: viewModel, navigator: self) - case .musicStyle(viewModel: let viewModel): + case .musicStyle(let viewModel): return MusicStyleViewController.init(viewModel: viewModel, navigator: self) - case .searchResults(viewModel: let viewModel): + case .searchResults(let viewModel): return SearchResultsController.init(viewModel: viewModel, navigator: self) - case .mineSingle(viewModel: let viewModel): + case .mineSingle(let viewModel): return MineSingleController.init(viewModel: viewModel, navigator: self) - case .mineJourna(viewModel: let viewModel): + case .mineJourna(let viewModel): return MineJournalViewController.init(viewModel: viewModel, navigator: self) - case .mineDownload(viewModel: let viewModel): + case .mineDownload(let viewModel): return MineDownloadViewController.init(viewModel: viewModel, navigator: self) - case .setting(viewModel: let viewModel): + case .setting(let viewModel): return SettingViewController.init(viewModel: viewModel, navigator: self) - case .message(viewModel: let viewModel): + case .message(let viewModel): return MessageViewController.init(viewModel: viewModel, navigator: self) - case .editInfo(viewModel: let viewModel): + case .editInfo(let viewModel): return EditInfoViewController.init(viewModel: viewModel, navigator: self) - case .editName(viewModel: let viewModel): + case .editName(let viewModel): return EditNameController.init(viewModel: viewModel, navigator: self) - case .editSignature(viewModel: let viewModel): + case .editSignature(let viewModel): return EditSignatureViewController.init(viewModel: viewModel, navigator: self) - case .editSex(viewModel: let viewModel): + case .editSex(let viewModel): return EditSexViewController.init(viewModel: viewModel, navigator: self) - case .editDate(viewModel: let viewModel): + case .editDate(let viewModel): return EditDateViewController.init(viewModel: viewModel, navigator: self) case .account(let viewModel): @@ -134,43 +138,48 @@ class Navigator { return ThanksViewController.init(viewModel: viewModel, navigator: self) case .feedback(let viewModel): return FeedbackViewController.init(viewModel: viewModel, navigator: self) - case .timing(viewModel: let viewModel): + case .feedbackMenu(let viewModel): + return FeedbackMenuViewController.init(viewModel: viewModel, navigator: self) + case .timing(let viewModel): return TimingViewController.init(viewModel: viewModel, navigator: self) - case .about(viewModel: let viewModel): + case .about(let viewModel): return AboutViewController.init(viewModel: viewModel, navigator: self) - case .cache(viewModel: let viewModel): + case .cache(let viewModel): return CacheViewController.init(viewModel: viewModel, navigator: self) - case .privacy(viewModel: let viewModel): + case .privacy(let viewModel): return PrivacyViewController.init(viewModel: viewModel, navigator: self) - case .personal(viewModel: let viewModel): + case .personal(let viewModel): return PersonalViewController.init(viewModel: viewModel, navigator: self) - case .following(viewModel: let viewModel): + case .following(let viewModel): return FollowingViewController.init(viewModel: viewModel, navigator: self) - case .followers(viewModel: let viewModel): + case .followers(let viewModel): return FollowersViewController.init(viewModel: viewModel, navigator: self) - case .myLikeList(viewModel: let viewModel): + case .myLikeList(let viewModel): return MyLikeListController.init(viewModel: viewModel, navigator: self) - case .myCommentList(viewModel: let viewModel): + case .myCommentList(let viewModel): return MyCommentListController.init(viewModel: viewModel, navigator: self) - case .comment(viewModel: let viewModel): + case .comment(let viewModel): return CommentViewController.init(viewModel: viewModel, navigator: self) - case .commentDetail(viewModel: let viewModel): + case .commentDetail(let viewModel): return CommentDetailViewController.init(viewModel: viewModel, navigator: self) - case .audioTrackList(viewModel: let viewModel): + + case .reportMenu(let viewModel): + return ReportMenuViewController.init(viewModel: viewModel, navigator: self) + case .audioTrackList(let viewModel): return AudioTrackListViewController.init(viewModel: viewModel, navigator: self) case .photoConfirm: return PhotoConfirmViewController.init() - case .phoneCode(viewModel: let viewModel): + case .phoneCode(let viewModel): return PhoneCodeController(viewModel: viewModel, navigator: self) - case .login(viewModel: let viewModel): + case .login(let viewModel): return LoginViewController(viewModel: viewModel, navigator: self) - case .bindPhone(viewModel: let viewModel): + case .bindPhone(let viewModel): return BindPhoneViewController(viewModel: viewModel, navigator: self) - case .internationalNumber(viewModel: let viewModel): + case .internationalNumber(let viewModel): return InternationalNumberViewController.init(viewModel: viewModel, navigator: self) - case .player(viewModel: let viewModel): + case .player(let viewModel): let player = PlayerViewController(viewModel: viewModel, navigator: self) return player @@ -254,7 +263,7 @@ class Navigator { if let nav = sender.navigationController { nav.pushViewController(target, animated: true) } - case .navigationPresent(let type): + case .navigationPresent: DispatchQueue.main.async { let nav = NavigationController(rootViewController: target) nav.modalPresentationStyle = .custom diff --git a/IndieMusic/IndieMusic/Common/PopoverListView.swift b/IndieMusic/IndieMusic/Common/PopoverListView.swift new file mode 100644 index 0000000..e339f9d --- /dev/null +++ b/IndieMusic/IndieMusic/Common/PopoverListView.swift @@ -0,0 +1,22 @@ +// +// PopoverListView.swift +// IndieMusic +// +// Created by WenLei on 2024/2/21. +// + +import Foundation +import FSPopoverView + +class PopoverListView: FSPopoverListView { + var dismissClosures: (()->())? = nil + + override func popoverViewShouldDismissOnTapOutside(_ popoverView: FSPopoverView) -> Bool { + + if let dismissClosures = self.dismissClosures { + dismissClosures() + } + + return super.popoverViewShouldDismissOnTapOutside(popoverView) + } +} diff --git a/IndieMusic/IndieMusic/Common/PresentationController.swift b/IndieMusic/IndieMusic/Common/PresentationController.swift index e3dbef1..c60f8dc 100644 --- a/IndieMusic/IndieMusic/Common/PresentationController.swift +++ b/IndieMusic/IndieMusic/Common/PresentationController.swift @@ -18,6 +18,8 @@ enum PresentationViewType { case editSex case editDate + case reportMenu + case feedbackMenu } class PresentationController: UIPresentationController { @@ -103,6 +105,12 @@ class CardPresentationController: PresentationController, UIGestureRecognizerDel return containerView.bounds .inset(by: UIEdgeInsets(top: containerView.bounds.height - 245 - BaseDimensions.bottomHeight, left: 0, bottom: 0, right: 0)) + case .reportMenu: + return containerView.bounds + .inset(by: UIEdgeInsets(top: containerView.bounds.height - 420 - BaseDimensions.bottomHeight, left: 0, bottom: 0, right: 0)) + case .feedbackMenu: + return containerView.bounds + .inset(by: UIEdgeInsets(top: containerView.bounds.height - 202 - BaseDimensions.bottomHeight, left: 0, bottom: 0, right: 0)) case .tips: return containerView.bounds @@ -210,6 +218,25 @@ class CardPresentationController: PresentationController, UIGestureRecognizerDel self?.containerView?.backgroundColor = UIColor.black.withAlphaComponent(0.8) }, completion: nil) + + case .reportMenu: + coordinator.animate(alongsideTransition: { [weak self] _ in + + self?.containerView?.backgroundColor = UIColor.black.withAlphaComponent(0.8) + }, completion: nil) + + let tapGesture = UITapGestureRecognizer.init(target: self, action: #selector(dismiss)) + tapGesture.delegate = self + case .feedbackMenu: + coordinator.animate(alongsideTransition: { [weak self] _ in + + self?.containerView?.backgroundColor = UIColor.black.withAlphaComponent(0.8) + }, completion: nil) + + let tapGesture = UITapGestureRecognizer.init(target: self, action: #selector(dismiss)) + tapGesture.delegate = self + + case .tips(let isTapDismiss): diff --git a/IndieMusic/IndieMusic/Extensions/AVPlayer.swift b/IndieMusic/IndieMusic/Extensions/AVPlayer.swift index a0ce0df..eca56d4 100644 --- a/IndieMusic/IndieMusic/Extensions/AVPlayer.swift +++ b/IndieMusic/IndieMusic/Extensions/AVPlayer.swift @@ -15,7 +15,7 @@ extension AVPlayer { let duration = CMTimeGetSeconds(duration), time = CMTimeGetSeconds(time) let progress = (time/duration) - print("1媒体文件总时长: \(duration) 秒") + print("媒体文件总时长: \(duration) 秒") completion(time, duration, progress) } }) diff --git a/IndieMusic/IndieMusic/Extensions/Date+IndieMusic.swift b/IndieMusic/IndieMusic/Extensions/Date+IndieMusic.swift index d4c9109..e2fbf16 100644 --- a/IndieMusic/IndieMusic/Extensions/Date+IndieMusic.swift +++ b/IndieMusic/IndieMusic/Extensions/Date+IndieMusic.swift @@ -179,3 +179,22 @@ func hoursToSeconds(hours: Int) -> Int { func minutesToSeconds(minutes: Int) -> Int { return minutes * 60 } + + + +extension Int { + /// 将秒数转换为分钟和秒数的元组 + func toMinutesSeconds() -> (minutes: Int, seconds: Int) { + let minutes = self / 60 + let seconds = self % 60 + return (minutes, seconds) + } + + /// 格式化时间为MM:SS字符串 + func formattedTime() -> String { + let time = self.toMinutesSeconds() + let formattedMinutes = String(format: "%02d", time.minutes) + let formattedSeconds = String(format: "%02d", time.seconds) + return "\(formattedMinutes):\(formattedSeconds)" + } +} diff --git a/IndieMusic/IndieMusic/Models/Feedback.swift b/IndieMusic/IndieMusic/Models/Feedback.swift index 9afb52c..f85e210 100644 --- a/IndieMusic/IndieMusic/Models/Feedback.swift +++ b/IndieMusic/IndieMusic/Models/Feedback.swift @@ -19,9 +19,32 @@ enum FeedbackPhotoType { case photo(UIImage) } +enum FeedbackType: Int, CaseIterable { + case bug = 0 + case suggestion = 1 + case other = 2 + + + var description: String { + switch self { + case .bug: + return "bug" + case .suggestion: + return "建议" + case .other: + return "其它" + } + } + + +} + + struct FeedbackSection { var items: [FeedbackPhotoType] + + } extension FeedbackSection: SectionModelType { @@ -30,5 +53,7 @@ extension FeedbackSection: SectionModelType { init(original: FeedbackSection, items: [Item]) { self = original self.items = items + + } } diff --git a/IndieMusic/IndieMusic/Models/Message.swift b/IndieMusic/IndieMusic/Models/Message.swift index 043004e..0872937 100644 --- a/IndieMusic/IndieMusic/Models/Message.swift +++ b/IndieMusic/IndieMusic/Models/Message.swift @@ -54,6 +54,13 @@ enum CustomMessageType: Codable { } +struct MessageList: Codable { + let comment: Message + let follow: Message + let thumbup: Message +} + + struct Message: Codable, IdentifiableType, Equatable { let sendTime: String? let title: String? diff --git a/IndieMusic/IndieMusic/Models/Player.swift b/IndieMusic/IndieMusic/Models/Player.swift index 09921de..4d2bfe7 100644 --- a/IndieMusic/IndieMusic/Models/Player.swift +++ b/IndieMusic/IndieMusic/Models/Player.swift @@ -14,10 +14,7 @@ enum PlayerShuffleType { case sequential case singleLoop case listLoop -} - - -extension PlayerShuffleType { + var description: String { switch self { case .random: @@ -57,31 +54,34 @@ extension PlayerShuffleType { return .sequential } } + } -struct PlayerLyrics: Codable, IdentifiableType, Equatable { - let lyrics: String - + + +struct LyricLine: Codable, IdentifiableType, Equatable { + let time: TimeInterval + let text: String var identity: String { - return lyrics + return text } - static func == (lhs: PlayerLyrics, rhs: PlayerLyrics) -> Bool { - return lhs.lyrics == rhs.lyrics + static func == (lhs: LyricLine, rhs: LyricLine) -> Bool { + return lhs.text == rhs.text && lhs.time == rhs.time } } struct PlayerLyricsSection { - var items: [PlayerLyrics] + var items: [LyricLine] } extension PlayerLyricsSection: SectionModelType { - typealias Item = PlayerLyrics + typealias Item = LyricLine init(original: PlayerLyricsSection, items: [Item]) { self = original diff --git a/IndieMusic/IndieMusic/Models/Sheet.swift b/IndieMusic/IndieMusic/Models/Sheet.swift index e55d89f..b713b7e 100644 --- a/IndieMusic/IndieMusic/Models/Sheet.swift +++ b/IndieMusic/IndieMusic/Models/Sheet.swift @@ -8,8 +8,34 @@ import Foundation import RxDataSources -struct Sheet: Codable { - let sex: SexType +enum ReportType: String, CaseIterable { + case diversion = "站外引流" + case illegal = "违法违规" + case vulgarity = "色情低俗" + case misinformation = "虚假不实" + case incitement = "不友善、引战" + case falsified = "实证不实" + case minors = "涉未成年人" +} + + + +enum Sheet { + case sex(SexType) + case report(ReportType) + case feedback(FeedbackType) + + + var description: String { + switch self { + case .sex(let sexType): + return sexType.description + case .report(let reportType): + return reportType.rawValue + case .feedback(let feedbackType): + return feedbackType.description + } + } } diff --git a/IndieMusic/IndieMusic/Models/Thanks.swift b/IndieMusic/IndieMusic/Models/Thanks.swift index cfb267e..60f994c 100644 --- a/IndieMusic/IndieMusic/Models/Thanks.swift +++ b/IndieMusic/IndieMusic/Models/Thanks.swift @@ -9,13 +9,14 @@ import Foundation import RxDataSources struct Thanks: Codable { - let icon: String - let title: String - let detail: String - + let id: String + let nickName: String? + let avatar: String? + let contributorRole: String? } struct ThanksSection { + var header: String var items: [Thanks] } diff --git a/IndieMusic/IndieMusic/Models/Timing.swift b/IndieMusic/IndieMusic/Models/Timing.swift index 9575b19..88f70c1 100644 --- a/IndieMusic/IndieMusic/Models/Timing.swift +++ b/IndieMusic/IndieMusic/Models/Timing.swift @@ -8,10 +8,73 @@ import Foundation import RxDataSources -struct Timing: Codable { - let title: String +enum Timing: CaseIterable, Equatable { + case timeing15(isSelected: Bool) + case timeing30(isSelected: Bool) + case timeing60(isSelected: Bool) + case timeing90(isSelected: Bool) + case timeing120(isSelected: Bool) + case custom(isSelected: Bool, timeing: Int) + + + var description: Int { + switch self { + case .timeing15: + return 15 + case .timeing30: + return 30 + case .timeing60: + return 60 + case .timeing90: + return 90 + case .timeing120: + return 120 + case .custom(_, let timeing): + return timeing + } + } + + var isSelected: Bool { + switch self { + case .timeing15(let isSelected): + return isSelected + case .timeing30(let isSelected): + return isSelected + case .timeing60(let isSelected): + return isSelected + case .timeing90(let isSelected): + return isSelected + case .timeing120(let isSelected): + return isSelected + case .custom(let isSelected, _): + return isSelected + } + } + + + static var allCases: [Timing] { + return [.timeing15(isSelected: false), + .timeing30(isSelected: false), + .timeing60(isSelected: false), + .timeing90(isSelected: false), + .timeing120(isSelected: false)] + } + + static func ==(lhs: Timing, rhs: Timing) -> Bool { + switch (lhs, rhs) { + case (.timeing15, .timeing15), (.timeing30, .timeing30), (.timeing60, .timeing60), (.timeing90, .timeing90), (.timeing120, .timeing120): + return true + default: + return false + } + } + } +enum TimingType { + case normal + case custom +} struct TimingSection { var items: [Timing] diff --git a/IndieMusic/IndieMusic/Models/User.swift b/IndieMusic/IndieMusic/Models/User.swift index 267112d..03e4e0d 100644 --- a/IndieMusic/IndieMusic/Models/User.swift +++ b/IndieMusic/IndieMusic/Models/User.swift @@ -68,6 +68,6 @@ struct User: Codable, IdentifiableType, Equatable { } static func == (lhs: User, rhs: User) -> Bool { - return lhs.id == rhs.id + return lhs.id == rhs.id && lhs.relation == rhs.relation } } diff --git a/IndieMusic/IndieMusic/Modules/Home/CommentCountButton.swift b/IndieMusic/IndieMusic/Modules/Home/CommentCountButton.swift index 387e456..a4f0253 100644 --- a/IndieMusic/IndieMusic/Modules/Home/CommentCountButton.swift +++ b/IndieMusic/IndieMusic/Modules/Home/CommentCountButton.swift @@ -12,6 +12,9 @@ class CommentCountButton: UIButton { lazy var commentImageView: UIImageView = { let commentImageView = UIImageView.init(image: UIImage.init(named: "audio_comment_btn")) + commentImageView.setContentHuggingPriority(.required, for: .horizontal) + commentImageView.setContentCompressionResistancePriority(.required, for: .horizontal) + return commentImageView }() @@ -19,7 +22,9 @@ class CommentCountButton: UIButton { let commentCountLabel = UILabel.init() commentCountLabel.font = UIFont.init(name: UIFont.Roboto, size: 8) - + commentCountLabel.setContentHuggingPriority(.required, for: .horizontal) + commentCountLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + return commentCountLabel }() @@ -61,6 +66,7 @@ class CommentCountButton: UIButton { commentCountLabel.snp.remakeConstraints { make in make.left.equalTo(commentImageView.snp.right) + make.right.equalTo(self) make.top.equalTo(commentImageView).offset(0) } } diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewController.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewController.swift index 28b784f..5362268 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewController.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewController.swift @@ -24,8 +24,8 @@ class CommentDetailViewController: TableViewController { return commentToolView }() - private let menuView: FSPopoverListView = { - let menuView = FSPopoverListView.init(scrollDirection: .horizontal) + private let menuView: PopoverListView = { + let menuView = PopoverListView.init(scrollDirection: .horizontal) menuView.shadowOpacity = 0 menuView.shadowRadius = 0 menuView.shadowColor = .clear @@ -80,7 +80,7 @@ class CommentDetailViewController: TableViewController { let dataSource = RxTableViewSectionedAnimatedDataSource( - animationConfiguration: AnimationConfiguration.init(insertAnimation: .top, reloadAnimation: .none, deleteAnimation: .automatic), + animationConfiguration: AnimationConfiguration.init(insertAnimation: .bottom, reloadAnimation: .none, deleteAnimation: .automatic), configureCell: { dataSource, tableView, indexPath, item in let cell: CommentViewCell = tableView.dequeueReusableCell(withIdentifier: "CommentViewCell", for: indexPath) as! CommentViewCell @@ -103,15 +103,19 @@ class CommentDetailViewController: TableViewController { cell.rx.longPressGesture().when(.recognized) .subscribe { [weak self] tap in guard let self = self else { return } + cell.setSelected(true, animated: true) - if item.userId == UserDefaults.AccountInfo.string(forKey: .userID) { - self.menuView.items = self.setupMenuItems(cell: cell, features: [.copy, .report, .delete]) + if item.userId != UserDefaults.AccountInfo.string(forKey: .userID) { + self.menuView.items = self.setupMenuItems(cell: cell, features: [.copy, .report]) } else { self.menuView.items = self.setupMenuItems(cell: cell, features: [.copy, .delete]) } if let location = tap.element?.location(in: cell) { self.menuView.present(fromPoint: location, in: cell, displayIn: self.view) + self.menuView.dismissClosures = { + cell.setSelected(false, animated: true) + } } }.disposed(by: cell.rx.disposeBag) return cell @@ -238,7 +242,7 @@ class CommentDetailViewController: TableViewController { } func setupMenuItems(cell: CommentViewCell, features: [PopoverMenu] = [.copy, .delete]) -> [FSPopoverListItem] { - guard let viewModel = viewModel as? CommentViewModel, + guard let viewModel = viewModel as? CommentDetailViewModel, let comment = cell.comment else { return []} let items: [FSPopoverListItem] = features.map { feature in @@ -249,6 +253,8 @@ class CommentDetailViewController: TableViewController { item.titleColor = .white item.contentInset = .init(top: 9, left: 15, bottom: 9, right: 15) item.selectedHandler = { item in + cell.setSelected(false, animated: true) + guard let item = item as? FSPopoverListTextItem else { return } @@ -257,7 +263,12 @@ class CommentDetailViewController: TableViewController { case PopoverMenu.copy.description: UIPasteboard.general.string = cell.comment?.content case PopoverMenu.report.description: - viewModel.itemReport.accept(comment) + guard let commentID = cell.comment?.id else { return } + + let reportMenuViewModel = ReportMenuViewModel.init(commentId: commentID, provider: viewModel.provider) + self.navigator.show(segue: .reportMenu(viewModel: reportMenuViewModel), sender: self, transition: .navigationPresent(type: .reportMenu)) + + case PopoverMenu.delete.description: viewModel.itemDelete.accept(comment) default: break diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewModel.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewModel.swift index e85a696..2e15bd6 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewModel.swift @@ -36,6 +36,7 @@ class CommentDetailViewModel: ViewModel, ViewModelType { let likeSelected = PublishRelay() let itemReport = PublishRelay() + let itemDelete = PublishRelay() let comment: Comment @@ -131,7 +132,15 @@ class CommentDetailViewModel: ViewModel, ViewModelType { }.disposed(by: rx.disposeBag) - + itemDelete.subscribe { comment in + guard let commentID = comment.element?.id else { return } + self.requestDeleteComment(commentId: commentID) + .subscribe { _ in + self.deleteComment(commentId: commentID) + } onError: { error in + + }.disposed(by: self.rx.disposeBag) + }.disposed(by: rx.disposeBag) return Output.init(comment: Driver.just(comment), items: items, @@ -162,6 +171,12 @@ class CommentDetailViewModel: ViewModel, ViewModelType { } + func requestDeleteComment(commentId: String) -> Observable { + return self.provider.deleteComment(commentId: commentId) + .trackActivity(loading) + .trackError(error) + } + func insertComment(newComment: Comment) { var firstSection = items.value.first firstSection?.items.insert(newComment, at: 0) @@ -185,4 +200,15 @@ class CommentDetailViewModel: ViewModel, ViewModelType { } + func deleteComment(commentId: String) { + let updatedSections = items.value.map { section -> CommentSection in + let updatedComments = section.items.filter { comment -> Bool in + return comment.id != commentId + } + return CommentSection(id: section.id, items: updatedComments) + } + + items.accept(updatedSections) + } + } diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentView.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentView.swift index 83e2dab..3011635 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentView.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentView.swift @@ -156,7 +156,6 @@ class CommentViewCell: UITableViewCell { nameLabel.text = comment.nickName -// let date = Date.init(dateString: comment.publishTime ?? "", format: "yyyy-MM-dd HH:mm:ss") dateLabel.text = Date().dateString() + " \(comment.location ?? "未知")" commentLabel.text = comment.content @@ -193,24 +192,18 @@ class CommentViewCell: UITableViewCell { } } - -// -// override func prepareForReuse() { -// super.prepareForReuse() -// disposeBag = DisposeBag() -// } - - override func awakeFromNib() { - super.awakeFromNib() - // Initialization code - } - override func setSelected(_ selected: Bool, animated: Bool) { + + UIView.animate(withDuration: 0.25) { + self.contentView.backgroundColor = selected ? UIColor.black.withAlphaComponent(0.25) : .white + } super.setSelected(selected, animated: animated) - - // Configure the view for the selected state } + func selectedCell(_ selected: Bool, animated: Bool) { + + } + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewController.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewController.swift index 7f1e53a..edaa402 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewController.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewController.swift @@ -11,7 +11,7 @@ import RxCocoa import RxDataSources import FSPopoverView -class CommentViewController: ViewController { +class CommentViewController: TableViewController { var commentHeaderView: CommentHeaderView = { let commentHeaderView = CommentHeaderView.init() @@ -19,32 +19,29 @@ class CommentViewController: ViewController { return commentHeaderView }() - var tableView: UITableView = { - let tableView = UITableView.init() - tableView.separatorColor = .clear - tableView.register(CommentViewCell.self, forCellReuseIdentifier: "CommentViewCell") - tableView.keyboardDismissMode = .onDrag - - return tableView - }() - + var commentToolView: CommentToolView = { let commentToolView = CommentToolView.init(isShowButton: false, frame: CGRect.zero) return commentToolView }() - private let menuView: FSPopoverListView = { - let menuView = FSPopoverListView.init(scrollDirection: .horizontal) + private let menuView: PopoverListView = { + let menuView = PopoverListView.init(scrollDirection: .horizontal) menuView.shadowOpacity = 0 menuView.shadowRadius = 0 menuView.shadowColor = .clear menuView.cornerRadius = 3 menuView.transitioningDelegate = nil + menuView.borderColor = .primaryText() menuView.arrowSize = CGSize.init(width: 18, height: 5) menuView.backgroundColor = .primaryText() - + menuView.shouldDismissOnTapOutside = true +// menuView.showsDimBackground = true + + + return menuView }() @@ -65,15 +62,24 @@ class CommentViewController: ViewController { } + tableView.register(CommentViewCell.self, forCellReuseIdentifier: "CommentViewCell") + tableView.keyboardDismissMode = .onDrag + + + let tapGesture = UITapGestureRecognizer() tableView.addGestureRecognizer(tapGesture) tapGesture.rx.event .bind(onNext: { [weak self] _ in self?.view.endEditing(true) + + self?.deselectSelectedRow() }) .disposed(by: rx.disposeBag) + + } @@ -81,7 +87,6 @@ class CommentViewController: ViewController { super.makeUI() view.addSubview(commentHeaderView) - view.addSubview(tableView) view.addSubview(commentToolView) } @@ -105,7 +110,7 @@ class CommentViewController: ViewController { let output = viewModel.transform(input: input) let dataSource = RxTableViewSectionedAnimatedDataSource( - animationConfiguration: AnimationConfiguration.init(insertAnimation: .top, reloadAnimation: .none, deleteAnimation: .automatic), + animationConfiguration: AnimationConfiguration.init(insertAnimation: .bottom, reloadAnimation: .none, deleteAnimation: .automatic), configureCell: { dataSource, tableView, indexPath, item in let cell: CommentViewCell = tableView.dequeueReusableCell(withIdentifier: "CommentViewCell", for: indexPath) as! CommentViewCell @@ -139,14 +144,29 @@ class CommentViewController: ViewController { .subscribe { [weak self] tap in guard let self = self else { return } - if item.userId == UserDefaults.AccountInfo.string(forKey: .userID) { - self.menuView.items = self.setupMenuItems(cell: cell, features: [.copy, .report, .delete]) + if item.userId != UserDefaults.AccountInfo.string(forKey: .userID) { + self.menuView.items = self.setupMenuItems(cell: cell, features: [.copy, .report]) } else { self.menuView.items = self.setupMenuItems(cell: cell, features: [.copy, .delete]) } if let location = tap.element?.location(in: cell) { + cell.setSelected(true, animated: true) self.menuView.present(fromPoint: location, in: cell, displayIn: self.view) + self.menuView.dismissClosures = { + cell.setSelected(false, animated: true) + } + + self.menuView.mm_dimBackgroundView.rx.tapGesture().when(.recognized) + .subscribe { [weak self] tap in + + + self?.menuView.dismiss() + self?.deselectSelectedRow() + }.disposed(by: rx.disposeBag) + + + } }.disposed(by: cell.rx.disposeBag) return cell @@ -215,20 +235,20 @@ class CommentViewController: ViewController { super.viewDidLayoutSubviews() - commentHeaderView.snp.makeConstraints { make in + commentHeaderView.snp.remakeConstraints { make in make.left.equalTo(view) make.right.equalTo(view) make.top.equalTo(view) } - commentToolView.snp.makeConstraints { make in + commentToolView.snp.remakeConstraints { make in make.left.equalTo(view) make.right.equalTo(view) make.bottom.equalTo(view) make.height.equalTo(BaseDimensions.bottomHeight + 48) } - tableView.snp.makeConstraints { make in + tableView.snp.remakeConstraints { make in make.left.equalTo(view) make.right.equalTo(view) make.top.equalTo(commentHeaderView.snp.bottom) @@ -261,7 +281,13 @@ class CommentViewController: ViewController { case PopoverMenu.copy.description: UIPasteboard.general.string = cell.comment?.content case PopoverMenu.report.description: - viewModel.itemReport.accept(comment) + guard let commentID = cell.comment?.id else { return } + + let reportMenuViewModel = ReportMenuViewModel.init(commentId: commentID, provider: viewModel.provider) + self.navigator.show(segue: .reportMenu(viewModel: reportMenuViewModel), sender: self, transition: .navigationPresent(type: .reportMenu)) + + + case PopoverMenu.delete.description: viewModel.itemDelete.accept(comment) default: break @@ -277,3 +303,4 @@ class CommentViewController: ViewController { } } + diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewModel.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewModel.swift index 5d6727f..51743f6 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewModel.swift @@ -48,10 +48,9 @@ class CommentViewModel: ViewModel, ViewModelType { let clearTextSubject = PublishRelay() let likeSelected = PublishRelay() - let itemReport = PublishRelay() let itemDelete = PublishRelay() - + var journal: Journal var subPage: Int = 1 @@ -159,16 +158,18 @@ class CommentViewModel: ViewModel, ViewModelType { } }) - itemReport.subscribe { comment in - - }.disposed(by: rx.disposeBag) itemDelete.subscribe { comment in - + guard let commentID = comment.element?.id else { return } + self.requestDeleteComment(commentId: commentID) + .subscribe { _ in + self.deleteComment(commentId: commentID) + } onError: { error in + + }.disposed(by: self.rx.disposeBag) }.disposed(by: rx.disposeBag) - return Output.init(items: items, itemSelected: itemSelected, @@ -219,6 +220,12 @@ class CommentViewModel: ViewModel, ViewModelType { .trackError(error) } + + func requestDeleteComment(commentId: String) -> Observable { + return self.provider.deleteComment(commentId: commentId) + .trackActivity(loading) + .trackError(error) + } @@ -243,6 +250,17 @@ class CommentViewModel: ViewModel, ViewModelType { items.accept(updatedSections) } + func deleteComment(commentId: String) { + let updatedSections = items.value.map { section -> CommentSection in + let updatedComments = section.items.filter { comment -> Bool in + return comment.id != commentId + } + return CommentSection(id: section.id, items: updatedComments) + } + + items.accept(updatedSections) + } + } diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailController.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailController.swift index 6feba12..1863f62 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailController.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailController.swift @@ -127,24 +127,25 @@ class JournalDetailController: TableViewController { output.modelSelected.drive {[weak self] journalItem in + self?.deselectSelectedRow() switch journalItem { case .audioItem(let track): - let audioModels: [AudioTrack] = output.items.value.flatMap { section -> [JournalItem] in + let audioTracks: [AudioTrack] = output.items.value.flatMap { section -> [AudioTrack] in switch section { - case .audioSection(header: nil, items: let items): - return items + case .audioSection(_, let journalItems): + return journalItems.compactMap { item -> AudioTrack? in + if case .audioItem(let model) = item { + return model + } else { + return nil + } + } default: return [] } - }.compactMap { item -> AudioTrack? in - if case let .audioItem(model) = item { - return model - } else { - return nil - } } - + let playerViewModel = PlayerViewModel.init(track: track, provider: viewModel.provider) self?.navigator.show(segue: .player(viewModel: playerViewModel), sender: self, transition: .modal) @@ -152,7 +153,7 @@ class JournalDetailController: TableViewController { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // 这里放置延迟执行的代码 - AudioManager.sharedInstance.setPlaylist(list: audioModels) + AudioManager.sharedInstance.setPlaylist(list: audioTracks) AudioManager.sharedInstance.playTrack(track: track) } diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/ReportMenuViewController.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/ReportMenuViewController.swift new file mode 100644 index 0000000..a8b570a --- /dev/null +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/ReportMenuViewController.swift @@ -0,0 +1,187 @@ +// +// ReportMenuViewController.swift +// IndieMusic +// +// Created by WenLei on 2024/2/20. +// + +import UIKit + +import UIKit +import RxSwift +import RxCocoa +import RxDataSources +import SVProgressHUD + +class ReportMenuViewModel: ViewModel, ViewModelType { + + struct Input { + let viewWillAppear: ControlEvent + let modelSelected: Driver + + } + + struct Output { + let items: BehaviorRelay<[SheetSection]> + let modelSelected: Driver + let popView: PublishSubject + } + + let items = BehaviorRelay<[SheetSection]>.init(value: []) + let popView = PublishSubject.init() + + let commentId: String + + init(commentId: String, provider: IndieMusicAPI) { + self.commentId = commentId + super.init(provider: provider) + } + + func transform(input: Input) -> Output { + + input.viewWillAppear.subscribe { (_) in + + let array = ReportType.allCases.map { reportType in + return Sheet.report(reportType) + } + + + self.items.accept([SheetSection.init(items: array)]) + + }.disposed(by: rx.disposeBag) + + + input.modelSelected.drive { sheet in + switch sheet { + case .report(let reportType): + + self.requestCommentReport(commentId: self.commentId, type: reportType.rawValue) + .subscribe { _ in + SVProgressHUD.showText(withStatus: "提交成功") + self.popView.onNext(()) + } onError: { error in + + }.disposed(by: self.rx.disposeBag) + default: break + } + + }.disposed(by: rx.disposeBag) + + + + return Output.init(items: items, + modelSelected: input.modelSelected, + popView: popView) + } + + func requestCommentReport(commentId: String, type: String) -> Observable { + self.provider.commentReport(commentId: commentId, type: type) + .trackActivity(loading) + .trackError(error) + } + + + +} + +class ReportMenuViewController: ViewController, UIScrollViewDelegate { + + let footerView: UIView = { + let footerView = UIView.init(frame: CGRect.init(x: 0, y: 0, width: BaseDimensions.screenWidth, height: 48)) + + + return footerView + }() + + lazy var dismissButton: UIButton = { + let dismissButton = UIButton.init(frame: footerView.bounds) + dismissButton.titleLabel?.font = UIFont.systemFont(ofSize: 15) + dismissButton.setTitleColor(.primaryText(), for: .normal) + dismissButton.setTitle("取消", for: .normal) + return dismissButton + }() + + lazy var tableView: TableView = { + let view = TableView(frame: view.bounds, style: .plain) + view.rx.setDelegate(self).disposed(by: rx.disposeBag) + return view + }() + + + override func viewDidLoad() { + super.viewDidLoad() + + } + + override func makeUI() { + super.makeUI() + view.backgroundColor = .white + + footerView.addSubview(dismissButton) + + tableView.tableFooterView = footerView + + tableView.register(SheetViewCell.self, forCellReuseIdentifier: "SheetViewCell") + + view.addSubview(tableView) + } + + + + override func bindViewModel() { + super.bindViewModel() + + guard let viewModel = viewModel as? ReportMenuViewModel else { return } + + let input = ReportMenuViewModel.Input.init(viewWillAppear: rx.viewWillAppear, + modelSelected: tableView.rx.modelSelected(Sheet.self).asDriver()) + + let output = viewModel.transform(input: input) + + let dataSource = ReportMenuViewController.dataSource() + + output.items.bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag) + + + + output.popView.subscribe { [weak self] _ in + self?.navigator.dismiss(sender: self) + }.disposed(by: rx.disposeBag) + + dismissButton.rx.tap.subscribe { [weak self] _ in + self?.navigator.dismiss(sender: self) + }.disposed(by: rx.disposeBag) + + + } + + +} + +extension ReportMenuViewController { + static func dataSource() -> RxTableViewSectionedReloadDataSource { + return RxTableViewSectionedReloadDataSource( + configureCell: { dataSource, tableView, indexPath, item in + let cell: SheetViewCell = tableView.dequeueReusableCell(withIdentifier: "SheetViewCell", for: indexPath) as! SheetViewCell + cell.sheet = item + + return cell + } + ) + } + +} + +extension ReportMenuViewController: UIViewControllerTransitioningDelegate { + func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { + + let carePresentationVC = CardPresentationController.init(presentedViewController: presented, presenting: presenting) + carePresentationVC.viewType = .reportMenu + + return carePresentationVC + } +} + + + + diff --git a/IndieMusic/IndieMusic/Modules/Message/MessageViewController.swift b/IndieMusic/IndieMusic/Modules/Message/MessageViewController.swift index 1ef81cd..6fbd5b6 100644 --- a/IndieMusic/IndieMusic/Modules/Message/MessageViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Message/MessageViewController.swift @@ -136,6 +136,14 @@ class MessageViewController: ViewController, UIScrollViewDelegate { output.modelSelected.drive { messageSectionItem in + switch messageSectionItem { + case .customMessage(let customMessageType): + self.navigateToScreen(customMessageType: customMessageType) + case .messageItem(let message): break + case .activitiesItem(let message): break + + } + }.disposed(by: rx.disposeBag) @@ -169,6 +177,23 @@ class MessageViewController: ViewController, UIScrollViewDelegate { } } + + func navigateToScreen(customMessageType: CustomMessageType) { + guard let viewModel = viewModel as? MessageViewModel else { return } + + switch customMessageType { + case .comment: + let myCommentListViewModel = MyCommentListViewModel(provider: viewModel.provider) + self.navigator.show(segue: .myCommentList(viewModel: myCommentListViewModel), sender: self) + case .like: + let myLikeListViewModel = MyLikeListViewModel(provider: viewModel.provider) + self.navigator.show(segue: .myLikeList(viewModel: myLikeListViewModel), sender: self) + case .follow: + let followersViewModel = FollowersViewModel(provider: viewModel.provider) + self.navigator.show(segue: .followers(viewModel: followersViewModel), sender: self) + } + } + } diff --git a/IndieMusic/IndieMusic/Modules/Message/MessageViewModel.swift b/IndieMusic/IndieMusic/Modules/Message/MessageViewModel.swift index 27eb93c..0b6be07 100644 --- a/IndieMusic/IndieMusic/Modules/Message/MessageViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/Message/MessageViewModel.swift @@ -43,119 +43,74 @@ class MessageViewModel: ViewModel, ViewModelType { func transform(input: Input) -> Output { - input.headerRefresh.withLatestFrom(input.messageType) - .flatMapLatest({ [weak self] (messageType) -> Observable<[Message]> in - guard let self = self else { return Observable.just([]) } - self.page = 1 - - if messageType == .message { - return self.requestMessageList(page: self.page, size: 10) - .trackActivity(self.headerLoading) - } else { - return self.requestActivitiesList(page: self.page, size: 10) - .trackActivity(self.headerLoading) - } - - }) - .subscribe(onNext: { (items) in - if self.messageType.value == .message { - let new = items.map { message in - MessageSectionItem.messageItem(model: message) - } - - self.items.accept([MessageSection.message(items: new)]) - } else { - var new = items.map { message in - MessageSectionItem.activitiesItem(model: message) - } - - self.items.accept([MessageSection.activities(items: new)]) - - } - - - }).disposed(by: rx.disposeBag) +// input.headerRefresh.withLatestFrom(input.messageType) +// .flatMapLatest({ [weak self] (messageType) -> Observable<[Message]> in +// guard let self = self else { return Observable.just([]) } +// self.page = 1 +// +// return self.requestActivitiesList(page: self.page, size: 10) +// .trackActivity(self.headerLoading) +// }) +// .subscribe(onNext: { (items) in +// var new = items.map { message in +// MessageSectionItem.activitiesItem(model: message) +// } +// +// self.items.accept([MessageSection.activities(items: new)]) +// }).disposed(by: rx.disposeBag) - input.footerRefresh.withLatestFrom(input.messageType) - .flatMapLatest({ [weak self] (messageType) -> Observable<[Message]> in - guard let self = self else { return Observable.just([]) } - self.page += 1 - - if messageType == .message { - return self.requestMessageList(page: self.page, size: 10) - .trackActivity(self.headerLoading) - } else { - return self.requestActivitiesList(page: self.page, size: 10) - .trackActivity(self.headerLoading) - } - }) - .subscribe(onNext: { (items) in - if self.messageType.value == .message { - var arr = self.items.value.first?.items - var new = items.map { message in - MessageSectionItem.messageItem(model: message) - } - arr?.append(contentsOf: new) - - self.items.accept([MessageSection.message(items: new)]) - } else { - var arr = self.items.value.first?.items - var new = items.map { message in - MessageSectionItem.activitiesItem(model: message) - } - arr?.append(contentsOf: new) - - self.items.accept([MessageSection.activities(items: new)]) } - }).disposed(by: rx.disposeBag) +// input.footerRefresh.withLatestFrom(input.messageType) +// .flatMapLatest({ [weak self] (messageType) -> Observable<[Message]> in +// guard let self = self else { return Observable.just([]) } +// self.page += 1 +// +// return self.requestActivitiesList(page: self.page, size: 10) +// .trackActivity(self.headerLoading) +// }) +// .subscribe(onNext: { (items) in +// if self.messageType.value == .message { +// var arr = self.items.value.first?.items +// var new = items.map { message in +// MessageSectionItem.messageItem(model: message) +// } +// arr?.append(contentsOf: new) +// +// self.items.accept([MessageSection.message(items: new)]) +// } else { +// var arr = self.items.value.first?.items +// var new = items.map { message in +// MessageSectionItem.activitiesItem(model: message) +// } +// arr?.append(contentsOf: new) +// +// self.items.accept([MessageSection.activities(items: new)]) } +// }).disposed(by: rx.disposeBag) input.messageType.subscribe { [weak self] messageType in - guard let self = self else { return } + guard let self = self, let messageType = messageType.element else { return } self.messageType.accept(messageType) print("messageType \(messageType)") self.page = 1 switch messageType { case .message: - let comment = CustomMessageType.comment("测试") - let like = CustomMessageType.like("测试1") - let follow = CustomMessageType.follow("测试2") - - let commentMessage = MessageSectionItem.customMessage(customMessageType: comment) - let likeMessage = MessageSectionItem.customMessage(customMessageType: like) - let followMessage = MessageSectionItem.customMessage(customMessageType: follow) - - - - self.messageItems.accept([MessageSection.message(items: [commentMessage, likeMessage, followMessage])]) + self.requestMessageList() + .subscribe { messageList in + var items = [messageList.comment, messageList.thumbup, messageList.follow] + + let comment = MessageSectionItem.customMessage(customMessageType: .comment(messageList.comment.sendUserNickName ?? "")) + let thumbup = MessageSectionItem.customMessage(customMessageType: .like(messageList.thumbup.sendUserNickName ?? "")) + let follow = MessageSectionItem.customMessage(customMessageType: .follow(messageList.follow.sendUserNickName ?? "")) - - - -// self.requestMessageList(page: self.page, size: 10) -// .subscribe { messageArray in -// var new = messageArray.map { message in -// MessageSectionItem.messageItem(model: message) -// } -// -// self.messageItems.accept([MessageSection.message(items: new)]) -// } onError: { error in -// -// }.disposed(by: self.rx.disposeBag) - - case .activities: - self.requestMessageList(page: self.page, size: 10) - .subscribe { [weak self] messageArray in - guard let self = self else { return } - - var new = messageArray.map { message in - MessageSectionItem.activitiesItem(model: message) - } - self.activitiesItems.accept([MessageSection.activities(items: new)]) + + self.messageItems.accept([MessageSection.message(items: [comment, thumbup, follow])]) } onError: { error in - + }.disposed(by: self.rx.disposeBag) + + case .activities: break } @@ -191,17 +146,17 @@ class MessageViewModel: ViewModel, ViewModelType { } - func requestMessageList(page: Int, size: Int) -> Observable<[Message]> { - return self.provider.messageList(page: page, size: size) + func requestMessageList() -> Observable { + return self.provider.messageList() .trackActivity(loading) .trackError(error) } - func requestActivitiesList(page: Int, size: Int) -> Observable<[Message]> { - return self.provider.messageList(page: page, size: size) - .trackActivity(loading) - .trackError(error) - } +// func requestActivitiesList(page: Int, size: Int) -> Observable<[Message]> { +// return self.provider.messageList(page: page, size: size) +// .trackActivity(loading) +// .trackError(error) +// } // func requestSingleList(page: Int, size: Int) -> Observable<[AudioTrack]> { diff --git a/IndieMusic/IndieMusic/Modules/Player/PlayerView.swift b/IndieMusic/IndieMusic/Modules/Player/PlayerView.swift index 0734885..a430a78 100644 --- a/IndieMusic/IndieMusic/Modules/Player/PlayerView.swift +++ b/IndieMusic/IndieMusic/Modules/Player/PlayerView.swift @@ -9,6 +9,7 @@ import UIKit import SVProgressHUD import MarqueeLabel import AVFoundation +import RxDataSources class PlayerViewTopBar: UIView { lazy var dropButton: UIButton = { @@ -204,7 +205,6 @@ class PlayerScrollView: UIScrollView { var audioTrack: AudioTrack? { didSet { - print("audioTrack nil \(audioTrack)") guard let audioTrack = audioTrack else { return } playerInfoView.titleLabel.text = audioTrack.title @@ -214,7 +214,7 @@ class PlayerScrollView: UIScrollView { playerInfoView.updateData(audioTrack: audioTrack) - print("audioTrack \(audioTrack)") + playerLyricsView.audioTrack = audioTrack } } @@ -289,9 +289,9 @@ class PlayerScrollView: UIScrollView { } -// let playerInfoPoint = convert(point, to: playerInfoView) -// if playerInfoView.point(inside: playerInfoPoint, with: event) { -// return playerInfoView +// let playerLyricsPoint = convert(point, to: playerLyricsView) +// if playerLyricsView.point(inside: playerLyricsPoint, with: event) { +// return playerLyricsView // } @@ -339,7 +339,8 @@ class PlayerLyricsView: UIView { lazy var tableView: UITableView = { let tableView = UITableView.init() tableView.backgroundColor = .clear - + tableView.register(PlayerLyricsCell.self, forCellReuseIdentifier: "PlayerLyricsCell") + return tableView }() @@ -352,7 +353,7 @@ class PlayerLyricsView: UIView { lazy var audioTrackLabel: UILabel = { let audioTrackLabel = UILabel.init() - audioTrackLabel.font = UIFont.systemFont(ofSize: 20) + audioTrackLabel.font = UIFont.systemFont(ofSize: 20, weight: .medium) audioTrackLabel.textColor = UIColor.init(hex: 0xFFFFFF) return audioTrackLabel @@ -366,6 +367,16 @@ class PlayerLyricsView: UIView { return artistLabel }() + var audioTrack: AudioTrack? { + didSet { + guard let audioTrack = audioTrack else { return } + audioTrackLabel.text = audioTrack.title + + artistLabel.text = (audioTrack.artist ?? "") + "/" + (audioTrack.album ?? "") + } + } + + override init(frame: CGRect) { super.init(frame: frame) @@ -421,26 +432,33 @@ class PlayerLyricsView: UIView { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - super.hitTest(point, with: event) - if self.isHidden { - return super.hitTest(point, with: event) + return nil } - - let sliderPoint = convert(point, to: tableView) - if tableView.point(inside: sliderPoint, with: event) { - return tableView - } - - if self.bounds.contains(point) { - return self + let tableViewPoint = convert(point, to: tableView) + if tableView.point(inside: tableViewPoint, with: event) { + return tableView.hitTest(tableViewPoint, with: event) } return super.hitTest(point, with: event) + } + +} + +extension PlayerLyricsView { + static func dataSource() -> RxTableViewSectionedReloadDataSource { + return RxTableViewSectionedReloadDataSource { dataSource, tableView, indexPath, item in + let cell: PlayerLyricsCell = tableView.dequeueReusableCell(withIdentifier: "PlayerLyricsCell", for: indexPath) as! PlayerLyricsCell + cell.lyricLine = item + + return cell + + } } + } class PlayerLyricsCell: UITableViewCell { @@ -448,21 +466,16 @@ class PlayerLyricsCell: UITableViewCell { let lyricsLabel = UILabel.init() lyricsLabel.font = UIFont.systemFont(ofSize: 18) lyricsLabel.textColor = .init(hex: 0xFFFFFF, alpha: 1) - + lyricsLabel.numberOfLines = 0 return lyricsLabel }() - override func awakeFromNib() { - super.awakeFromNib() - // Initialization code - } - - override func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - - // Configure the view for the selected state + var lyricLine: LyricLine? { + didSet { + lyricsLabel.text = lyricLine?.text + } } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -480,7 +493,6 @@ class PlayerLyricsCell: UITableViewCell { func makeUI() { backgroundColor = .clear -// contentView.backgroundColor = .clear contentView.addSubview(lyricsLabel) diff --git a/IndieMusic/IndieMusic/Modules/Player/PlayerViewController.swift b/IndieMusic/IndieMusic/Modules/Player/PlayerViewController.swift index 45d41bd..dd27a68 100644 --- a/IndieMusic/IndieMusic/Modules/Player/PlayerViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Player/PlayerViewController.swift @@ -82,7 +82,6 @@ class PlayerViewController: ViewController { override func bindViewModel() { super.bindViewModel() - playerScrollView.playerLyricsView.tableView.register(PlayerLyricsCell.self, forCellReuseIdentifier: "PlayerLyricsCell") guard let viewModel = viewModel as? PlayerViewModel, let audioTrack = viewModel.track else { return } @@ -106,7 +105,7 @@ class PlayerViewController: ViewController { let output = viewModel.transform(input: input) - let dataSource = PlayerViewController.dataSource() + let dataSource = PlayerLyricsView.dataSource() output.items.bind(to: playerScrollView.playerLyricsView.tableView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag) @@ -116,15 +115,7 @@ class PlayerViewController: ViewController { }.disposed(by: rx.disposeBag) - -// viewModel.isLike -// .bind(to: self.playerScrollView.playerInfoView.likeButton.rx.isSelected) -// .disposed(by: rx.disposeBag) - -// viewModel.isLike.subscribe { [weak self] isLike in -// self?.playerScrollView.playerInfoView.likeButton.isSelected = isLike -// }.disposed(by: rx.disposeBag) - + output.toShare.subscribe { [weak self] _ in let share = ShareActionViewModel.init(audioTrack: viewModel.track, journal: nil, provider: viewModel.provider) @@ -145,14 +136,15 @@ class PlayerViewController: ViewController { self?.playerControlView.playButton.isSelected = isPlaying } .disposed(by: rx.disposeBag) - print("audioTrack222 ") + - output.audioTrack.subscribe { [weak self] audioTrack in + output.currentAudioTrack.subscribe { [weak self] audioTrack in print("audioTrack111 \(audioTrack)") guard let audioTrack = audioTrack.element else { return } self?.playerScrollView.audioTrack = audioTrack - self?.blurEffectView.imageView.kf.setImage(with: URL.init(string: audioTrack.pic ?? "")) + self?.blurEffectView.imageView.kf.setImage(with: URL.init(string: audioTrack?.pic ?? "")) + }.disposed(by: rx.disposeBag) @@ -208,6 +200,16 @@ class PlayerViewController: ViewController { + output.moreButtonTrigger.drive { [weak self] _ in + + let audioMoreActionViewModel = AudioMoreActionViewModel.init(audioTrack: viewModel.currentAudioTrack, provider: viewModel.provider) + + self?.navigator.show(segue: .audioMore(viewModel: audioMoreActionViewModel), sender: self, transition: .navigationPresent(type: .audioMore)) + + + + }.disposed(by: rx.disposeBag) + } @@ -246,43 +248,8 @@ class PlayerViewController: ViewController { } - @objc func handleDismissGesture(_ gesture: UIPanGestureRecognizer) { -// let translation = gesture.translation(in: view) -// let progress = translation.y / view.bounds.height -// -// switch gesture.state { -// case .changed: -// // 这里可以添加一些交互式动画的逻辑 -// break -// case .ended: -// if progress > 0.5 { -// // 如果下滑距离足够,关闭视图控制器 -// self.navigationController?.dismiss(animated: true, completion: nil) -// } else { -// // 如果下滑距离不够,可以取消或重置动画 -// } -// default: -// break -// } - } - - } -extension PlayerViewController { - static func dataSource() -> RxTableViewSectionedReloadDataSource { - return RxTableViewSectionedReloadDataSource { dataSource, tableView, indexPath, item in - let cell: PlayerLyricsCell = tableView.dequeueReusableCell(withIdentifier: "PlayerLyricsCell", for: indexPath) as! PlayerLyricsCell - cell.lyricsLabel.text = item.lyrics - return cell - - } - - } - - - -} extension PlayerViewController: SegmentControlDelegate { func didSelected(segement: SegmentControl, index: Int) { @@ -294,6 +261,11 @@ extension PlayerViewController: SegmentControlDelegate { extension PlayerViewController: PlayerScrollViewDelegate { func scrollViewDidChange(index: Int) { playerViewTopBar.segmentControl.move(to: index, animated: true) + + guard let nav = self.navigationController as?NavigationController else { return } + nav.isPullToDismissEnabled = index != 1 + + } } diff --git a/IndieMusic/IndieMusic/Modules/Player/PlayerViewModel.swift b/IndieMusic/IndieMusic/Modules/Player/PlayerViewModel.swift index 3ea7f1b..923aa2c 100644 --- a/IndieMusic/IndieMusic/Modules/Player/PlayerViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/Player/PlayerViewModel.swift @@ -33,7 +33,7 @@ class PlayerViewModel: ViewModel, ViewModelType { struct Output { let items: BehaviorRelay<[PlayerLyricsSection]> let shuffle: BehaviorRelay - let audioTrack: PublishSubject + let currentAudioTrack: BehaviorRelay let toShare: PublishSubject let toList: BehaviorRelay let playShuffleType: BehaviorRelay @@ -41,6 +41,7 @@ class PlayerViewModel: ViewModel, ViewModelType { let duration: BehaviorRelay let time: BehaviorRelay let isPlaying: BehaviorRelay + let moreButtonTrigger: Driver } @@ -49,7 +50,7 @@ class PlayerViewModel: ViewModel, ViewModelType { let isPlaying = BehaviorRelay.init(value: false) let shuffle = BehaviorRelay.init(value: false) - let audioTrack = PublishSubject.init() + let currentAudioTrack = BehaviorRelay.init(value: nil) let toShare = PublishSubject.init() let toList = BehaviorRelay.init(value: ()) @@ -65,9 +66,9 @@ class PlayerViewModel: ViewModel, ViewModelType { super.init(provider: provider) guard let track = track else { return } - print("audioTrack333 ") + self.track = track - self.audioTrack.onNext(track) + self.currentAudioTrack.accept(track) AudioManager.sharedInstance.player?.addProgressObserver { currentTime, duration, progress in @@ -85,15 +86,15 @@ class PlayerViewModel: ViewModel, ViewModelType { input.likeButtonTrigger.asObservable() - .withLatestFrom(audioTrack) // 使用 withLatestFrom 获取 audioTrack 的最新值 - .subscribe(onNext: { [weak self] latestAudioTrack in - guard let self = self else { return } - if latestAudioTrack.haveCollect == false { - self.requestLike(objectId: latestAudioTrack.id) + .subscribe(onNext: {[weak self] in + guard let self = self, + let audioTrack = self.currentAudioTrack.value else { return } + if audioTrack.haveCollect == false { + self.requestLike(objectId: audioTrack.id) .subscribe { [weak self] _ in guard let self = self else { return } - var new = latestAudioTrack + var new = audioTrack new.haveCollect = true self.updateAudioTrack(audioTrack: new) @@ -101,11 +102,11 @@ class PlayerViewModel: ViewModel, ViewModelType { }.disposed(by: self.rx.disposeBag) } else { - self.requestCancelLike(objectId: latestAudioTrack.id) + self.requestCancelLike(objectId: audioTrack.id) .subscribe { [weak self] _ in guard let self = self else { return } - var new = latestAudioTrack + var new = audioTrack new.haveCollect = false self.updateAudioTrack(audioTrack: new) @@ -115,13 +116,7 @@ class PlayerViewModel: ViewModel, ViewModelType { } }) .disposed(by: rx.disposeBag) - - - let lyrics = PlayerLyrics.init(lyrics: "1233211232") - let section = PlayerLyricsSection.init(items: [lyrics, lyrics, lyrics, lyrics, lyrics, lyrics, lyrics, lyrics, lyrics, lyrics, lyrics, lyrics, lyrics, lyrics]) - - items.accept([section]) - + input.shareButtonTrigger.drive { _ in self.toShare.onNext(()) @@ -162,7 +157,7 @@ class PlayerViewModel: ViewModel, ViewModelType { input.notiPlayAudioTrack.subscribe { [weak self] noti in guard let track = noti.element?.object as? AudioTrack else { return } - self?.audioTrack.onNext(track) + self?.currentAudioTrack.accept(track) AudioManager.sharedInstance.player?.addProgressObserver { currentTime, duration, progress in self?.progress.accept(Float(progress)) @@ -170,7 +165,17 @@ class PlayerViewModel: ViewModel, ViewModelType { self?.time.accept(Float(currentTime)) print("viewmodel addProgressObserver : \(progress)") } + self?.items.accept([PlayerLyricsSection.init(items: [])]) + + + if let lrc = track.lrc { + self?.downloadLyricContent(from: lrc, completion: { content in + guard let content = content else { return } + self?.items.accept([PlayerLyricsSection.init(items: self?.parseLyricContent(content) ?? [])]) + }) + + } }.disposed(by: rx.disposeBag) @@ -203,19 +208,20 @@ class PlayerViewModel: ViewModel, ViewModelType { return Output.init(items: items, shuffle: shuffle, - audioTrack: audioTrack, + currentAudioTrack: currentAudioTrack, toShare: toShare, toList: toList, playShuffleType: playShuffleType, progress: progress, duration: duration, time: time, - isPlaying: isPlaying) + isPlaying: isPlaying, + moreButtonTrigger: input.moreButtonTrigger) } func updateAudioTrack(audioTrack: AudioTrack) { - self.audioTrack.onNext(audioTrack) + self.currentAudioTrack.accept(audioTrack) if let index = AudioManager.sharedInstance.playlist?.firstIndex(where: { $0.id == audioTrack.id }) { @@ -245,25 +251,45 @@ class PlayerViewModel: ViewModel, ViewModelType { extension PlayerViewModel { func playTrack(track: AudioTrack) { - self.audioTrack.onNext(track) - -// if let url = track.album?.images.first?.url { -// ImageManager.sharedInstance.getImageFromURL(url: URL(string: url)!) { result in -// switch result { -// case .success(let image): -// self.coverImage.image = image -// if let color = self.coverImage.image?.averageColor, let color2 = color.darker(by: 20) { -// self.gradientBackground.colors = [color.cgColor, color2.cgColor] -// } -// case .failure: -// break -// } -// } -// } + self.currentAudioTrack.accept(track) AudioManager.sharedInstance.player?.addProgressObserver { time, duration, progress in self.progress.accept(Float(progress)) } } + + + + + func downloadLyricContent(from urlString: String, completion: @escaping (String?) -> Void) { + guard let url = URL(string: urlString) else { + print("Invalid URL") + completion(nil) + return + } + + let task = URLSession.shared.dataTask(with: url) { data, response, error in + guard let data = data, error == nil else { + print("Error downloading lyric content: \(error?.localizedDescription ?? "Unknown error")") + completion(nil) + return + } + + let content = String(data: data, encoding: .utf8) + completion(content) + } + + task.resume() + } + + + func parseLyricContent(_ content: String) -> [LyricLine] { + let lines = content.split(separator: "\n") + return lines.map { substring in + return LyricLine(time: 0, text: String(substring)) + } + } + + } diff --git a/IndieMusic/IndieMusic/Modules/Setting/EditInfo/EditSexViewController.swift b/IndieMusic/IndieMusic/Modules/Setting/EditInfo/EditSexViewController.swift index 61dced4..b3d6d99 100644 --- a/IndieMusic/IndieMusic/Modules/Setting/EditInfo/EditSexViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Setting/EditInfo/EditSexViewController.swift @@ -14,18 +14,17 @@ class EditSexViewModel: ViewModel, ViewModelType { struct Input { let viewWillAppear: ControlEvent - let selection: Driver + let modelSelected: Driver } struct Output { let items: BehaviorRelay<[SheetSection]> - let itemSelected: PublishSubject + let modelSelected: Driver let popView: PublishSubject } let items = BehaviorRelay<[SheetSection]>.init(value: []) - let itemSelected = PublishSubject() let popView = PublishSubject.init() var sexType = PublishRelay.init() @@ -39,29 +38,30 @@ class EditSexViewModel: ViewModel, ViewModelType { func transform(input: Input) -> Output { input.viewWillAppear.subscribe { (_) in + let male = Sheet.sex(.male) + let female = Sheet.sex(.female) + let secret = Sheet.sex(.secret) + - }.disposed(by: rx.disposeBag) - - let male = Sheet(sex: .male) - let female = Sheet(sex: .female) - let secret = Sheet(sex: .secret) + self.items.accept([SheetSection.init(items: [male, female, secret])]) + }.disposed(by: rx.disposeBag) - items.accept([SheetSection.init(items: [male, female, secret])]) - - input.selection.drive { indexPath in - guard let sectionItem = self.items.value.first?.items[indexPath.row] else { return } - self.itemSelected.onNext(sectionItem) - + input.modelSelected.drive { sheet in + switch sheet { + case .sex(let sexType): + self.sexType.accept(sexType) + self.popView.onNext(()) + default: break + } - self.sexType.accept(sectionItem.sex) - self.popView.onNext(()) }.disposed(by: rx.disposeBag) + return Output.init(items: items, - itemSelected: itemSelected, + modelSelected: input.modelSelected, popView: popView) } @@ -122,7 +122,7 @@ class EditSexViewController: ViewController, UIScrollViewDelegate { guard let viewModel = viewModel as? EditSexViewModel else { return } let input = EditSexViewModel.Input.init(viewWillAppear: rx.viewWillAppear, - selection: tableView.rx.itemSelected.asDriver()) + modelSelected: tableView.rx.modelSelected(Sheet.self).asDriver()) let output = viewModel.transform(input: input) @@ -173,42 +173,3 @@ extension EditSexViewController: UIViewControllerTransitioningDelegate { -class SheetViewCell: UITableViewCell { - var titleLabel: UILabel = { - let titleLabel = UILabel.init() - titleLabel.textColor = .primaryText() - titleLabel.font = UIFont.systemFont(ofSize: 15) - - return titleLabel - }() - - var sheet: Sheet? { - didSet { - titleLabel.text = sheet?.sex.description - } - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - makeUI() - } - - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func makeUI() { - contentView.addSubview(titleLabel) - - - titleLabel.snp.makeConstraints { make in - make.centerX.equalTo(contentView) - make.height.equalTo(51) - make.top.equalTo(contentView) - make.bottom.equalTo(contentView) - } - } - -} diff --git a/IndieMusic/IndieMusic/Modules/Setting/EditInfo/SheetViewCell.swift b/IndieMusic/IndieMusic/Modules/Setting/EditInfo/SheetViewCell.swift new file mode 100644 index 0000000..e5623ae --- /dev/null +++ b/IndieMusic/IndieMusic/Modules/Setting/EditInfo/SheetViewCell.swift @@ -0,0 +1,48 @@ +// +// SheetViewCell.swift +// IndieMusic +// +// Created by WenLei on 2024/2/20. +// + +import Foundation + +class SheetViewCell: UITableViewCell { + var titleLabel: UILabel = { + let titleLabel = UILabel.init() + titleLabel.textColor = .primaryText() + titleLabel.font = UIFont.systemFont(ofSize: 15) + + return titleLabel + }() + + var sheet: Sheet? { + didSet { + titleLabel.text = sheet?.description + } + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + makeUI() + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func makeUI() { + contentView.addSubview(titleLabel) + + + titleLabel.snp.makeConstraints { make in + make.centerX.equalTo(contentView) + make.height.equalTo(51) + make.top.equalTo(contentView) + make.bottom.equalTo(contentView) + } + } + +} diff --git a/IndieMusic/IndieMusic/Modules/Setting/FeedbackMenuViewController.swift b/IndieMusic/IndieMusic/Modules/Setting/FeedbackMenuViewController.swift new file mode 100644 index 0000000..78ded56 --- /dev/null +++ b/IndieMusic/IndieMusic/Modules/Setting/FeedbackMenuViewController.swift @@ -0,0 +1,174 @@ +// +// FeedbackMenuViewController.swift +// IndieMusic +// +// Created by WenLei on 2024/2/21. +// + +import Foundation +import RxSwift +import RxCocoa +import RxDataSources + +class FeedbackMenuViewModel: ViewModel, ViewModelType { + + struct Input { + let viewWillAppear: ControlEvent + let modelSelected: Driver + + } + + struct Output { + let items: BehaviorRelay<[SheetSection]> + let modelSelected: Driver + let popView: PublishSubject + } + + let items = BehaviorRelay<[SheetSection]>.init(value: []) + let popView = PublishSubject.init() + var feedbackType = PublishRelay.init() + + + init(feedbackType: PublishRelay, provider: IndieMusicAPI) { + super.init(provider: provider) + + self.feedbackType = feedbackType + } + + func transform(input: Input) -> Output { + + input.viewWillAppear.subscribe { (_) in + let array = FeedbackType.allCases.map { feedbackType in + return Sheet.feedback(feedbackType) + } + + self.items.accept([SheetSection.init(items: array)]) + + }.disposed(by: rx.disposeBag) + + input.modelSelected.drive { sheet in + switch sheet { + case .feedback(let feedbackType): + self.feedbackType.accept(feedbackType) + self.popView.onNext(()) + default: break + } + + }.disposed(by: rx.disposeBag) + + + + + return Output.init(items: items, + modelSelected: input.modelSelected, + popView: popView) + } + + + + +} + +class FeedbackMenuViewController: ViewController, UIScrollViewDelegate { + + let footerView: UIView = { + let footerView = UIView.init(frame: CGRect.init(x: 0, y: 0, width: BaseDimensions.screenWidth, height: 48)) + + + return footerView + }() + + lazy var dismissButton: UIButton = { + let dismissButton = UIButton.init(frame: footerView.bounds) + dismissButton.titleLabel?.font = UIFont.systemFont(ofSize: 15) + dismissButton.setTitleColor(.primaryText(), for: .normal) + dismissButton.setTitle("取消", for: .normal) + return dismissButton + }() + + lazy var tableView: TableView = { + let view = TableView(frame: view.bounds, style: .plain) +// view.emptyDataSetSource = self +// view.emptyDataSetDelegate = self + view.rx.setDelegate(self).disposed(by: rx.disposeBag) + return view + }() + + + override func viewDidLoad() { + super.viewDidLoad() + + } + + override func makeUI() { + super.makeUI() + view.backgroundColor = .white + + footerView.addSubview(dismissButton) + + tableView.tableFooterView = footerView + + tableView.register(SheetViewCell.self, forCellReuseIdentifier: "SheetViewCell") + + view.addSubview(tableView) + } + + + + override func bindViewModel() { + super.bindViewModel() + + guard let viewModel = viewModel as? FeedbackMenuViewModel else { return } + + let input = FeedbackMenuViewModel.Input.init(viewWillAppear: rx.viewWillAppear, + modelSelected: tableView.rx.modelSelected(Sheet.self).asDriver()) + + let output = viewModel.transform(input: input) + + let dataSource = FeedbackMenuViewController.dataSource() + + output.items.bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag) + + + + output.popView.subscribe { [weak self] _ in + self?.navigator.dismiss(sender: self) + }.disposed(by: rx.disposeBag) + + dismissButton.rx.tap.subscribe { [weak self] _ in + self?.navigator.dismiss(sender: self) + }.disposed(by: rx.disposeBag) + + + } + + +} + +extension FeedbackMenuViewController { + static func dataSource() -> RxTableViewSectionedReloadDataSource { + return RxTableViewSectionedReloadDataSource( + configureCell: { dataSource, tableView, indexPath, item in + let cell: SheetViewCell = tableView.dequeueReusableCell(withIdentifier: "SheetViewCell", for: indexPath) as! SheetViewCell + cell.sheet = item + + return cell + } + ) + } + +} + +extension FeedbackMenuViewController: UIViewControllerTransitioningDelegate { + func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { + + let carePresentationVC = CardPresentationController.init(presentedViewController: presented, presenting: presenting) + carePresentationVC.viewType = .feedbackMenu + + return carePresentationVC + } +} + + + + diff --git a/IndieMusic/IndieMusic/Modules/Setting/FeedbackViewController.swift b/IndieMusic/IndieMusic/Modules/Setting/FeedbackViewController.swift index ca2dbaf..b94b16d 100644 --- a/IndieMusic/IndieMusic/Modules/Setting/FeedbackViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Setting/FeedbackViewController.swift @@ -85,6 +85,7 @@ class FeedbackViewController: ViewController { let input = FeedbackViewModel.Input.init(viewWillAppear: rx.viewWillAppear, modelSelected: feedbackDetailView.feedbackDetailSubView.collectionView.rx.modelSelected(FeedbackPhotoType.self).asDriver(), photoItems: photoItems, + feedbackMenuTrigger: self.feedbackTypeView.feedbackTypeControl.rx.controlEvent(.touchUpInside).asDriver(), category: category, content: feedbackDetailView.feedbackDetailSubView.textView.rx.text.asDriver(), contact: feedbackContactView.textFieldView.rx.text.asDriver(), @@ -108,6 +109,12 @@ class FeedbackViewController: ViewController { }.disposed(by: rx.disposeBag) + input.feedbackMenuTrigger.drive { _ in + let feedbackMenuViewModel = FeedbackMenuViewModel.init(feedbackType: viewModel.feedbackType, provider: viewModel.provider) + + self.navigator.show(segue: .feedbackMenu(viewModel: feedbackMenuViewModel), sender: self, transition: .navigationPresent(type: .feedbackMenu)) + }.disposed(by: rx.disposeBag) + output.toPhotoSelect.subscribe { imageArray in @@ -138,6 +145,19 @@ class FeedbackViewController: ViewController { }.disposed(by: rx.disposeBag) + + output.feedbackType.subscribe { feedbackType in + + self.feedbackTypeView.feedbackTypeControl.titleLabel.text = feedbackType.element?.description + + }.disposed(by: rx.disposeBag) + + + output.popView.subscribe { _ in + self.navigator.pop(sender: self) + }.disposed(by: rx.disposeBag) + + } @@ -176,7 +196,7 @@ class FeedbackViewController: ViewController { } -class FeedbackTypeView: UIView { +class FeedbackTypeView: UIControl { let titleLabel: UILabel = { let titleLabel = UILabel.init() titleLabel.text = "反馈类型*" diff --git a/IndieMusic/IndieMusic/Modules/Setting/FeedbackViewModel.swift b/IndieMusic/IndieMusic/Modules/Setting/FeedbackViewModel.swift index bca5dad..949aea2 100644 --- a/IndieMusic/IndieMusic/Modules/Setting/FeedbackViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/Setting/FeedbackViewModel.swift @@ -8,6 +8,7 @@ import Foundation import RxSwift import RxCocoa +import SVProgressHUD class FeedbackViewModel: ViewModel, ViewModelType { @@ -15,6 +16,7 @@ class FeedbackViewModel: ViewModel, ViewModelType { let viewWillAppear: ControlEvent let modelSelected: Driver let photoItems: PublishRelay<[UIImage]> + let feedbackMenuTrigger: Driver let category: PublishRelay let content: Driver let contact: Driver @@ -24,11 +26,15 @@ class FeedbackViewModel: ViewModel, ViewModelType { struct Output { let photoItems: BehaviorRelay<[FeedbackSection]> let toPhotoSelect: PublishRelay<[UIImage]> + let feedbackType: PublishRelay + let popView: PublishRelay } let photoItems = BehaviorRelay<[FeedbackSection]>.init(value: [.init(items: [.add])]) let toPhotoSelect = PublishRelay<[UIImage]>.init() + let feedbackType = PublishRelay.init() + let popView = PublishRelay.init() func transform(input: Input) -> Output { input.viewWillAppear.subscribe { (_) in @@ -69,36 +75,32 @@ class FeedbackViewModel: ViewModel, ViewModelType { -// input.confirmTrigger.drive { _ in - -// self.uploadFeedback(type: 0, content: "", images: [], contact: "") -// .subscribe { _ in -// -// } onError: { error in -// -// }.disposed(by: self.rx.disposeBag) -// }.disposed(by: rx.disposeBag) - let parms = Driver.combineLatest( input.photoItems.startWith([]).asDriver(onErrorJustReturn: []), - input.category.startWith("").asDriver(onErrorJustReturn: ""), - input.content, + feedbackType.startWith(FeedbackType.other).asDriver(onErrorJustReturn: FeedbackType.other), + input.content.filterNil(), input.contact ) input.confirmTrigger .withLatestFrom(parms) // 这里不需要额外的解构,因为闭包会直接接收到元组 - .flatMapLatest { tuple in // 使用单个参数接收元组 - // 解构元组以获取各个值 + .flatMapLatest { tuple in print("点击发送") - let (photoItems, category, content, contact) = tuple - // 确保content和contact有默认值,以避免Optional - return self.uploadFeedback(type: 0, content: "", images: [], contact: "") - .asDriver(onErrorJustReturn: ()) + let (photoItems, feedbackType, content, contact) = tuple + + return self.uploadFeedback(type: feedbackType.rawValue, content: content, images: photoItems, contact: contact ?? "") + .catch { error in + if case let HTTPServiceError.errorJudge(err) = error { + SVProgressHUD.showError(withStatus: err.message) + } + return Observable.empty() + } + .asDriver(onErrorDriveWith: Driver.empty()) } .drive(onNext: { _ in - + self.popView.accept(()) + SVProgressHUD.showText(withStatus: "感谢您的反馈") }) .disposed(by: rx.disposeBag) @@ -106,7 +108,9 @@ class FeedbackViewModel: ViewModel, ViewModelType { return Output.init(photoItems: self.photoItems, - toPhotoSelect: toPhotoSelect) + toPhotoSelect: toPhotoSelect, + feedbackType: feedbackType, + popView: popView) } diff --git a/IndieMusic/IndieMusic/Modules/Setting/SettingViewController.swift b/IndieMusic/IndieMusic/Modules/Setting/SettingViewController.swift index 57ab40f..8a400d8 100644 --- a/IndieMusic/IndieMusic/Modules/Setting/SettingViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Setting/SettingViewController.swift @@ -60,6 +60,7 @@ class SettingViewController: TableViewController { output.modelSelected.drive { [weak self] sectionItem in + self?.deselectSelectedRow() switch sectionItem { case .editInfo: diff --git a/IndieMusic/IndieMusic/Modules/Setting/ThanksViewController.swift b/IndieMusic/IndieMusic/Modules/Setting/ThanksViewController.swift index 51bed3d..511a698 100644 --- a/IndieMusic/IndieMusic/Modules/Setting/ThanksViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Setting/ThanksViewController.swift @@ -32,7 +32,7 @@ class ThanksViewController: ViewController { // 创建水平子组(每排) let verticalGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(151)) - let verticalGroup = NSCollectionLayoutGroup.horizontal(layoutSize: verticalGroupSize, subitem: item, count: 5) + let verticalGroup = NSCollectionLayoutGroup.horizontal(layoutSize: verticalGroupSize, subitem: item, count: 3) verticalGroup.interItemSpacing = .fixed(12) @@ -48,19 +48,11 @@ class ThanksViewController: ViewController { let collectionView = UICollectionView.init(frame: CGRect.zero, collectionViewLayout: layout) collectionView.register(ThanksViewCell.self, forCellWithReuseIdentifier: "ThanksViewCell") collectionView.showsHorizontalScrollIndicator = false - collectionView.backgroundColor = .init(hex: 0xfbfbfb) + collectionView.backgroundColor = .init(hex: 0xF8F8F8) return collectionView }() - let confirmButton: UIButton = { - let confirmButton = UIButton.init() - confirmButton.setTitle("成为贡献者", for: .normal) - confirmButton.titleLabel?.font = UIFont.systemFont(ofSize: 15) - confirmButton.backgroundColor = .init(hex: 0xAD3030) - - return confirmButton - }() @@ -74,8 +66,12 @@ class ThanksViewController: ViewController { override func makeUI() { super.makeUI() + let navTitleView = ThanksTitleView(frame: CGRect(x: 0, y: 0, width: 100, height: 40)) + navTitleView.backgroundColor = .red + self.navigationController?.navigationItem.titleView = navTitleView + view.addSubview(collectionView) - view.addSubview(confirmButton) + view.addSubview(thanksBottomView) } @@ -86,25 +82,17 @@ class ThanksViewController: ViewController { guard let viewModel = viewModel as? ThanksViewModel else { return } let input = ThanksViewModel.Input.init(viewWillAppear: rx.viewWillAppear, - selection: collectionView.rx.itemSelected.asDriver()) + modelSelected: collectionView.rx.modelSelected(Thanks.self).asDriver()) let output = viewModel.transform(input: input) let dataSource = ThanksViewController.dataSource() output.items.bind(to: collectionView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag) - - - output.itemSelected.subscribe { [weak self] sectionItem in + output.modelSelected.drive { [weak self] thanks in - - }.disposed(by: rx.disposeBag) - - - output.selection.drive { [weak self] sectionItem in - - let accountViewModel = AccountViewModel.init(provider: viewModel.provider) - self?.navigator.show(segue: .account(viewModel: accountViewModel), sender: self) + let personalViewModel = PersonalViewModel.init(userID: thanks.id, provider: viewModel.provider) + self?.navigator.show(segue: .personal(viewModel: personalViewModel), sender: self) }.disposed(by: rx.disposeBag) @@ -121,9 +109,11 @@ class ThanksViewController: ViewController { make.edges.equalTo(view) } - confirmButton.snp.makeConstraints { make in - make.centerX.equalTo(view) - make.bottom.equalTo(view).offset(-BaseDimensions.bottomHeight - 10) + thanksBottomView.snp.makeConstraints { make in + make.left.equalTo(view) + make.right.equalTo(view) + make.bottom.equalTo(view) + make.height.equalTo(BaseDimensions.bottomHeight + 93) } } @@ -203,6 +193,57 @@ class ThanksHeaderView: UICollectionReusableView { } +class ThanksTitleView: UIView { + let imageView: UIImageView = { + let imageView = UIImageView.init() + imageView.image = UIImage.init(named: "thanks_nav_icon") + return imageView + }() + + let titleLabel: UILabel = { + let titleLabel = UILabel.init() + titleLabel.font = UIFont.systemFont(ofSize: 17) + titleLabel.text = "感谢" + + return titleLabel + }() + + + override init(frame: CGRect) { + super.init(frame: frame) + + makeUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func makeUI() { + addSubview(titleLabel) + addSubview(imageView) + + } + + + override func layoutSubviews() { + super.layoutSubviews() + + titleLabel.snp.makeConstraints { make in + make.left.equalTo(self).offset(8) + make.top.equalTo(self).offset(8) + make.bottom.equalTo(self).offset(-8) + } + + imageView.snp.makeConstraints { make in + make.left.equalTo(titleLabel.snp.right).offset(5) + make.right.equalTo(self).offset(-8) + } + + + } +} + class ThanksViewCell: UICollectionViewCell { @@ -235,11 +276,12 @@ class ThanksViewCell: UICollectionViewCell { var thanks: Thanks? { didSet { - avatarView.kf.setImage(with: URL.init(string: thanks?.icon ?? "")) + guard let thanks = thanks else { return } + avatarView.kf.setImage(with: URL.init(string: thanks.avatar ?? "")) - titleLabel.text = thanks?.title + titleLabel.text = thanks.nickName - detailLabel.text = thanks?.detail + detailLabel.text = thanks.contributorRole } } @@ -254,6 +296,11 @@ class ThanksViewCell: UICollectionViewCell { } func makeUI() { + contentView.layer.cornerRadius = 5 + contentView.layer.masksToBounds = true + contentView.backgroundColor = .white + + contentView.addSubview(avatarView) contentView.addSubview(titleLabel) contentView.addSubview(detailLabel) @@ -267,14 +314,14 @@ class ThanksViewCell: UICollectionViewCell { titleLabel.snp.makeConstraints { make in make.top.equalTo(avatarView.snp.bottom).offset(18) - make.left.equalTo(contentView).offset(18) - make.right.equalTo(contentView).offset(-18) + make.left.equalTo(contentView).offset(5) + make.right.equalTo(contentView).offset(-5) } detailLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(1) - make.left.equalTo(contentView).offset(18) - make.right.equalTo(contentView).offset(-18) + make.left.equalTo(contentView).offset(5) + make.right.equalTo(contentView).offset(-5) } @@ -315,7 +362,7 @@ class ThanksBottomView: UIView { button.snp.makeConstraints { make in make.left.equalTo(self).offset(46) make.right.equalTo(self).offset(-46) - make.top.equalTo(self).offset(46) + make.centerY.equalTo(self).offset(10) make.height.equalTo(44) } } diff --git a/IndieMusic/IndieMusic/Modules/Setting/ThanksViewModel.swift b/IndieMusic/IndieMusic/Modules/Setting/ThanksViewModel.swift index 5194385..1ba63c3 100644 --- a/IndieMusic/IndieMusic/Modules/Setting/ThanksViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/Setting/ThanksViewModel.swift @@ -13,15 +13,13 @@ class ThanksViewModel: ViewModel, ViewModelType { struct Input { let viewWillAppear: ControlEvent - let selection: Driver + let modelSelected: Driver } struct Output { let items: BehaviorRelay<[ThanksSection]> - let selection: Driver - let itemSelected: PublishSubject - + let modelSelected: Driver } let itemSelected = PublishSubject() @@ -31,19 +29,26 @@ class ThanksViewModel: ViewModel, ViewModelType { input.viewWillAppear.subscribe { (_) in + self.requestThanksListData() + .subscribe { thanksArray in + self.items.accept([ThanksSection.init(header: "", items: thanksArray)]) + } onError: { error in + + }.disposed(by: self.rx.disposeBag) }.disposed(by: rx.disposeBag) - - input.selection.drive { indexPath in - guard let sectionItem = self.items.value.first?.items[indexPath.row] else { return } - - }.disposed(by: rx.disposeBag) return Output.init(items: items, - selection: input.selection, - itemSelected: itemSelected) + modelSelected: input.modelSelected) } + + func requestThanksListData() -> Observable<[Thanks]> { + return self.provider.thanksList() + .trackActivity(loading) + .trackError(error) + + } } diff --git a/IndieMusic/IndieMusic/Modules/Setting/TimingViewController.swift b/IndieMusic/IndieMusic/Modules/Setting/TimingViewController.swift index bda51c5..25b3355 100644 --- a/IndieMusic/IndieMusic/Modules/Setting/TimingViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Setting/TimingViewController.swift @@ -20,7 +20,7 @@ class TimingViewController: ViewController { let customTimingView: CustomTimingView = { let customTimingView = CustomTimingView.init() - customTimingView.isHidden = false + customTimingView.isHidden = true return customTimingView }() @@ -30,7 +30,7 @@ class TimingViewController: ViewController { override func viewDidLoad() { super.viewDidLoad() - // Do any additional setup after loading the view. + } @@ -42,6 +42,7 @@ class TimingViewController: ViewController { view.addSubview(timingView) view.addSubview(customTimingView) + } @@ -52,25 +53,48 @@ class TimingViewController: ViewController { guard let viewModel = viewModel as? TimingViewModel else { return } let input = TimingViewModel.Input.init(viewWillAppear: rx.viewWillAppear, - selection: timingView.collectionView.rx.itemSelected.asDriver()) + modelSelected: timingView.collectionView.rx.modelSelected(Timing.self).asDriver(), + typeTrigger: timingView.customButton.rx.tap.asDriver(), + customTiming: customTimingView.textField.rx.text.asDriver(), + confirmTrigger: customTimingView.confirmButton.rx.tap.asDriver(), + switchTrigger: timingView.switchControl.rx.isOn.asDriver() + ) let output = viewModel.transform(input: input) let dataSource = TimingViewController.dataSource() output.items.bind(to: timingView.collectionView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag) - - - output.itemSelected.subscribe { [weak self] sectionItem in + output.modelSelected.drive { timing in + - }.disposed(by: rx.disposeBag) - output.selection.drive { [weak self] sectionItem in - - + output.timingType.subscribe { timingType in + self.timingView.isHidden = timingType.element != .normal + self.customTimingView.isHidden = timingType.element == .normal }.disposed(by: rx.disposeBag) + + RxTimer.shared.timeObservable + .map { $0.formattedTime() } + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] timeString in + + self?.timingView.countLabel.text = timeString + }) + .disposed(by: rx.disposeBag) + + RxTimer.shared.isRunning.bind(to: self.timingView.switchControl.rx.isOn).disposed(by: rx.disposeBag) + + + output.switchState.subscribe(onNext: { isOn in + self.timingView.switchControl.isOn = isOn + if isOn == false { + self.timingView.countLabel.text = "min" + } + + }).disposed(by: rx.disposeBag) } @@ -98,11 +122,9 @@ extension TimingViewController { static func dataSource() -> RxCollectionViewSectionedReloadDataSource { return RxCollectionViewSectionedReloadDataSource( configureCell: { dataSource, collectionView, indexPath, item in - // 配置和返回 cell let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TimingViewCell", for: indexPath) as! TimingViewCell - // 配置 cell - cell.titleLabel.text = item.title + cell.timing = item return cell } @@ -234,10 +256,23 @@ class TimingViewCell: UICollectionViewCell { let titleLabel = UILabel.init() titleLabel.font = UIFont.systemFont(ofSize: 15) titleLabel.textAlignment = .center + titleLabel.textColor = .primaryText() return titleLabel }() + var timing: Timing? { + didSet { + guard let timing = timing else { return } + titleLabel.text = "\(timing.description)" + + titleLabel.textColor = timing.isSelected ? .white : .primaryText() + contentView.backgroundColor = timing.isSelected ? .primaryText() : .white + + } + } + + override init(frame: CGRect) { super.init(frame: frame) diff --git a/IndieMusic/IndieMusic/Modules/Setting/TimingViewModel.swift b/IndieMusic/IndieMusic/Modules/Setting/TimingViewModel.swift index f0bae06..7b8a06b 100644 --- a/IndieMusic/IndieMusic/Modules/Setting/TimingViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/Setting/TimingViewModel.swift @@ -15,55 +15,136 @@ class TimingViewModel: ViewModel, ViewModelType { struct Input { let viewWillAppear: ControlEvent - let selection: Driver + let modelSelected: Driver + let typeTrigger: Driver + let customTiming: Driver + let confirmTrigger: Driver + let switchTrigger: Driver } struct Output { let items: BehaviorRelay<[TimingSection]> - let selection: Driver - let itemSelected: PublishSubject - + let modelSelected: Driver + let timingType: BehaviorRelay + let switchState: BehaviorRelay } let itemSelected = PublishSubject() let items = BehaviorRelay<[TimingSection]>.init(value: []) - + let currentItem = BehaviorRelay.init(value: nil) + + let timingType = BehaviorRelay.init(value: .normal) + let switchState: BehaviorRelay = BehaviorRelay(value: false) + func transform(input: Input) -> Output { input.viewWillAppear.subscribe { (_) in - + self.items.accept([TimingSection.init(items: Timing.allCases)]) + }.disposed(by: rx.disposeBag) - - - - let time0 = Timing.init(title: "15") - let time1 = Timing.init(title: "30") - let time2 = Timing.init(title: "60") - let time3 = Timing.init(title: "90") - let time4 = Timing.init(title: "120") + input.typeTrigger.drive { _ in + self.timingType.accept(.custom) + }.disposed(by: rx.disposeBag) + input.modelSelected.drive { selectedTiming in + self.currentItem.accept(selectedTiming) + + }.disposed(by: rx.disposeBag) - //TODO - items.accept([TimingSection.init(items: [time0, time1, time2, time3, time4])]) - + currentItem.subscribe { selectedTiming in + if selectedTiming != nil { + self.switchState.accept(true) + + let updatedSections = self.items.value.map { section -> TimingSection in + let updatedItems = section.items.map { timing -> Timing in + switch (timing, selectedTiming) { + case (.timeing15, .timeing15(_)), + (.timeing30, .timeing30(_)), + (.timeing60, .timeing60(_)), + (.timeing90, .timeing90(_)), + (.timeing120, .timeing120(_)): + return self.updateTiming(timing, isSelected: true) + default: + + return self.updateTiming(timing, isSelected: false) + } + } + return TimingSection(items: updatedItems) + } + + self.items.accept(updatedSections) + + } else { + self.items.accept([TimingSection.init(items: Timing.allCases)]) + } + + }.disposed(by: rx.disposeBag) + - input.selection.drive { indexPath in - guard let sectionItem = self.items.value.first?.items[indexPath.row] else { return } - - self.itemSelected.onNext(sectionItem) + input.confirmTrigger + .withLatestFrom(input.customTiming) + .drive { customTiming in + guard let customTiming = customTiming, + let timing = Int(customTiming) else { return } + + self.startTimer(timing: Timing.custom(isSelected: false, timeing: timing)) + self.timingType.accept(.normal) + }.disposed(by: rx.disposeBag) + input.switchTrigger.drive { isOn in + self.switchState.accept(isOn) }.disposed(by: rx.disposeBag) + + switchState.subscribe { isOn in + guard let defaultTime = self.items.value.first?.items.first else { return } + let timing = self.currentItem.value ?? defaultTime + if isOn { + self.startTimer(timing: timing) + } else { + self.currentItem.accept(nil) + RxTimer.shared.invalidate() + } + }.disposed(by: rx.disposeBag) return Output.init(items: items, - selection: input.selection, - itemSelected: itemSelected) + modelSelected: input.modelSelected, + timingType: timingType, + switchState: switchState) } + func startTimer(timing: Timing) { + RxTimer.shared.defaultTime = timing.description * 60 + + + RxTimer.shared.fire().subscribe { timingStr in + + print("倒计时\(timingStr)") + }.disposed(by: self.rx.disposeBag) + } + + + func updateTiming(_ timing: Timing, isSelected: Bool) -> Timing { + switch timing { + case .timeing15: + return .timeing15(isSelected: isSelected) + case .timeing30: + return .timeing30(isSelected: isSelected) + case .timeing60: + return .timeing60(isSelected: isSelected) + case .timeing90: + return .timeing90(isSelected: isSelected) + case .timeing120: + return .timeing120(isSelected: isSelected) + case .custom(isSelected: let isSelected, timeing: let timeing): + return .custom(isSelected: isSelected, timeing: timeing) + } + } + } diff --git a/IndieMusic/IndieMusic/Networking/Api.swift b/IndieMusic/IndieMusic/Networking/Api.swift index d7949b8..6bb327d 100644 --- a/IndieMusic/IndieMusic/Networking/Api.swift +++ b/IndieMusic/IndieMusic/Networking/Api.swift @@ -50,7 +50,7 @@ protocol IndieMusicAPI { /// 期刊信息 func journal(journalNo: String) -> Single /// 消息列表 - func messageList(page: Int, size: Int) -> Single<[Message]> + func messageList() -> Single /// 用户收藏歌曲列表 func collectSongList(userId: String, page: Int, size: Int) -> Single<[AudioTrack]> /// 用户收藏期刊列表 @@ -100,5 +100,11 @@ protocol IndieMusicAPI { /// 搜索建议 func suggestions(query: String, limit: Int) -> Single<[String]> + /// 评论举报 + func commentReport(commentId: String, type: String) -> Single + /// 评论删除 + func deleteComment(commentId: String) -> Single + /// 贡献者列表 + func thanksList() -> Single<[Thanks]> } diff --git a/IndieMusic/IndieMusic/Networking/Rest/APIConfig.swift b/IndieMusic/IndieMusic/Networking/Rest/APIConfig.swift index 0a003f8..d8e45cb 100644 --- a/IndieMusic/IndieMusic/Networking/Rest/APIConfig.swift +++ b/IndieMusic/IndieMusic/Networking/Rest/APIConfig.swift @@ -33,7 +33,7 @@ enum APIConfig { case cancelLike([String: Any]) case single(String) case journal(String) - case messageList(Int, Int) + case messageList case collectSongList([String: Any]) case journalCollectList([String: Any]) @@ -60,6 +60,9 @@ enum APIConfig { case myCommentReplyList(Int, Int) case feedback([Data], [String: Any]) case suggestions([String: Any]) + case commentReport(String, String) + case deleteComment(String) + case thanks } extension APIConfig: TargetType { @@ -102,8 +105,8 @@ extension APIConfig: TargetType { return "luoo-music/song/\(songID)" case .journal(let journalNo): return "luoo-music/song/getByJournalNo/\(journalNo)" - case .messageList(let page, let size): - return "luoo-user/userMessage/list/\(page)/\(size)" + case .messageList: + return "luoo-user/userMessage/list/" case .collectSongList: return "luoo-music/song/collect" case .journalCollectList: @@ -149,18 +152,24 @@ extension APIConfig: TargetType { return "luoo-user/my/feedback" case .suggestions: return "luoo-music/search/autoComplete" + case .commentReport(let commentId, let type): + return "luoo-comment/comment/complaint/\(commentId)/\(type)" + case .deleteComment(let commentId): + return "luoo-comment/comment/\(commentId)" + case .thanks: + return "user/my/thanks" } } 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, .serach, .randomAudioTrack, .myThumbupList, .myCommentReplyList, .suggestions: + case .wechatAccessToken, .journalList, .journalMusic, .countryCode, .imageCheckCode, .getUserInfo, .carousel, .otherUserInfo, .single, .journal, .messageList, .collectSongList, .journalCollectList, .followingList, .followerList, .blackList, .hotCommentList, .latestCommentList, .subCommentList, .filterMenu, .journalRecommend, .searchCategory, .serach, .randomAudioTrack, .myThumbupList, .myCommentReplyList, .suggestions, .thanks: return .get - case .sendsms, .login, .autoLogin, .editAvatar, .like, .checkVersion, .logout, .sendComment, .feedback: + case .sendsms, .login, .autoLogin, .editAvatar, .like, .checkVersion, .logout, .sendComment, .feedback, .commentReport: return .post case .editUserInfo, .commentLike: return .put - case .cancelLike: + case .cancelLike, .deleteComment: return .delete } @@ -168,7 +177,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, .serach, .randomAudioTrack, .myThumbupList, .myCommentReplyList, .suggestions: + 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, .serach, .randomAudioTrack, .myThumbupList, .myCommentReplyList, .suggestions, .commentReport, .deleteComment, .thanks: return URLEncoding.default case .autoLogin, .editUserInfo, .editAvatar, .checkVersion, .logout, .commentLike, .sendComment, .feedback: @@ -180,7 +189,7 @@ extension APIConfig: TargetType { var task: Task { var parameters: [String: Any] = [:] switch self { - case .wechatAccessToken, .countryCode, .journalMusic, .imageCheckCode, .getUserInfo, .carousel, .otherUserInfo, .single, .journal, .messageList, .followingList, .followerList, .blackList, .hotCommentList, .latestCommentList, .subCommentList, .filterMenu, .journalRecommend, .searchCategory, .serach, .randomAudioTrack, .myThumbupList, .myCommentReplyList: + case .wechatAccessToken, .countryCode, .journalMusic, .imageCheckCode, .getUserInfo, .carousel, .otherUserInfo, .single, .journal, .messageList, .followingList, .followerList, .blackList, .hotCommentList, .latestCommentList, .subCommentList, .filterMenu, .journalRecommend, .searchCategory, .serach, .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): @@ -226,7 +235,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, .serach, .randomAudioTrack, .sendComment, .myThumbupList, .myCommentReplyList, .feedback, .suggestions: + 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, .serach, .randomAudioTrack, .sendComment, .myThumbupList, .myCommentReplyList, .feedback, .suggestions, .commentReport, .deleteComment: 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 fa714aa..2e5656e 100644 --- a/IndieMusic/IndieMusic/Networking/Rest/RestApi.swift +++ b/IndieMusic/IndieMusic/Networking/Rest/RestApi.swift @@ -201,8 +201,8 @@ extension RestApi { return requestObject(.journal(journalNo), with: "data", type: Journal.self) } /// 消息列表 - func messageList(page: Int, size: Int) -> Single<[Message]> { - return requestObject(.messageList(page, size), with: "data.rows", type: [Message].self) + func messageList() -> Single { + return requestObject(.messageList, with: "data", type: MessageList.self) } @@ -317,7 +317,7 @@ extension RestApi { func feedback(type: Int, content: String, images: [UIImage], contact: String) -> Single { - let dic = ["type": type, + let dic = ["type": "\(type)", "content": content, "contact": contact] as [String : Any] @@ -336,6 +336,16 @@ extension RestApi { return requestObject(.suggestions(dic), with: "data", type: [String].self) } + + func commentReport(commentId: String, type: String) -> Single { + return requestWithoutMapping(.commentReport(commentId, type)).map { _ in } + } + func deleteComment(commentId: String) -> Single { + return requestWithoutMapping(.deleteComment(commentId)).map { _ in } + } + func thanksList() -> Single<[Thanks]> { + return requestObject(.thanks, with: "data", type: [Thanks].self) + } } diff --git a/IndieMusic/IndieMusic/Resources/02.lyric b/IndieMusic/IndieMusic/Resources/02.lyric new file mode 100644 index 0000000..73ce4cc --- /dev/null +++ b/IndieMusic/IndieMusic/Resources/02.lyric @@ -0,0 +1,16 @@ +I am the Electric Man +Yes, I am + +I've got the electric band +I've got everything but the electric van +Electric Man's always got a helping hand for the motherland +But it makes it so hard to hold on to the metal can + +"Come on Electric Man!" +I'm trying +"Yes, you can!" + +I am the Electric Man +"He is the Electric Man" +I am the Electric Man +Yes I am \ No newline at end of file diff --git a/IndieMusic/IndieMusic/Resources/Assets.xcassets/thanks/Contents.json b/IndieMusic/IndieMusic/Resources/Assets.xcassets/thanks/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/IndieMusic/IndieMusic/Resources/Assets.xcassets/thanks/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IndieMusic/IndieMusic/Resources/Assets.xcassets/thanks/thanks_nav_icon.imageset/Contents.json b/IndieMusic/IndieMusic/Resources/Assets.xcassets/thanks/thanks_nav_icon.imageset/Contents.json new file mode 100644 index 0000000..afe40ad --- /dev/null +++ b/IndieMusic/IndieMusic/Resources/Assets.xcassets/thanks/thanks_nav_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "thanks_nav_icon.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IndieMusic/IndieMusic/Resources/Assets.xcassets/thanks/thanks_nav_icon.imageset/thanks_nav_icon.svg b/IndieMusic/IndieMusic/Resources/Assets.xcassets/thanks/thanks_nav_icon.imageset/thanks_nav_icon.svg new file mode 100644 index 0000000..870efb8 --- /dev/null +++ b/IndieMusic/IndieMusic/Resources/Assets.xcassets/thanks/thanks_nav_icon.imageset/thanks_nav_icon.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IndieMusic/IndieMusic/Third Party/RxTimer.swift b/IndieMusic/IndieMusic/Third Party/RxTimer.swift new file mode 100644 index 0000000..edcb70a --- /dev/null +++ b/IndieMusic/IndieMusic/Third Party/RxTimer.swift @@ -0,0 +1,102 @@ +// +// RxTimer.swift +// IndieMusic +// +// Created by WenLei on 2024/2/20. +// + +import Foundation +import RxSwift +import RxCocoa +import UIKit + +public class RxTimer: NSObject { + static let shared = RxTimer() + + public var defaultTime: Int = 60 + public let isRunning: BehaviorRelay = BehaviorRelay(value: false) + private lazy var residueTime = BehaviorRelay(value: defaultTime) + + private var timer: DispatchSourceTimer? = nil + private var backgroundTask: UIBackgroundTaskIdentifier = .invalid + + public var timeObservable: Observable { + return residueTime.asObservable() + } + deinit { + invalidate() + } + + private func beginBackgroundTask() { + endBackgroundTask() + backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in + // 如果系统即将终止后台任务,结束它 + self?.endBackgroundTask() + } + } + + private func endBackgroundTask() { + if backgroundTask != .invalid { + UIApplication.shared.endBackgroundTask(backgroundTask) + backgroundTask = .invalid + } + } +} + +extension RxTimer { + public func fire() -> Observable { + invalidate() // 确保先清除之前的计时器和任务 + + // 重置剩余时间为默认时间 + residueTime.accept(defaultTime + 1) + + // 更新计时状态 + isRunning.accept(true) + + // 创建并配置定时器 + setupTimer() + + // 返回格式化的剩余时间字符串 + return residueTime + .map { "\($0)s" } + .share(replay: 1, scope: .forever) + } + + private func setupTimer() { + // 确保在后台也能继续运行 + beginBackgroundTask() + + // 创建并配置定时器 + let timer = DispatchSource.makeTimerSource(queue: .global()) + timer.schedule(deadline: .now(), repeating: .seconds(1)) + timer.setEventHandler { [weak self] in + guard let strongSelf = self else { return } + strongSelf.updateResidueTime() + if strongSelf.residueTime.value <= 0 { + strongSelf.endBackgroundTask() // 如果定时器结束,确保结束后台任务 + } + } + timer.resume() + self.timer = timer + } + + + public func invalidate() { + timer?.cancel() + timer = nil + endBackgroundTask() + isRunning.accept(false) + + AudioManager.sharedInstance.pause() + } + + private func updateResidueTime() { + let time = residueTime.value - 1 + if time <= 0 { + invalidate() + residueTime.accept(defaultTime) + } else { + residueTime.accept(time) + } + } +}