feat: implement pick server view search cell & server list cell
This commit is contained in:
parent
eb7a33932e
commit
027fec1cc9
|
@ -16,6 +16,8 @@
|
|||
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */; };
|
||||
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */; };
|
||||
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D31D25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift */; };
|
||||
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */; };
|
||||
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; };
|
||||
18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; };
|
||||
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */; };
|
||||
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A8B25C295CC009AA50C /* StatusView.swift */; };
|
||||
|
@ -214,6 +216,8 @@
|
|||
0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoriesCell.swift; sourceTree = "<group>"; };
|
||||
0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoryView.swift; sourceTree = "<group>"; };
|
||||
0FB3D31D25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoryCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSearchCell.swift; sourceTree = "<group>"; };
|
||||
0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = "<group>"; };
|
||||
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = "<group>"; };
|
||||
2D152A8B25C295CC009AA50C /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; };
|
||||
2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = "<group>"; };
|
||||
|
@ -438,6 +442,8 @@
|
|||
children = (
|
||||
0FB3D2FD25E4CB6400AAD544 /* PickServerTitleCell.swift */,
|
||||
0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */,
|
||||
0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */,
|
||||
0FB3D33725E6401400AAD544 /* PickServerCell.swift */,
|
||||
);
|
||||
path = TableViewCell;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1370,6 +1376,7 @@
|
|||
0FB3D2F725E4C24D00AAD544 /* PickServerViewModel.swift in Sources */,
|
||||
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
|
||||
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
|
||||
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */,
|
||||
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */,
|
||||
0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */,
|
||||
DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */,
|
||||
|
@ -1417,6 +1424,7 @@
|
|||
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */,
|
||||
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
|
||||
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */,
|
||||
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */,
|
||||
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
|
||||
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */,
|
||||
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */,
|
||||
|
|
|
@ -111,6 +111,10 @@ internal enum L10n {
|
|||
/// Pick a Server,\nany server.
|
||||
internal static let title = L10n.tr("Localizable", "Scene.ServerPicker.Title")
|
||||
internal enum Button {
|
||||
/// See less
|
||||
internal static let seeLess = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeLess")
|
||||
/// See More
|
||||
internal static let seeMore = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeMore")
|
||||
internal enum Category {
|
||||
/// All
|
||||
internal static let all = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.All")
|
||||
|
@ -120,6 +124,14 @@ internal enum L10n {
|
|||
/// Find a server or join your own...
|
||||
internal static let placeholder = L10n.tr("Localizable", "Scene.ServerPicker.Input.Placeholder")
|
||||
}
|
||||
internal enum Label {
|
||||
/// CATEGORY
|
||||
internal static let category = L10n.tr("Localizable", "Scene.ServerPicker.Label.Category")
|
||||
/// LANGUAGE
|
||||
internal static let language = L10n.tr("Localizable", "Scene.ServerPicker.Label.Language")
|
||||
/// USERS
|
||||
internal static let users = L10n.tr("Localizable", "Scene.ServerPicker.Label.Users")
|
||||
}
|
||||
}
|
||||
internal enum ServerRules {
|
||||
/// By continuing, you're subject to the terms of service and privacy policy for %@.
|
||||
|
|
|
@ -33,6 +33,12 @@
|
|||
"Scene.ServerPicker.Title" = "Pick a Server,
|
||||
any server.";
|
||||
"Scene.ServerPicker.Button.Category.All" = "All";
|
||||
"Scene.ServerPicker.Button.SeeLess" = "See less";
|
||||
"Scene.ServerPicker.Button.SeeMore" = "See More";
|
||||
"Scene.ServerPicker.Label.Language" = "LANGUAGE";
|
||||
"Scene.ServerPicker.Label.Users" = "USERS";
|
||||
"Scene.ServerPicker.Label.Category" = "CATEGORY";
|
||||
|
||||
"Scene.ServerRules.Button.Confirm" = "I Agree";
|
||||
"Scene.ServerRules.Prompt" = "By continuing, you're subject to the terms of service and privacy policy for %@.";
|
||||
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";
|
||||
|
|
|
@ -32,7 +32,8 @@ final class PickServerViewController: UIViewController, NeedsDependency {
|
|||
let tableView = ControlContainableTableView()
|
||||
tableView.register(PickServerTitleCell.self, forCellReuseIdentifier: String(describing: PickServerTitleCell.self))
|
||||
tableView.register(PickServerCategoriesCell.self, forCellReuseIdentifier: String(describing: PickServerCategoriesCell.self))
|
||||
// tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||
tableView.register(PickServerSearchCell.self, forCellReuseIdentifier: String(describing: PickServerSearchCell.self))
|
||||
tableView.register(PickServerCell.self, forCellReuseIdentifier: String(describing: PickServerCell.self))
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .none
|
||||
tableView.backgroundColor = .clear
|
||||
|
@ -83,7 +84,20 @@ extension PickServerViewController {
|
|||
nextStepButton.setTitle(L10n.Common.Controls.Actions.continue, for: .normal)
|
||||
}
|
||||
|
||||
viewModel.tableView = tableView
|
||||
tableView.delegate = viewModel
|
||||
tableView.dataSource = viewModel
|
||||
|
||||
viewModel.searchedServers
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { completion in
|
||||
print("22")
|
||||
} receiveValue: { [weak self] servers in
|
||||
self?.tableView.reloadSections(IndexSet(integer: 3), with: .automatic)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
||||
viewModel.fetchAllServers()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ class PickServerViewModel: NSObject {
|
|||
enum Section: CaseIterable {
|
||||
case title
|
||||
case categories
|
||||
case search
|
||||
case serverList
|
||||
}
|
||||
|
||||
|
@ -32,23 +33,24 @@ class PickServerViewModel: NSObject {
|
|||
case .All:
|
||||
return L10n.Scene.ServerPicker.Button.Category.all
|
||||
case .Some(let masCategory):
|
||||
// TODO: Use emoji as placeholders
|
||||
switch masCategory.category {
|
||||
case .academia:
|
||||
return "AC"
|
||||
return "📚"
|
||||
case .activism:
|
||||
return "AT"
|
||||
return "✊"
|
||||
case .food:
|
||||
return "F"
|
||||
return "🍕"
|
||||
case .furry:
|
||||
return "FU"
|
||||
return "🦁"
|
||||
case .games:
|
||||
return "G"
|
||||
return "🕹"
|
||||
case .general:
|
||||
return "GE"
|
||||
case .journalism:
|
||||
return "JO"
|
||||
return "📰"
|
||||
case .lgbt:
|
||||
return "LG"
|
||||
return "🏳️🌈"
|
||||
case .regional:
|
||||
return "📍"
|
||||
case .art:
|
||||
|
@ -58,7 +60,7 @@ class PickServerViewModel: NSObject {
|
|||
case .tech:
|
||||
return "📱"
|
||||
case ._other:
|
||||
return "UN"
|
||||
return "❓"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,11 +74,15 @@ class PickServerViewModel: NSObject {
|
|||
|
||||
let searchText = CurrentValueSubject<String?, Never>(nil)
|
||||
|
||||
let allServers = CurrentValueSubject<[Mastodon.Entity.Instance], Error>([])
|
||||
let searchedServers = CurrentValueSubject<[Mastodon.Entity.Instance], Error>([])
|
||||
let allServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([])
|
||||
let searchedServers = CurrentValueSubject<[Mastodon.Entity.Server], Error>([])
|
||||
|
||||
let nextButtonEnable = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
private var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
weak var tableView: UITableView?
|
||||
|
||||
init(context: AppContext, mode: PickServerMode) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
|
@ -89,20 +95,89 @@ class PickServerViewModel: NSObject {
|
|||
let masCategories = context.apiService.stubCategories()
|
||||
categories.append(.All)
|
||||
categories.append(contentsOf: masCategories.map { Category.Some($0) })
|
||||
|
||||
Publishers.CombineLatest3(
|
||||
selectCategoryIndex,
|
||||
searchText.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates(),
|
||||
allServers
|
||||
)
|
||||
.flatMap { [weak self] (selectCategoryIndex, searchText, allServers) -> AnyPublisher<[Mastodon.Entity.Server], Error> in
|
||||
guard let self = self else { return Just([]).setFailureType(to: Error.self).eraseToAnyPublisher() }
|
||||
|
||||
// 1. Search from the servers recorded in joinmastodon.org
|
||||
let searchedServersFromAPI = self.searchServersFromAPI(category: self.categories[selectCategoryIndex], searchText: searchText, allServers: allServers)
|
||||
if !searchedServersFromAPI.isEmpty {
|
||||
// If found servers, just return
|
||||
return Just(searchedServersFromAPI).setFailureType(to: Error.self).eraseToAnyPublisher()
|
||||
}
|
||||
// 2. No server found in the recorded list, check if searchText is a valid mastodon server domain
|
||||
if let toSearchText = searchText, !toSearchText.isEmpty {
|
||||
return self.context.apiService.instance(domain: toSearchText)
|
||||
.map { return [Mastodon.Entity.Server(instance: $0.value)] }.eraseToAnyPublisher()
|
||||
}
|
||||
return Just(searchedServersFromAPI).setFailureType(to: Error.self).eraseToAnyPublisher()
|
||||
}
|
||||
.sink { completion in
|
||||
print("1")
|
||||
} receiveValue: { [weak self] servers in
|
||||
self?.searchedServers.send(servers)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
||||
}
|
||||
|
||||
func fetchAllServers() {
|
||||
context.apiService.servers(language: nil, category: nil)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { error in
|
||||
print("11")
|
||||
} receiveValue: { [weak self] result in
|
||||
self?.allServers.send(result.value)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
}
|
||||
|
||||
private func searchServersFromAPI(category: Category, searchText: String?, allServers: [Mastodon.Entity.Server]) -> [Mastodon.Entity.Server] {
|
||||
return allServers
|
||||
// 1. Filter the category
|
||||
.filter {
|
||||
switch category {
|
||||
case .All:
|
||||
return true
|
||||
case .Some(let masCategory):
|
||||
return $0.category.caseInsensitiveCompare(masCategory.category.rawValue) == .orderedSame
|
||||
}
|
||||
}
|
||||
// 2. Filter the searchText
|
||||
.filter {
|
||||
if let searchText = searchText {
|
||||
return $0.domain.contains(searchText)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerViewModel: UITableViewDelegate {
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
if section == 0 {
|
||||
let category = Section.allCases[section]
|
||||
switch category {
|
||||
case .title:
|
||||
return 20
|
||||
}
|
||||
else if section == 1 {
|
||||
case .categories:
|
||||
// Since category view has a blur shadow effect, its height need to be large than the actual height,
|
||||
// Thus we reduce the section header's height by 10, and make the category cell height 60+20(10 inset for top and bottom)
|
||||
return 10
|
||||
}
|
||||
else {
|
||||
case .search:
|
||||
// Same reason as above
|
||||
return 10
|
||||
case .serverList:
|
||||
// Header with 1 height as the separator
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -121,7 +196,8 @@ extension PickServerViewModel: UITableViewDataSource {
|
|||
let section = Self.Section.allCases[section]
|
||||
switch section {
|
||||
case .title,
|
||||
.categories:
|
||||
.categories,
|
||||
.search:
|
||||
return 1
|
||||
case .serverList:
|
||||
return searchedServers.value.count
|
||||
|
@ -140,8 +216,15 @@ extension PickServerViewModel: UITableViewDataSource {
|
|||
cell.dataSource = self
|
||||
cell.delegate = self
|
||||
return cell
|
||||
case .search:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerSearchCell.self), for: indexPath) as! PickServerSearchCell
|
||||
cell.delegate = self
|
||||
return cell
|
||||
case .serverList:
|
||||
return UITableViewCell(style: .default, reuseIdentifier: "1")
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerCell.self), for: indexPath) as! PickServerCell
|
||||
cell.server = searchedServers.value[indexPath.row]
|
||||
cell.delegate = self
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -163,3 +246,17 @@ extension PickServerViewModel: PickServerCategoriesDataSource, PickServerCategor
|
|||
selectCategoryIndex.send(index)
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerViewModel: PickServerSearchCellDelegate {
|
||||
func pickServerSearchCell(didChange searchText: String?) {
|
||||
self.searchText.send(searchText)
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerViewModel: PickServerCellDelegate {
|
||||
func pickServerCell(modeChange updates: (() -> Void)) {
|
||||
tableView?.beginUpdates()
|
||||
tableView?.performBatchUpdates(updates, completion: nil)
|
||||
tableView?.endUpdates()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,322 @@
|
|||
//
|
||||
// PickServerCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by BradGao on 2021/2/24.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
import Kingfisher
|
||||
|
||||
protocol PickServerCellDelegate: class {
|
||||
func pickServerCell(modeChange updates: (() -> Void))
|
||||
}
|
||||
|
||||
class PickServerCell: UITableViewCell {
|
||||
|
||||
weak var delegate: PickServerCellDelegate?
|
||||
|
||||
enum Mode {
|
||||
case collapse
|
||||
case expand
|
||||
}
|
||||
|
||||
private var bgView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Colors.lightWhite.color
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
private var domainLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .preferredFont(forTextStyle: .headline)
|
||||
label.textColor = Asset.Colors.lightDarkGray.color
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
private var checkbox: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private var descriptionLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .preferredFont(forTextStyle: .subheadline)
|
||||
label.numberOfLines = 0
|
||||
label.textColor = Asset.Colors.lightDarkGray.color
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
private var thumbImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.clipsToBounds = true
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private var infoStackView: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.alignment = .fill
|
||||
stackView.distribution = .fillEqually
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
return stackView
|
||||
}()
|
||||
|
||||
private var expandBox: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
private var expandButton: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
button.setTitle(L10n.Scene.ServerPicker.Button.seeMore, for: .normal)
|
||||
button.setTitle(L10n.Scene.ServerPicker.Button.seeLess, for: .selected)
|
||||
button.setTitleColor(Asset.Colors.lightBrandBlue.color, for: .normal)
|
||||
button.titleLabel?.font = .preferredFont(forTextStyle: .footnote)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
return button
|
||||
}()
|
||||
|
||||
private var seperator: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Colors.lightBackground.color
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
private var langValueLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.lightDarkGray.color
|
||||
label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold))
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
private var usersValueLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.lightDarkGray.color
|
||||
label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold))
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
private var categoryValueLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.lightDarkGray.color
|
||||
label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold))
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
private var langTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.lightDarkGray.color
|
||||
label.font = .preferredFont(forTextStyle: .caption2)
|
||||
label.text = L10n.Scene.ServerPicker.Label.language
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
private var usersTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.lightDarkGray.color
|
||||
label.font = .preferredFont(forTextStyle: .caption2)
|
||||
label.text = L10n.Scene.ServerPicker.Label.users
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
private var categoryTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.lightDarkGray.color
|
||||
label.font = .preferredFont(forTextStyle: .caption2)
|
||||
label.text = L10n.Scene.ServerPicker.Label.category
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
private var collapseConstraints: [NSLayoutConstraint] = []
|
||||
private var expandConstraints: [NSLayoutConstraint] = []
|
||||
|
||||
var mode: PickServerCell.Mode = .collapse {
|
||||
didSet {
|
||||
updateMode()
|
||||
}
|
||||
}
|
||||
|
||||
var server: Mastodon.Entity.Server? {
|
||||
didSet {
|
||||
updateServerInfo()
|
||||
}
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Methods to configure appearance
|
||||
extension PickServerCell {
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
contentView.addSubview(bgView)
|
||||
contentView.addSubview(domainLabel)
|
||||
contentView.addSubview(checkbox)
|
||||
contentView.addSubview(descriptionLabel)
|
||||
contentView.addSubview(seperator)
|
||||
|
||||
contentView.addSubview(expandButton)
|
||||
|
||||
// Always add the expandbox which contains elements only visible in expand mode
|
||||
contentView.addSubview(expandBox)
|
||||
expandBox.addSubview(thumbImageView)
|
||||
expandBox.addSubview(infoStackView)
|
||||
expandBox.isHidden = true
|
||||
|
||||
let verticalInfoStackViewLang = makeVerticalInfoStackView(arrangedView: langValueLabel, langTitleLabel)
|
||||
let verticalInfoStackViewUsers = makeVerticalInfoStackView(arrangedView: usersValueLabel, usersTitleLabel)
|
||||
let verticalInfoStackViewCategory = makeVerticalInfoStackView(arrangedView: categoryValueLabel, categoryTitleLabel)
|
||||
infoStackView.addArrangedSubview(verticalInfoStackViewLang)
|
||||
infoStackView.addArrangedSubview(verticalInfoStackViewUsers)
|
||||
infoStackView.addArrangedSubview(verticalInfoStackViewCategory)
|
||||
|
||||
let expandButtonTopConstraintInCollapse = expandButton.topAnchor.constraint(equalTo: descriptionLabel.lastBaselineAnchor, constant: 12)
|
||||
collapseConstraints.append(expandButtonTopConstraintInCollapse)
|
||||
|
||||
let expandButtonTopConstraintInExpand = expandButton.topAnchor.constraint(equalTo: expandBox.bottomAnchor, constant: 8)
|
||||
expandConstraints.append(expandButtonTopConstraintInExpand)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
// Set background view
|
||||
bgView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
bgView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: bgView.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: bgView.bottomAnchor, constant: 1),
|
||||
|
||||
// Set bottom separator
|
||||
seperator.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: seperator.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: seperator.bottomAnchor),
|
||||
seperator.heightAnchor.constraint(equalToConstant: 1),
|
||||
|
||||
domainLabel.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 16),
|
||||
domainLabel.topAnchor.constraint(equalTo: bgView.topAnchor, constant: 16),
|
||||
|
||||
checkbox.widthAnchor.constraint(equalToConstant: 23),
|
||||
checkbox.heightAnchor.constraint(equalToConstant: 22),
|
||||
bgView.trailingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: 16),
|
||||
checkbox.leadingAnchor.constraint(equalTo: domainLabel.trailingAnchor, constant: 16),
|
||||
|
||||
descriptionLabel.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 16),
|
||||
descriptionLabel.topAnchor.constraint(equalTo: domainLabel.firstBaselineAnchor, constant: 8),
|
||||
bgView.trailingAnchor.constraint(equalTo: descriptionLabel.trailingAnchor, constant: 16),
|
||||
|
||||
// Set expandBox constraints
|
||||
expandBox.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 16),
|
||||
bgView.trailingAnchor.constraint(equalTo: expandBox.trailingAnchor, constant: 16),
|
||||
expandBox.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 8),
|
||||
expandBox.bottomAnchor.constraint(equalTo: infoStackView.bottomAnchor),
|
||||
|
||||
thumbImageView.leadingAnchor.constraint(equalTo: expandBox.leadingAnchor),
|
||||
expandBox.trailingAnchor.constraint(equalTo: thumbImageView.trailingAnchor),
|
||||
thumbImageView.topAnchor.constraint(equalTo: expandBox.topAnchor),
|
||||
thumbImageView.heightAnchor.constraint(equalTo: thumbImageView.widthAnchor, multiplier: 151.0 / 303.0),
|
||||
|
||||
infoStackView.leadingAnchor.constraint(equalTo: expandBox.leadingAnchor),
|
||||
expandBox.trailingAnchor.constraint(equalTo: infoStackView.trailingAnchor),
|
||||
infoStackView.topAnchor.constraint(equalTo: thumbImageView.bottomAnchor, constant: 16),
|
||||
|
||||
expandButton.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 16),
|
||||
bgView.trailingAnchor.constraint(equalTo: expandButton.trailingAnchor, constant: 16),
|
||||
bgView.bottomAnchor.constraint(equalTo: expandButton.bottomAnchor, constant: 8),
|
||||
])
|
||||
|
||||
NSLayoutConstraint.activate(collapseConstraints)
|
||||
|
||||
expandButton.addTarget(self, action: #selector(expandButtonDidClicked(_:)), for: .touchUpInside)
|
||||
|
||||
}
|
||||
|
||||
private func makeVerticalInfoStackView(arrangedView: UIView...) -> UIStackView {
|
||||
let stackView = UIStackView()
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .center
|
||||
stackView.distribution = .equalCentering
|
||||
stackView.spacing = 2
|
||||
arrangedView.forEach { stackView.addArrangedSubview($0) }
|
||||
return stackView
|
||||
}
|
||||
|
||||
private func updateMode() {
|
||||
switch mode {
|
||||
case .collapse:
|
||||
expandBox.isHidden = true
|
||||
NSLayoutConstraint.deactivate(expandConstraints)
|
||||
NSLayoutConstraint.activate(collapseConstraints)
|
||||
case .expand:
|
||||
expandBox.isHidden = false
|
||||
NSLayoutConstraint.activate(expandConstraints)
|
||||
NSLayoutConstraint.deactivate(collapseConstraints)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func expandButtonDidClicked(_ sender: UIButton) {
|
||||
delegate?.pickServerCell(modeChange: {
|
||||
let newMode: Mode = mode == .collapse ? .expand : .collapse
|
||||
self.mode = newMode
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Methods to update data
|
||||
extension PickServerCell {
|
||||
private func updateServerInfo() {
|
||||
guard let serverInfo = server else { return }
|
||||
domainLabel.text = serverInfo.domain
|
||||
descriptionLabel.text = serverInfo.description
|
||||
let processor = RoundCornerImageProcessor(cornerRadius: 3)
|
||||
thumbImageView.kf.indicatorType = .activity
|
||||
thumbImageView.kf.setImage(with: URL(string: serverInfo.proxiedThumbnail ?? "")!, placeholder: UIImage.placeholder(color: .yellow), options: [
|
||||
.processor(processor),
|
||||
.scaleFactor(UIScreen.main.scale),
|
||||
.transition(.fade(1))
|
||||
])
|
||||
langValueLabel.text = serverInfo.language.uppercased()
|
||||
usersValueLabel.text = "\(serverInfo.totalUsers)"
|
||||
categoryValueLabel.text = serverInfo.category.uppercased()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
//
|
||||
// PickServerSearchCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by BradGao on 2021/2/24.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol PickServerSearchCellDelegate: class {
|
||||
func pickServerSearchCell(didChange searchText: String?)
|
||||
}
|
||||
|
||||
class PickServerSearchCell: UITableViewCell {
|
||||
|
||||
weak var delegate: PickServerSearchCellDelegate?
|
||||
|
||||
private var bgView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Colors.lightWhite.color
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.layer.maskedCorners = [
|
||||
.layerMinXMinYCorner,
|
||||
.layerMaxXMinYCorner
|
||||
]
|
||||
view.layer.cornerCurve = .continuous
|
||||
view.layer.cornerRadius = 10
|
||||
return view
|
||||
}()
|
||||
|
||||
private var textFieldBgView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Colors.lightBackground.color.withAlphaComponent(0.6)
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.layer.masksToBounds = true
|
||||
view.layer.cornerRadius = 6
|
||||
view.layer.cornerCurve = .continuous
|
||||
return view
|
||||
}()
|
||||
|
||||
private var searchTextField: UITextField = {
|
||||
let textField = UITextField()
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
textField.font = .preferredFont(forTextStyle: .headline)
|
||||
textField.tintColor = Asset.Colors.lightDarkGray.color
|
||||
textField.textColor = Asset.Colors.lightDarkGray.color
|
||||
textField.adjustsFontForContentSizeCategory = true
|
||||
textField.attributedPlaceholder =
|
||||
NSAttributedString(string: L10n.Scene.ServerPicker.Input.placeholder,
|
||||
attributes: [.font: UIFont.preferredFont(forTextStyle: .headline),
|
||||
.foregroundColor: Asset.Colors.lightSecondaryText.color.withAlphaComponent(0.6)])
|
||||
textField.clearButtonMode = .whileEditing
|
||||
return textField
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerSearchCell {
|
||||
private func _init() {
|
||||
self.selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
searchTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
|
||||
|
||||
contentView.addSubview(bgView)
|
||||
contentView.addSubview(textFieldBgView)
|
||||
contentView.addSubview(searchTextField)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
bgView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
bgView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
bgView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
bgView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
|
||||
textFieldBgView.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 14),
|
||||
textFieldBgView.topAnchor.constraint(equalTo: bgView.topAnchor, constant: 12),
|
||||
bgView.trailingAnchor.constraint(equalTo: textFieldBgView.trailingAnchor, constant: 14),
|
||||
bgView.bottomAnchor.constraint(equalTo: textFieldBgView.bottomAnchor, constant: 13),
|
||||
|
||||
searchTextField.leadingAnchor.constraint(equalTo: textFieldBgView.leadingAnchor, constant: 11),
|
||||
searchTextField.topAnchor.constraint(equalTo: textFieldBgView.topAnchor, constant: 4),
|
||||
textFieldBgView.trailingAnchor.constraint(equalTo: searchTextField.trailingAnchor, constant: 11),
|
||||
textFieldBgView.bottomAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: 4),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerSearchCell {
|
||||
@objc func textFieldDidChange(_ textField: UITextField) {
|
||||
delegate?.pickServerSearchCell(didChange: textField.text)
|
||||
}
|
||||
}
|
|
@ -54,20 +54,12 @@ class PickServerCategoryView: UIView {
|
|||
|
||||
extension PickServerCategoryView {
|
||||
private func configure() {
|
||||
// bgShadowView.backgroundColor = nil
|
||||
// addSubview(bgShadowView)
|
||||
// bgShadowView.addSubview(bgView)
|
||||
addSubview(bgView)
|
||||
addSubview(titleLabel)
|
||||
|
||||
bgView.backgroundColor = .white
|
||||
bgView.backgroundColor = Asset.Colors.lightWhite.color
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
// bgShadowView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
||||
// bgShadowView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
||||
// bgShadowView.topAnchor.constraint(equalTo: self.topAnchor),
|
||||
// bgShadowView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
||||
|
||||
bgView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
||||
bgView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
||||
bgView.topAnchor.constraint(equalTo: self.topAnchor),
|
||||
|
@ -94,10 +86,10 @@ extension PickServerCategoryView {
|
|||
bgView.backgroundColor = Asset.Colors.lightBrandBlue.color
|
||||
bgView.applyShadow(color: Asset.Colors.lightBrandBlue.color, alpha: 1, x: 0, y: 0, blur: 4.0)
|
||||
if case .All = category {
|
||||
titleLabel.textColor = .white
|
||||
titleLabel.textColor = Asset.Colors.lightWhite.color
|
||||
}
|
||||
} else {
|
||||
bgView.backgroundColor = .white
|
||||
bgView.backgroundColor = Asset.Colors.lightWhite.color
|
||||
bgView.applyShadow(color: Asset.Colors.lightBrandBlue.color, alpha: 0, x: 0, y: 0, blur: 0.0)
|
||||
if case .All = category {
|
||||
titleLabel.textColor = Asset.Colors.lightBackground.color
|
||||
|
|
|
@ -37,6 +37,21 @@ extension Mastodon.Entity {
|
|||
case language
|
||||
case category
|
||||
}
|
||||
|
||||
public init(instance: Instance) {
|
||||
self.domain = instance.title
|
||||
self.version = "\(instance.version)"
|
||||
self.description = instance.description
|
||||
self.language = instance.languages?.first ?? ""
|
||||
self.languages = instance.languages ?? []
|
||||
self.region = "Unknown" // TODO: how to handle properties not in an instance
|
||||
self.categories = []
|
||||
self.category = "Unknown"
|
||||
self.proxiedThumbnail = instance.thumbnail
|
||||
self.totalUsers = instance.statistics?.userCount ?? 0
|
||||
self.lastWeekUsers = 0
|
||||
self.approvalRequired = instance.approvalRequired ?? false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue