diff --git a/yobble/Network/ProfileModels.swift b/yobble/Network/ProfileModels.swift index e03cd44..6e621d8 100644 --- a/yobble/Network/ProfileModels.swift +++ b/yobble/Network/ProfileModels.swift @@ -5,6 +5,7 @@ struct ProfileDataPayload: Decodable { let login: String let fullName: String? let bio: String? + let avatars: Avatars? let balances: [WalletBalancePayload] let createdAt: Date? let stories: [JSONValue] @@ -15,6 +16,7 @@ struct ProfileDataPayload: Decodable { case login case fullName case bio + case avatars case balances case createdAt case stories @@ -27,6 +29,7 @@ struct ProfileDataPayload: Decodable { self.login = try container.decode(String.self, forKey: .login) self.fullName = try container.decodeIfPresent(String.self, forKey: .fullName) self.bio = try container.decodeIfPresent(String.self, forKey: .bio) + self.avatars = try container.decodeIfPresent(Avatars.self, forKey: .avatars) self.balances = try container.decodeIfPresent([WalletBalancePayload].self, forKey: .balances) ?? [] self.createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt) self.stories = try container.decodeIfPresent([JSONValue].self, forKey: .stories) ?? [] @@ -34,6 +37,19 @@ struct ProfileDataPayload: Decodable { } } +//struct AvatarInfo: Decodable { +// let fileId: String +// let mime: String? +// let size: Int? +// let width: Int? +// let height: Int? +// let createdAt: Date? +//} +// +//struct Avatars: Decodable { +// let current: AvatarInfo? +//} + struct WalletBalancePayload: Decodable { let currency: String let balance: Decimal diff --git a/yobble/Views/Tab/Settings/EditProfileView.swift b/yobble/Views/Tab/Settings/EditProfileView.swift index 652820f..c425ae3 100644 --- a/yobble/Views/Tab/Settings/EditProfileView.swift +++ b/yobble/Views/Tab/Settings/EditProfileView.swift @@ -1,80 +1,144 @@ import SwiftUI struct EditProfileView: View { + // State for form fields @State private var displayName = "" @State private var description = "" + // State for profile data and avatar + @State private var profile: ProfileDataPayload? @State private var avatarImage: UIImage? @State private var showImagePicker = false + // State for loading and errors + @State private var isLoading = false + @State private var alertMessage: String? + @State private var showAlert = false + + private let profileService = ProfileService() private let descriptionLimit = 1024 var body: some View { - Form { - Section { - HStack { - Spacer() - VStack { - if let image = avatarImage { - Image(uiImage: image) - .resizable() - .scaledToFill() - .frame(width: 120, height: 120) - .clipShape(Circle()) - } else { - Circle() - .fill(Color.secondary.opacity(0.2)) - .frame(width: 120, height: 120) - .overlay( - Image(systemName: "person.fill") - .font(.system(size: 60)) - .foregroundColor(.gray) - ) - } - Button("Изменить фото") { - showImagePicker = true - } - .padding(.top, 8) - } - Spacer() - } - } - .listRowBackground(Color(UIColor.systemGroupedBackground)) - - Section(header: Text("Публичная информация")) { - TextField("Отображаемое имя", text: $displayName) - - VStack(alignment: .leading, spacing: 5) { - Text("Описание") - .font(.caption) - .foregroundColor(.secondary) - TextEditor(text: $description) - .frame(height: 150) - .onChange(of: description) { newValue in - if newValue.count > descriptionLimit { - description = String(newValue.prefix(descriptionLimit)) - } - } + ZStack { + Form { + Section { HStack { Spacer() - Text("\(description.count) / \(descriptionLimit)") - .font(.caption) - .foregroundColor(description.count > descriptionLimit ? .red : .secondary) + VStack { + if let image = avatarImage { + Image(uiImage: image) + .resizable() + .scaledToFill() + .frame(width: 120, height: 120) + .clipShape(Circle()) + } else if let profile = profile, + let fileId = profile.avatars?.current?.fileId, + let url = avatarUrl(for: profile, fileId: fileId) { + CachedAvatarView(url: url, fileId: fileId, userId: profile.userId.uuidString) { + avatarPlaceholder + } + .aspectRatio(contentMode: .fill) + .frame(width: 120, height: 120) + .clipShape(Circle()) + } else { + avatarPlaceholder + } + + Button("Изменить фото") { + showImagePicker = true + } + .padding(.top, 8) + } + Spacer() } } - } + .listRowBackground(Color(UIColor.systemGroupedBackground)) - Button(action: { - // Действие для сохранения профиля - print("DisplayName: \(displayName)") - print("Description: \(description)") - }) { - Text("Применить") + Section(header: Text("Публичная информация")) { + TextField("Отображаемое имя", text: $displayName) + + VStack(alignment: .leading, spacing: 5) { + Text("Описание") + .font(.caption) + .foregroundColor(.secondary) + TextEditor(text: $description) + .frame(height: 150) + .onChange(of: description) { newValue in + if newValue.count > descriptionLimit { + description = String(newValue.prefix(descriptionLimit)) + } + } + HStack { + Spacer() + Text("\(description.count) / \(descriptionLimit)") + .font(.caption) + .foregroundColor(description.count > descriptionLimit ? .red : .secondary) + } + } + } + + Button(action: { + // Действие для сохранения профиля + print("DisplayName: \(displayName)") + print("Description: \(description)") + }) { + Text("Применить") + } + } + .navigationTitle("Редактировать профиль") + .onAppear(perform: loadProfile) + .sheet(isPresented: $showImagePicker) { + ImagePicker(image: $avatarImage) + } + .alert("Ошибка", isPresented: $showAlert, presenting: alertMessage) { _ in + Button("OK") {} + } message: { message in + Text(message) + } + + if isLoading { + Color.black.opacity(0.4).ignoresSafeArea() + ProgressView("Загрузка...") + .padding() + .background(Color.secondary.colorInvert()) + .cornerRadius(10) } } - .navigationTitle("Редактировать профиль") - .sheet(isPresented: $showImagePicker) { - ImagePicker(image: $avatarImage) + } + + private var avatarPlaceholder: some View { + Circle() + .fill(Color.secondary.opacity(0.2)) + .frame(width: 120, height: 120) + .overlay( + Image(systemName: "person.fill") + .font(.system(size: 60)) + .foregroundColor(.gray) + ) + } + + private func avatarUrl(for profile: ProfileDataPayload, fileId: String) -> URL? { + return URL(string: "\(AppConfig.API_SERVER)/v1/storage/download/avatar/\(profile.userId)?file_id=\(fileId)") + } + + private func loadProfile() { + isLoading = true + Task { + do { + let profile = try await profileService.fetchMyProfile() + await MainActor.run { + self.profile = profile + self.displayName = profile.fullName ?? "" + self.description = profile.bio ?? "" + self.isLoading = false + } + } catch { + await MainActor.run { + self.alertMessage = error.localizedDescription + self.showAlert = true + self.isLoading = false + } + } } } }