feature: Add context menu for avatar select for sign up page
This commit is contained in:
parent
b9d9233e80
commit
ca25d43f4f
|
@ -103,6 +103,9 @@
|
||||||
"register": {
|
"register": {
|
||||||
"title": "Tell us about you.",
|
"title": "Tell us about you.",
|
||||||
"input": {
|
"input": {
|
||||||
|
"avatar": {
|
||||||
|
"delete": "delete"
|
||||||
|
},
|
||||||
"username": {
|
"username": {
|
||||||
"placeholder": "username",
|
"placeholder": "username",
|
||||||
"duplicate_prompt": "This username is taken."
|
"duplicate_prompt": "This username is taken."
|
||||||
|
|
|
@ -331,6 +331,10 @@ internal enum L10n {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
internal enum Input {
|
internal enum Input {
|
||||||
|
internal enum Avatar {
|
||||||
|
/// delete
|
||||||
|
internal static let delete = L10n.tr("Localizable", "Scene.Register.Input.Avatar.Delete")
|
||||||
|
}
|
||||||
internal enum DisplayName {
|
internal enum DisplayName {
|
||||||
/// display name
|
/// display name
|
||||||
internal static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.DisplayName.Placeholder")
|
internal static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.DisplayName.Placeholder")
|
||||||
|
|
|
@ -102,6 +102,7 @@ tap the link to confirm your account.";
|
||||||
"Scene.Register.Error.Special.PasswordTooShort" = "Password is too short (must be at least 8 characters)";
|
"Scene.Register.Error.Special.PasswordTooShort" = "Password is too short (must be at least 8 characters)";
|
||||||
"Scene.Register.Error.Special.UsernameInvalid" = "Username must only contain alphanumeric characters and underscores";
|
"Scene.Register.Error.Special.UsernameInvalid" = "Username must only contain alphanumeric characters and underscores";
|
||||||
"Scene.Register.Error.Special.UsernameTooLong" = "Username is too long (can't be longer than 30 characters)";
|
"Scene.Register.Error.Special.UsernameTooLong" = "Username is too long (can't be longer than 30 characters)";
|
||||||
|
"Scene.Register.Input.Avatar.Delete" = "delete";
|
||||||
"Scene.Register.Input.DisplayName.Placeholder" = "display name";
|
"Scene.Register.Input.DisplayName.Placeholder" = "display name";
|
||||||
"Scene.Register.Input.Email.Placeholder" = "email";
|
"Scene.Register.Input.Email.Placeholder" = "email";
|
||||||
"Scene.Register.Input.Invite.RegistrationUserInviteRequest" = "Why do you want to join?";
|
"Scene.Register.Input.Invite.RegistrationUserInviteRequest" = "Why do you want to join?";
|
||||||
|
|
|
@ -7,10 +7,58 @@
|
||||||
|
|
||||||
import CropViewController
|
import CropViewController
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import OSLog
|
||||||
import PhotosUI
|
import PhotosUI
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
extension MastodonRegisterViewController {
|
||||||
|
func createMediaContextMenu() -> 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 }
|
||||||
|
self.present(self.imagePicker, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
children.append(photoLibraryAction)
|
||||||
|
if UIImagePickerController.isSourceTypeAvailable(.camera) {
|
||||||
|
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 }
|
||||||
|
self.present(self.imagePickerController, animated: true, completion: nil)
|
||||||
|
})
|
||||||
|
children.append(cameraAction)
|
||||||
|
}
|
||||||
|
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 }
|
||||||
|
self.present(self.documentPickerController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
children.append(browseAction)
|
||||||
|
if self.viewModel.avatarImage.value != nil {
|
||||||
|
let deleteAction = UIAction(title: L10n.Scene.Register.Input.Avatar.delete, image: UIImage(systemName: "delete.left"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.viewModel.avatarImage.value = nil
|
||||||
|
self.avatarButton.setImage(nil, for: .normal)
|
||||||
|
}
|
||||||
|
children.append(deleteAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cropImage(image:UIImage,pickerViewController:UIViewController) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let cropController = CropViewController(croppingStyle: .default, image: image)
|
||||||
|
cropController.delegate = self
|
||||||
|
cropController.setAspectRatioPreset(.presetSquare, animated: true)
|
||||||
|
cropController.aspectRatioPickerButtonHidden = true
|
||||||
|
cropController.aspectRatioLockEnabled = true
|
||||||
|
pickerViewController.dismiss(animated: true, completion: {
|
||||||
|
self.present(cropController, animated: true, completion: nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - PHPickerViewControllerDelegate
|
// MARK: - PHPickerViewControllerDelegate
|
||||||
|
|
||||||
extension MastodonRegisterViewController: PHPickerViewControllerDelegate {
|
extension MastodonRegisterViewController: PHPickerViewControllerDelegate {
|
||||||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||||
guard let itemProvider = results.first?.itemProvider, itemProvider.canLoadObject(ofClass: UIImage.self) else {
|
guard let itemProvider = results.first?.itemProvider, itemProvider.canLoadObject(ofClass: UIImage.self) else {
|
||||||
|
@ -20,11 +68,11 @@ extension MastodonRegisterViewController: PHPickerViewControllerDelegate {
|
||||||
itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
|
itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard let image = image as? UIImage else {
|
guard let image = image as? UIImage else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
guard let error = error else { return }
|
guard let error = error else { return }
|
||||||
let alertController = UIAlertController(for: error, title: "", preferredStyle: .alert)
|
let alertController = UIAlertController(for: error, title: "", preferredStyle: .alert)
|
||||||
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
|
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
|
||||||
alertController.addAction(okAction)
|
alertController.addAction(okAction)
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.coordinator.present(
|
self.coordinator.present(
|
||||||
scene: .alertController(alertController: alertController),
|
scene: .alertController(alertController: alertController),
|
||||||
from: nil,
|
from: nil,
|
||||||
|
@ -33,21 +81,48 @@ extension MastodonRegisterViewController: PHPickerViewControllerDelegate {
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async {
|
self.cropImage(image: image, pickerViewController: picker)
|
||||||
let cropController = CropViewController(croppingStyle: .default, image: image)
|
|
||||||
cropController.delegate = self
|
|
||||||
cropController.setAspectRatioPreset(.presetSquare, animated: true)
|
|
||||||
cropController.aspectRatioPickerButtonHidden = true
|
|
||||||
cropController.aspectRatioLockEnabled = true
|
|
||||||
picker.dismiss(animated: true, completion: {
|
|
||||||
self.present(cropController, animated: true, completion: nil)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UIImagePickerControllerDelegate
|
||||||
|
|
||||||
|
extension MastodonRegisterViewController: UIImagePickerControllerDelegate & UINavigationControllerDelegate {
|
||||||
|
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
|
||||||
|
picker.dismiss(animated: true, completion: nil)
|
||||||
|
|
||||||
|
guard let image = info[.originalImage] as? UIImage else { return }
|
||||||
|
|
||||||
|
cropImage(image: image, pickerViewController: picker)
|
||||||
|
}
|
||||||
|
|
||||||
|
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
||||||
|
picker.dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UIDocumentPickerDelegate
|
||||||
|
|
||||||
|
extension MastodonRegisterViewController: UIDocumentPickerDelegate {
|
||||||
|
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||||
|
guard let url = urls.first else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
guard url.startAccessingSecurityScopedResource() else { return }
|
||||||
|
defer { url.stopAccessingSecurityScopedResource() }
|
||||||
|
let imageData = try Data(contentsOf: url)
|
||||||
|
guard let image = UIImage(data: imageData) else { return }
|
||||||
|
cropImage(image: image, pickerViewController: controller)
|
||||||
|
} catch {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - CropViewControllerDelegate
|
// MARK: - CropViewControllerDelegate
|
||||||
|
|
||||||
extension MastodonRegisterViewController: CropViewControllerDelegate {
|
extension MastodonRegisterViewController: 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) {
|
||||||
self.viewModel.avatarImage.value = image
|
self.viewModel.avatarImage.value = image
|
||||||
|
@ -56,8 +131,3 @@ extension MastodonRegisterViewController: CropViewControllerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MastodonRegisterViewController {
|
|
||||||
@objc func avatarButtonPressed(_ sender: UIButton) {
|
|
||||||
self.present(imagePicker, animated: true, completion: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -20,14 +20,28 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
||||||
|
|
||||||
var viewModel: MastodonRegisterViewModel!
|
var viewModel: MastodonRegisterViewModel!
|
||||||
|
|
||||||
lazy var imagePicker: PHPickerViewController = {
|
// picker
|
||||||
|
private(set) lazy var imagePicker: PHPickerViewController = {
|
||||||
var configuration = PHPickerConfiguration()
|
var configuration = PHPickerConfiguration()
|
||||||
configuration.filter = .images
|
configuration.filter = .images
|
||||||
|
configuration.selectionLimit = 1
|
||||||
|
|
||||||
let imagePicker = PHPickerViewController(configuration: configuration)
|
let imagePicker = PHPickerViewController(configuration: configuration)
|
||||||
imagePicker.delegate = self
|
imagePicker.delegate = self
|
||||||
return imagePicker
|
return imagePicker
|
||||||
}()
|
}()
|
||||||
|
private(set) lazy var imagePickerController: UIImagePickerController = {
|
||||||
|
let imagePickerController = UIImagePickerController()
|
||||||
|
imagePickerController.sourceType = .camera
|
||||||
|
imagePickerController.delegate = self
|
||||||
|
return imagePickerController
|
||||||
|
}()
|
||||||
|
|
||||||
|
private(set) lazy var documentPickerController: UIDocumentPickerViewController = {
|
||||||
|
let documentPickerController = UIDocumentPickerViewController(documentTypes: ["public.image"], in: .open)
|
||||||
|
documentPickerController.delegate = self
|
||||||
|
return documentPickerController
|
||||||
|
}()
|
||||||
|
|
||||||
let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||||
|
|
||||||
|
@ -56,7 +70,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let avatarButton: UIButton = {
|
let avatarButton: UIButton = {
|
||||||
let button = UIButton(type: .custom)
|
let button = HighlightDimmableButton()
|
||||||
let boldFont = UIFont.systemFont(ofSize: 42)
|
let boldFont = UIFont.systemFont(ofSize: 42)
|
||||||
let configuration = UIImage.SymbolConfiguration(font: boldFont)
|
let configuration = UIImage.SymbolConfiguration(font: boldFont)
|
||||||
let image = UIImage(systemName: "person.fill.viewfinder", withConfiguration: configuration)
|
let image = UIImage(systemName: "person.fill.viewfinder", withConfiguration: configuration)
|
||||||
|
@ -227,6 +241,9 @@ extension MastodonRegisterViewController {
|
||||||
setupOnboardingAppearance()
|
setupOnboardingAppearance()
|
||||||
defer { setupNavigationBarBackgroundView() }
|
defer { setupNavigationBarBackgroundView() }
|
||||||
|
|
||||||
|
avatarButton.menu = createMediaContextMenu()
|
||||||
|
avatarButton.showsMenuAsPrimaryAction = true
|
||||||
|
|
||||||
domainLabel.text = "@" + viewModel.domain + " "
|
domainLabel.text = "@" + viewModel.domain + " "
|
||||||
domainLabel.sizeToFit()
|
domainLabel.sizeToFit()
|
||||||
passwordCheckLabel.attributedText = MastodonRegisterViewModel.attributeStringForPassword(validateState: .empty)
|
passwordCheckLabel.attributedText = MastodonRegisterViewModel.attributeStringForPassword(validateState: .empty)
|
||||||
|
@ -388,9 +405,8 @@ extension MastodonRegisterViewController {
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] isHighlighted in
|
.sink { [weak self] isHighlighted in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let alpha: CGFloat = isHighlighted ? 0.8 : 1
|
let alpha: CGFloat = isHighlighted ? 0.6 : 1
|
||||||
self.plusIconImageView.alpha = alpha
|
self.plusIconImageView.alpha = alpha
|
||||||
self.avatarButton.alpha = alpha
|
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
@ -550,7 +566,6 @@ extension MastodonRegisterViewController {
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
avatarButton.addTarget(self, action: #selector(MastodonRegisterViewController.avatarButtonPressed(_:)), for: .touchUpInside)
|
|
||||||
signUpButton.addTarget(self, action: #selector(MastodonRegisterViewController.signUpButtonPressed(_:)), for: .touchUpInside)
|
signUpButton.addTarget(self, action: #selector(MastodonRegisterViewController.signUpButtonPressed(_:)), for: .touchUpInside)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue