Comment reporting, comment deletion, question feedback module

dev
wenlei 9 months ago
parent 33577405d6
commit d4d3f26316

@ -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 = "<group>"; };
77C9B9EE2B4C2A910006C83F /* AudioTrackListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioTrackListViewController.swift; sourceTree = "<group>"; };
77C9B9F02B4C2B3A0006C83F /* AudioTrackListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioTrackListViewModel.swift; sourceTree = "<group>"; };
77C9C0712B845FF3000A277B /* RxTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RxTimer.swift; sourceTree = "<group>"; };
77C9C0732B8495A0000A277B /* 02.lyric */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = 02.lyric; sourceTree = "<group>"; };
77C9C0752B84E513000A277B /* ReportMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportMenuViewController.swift; sourceTree = "<group>"; };
77C9C0772B84EA19000A277B /* SheetViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetViewCell.swift; sourceTree = "<group>"; };
77C9C0792B85B036000A277B /* PopoverListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverListView.swift; sourceTree = "<group>"; };
77C9C07B2B85C33A000A277B /* FeedbackMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackMenuViewController.swift; sourceTree = "<group>"; };
77CEFEFB2B81EC600071B671 /* PresentAndDismissTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentAndDismissTransition.swift; sourceTree = "<group>"; };
77CEFEFD2B82F18A0071B671 /* CommentToolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentToolView.swift; sourceTree = "<group>"; };
77DFA9C42B4E8388005B8B13 /* MineDownloadViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MineDownloadViewController.swift; sourceTree = "<group>"; };
@ -565,6 +577,7 @@
7736FF432B4CECF2008D5DAD /* CommentViewModel.swift */,
7736FF452B4CF0E6008D5DAD /* CommentDetailViewController.swift */,
778638932B4D123D00B00AF9 /* CommentDetailViewModel.swift */,
77C9C0752B84E513000A277B /* ReportMenuViewController.swift */,
);
path = JournalDetail;
sourceTree = "<group>";
@ -616,6 +629,7 @@
77620D992B69DA1A00798861 /* EditSignatureViewController.swift */,
77620D932B68E65300798861 /* EditDateViewController.swift */,
77620D8F2B68DDA000798861 /* EditSexViewController.swift */,
77C9C0772B84EA19000A277B /* SheetViewCell.swift */,
);
path = EditInfo;
sourceTree = "<group>";
@ -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 = "<group>";
@ -856,6 +872,7 @@
770228E32B54FA3600E07F7A /* RxActivityIndicator */,
778B8A602AF8ECC20034AFD4 /* RxErrorTracker */,
7751D3812B45324300F1F2BD /* IndieMusic-Bridging-Header.h */,
77C9C0712B845FF3000A277B /* RxTimer.swift */,
);
path = "Third Party";
sourceTree = "<group>";
@ -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 */,

@ -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

@ -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)
}
}

@ -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
@ -211,6 +219,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):
coordinator.animate(alongsideTransition: { [weak self] _ in

@ -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)
}
})

@ -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)"
}
}

@ -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
}
}

@ -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?

@ -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

@ -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
}
}
}

@ -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]
}

@ -8,11 +8,74 @@
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]
}

@ -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
}
}

@ -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,6 +22,8 @@ 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)
}
}

@ -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<CommentSection>(
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

@ -36,6 +36,7 @@ class CommentDetailViewModel: ViewModel, ViewModelType {
let likeSelected = PublishRelay<Comment>()
let itemReport = PublishRelay<Comment>()
let itemDelete = PublishRelay<Comment>()
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<Void> {
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)
}
}

@ -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,22 +192,16 @@ class CommentViewCell: UITableViewCell {
}
}
override func setSelected(_ selected: Bool, animated: Bool) {
//
// override func prepareForReuse() {
// super.prepareForReuse()
// disposeBag = DisposeBag()
// }
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
UIView.animate(withDuration: 0.25) {
self.contentView.backgroundColor = selected ? UIColor.black.withAlphaComponent(0.25) : .white
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func selectedCell(_ selected: Bool, animated: Bool) {
// Configure the view for the selected state
}

@ -11,7 +11,7 @@ import RxCocoa
import RxDataSources
import FSPopoverView
class CommentViewController: ViewController {
class CommentViewController: TableViewController {
var commentHeaderView: CommentHeaderView = {
let commentHeaderView = CommentHeaderView.init()
@ -19,14 +19,6 @@ 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)
@ -34,16 +26,21 @@ class CommentViewController: ViewController {
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<CommentSection>(
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 {
}
}

@ -48,7 +48,6 @@ class CommentViewModel: ViewModel, ViewModelType {
let clearTextSubject = PublishRelay<Void>()
let likeSelected = PublishRelay<Comment>()
let itemReport = PublishRelay<Comment>()
let itemDelete = PublishRelay<Comment>()
@ -159,17 +158,19 @@ 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,
insertCommment: insertCommment,
@ -220,6 +221,12 @@ class CommentViewModel: ViewModel, ViewModelType {
}
func requestDeleteComment(commentId: String) -> Observable<Void> {
return self.provider.deleteComment(commentId: commentId)
.trackActivity(loading)
.trackError(error)
}
func insertComment(newComment: Comment) {
@ -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)
}
}

@ -127,23 +127,24 @@ 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
default:
return []
}
}.compactMap { item -> AudioTrack? in
if case let .audioItem(model) = item {
case .audioSection(_, let journalItems):
return journalItems.compactMap { item -> AudioTrack? in
if case .audioItem(let model) = item {
return model
} else {
return nil
}
}
default:
return []
}
}
let playerViewModel = PlayerViewModel.init(track: track, provider: viewModel.provider)
@ -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)
}

@ -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<Bool>
let modelSelected: Driver<Sheet>
}
struct Output {
let items: BehaviorRelay<[SheetSection]>
let modelSelected: Driver<Sheet>
let popView: PublishSubject<Void>
}
let items = BehaviorRelay<[SheetSection]>.init(value: [])
let popView = PublishSubject<Void>.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<Void> {
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<SheetSection> {
return RxTableViewSectionedReloadDataSource<SheetSection>(
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
}
}

@ -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)
@ -170,6 +178,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)
}
}
}

@ -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.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)
// 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)
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]
// 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)
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 ?? ""))
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<MessageList> {
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]> {

@ -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,6 +339,7 @@ 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,48 +432,50 @@ 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 tableViewPoint = convert(point, to: tableView)
if tableView.point(inside: tableViewPoint, with: event) {
return tableView.hitTest(tableViewPoint, with: event)
}
let sliderPoint = convert(point, to: tableView)
if tableView.point(inside: sliderPoint, with: event) {
return tableView
return super.hitTest(point, with: event)
}
if self.bounds.contains(point) {
return self
}
return super.hitTest(point, with: event)
extension PlayerLyricsView {
static func dataSource() -> RxTableViewSectionedReloadDataSource<PlayerLyricsSection> {
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 {
var lyricsLabel: UILabel = {
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
var lyricLine: LyricLine? {
didSet {
lyricsLabel.text = lyricLine?.text
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
@ -480,7 +493,6 @@ class PlayerLyricsCell: UITableViewCell {
func makeUI() {
backgroundColor = .clear
// contentView.backgroundColor = .clear
contentView.addSubview(lyricsLabel)

@ -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)
@ -117,14 +116,6 @@ 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,44 +248,9 @@ 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<PlayerLyricsSection> {
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
}
}

@ -33,7 +33,7 @@ class PlayerViewModel: ViewModel, ViewModelType {
struct Output {
let items: BehaviorRelay<[PlayerLyricsSection]>
let shuffle: BehaviorRelay<Bool>
let audioTrack: PublishSubject<AudioTrack>
let currentAudioTrack: BehaviorRelay<AudioTrack?>
let toShare: PublishSubject<Void>
let toList: BehaviorRelay<Void>
let playShuffleType: BehaviorRelay<PlayerShuffleType>
@ -41,6 +41,7 @@ class PlayerViewModel: ViewModel, ViewModelType {
let duration: BehaviorRelay<Float>
let time: BehaviorRelay<Float>
let isPlaying: BehaviorRelay<Bool>
let moreButtonTrigger: Driver<Void>
}
@ -49,7 +50,7 @@ class PlayerViewModel: ViewModel, ViewModelType {
let isPlaying = BehaviorRelay<Bool>.init(value: false)
let shuffle = BehaviorRelay<Bool>.init(value: false)
let audioTrack = PublishSubject<AudioTrack>.init()
let currentAudioTrack = BehaviorRelay<AudioTrack?>.init(value: nil)
let toShare = PublishSubject<Void>.init()
let toList = BehaviorRelay<Void>.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)
@ -117,12 +118,6 @@ 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(())
}.disposed(by: rx.disposeBag)
@ -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))
}
}
}

@ -14,18 +14,17 @@ class EditSexViewModel: ViewModel, ViewModelType {
struct Input {
let viewWillAppear: ControlEvent<Bool>
let selection: Driver<IndexPath>
let modelSelected: Driver<Sheet>
}
struct Output {
let items: BehaviorRelay<[SheetSection]>
let itemSelected: PublishSubject<Sheet>
let modelSelected: Driver<Sheet>
let popView: PublishSubject<Void>
}
let items = BehaviorRelay<[SheetSection]>.init(value: [])
let itemSelected = PublishSubject<Sheet>()
let popView = PublishSubject<Void>.init()
var sexType = PublishRelay<SexType>.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)
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)
self.items.accept([SheetSection.init(items: [male, female, secret])])
}.disposed(by: rx.disposeBag)
self.sexType.accept(sectionItem.sex)
input.modelSelected.drive { sheet in
switch sheet {
case .sex(let sexType):
self.sexType.accept(sexType)
self.popView.onNext(())
default: break
}
}.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)
}
}
}

@ -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)
}
}
}

@ -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<Bool>
let modelSelected: Driver<Sheet>
}
struct Output {
let items: BehaviorRelay<[SheetSection]>
let modelSelected: Driver<Sheet>
let popView: PublishSubject<Void>
}
let items = BehaviorRelay<[SheetSection]>.init(value: [])
let popView = PublishSubject<Void>.init()
var feedbackType = PublishRelay<FeedbackType>.init()
init(feedbackType: PublishRelay<FeedbackType>, 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<SheetSection> {
return RxTableViewSectionedReloadDataSource<SheetSection>(
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
}
}

@ -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
@ -139,6 +146,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 = "反馈类型*"

@ -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<Bool>
let modelSelected: Driver<FeedbackPhotoType>
let photoItems: PublishRelay<[UIImage]>
let feedbackMenuTrigger: Driver<Void>
let category: PublishRelay<String>
let content: Driver<String?>
let contact: Driver<String?>
@ -24,11 +26,15 @@ class FeedbackViewModel: ViewModel, ViewModelType {
struct Output {
let photoItems: BehaviorRelay<[FeedbackSection]>
let toPhotoSelect: PublishRelay<[UIImage]>
let feedbackType: PublishRelay<FeedbackType>
let popView: PublishRelay<Void>
}
let photoItems = BehaviorRelay<[FeedbackSection]>.init(value: [.init(items: [.add])])
let toPhotoSelect = PublishRelay<[UIImage]>.init()
let feedbackType = PublishRelay<FeedbackType>.init()
let popView = PublishRelay<Void>.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
// contentcontactOptional
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)
}

@ -60,6 +60,7 @@ class SettingViewController: TableViewController {
output.modelSelected.drive { [weak self] sectionItem in
self?.deselectSelectedRow()
switch sectionItem {
case .editInfo:

@ -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.modelSelected.drive { [weak self] thanks in
output.itemSelected.subscribe { [weak self] sectionItem 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)
}
}

@ -13,15 +13,13 @@ class ThanksViewModel: ViewModel, ViewModelType {
struct Input {
let viewWillAppear: ControlEvent<Bool>
let selection: Driver<IndexPath>
let modelSelected: Driver<Thanks>
}
struct Output {
let items: BehaviorRelay<[ThanksSection]>
let selection: Driver<IndexPath>
let itemSelected: PublishSubject<Thanks>
let modelSelected: Driver<Thanks>
}
let itemSelected = PublishSubject<Thanks>()
@ -31,19 +29,26 @@ class ThanksViewModel: ViewModel, ViewModelType {
input.viewWillAppear.subscribe { (_) in
}.disposed(by: rx.disposeBag)
input.selection.drive { indexPath in
guard let sectionItem = self.items.value.first?.items[indexPath.row] else { return }
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)
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)
}
}

@ -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.modelSelected.drive { timing in
output.itemSelected.subscribe { [weak self] sectionItem in
}.disposed(by: rx.disposeBag)
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
output.selection.drive { [weak self] sectionItem in
self?.timingView.countLabel.text = timeString
})
.disposed(by: rx.disposeBag)
RxTimer.shared.isRunning.bind(to: self.timingView.switchControl.rx.isOn).disposed(by: rx.disposeBag)
}.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<TimingSection> {
return RxCollectionViewSectionedReloadDataSource<TimingSection>(
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)

@ -15,55 +15,136 @@ class TimingViewModel: ViewModel, ViewModelType {
struct Input {
let viewWillAppear: ControlEvent<Bool>
let selection: Driver<IndexPath>
let modelSelected: Driver<Timing>
let typeTrigger: Driver<Void>
let customTiming: Driver<String?>
let confirmTrigger: Driver<Void>
let switchTrigger: Driver<Bool>
}
struct Output {
let items: BehaviorRelay<[TimingSection]>
let selection: Driver<IndexPath>
let itemSelected: PublishSubject<Timing>
let modelSelected: Driver<Timing>
let timingType: BehaviorRelay<TimingType>
let switchState: BehaviorRelay<Bool>
}
let itemSelected = PublishSubject<Timing>()
let items = BehaviorRelay<[TimingSection]>.init(value: [])
let currentItem = BehaviorRelay<Timing?>.init(value: nil)
let timingType = BehaviorRelay<TimingType>.init(value: .normal)
let switchState: BehaviorRelay<Bool> = BehaviorRelay(value: false)
func transform(input: Input) -> Output {
input.viewWillAppear.subscribe { (_) in
self.items.accept([TimingSection.init(items: Timing.allCases)])
}.disposed(by: rx.disposeBag)
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)
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)
}
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")
self.items.accept(updatedSections)
} else {
self.items.accept([TimingSection.init(items: Timing.allCases)])
}
//TODO
items.accept([TimingSection.init(items: [time0, time1, time2, time3, time4])])
}.disposed(by: rx.disposeBag)
input.selection.drive { indexPath in
guard let sectionItem = self.items.value.first?.items[indexPath.row] else { return }
input.confirmTrigger
.withLatestFrom(input.customTiming)
.drive { customTiming in
guard let customTiming = customTiming,
let timing = Int(customTiming) else { return }
self.itemSelected.onNext(sectionItem)
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)
}
}
}

@ -50,7 +50,7 @@ protocol IndieMusicAPI {
///
func journal(journalNo: String) -> Single<Journal>
///
func messageList(page: Int, size: Int) -> Single<[Message]>
func messageList() -> Single<MessageList>
///
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<Void>
///
func deleteComment(commentId: String) -> Single<Void>
///
func thanksList() -> Single<[Thanks]>
}

@ -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

@ -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<MessageList> {
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<Void> {
let dic = ["type": type,
let dic = ["type": "\(type)",
"content": content,
"contact": contact] as [String : Any]
@ -337,5 +337,15 @@ extension RestApi {
}
func commentReport(commentId: String, type: String) -> Single<Void> {
return requestWithoutMapping(.commentReport(commentId, type)).map { _ in }
}
func deleteComment(commentId: String) -> Single<Void> {
return requestWithoutMapping(.deleteComment(commentId)).map { _ in }
}
func thanksList() -> Single<[Thanks]> {
return requestObject(.thanks, with: "data", type: [Thanks].self)
}
}

@ -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

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

@ -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
}
}

@ -0,0 +1,36 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 2.57735C11.6188 2.22008 12.3812 2.22008 13 2.57735L19.6603 6.42265C20.2791 6.77992 20.6603 7.44017 20.6603 8.1547V15.8453C20.6603 16.5598 20.2791 17.2201 19.6603 17.5774L13 21.4226C12.3812 21.7799 11.6188 21.7799 11 21.4226L4.33975 17.5774C3.72094 17.2201 3.33975 16.5598 3.33975 15.8453V8.1547C3.33975 7.44017 3.72094 6.77992 4.33975 6.42265L11 2.57735Z" fill="url(#paint0_linear_615_3690)" style=""/>
<path d="M14.0517 11.836L14.0512 11.8363C12.8856 12.8522 12.55 13.7099 12.55 14.2201V18.4395C12.7066 17.3181 13.4029 16.4245 14.5051 15.4906C15.1949 14.8515 15.9449 13.9151 15.95 12.6075V8.69284C15.9077 9.2512 15.6863 9.80075 15.3686 10.3107C15.0143 10.8794 14.5386 11.4016 14.0517 11.836Z" fill="url(#paint1_linear_615_3690)" stroke="url(#paint2_radial_615_3690)" style="" stroke-width="0.1"/>
<path d="M9.94834 11.836L9.94877 11.8363C11.1144 12.8522 11.45 13.7099 11.45 14.2201V18.4395C11.2934 17.3181 10.5971 16.4245 9.49494 15.4906C8.80508 14.8515 8.0551 13.9151 8.05 12.6075V8.69284C8.09228 9.2512 8.31373 9.80075 8.63138 10.3107C8.98569 10.8794 9.46141 11.4016 9.94834 11.836Z" fill="url(#paint3_linear_615_3690)" stroke="url(#paint4_radial_615_3690)" style="" stroke-width="0.1"/>
<circle cx="12" cy="8" r="1.94744" fill="url(#paint5_linear_615_3690)" stroke="url(#paint6_radial_615_3690)" style="" stroke-width="0.105114"/>
<defs>
<linearGradient id="paint0_linear_615_3690" x1="5.52941" y1="6.04412" x2="17.1471" y2="19.8676" gradientUnits="userSpaceOnUse">
<stop style="stop-color:black;stop-opacity:1;"/>
<stop offset="1" stop-color="#484C58" style="stop-color:#484C58;stop-color:color(display-p3 0.2824 0.2964 0.3458);stop-opacity:1;"/>
</linearGradient>
<linearGradient id="paint1_linear_615_3690" x1="15.9652" y1="9.31439" x2="11.0203" y2="16.704" gradientUnits="userSpaceOnUse">
<stop stop-color="white" style="stop-color:white;stop-opacity:1;"/>
<stop offset="1" stop-color="white" stop-opacity="0.34" style="stop-color:white;stop-opacity:0.34;"/>
</linearGradient>
<radialGradient id="paint2_radial_615_3690" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(15.9854 10.0318) rotate(107.951) scale(5.63076 2.13783)">
<stop stop-color="white" style="stop-color:white;stop-opacity:1;"/>
<stop offset="1" stop-color="white" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
</radialGradient>
<linearGradient id="paint3_linear_615_3690" x1="8.03484" y1="9.31439" x2="12.9797" y2="16.704" gradientUnits="userSpaceOnUse">
<stop stop-color="white" style="stop-color:white;stop-opacity:1;"/>
<stop offset="1" stop-color="white" stop-opacity="0.34" style="stop-color:white;stop-opacity:0.34;"/>
</linearGradient>
<radialGradient id="paint4_radial_615_3690" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(8.01459 10.0318) rotate(72.049) scale(5.63076 2.13783)">
<stop stop-color="white" style="stop-color:white;stop-opacity:1;"/>
<stop offset="1" stop-color="white" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
</radialGradient>
<linearGradient id="paint5_linear_615_3690" x1="10.0398" y1="6.31024" x2="10.9058" y2="10.1927" gradientUnits="userSpaceOnUse">
<stop stop-color="white" style="stop-color:white;stop-opacity:1;"/>
<stop offset="1" stop-color="white" stop-opacity="0.65" style="stop-color:white;stop-opacity:0.65;"/>
</linearGradient>
<radialGradient id="paint6_radial_615_3690" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10.0167 6.58356) rotate(45.8158) scale(2.84566 1.84171)">
<stop stop-color="white" style="stop-color:white;stop-opacity:1;"/>
<stop offset="1" stop-color="white" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

@ -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<Bool> = BehaviorRelay(value: false)
private lazy var residueTime = BehaviorRelay(value: defaultTime)
private var timer: DispatchSourceTimer? = nil
private var backgroundTask: UIBackgroundTaskIdentifier = .invalid
public var timeObservable: Observable<Int> {
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<String> {
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)
}
}
}
Loading…
Cancel
Save