diff --git a/Tusker/Screens/Main/MainSidebarMyProfileCollectionViewCell.swift b/Tusker/Screens/Main/MainSidebarMyProfileCollectionViewCell.swift index 8e9d66b0..48ed96df 100644 --- a/Tusker/Screens/Main/MainSidebarMyProfileCollectionViewCell.swift +++ b/Tusker/Screens/Main/MainSidebarMyProfileCollectionViewCell.swift @@ -11,14 +11,14 @@ import UserAccounts class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell { - private var verticalImageInset: CGFloat { + static var verticalImageInset: CGFloat { if UIDevice.current.userInterfaceIdiom == .mac { return (28 - avatarImageSize) / 2 } else { return (44 - avatarImageSize) / 2 } } - private var avatarImageSize: CGFloat { + static var avatarImageSize: CGFloat { if UIDevice.current.userInterfaceIdiom == .mac { return 20 } else { @@ -72,11 +72,11 @@ class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell { return } config.image = image - config.directionalLayoutMargins.top = self.verticalImageInset - config.directionalLayoutMargins.bottom = self.verticalImageInset - config.imageProperties.maximumSize = CGSize(width: self.avatarImageSize, height: self.avatarImageSize) + config.directionalLayoutMargins.top = MainSidebarMyProfileCollectionViewCell.verticalImageInset + config.directionalLayoutMargins.bottom = MainSidebarMyProfileCollectionViewCell.verticalImageInset + config.imageProperties.maximumSize = CGSize(width: MainSidebarMyProfileCollectionViewCell.avatarImageSize, height: MainSidebarMyProfileCollectionViewCell.avatarImageSize) config.imageProperties.reservedLayoutSize = CGSize(width: UIListContentConfiguration.ImageProperties.standardDimension, height: 0) - config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * self.avatarImageSize + config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * MainSidebarMyProfileCollectionViewCell.avatarImageSize self.contentConfiguration = config } } @@ -86,7 +86,7 @@ class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell { guard var config = self.contentConfiguration as? UIListContentConfiguration else { return } - config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * avatarImageSize + config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * MainSidebarMyProfileCollectionViewCell.avatarImageSize self.contentConfiguration = config } diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index 4726f44d..4b48d0cd 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -9,6 +9,7 @@ import UIKit import Combine import Pachyderm +import TuskerPreferences @available(iOS 18.0, *) class NewMainTabBarViewController: BaseMainTabBarViewController { @@ -52,7 +53,7 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { bookmarksTab.preferredPlacement = .optional favoritesTab = UITab(title: "Favorites", image: UIImage(systemName: "star"), identifier: Tab.favorites.rawValue, viewControllerProvider: viewControllerProvider) favoritesTab.preferredPlacement = .optional - myProfileTab = UITab(title: "My Profile", image: UIImage(systemName: "person"), identifier: Tab.myProfile.rawValue, viewControllerProvider: viewControllerProvider) + myProfileTab = MyProfileTab(mastodonController: mastodonController, viewControllerProvider: viewControllerProvider) listsGroup = UITabGroup(title: "Lists", image: nil, identifier: Tab.lists.rawValue, children: []) { _ in // this closure is necessary to prevent UIKit from crashing (FB14860961) @@ -362,6 +363,13 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate { } } +private var fastAccountSwitcherIndicator: UIView = { + let indicator = FastAccountSwitcherIndicatorView() + // need to explicitly set the frame to get it vertically centered + indicator.frame = CGRect(origin: .zero, size: indicator.intrinsicContentSize) + return indicator +}() + @available(iOS 18.0, *) extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { func tabBarController(_ tabBarController: UITabBarController, sidebarVisibilityWillChange sidebar: UITabBarController.Sidebar, animator: any UITabBarController.Sidebar.Animating) { @@ -375,16 +383,22 @@ extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { func tabBarController(_ tabBarController: UITabBarController, sidebar: UITabBarController.Sidebar, itemFor request: UITabSidebarItem.Request) -> UITabSidebarItem { let item = UITabSidebarItem(request: request) if case .tab(let tab) = request.content, - UIDevice.current.userInterfaceIdiom != .mac, - tab.identifier == Tab.myProfile.rawValue { - let indicator = FastAccountSwitcherIndicatorView() - // need to explicitly set the frame to get it vertically centered - indicator.frame = CGRect(origin: .zero, size: indicator.intrinsicContentSize) - item.accessories = [ - .customView(configuration: .init(customView: indicator, placement: .trailing())) - ] - item.contentConfiguration = MyProfileContentConfiguration(wrapped: item.contentConfiguration, view: $myProfileCell) { [unowned self] in - $0.addGestureRecognizer(self.fastAccountSwitcher.createSwitcherGesture()) + tab.identifier == Tab.myProfile.rawValue, + var config = item.contentConfiguration as? UIListContentConfiguration { + config.directionalLayoutMargins.top = MainSidebarMyProfileCollectionViewCell.verticalImageInset + config.directionalLayoutMargins.bottom = MainSidebarMyProfileCollectionViewCell.verticalImageInset + config.imageProperties.maximumSize = CGSize(width: MainSidebarMyProfileCollectionViewCell.avatarImageSize, height: MainSidebarMyProfileCollectionViewCell.avatarImageSize) + config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * MainSidebarMyProfileCollectionViewCell.avatarImageSize + + if UIDevice.current.userInterfaceIdiom != .mac { + item.accessories = [ + .customView(configuration: .init(customView: fastAccountSwitcherIndicator, placement: .trailing())) + ] + item.contentConfiguration = MyProfileContentConfiguration(wrapped: config, view: $myProfileCell) { [unowned self] in + $0.addGestureRecognizer(self.fastAccountSwitcher.createSwitcherGesture()) + } + } else { + item.contentConfiguration = config } } return item @@ -502,3 +516,70 @@ private struct MyProfileContentConfiguration: UIContentConfiguration { return .init(wrapped: wrapped.updated(for: state), view: $view, configureView: configureView) } } + +@available(iOS 18.0, *) +private class MyProfileTab: UITab { + private let mastodonController: MastodonController + private var avatarStyle: AvatarStyle? + + init(mastodonController: MastodonController, viewControllerProvider: @escaping (UITab) -> UIViewController) { + self.mastodonController = mastodonController + + // try to add the avatar image synchronously if possible + var avatarImage: UIImage? + if !Preferences.shared.grayscaleImages, + let account = mastodonController.account, + let avatarURL = account.avatar, + let avatar = ImageCache.avatars.get(avatarURL) { + avatarImage = Self.renderAvatar(avatar.image) + self.avatarStyle = Preferences.shared.avatarStyle + } + + let image = avatarImage ?? UIImage(systemName: "person")! + super.init(title: "My Profile", image: image, identifier: NewMainTabBarViewController.Tab.myProfile.rawValue, viewControllerProvider: viewControllerProvider) + + if avatarImage == nil { + Task { + await updateAvatar() + } + } + + NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) + } + + private func updateAvatar() async { + guard let account = try? await mastodonController.getOwnAccount(), + let avatarURL = account.avatar, + let image = await ImageCache.avatars.get(avatarURL).1 else { + return + } + + let maybeGrayscale = await ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) ?? image + let rendered = Self.renderAvatar(maybeGrayscale) + + self.avatarStyle = Preferences.shared.avatarStyle + self.image = rendered + } + + private static func renderAvatar(_ image: UIImage) -> UIImage { + let size = MainSidebarMyProfileCollectionViewCell.avatarImageSize + let radius = Preferences.shared.avatarStyle.cornerRadiusFraction * size + let rect = CGRect(x: 0, y: 0, width: size, height: size) + let renderer = UIGraphicsImageRenderer(bounds: rect) + let rendered = renderer.image { ctx in + UIBezierPath(roundedRect: rect, cornerRadius: radius).addClip() + image.draw(in: rect) + } + return rendered.withRenderingMode(.alwaysOriginal) + } + + @objc private func preferencesChanged() { + if avatarStyle != nil, + avatarStyle != Preferences.shared.avatarStyle { + Task { + await updateAvatar() + } + } + } + +}