From 33577405d64ace3df15dd2f2962b18f63a2b2801 Mon Sep 17 00:00:00 2001 From: wenlei Date: Tue, 20 Feb 2024 10:18:15 +0800 Subject: [PATCH] Search for suggestion modules --- IndieMusic/IndieMusic/Models/Comment.swift | 85 ++++++- IndieMusic/IndieMusic/Models/Search.swift | 17 ++ .../IndieMusic/Modules/Home/HomeView.swift | 1 - .../AudioMoreActionViewModel.swift | 13 +- .../CommentDetailViewController.swift | 144 +++++++---- .../CommentDetailViewModel.swift | 87 +++++-- .../Modules/JournalDetail/CommentView.swift | 6 + .../JournalDetail/CommentViewController.swift | 132 +++++----- .../JournalDetail/CommentViewModel.swift | 139 +++++------ .../JournalDetailController.swift | 2 +- .../JournalDetail/JournalDetailView.swift | 1 + .../JournalDetailViewModel.swift | 44 ++-- .../Personal/MyCommentListController.swift | 20 +- .../Personal/MyCommentListViewModel.swift | 17 +- .../Personal/PersonalViewController.swift | 6 +- .../Modules/Personal/PersonalViewModel.swift | 6 +- .../Search/SearchResultsController.swift | 226 ++++++++++++++---- .../Search/SearchResultsViewModel.swift | 68 +++++- .../Modules/Search/SearchViewController.swift | 3 +- IndieMusic/IndieMusic/Networking/Api.swift | 5 +- .../Networking/Rest/APIConfig.swift | 12 +- .../IndieMusic/Networking/Rest/RestApi.swift | 13 +- 22 files changed, 712 insertions(+), 335 deletions(-) diff --git a/IndieMusic/IndieMusic/Models/Comment.swift b/IndieMusic/IndieMusic/Models/Comment.swift index 691d00e..cd8a1a1 100644 --- a/IndieMusic/IndieMusic/Models/Comment.swift +++ b/IndieMusic/IndieMusic/Models/Comment.swift @@ -8,12 +8,38 @@ import Foundation import RxDataSources +struct MyComment: Codable { + let id: String? + let commenterId: String? + let nickName: String? + let createTime: String? + let commentContent: String? + let journalId: String? + let commenterAvatar: String? + let userId: String? + let content: String? + let journalImage: String? + + private enum CodingKeys: String, CodingKey { + case id = "_id" + case commenterId + case nickName + case createTime + case commentContent + case journalId + case commenterAvatar + case userId + case content + case journalImage + } +} + struct MyCommentListSection { - var items: [Comment] + var items: [MyComment] } extension MyCommentListSection: SectionModelType { - typealias Item = Comment + typealias Item = MyComment init(original: MyCommentListSection, items: [Item]) { self = original @@ -45,16 +71,16 @@ struct Comment: Codable, IdentifiableType, Equatable { let avatar: String? let state: Int? let userId: String? - let content: String? + var content: String? let commentCount: Int? let nickName: String? let publishTime: String? let location: String? let parentId: String? let id: String - let thumbupCountString: String? + var thumbupCountString: String? let journalImage: String? - let haveThumbup: Bool? + var haveThumbup: Bool? // 自定义父评论个数 var parentCommentCount: Int? @@ -85,22 +111,42 @@ struct Comment: Codable, IdentifiableType, Equatable { } static func == (lhs: Comment, rhs: Comment) -> Bool { - return lhs.id == rhs.id + return lhs.id == rhs.id && lhs.haveThumbup == rhs.haveThumbup && lhs.content == rhs.content } } -enum CommentType { +enum CommentType: IdentifiableType, Equatable { case comment(Comment) case quote(Comment) + + var identity: String { + switch self { + case .comment(let comment), .quote(let comment): + return comment.id + } + } + + static func == (lhs: CommentType, rhs: CommentType) -> Bool { + return lhs.identity == rhs.identity + } } -struct CommentSection { - var items: [CommentType] +struct CommentSection: IdentifiableType, Equatable { + var id: String + var items: [Comment] + + var identity: String { + return id + } + + static func == (lhs: CommentSection, rhs: CommentSection) -> Bool { + return lhs.id == rhs.id && lhs.items == rhs.items + } } -extension CommentSection: SectionModelType { - typealias Item = CommentType +extension CommentSection: AnimatableSectionModelType { + typealias Item = Comment init(original: CommentSection, items: [Item]) { self = original @@ -111,5 +157,22 @@ extension CommentSection: SectionModelType { struct CommentThumbState: Codable { let thumbState: Bool + + + enum CodingKeys: String, CodingKey { + case thumbState + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + do { + thumbState = try container.decode(Bool.self, forKey: .thumbState) + } catch DecodingError.typeMismatch { + let intValue = try container.decode(Int.self, forKey: .thumbState) + thumbState = intValue != 0 + } + } + + } diff --git a/IndieMusic/IndieMusic/Models/Search.swift b/IndieMusic/IndieMusic/Models/Search.swift index 6ff40d7..e260dc9 100644 --- a/IndieMusic/IndieMusic/Models/Search.swift +++ b/IndieMusic/IndieMusic/Models/Search.swift @@ -90,3 +90,20 @@ extension SearchCategorySection: SectionModelType { self.items = items } } + + + + +struct SearchSuggestionSection: Codable { + var items: [String] + +} + +extension SearchSuggestionSection: SectionModelType { + typealias Item = String + + init(original: SearchSuggestionSection, items: [Item]) { + self = original + self.items = items + } +} diff --git a/IndieMusic/IndieMusic/Modules/Home/HomeView.swift b/IndieMusic/IndieMusic/Modules/Home/HomeView.swift index 99025a9..822259e 100644 --- a/IndieMusic/IndieMusic/Modules/Home/HomeView.swift +++ b/IndieMusic/IndieMusic/Modules/Home/HomeView.swift @@ -157,7 +157,6 @@ class HomeViewCell: UITableViewCell { lazy var rollingNoticeView: GYRollingNoticeView = { let rollingNoticeView = GYRollingNoticeView.init() - rollingNoticeView.backgroundColor = .red rollingNoticeView.stayInterval = 5 rollingNoticeView.dataSource = self // rollingNoticeView.delegate = self diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/AudioMoreActionViewModel.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/AudioMoreActionViewModel.swift index fad3a4f..6a65e9a 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/AudioMoreActionViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/AudioMoreActionViewModel.swift @@ -87,11 +87,14 @@ class AudioMoreActionViewModel: ViewModel, ViewModelType { toShare.accept(()) case 1://收藏 - guard let id = audioTrack?.id else { return } - if isLike.value { + guard let id = audioTrack?.id, + let haveCollect = audioTrack.value?.haveCollect else { return } + if haveCollect { self.requestCancelLike(journalNo: id) .subscribe { _ in - isLike.accept(false) + var new = self.audioTrack.value + new?.haveCollect = false + self.audioTrack.accept(new) dismiss.accept(()) } onError: { error in @@ -100,7 +103,9 @@ class AudioMoreActionViewModel: ViewModel, ViewModelType { } else { self.requestLike(journalNo: id) .subscribe { _ in - isLike.accept(true) + var new = self.audioTrack.value + new?.haveCollect = true + self.audioTrack.accept(new) dismiss.accept(()) } onError: { error in diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewController.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewController.swift index 918cae3..28b784f 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewController.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewController.swift @@ -9,6 +9,7 @@ import UIKit import RxSwift import RxCocoa import RxDataSources +import FSPopoverView class CommentDetailViewController: TableViewController { let commentDetailHeaderView: CommentDetailHeaderView = { @@ -23,23 +24,27 @@ class CommentDetailViewController: TableViewController { return commentToolView }() + private let menuView: FSPopoverListView = { + let menuView = FSPopoverListView.init(scrollDirection: .horizontal) + menuView.shadowOpacity = 0 + menuView.shadowRadius = 0 + menuView.shadowColor = .clear + menuView.cornerRadius = 3 + menuView.transitioningDelegate = nil + + menuView.arrowSize = CGSize.init(width: 18, height: 5) + menuView.backgroundColor = .primaryText() + + return menuView + }() + + override func viewDidLoad() { super.viewDidLoad() } -// -// override func viewWillAppear(_ animated: Bool) { -// super.viewWillAppear(animated) -// -// self.navigationController?.setNavigationBarHidden(true, animated: false) -// } -// -// -// override func viewWillDisappear(_ animated: Bool) { -// super.viewWillDisappear(animated) -// self.navigationController?.setNavigationBarHidden(false, animated: false) -// } + override func makeUI() { super.makeUI() @@ -47,7 +52,6 @@ class CommentDetailViewController: TableViewController { view.backgroundColor = .clear view.addSubview(tableView) self.navigationController?.view.backgroundColor = UIColor.clear -// UIColor.black.withAlphaComponent(0.5) tableView.register(CommentViewCell.self, forCellReuseIdentifier: "CommentViewCell") tableView.tableHeaderView = commentDetailHeaderView @@ -67,14 +71,55 @@ class CommentDetailViewController: TableViewController { guard let viewModel = viewModel as? CommentDetailViewModel else { return } let input = CommentDetailViewModel.Input.init(viewWillAppear: rx.viewWillAppear, - modelSelected: tableView.rx.modelSelected(CommentType.self).asDriver(), + modelSelected: tableView.rx.modelSelected(Comment.self).asDriver(), footerRefresh: footerRefreshTrigger, commentText: commentToolView.textField.rx.text.asDriver(), sendCommentTrigger: commentToolView.textField.rx.controlEvent(.editingDidEndOnExit).asDriver()) let output = viewModel.transform(input: input) - let dataSource = CommentDetailViewController.dataSource() + + let dataSource = RxTableViewSectionedAnimatedDataSource( + animationConfiguration: AnimationConfiguration.init(insertAnimation: .top, reloadAnimation: .none, deleteAnimation: .automatic), + configureCell: { dataSource, tableView, indexPath, item in + let cell: CommentViewCell = tableView.dequeueReusableCell(withIdentifier: "CommentViewCell", for: indexPath) as! CommentViewCell + + cell.comment = item + cell.likeButton.rx.tap.subscribe { _ in + + viewModel.likeSelected.accept(item) + }.disposed(by: cell.rx.disposeBag) + +// cell.avatarView.rx.tapGesture().when(.recognized) +// .subscribe { [weak self] tap in +// guard let userID = item.userId else { return } +// +// let personalViewModel = PersonalViewModel.init(userID: userID, provider: viewModel.provider) +// self?.navigator.show(segue: .personal(viewModel: personalViewModel), sender: self?.presentingViewController) +// +// }.disposed(by: cell.rx.disposeBag) + + + cell.rx.longPressGesture().when(.recognized) + .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]) + } 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) + } + }.disposed(by: cell.rx.disposeBag) + return cell + } + ) + + + output.items.bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag) @@ -91,6 +136,11 @@ class CommentDetailViewController: TableViewController { self.updateHeaderViewFrame() }.disposed(by: rx.disposeBag) + + output.clearTextSubject.subscribe { [weak self] _ in + self?.commentToolView.textField.text = "" + }.disposed(by: rx.disposeBag) + // 监听键盘弹出事件 NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification) @@ -140,9 +190,6 @@ class CommentDetailViewController: TableViewController { tapGesture.rx.event .bind(onNext: { [weak self] _ in self?.view.endEditing(true) - - self?.navigator.dismiss(sender: self) - }) .disposed(by: rx.disposeBag) @@ -190,6 +237,42 @@ class CommentDetailViewController: TableViewController { } + func setupMenuItems(cell: CommentViewCell, features: [PopoverMenu] = [.copy, .delete]) -> [FSPopoverListItem] { + guard let viewModel = viewModel as? CommentViewModel, + let comment = cell.comment else { return []} + + let items: [FSPopoverListItem] = features.map { feature in + let item = FSPopoverListTextItem(scrollDirection: .horizontal) + item.title = feature.description + item.titleFont = UIFont.systemFont(ofSize: 12) + item.isSeparatorHidden = false + item.titleColor = .white + item.contentInset = .init(top: 9, left: 15, bottom: 9, right: 15) + item.selectedHandler = { item in + guard let item = item as? FSPopoverListTextItem else { + return + } + + switch item.title { + case PopoverMenu.copy.description: + UIPasteboard.general.string = cell.comment?.content + case PopoverMenu.report.description: + viewModel.itemReport.accept(comment) + case PopoverMenu.delete.description: + viewModel.itemDelete.accept(comment) + default: break + } + } + item.updateLayout() + return item + } + items.last?.isSeparatorHidden = true + + return items + + } + + } extension CommentDetailViewController { @@ -208,29 +291,6 @@ extension CommentDetailViewController { } -extension CommentDetailViewController { - static func dataSource() -> RxTableViewSectionedReloadDataSource { - return RxTableViewSectionedReloadDataSource( - configureCell: { dataSource, tableView, indexPath, item in - - switch item { - case .comment(let comment): - let cell: CommentViewCell = tableView.dequeueReusableCell(withIdentifier: "CommentViewCell", for: indexPath) as! CommentViewCell - - cell.comment = comment - return cell - - - case .quote(let commentQuote): - let cell: CommentQuoteViewCell = tableView.dequeueReusableCell(withIdentifier: "CommentQuoteViewCell", for: indexPath) as! CommentQuoteViewCell - cell.comment = commentQuote - - return cell - } - }) - } -} - class CommentDetailHeaderView: UIView { @@ -300,9 +360,7 @@ class CommentDetailHeaderView: UIView { avatarView.snp.makeConstraints { make in make.left.equalTo(self).offset(18) make.size.equalTo(CGSize.init(width: 40, height: 40)) -// make.centerY.equalTo(self) make.top.equalTo(self).offset(24) -// make.bottom.equalTo(self).offset(-21) } nameLabel.snp.makeConstraints { make in diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewModel.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewModel.swift index 6d6ec00..e85a696 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewModel.swift @@ -14,7 +14,7 @@ class CommentDetailViewModel: ViewModel, ViewModelType { struct Input { let viewWillAppear: ControlEvent - let modelSelected: Driver + let modelSelected: Driver let footerRefresh: Observable let commentText: Driver let sendCommentTrigger: Driver @@ -25,12 +25,17 @@ class CommentDetailViewModel: ViewModel, ViewModelType { let comment: Driver let items: BehaviorRelay<[CommentSection]> - let modelSelected: Driver + let modelSelected: Driver + let clearTextSubject: PublishRelay } let items = BehaviorRelay<[CommentSection]>.init(value: []) let itemSelected = PublishSubject() + let clearTextSubject = PublishRelay() + + let likeSelected = PublishRelay() + let itemReport = PublishRelay() let comment: Comment @@ -44,11 +49,8 @@ class CommentDetailViewModel: ViewModel, ViewModelType { input.viewWillAppear.subscribe { _ in self.requestSubCommentData(parentId: self.comment.id, page: self.page, size: 10) .subscribe { commentArray in - let newArray = commentArray.map { comment in - return CommentType.comment(comment) - } - self.items.accept([CommentSection.init(items: newArray)]) + self.items.accept([CommentSection.init(id: "", items: commentArray)]) } onError: { error in @@ -63,14 +65,10 @@ class CommentDetailViewModel: ViewModel, ViewModelType { .trackActivity(self.footerLoading) }) .subscribe(onNext: { (commentArray) in - let newArray = commentArray.map { comment in - return CommentType.comment(comment) - } - guard let new = self.items.value.first else { return } - self.items.accept([CommentSection.init(items: new.items + newArray)]) + self.items.accept([CommentSection.init(id: "", items: new.items + commentArray)]) }).disposed(by: rx.disposeBag) @@ -93,8 +91,9 @@ class CommentDetailViewModel: ViewModel, ViewModelType { self.sendCommentData(content: comment, journalId: self.comment.journalId, parentId: self.comment.id, journalImage: self.comment.journalImage) .subscribe { comment in - //TODO 更新cell的点赞状态 - SVProgressHUD.showText(withStatus: "发送成功") + self.insertComment(newComment: comment) + self.clearTextSubject.accept(()) + } onError: { error in }.disposed(by: self.rx.disposeBag) @@ -104,12 +103,40 @@ class CommentDetailViewModel: ViewModel, ViewModelType { }) - + likeSelected.subscribe { [weak self] comment in + guard let self = self, + let comment = comment.element else { return } + + self.requestCommentLike(commentID: comment.id) + .observe(on: MainScheduler.instance) + .subscribe(onNext: { commentThumbState in + + var new = comment + + + if let thumbupCount = Int(new.thumbupCountString ?? "0") { + let newThumbupCount = commentThumbState.thumbState == true ? (thumbupCount + 1) : (thumbupCount - 1) + new.thumbupCountString = "\(newThumbupCount)" + } + new.haveThumbup = commentThumbState.thumbState + + self.updateComment(withUpdatedComment: new) + + }, onError: { error in + + }).disposed(by: self.rx.disposeBag) + + + + + }.disposed(by: rx.disposeBag) + return Output.init(comment: Driver.just(comment), items: items, - modelSelected: input.modelSelected) + modelSelected: input.modelSelected, + clearTextSubject: clearTextSubject) } @@ -127,19 +154,35 @@ class CommentDetailViewModel: ViewModel, ViewModelType { .trackError(error) } - func insertComment(in index: Int, with newCommentType: CommentType) { + + func sendCommentData(content: String, journalId: String?, parentId: String?, journalImage: String?) -> Observable { + return self.provider.sendComment(content: content, journalId: journalId, parentId: parentId, journalImage: journalImage) + .trackActivity(loading) + .trackError(error) + + } + + func insertComment(newComment: Comment) { var firstSection = items.value.first - firstSection?.items.insert(newCommentType, at: index + 1) + firstSection?.items.insert(newComment, at: 0) if let updatedSection = firstSection { items.accept([updatedSection]) } } + - func sendCommentData(content: String, journalId: String?, parentId: String?, journalImage: String?) -> Observable { - return self.provider.sendComment(content: content, journalId: journalId, parentId: parentId, journalImage: journalImage) - .trackActivity(loading) - .trackError(error) - + func updateComment(withUpdatedComment updatedComment: Comment) { + let updatedSections = items.value.map { section -> CommentSection in + let updatedComments = section.items.map { comment -> Comment in + guard comment.id == updatedComment.id else { return comment } + return updatedComment + } + return CommentSection(id: section.id, items: updatedComments) + } + + items.accept(updatedSections) } + + } diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentView.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentView.swift index ebaf1c1..83e2dab 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentView.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentView.swift @@ -7,6 +7,7 @@ import UIKit import AttributedString +import RxSwift class CommentHeaderView: UIView { var titleLabel: UILabel = { @@ -193,6 +194,11 @@ class CommentViewCell: UITableViewCell { } +// +// override func prepareForReuse() { +// super.prepareForReuse() +// disposeBag = DisposeBag() +// } override func awakeFromNib() { super.awakeFromNib() diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewController.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewController.swift index 58096bc..7f1e53a 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewController.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewController.swift @@ -23,7 +23,6 @@ class CommentViewController: ViewController { let tableView = UITableView.init() tableView.separatorColor = .clear tableView.register(CommentViewCell.self, forCellReuseIdentifier: "CommentViewCell") - tableView.register(CommentQuoteViewCell.self, forCellReuseIdentifier: "CommentQuoteViewCell") tableView.keyboardDismissMode = .onDrag return tableView @@ -92,6 +91,7 @@ class CommentViewController: ViewController { guard let viewModel = viewModel as? CommentViewModel else { return } + let input = CommentViewModel.Input.init(viewWillAppear: rx.viewWillAppear, selection: tableView.rx.itemSelected.asDriver(), currentCommentListType: currentCommentListType, @@ -104,90 +104,64 @@ class CommentViewController: ViewController { ) let output = viewModel.transform(input: input) - let dataSource = RxTableViewSectionedReloadDataSource( + let dataSource = RxTableViewSectionedAnimatedDataSource( + animationConfiguration: AnimationConfiguration.init(insertAnimation: .top, reloadAnimation: .none, deleteAnimation: .automatic), configureCell: { dataSource, tableView, indexPath, item in + let cell: CommentViewCell = tableView.dequeueReusableCell(withIdentifier: "CommentViewCell", for: indexPath) as! CommentViewCell - switch item { - case .comment(let comment): - let cell: CommentViewCell = tableView.dequeueReusableCell(withIdentifier: "CommentViewCell", for: indexPath) as! CommentViewCell - - cell.comment = comment - + cell.comment = item + cell.likeButton.rx.tap.subscribe { _ in - cell.avatarView.rx.tapGesture().when(.recognized) - .subscribe { [weak self] tap in - guard let userID = comment.userId else { return } - - let personalViewModel = PersonalViewModel.init(userID: userID, provider: viewModel.provider) - self?.navigator.show(segue: .personal(viewModel: personalViewModel), sender: self) - - }.disposed(by: self.rx.disposeBag) - - cell.likeButton.rx.tap.subscribe { _ in - - viewModel.likeSelected.onNext(comment.id) - }.disposed(by: self.rx.disposeBag) - - cell.moreButton.rx.tap.subscribe { _ in - - let commentDetailViewModel = CommentDetailViewModel.init(comment: comment, provider: viewModel.provider) - - self.navigator.show(segue: .commentDetail(viewModel:commentDetailViewModel), sender: self, transition: .modal) + viewModel.likeSelected.accept(item) + }.disposed(by: cell.rx.disposeBag) + + cell.avatarView.rx.tapGesture().when(.recognized) + .subscribe { [weak self] tap in + guard let userID = item.userId else { return } - }.disposed(by: self.rx.disposeBag) - - - - cell.rx.longPressGesture().when(.recognized) - .subscribe { [weak self] tap in - guard let self = self else { return } - - if comment.userId == UserDefaults.AccountInfo.string(forKey: .userID) { - self.menuView.items = self.setupMenuItems(features: [.copy, .report, .delete]) - } else { - self.menuView.items = self.setupMenuItems(features: [.copy, .delete]) - } - - if let location = tap.element?.location(in: cell) { - self.menuView.present(fromPoint: location, in: cell, displayIn: self.view) - } - }.disposed(by: self.rx.disposeBag) - - - - return cell - - - case .quote(let commentQuote): - let cell: CommentQuoteViewCell = tableView.dequeueReusableCell(withIdentifier: "CommentQuoteViewCell", for: indexPath) as! CommentQuoteViewCell - - cell.comment = commentQuote - - cell.nameClousres = {[weak self] userID in let personalViewModel = PersonalViewModel.init(userID: userID, provider: viewModel.provider) self?.navigator.show(segue: .personal(viewModel: personalViewModel), sender: self) - } + + }.disposed(by: cell.rx.disposeBag) + + + cell.moreButton.rx.tap.subscribe { _ in - cell.moreClousres = {[weak self] commentID in -// let commentDetailViewModel = CommentDetailViewModel.init(commentID: commentID, provider: viewModel.provider) -// self?.navigator.show(segue: .commentDetail(viewModel: commentDetailViewModel), sender: self) - } + let commentDetailViewModel = CommentDetailViewModel.init(comment: item, provider: viewModel.provider) - return cell - } + self.navigator.show(segue: .commentDetail(viewModel:commentDetailViewModel), sender: self, transition: .modal) + + }.disposed(by: cell.rx.disposeBag) + + + + cell.rx.longPressGesture().when(.recognized) + .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]) + } 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) + } + }.disposed(by: cell.rx.disposeBag) + return cell } ) - output.items.bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag) - - + + output.items + .bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag) - output.itemSelected.subscribe { [weak self] sectionItem in - + output.clearTextSubject.subscribe { [weak self] _ in + self?.commentToolView.textField.text = "" }.disposed(by: rx.disposeBag) - - + // 监听键盘弹出事件 NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification) @@ -267,7 +241,10 @@ class CommentViewController: ViewController { - func setupMenuItems(features: [PopoverMenu] = [.copy, .delete]) -> [FSPopoverListItem] { + func setupMenuItems(cell: CommentViewCell, features: [PopoverMenu] = [.copy, .delete]) -> [FSPopoverListItem] { + guard let viewModel = viewModel as? CommentViewModel, + let comment = cell.comment else { return []} + let items: [FSPopoverListItem] = features.map { feature in let item = FSPopoverListTextItem(scrollDirection: .horizontal) item.title = feature.description @@ -279,7 +256,16 @@ class CommentViewController: ViewController { guard let item = item as? FSPopoverListTextItem else { return } - print(item.title ?? "") + + switch item.title { + case PopoverMenu.copy.description: + UIPasteboard.general.string = cell.comment?.content + case PopoverMenu.report.description: + viewModel.itemReport.accept(comment) + case PopoverMenu.delete.description: + viewModel.itemDelete.accept(comment) + default: break + } } item.updateLayout() return item diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewModel.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewModel.swift index f542212..5d6727f 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewModel.swift @@ -31,21 +31,25 @@ class CommentViewModel: ViewModel, ViewModelType { let insertCommment: PublishSubject let toCommentDetail: PublishRelay - + let clearTextSubject: PublishRelay } let items = BehaviorRelay<[CommentSection]>.init(value: []) - let commentItems = BehaviorRelay<[CommentType]>.init(value: []) - let quoteItems = BehaviorRelay<[CommentType]>.init(value: []) +// let commentItems = BehaviorRelay<[CommentType]>.init(value: []) +// let quoteItems = BehaviorRelay<[CommentType]>.init(value: []) let itemSelected = PublishSubject() - let likeSelected = PublishSubject() let insertCommment: PublishSubject = .init() let toCommentDetail: PublishRelay = .init() + let clearTextSubject = PublishRelay() + + let likeSelected = PublishRelay() + let itemReport = PublishRelay() + let itemDelete = PublishRelay() var journal: Journal @@ -92,16 +96,13 @@ class CommentViewModel: ViewModel, ViewModelType { self.requestLatestCommentListData(journalID: self.journal.id, page: self.page, size: 10) .subscribe { [weak self] comments in guard let self = self else { return } - + self.handleReceivedComments(comments: comments) - + } onError: { error in } .disposed(by: self.rx.disposeBag) - - - } @@ -109,29 +110,26 @@ class CommentViewModel: ViewModel, ViewModelType { }.disposed(by: rx.disposeBag) - - - items.subscribe { commentSectionArray in - guard let firstSection = self.items.value.first else { return } - - - }.disposed(by: rx.disposeBag) - - input.selection.drive { indexPath in - guard let sectionItem = self.items.value.first?.items[indexPath.row] else { return } - -// self.itemSelected.onNext(sectionItem) - }.disposed(by: rx.disposeBag) - - - likeSelected.subscribe { [weak self] commentID in - guard let self = self else { return } + likeSelected.subscribe { [weak self] comment in + guard let self = self, + let comment = comment.element else { return } - - self.requestCommentLike(commentID: commentID) + self.requestCommentLike(commentID: comment.id) + .observe(on: MainScheduler.instance) .subscribe(onNext: { commentThumbState in + var new = comment + + + if let thumbupCount = Int(new.thumbupCountString ?? "0") { + let newThumbupCount = commentThumbState.thumbState == true ? (thumbupCount + 1) : (thumbupCount - 1) + new.thumbupCountString = "\(newThumbupCount)" + } + new.haveThumbup = commentThumbState.thumbState + + self.updateComment(withUpdatedComment: new) + }, onError: { error in }).disposed(by: self.rx.disposeBag) @@ -150,11 +148,9 @@ class CommentViewModel: ViewModel, ViewModelType { self.sendCommentData(content: comment, journalId: self.journal.id, parentId: nil, journalImage: self.journal.image) .subscribe { comment in - - //TODO 更新cell的点赞状态 - - - + + self.insertComment(newComment: comment) + self.clearTextSubject.accept(()) } onError: { error in }.disposed(by: self.rx.disposeBag) @@ -162,6 +158,14 @@ class CommentViewModel: ViewModel, ViewModelType { print("评论为空,不发送") } }) + + itemReport.subscribe { comment in + + }.disposed(by: rx.disposeBag) + + itemDelete.subscribe { comment in + + }.disposed(by: rx.disposeBag) @@ -169,44 +173,16 @@ class CommentViewModel: ViewModel, ViewModelType { return Output.init(items: items, itemSelected: itemSelected, insertCommment: insertCommment, - toCommentDetail: toCommentDetail) + toCommentDetail: toCommentDetail, + clearTextSubject: clearTextSubject) } private func handleReceivedComments(comments: [Comment]) { - let commentTypes = comments.map { CommentType.comment($0) } - var commentSections = [CommentSection(items: commentTypes)] + let commentSections = [CommentSection.init(id: "", items: comments)] self.items.accept(commentSections) -// for (index, commentType) in commentTypes.enumerated() { -// if case let .comment(comment) = commentType, comment.commentCount != 0 { -// self.requestSubCommentData(parentId: comment.id, page: 1, size: 1) -// .subscribe { [weak self] subCommentArray in -// guard let self = self else { return } -// -// self.handleReceivedSubComments(parentCommentCount: comment.commentCount ?? 0, subComments: subCommentArray, mainCommentIndex: index) -// } onError: { error in -// print(error) -// }.disposed(by: self.rx.disposeBag) -// } -// } } - - - private func handleReceivedSubComments(parentCommentCount: Int, subComments: [Comment], mainCommentIndex: Int) { - guard var firstSection = self.items.value.first else { return } - let quoteTypes = subComments.map { comment in - var newComment = comment - newComment.parentCommentCount = parentCommentCount - return CommentType.quote(newComment) - } - - firstSection.items.insert(contentsOf: quoteTypes, at: mainCommentIndex + 1) - - self.items.accept([firstSection]) - } - - func requestHotCommentListData(journalID: String, page: Int, size: Int) -> Observable<[Comment]> { return self.provider.hotCommentList(journalId: journalID, page: page, size: size) @@ -222,10 +198,6 @@ class CommentViewModel: ViewModel, ViewModelType { } - - - - func requestSubCommentData(parentId: String, page: Int, size: Int) -> Observable<[Comment]> { return self.provider.subCommentList(parentId: parentId, page: page, size: size) .trackActivity(loading) @@ -240,14 +212,6 @@ class CommentViewModel: ViewModel, ViewModelType { .trackError(error) } - func insertComment(in index: Int, with newCommentType: CommentType) { - var firstSection = items.value.first - firstSection?.items.insert(newCommentType, at: index + 1) - if let updatedSection = firstSection { - items.accept([updatedSection]) - } - } - func sendCommentData(content: String, journalId: String?, parentId: String?, journalImage: String?) -> Observable { return self.provider.sendComment(content: content, journalId: journalId, parentId: parentId, journalImage: journalImage) @@ -258,4 +222,27 @@ class CommentViewModel: ViewModel, ViewModelType { + func insertComment(newComment: Comment) { + var firstSection = items.value.first + firstSection?.items.insert(newComment, at: 0) + if let updatedSection = firstSection { + items.accept([updatedSection]) + } + } + + + func updateComment(withUpdatedComment updatedComment: Comment) { + let updatedSections = items.value.map { section -> CommentSection in + let updatedComments = section.items.map { comment -> Comment in + guard comment.id == updatedComment.id else { return comment } + return updatedComment + } + return CommentSection(id: section.id, items: updatedComments) + } + + items.accept(updatedSections) + } + + + } diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailController.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailController.swift index 9c90d9b..6feba12 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailController.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailController.swift @@ -177,7 +177,7 @@ class JournalDetailController: TableViewController { output.journal.subscribe { [weak self] journal in guard let self = self else { return } - self.commentToolView.commentCountButton.commentCountLabel.text = "\(journal)" + self.commentToolView.commentCountButton.commentCountLabel.text = "\(journal.totalCommentReply ?? "0")" self.journalCollapsibleHeaderView.journal = journal self.updateHeader() diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailView.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailView.swift index c407089..ffb7fe6 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailView.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailView.swift @@ -227,6 +227,7 @@ class JournalCollapsibleHeaderView: UIView { saveButton.rx.tap.subscribe { [weak self] _ in guard let self = self else { return } + self.popoverView.dismiss() }.disposed(by: rx.disposeBag) diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailViewModel.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailViewModel.swift index b4cd0c0..140303a 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailViewModel.swift @@ -172,18 +172,11 @@ class JournalDetailViewModel: ViewModel, ViewModelType { item.subscribe { [weak self] audioTrack in - guard let audioTrack = audioTrack.element else { return } + guard let audioTrack = audioTrack.element, + let newTrack = audioTrack else { return } -// -// var new = items.value -// if var header = new.first?.header { -// header.haveCollect = false -// -// if let items = new.first?.items { -// new[0] = .audio(header: header, items: items) -// items.accept(new) -// } -// } + + self?.updateAudioTrack(with: newTrack, identifiedBy: newTrack.id) }.disposed(by: rx.disposeBag) @@ -198,13 +191,30 @@ class JournalDetailViewModel: ViewModel, ViewModelType { ) } - func sectionType(for sectionIndex: Int) -> JournalItem? { - guard sectionIndex < self.items.value.count else { return nil } - let section = items.value[sectionIndex] - // 这里我们简单地返回第一个item作为该节的代表类型 - // 你可能需要根据实际逻辑调整这部分 - return section.items.first + func updateAudioTrack(with newTrack: AudioTrack, identifiedBy trackId: String) { + let currentSections = items.value + + let updatedSections = currentSections.map { section -> JournalSection in + switch section { + case .audioSection(let header, let items): + + let updatedItems = items.map { item -> JournalItem in + switch item { + case .audioItem(let model) where model.id == trackId: + return .audioItem(model: newTrack) + default: + return item + } + } + return .audioSection(header: header, items: updatedItems) + case .journalSection(let header, let items): + return .journalSection(header: header, items: items) + } + } + + items.accept(updatedSections) } + func requestMusic(journalNo: String) -> Observable<[AudioTrack]> { diff --git a/IndieMusic/IndieMusic/Modules/Personal/MyCommentListController.swift b/IndieMusic/IndieMusic/Modules/Personal/MyCommentListController.swift index beaf979..f53ea07 100644 --- a/IndieMusic/IndieMusic/Modules/Personal/MyCommentListController.swift +++ b/IndieMusic/IndieMusic/Modules/Personal/MyCommentListController.swift @@ -85,7 +85,7 @@ extension MyCommentListController { let cell: CommentListViewCell = tableView.dequeueReusableCell(withIdentifier: "CommentListViewCell", for: indexPath) as! CommentListViewCell - cell.comment = item + cell.myComment = item return cell } @@ -180,17 +180,17 @@ class CommentListViewCell: UITableViewCell { var buttonTapCallback: ((User) -> ())? - var comment: Comment? { + var myComment: MyComment? { didSet { - guard let comment = comment else { return } - avatarView.kf.setImage(with: URL.init(string: comment.avatar ?? "")) - audioTrackView.kf.setImage(with: URL.init(string: comment.journalImage ?? "")) + guard let myComment = myComment else { return } + avatarView.kf.setImage(with: URL.init(string: myComment.commenterAvatar ?? "")) + audioTrackView.kf.setImage(with: URL.init(string: myComment.journalImage ?? "")) - nameLabel.text = comment.nickName - dateLabel.text = "\(comment.publishTime)" - commentLabel.text = comment.content -// myCommentLabel.text = "我的评论:\(comment.comm)" - + nameLabel.text = myComment.nickName + let date = Date.init(dateString: myComment.createTime ?? "", format: "yyyy-MM-dd HH:mm:ss") + dateLabel.text = date.timeAgoSinceNow + commentLabel.text = myComment.content + myCommentLabel.text = "我的评论:\(myComment.commentContent ?? "")" } } diff --git a/IndieMusic/IndieMusic/Modules/Personal/MyCommentListViewModel.swift b/IndieMusic/IndieMusic/Modules/Personal/MyCommentListViewModel.swift index c499430..4ec60d0 100644 --- a/IndieMusic/IndieMusic/Modules/Personal/MyCommentListViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/Personal/MyCommentListViewModel.swift @@ -30,20 +30,9 @@ class MyCommentListViewModel: ViewModel, ViewModelType { input.viewWillAppear.subscribe { [weak self] _ in guard let self = self else { return } - self.requestCommentList(page: self.page, size: 10) - .subscribe { [weak self] commentArray in - guard let self = self else { return } - - - - } onError: { error in - - }.disposed(by: self.rx.disposeBag) - - }.disposed(by: rx.disposeBag) - input.headerRefresh.flatMapLatest({ [weak self] () -> Observable<[Comment]> in + input.headerRefresh.flatMapLatest({ [weak self] () -> Observable<[MyComment]> in guard let self = self else { return Observable.just([]) } self.page = 1 @@ -54,7 +43,7 @@ class MyCommentListViewModel: ViewModel, ViewModelType { self.items.accept([MyCommentListSection.init(items: items)]) }).disposed(by: rx.disposeBag) - input.footerRefresh.flatMapLatest({ [weak self] () -> Observable<[Comment]> in + input.footerRefresh.flatMapLatest({ [weak self] () -> Observable<[MyComment]> in guard let self = self else { return Observable.just([]) } self.page += 1 return self.requestCommentList(page: self.page, size: 10) @@ -68,7 +57,7 @@ class MyCommentListViewModel: ViewModel, ViewModelType { } - func requestCommentList(page: Int, size: Int) -> Observable<[Comment]> { + func requestCommentList(page: Int, size: Int) -> Observable<[MyComment]> { self.provider.myCommentList(page: page, size: size) .trackActivity(loading) .trackError(error) diff --git a/IndieMusic/IndieMusic/Modules/Personal/PersonalViewController.swift b/IndieMusic/IndieMusic/Modules/Personal/PersonalViewController.swift index 6dfd0ca..dcf5350 100644 --- a/IndieMusic/IndieMusic/Modules/Personal/PersonalViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Personal/PersonalViewController.swift @@ -104,7 +104,7 @@ class PersonalViewController: ViewController { }.disposed(by: rx.disposeBag) - + collectionView.mj_header = nil } @@ -137,7 +137,9 @@ class PersonalViewController: ViewController { cell.audioTrack = audioTrack cell.buttonTapCallback = { [weak self] audioTrack in - let audioMoreActionViewModel = AudioMoreActionViewModel.init(audioTrack: viewModel.needRefresh, provider: viewModel.provider) + viewModel.item.accept(audioTrack) + + let audioMoreActionViewModel = AudioMoreActionViewModel.init(audioTrack: viewModel.item, provider: viewModel.provider) self?.navigator.show(segue: .audioMore(viewModel: audioMoreActionViewModel), sender: self, transition: .navigationPresent(type: .audioMore)) diff --git a/IndieMusic/IndieMusic/Modules/Personal/PersonalViewModel.swift b/IndieMusic/IndieMusic/Modules/Personal/PersonalViewModel.swift index 18aedef..36e55e5 100644 --- a/IndieMusic/IndieMusic/Modules/Personal/PersonalViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/Personal/PersonalViewModel.swift @@ -42,7 +42,7 @@ class PersonalViewModel: ViewModel, ViewModelType { let itemSelected = PublishSubject() let user = BehaviorRelay.init(value: nil) let personInfoLikeType = BehaviorRelay.init(value: .audio) - let needRefresh: BehaviorRelay = .init(value: nil) + let item: BehaviorRelay = .init(value: nil) let followingButtonTrigger = PublishRelay.init() @@ -84,10 +84,10 @@ class PersonalViewModel: ViewModel, ViewModelType { self.page = 1 if personInfoLikeType == .audio { - return self.requestCollectSongList(userId: UserDefaults.AccountInfo.string(forKey: .userID) ?? "", page: self.page, size: 10) + return self.requestCollectSongList(userId: self.userID, page: self.page, size: 10) .trackActivity(self.headerLoading) } else { - return self.requestJournalCollectList(userId: UserDefaults.AccountInfo.string(forKey: .userID) ?? "", page: self.page, size: 10) + return self.requestJournalCollectList(userId: self.userID, page: self.page, size: 10) .trackActivity(self.headerLoading) } diff --git a/IndieMusic/IndieMusic/Modules/Search/SearchResultsController.swift b/IndieMusic/IndieMusic/Modules/Search/SearchResultsController.swift index 6ef2877..b0ebaa2 100644 --- a/IndieMusic/IndieMusic/Modules/Search/SearchResultsController.swift +++ b/IndieMusic/IndieMusic/Modules/Search/SearchResultsController.swift @@ -15,9 +15,45 @@ class SearchResultsController: ViewController { let searchTopBar: SearchTopBar = { let searchTopBar = SearchTopBar.init() + searchTopBar.setContentHuggingPriority(.required, for: .horizontal) + searchTopBar.setContentCompressionResistancePriority(.required, for: .horizontal) + return searchTopBar }() + lazy var segmentControl: ScrollSegmentView = { + + var style = SegmentStyle() + style.showLine = true + style.normalTitleColor = UIColor.tertiaryText() + style.selectedTitleColor = UIColor.white + style.backgroundColor = UIColor.white + style.titleSelectFont = UIFont.systemFont(ofSize: 14, weight: .medium) + style.titleFont = UIFont.systemFont(ofSize: 14) + + style.scrollLineHeight = 0 + style.scrollLineColor = .clear + style.coverBackgroundColor = .init(hex: 0x0d0d0d) + style.normalborderColor = UIColor.tertiaryText() + style.scrollTitle = false + style.showCover = true + +// style.scrollTitle = true +// style.lineSpace = 12 + + + + let segmentControl = ScrollSegmentView.init(frame: CGRect.init(x: 0, y: 0, width: 200, height: 32), segmentStyle: style, titles: ["单曲", "期刊"]) + segmentControl.isHidden = true +// segmentControl.scrollView.backgroundColor = .red + + segmentControl.setContentHuggingPriority(.required, for: .horizontal) + segmentControl.setContentCompressionResistancePriority(.required, for: .horizontal) + + + return segmentControl + }() + let searchNoDataView: SearchNoDataView = { @@ -54,6 +90,15 @@ class SearchResultsController: ViewController { return collectionView }() + + let searchSuggestionsView: SearchSuggestionsView = { + let searchSuggestionsView = SearchSuggestionsView.init() + searchSuggestionsView.isHidden = true + + return searchSuggestionsView + }() + + let searchType = BehaviorRelay.init(value: .audio) @@ -66,7 +111,7 @@ class SearchResultsController: ViewController { override func makeUI() { super.makeUI() - searchTopBar.segmentControl.titleBtnOnClick = { [weak self] (label, index) in + segmentControl.titleBtnOnClick = { [weak self] (label, index) in if index == 0 { self?.searchType.accept(.audio) } else { @@ -80,8 +125,10 @@ class SearchResultsController: ViewController { view.backgroundColor = .white view.addSubview(searchTopBar) + view.addSubview(segmentControl) view.addSubview(searchNoDataView) view.addSubview(collectionView) + view.addSubview(searchSuggestionsView) } @@ -108,20 +155,33 @@ class SearchResultsController: ViewController { let input = SearchResultsViewModel.Input.init(viewWillAppear: rx.viewWillAppear, closeButtonTrigger: self.searchTopBar.cancelButton.rx.tap.asDriver(), searchText: searchTopBar.searchControl.textField.rx.text.asDriver(), + searchTrigger: searchTopBar.searchControl.textField.rx.controlEvent(.editingDidEndOnExit).asDriver(), modelSelected: collectionView.rx.modelSelected(SearchResultsItem.self).asDriver(), + suggestionSelected: searchSuggestionsView.tableView.rx.modelSelected(String.self).asDriver(), searchType: searchType) let output = viewModel.transform(input: input) let dataSource = SearchResultsController.dataSource { [weak self] cell, audioTrack in -// let audioMoreActionViewModel = AudioMoreActionViewModel.init(audioTrack: audioTrack, provider: viewModel.provider) -// -// self?.navigator.show(segue: .audioMore(viewModel: audioMoreActionViewModel), sender: self, transition: .navigationPresent(type: .audioMore)) + let item: BehaviorRelay = .init(value: audioTrack) + + let audioMoreActionViewModel = AudioMoreActionViewModel.init(audioTrack: item, provider: viewModel.provider) + + self?.navigator.show(segue: .audioMore(viewModel: audioMoreActionViewModel), sender: self, transition: .navigationPresent(type: .audioMore)) } + output.items.bind(to: collectionView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag) + + output.items.subscribe { items in + + self.segmentControl.isHidden = items.element?.first?.items.isEmpty ?? true + self.searchSuggestionsView.isHidden = true + + }.disposed(by: rx.disposeBag) + searchTopBar.cancelButton.rx.tap.subscribe { [weak self] _ in self?.navigator.pop(sender: self) @@ -155,6 +215,19 @@ class SearchResultsController: ViewController { }.disposed(by: rx.disposeBag) + + let searchSuggestionsDataSource = SearchSuggestionsView.dataSource() + + output.suggestionsItems.bind(to: searchSuggestionsView.tableView.rx.items(dataSource: searchSuggestionsDataSource)).disposed(by: rx.disposeBag) + + output.suggestionsItems.subscribe { suggestionsItems in + self.searchSuggestionsView.isHidden = suggestionsItems.element?.first?.items.isEmpty ?? true + }.disposed(by: rx.disposeBag) + + searchSuggestionsView.tableView.rx.itemSelected + .subscribe { indexPath in + self.searchSuggestionsView.tableView.deselectRow(at: indexPath, animated: true) + }.disposed(by: rx.disposeBag) } @@ -167,7 +240,13 @@ class SearchResultsController: ViewController { make.left.equalTo(view) make.right.equalTo(view) make.top.equalTo(view).offset(BaseDimensions.statusBarHeight + 15) - make.height.equalTo(135) + } + + segmentControl.snp.makeConstraints { make in + make.left.equalTo(view).offset(18) + make.top.equalTo(searchTopBar.snp.bottom).offset(9) + make.width.equalTo(200) + make.height.equalTo(40) } searchNoDataView.snp.makeConstraints { make in @@ -178,7 +257,14 @@ class SearchResultsController: ViewController { collectionView.snp.makeConstraints { make in make.left.equalTo(view) make.right.equalTo(view) - make.top.equalTo(searchTopBar.snp.bottom).offset(0) + make.top.equalTo(segmentControl.snp.bottom).offset(0) + make.bottom.equalTo(view) + } + + searchSuggestionsView.snp.makeConstraints { make in + make.left.equalTo(view) + make.right.equalTo(view) + make.top.equalTo(searchTopBar.snp.bottom) make.bottom.equalTo(view) } @@ -190,7 +276,7 @@ class SearchResultsController: ViewController { extension SearchResultsController { //TODO - static func dataSource(_ buttonTapHandler: @escaping (UITableViewCell, AudioTrack) -> Void) -> RxCollectionViewSectionedReloadDataSource { + static func dataSource(_ buttonTapHandler: @escaping (UICollectionViewCell, AudioTrack) -> Void) -> RxCollectionViewSectionedReloadDataSource { return RxCollectionViewSectionedReloadDataSource( configureCell: { dataSource, collectionView, indexPath, item in @@ -200,6 +286,10 @@ extension SearchResultsController { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "JournalAudioCollectionViewCell", for: indexPath) as! JournalAudioCollectionViewCell cell.audioTrack = audioTrack + cell.buttonTapCallback = { audioTrack in + buttonTapHandler(cell, audioTrack) + } + return cell case .journal(let journal): @@ -252,7 +342,6 @@ class SearchTopBar: UIView { let searchControl: SearchControl = { let searchControl = SearchControl.init() searchControl.layer.cornerRadius = 20 - return searchControl }() @@ -267,33 +356,7 @@ class SearchTopBar: UIView { - lazy var segmentControl: ScrollSegmentView = { - - var style = SegmentStyle() - style.showLine = true - style.normalTitleColor = UIColor.tertiaryText() - style.selectedTitleColor = UIColor.white - style.backgroundColor = UIColor.white - style.titleSelectFont = UIFont.systemFont(ofSize: 14, weight: .medium) - style.titleFont = UIFont.systemFont(ofSize: 14) - - style.scrollLineHeight = 0 - style.scrollLineColor = .clear - style.coverBackgroundColor = .init(hex: 0x0d0d0d) - style.normalborderColor = UIColor.tertiaryText() - style.scrollTitle = false - style.showCover = true -// style.scrollTitle = true -// style.lineSpace = 12 - - - - let segmentControl = ScrollSegmentView.init(frame: CGRect.init(x: 0, y: 0, width: 200, height: 32), segmentStyle: style, titles: ["单曲", "期刊"]) - -// segmentControl.scrollView.backgroundColor = .red - - return segmentControl - }() + @@ -312,7 +375,6 @@ class SearchTopBar: UIView { addSubview(searchControl) addSubview(cancelButton) - addSubview(segmentControl) } @@ -329,17 +391,10 @@ class SearchTopBar: UIView { searchControl.snp.makeConstraints { make in make.left.equalTo(self).offset(18) make.top.equalTo(self).offset(15) + make.bottom.equalTo(self).offset(-15) make.height.equalTo(40) make.right.equalTo(cancelButton.snp.left).offset(-12) } - - segmentControl.snp.makeConstraints { make in - make.left.equalTo(self).offset(18) - make.top.equalTo(searchControl.snp.bottom).offset(24) - make.bottom.equalTo(self).offset(-24) - make.width.equalTo(200) - } - } } @@ -376,3 +431,86 @@ class SearchNoDataView: UIView { } } } + + +class SearchSuggestionsView: UIView { + let tableView: UITableView = { + let tableView = UITableView.init() + tableView.register(SearchSuggestionsViewCell.self, forCellReuseIdentifier: "SearchSuggestionsViewCell") + + return tableView + }() + + + override init(frame: CGRect) { + super.init(frame: frame) + + makeUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func makeUI() { + addSubview(tableView) + } + + override func layoutSubviews() { + super.layoutSubviews() + + tableView.snp.makeConstraints { make in + make.edges.equalTo(self) + } + } + +} + +extension SearchSuggestionsView { + static func dataSource() -> RxTableViewSectionedReloadDataSource { + return RxTableViewSectionedReloadDataSource( + configureCell: { dataSource, tableView, indexPath, item in + let cell: SearchSuggestionsViewCell = tableView.dequeueReusableCell(withIdentifier: "SearchSuggestionsViewCell", for: indexPath) as! SearchSuggestionsViewCell + + cell.titleLabel.text = item + + return cell + } + ) + } + +} + + +class SearchSuggestionsViewCell: UITableViewCell { + let titleLabel: UILabel = { + let titleLabel = UILabel.init() + titleLabel.textColor = .tertiaryText() + + return titleLabel + }() + + + + 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.left.equalTo(contentView).offset(18) + make.centerY.equalTo(contentView) + make.right.equalTo(contentView).offset(-18) + } + } + + +} diff --git a/IndieMusic/IndieMusic/Modules/Search/SearchResultsViewModel.swift b/IndieMusic/IndieMusic/Modules/Search/SearchResultsViewModel.swift index 35ca8df..3de4280 100644 --- a/IndieMusic/IndieMusic/Modules/Search/SearchResultsViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/Search/SearchResultsViewModel.swift @@ -16,18 +16,21 @@ class SearchResultsViewModel: ViewModel, ViewModelType { let viewWillAppear: ControlEvent let closeButtonTrigger: Driver let searchText: Driver + let searchTrigger: Driver let modelSelected: Driver + let suggestionSelected: Driver let searchType: BehaviorRelay } struct Output { let items: BehaviorRelay<[SearchResultsSection]> - + let suggestionsItems: BehaviorRelay<[SearchSuggestionSection]> let modelSelected: Driver } let items = BehaviorRelay<[SearchResultsSection]>.init(value: []) + let suggestionsItems = BehaviorRelay<[SearchSuggestionSection]>.init(value: []) let audioTrackItems = BehaviorRelay<[SearchResultsItem]>.init(value: []) let journalItems = BehaviorRelay<[SearchResultsItem]>.init(value: []) @@ -43,9 +46,50 @@ class SearchResultsViewModel: ViewModel, ViewModelType { }.disposed(by: rx.disposeBag) - input.searchText.debounce(.milliseconds(500)) + input.searchText.distinctUntilChanged().debounce(.milliseconds(500)) .drive { searchText in - self.fetchSearchData(keyword: searchText ?? "") + self.fetchSuggestionsData(keyword: searchText ?? "") + .subscribe { suggestions in + + self.suggestionsItems.accept([SearchSuggestionSection.init(items: suggestions)]) + } onError: { error in + self.items.accept([SearchResultsSection.journal(title: "", items: [])]) + + }.disposed(by: self.rx.disposeBag) + + + + + }.disposed(by: rx.disposeBag) + + input.searchTrigger + .withLatestFrom(input.searchText) + .drive { searchText in + self.fetchSearchData(keyword: searchText ?? "") + .subscribe { [weak self] searchResults in + + let audioTrackArray = searchResults.songs?.map({ audioTrack in + return SearchResultsItem.single(item: audioTrack) + }) + + let journalArray = searchResults.journals?.map({ journal in + return SearchResultsItem.journal(item: journal) + }) + + self?.audioTrackItems.accept(audioTrackArray ?? []) + + self?.journalItems.accept(journalArray ?? []) + } onError: { error in + self.items.accept([SearchResultsSection.journal(title: "", items: [])]) + + }.disposed(by: self.rx.disposeBag) + + + }.disposed(by: rx.disposeBag) + + + input.suggestionSelected.drive { searchText in + self.fetchSearchData(keyword: searchText) .subscribe { [weak self] searchResults in let audioTrackArray = searchResults.songs?.map({ audioTrack in @@ -60,10 +104,16 @@ class SearchResultsViewModel: ViewModel, ViewModelType { self?.journalItems.accept(journalArray ?? []) } onError: { error in - + self.items.accept([SearchResultsSection.journal(title: "", items: [])]) + }.disposed(by: self.rx.disposeBag) + + }.disposed(by: rx.disposeBag) + + + @@ -103,7 +153,8 @@ class SearchResultsViewModel: ViewModel, ViewModelType { - return Output.init(items: items, + return Output.init(items: items, + suggestionsItems: suggestionsItems, modelSelected: input.modelSelected) } @@ -114,4 +165,11 @@ class SearchResultsViewModel: ViewModel, ViewModelType { .trackError(error) } + + func fetchSuggestionsData(keyword: String, limit: Int = 10) -> Observable<[String]> { + return self.provider.suggestions(query: keyword, limit: limit) + .trackActivity(loading) + .trackError(error) + } + } diff --git a/IndieMusic/IndieMusic/Modules/Search/SearchViewController.swift b/IndieMusic/IndieMusic/Modules/Search/SearchViewController.swift index 1d15c38..8beed07 100644 --- a/IndieMusic/IndieMusic/Modules/Search/SearchViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Search/SearchViewController.swift @@ -338,7 +338,8 @@ class SearchControl: UIControl { let textField = UITextField.init() textField.font = UIFont.systemFont(ofSize: 15) textField.placeholder = "输入期刊/歌曲名" - + textField.returnKeyType = .send + return textField }() diff --git a/IndieMusic/IndieMusic/Networking/Api.swift b/IndieMusic/IndieMusic/Networking/Api.swift index 17b8449..d7949b8 100644 --- a/IndieMusic/IndieMusic/Networking/Api.swift +++ b/IndieMusic/IndieMusic/Networking/Api.swift @@ -93,9 +93,12 @@ protocol IndieMusicAPI { /// 我的获赞 func myThumbupList(page: Int, size: Int) -> Single<[MineThumbup]> /// 我的评论 - func myCommentList(page: Int, size: Int) -> Single<[Comment]> + func myCommentList(page: Int, size: Int) -> Single<[MyComment]> /// 反馈 func feedback(type: Int, content: String, images: [UIImage], contact: String) -> Single + + /// 搜索建议 + func suggestions(query: String, limit: Int) -> Single<[String]> } diff --git a/IndieMusic/IndieMusic/Networking/Rest/APIConfig.swift b/IndieMusic/IndieMusic/Networking/Rest/APIConfig.swift index 2dc8d63..0a003f8 100644 --- a/IndieMusic/IndieMusic/Networking/Rest/APIConfig.swift +++ b/IndieMusic/IndieMusic/Networking/Rest/APIConfig.swift @@ -59,7 +59,7 @@ enum APIConfig { case myThumbupList(Int, Int) case myCommentReplyList(Int, Int) case feedback([Data], [String: Any]) - + case suggestions([String: Any]) } extension APIConfig: TargetType { @@ -147,12 +147,14 @@ extension APIConfig: TargetType { case .feedback: return "luoo-user/my/feedback" + case .suggestions: + return "luoo-music/search/autoComplete" } } 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: + 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: return .get case .sendsms, .login, .autoLogin, .editAvatar, .like, .checkVersion, .logout, .sendComment, .feedback: return .post @@ -166,7 +168,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: + 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: return URLEncoding.default case .autoLogin, .editUserInfo, .editAvatar, .checkVersion, .logout, .commentLike, .sendComment, .feedback: @@ -181,7 +183,7 @@ extension APIConfig: TargetType { case .wechatAccessToken, .countryCode, .journalMusic, .imageCheckCode, .getUserInfo, .carousel, .otherUserInfo, .single, .journal, .messageList, .followingList, .followerList, .blackList, .hotCommentList, .latestCommentList, .subCommentList, .filterMenu, .journalRecommend, .searchCategory, .serach, .randomAudioTrack, .myThumbupList, .myCommentReplyList: 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): + 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): parameters = dic return .requestParameters(parameters: parameters, encoding: parameterEncoding) @@ -224,7 +226,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: + 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: return ["Authorization": AuthManager.shared.token?.basicToken ?? ""] default: return nil diff --git a/IndieMusic/IndieMusic/Networking/Rest/RestApi.swift b/IndieMusic/IndieMusic/Networking/Rest/RestApi.swift index 28475c9..fa714aa 100644 --- a/IndieMusic/IndieMusic/Networking/Rest/RestApi.swift +++ b/IndieMusic/IndieMusic/Networking/Rest/RestApi.swift @@ -311,8 +311,8 @@ extension RestApi { return requestObject(.myThumbupList(page, size), with: "data.rows", type: [MineThumbup].self) } - func myCommentList(page: Int, size: Int) -> Single<[Comment]> { - return requestObject(.myCommentReplyList(page, size), with: "data.rows", type: [Comment].self) + func myCommentList(page: Int, size: Int) -> Single<[MyComment]> { + return requestObject(.myCommentReplyList(page, size), with: "data.rows", type: [MyComment].self) } @@ -329,4 +329,13 @@ extension RestApi { return requestWithoutMapping(.feedback(nonNilDataArray, dic)).map { _ in } } + func suggestions(query: String, limit: Int) -> Single<[String]> { + let dic = ["query": query, + "limit": limit + ] as [String : Any] + return requestObject(.suggestions(dic), with: "data", type: [String].self) + + } + + }