diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index c5dbbecd4..26ecbb4db 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -60,7 +60,8 @@ final class ProfileHeaderViewController: UIViewController, NeedsDependency, Medi // private var isAdjustBannerImageViewForSafeAreaInset = false private var containerSafeAreaInset: UIEdgeInsets = .zero - + + private var currentImageType = ImageType.avatar private(set) lazy var imagePicker: PHPickerViewController = { var configuration = PHPickerConfiguration() configuration.filter = .images @@ -125,7 +126,9 @@ extension ProfileHeaderViewController { } .store(in: &disposeBag) - profileHeaderView.editAvatarButtonOverlayIndicatorView.menu = createAvatarContextMenu() + profileHeaderView.editBannerButton.menu = createImageContextMenu(.banner) + profileHeaderView.editBannerButton.showsMenuAsPrimaryAction = true + profileHeaderView.editAvatarButtonOverlayIndicatorView.menu = createImageContextMenu(.avatar) profileHeaderView.editAvatarButtonOverlayIndicatorView.showsMenuAsPrimaryAction = true profileHeaderView.delegate = self @@ -173,7 +176,7 @@ extension ProfileHeaderViewController { profileHeaderView.viewModel.viewDidAppear.send() // set display after view appear - profileHeaderView.setupAvatarOverlayViews() + profileHeaderView.setupImageOverlayViews() } override func viewDidLayoutSubviews() { @@ -185,11 +188,15 @@ extension ProfileHeaderViewController { } extension ProfileHeaderViewController { - private func createAvatarContextMenu() -> UIMenu { + fileprivate enum ImageType { + case avatar + case banner + } + private func createImageContextMenu(_ type: ImageType) -> UIMenu { var children: [UIMenuElement] = [] let photoLibraryAction = UIAction(title: L10n.Scene.Compose.MediaSelection.photoLibrary, image: UIImage(systemName: "rectangle.on.rectangle"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in guard let self = self else { return } - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .photoLibaray", ((#file as NSString).lastPathComponent), #line, #function) + self.currentImageType = type self.present(self.imagePicker, animated: true, completion: nil) } children.append(photoLibraryAction) @@ -197,6 +204,7 @@ extension ProfileHeaderViewController { let cameraAction = UIAction(title: L10n.Scene.Compose.MediaSelection.camera, image: UIImage(systemName: "camera"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] _ in guard let self = self else { return } os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .camera", ((#file as NSString).lastPathComponent), #line, #function) + self.currentImageType = type self.present(self.imagePickerController, animated: true, completion: nil) }) children.append(cameraAction) @@ -204,6 +212,7 @@ extension ProfileHeaderViewController { let browseAction = UIAction(title: L10n.Scene.Compose.MediaSelection.browse, image: UIImage(systemName: "ellipsis"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in guard let self = self else { return } os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .browse", ((#file as NSString).lastPathComponent), #line, #function) + self.currentImageType = type self.present(self.documentPickerController, animated: true, completion: nil) } children.append(browseAction) @@ -443,7 +452,12 @@ extension ProfileHeaderViewController: UIDocumentPickerDelegate { // MARK: - CropViewControllerDelegate extension ProfileHeaderViewController: CropViewControllerDelegate { public func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) { - viewModel.profileInfoEditing.avatar = image + switch currentImageType { + case .banner: + viewModel.profileInfoEditing.header = image + case .avatar: + viewModel.profileInfoEditing.avatar = image + } cropViewController.dismiss(animated: true, completion: nil) } } diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift index 65f15efa7..2504153e2 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift @@ -52,6 +52,9 @@ final class ProfileHeaderViewModel { .sink { [weak self] account in guard let self = self else { return } guard let account = account else { return } + // banner + self.profileInfo.header = nil + self.profileInfoEditing.header = nil // avatar self.profileInfo.avatar = nil self.profileInfoEditing.avatar = nil @@ -72,6 +75,7 @@ final class ProfileHeaderViewModel { extension ProfileHeaderViewModel { class ProfileInfo { // input + @Published var header: UIImage? @Published var avatar: UIImage? @Published var name: String? @Published var note: String? @@ -99,6 +103,7 @@ extension ProfileHeaderViewModel: ProfileViewModelEditable { var isEdited: Bool { guard isEditing else { return false } + guard profileInfoEditing.header == nil else { return true } guard profileInfoEditing.avatar == nil else { return true } guard profileInfo.name == profileInfoEditing.name else { return true } guard profileInfo.note == profileInfoEditing.note else { return true } diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index c51ccfab3..ceb291bac 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -262,22 +262,29 @@ extension ProfileHeaderView { animator.addAnimations { self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundNormalColor self.nameTextFieldBackgroundView.backgroundColor = .clear + self.editBannerButton.alpha = 0 self.editAvatarBackgroundView.alpha = 0 } animator.addCompletion { _ in + self.editBannerButton.isHidden = true self.editAvatarBackgroundView.isHidden = true + self.bannerImageViewSingleTapGestureRecognizer.isEnabled = true } case .editing: nameMetaText.textView.alpha = 0 nameTextField.isEnabled = true nameTextField.alpha = 1 + editBannerButton.isHidden = false + editBannerButton.alpha = 0 editAvatarBackgroundView.isHidden = false editAvatarBackgroundView.alpha = 0 bioMetaText.textView.backgroundColor = .clear + bannerImageViewSingleTapGestureRecognizer.isEnabled = false animator.addAnimations { self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundEditingColor self.nameTextFieldBackgroundView.backgroundColor = Asset.Scene.Profile.Banner.nameEditBackgroundGray.color + self.editBannerButton.alpha = 1 self.editAvatarBackgroundView.alpha = 1 self.bioMetaText.textView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color } diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index a2194758b..a9941453b 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -50,6 +50,7 @@ final class ProfileHeaderView: UIView { return viewModel }() + let bannerImageViewSingleTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer let bannerContainerView = UIView() let bannerImageView: UIImageView = { let imageView = UIImageView() @@ -101,7 +102,9 @@ final class ProfileHeaderView: UIView { return button }() - func setupAvatarOverlayViews() { + func setupImageOverlayViews() { + editBannerButton.tintColor = .white + editAvatarBackgroundView.backgroundColor = UIColor.black.withAlphaComponent(0.6) editAvatarButtonOverlayIndicatorView.tintColor = .white } @@ -113,6 +116,13 @@ final class ProfileHeaderView: UIView { return visualEffectView }() + let editBannerButton: HighlightDimmableButton = { + let button = HighlightDimmableButton() + button.setImage(UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(pointSize: 28)), for: .normal) + button.tintColor = .clear + return button + }() + let editAvatarBackgroundView: UIView = { let view = UIView() view.backgroundColor = .clear // set value after view appeared @@ -271,7 +281,17 @@ extension ProfileHeaderView { bannerImageViewOverlayVisualEffectView.trailingAnchor.constraint(equalTo: bannerImageView.trailingAnchor), bannerImageViewOverlayVisualEffectView.bottomAnchor.constraint(equalTo: bannerImageView.bottomAnchor), ]) - + + editBannerButton.translatesAutoresizingMaskIntoConstraints = false + bannerContainerView.addSubview(editBannerButton) + NSLayoutConstraint.activate([ + editBannerButton.topAnchor.constraint(equalTo: bannerImageView.topAnchor), + editBannerButton.leadingAnchor.constraint(equalTo: bannerImageView.leadingAnchor), + editBannerButton.trailingAnchor.constraint(equalTo: bannerImageView.trailingAnchor), + editBannerButton.bottomAnchor.constraint(equalTo: bannerImageView.bottomAnchor), + ]) + bannerContainerView.isUserInteractionEnabled = true + // follows you followsYouBlurEffectView.translatesAutoresizingMaskIntoConstraints = false addSubview(followsYouBlurEffectView) @@ -455,8 +475,7 @@ extension ProfileHeaderView { statusDashboardView.delegate = self bioMetaText.textView.delegate = self bioMetaText.textView.linkDelegate = self - - let bannerImageViewSingleTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer + bannerImageView.addGestureRecognizer(bannerImageViewSingleTapGestureRecognizer) bannerImageViewSingleTapGestureRecognizer.addTarget(self, action: #selector(ProfileHeaderView.bannerImageViewDidPressed(_:))) diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index e23b465d4..1ca1d4bea 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -215,8 +215,11 @@ extension ProfileViewModel { let authenticationBox = authContext.mastodonAuthenticationBox let domain = authenticationBox.domain let authorization = authenticationBox.userAuthorization - - let _image: UIImage? = { + + // TODO: constrain size? + let _header: UIImage? = headerProfileInfo.header + + let _avatar: UIImage? = { guard let image = headerProfileInfo.avatar else { return nil } guard image.size.width <= ProfileHeaderViewModel.avatarImageMaxSizeInPixel.width else { return image.af.imageScaled(to: ProfileHeaderViewModel.avatarImageMaxSizeInPixel) @@ -233,8 +236,8 @@ extension ProfileViewModel { bot: nil, displayName: headerProfileInfo.name, note: headerProfileInfo.note, - avatar: _image.flatMap { Mastodon.Query.MediaAttachment.png($0.pngData()) }, - header: nil, + avatar: _avatar.flatMap { Mastodon.Query.MediaAttachment.png($0.pngData()) }, + header: _header.flatMap { Mastodon.Query.MediaAttachment.png($0.pngData()) }, locked: nil, source: nil, fieldsAttributes: fieldsAttributes