From 46ebdd80596035e44ac82984a730f54953d040fd Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 14 Sep 2021 18:21:15 +0800 Subject: [PATCH] feat: update account list UI --- Localization/app.json | 5 +- Mastodon.xcodeproj/project.pbxproj | 43 ++++++++- .../xcschemes/xcschememanagement.plist | 8 +- .../xcshareddata/swiftpm/Package.resolved | 9 ++ Mastodon/Coordinator/SceneCoordinator.swift | 21 ++-- Mastodon/Extension/MetaLabel.swift | 8 ++ .../Account/AccountListTableViewCell.swift | 34 ------- .../Scene/Account/AccountListViewModel.swift | 36 ++++++- .../Scene/Account/AccountViewController.swift | 78 +++++++++++---- .../Cell/AccountListTableViewCell.swift | 95 +++++++++++++++++++ .../Cell/AddAccountTableViewCell.swift | 75 +++++++++++++++ .../Account/View/DragIndicatorView.swift | 55 +++++++++++ .../Scene/MainTab/MainTabBarController.swift | 38 ++++++++ README.md | 1 + 14 files changed, 438 insertions(+), 68 deletions(-) delete mode 100644 Mastodon/Scene/Account/AccountListTableViewCell.swift create mode 100644 Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift create mode 100644 Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift create mode 100644 Mastodon/Scene/Account/View/DragIndicatorView.swift diff --git a/Localization/app.json b/Localization/app.json index b60ade9c1..ae1593940 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -534,6 +534,9 @@ "show_next": "Show Next", "show_previous": "Show Previous" } + }, + "account_list": { + "add_account": "Add Account" } } -} \ No newline at end of file +} diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index fac9174ca..6e0980275 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -425,6 +425,9 @@ DBA465952696E387002B41DB /* AppPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA465942696E387002B41DB /* AppPreference.swift */; }; DBA4B0F626C269880077136E /* Intents.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = DBA4B0F926C269880077136E /* Intents.stringsdict */; }; DBA4B0F726C269880077136E /* Intents.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = DBA4B0F926C269880077136E /* Intents.stringsdict */; }; + DBA5A52F26F07ED800CACBAA /* PanModal in Frameworks */ = {isa = PBXBuildFile; productRef = DBA5A52E26F07ED800CACBAA /* PanModal */; }; + DBA5A53126F08EF000CACBAA /* DragIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5A53026F08EF000CACBAA /* DragIndicatorView.swift */; }; + DBA5A53526F0A36A00CACBAA /* AddAccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5A53426F0A36A00CACBAA /* AddAccountTableViewCell.swift */; }; DBA5E7A3263AD0A3004598BB /* PhotoLibraryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */; }; DBA5E7A5263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5E7A4263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift */; }; DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5E7A8263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift */; }; @@ -1225,6 +1228,8 @@ DBA4B0EF26C153B20077136E /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = ""; }; DBA4B0F526C2621D0077136E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Intents.strings; sourceTree = ""; }; DBA4B0F826C269880077136E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Intents.stringsdict; sourceTree = ""; }; + DBA5A53026F08EF000CACBAA /* DragIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DragIndicatorView.swift; sourceTree = ""; }; + DBA5A53426F0A36A00CACBAA /* AddAccountTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountTableViewCell.swift; sourceTree = ""; }; DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryService.swift; sourceTree = ""; }; DBA5E7A4263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuImagePreviewViewModel.swift; sourceTree = ""; }; DBA5E7A8263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuImagePreviewViewController.swift; sourceTree = ""; }; @@ -1387,6 +1392,7 @@ 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */, DBF7A0FC26830C33004176A2 /* FPSIndicator in Frameworks */, DB01E23526A98F0900C3965B /* MetaTextKit in Frameworks */, + DBA5A52F26F07ED800CACBAA /* PanModal in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2785,13 +2791,31 @@ DB9F58ED26EF435800E7BBE9 /* Account */ = { isa = PBXGroup; children = ( + DBA5A53226F08EF300CACBAA /* View */, + DBA5A53326F0932E00CACBAA /* Cell */, DB9F58EB26EF435000E7BBE9 /* AccountViewController.swift */, DB9F58EE26EF491E00E7BBE9 /* AccountListViewModel.swift */, - DB9F58F026EF512300E7BBE9 /* AccountListTableViewCell.swift */, ); path = Account; sourceTree = ""; }; + DBA5A53226F08EF300CACBAA /* View */ = { + isa = PBXGroup; + children = ( + DBA5A53026F08EF000CACBAA /* DragIndicatorView.swift */, + ); + path = View; + sourceTree = ""; + }; + DBA5A53326F0932E00CACBAA /* Cell */ = { + isa = PBXGroup; + children = ( + DB9F58F026EF512300E7BBE9 /* AccountListTableViewCell.swift */, + DBA5A53426F0A36A00CACBAA /* AddAccountTableViewCell.swift */, + ); + path = Cell; + sourceTree = ""; + }; DBA5E7A6263BD298004598BB /* ContextMenu */ = { isa = PBXGroup; children = ( @@ -3161,6 +3185,7 @@ DB01E23226A98F0900C3965B /* MastodonMeta */, DB01E23426A98F0900C3965B /* MetaTextKit */, DB552D4E26BBD10C00E481F6 /* OrderedCollections */, + DBA5A52E26F07ED800CACBAA /* PanModal */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -3420,6 +3445,7 @@ DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */, DB01E23126A98F0900C3965B /* XCRemoteSwiftPackageReference "MetaTextKit" */, DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */, + DBA5A52D26F07ED800CACBAA /* XCRemoteSwiftPackageReference "PanModal" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -3773,6 +3799,7 @@ 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */, DB6180EF26391CA50018D199 /* MediaPreviewImageViewController.swift in Sources */, DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */, + DBA5A53126F08EF000CACBAA /* DragIndicatorView.swift in Sources */, DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */, DB6180F626391D580018D199 /* MediaPreviewableViewController.swift in Sources */, 2D571B2F26004EC000540450 /* NavigationBarProgressView.swift in Sources */, @@ -4042,6 +4069,7 @@ DB6D9F6326357848008423CD /* SettingService.swift in Sources */, 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */, 2D24E12D2626FD2E00A59D4F /* NotificationViewModel+LoadOldestState.swift in Sources */, + DBA5A53526F0A36A00CACBAA /* AddAccountTableViewCell.swift in Sources */, 2DB72C8C262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift in Sources */, 2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */, DB084B5725CBC56C00F898ED /* Status.swift in Sources */, @@ -5845,6 +5873,14 @@ minimumVersion = 1.4.1; }; }; + DBA5A52D26F07ED800CACBAA /* XCRemoteSwiftPackageReference "PanModal" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/slackhq/PanModal.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.2.7; + }; + }; DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ra1028/DifferenceKit.git"; @@ -5957,6 +5993,11 @@ package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */; productName = "UITextView+Placeholder"; }; + DBA5A52E26F07ED800CACBAA /* PanModal */ = { + isa = XCSwiftPackageProductDependency; + package = DBA5A52D26F07ED800CACBAA /* XCRemoteSwiftPackageReference "PanModal" */; + productName = PanModal; + }; DBAC6482267D0B21007FE9FD /* DifferenceKit */ = { isa = XCSwiftPackageProductDependency; package = DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 62193f0d7..0db21afd2 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,12 +7,12 @@ AppShared.xcscheme_^#shared#^_ orderHint - 24 + 22 CoreDataStack.xcscheme_^#shared#^_ orderHint - 23 + 24 Mastodon - ASDK.xcscheme_^#shared#^_ @@ -97,7 +97,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 25 + 26 MastodonIntents.xcscheme_^#shared#^_ @@ -117,7 +117,7 @@ ShareActionExtension.xcscheme_^#shared#^_ orderHint - 22 + 23 SuppressBuildableAutocreation diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5d8511197..c6a659bd5 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -127,6 +127,15 @@ "version": "3.6.2" } }, + { + "package": "PanModal", + "repositoryURL": "https://github.com/slackhq/PanModal.git", + "state": { + "branch": null, + "revision": "b012aecb6b67a8e46369227f893c12544846613f", + "version": "1.2.7" + } + }, { "package": "SDWebImage", "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 43a51fcda..f604ef546 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -7,6 +7,7 @@ import UIKit import SafariServices import CoreDataStack +import PanModal final public class SceneCoordinator { @@ -31,6 +32,7 @@ extension SceneCoordinator { case show // push case showDetail // replace case modal(animated: Bool, completion: (() -> Void)? = nil) + case panModal case custom(transitioningDelegate: UIViewControllerTransitioningDelegate) case customPush case safariPresent(animated: Bool, completion: (() -> Void)? = nil) @@ -66,9 +68,10 @@ extension SceneCoordinator { case hashtagTimeline(viewModel: HashtagTimelineViewModel) // profile + case accountList case profile(viewModel: ProfileViewModel) case favorite(viewModel: FavoriteViewModel) - + // setting case settings(viewModel: SettingsViewModel) @@ -87,7 +90,6 @@ extension SceneCoordinator { case activityViewController(activityViewController: UIActivityViewController, sourceView: UIView?, barButtonItem: UIBarButtonItem?) #if DEBUG - case accountList case publicTimeline #endif @@ -184,7 +186,14 @@ extension SceneCoordinator { modalNavigationController.presentationController?.delegate = adaptivePresentationControllerDelegate } presentingViewController.present(modalNavigationController, animated: animated, completion: completion) - + + case .panModal: + guard let panModalPresentable = viewController as? PanModalPresentable & UIViewController else { + assertionFailure() + return nil + } + presentingViewController.presentPanModal(panModalPresentable) + case .custom(let transitioningDelegate): viewController.modalPresentationStyle = .custom viewController.transitioningDelegate = transitioningDelegate @@ -274,6 +283,9 @@ private extension SceneCoordinator { let _viewController = HashtagTimelineViewController() _viewController.viewModel = viewModel viewController = _viewController + case .accountList: + let _viewController = AccountListViewController() + viewController = _viewController case .profile(let viewModel): let _viewController = ProfileViewController() _viewController.viewModel = viewModel @@ -322,9 +334,6 @@ private extension SceneCoordinator { _viewController.viewModel = viewModel viewController = _viewController #if DEBUG - case .accountList: - let _viewController = AccountListViewController() - viewController = _viewController case .publicTimeline: let _viewController = PublicTimelineViewController() _viewController.viewModel = PublicTimelineViewModel(context: appContext) diff --git a/Mastodon/Extension/MetaLabel.swift b/Mastodon/Extension/MetaLabel.swift index 9e7920a8e..a9696892a 100644 --- a/Mastodon/Extension/MetaLabel.swift +++ b/Mastodon/Extension/MetaLabel.swift @@ -20,6 +20,8 @@ extension MetaLabel { case titleView case settingTableFooter case autoCompletion + case accountListName + case accountListUsername } convenience init(style: Style) { @@ -74,6 +76,12 @@ extension MetaLabel { case .autoCompletion: font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22) textColor = Asset.Colors.brandBlue.color + case .accountListName: + font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 22) + textColor = Asset.Colors.Label.primary.color + case .accountListUsername: + font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular), maximumPointSize: 20) + textColor = Asset.Colors.Label.secondary.color } self.font = font diff --git a/Mastodon/Scene/Account/AccountListTableViewCell.swift b/Mastodon/Scene/Account/AccountListTableViewCell.swift deleted file mode 100644 index c98e94ec1..000000000 --- a/Mastodon/Scene/Account/AccountListTableViewCell.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// AccountListTableViewCell.swift -// Mastodon -// -// Created by Cirno MainasuK on 2021-9-13. -// - - -#if DEBUG -import UIKit - -final class AccountListTableViewCell: UITableViewCell { - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension AccountListTableViewCell { - - private func _init() { - - } - -} - -#endif diff --git a/Mastodon/Scene/Account/AccountListViewModel.swift b/Mastodon/Scene/Account/AccountListViewModel.swift index 3f56134b0..6842806e2 100644 --- a/Mastodon/Scene/Account/AccountListViewModel.swift +++ b/Mastodon/Scene/Account/AccountListViewModel.swift @@ -5,11 +5,11 @@ // Created by Cirno MainasuK on 2021-9-13. // -#if DEBUG import UIKit import Combine import CoreData import CoreDataStack +import MastodonMeta final class AccountListViewModel { @@ -43,6 +43,7 @@ final class AccountListViewModel { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) snapshot.appendItems(authentications, toSection: .main) + snapshot.appendItems([.addAccount], toSection: .main) diffableDataSource.apply(snapshot) } @@ -58,6 +59,7 @@ extension AccountListViewModel { enum Item: Hashable { case authentication(objectID: NSManagedObjectID) + case addAccount } func setupDiffableDataSource( @@ -70,11 +72,37 @@ extension AccountListViewModel { let authentication = managedObjectContext.object(with: objectID) as! MastodonAuthentication let user = authentication.user let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AccountListTableViewCell.self), for: indexPath) as! AccountListTableViewCell - cell.textLabel?.text = user.acctWithDomain + AccountListViewModel.configure(cell: cell, user: user) + return cell + case .addAccount: + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AddAccountTableViewCell.self), for: indexPath) as! AddAccountTableViewCell return cell } } + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + diffableDataSource?.apply(snapshot) + } + + static func configure( + cell: AccountListTableViewCell, + user: MastodonUser + ) { + // avatar + cell.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: user.avatarImageURL())) + + // name + do { + let content = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojiMeta) + let metaContent = try MastodonMetaContent.convert(document: content) + cell.nameLabel.configure(content: metaContent) + } catch { + assertionFailure() + cell.nameLabel.configure(content: PlaintextMetaContent(string: user.displayNameWithFallback)) + } + + // username + cell.usernameLabel.configure(content: PlaintextMetaContent(string: user.acctWithDomain)) } } - -#endif diff --git a/Mastodon/Scene/Account/AccountViewController.swift b/Mastodon/Scene/Account/AccountViewController.swift index 65c41620d..28ee88c95 100644 --- a/Mastodon/Scene/Account/AccountViewController.swift +++ b/Mastodon/Scene/Account/AccountViewController.swift @@ -5,12 +5,11 @@ // Created by Cirno MainasuK on 2021-9-13. // -#if DEBUG - import os.log import UIKit import Combine import CoreDataStack +import PanModal final class AccountListViewController: UIViewController, NeedsDependency { @@ -32,25 +31,61 @@ final class AccountListViewController: UIViewController, NeedsDependency { return barButtonItem }() + let dragIndicatorView = DragIndicatorView() + private(set) lazy var tableView: UITableView = { let tableView = UITableView() tableView.register(AccountListTableViewCell.self, forCellReuseIdentifier: String(describing: AccountListTableViewCell.self)) + tableView.register(AddAccountTableViewCell.self, forCellReuseIdentifier: String(describing: AddAccountTableViewCell.self)) + tableView.backgroundColor = .clear + tableView.tableFooterView = UIView() + tableView.separatorStyle = .none return tableView }() } +// MARK: - PanModalPresentable +extension AccountListViewController: PanModalPresentable { + var panScrollable: UIScrollView? { tableView } + var showDragIndicator: Bool { false } + + var shortFormHeight: PanModalHeight { + return .contentHeight(300) + } + + var longFormHeight: PanModalHeight { + return .maxHeightWithTopInset(40) + } +} + extension AccountListViewController { override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .systemBackground + view.backgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor.withAlphaComponent(0.9) + ThemeService.shared.currentTheme + .receive(on: DispatchQueue.main) + .sink { [weak self] theme in + guard let self = self else { return } + self.setupBackgroundColor(theme: theme) + } + .store(in: &disposeBag) navigationItem.rightBarButtonItem = addBarButtonItem + dragIndicatorView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(dragIndicatorView) + NSLayoutConstraint.activate([ + dragIndicatorView.topAnchor.constraint(equalTo: view.topAnchor), + dragIndicatorView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + dragIndicatorView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + dragIndicatorView.heightAnchor.constraint(equalToConstant: DragIndicatorView.height).priority(.required - 1), + ]) + tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.topAnchor.constraint(equalTo: dragIndicatorView.bottomAnchor), tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), @@ -62,6 +97,11 @@ extension AccountListViewController { managedObjectContext: context.managedObjectContext ) } + + private func setupBackgroundColor(theme: Theme) { + view.backgroundColor = theme.systemBackgroundColor.withAlphaComponent(0.9) + } + } extension AccountListViewController { @@ -79,20 +119,22 @@ extension AccountListViewController: UITableViewDelegate { tableView.deselectRow(at: indexPath, animated: true) guard let diffableDataSource = viewModel.diffableDataSource else { return } - guard case let .authentication(objectID) = diffableDataSource.itemIdentifier(for: indexPath) else { return } - assert(Thread.isMainThread) - - let authentication = context.managedObjectContext.object(with: objectID) as! MastodonAuthentication - context.authenticationService.activeMastodonUser(domain: authentication.domain, userID: authentication.userID) - .receive(on: DispatchQueue.main) - .sink { [weak self] result in - guard let self = self else { return } - self.coordinator.setup() - } - .store(in: &disposeBag) + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + switch item { + case .authentication(let objectID): + assert(Thread.isMainThread) + let authentication = context.managedObjectContext.object(with: objectID) as! MastodonAuthentication + context.authenticationService.activeMastodonUser(domain: authentication.domain, userID: authentication.userID) + .receive(on: DispatchQueue.main) + .sink { [weak self] result in + guard let self = self else { return } + self.coordinator.setup() + } + .store(in: &disposeBag) + case .addAccount: + // TODO: add dismiss entry for welcome scene + coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil)) + } } } - - -#endif diff --git a/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift b/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift new file mode 100644 index 000000000..9125923a1 --- /dev/null +++ b/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift @@ -0,0 +1,95 @@ +// +// AccountListTableViewCell.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-9-13. +// + +import UIKit +import FLAnimatedImage +import MetaTextKit + +final class CircleAvatarButton: AvatarButton { + override func layoutSubviews() { + super.layoutSubviews() + + layer.masksToBounds = true + layer.cornerRadius = frame.width * 0.5 + layer.borderColor = UIColor.systemFill.cgColor + layer.borderWidth = 1 + } +} + +final class AccountListTableViewCell: UITableViewCell { + + let avatarButton = CircleAvatarButton() + let nameLabel = MetaLabel(style: .accountListName) + let usernameLabel = MetaLabel(style: .accountListUsername) + let separatorLine = UIView.separatorLine + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension AccountListTableViewCell { + + private func _init() { + avatarButton.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(avatarButton) + NSLayoutConstraint.activate([ + avatarButton.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + avatarButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + avatarButton.heightAnchor.constraint(equalTo: avatarButton.widthAnchor, multiplier: 1.0).priority(.required - 1), + avatarButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 30).priority(.required - 1), + ]) + avatarButton.setContentHuggingPriority(.defaultLow, for: .horizontal) + avatarButton.setContentHuggingPriority(.defaultLow, for: .vertical) + + let labelContainerStackView = UIStackView() + labelContainerStackView.axis = .vertical + labelContainerStackView.distribution = .equalCentering + labelContainerStackView.spacing = 2 + labelContainerStackView.distribution = .fillProportionally + labelContainerStackView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(labelContainerStackView) + NSLayoutConstraint.activate([ + labelContainerStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), + labelContainerStackView.leadingAnchor.constraint(equalTo: avatarButton.trailingAnchor, constant: 10), + contentView.bottomAnchor.constraint(equalTo: labelContainerStackView.bottomAnchor, constant: 10), + avatarButton.heightAnchor.constraint(equalTo: labelContainerStackView.heightAnchor, multiplier: 0.8).priority(.required - 10), + labelContainerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + ]) + + labelContainerStackView.addArrangedSubview(nameLabel) + labelContainerStackView.addArrangedSubview(usernameLabel) + + avatarButton.isUserInteractionEnabled = false + nameLabel.isUserInteractionEnabled = false + usernameLabel.isUserInteractionEnabled = false + + separatorLine.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(separatorLine) + NSLayoutConstraint.activate([ + separatorLine.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + separatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), + ]) + } + +} + +// MARK: - AvatarConfigurableView +extension AccountListTableViewCell: AvatarConfigurableView { + static var configurableAvatarImageSize: CGSize { CGSize(width: 30, height: 30) } + static var configurableAvatarImageCornerRadius: CGFloat { 0 } + var configurableAvatarImageView: FLAnimatedImageView? { avatarButton.avatarImageView } +} diff --git a/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift b/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift new file mode 100644 index 000000000..0ac022c5d --- /dev/null +++ b/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift @@ -0,0 +1,75 @@ +// +// AddAccountTableViewCell.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-9-14. +// + +import UIKit +import MetaTextKit + +final class AddAccountTableViewCell: UITableViewCell { + + let iconImageView: UIImageView = { + let image = UIImage(systemName: "plus.circle.fill")! + let imageView = UIImageView(image: image) + imageView.tintColor = Asset.Colors.Label.primary.color + return imageView + }() + let titleLabel: UILabel = { + let label = UILabel() + label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 22) + label.textColor = Asset.Colors.Label.primary.color + label.text = "Add Account" // TODO: i18n + return label + }() + let usernameLabel = MetaLabel(style: .accountListUsername) + let separatorLine = UIView.separatorLine + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension AddAccountTableViewCell { + + private func _init() { + iconImageView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(iconImageView) + NSLayoutConstraint.activate([ + iconImageView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + iconImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + iconImageView.heightAnchor.constraint(equalTo: iconImageView.widthAnchor, multiplier: 1.0).priority(.required - 1), + iconImageView.heightAnchor.constraint(greaterThanOrEqualToConstant: 30).priority(.required - 1), + ]) + iconImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) + iconImageView.setContentHuggingPriority(.defaultLow, for: .vertical) + + titleLabel.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(titleLabel) + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 19), + titleLabel.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 10), + contentView.bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 19), + iconImageView.heightAnchor.constraint(equalTo: titleLabel.heightAnchor, multiplier: 1.0).priority(.required - 10), + titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + ]) + + separatorLine.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(separatorLine) + NSLayoutConstraint.activate([ + separatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + separatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), + ]) + } + +} diff --git a/Mastodon/Scene/Account/View/DragIndicatorView.swift b/Mastodon/Scene/Account/View/DragIndicatorView.swift new file mode 100644 index 000000000..5efa141bc --- /dev/null +++ b/Mastodon/Scene/Account/View/DragIndicatorView.swift @@ -0,0 +1,55 @@ +// +// DragIndicatorView.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-9-14. +// + +import UIKit + +final class DragIndicatorView: UIView { + + static let height: CGFloat = 38 + + let barView = UIView() + let separatorLine = UIView.separatorLine + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension DragIndicatorView { + + private func _init() { + barView.backgroundColor = Asset.Colors.Label.secondary.color + barView.layer.masksToBounds = true + barView.layer.cornerRadius = 2.5 + + barView.translatesAutoresizingMaskIntoConstraints = false + addSubview(barView) + NSLayoutConstraint.activate([ + barView.centerXAnchor.constraint(equalTo: centerXAnchor), + barView.centerYAnchor.constraint(equalTo: centerYAnchor), + barView.heightAnchor.constraint(equalToConstant: 5).priority(.required - 1), + barView.widthAnchor.constraint(equalToConstant: 36).priority(.required - 1), + ]) + + separatorLine.translatesAutoresizingMaskIntoConstraints = false + addSubview(separatorLine) + NSLayoutConstraint.activate([ + separatorLine.leadingAnchor.constraint(equalTo: leadingAnchor), + separatorLine.trailingAnchor.constraint(equalTo: trailingAnchor), + separatorLine.bottomAnchor.constraint(equalTo: bottomAnchor), + separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)), + ]) + } + +} diff --git a/Mastodon/Scene/MainTab/MainTabBarController.swift b/Mastodon/Scene/MainTab/MainTabBarController.swift index 1f9bcb578..8e6edd2aa 100644 --- a/Mastodon/Scene/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/MainTab/MainTabBarController.swift @@ -11,6 +11,8 @@ import Combine import SafariServices class MainTabBarController: UITabBarController { + + let logger = Logger(subsystem: "MainTabBarController", category: "UI") var disposeBag = Set() @@ -24,6 +26,10 @@ class MainTabBarController: UITabBarController { case search case notification case me + + var tag: Int { + return rawValue + } var title: String { switch self { @@ -121,6 +127,7 @@ extension MainTabBarController { let tabs = Tab.allCases let viewControllers: [UIViewController] = tabs.map { tab in let viewController = tab.viewController(context: context, coordinator: coordinator) + viewController.tabBarItem.tag = tab.tag viewController.tabBarItem.title = tab.title viewController.tabBarItem.image = tab.image viewController.tabBarItem.accessibilityLabel = tab.title @@ -204,6 +211,10 @@ extension MainTabBarController { } .store(in: &disposeBag) + let tabBarLongPressGestureRecognizer = UILongPressGestureRecognizer() + tabBarLongPressGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarLongPressGestureRecognizerHandler(_:))) + tabBar.addGestureRecognizer(tabBarLongPressGestureRecognizer) + #if DEBUG // selectedIndex = 1 #endif @@ -211,6 +222,33 @@ extension MainTabBarController { } +extension MainTabBarController { + @objc private func tabBarLongPressGestureRecognizerHandler(_ sender: UILongPressGestureRecognizer) { + guard sender.state == .began else { return } + + var _tab: Tab? + let location = sender.location(in: tabBar) + for item in tabBar.items ?? [] { + guard let tab = Tab(rawValue: item.tag) else { continue } + guard let view = item.value(forKey: "view") as? UIView else { continue } + guard view.frame.contains(location) else { continue} + + _tab = tab + break + } + + guard let tab = _tab else { return } + logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): long press \(tab.title) tab") + + switch tab { + case .me: + coordinator.present(scene: .accountList, from: nil, transition: .panModal) + default: + break + } + } +} + extension MainTabBarController { var notificationViewController: NotificationViewController? { diff --git a/README.md b/README.md index e31c4879b..08c87e558 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ The app is compatible with [toot-relay](https://github.com/DagAgren/toot-relay) - [Nuke-FLAnimatedImage-Plugin](https://github.com/kean/Nuke-FLAnimatedImage-Plugin) - [Nuke](https://github.com/kean/Nuke) - [Pageboy](https://github.com/uias/Pageboy#the-basics) +- [PanModal](https://github.com/slackhq/PanModal.git) - [SDWebImage](https://github.com/SDWebImage/SDWebImage) - [swift-collections](https://github.com/apple/swift-collections) - [swift-nio](https://github.com/apple/swift-nio)