Search for suggestion modules

dev
wenlei 11 months ago
parent b62de39551
commit 33577405d6

@ -8,12 +8,38 @@
import Foundation import Foundation
import RxDataSources 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 { struct MyCommentListSection {
var items: [Comment] var items: [MyComment]
} }
extension MyCommentListSection: SectionModelType { extension MyCommentListSection: SectionModelType {
typealias Item = Comment typealias Item = MyComment
init(original: MyCommentListSection, items: [Item]) { init(original: MyCommentListSection, items: [Item]) {
self = original self = original
@ -45,16 +71,16 @@ struct Comment: Codable, IdentifiableType, Equatable {
let avatar: String? let avatar: String?
let state: Int? let state: Int?
let userId: String? let userId: String?
let content: String? var content: String?
let commentCount: Int? let commentCount: Int?
let nickName: String? let nickName: String?
let publishTime: String? let publishTime: String?
let location: String? let location: String?
let parentId: String? let parentId: String?
let id: String let id: String
let thumbupCountString: String? var thumbupCountString: String?
let journalImage: String? let journalImage: String?
let haveThumbup: Bool? var haveThumbup: Bool?
// //
var parentCommentCount: Int? var parentCommentCount: Int?
@ -85,22 +111,42 @@ struct Comment: Codable, IdentifiableType, Equatable {
} }
static func == (lhs: Comment, rhs: Comment) -> Bool { 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 comment(Comment)
case quote(Comment) case quote(Comment)
var identity: String {
switch self {
case .comment(let comment), .quote(let comment):
return comment.id
}
} }
struct CommentSection { static func == (lhs: CommentType, rhs: CommentType) -> Bool {
var items: [CommentType] return lhs.identity == rhs.identity
}
} }
extension CommentSection: SectionModelType { struct CommentSection: IdentifiableType, Equatable {
typealias Item = CommentType 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: AnimatableSectionModelType {
typealias Item = Comment
init(original: CommentSection, items: [Item]) { init(original: CommentSection, items: [Item]) {
self = original self = original
@ -111,5 +157,22 @@ extension CommentSection: SectionModelType {
struct CommentThumbState: Codable { struct CommentThumbState: Codable {
let thumbState: Bool 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
}
}
} }

@ -90,3 +90,20 @@ extension SearchCategorySection: SectionModelType {
self.items = items 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
}
}

@ -157,7 +157,6 @@ class HomeViewCell: UITableViewCell {
lazy var rollingNoticeView: GYRollingNoticeView = { lazy var rollingNoticeView: GYRollingNoticeView = {
let rollingNoticeView = GYRollingNoticeView.init() let rollingNoticeView = GYRollingNoticeView.init()
rollingNoticeView.backgroundColor = .red
rollingNoticeView.stayInterval = 5 rollingNoticeView.stayInterval = 5
rollingNoticeView.dataSource = self rollingNoticeView.dataSource = self
// rollingNoticeView.delegate = self // rollingNoticeView.delegate = self

@ -87,11 +87,14 @@ class AudioMoreActionViewModel: ViewModel, ViewModelType {
toShare.accept(()) toShare.accept(())
case 1:// case 1://
guard let id = audioTrack?.id else { return } guard let id = audioTrack?.id,
if isLike.value { let haveCollect = audioTrack.value?.haveCollect else { return }
if haveCollect {
self.requestCancelLike(journalNo: id) self.requestCancelLike(journalNo: id)
.subscribe { _ in .subscribe { _ in
isLike.accept(false) var new = self.audioTrack.value
new?.haveCollect = false
self.audioTrack.accept(new)
dismiss.accept(()) dismiss.accept(())
} onError: { error in } onError: { error in
@ -100,7 +103,9 @@ class AudioMoreActionViewModel: ViewModel, ViewModelType {
} else { } else {
self.requestLike(journalNo: id) self.requestLike(journalNo: id)
.subscribe { _ in .subscribe { _ in
isLike.accept(true) var new = self.audioTrack.value
new?.haveCollect = true
self.audioTrack.accept(new)
dismiss.accept(()) dismiss.accept(())
} onError: { error in } onError: { error in

@ -9,6 +9,7 @@ import UIKit
import RxSwift import RxSwift
import RxCocoa import RxCocoa
import RxDataSources import RxDataSources
import FSPopoverView
class CommentDetailViewController: TableViewController { class CommentDetailViewController: TableViewController {
let commentDetailHeaderView: CommentDetailHeaderView = { let commentDetailHeaderView: CommentDetailHeaderView = {
@ -23,23 +24,27 @@ class CommentDetailViewController: TableViewController {
return commentToolView 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() { override func viewDidLoad() {
super.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() { override func makeUI() {
super.makeUI() super.makeUI()
@ -47,7 +52,6 @@ class CommentDetailViewController: TableViewController {
view.backgroundColor = .clear view.backgroundColor = .clear
view.addSubview(tableView) view.addSubview(tableView)
self.navigationController?.view.backgroundColor = UIColor.clear self.navigationController?.view.backgroundColor = UIColor.clear
// UIColor.black.withAlphaComponent(0.5)
tableView.register(CommentViewCell.self, forCellReuseIdentifier: "CommentViewCell") tableView.register(CommentViewCell.self, forCellReuseIdentifier: "CommentViewCell")
tableView.tableHeaderView = commentDetailHeaderView tableView.tableHeaderView = commentDetailHeaderView
@ -67,14 +71,55 @@ class CommentDetailViewController: TableViewController {
guard let viewModel = viewModel as? CommentDetailViewModel else { return } guard let viewModel = viewModel as? CommentDetailViewModel else { return }
let input = CommentDetailViewModel.Input.init(viewWillAppear: rx.viewWillAppear, let input = CommentDetailViewModel.Input.init(viewWillAppear: rx.viewWillAppear,
modelSelected: tableView.rx.modelSelected(CommentType.self).asDriver(), modelSelected: tableView.rx.modelSelected(Comment.self).asDriver(),
footerRefresh: footerRefreshTrigger, footerRefresh: footerRefreshTrigger,
commentText: commentToolView.textField.rx.text.asDriver(), commentText: commentToolView.textField.rx.text.asDriver(),
sendCommentTrigger: commentToolView.textField.rx.controlEvent(.editingDidEndOnExit).asDriver()) sendCommentTrigger: commentToolView.textField.rx.controlEvent(.editingDidEndOnExit).asDriver())
let output = viewModel.transform(input: input) let output = viewModel.transform(input: input)
let dataSource = CommentDetailViewController.dataSource()
let dataSource = RxTableViewSectionedAnimatedDataSource<CommentSection>(
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) output.items.bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag)
@ -92,6 +137,11 @@ class CommentDetailViewController: TableViewController {
self.updateHeaderViewFrame() self.updateHeaderViewFrame()
}.disposed(by: rx.disposeBag) }.disposed(by: rx.disposeBag)
output.clearTextSubject.subscribe { [weak self] _ in
self?.commentToolView.textField.text = ""
}.disposed(by: rx.disposeBag)
// //
NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification) NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification)
.subscribe(onNext: { notification in .subscribe(onNext: { notification in
@ -140,9 +190,6 @@ class CommentDetailViewController: TableViewController {
tapGesture.rx.event tapGesture.rx.event
.bind(onNext: { [weak self] _ in .bind(onNext: { [weak self] _ in
self?.view.endEditing(true) self?.view.endEditing(true)
self?.navigator.dismiss(sender: self)
}) })
.disposed(by: rx.disposeBag) .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 { extension CommentDetailViewController {
@ -208,29 +291,6 @@ extension CommentDetailViewController {
} }
extension CommentDetailViewController {
static func dataSource() -> RxTableViewSectionedReloadDataSource<CommentSection> {
return RxTableViewSectionedReloadDataSource<CommentSection>(
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 { class CommentDetailHeaderView: UIView {
@ -300,9 +360,7 @@ class CommentDetailHeaderView: UIView {
avatarView.snp.makeConstraints { make in avatarView.snp.makeConstraints { make in
make.left.equalTo(self).offset(18) make.left.equalTo(self).offset(18)
make.size.equalTo(CGSize.init(width: 40, height: 40)) make.size.equalTo(CGSize.init(width: 40, height: 40))
// make.centerY.equalTo(self)
make.top.equalTo(self).offset(24) make.top.equalTo(self).offset(24)
// make.bottom.equalTo(self).offset(-21)
} }
nameLabel.snp.makeConstraints { make in nameLabel.snp.makeConstraints { make in

@ -14,7 +14,7 @@ class CommentDetailViewModel: ViewModel, ViewModelType {
struct Input { struct Input {
let viewWillAppear: ControlEvent<Bool> let viewWillAppear: ControlEvent<Bool>
let modelSelected: Driver<CommentType> let modelSelected: Driver<Comment>
let footerRefresh: Observable<Void> let footerRefresh: Observable<Void>
let commentText: Driver<String?> let commentText: Driver<String?>
let sendCommentTrigger: Driver<Void> let sendCommentTrigger: Driver<Void>
@ -25,12 +25,17 @@ class CommentDetailViewModel: ViewModel, ViewModelType {
let comment: Driver<Comment> let comment: Driver<Comment>
let items: BehaviorRelay<[CommentSection]> let items: BehaviorRelay<[CommentSection]>
let modelSelected: Driver<CommentType> let modelSelected: Driver<Comment>
let clearTextSubject: PublishRelay<Void>
} }
let items = BehaviorRelay<[CommentSection]>.init(value: []) let items = BehaviorRelay<[CommentSection]>.init(value: [])
let itemSelected = PublishSubject<Comment>() let itemSelected = PublishSubject<Comment>()
let clearTextSubject = PublishRelay<Void>()
let likeSelected = PublishRelay<Comment>()
let itemReport = PublishRelay<Comment>()
let comment: Comment let comment: Comment
@ -44,11 +49,8 @@ class CommentDetailViewModel: ViewModel, ViewModelType {
input.viewWillAppear.subscribe { _ in input.viewWillAppear.subscribe { _ in
self.requestSubCommentData(parentId: self.comment.id, page: self.page, size: 10) self.requestSubCommentData(parentId: self.comment.id, page: self.page, size: 10)
.subscribe { commentArray in .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 } onError: { error in
@ -63,14 +65,10 @@ class CommentDetailViewModel: ViewModel, ViewModelType {
.trackActivity(self.footerLoading) .trackActivity(self.footerLoading)
}) })
.subscribe(onNext: { (commentArray) in .subscribe(onNext: { (commentArray) in
let newArray = commentArray.map { comment in
return CommentType.comment(comment)
}
guard let new = self.items.value.first else { return } 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) }).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) self.sendCommentData(content: comment, journalId: self.comment.journalId, parentId: self.comment.id, journalImage: self.comment.journalImage)
.subscribe { comment in .subscribe { comment in
//TODO cell self.insertComment(newComment: comment)
SVProgressHUD.showText(withStatus: "发送成功") self.clearTextSubject.accept(())
} onError: { error in } onError: { error in
}.disposed(by: self.rx.disposeBag) }.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), return Output.init(comment: Driver.just(comment),
items: items, items: items,
modelSelected: input.modelSelected) modelSelected: input.modelSelected,
clearTextSubject: clearTextSubject)
} }
@ -127,19 +154,35 @@ class CommentDetailViewModel: ViewModel, ViewModelType {
.trackError(error) .trackError(error)
} }
func insertComment(in index: Int, with newCommentType: CommentType) {
func sendCommentData(content: String, journalId: String?, parentId: String?, journalImage: String?) -> Observable<Comment> {
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 var firstSection = items.value.first
firstSection?.items.insert(newCommentType, at: index + 1) firstSection?.items.insert(newComment, at: 0)
if let updatedSection = firstSection { if let updatedSection = firstSection {
items.accept([updatedSection]) items.accept([updatedSection])
} }
} }
func sendCommentData(content: String, journalId: String?, parentId: String?, journalImage: String?) -> Observable<Comment> {
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)
} }
} }

@ -7,6 +7,7 @@
import UIKit import UIKit
import AttributedString import AttributedString
import RxSwift
class CommentHeaderView: UIView { class CommentHeaderView: UIView {
var titleLabel: UILabel = { var titleLabel: UILabel = {
@ -193,6 +194,11 @@ class CommentViewCell: UITableViewCell {
} }
//
// override func prepareForReuse() {
// super.prepareForReuse()
// disposeBag = DisposeBag()
// }
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()

@ -23,7 +23,6 @@ class CommentViewController: ViewController {
let tableView = UITableView.init() let tableView = UITableView.init()
tableView.separatorColor = .clear tableView.separatorColor = .clear
tableView.register(CommentViewCell.self, forCellReuseIdentifier: "CommentViewCell") tableView.register(CommentViewCell.self, forCellReuseIdentifier: "CommentViewCell")
tableView.register(CommentQuoteViewCell.self, forCellReuseIdentifier: "CommentQuoteViewCell")
tableView.keyboardDismissMode = .onDrag tableView.keyboardDismissMode = .onDrag
return tableView return tableView
@ -92,6 +91,7 @@ class CommentViewController: ViewController {
guard let viewModel = viewModel as? CommentViewModel else { return } guard let viewModel = viewModel as? CommentViewModel else { return }
let input = CommentViewModel.Input.init(viewWillAppear: rx.viewWillAppear, let input = CommentViewModel.Input.init(viewWillAppear: rx.viewWillAppear,
selection: tableView.rx.itemSelected.asDriver(), selection: tableView.rx.itemSelected.asDriver(),
currentCommentListType: currentCommentListType, currentCommentListType: currentCommentListType,
@ -104,37 +104,34 @@ class CommentViewController: ViewController {
) )
let output = viewModel.transform(input: input) let output = viewModel.transform(input: input)
let dataSource = RxTableViewSectionedReloadDataSource<CommentSection>( let dataSource = RxTableViewSectionedAnimatedDataSource<CommentSection>(
animationConfiguration: AnimationConfiguration.init(insertAnimation: .top, reloadAnimation: .none, deleteAnimation: .automatic),
configureCell: { dataSource, tableView, indexPath, item in configureCell: { dataSource, tableView, indexPath, item in
switch item {
case .comment(let comment):
let cell: CommentViewCell = tableView.dequeueReusableCell(withIdentifier: "CommentViewCell", for: indexPath) as! CommentViewCell let cell: CommentViewCell = tableView.dequeueReusableCell(withIdentifier: "CommentViewCell", for: indexPath) as! CommentViewCell
cell.comment = comment cell.comment = item
cell.likeButton.rx.tap.subscribe { _ in
viewModel.likeSelected.accept(item)
}.disposed(by: cell.rx.disposeBag)
cell.avatarView.rx.tapGesture().when(.recognized) cell.avatarView.rx.tapGesture().when(.recognized)
.subscribe { [weak self] tap in .subscribe { [weak self] tap in
guard let userID = comment.userId else { return } guard let userID = item.userId else { return }
let personalViewModel = PersonalViewModel.init(userID: userID, provider: viewModel.provider) let personalViewModel = PersonalViewModel.init(userID: userID, provider: viewModel.provider)
self?.navigator.show(segue: .personal(viewModel: personalViewModel), sender: self) self?.navigator.show(segue: .personal(viewModel: personalViewModel), sender: self)
}.disposed(by: self.rx.disposeBag) }.disposed(by: cell.rx.disposeBag)
cell.likeButton.rx.tap.subscribe { _ in
viewModel.likeSelected.onNext(comment.id)
}.disposed(by: self.rx.disposeBag)
cell.moreButton.rx.tap.subscribe { _ in cell.moreButton.rx.tap.subscribe { _ in
let commentDetailViewModel = CommentDetailViewModel.init(comment: comment, provider: viewModel.provider) let commentDetailViewModel = CommentDetailViewModel.init(comment: item, provider: viewModel.provider)
self.navigator.show(segue: .commentDetail(viewModel:commentDetailViewModel), sender: self, transition: .modal) self.navigator.show(segue: .commentDetail(viewModel:commentDetailViewModel), sender: self, transition: .modal)
}.disposed(by: self.rx.disposeBag) }.disposed(by: cell.rx.disposeBag)
@ -142,53 +139,30 @@ class CommentViewController: ViewController {
.subscribe { [weak self] tap in .subscribe { [weak self] tap in
guard let self = self else { return } guard let self = self else { return }
if comment.userId == UserDefaults.AccountInfo.string(forKey: .userID) { if item.userId == UserDefaults.AccountInfo.string(forKey: .userID) {
self.menuView.items = self.setupMenuItems(features: [.copy, .report, .delete]) self.menuView.items = self.setupMenuItems(cell: cell, features: [.copy, .report, .delete])
} else { } else {
self.menuView.items = self.setupMenuItems(features: [.copy, .delete]) self.menuView.items = self.setupMenuItems(cell: cell, features: [.copy, .delete])
} }
if let location = tap.element?.location(in: cell) { if let location = tap.element?.location(in: cell) {
self.menuView.present(fromPoint: location, in: cell, displayIn: self.view) self.menuView.present(fromPoint: location, in: cell, displayIn: self.view)
} }
}.disposed(by: self.rx.disposeBag) }.disposed(by: cell.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)
}
cell.moreClousres = {[weak self] commentID in
// let commentDetailViewModel = CommentDetailViewModel.init(commentID: commentID, provider: viewModel.provider)
// self?.navigator.show(segue: .commentDetail(viewModel: commentDetailViewModel), sender: self)
}
return cell 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.clearTextSubject.subscribe { [weak self] _ in
output.itemSelected.subscribe { [weak self] sectionItem in self?.commentToolView.textField.text = ""
}.disposed(by: rx.disposeBag) }.disposed(by: rx.disposeBag)
// //
NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification) NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification)
.subscribe(onNext: { notification in .subscribe(onNext: { notification in
@ -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 items: [FSPopoverListItem] = features.map { feature in
let item = FSPopoverListTextItem(scrollDirection: .horizontal) let item = FSPopoverListTextItem(scrollDirection: .horizontal)
item.title = feature.description item.title = feature.description
@ -279,7 +256,16 @@ class CommentViewController: ViewController {
guard let item = item as? FSPopoverListTextItem else { guard let item = item as? FSPopoverListTextItem else {
return 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() item.updateLayout()
return item return item

@ -31,21 +31,25 @@ class CommentViewModel: ViewModel, ViewModelType {
let insertCommment: PublishSubject<Comment> let insertCommment: PublishSubject<Comment>
let toCommentDetail: PublishRelay<Comment> let toCommentDetail: PublishRelay<Comment>
let clearTextSubject: PublishRelay<Void>
} }
let items = BehaviorRelay<[CommentSection]>.init(value: []) let items = BehaviorRelay<[CommentSection]>.init(value: [])
let commentItems = BehaviorRelay<[CommentType]>.init(value: []) // let commentItems = BehaviorRelay<[CommentType]>.init(value: [])
let quoteItems = BehaviorRelay<[CommentType]>.init(value: []) // let quoteItems = BehaviorRelay<[CommentType]>.init(value: [])
let itemSelected = PublishSubject<Comment>() let itemSelected = PublishSubject<Comment>()
let likeSelected = PublishSubject<String>()
let insertCommment: PublishSubject<Comment> = .init() let insertCommment: PublishSubject<Comment> = .init()
let toCommentDetail: PublishRelay<Comment> = .init() let toCommentDetail: PublishRelay<Comment> = .init()
let clearTextSubject = PublishRelay<Void>()
let likeSelected = PublishRelay<Comment>()
let itemReport = PublishRelay<Comment>()
let itemDelete = PublishRelay<Comment>()
var journal: Journal var journal: Journal
@ -99,9 +103,6 @@ class CommentViewModel: ViewModel, ViewModelType {
} onError: { error in } onError: { error in
} .disposed(by: self.rx.disposeBag) } .disposed(by: self.rx.disposeBag)
} }
@ -110,27 +111,24 @@ class CommentViewModel: ViewModel, ViewModelType {
}.disposed(by: rx.disposeBag) }.disposed(by: rx.disposeBag)
likeSelected.subscribe { [weak self] comment in
guard let self = self,
let comment = comment.element else { return }
items.subscribe { commentSectionArray in self.requestCommentLike(commentID: comment.id)
guard let firstSection = self.items.value.first else { return } .observe(on: MainScheduler.instance)
.subscribe(onNext: { commentThumbState in
}.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)
var new = comment
likeSelected.subscribe { [weak self] commentID in
guard let self = self else { return }
if let thumbupCount = Int(new.thumbupCountString ?? "0") {
let newThumbupCount = commentThumbState.thumbState == true ? (thumbupCount + 1) : (thumbupCount - 1)
new.thumbupCountString = "\(newThumbupCount)"
}
new.haveThumbup = commentThumbState.thumbState
self.requestCommentLike(commentID: commentID) self.updateComment(withUpdatedComment: new)
.subscribe(onNext: { commentThumbState in
}, onError: { error in }, onError: { error in
@ -151,10 +149,8 @@ class CommentViewModel: ViewModel, ViewModelType {
self.sendCommentData(content: comment, journalId: self.journal.id, parentId: nil, journalImage: self.journal.image) self.sendCommentData(content: comment, journalId: self.journal.id, parentId: nil, journalImage: self.journal.image)
.subscribe { comment in .subscribe { comment in
//TODO cell self.insertComment(newComment: comment)
self.clearTextSubject.accept(())
} onError: { error in } onError: { error in
}.disposed(by: self.rx.disposeBag) }.disposed(by: self.rx.disposeBag)
@ -163,50 +159,30 @@ class CommentViewModel: ViewModel, ViewModelType {
} }
}) })
itemReport.subscribe { comment in
}.disposed(by: rx.disposeBag)
itemDelete.subscribe { comment in
}.disposed(by: rx.disposeBag)
return Output.init(items: items, return Output.init(items: items,
itemSelected: itemSelected, itemSelected: itemSelected,
insertCommment: insertCommment, insertCommment: insertCommment,
toCommentDetail: toCommentDetail) toCommentDetail: toCommentDetail,
clearTextSubject: clearTextSubject)
} }
private func handleReceivedComments(comments: [Comment]) { private func handleReceivedComments(comments: [Comment]) {
let commentTypes = comments.map { CommentType.comment($0) } let commentSections = [CommentSection.init(id: "", items: comments)]
var commentSections = [CommentSection(items: commentTypes)]
self.items.accept(commentSections) 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]> { func requestHotCommentListData(journalID: String, page: Int, size: Int) -> Observable<[Comment]> {
return self.provider.hotCommentList(journalId: journalID, page: page, size: size) 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]> { func requestSubCommentData(parentId: String, page: Int, size: Int) -> Observable<[Comment]> {
return self.provider.subCommentList(parentId: parentId, page: page, size: size) return self.provider.subCommentList(parentId: parentId, page: page, size: size)
.trackActivity(loading) .trackActivity(loading)
@ -240,20 +212,35 @@ class CommentViewModel: ViewModel, ViewModelType {
.trackError(error) .trackError(error)
} }
func insertComment(in index: Int, with newCommentType: CommentType) {
func sendCommentData(content: String, journalId: String?, parentId: String?, journalImage: String?) -> Observable<Comment> {
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 var firstSection = items.value.first
firstSection?.items.insert(newCommentType, at: index + 1) firstSection?.items.insert(newComment, at: 0)
if let updatedSection = firstSection { if let updatedSection = firstSection {
items.accept([updatedSection]) items.accept([updatedSection])
} }
} }
func sendCommentData(content: String, journalId: String?, parentId: String?, journalImage: String?) -> Observable<Comment> { func updateComment(withUpdatedComment updatedComment: Comment) {
return self.provider.sendComment(content: content, journalId: journalId, parentId: parentId, journalImage: journalImage) let updatedSections = items.value.map { section -> CommentSection in
.trackActivity(loading) let updatedComments = section.items.map { comment -> Comment in
.trackError(error) guard comment.id == updatedComment.id else { return comment }
return updatedComment
}
return CommentSection(id: section.id, items: updatedComments)
}
items.accept(updatedSections)
} }

@ -177,7 +177,7 @@ class JournalDetailController: TableViewController {
output.journal.subscribe { [weak self] journal in output.journal.subscribe { [weak self] journal in
guard let self = self else { return } 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.journalCollapsibleHeaderView.journal = journal
self.updateHeader() self.updateHeader()

@ -227,6 +227,7 @@ class JournalCollapsibleHeaderView: UIView {
saveButton.rx.tap.subscribe { [weak self] _ in saveButton.rx.tap.subscribe { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
self.popoverView.dismiss() self.popoverView.dismiss()
}.disposed(by: rx.disposeBag) }.disposed(by: rx.disposeBag)

@ -172,18 +172,11 @@ class JournalDetailViewModel: ViewModel, ViewModelType {
item.subscribe { [weak self] audioTrack in 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 self?.updateAudioTrack(with: newTrack, identifiedBy: newTrack.id)
// 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)
// }
// }
}.disposed(by: rx.disposeBag) }.disposed(by: rx.disposeBag)
@ -198,13 +191,30 @@ class JournalDetailViewModel: ViewModel, ViewModelType {
) )
} }
func sectionType(for sectionIndex: Int) -> JournalItem? { func updateAudioTrack(with newTrack: AudioTrack, identifiedBy trackId: String) {
guard sectionIndex < self.items.value.count else { return nil } let currentSections = items.value
let section = items.value[sectionIndex]
// item let updatedSections = currentSections.map { section -> JournalSection in
// switch section {
return section.items.first 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]> { func requestMusic(journalNo: String) -> Observable<[AudioTrack]> {

@ -85,7 +85,7 @@ extension MyCommentListController {
let cell: CommentListViewCell = tableView.dequeueReusableCell(withIdentifier: "CommentListViewCell", for: indexPath) as! CommentListViewCell let cell: CommentListViewCell = tableView.dequeueReusableCell(withIdentifier: "CommentListViewCell", for: indexPath) as! CommentListViewCell
cell.comment = item cell.myComment = item
return cell return cell
} }
@ -180,17 +180,17 @@ class CommentListViewCell: UITableViewCell {
var buttonTapCallback: ((User) -> ())? var buttonTapCallback: ((User) -> ())?
var comment: Comment? { var myComment: MyComment? {
didSet { didSet {
guard let comment = comment else { return } guard let myComment = myComment else { return }
avatarView.kf.setImage(with: URL.init(string: comment.avatar ?? "")) avatarView.kf.setImage(with: URL.init(string: myComment.commenterAvatar ?? ""))
audioTrackView.kf.setImage(with: URL.init(string: comment.journalImage ?? "")) audioTrackView.kf.setImage(with: URL.init(string: myComment.journalImage ?? ""))
nameLabel.text = comment.nickName nameLabel.text = myComment.nickName
dateLabel.text = "\(comment.publishTime)" let date = Date.init(dateString: myComment.createTime ?? "", format: "yyyy-MM-dd HH:mm:ss")
commentLabel.text = comment.content dateLabel.text = date.timeAgoSinceNow
// myCommentLabel.text = "\(comment.comm)" commentLabel.text = myComment.content
myCommentLabel.text = "我的评论:\(myComment.commentContent ?? "")"
} }
} }

@ -30,20 +30,9 @@ class MyCommentListViewModel: ViewModel, ViewModelType {
input.viewWillAppear.subscribe { [weak self] _ in input.viewWillAppear.subscribe { [weak self] _ in
guard let self = self else { return } 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) }.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([]) } guard let self = self else { return Observable.just([]) }
self.page = 1 self.page = 1
@ -54,7 +43,7 @@ class MyCommentListViewModel: ViewModel, ViewModelType {
self.items.accept([MyCommentListSection.init(items: items)]) self.items.accept([MyCommentListSection.init(items: items)])
}).disposed(by: rx.disposeBag) }).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([]) } guard let self = self else { return Observable.just([]) }
self.page += 1 self.page += 1
return self.requestCommentList(page: self.page, size: 10) 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) self.provider.myCommentList(page: page, size: size)
.trackActivity(loading) .trackActivity(loading)
.trackError(error) .trackError(error)

@ -104,7 +104,7 @@ class PersonalViewController: ViewController {
}.disposed(by: rx.disposeBag) }.disposed(by: rx.disposeBag)
collectionView.mj_header = nil
} }
@ -137,7 +137,9 @@ class PersonalViewController: ViewController {
cell.audioTrack = audioTrack cell.audioTrack = audioTrack
cell.buttonTapCallback = { [weak self] audioTrack in 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)) self?.navigator.show(segue: .audioMore(viewModel: audioMoreActionViewModel), sender: self, transition: .navigationPresent(type: .audioMore))

@ -42,7 +42,7 @@ class PersonalViewModel: ViewModel, ViewModelType {
let itemSelected = PublishSubject<PersonInfoItem>() let itemSelected = PublishSubject<PersonInfoItem>()
let user = BehaviorRelay<User?>.init(value: nil) let user = BehaviorRelay<User?>.init(value: nil)
let personInfoLikeType = BehaviorRelay<PersonInfoLikeType>.init(value: .audio) let personInfoLikeType = BehaviorRelay<PersonInfoLikeType>.init(value: .audio)
let needRefresh: BehaviorRelay<AudioTrack?> = .init(value: nil) let item: BehaviorRelay<AudioTrack?> = .init(value: nil)
let followingButtonTrigger = PublishRelay<Void>.init() let followingButtonTrigger = PublishRelay<Void>.init()
@ -84,10 +84,10 @@ class PersonalViewModel: ViewModel, ViewModelType {
self.page = 1 self.page = 1
if personInfoLikeType == .audio { 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) .trackActivity(self.headerLoading)
} else { } 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) .trackActivity(self.headerLoading)
} }

@ -15,9 +15,45 @@ class SearchResultsController: ViewController {
let searchTopBar: SearchTopBar = { let searchTopBar: SearchTopBar = {
let searchTopBar = SearchTopBar.init() let searchTopBar = SearchTopBar.init()
searchTopBar.setContentHuggingPriority(.required, for: .horizontal)
searchTopBar.setContentCompressionResistancePriority(.required, for: .horizontal)
return searchTopBar 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 = { let searchNoDataView: SearchNoDataView = {
@ -55,6 +91,15 @@ class SearchResultsController: ViewController {
return collectionView return collectionView
}() }()
let searchSuggestionsView: SearchSuggestionsView = {
let searchSuggestionsView = SearchSuggestionsView.init()
searchSuggestionsView.isHidden = true
return searchSuggestionsView
}()
let searchType = BehaviorRelay<SearchType>.init(value: .audio) let searchType = BehaviorRelay<SearchType>.init(value: .audio)
override func viewDidLoad() { override func viewDidLoad() {
@ -66,7 +111,7 @@ class SearchResultsController: ViewController {
override func makeUI() { override func makeUI() {
super.makeUI() super.makeUI()
searchTopBar.segmentControl.titleBtnOnClick = { [weak self] (label, index) in segmentControl.titleBtnOnClick = { [weak self] (label, index) in
if index == 0 { if index == 0 {
self?.searchType.accept(.audio) self?.searchType.accept(.audio)
} else { } else {
@ -80,8 +125,10 @@ class SearchResultsController: ViewController {
view.backgroundColor = .white view.backgroundColor = .white
view.addSubview(searchTopBar) view.addSubview(searchTopBar)
view.addSubview(segmentControl)
view.addSubview(searchNoDataView) view.addSubview(searchNoDataView)
view.addSubview(collectionView) view.addSubview(collectionView)
view.addSubview(searchSuggestionsView)
} }
@ -108,21 +155,34 @@ class SearchResultsController: ViewController {
let input = SearchResultsViewModel.Input.init(viewWillAppear: rx.viewWillAppear, let input = SearchResultsViewModel.Input.init(viewWillAppear: rx.viewWillAppear,
closeButtonTrigger: self.searchTopBar.cancelButton.rx.tap.asDriver(), closeButtonTrigger: self.searchTopBar.cancelButton.rx.tap.asDriver(),
searchText: searchTopBar.searchControl.textField.rx.text.asDriver(), searchText: searchTopBar.searchControl.textField.rx.text.asDriver(),
searchTrigger: searchTopBar.searchControl.textField.rx.controlEvent(.editingDidEndOnExit).asDriver(),
modelSelected: collectionView.rx.modelSelected(SearchResultsItem.self).asDriver(), modelSelected: collectionView.rx.modelSelected(SearchResultsItem.self).asDriver(),
suggestionSelected: searchSuggestionsView.tableView.rx.modelSelected(String.self).asDriver(),
searchType: searchType) searchType: searchType)
let output = viewModel.transform(input: input) let output = viewModel.transform(input: input)
let dataSource = SearchResultsController.dataSource { [weak self] cell, audioTrack in let dataSource = SearchResultsController.dataSource { [weak self] cell, audioTrack in
// let audioMoreActionViewModel = AudioMoreActionViewModel.init(audioTrack: audioTrack, provider: viewModel.provider) let item: BehaviorRelay<AudioTrack?> = .init(value: audioTrack)
//
// self?.navigator.show(segue: .audioMore(viewModel: audioMoreActionViewModel), sender: self, transition: .navigationPresent(type: .audioMore)) 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.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 searchTopBar.cancelButton.rx.tap.subscribe { [weak self] _ in
self?.navigator.pop(sender: self) self?.navigator.pop(sender: self)
@ -156,6 +216,19 @@ class SearchResultsController: ViewController {
}.disposed(by: rx.disposeBag) }.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.left.equalTo(view)
make.right.equalTo(view) make.right.equalTo(view)
make.top.equalTo(view).offset(BaseDimensions.statusBarHeight + 15) 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 searchNoDataView.snp.makeConstraints { make in
@ -178,7 +257,14 @@ class SearchResultsController: ViewController {
collectionView.snp.makeConstraints { make in collectionView.snp.makeConstraints { make in
make.left.equalTo(view) make.left.equalTo(view)
make.right.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) make.bottom.equalTo(view)
} }
@ -190,7 +276,7 @@ class SearchResultsController: ViewController {
extension SearchResultsController { extension SearchResultsController {
//TODO //TODO
static func dataSource(_ buttonTapHandler: @escaping (UITableViewCell, AudioTrack) -> Void) -> RxCollectionViewSectionedReloadDataSource<SearchResultsSection> { static func dataSource(_ buttonTapHandler: @escaping (UICollectionViewCell, AudioTrack) -> Void) -> RxCollectionViewSectionedReloadDataSource<SearchResultsSection> {
return RxCollectionViewSectionedReloadDataSource<SearchResultsSection>( return RxCollectionViewSectionedReloadDataSource<SearchResultsSection>(
configureCell: { dataSource, collectionView, indexPath, item in configureCell: { dataSource, collectionView, indexPath, item in
@ -200,6 +286,10 @@ extension SearchResultsController {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "JournalAudioCollectionViewCell", for: indexPath) as! JournalAudioCollectionViewCell let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "JournalAudioCollectionViewCell", for: indexPath) as! JournalAudioCollectionViewCell
cell.audioTrack = audioTrack cell.audioTrack = audioTrack
cell.buttonTapCallback = { audioTrack in
buttonTapHandler(cell, audioTrack)
}
return cell return cell
case .journal(let journal): case .journal(let journal):
@ -252,7 +342,6 @@ class SearchTopBar: UIView {
let searchControl: SearchControl = { let searchControl: SearchControl = {
let searchControl = SearchControl.init() let searchControl = SearchControl.init()
searchControl.layer.cornerRadius = 20 searchControl.layer.cornerRadius = 20
return searchControl 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(searchControl)
addSubview(cancelButton) addSubview(cancelButton)
addSubview(segmentControl)
} }
@ -329,17 +391,10 @@ class SearchTopBar: UIView {
searchControl.snp.makeConstraints { make in searchControl.snp.makeConstraints { make in
make.left.equalTo(self).offset(18) make.left.equalTo(self).offset(18)
make.top.equalTo(self).offset(15) make.top.equalTo(self).offset(15)
make.bottom.equalTo(self).offset(-15)
make.height.equalTo(40) make.height.equalTo(40)
make.right.equalTo(cancelButton.snp.left).offset(-12) 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<SearchSuggestionSection> {
return RxTableViewSectionedReloadDataSource<SearchSuggestionSection>(
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)
}
}
}

@ -16,18 +16,21 @@ class SearchResultsViewModel: ViewModel, ViewModelType {
let viewWillAppear: ControlEvent<Bool> let viewWillAppear: ControlEvent<Bool>
let closeButtonTrigger: Driver<Void> let closeButtonTrigger: Driver<Void>
let searchText: Driver<String?> let searchText: Driver<String?>
let searchTrigger: Driver<Void>
let modelSelected: Driver<SearchResultsItem> let modelSelected: Driver<SearchResultsItem>
let suggestionSelected: Driver<String>
let searchType: BehaviorRelay<SearchType> let searchType: BehaviorRelay<SearchType>
} }
struct Output { struct Output {
let items: BehaviorRelay<[SearchResultsSection]> let items: BehaviorRelay<[SearchResultsSection]>
let suggestionsItems: BehaviorRelay<[SearchSuggestionSection]>
let modelSelected: Driver<SearchResultsItem> let modelSelected: Driver<SearchResultsItem>
} }
let items = BehaviorRelay<[SearchResultsSection]>.init(value: []) let items = BehaviorRelay<[SearchResultsSection]>.init(value: [])
let suggestionsItems = BehaviorRelay<[SearchSuggestionSection]>.init(value: [])
let audioTrackItems = BehaviorRelay<[SearchResultsItem]>.init(value: []) let audioTrackItems = BehaviorRelay<[SearchResultsItem]>.init(value: [])
let journalItems = BehaviorRelay<[SearchResultsItem]>.init(value: []) let journalItems = BehaviorRelay<[SearchResultsItem]>.init(value: [])
@ -43,7 +46,24 @@ class SearchResultsViewModel: ViewModel, ViewModelType {
}.disposed(by: rx.disposeBag) }.disposed(by: rx.disposeBag)
input.searchText.debounce(.milliseconds(500)) input.searchText.distinctUntilChanged().debounce(.milliseconds(500))
.drive { searchText in
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 .drive { searchText in
self.fetchSearchData(keyword: searchText ?? "") self.fetchSearchData(keyword: searchText ?? "")
.subscribe { [weak self] searchResults in .subscribe { [weak self] searchResults in
@ -60,12 +80,42 @@ class SearchResultsViewModel: ViewModel, ViewModelType {
self?.journalItems.accept(journalArray ?? []) self?.journalItems.accept(journalArray ?? [])
} onError: { error in } onError: { error in
self.items.accept([SearchResultsSection.journal(title: "", items: [])])
}.disposed(by: self.rx.disposeBag) }.disposed(by: self.rx.disposeBag)
}.disposed(by: 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
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.searchType.subscribe { [weak self] searchType in input.searchType.subscribe { [weak self] searchType in
guard let self = self else { return } guard let self = self else { return }
@ -104,6 +154,7 @@ class SearchResultsViewModel: ViewModel, ViewModelType {
return Output.init(items: items, return Output.init(items: items,
suggestionsItems: suggestionsItems,
modelSelected: input.modelSelected) modelSelected: input.modelSelected)
} }
@ -114,4 +165,11 @@ class SearchResultsViewModel: ViewModel, ViewModelType {
.trackError(error) .trackError(error)
} }
func fetchSuggestionsData(keyword: String, limit: Int = 10) -> Observable<[String]> {
return self.provider.suggestions(query: keyword, limit: limit)
.trackActivity(loading)
.trackError(error)
}
} }

@ -338,6 +338,7 @@ class SearchControl: UIControl {
let textField = UITextField.init() let textField = UITextField.init()
textField.font = UIFont.systemFont(ofSize: 15) textField.font = UIFont.systemFont(ofSize: 15)
textField.placeholder = "输入期刊/歌曲名" textField.placeholder = "输入期刊/歌曲名"
textField.returnKeyType = .send
return textField return textField
}() }()

@ -93,9 +93,12 @@ protocol IndieMusicAPI {
/// ///
func myThumbupList(page: Int, size: Int) -> Single<[MineThumbup]> 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<Void> func feedback(type: Int, content: String, images: [UIImage], contact: String) -> Single<Void>
///
func suggestions(query: String, limit: Int) -> Single<[String]>
} }

@ -59,7 +59,7 @@ enum APIConfig {
case myThumbupList(Int, Int) case myThumbupList(Int, Int)
case myCommentReplyList(Int, Int) case myCommentReplyList(Int, Int)
case feedback([Data], [String: Any]) case feedback([Data], [String: Any])
case suggestions([String: Any])
} }
extension APIConfig: TargetType { extension APIConfig: TargetType {
@ -147,12 +147,14 @@ extension APIConfig: TargetType {
case .feedback: case .feedback:
return "luoo-user/my/feedback" return "luoo-user/my/feedback"
case .suggestions:
return "luoo-music/search/autoComplete"
} }
} }
var method: Moya.Method { var method: Moya.Method {
switch self { 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 return .get
case .sendsms, .login, .autoLogin, .editAvatar, .like, .checkVersion, .logout, .sendComment, .feedback: case .sendsms, .login, .autoLogin, .editAvatar, .like, .checkVersion, .logout, .sendComment, .feedback:
return .post return .post
@ -166,7 +168,7 @@ extension APIConfig: TargetType {
var parameterEncoding: ParameterEncoding { var parameterEncoding: ParameterEncoding {
switch self { 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 return URLEncoding.default
case .autoLogin, .editUserInfo, .editAvatar, .checkVersion, .logout, .commentLike, .sendComment, .feedback: 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: 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 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 parameters = dic
return .requestParameters(parameters: parameters, encoding: parameterEncoding) return .requestParameters(parameters: parameters, encoding: parameterEncoding)
@ -224,7 +226,7 @@ extension APIConfig: TargetType {
var headers : [String : String]? { var headers : [String : String]? {
switch self { 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 ?? ""] return ["Authorization": AuthManager.shared.token?.basicToken ?? ""]
default: default:
return nil return nil

@ -311,8 +311,8 @@ extension RestApi {
return requestObject(.myThumbupList(page, size), with: "data.rows", type: [MineThumbup].self) return requestObject(.myThumbupList(page, size), with: "data.rows", type: [MineThumbup].self)
} }
func myCommentList(page: Int, size: Int) -> Single<[Comment]> { func myCommentList(page: Int, size: Int) -> Single<[MyComment]> {
return requestObject(.myCommentReplyList(page, size), with: "data.rows", type: [Comment].self) return requestObject(.myCommentReplyList(page, size), with: "data.rows", type: [MyComment].self)
} }
@ -329,4 +329,13 @@ extension RestApi {
return requestWithoutMapping(.feedback(nonNilDataArray, dic)).map { _ in } 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)
}
} }

Loading…
Cancel
Save