Basic banner editing support
This commit is contained in:
parent
c2bb14eaab
commit
5d91bcf8b5
@ -61,6 +61,7 @@ final class ProfileHeaderViewController: UIViewController, NeedsDependency, Medi
|
|||||||
// private var isAdjustBannerImageViewForSafeAreaInset = false
|
// private var isAdjustBannerImageViewForSafeAreaInset = false
|
||||||
private var containerSafeAreaInset: UIEdgeInsets = .zero
|
private var containerSafeAreaInset: UIEdgeInsets = .zero
|
||||||
|
|
||||||
|
private var currentImageType = ImageType.avatar
|
||||||
private(set) lazy var imagePicker: PHPickerViewController = {
|
private(set) lazy var imagePicker: PHPickerViewController = {
|
||||||
var configuration = PHPickerConfiguration()
|
var configuration = PHPickerConfiguration()
|
||||||
configuration.filter = .images
|
configuration.filter = .images
|
||||||
@ -125,7 +126,9 @@ extension ProfileHeaderViewController {
|
|||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.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.editAvatarButtonOverlayIndicatorView.showsMenuAsPrimaryAction = true
|
||||||
profileHeaderView.delegate = self
|
profileHeaderView.delegate = self
|
||||||
|
|
||||||
@ -173,7 +176,7 @@ extension ProfileHeaderViewController {
|
|||||||
profileHeaderView.viewModel.viewDidAppear.send()
|
profileHeaderView.viewModel.viewDidAppear.send()
|
||||||
|
|
||||||
// set display after view appear
|
// set display after view appear
|
||||||
profileHeaderView.setupAvatarOverlayViews()
|
profileHeaderView.setupImageOverlayViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLayoutSubviews() {
|
override func viewDidLayoutSubviews() {
|
||||||
@ -185,11 +188,15 @@ extension ProfileHeaderViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileHeaderViewController {
|
extension ProfileHeaderViewController {
|
||||||
private func createAvatarContextMenu() -> UIMenu {
|
fileprivate enum ImageType {
|
||||||
|
case avatar
|
||||||
|
case banner
|
||||||
|
}
|
||||||
|
private func createImageContextMenu(_ type: ImageType) -> UIMenu {
|
||||||
var children: [UIMenuElement] = []
|
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
|
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 }
|
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)
|
self.present(self.imagePicker, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
children.append(photoLibraryAction)
|
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
|
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 }
|
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)
|
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)
|
self.present(self.imagePickerController, animated: true, completion: nil)
|
||||||
})
|
})
|
||||||
children.append(cameraAction)
|
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
|
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 }
|
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)
|
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)
|
self.present(self.documentPickerController, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
children.append(browseAction)
|
children.append(browseAction)
|
||||||
@ -443,7 +452,12 @@ extension ProfileHeaderViewController: UIDocumentPickerDelegate {
|
|||||||
// MARK: - CropViewControllerDelegate
|
// MARK: - CropViewControllerDelegate
|
||||||
extension ProfileHeaderViewController: CropViewControllerDelegate {
|
extension ProfileHeaderViewController: CropViewControllerDelegate {
|
||||||
public func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) {
|
public func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) {
|
||||||
|
switch currentImageType {
|
||||||
|
case .banner:
|
||||||
|
viewModel.profileInfoEditing.header = image
|
||||||
|
case .avatar:
|
||||||
viewModel.profileInfoEditing.avatar = image
|
viewModel.profileInfoEditing.avatar = image
|
||||||
|
}
|
||||||
cropViewController.dismiss(animated: true, completion: nil)
|
cropViewController.dismiss(animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,9 @@ final class ProfileHeaderViewModel {
|
|||||||
.sink { [weak self] account in
|
.sink { [weak self] account in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard let account = account else { return }
|
guard let account = account else { return }
|
||||||
|
// banner
|
||||||
|
self.profileInfo.header = nil
|
||||||
|
self.profileInfoEditing.header = nil
|
||||||
// avatar
|
// avatar
|
||||||
self.profileInfo.avatar = nil
|
self.profileInfo.avatar = nil
|
||||||
self.profileInfoEditing.avatar = nil
|
self.profileInfoEditing.avatar = nil
|
||||||
@ -72,6 +75,7 @@ final class ProfileHeaderViewModel {
|
|||||||
extension ProfileHeaderViewModel {
|
extension ProfileHeaderViewModel {
|
||||||
class ProfileInfo {
|
class ProfileInfo {
|
||||||
// input
|
// input
|
||||||
|
@Published var header: UIImage?
|
||||||
@Published var avatar: UIImage?
|
@Published var avatar: UIImage?
|
||||||
@Published var name: String?
|
@Published var name: String?
|
||||||
@Published var note: String?
|
@Published var note: String?
|
||||||
@ -99,6 +103,7 @@ extension ProfileHeaderViewModel: ProfileViewModelEditable {
|
|||||||
var isEdited: Bool {
|
var isEdited: Bool {
|
||||||
guard isEditing else { return false }
|
guard isEditing else { return false }
|
||||||
|
|
||||||
|
guard profileInfoEditing.header == nil else { return true }
|
||||||
guard profileInfoEditing.avatar == nil else { return true }
|
guard profileInfoEditing.avatar == nil else { return true }
|
||||||
guard profileInfo.name == profileInfoEditing.name else { return true }
|
guard profileInfo.name == profileInfoEditing.name else { return true }
|
||||||
guard profileInfo.note == profileInfoEditing.note else { return true }
|
guard profileInfo.note == profileInfoEditing.note else { return true }
|
||||||
|
@ -262,22 +262,29 @@ extension ProfileHeaderView {
|
|||||||
animator.addAnimations {
|
animator.addAnimations {
|
||||||
self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundNormalColor
|
self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundNormalColor
|
||||||
self.nameTextFieldBackgroundView.backgroundColor = .clear
|
self.nameTextFieldBackgroundView.backgroundColor = .clear
|
||||||
|
self.editBannerButton.alpha = 0
|
||||||
self.editAvatarBackgroundView.alpha = 0
|
self.editAvatarBackgroundView.alpha = 0
|
||||||
}
|
}
|
||||||
animator.addCompletion { _ in
|
animator.addCompletion { _ in
|
||||||
|
self.editBannerButton.isHidden = true
|
||||||
self.editAvatarBackgroundView.isHidden = true
|
self.editAvatarBackgroundView.isHidden = true
|
||||||
|
self.bannerImageViewSingleTapGestureRecognizer.isEnabled = true
|
||||||
}
|
}
|
||||||
case .editing:
|
case .editing:
|
||||||
nameMetaText.textView.alpha = 0
|
nameMetaText.textView.alpha = 0
|
||||||
nameTextField.isEnabled = true
|
nameTextField.isEnabled = true
|
||||||
nameTextField.alpha = 1
|
nameTextField.alpha = 1
|
||||||
|
|
||||||
|
editBannerButton.isHidden = false
|
||||||
|
editBannerButton.alpha = 0
|
||||||
editAvatarBackgroundView.isHidden = false
|
editAvatarBackgroundView.isHidden = false
|
||||||
editAvatarBackgroundView.alpha = 0
|
editAvatarBackgroundView.alpha = 0
|
||||||
bioMetaText.textView.backgroundColor = .clear
|
bioMetaText.textView.backgroundColor = .clear
|
||||||
|
bannerImageViewSingleTapGestureRecognizer.isEnabled = false
|
||||||
animator.addAnimations {
|
animator.addAnimations {
|
||||||
self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundEditingColor
|
self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundEditingColor
|
||||||
self.nameTextFieldBackgroundView.backgroundColor = Asset.Scene.Profile.Banner.nameEditBackgroundGray.color
|
self.nameTextFieldBackgroundView.backgroundColor = Asset.Scene.Profile.Banner.nameEditBackgroundGray.color
|
||||||
|
self.editBannerButton.alpha = 1
|
||||||
self.editAvatarBackgroundView.alpha = 1
|
self.editAvatarBackgroundView.alpha = 1
|
||||||
self.bioMetaText.textView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color
|
self.bioMetaText.textView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,7 @@ final class ProfileHeaderView: UIView {
|
|||||||
return viewModel
|
return viewModel
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
let bannerImageViewSingleTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||||
let bannerContainerView = UIView()
|
let bannerContainerView = UIView()
|
||||||
let bannerImageView: UIImageView = {
|
let bannerImageView: UIImageView = {
|
||||||
let imageView = UIImageView()
|
let imageView = UIImageView()
|
||||||
@ -101,7 +102,9 @@ final class ProfileHeaderView: UIView {
|
|||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
|
||||||
func setupAvatarOverlayViews() {
|
func setupImageOverlayViews() {
|
||||||
|
editBannerButton.tintColor = .white
|
||||||
|
|
||||||
editAvatarBackgroundView.backgroundColor = UIColor.black.withAlphaComponent(0.6)
|
editAvatarBackgroundView.backgroundColor = UIColor.black.withAlphaComponent(0.6)
|
||||||
editAvatarButtonOverlayIndicatorView.tintColor = .white
|
editAvatarButtonOverlayIndicatorView.tintColor = .white
|
||||||
}
|
}
|
||||||
@ -113,6 +116,13 @@ final class ProfileHeaderView: UIView {
|
|||||||
return visualEffectView
|
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 editAvatarBackgroundView: UIView = {
|
||||||
let view = UIView()
|
let view = UIView()
|
||||||
view.backgroundColor = .clear // set value after view appeared
|
view.backgroundColor = .clear // set value after view appeared
|
||||||
@ -272,6 +282,16 @@ extension ProfileHeaderView {
|
|||||||
bannerImageViewOverlayVisualEffectView.bottomAnchor.constraint(equalTo: bannerImageView.bottomAnchor),
|
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
|
// follows you
|
||||||
followsYouBlurEffectView.translatesAutoresizingMaskIntoConstraints = false
|
followsYouBlurEffectView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
addSubview(followsYouBlurEffectView)
|
addSubview(followsYouBlurEffectView)
|
||||||
@ -456,7 +476,6 @@ extension ProfileHeaderView {
|
|||||||
bioMetaText.textView.delegate = self
|
bioMetaText.textView.delegate = self
|
||||||
bioMetaText.textView.linkDelegate = self
|
bioMetaText.textView.linkDelegate = self
|
||||||
|
|
||||||
let bannerImageViewSingleTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
|
||||||
bannerImageView.addGestureRecognizer(bannerImageViewSingleTapGestureRecognizer)
|
bannerImageView.addGestureRecognizer(bannerImageViewSingleTapGestureRecognizer)
|
||||||
bannerImageViewSingleTapGestureRecognizer.addTarget(self, action: #selector(ProfileHeaderView.bannerImageViewDidPressed(_:)))
|
bannerImageViewSingleTapGestureRecognizer.addTarget(self, action: #selector(ProfileHeaderView.bannerImageViewDidPressed(_:)))
|
||||||
|
|
||||||
|
@ -216,7 +216,10 @@ extension ProfileViewModel {
|
|||||||
let domain = authenticationBox.domain
|
let domain = authenticationBox.domain
|
||||||
let authorization = authenticationBox.userAuthorization
|
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 let image = headerProfileInfo.avatar else { return nil }
|
||||||
guard image.size.width <= ProfileHeaderViewModel.avatarImageMaxSizeInPixel.width else {
|
guard image.size.width <= ProfileHeaderViewModel.avatarImageMaxSizeInPixel.width else {
|
||||||
return image.af.imageScaled(to: ProfileHeaderViewModel.avatarImageMaxSizeInPixel)
|
return image.af.imageScaled(to: ProfileHeaderViewModel.avatarImageMaxSizeInPixel)
|
||||||
@ -233,8 +236,8 @@ extension ProfileViewModel {
|
|||||||
bot: nil,
|
bot: nil,
|
||||||
displayName: headerProfileInfo.name,
|
displayName: headerProfileInfo.name,
|
||||||
note: headerProfileInfo.note,
|
note: headerProfileInfo.note,
|
||||||
avatar: _image.flatMap { Mastodon.Query.MediaAttachment.png($0.pngData()) },
|
avatar: _avatar.flatMap { Mastodon.Query.MediaAttachment.png($0.pngData()) },
|
||||||
header: nil,
|
header: _header.flatMap { Mastodon.Query.MediaAttachment.png($0.pngData()) },
|
||||||
locked: nil,
|
locked: nil,
|
||||||
source: nil,
|
source: nil,
|
||||||
fieldsAttributes: fieldsAttributes
|
fieldsAttributes: fieldsAttributes
|
||||||
|
Loading…
x
Reference in New Issue
Block a user