// // MastodonRegisterView.swift // Mastodon // // Created by MainasuK on 2022-4-27. // import UIKit import SwiftUI import MastodonLocalization import MastodonSDK import MastodonAsset struct MastodonRegisterView: View { @ObservedObject var viewModel: MastodonRegisterViewModel @State var usernameRightViewWidth: CGFloat = 300 var body: some View { ScrollView(.vertical) { let margin: CGFloat = 16 // header HStack { Text(L10n.Scene.Register.title(viewModel.domain)) .font(Font(MastodonPickServerViewController.largeTitleFont as CTFont)) .foregroundColor(Color(Asset.Colors.Label.primary.color)) Spacer() } .padding(.horizontal, margin) // Avatar selector Menu { // Photo Library Button { viewModel.avatarMediaMenuActionPublisher.send(.photoLibrary) } label: { Label(L10n.Scene.Compose.MediaSelection.photoLibrary, systemImage: "photo") } // Camera if UIImagePickerController.isSourceTypeAvailable(.camera) { Button { viewModel.avatarMediaMenuActionPublisher.send(.camera) } label: { Label(L10n.Scene.Compose.MediaSelection.camera, systemImage: "camera") } } // Browse Button { viewModel.avatarMediaMenuActionPublisher.send(.browse) } label: { Label(L10n.Scene.Compose.MediaSelection.browse, systemImage: "folder") } // Delete if viewModel.avatarImage != nil { Divider() if #available(iOS 15.0, *) { Button(role: .destructive) { viewModel.avatarMediaMenuActionPublisher.send(.delete) } label: { Label(L10n.Scene.Register.Input.Avatar.delete, systemImage: "delete.left") } } else { // Fallback on earlier ve rsions Button { viewModel.avatarMediaMenuActionPublisher.send(.delete) } label: { Label(L10n.Scene.Register.Input.Avatar.delete, systemImage: "delete.left") } } } } label: { let avatarImage = viewModel.avatarImage ?? Asset.Scene.Onboarding.avatarPlaceholder.image Image(uiImage: avatarImage) .resizable() .frame(width: 88, height: 88, alignment: .center) .overlay(ZStack { Color.black.opacity(0.5) .frame(height: 22, alignment: .bottom) Text(L10n.Common.Controls.Actions.edit) .font(.system(size: 13, weight: .semibold)) .foregroundColor(.white) }, alignment: .bottom) .cornerRadius(22) } .padding(EdgeInsets(top: 20, leading: 0, bottom: 20, trailing: 0)) // Display Name & Uesrname VStack(alignment: .leading, spacing: 11) { TextField(L10n.Scene.Register.Input.DisplayName.placeholder.localizedCapitalized, text: $viewModel.name) .textContentType(.name) .disableAutocorrection(true) .modifier(FormTextFieldModifier(validateState: viewModel.displayNameValidateState)) HStack { TextField(L10n.Scene.Register.Input.Username.placeholder.localizedCapitalized, text: $viewModel.username) .textContentType(.username) .autocapitalization(.none) .disableAutocorrection(true) .keyboardType(.asciiCapable) Text("@\(viewModel.domain)") .lineLimit(1) .truncationMode(.middle) .measureWidth { usernameRightViewWidth = $0 } .frame(width: min(300.0, usernameRightViewWidth), alignment: .trailing) } .modifier(FormTextFieldModifier(validateState: viewModel.usernameValidateState)) .environment(\.layoutDirection, .leftToRight) // force LTR if let errorPrompt = viewModel.usernameErrorPrompt { Text(errorPrompt) .modifier(FormFootnoteModifier()) } } .padding(.horizontal, margin) .padding(.bottom, 22) // Email & Password & Password hint VStack(alignment: .leading, spacing: 11) { TextField(L10n.Scene.Register.Input.Email.placeholder.localizedCapitalized, text: $viewModel.email) .textContentType(.emailAddress) .autocapitalization(.none) .disableAutocorrection(true) .keyboardType(.emailAddress) .modifier(FormTextFieldModifier(validateState: viewModel.emailValidateState)) if let errorPrompt = viewModel.emailErrorPrompt { Text(errorPrompt) .modifier(FormFootnoteModifier()) } SecureField(L10n.Scene.Register.Input.Password.placeholder.localizedCapitalized, text: $viewModel.password) .textContentType(.newPassword) .modifier(FormTextFieldModifier(validateState: viewModel.passwordValidateState)) Text(L10n.Scene.Register.Input.Password.hint) .modifier(FormFootnoteModifier(foregroundColor: .secondary)) if let errorPrompt = viewModel.passwordErrorPrompt { Text(errorPrompt) .modifier(FormFootnoteModifier()) } } .padding(.horizontal, margin) .padding(.bottom, 22) // Reason if viewModel.approvalRequired { VStack(alignment: .leading, spacing: 11) { TextField(L10n.Scene.Register.Input.Invite.registrationUserInviteRequest.localizedCapitalized, text: $viewModel.reason) .modifier(FormTextFieldModifier(validateState: viewModel.reasonValidateState)) if let errorPrompt = viewModel.reasonErrorPrompt { Text(errorPrompt) .modifier(FormFootnoteModifier()) } } .padding(.horizontal, margin) } Spacer() .frame(minHeight: viewModel.bottomPaddingHeight) } .background( Color(viewModel.backgroundColor) .onTapGesture { viewModel.endEditing.send() } ) } struct FormTextFieldModifier: ViewModifier { var validateState: MastodonRegisterViewModel.ValidateState func body(content: Content) -> some View { ZStack { let shadowColor: Color = { switch validateState { case .empty: return .black.opacity(0.125) case .invalid: return Color(Asset.Colors.TextField.invalid.color) case .valid: return Color(Asset.Colors.TextField.valid.color) } }() Color(Asset.Scene.Onboarding.textFieldBackground.color) .cornerRadius(10) .shadow(color: shadowColor, radius: 1, x: 0, y: 2) .animation(.easeInOut, value: validateState) content .padding() .background(Color(Asset.Scene.Onboarding.textFieldBackground.color)) .cornerRadius(10) } } } struct FormFootnoteModifier: ViewModifier { var foregroundColor = Color(Asset.Colors.TextField.invalid.color) func body(content: Content) -> some View { content .font(.footnote) .foregroundColor(foregroundColor) .padding(.horizontal) } } } struct WidthKey: PreferenceKey { static let defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } } extension View { func measureWidth(_ f: @escaping (CGFloat) -> ()) -> some View { overlay(GeometryReader { proxy in Color.clear.preference(key: WidthKey.self, value: proxy.size.width) } .onPreferenceChange(WidthKey.self, perform: f)) } } #if DEBUG struct MastodonRegisterView_Previews: PreviewProvider { static var viewMdoel: MastodonRegisterViewModel { let domain = "mstdn.jp" return MastodonRegisterViewModel( context: .shared, domain: domain, authenticateInfo: AuthenticationViewModel.AuthenticateInfo( domain: domain, application: Mastodon.Entity.Application( name: "Preview", website: nil, vapidKey: nil, redirectURI: nil, clientID: "", clientSecret: "" ), redirectURI: "" )!, instance: Mastodon.Entity.Instance(domain: "mstdn.jp"), applicationToken: Mastodon.Entity.Token( accessToken: "", tokenType: "", scope: "", createdAt: Date() ) ) } static var viewMdoel2: MastodonRegisterViewModel { let domain = "mstdn.jp" return MastodonRegisterViewModel( context: .shared, domain: domain, authenticateInfo: AuthenticationViewModel.AuthenticateInfo( domain: domain, application: Mastodon.Entity.Application( name: "Preview", website: nil, vapidKey: nil, redirectURI: nil, clientID: "", clientSecret: "" ), redirectURI: "" )!, instance: Mastodon.Entity.Instance(domain: "mstdn.jp", approvalRequired: true), applicationToken: Mastodon.Entity.Token( accessToken: "", tokenType: "", scope: "", createdAt: Date() ) ) } static var previews: some View { Group { NavigationView { MastodonRegisterView(viewModel: viewMdoel) .navigationBarTitle(Text("")) .navigationBarTitleDisplayMode(.inline) } NavigationView { MastodonRegisterView(viewModel: viewMdoel) .navigationBarTitle(Text("")) .navigationBarTitleDisplayMode(.inline) } .preferredColorScheme(.dark) NavigationView { MastodonRegisterView(viewModel: viewMdoel) .navigationBarTitle(Text("")) .navigationBarTitleDisplayMode(.inline) } .environment(\.sizeCategory, .accessibilityExtraLarge) NavigationView { MastodonRegisterView(viewModel: viewMdoel2) .navigationBarTitle(Text("")) .navigationBarTitleDisplayMode(.inline) } } } } #endif