diff --git a/Localization/app.json b/Localization/app.json index c0a305d96..43cc8f6db 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -103,6 +103,9 @@ "register": { "title": "Tell us about you.", "input": { + "avatar": { + "delete": "delete" + }, "username": { "placeholder": "username", "duplicate_prompt": "This username is taken." @@ -228,4 +231,4 @@ } } } -} \ No newline at end of file +} diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index a875994ea..fe8e6294e 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -331,6 +331,10 @@ internal enum L10n { } } internal enum Input { + internal enum Avatar { + /// delete + internal static let delete = L10n.tr("Localizable", "Scene.Register.Input.Avatar.Delete") + } internal enum DisplayName { /// display name internal static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.DisplayName.Placeholder") diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index d2ebb4071..8c93414aa 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -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.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.Input.Avatar.Delete" = "delete"; "Scene.Register.Input.DisplayName.Placeholder" = "display name"; "Scene.Register.Input.Email.Placeholder" = "email"; "Scene.Register.Input.Invite.RegistrationUserInviteRequest" = "Why do you want to join?"; diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController+Avatar.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController+Avatar.swift index a23585271..3a25fad74 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController+Avatar.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController+Avatar.swift @@ -7,10 +7,58 @@ import CropViewController import Foundation +import OSLog import PhotosUI 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 + extension MastodonRegisterViewController: PHPickerViewControllerDelegate { func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { 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 guard let self = self else { return } guard let image = image as? UIImage else { - guard let error = error else { return } - let alertController = UIAlertController(for: error, title: "", preferredStyle: .alert) - let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) - alertController.addAction(okAction) DispatchQueue.main.async { + guard let error = error else { return } + let alertController = UIAlertController(for: error, title: "", preferredStyle: .alert) + let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) + alertController.addAction(okAction) self.coordinator.present( scene: .alertController(alertController: alertController), from: nil, @@ -33,21 +81,48 @@ extension MastodonRegisterViewController: PHPickerViewControllerDelegate { } return } - DispatchQueue.main.async { - 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) - }) - } + self.cropImage(image: image, pickerViewController: picker) + } + } +} + +// 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 + extension MastodonRegisterViewController: CropViewControllerDelegate { public func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) { 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) - } -} diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift index 04aea3c19..8f0162cd3 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift @@ -20,14 +20,28 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O var viewModel: MastodonRegisterViewModel! - lazy var imagePicker: PHPickerViewController = { + // picker + private(set) lazy var imagePicker: PHPickerViewController = { var configuration = PHPickerConfiguration() configuration.filter = .images + configuration.selectionLimit = 1 let imagePicker = PHPickerViewController(configuration: configuration) imagePicker.delegate = self 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 @@ -56,7 +70,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O }() let avatarButton: UIButton = { - let button = UIButton(type: .custom) + let button = HighlightDimmableButton() let boldFont = UIFont.systemFont(ofSize: 42) let configuration = UIImage.SymbolConfiguration(font: boldFont) let image = UIImage(systemName: "person.fill.viewfinder", withConfiguration: configuration) @@ -227,6 +241,9 @@ extension MastodonRegisterViewController { setupOnboardingAppearance() defer { setupNavigationBarBackgroundView() } + avatarButton.menu = createMediaContextMenu() + avatarButton.showsMenuAsPrimaryAction = true + domainLabel.text = "@" + viewModel.domain + " " domainLabel.sizeToFit() passwordCheckLabel.attributedText = MastodonRegisterViewModel.attributeStringForPassword(validateState: .empty) @@ -388,9 +405,8 @@ extension MastodonRegisterViewController { .receive(on: DispatchQueue.main) .sink { [weak self] isHighlighted in 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.avatarButton.alpha = alpha } .store(in: &disposeBag) @@ -550,7 +566,6 @@ extension MastodonRegisterViewController { .store(in: &disposeBag) } - avatarButton.addTarget(self, action: #selector(MastodonRegisterViewController.avatarButtonPressed(_:)), for: .touchUpInside) signUpButton.addTarget(self, action: #selector(MastodonRegisterViewController.signUpButtonPressed(_:)), for: .touchUpInside) }