Files
mobile-ios/Rosetta/Features/Settings/ProfileEditView.swift

214 lines
7.4 KiB
Swift

import PhotosUI
import SwiftUI
/// Embedded profile editing content (no NavigationStack lives inside SettingsView's).
/// Avatar + photo picker, name fields with validation.
struct ProfileEditView: View {
@Binding var displayName: String
@Binding var username: String
let publicKey: String
@Binding var displayNameError: String?
@Binding var usernameError: String?
@State private var selectedPhotoItem: PhotosPickerItem?
@State private var selectedPhoto: UIImage?
private var initials: String {
RosettaColors.initials(name: displayName, publicKey: publicKey)
}
private var avatarColorIndex: Int {
RosettaColors.avatarColorIndex(for: displayName, publicKey: publicKey)
}
var body: some View {
VStack(spacing: 0) {
avatarSection
.padding(.bottom, 24)
nameSection
helperText("Enter your name and add an optional profile photo.")
.padding(.top, 8)
.padding(.bottom, 24)
addAccountSection
}
.padding(.horizontal, 16)
.padding(.top, 24)
.padding(.bottom, 100)
}
}
// MARK: - Avatar Section
private extension ProfileEditView {
var avatarSection: some View {
VStack(spacing: 12) {
if let selectedPhoto {
Image(uiImage: selectedPhoto)
.resizable()
.scaledToFill()
.frame(width: 80, height: 80)
.clipShape(Circle())
} else {
AvatarView(
initials: initials,
colorIndex: avatarColorIndex,
size: 80,
isSavedMessages: false
)
}
PhotosPicker(selection: $selectedPhotoItem, matching: .images) {
Text("Set New Photo")
.font(.system(size: 15, weight: .medium))
.foregroundStyle(RosettaColors.primaryBlue)
}
.buttonStyle(.plain)
.onChange(of: selectedPhotoItem) { _, item in
Task {
if let data = try? await item?.loadTransferable(type: Data.self),
let image = UIImage(data: data) {
selectedPhoto = image
}
}
}
}
}
}
// MARK: - Name Section
private extension ProfileEditView {
var nameSection: some View {
VStack(alignment: .leading, spacing: 0) {
GlassCard(cornerRadius: 26, fillOpacity: 0.08) {
VStack(spacing: 0) {
// Display Name field
HStack {
TextField("First Name", text: $displayName)
.font(.system(size: 17))
.foregroundStyle(RosettaColors.Adaptive.text)
.autocorrectionDisabled()
.textInputAutocapitalization(.words)
.onChange(of: displayName) { _, newValue in
displayNameError = ProfileValidator.validateDisplayName(newValue)?.errorDescription
}
if !displayName.isEmpty {
Button { displayName = "" } label: {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 18))
.foregroundStyle(RosettaColors.tertiaryText)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, 16)
.frame(height: 52)
Divider()
.background(RosettaColors.Adaptive.divider)
.padding(.leading, 16)
// Username field
HStack {
TextField("Username", text: $username)
.font(.system(size: 17))
.foregroundStyle(RosettaColors.Adaptive.text)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.onChange(of: username) { _, newValue in
let lowered = newValue.lowercased()
if lowered != newValue {
username = lowered
}
usernameError = ProfileValidator.validateUsername(lowered)?.errorDescription
}
if !username.isEmpty {
Text("\(username.count)/32")
.font(.system(size: 13))
.foregroundStyle(RosettaColors.tertiaryText)
.padding(.trailing, 4)
Button { username = "" } label: {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 18))
.foregroundStyle(RosettaColors.tertiaryText)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, 16)
.frame(height: 52)
}
}
// Validation errors below the card
if let displayNameError {
Text(displayNameError)
.font(.system(size: 13))
.foregroundStyle(RosettaColors.error)
.padding(.horizontal, 16)
.padding(.top, 8)
}
if let usernameError {
Text(usernameError)
.font(.system(size: 13))
.foregroundStyle(RosettaColors.error)
.padding(.horizontal, 16)
.padding(.top, displayNameError != nil ? 2 : 8)
}
}
}
}
// MARK: - Add Account & Helpers
private extension ProfileEditView {
var addAccountSection: some View {
GlassCard(cornerRadius: 26, fillOpacity: 0.08) {
Button {} label: {
HStack {
Spacer()
Text("Add Another Account")
.font(.system(size: 17))
.foregroundStyle(RosettaColors.primaryBlue)
Spacer()
}
.frame(height: 52)
}
.buttonStyle(.plain)
}
}
func helperText(_ text: String) -> some View {
Text(text)
.font(.system(size: 13))
.foregroundStyle(RosettaColors.secondaryText)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 16)
}
}
// MARK: - Preview
#Preview {
NavigationStack {
ScrollView {
ProfileEditView(
displayName: .constant("Gaidar"),
username: .constant("GaidarTheDev"),
publicKey: "028d1c9d0000000000000000000000000000000000000000000000000000008e03ec",
displayNameError: .constant(nil),
usernameError: .constant(nil)
)
}
.background(RosettaColors.Adaptive.background)
}
.preferredColorScheme(.dark)
}