2023-03-24 17:45:27 +01:00
|
|
|
//
|
|
|
|
// https://mczachurski.dev
|
|
|
|
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
|
|
|
// Licensed under the MIT License.
|
|
|
|
//
|
|
|
|
|
2023-03-24 18:44:16 +01:00
|
|
|
import PhotosUI
|
2023-03-24 17:45:27 +01:00
|
|
|
import SwiftUI
|
|
|
|
import PixelfedKit
|
|
|
|
|
|
|
|
struct EditProfileView: View {
|
|
|
|
@EnvironmentObject private var applicationState: ApplicationState
|
|
|
|
@EnvironmentObject private var client: Client
|
|
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
|
2023-03-24 18:44:16 +01:00
|
|
|
@State private var photosPickerVisible = false
|
|
|
|
@State private var selectedItems: [PhotosPickerItem] = []
|
2023-03-24 17:45:27 +01:00
|
|
|
@State private var saveDisabled = false
|
2023-03-24 18:44:16 +01:00
|
|
|
@State private var displayName: String = ""
|
|
|
|
@State private var bio: String = ""
|
2023-03-25 11:37:02 +01:00
|
|
|
@State private var website: String = ""
|
2023-03-24 18:44:16 +01:00
|
|
|
@State private var avatarData: Data?
|
2023-03-24 17:45:27 +01:00
|
|
|
|
|
|
|
private let account: Account
|
2023-03-25 17:01:28 +01:00
|
|
|
private let bioMaxLength = 200
|
|
|
|
private let displayNameMaxLength = 30
|
|
|
|
private let websiteMaxLength = 120
|
2023-03-24 17:45:27 +01:00
|
|
|
|
|
|
|
init(account: Account) {
|
|
|
|
self.account = account
|
|
|
|
}
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
Form {
|
2023-03-25 11:37:02 +01:00
|
|
|
HStack {
|
|
|
|
Spacer()
|
|
|
|
VStack {
|
|
|
|
ZStack {
|
|
|
|
if let avatarData, let uiAvatar = UIImage(data: avatarData) {
|
|
|
|
Image(uiImage: uiAvatar)
|
|
|
|
.resizable()
|
|
|
|
.clipShape(applicationState.avatarShape.shape())
|
|
|
|
.aspectRatio(contentMode: .fill)
|
|
|
|
.frame(width: 120, height: 120)
|
|
|
|
} else {
|
|
|
|
UserAvatar(accountAvatar: account.avatar, size: .large)
|
|
|
|
}
|
|
|
|
|
|
|
|
LoadingIndicator(isVisible: $saveDisabled)
|
2023-03-24 18:44:16 +01:00
|
|
|
|
2023-03-25 11:37:02 +01:00
|
|
|
BottomRight {
|
|
|
|
Button {
|
|
|
|
self.photosPickerVisible = true
|
|
|
|
} label: {
|
|
|
|
ZStack {
|
|
|
|
Circle()
|
|
|
|
.foregroundColor(.accentColor.opacity(0.8))
|
|
|
|
.frame(width: 40, height: 40)
|
2023-03-24 17:45:27 +01:00
|
|
|
Image(systemName: "person.crop.circle.badge.plus")
|
2023-03-25 11:37:02 +01:00
|
|
|
.font(.title2)
|
|
|
|
.foregroundColor(.white)
|
2023-03-24 17:45:27 +01:00
|
|
|
}
|
|
|
|
}
|
2023-03-25 11:37:02 +01:00
|
|
|
.buttonStyle(PlainButtonStyle())
|
2023-03-24 17:45:27 +01:00
|
|
|
}
|
2023-03-25 11:37:02 +01:00
|
|
|
.frame(width: 130, height: 130)
|
2023-03-24 17:45:27 +01:00
|
|
|
}
|
|
|
|
|
2023-03-25 11:37:02 +01:00
|
|
|
Text("@\(self.account.acct)")
|
2023-03-25 17:01:28 +01:00
|
|
|
.font(.headline)
|
2023-03-25 11:37:02 +01:00
|
|
|
.foregroundColor(.lightGrayColor)
|
2023-03-25 17:01:28 +01:00
|
|
|
|
|
|
|
if self.avatarData != nil {
|
|
|
|
HStack {
|
|
|
|
Image(systemName: "info.circle")
|
|
|
|
.font(.body)
|
|
|
|
.foregroundColor(.accentColor)
|
|
|
|
Text("editProfile.title.photoInfo")
|
|
|
|
.font(.footnote)
|
|
|
|
.foregroundColor(.lightGrayColor)
|
|
|
|
}
|
|
|
|
.padding(.top, 4)
|
|
|
|
}
|
2023-03-24 17:45:27 +01:00
|
|
|
}
|
2023-03-25 11:37:02 +01:00
|
|
|
|
|
|
|
Spacer()
|
2023-03-24 17:45:27 +01:00
|
|
|
}
|
2023-03-25 11:37:02 +01:00
|
|
|
.padding(-10)
|
2023-03-24 17:45:27 +01:00
|
|
|
.listRowBackground(Color(UIColor.systemGroupedBackground))
|
|
|
|
.listRowSeparator(Visibility.hidden)
|
2023-03-25 11:37:02 +01:00
|
|
|
|
2023-03-25 17:01:28 +01:00
|
|
|
Section {
|
2023-03-24 17:45:27 +01:00
|
|
|
TextField("", text: $displayName)
|
2023-03-25 17:01:28 +01:00
|
|
|
.onChange(of: self.displayName, perform: { newValue in
|
|
|
|
self.displayName = String(self.displayName.prefix(self.displayNameMaxLength))
|
|
|
|
})
|
|
|
|
} header: {
|
|
|
|
Text("editProfile.title.displayName", comment: "Display name")
|
|
|
|
} footer: {
|
|
|
|
HStack {
|
|
|
|
Spacer()
|
|
|
|
Text("\(self.displayName.count)/\(self.displayNameMaxLength)")
|
|
|
|
}
|
2023-03-24 17:45:27 +01:00
|
|
|
}
|
|
|
|
|
2023-03-25 17:01:28 +01:00
|
|
|
Section {
|
2023-03-24 17:45:27 +01:00
|
|
|
TextField("", text: $bio, axis: .vertical)
|
2023-03-24 18:44:16 +01:00
|
|
|
.lineLimit(5, reservesSpace: true)
|
2023-03-25 17:01:28 +01:00
|
|
|
.onChange(of: self.bio, perform: { newValue in
|
|
|
|
self.bio = String(self.bio.prefix(self.bioMaxLength))
|
|
|
|
})
|
|
|
|
} header: {
|
|
|
|
Text("editProfile.title.bio", comment: "Bio")
|
|
|
|
} footer: {
|
|
|
|
HStack {
|
|
|
|
Spacer()
|
|
|
|
Text("\(self.bio.count)/\(self.bioMaxLength)")
|
|
|
|
}
|
2023-03-24 17:45:27 +01:00
|
|
|
}
|
2023-03-25 11:37:02 +01:00
|
|
|
|
2023-03-25 17:01:28 +01:00
|
|
|
Section {
|
2023-03-25 11:37:02 +01:00
|
|
|
TextField("", text: $website)
|
|
|
|
.autocapitalization(.none)
|
|
|
|
.keyboardType(.URL)
|
|
|
|
.autocorrectionDisabled()
|
2023-03-25 17:01:28 +01:00
|
|
|
.onChange(of: self.website, perform: { newValue in
|
|
|
|
self.website = String(self.website.prefix(self.websiteMaxLength))
|
|
|
|
})
|
|
|
|
} header: {
|
|
|
|
Text("editProfile.title.website", comment: "Website")
|
|
|
|
} footer: {
|
|
|
|
HStack {
|
|
|
|
Spacer()
|
|
|
|
Text("\(self.website.count)/\(self.websiteMaxLength)")
|
|
|
|
}
|
2023-03-25 11:37:02 +01:00
|
|
|
}
|
2023-03-24 17:45:27 +01:00
|
|
|
}
|
|
|
|
.toolbar {
|
|
|
|
ToolbarItem(placement: .primaryAction) {
|
|
|
|
ActionButton(showLoader: false) {
|
|
|
|
await self.saveProfile()
|
|
|
|
} label: {
|
|
|
|
Text("editProfile.title.save", comment: "Save")
|
|
|
|
}
|
|
|
|
.disabled(self.saveDisabled)
|
|
|
|
.buttonStyle(.borderedProminent)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.navigationTitle("editProfile.navigationBar.title")
|
|
|
|
.onAppear {
|
|
|
|
self.displayName = self.account.displayName ?? String.empty()
|
2023-03-25 11:37:02 +01:00
|
|
|
self.website = self.account.website ?? String.empty()
|
2023-03-24 17:45:27 +01:00
|
|
|
|
|
|
|
let markdownBio = self.account.note?.asMarkdown ?? String.empty()
|
|
|
|
if let attributedString = try? AttributedString(markdown: markdownBio) {
|
|
|
|
self.bio = String(attributedString.characters)
|
|
|
|
}
|
|
|
|
}
|
2023-03-24 18:44:16 +01:00
|
|
|
.onChange(of: self.selectedItems) { selectedItem in
|
|
|
|
Task {
|
|
|
|
await self.getAvatar()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.photosPicker(isPresented: $photosPickerVisible,
|
|
|
|
selection: $selectedItems,
|
|
|
|
maxSelectionCount: 1,
|
|
|
|
matching: .images)
|
2023-03-24 17:45:27 +01:00
|
|
|
}
|
|
|
|
|
2023-03-25 17:01:28 +01:00
|
|
|
@MainActor
|
2023-03-24 17:45:27 +01:00
|
|
|
private func saveProfile() async {
|
|
|
|
do {
|
2023-03-25 17:01:28 +01:00
|
|
|
_ = try await self.client.accounts?.update(displayName: self.displayName,
|
|
|
|
bio: self.bio,
|
|
|
|
website: self.website,
|
|
|
|
image: nil)
|
|
|
|
|
|
|
|
if let avatarData = self.avatarData {
|
|
|
|
_ = try await self.client.accounts?.avatar(image: avatarData)
|
|
|
|
|
|
|
|
if let accountData = AccountDataHandler.shared.getAccountData(accountId: self.account.id) {
|
|
|
|
accountData.avatarData = avatarData
|
|
|
|
self.applicationState.account?.avatarData = avatarData
|
|
|
|
CoreDataHandler.shared.save()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let savedAccount = try await self.client.accounts?.account(withId: self.account.id)
|
|
|
|
// self.applicationState.account?.avatar,
|
2023-03-25 11:37:02 +01:00
|
|
|
self.applicationState.updatedProfile = savedAccount
|
|
|
|
|
2023-03-24 17:45:27 +01:00
|
|
|
ToastrService.shared.showSuccess("editProfile.title.accountSaved", imageSystemName: "person.crop.circle")
|
|
|
|
dismiss()
|
|
|
|
} catch {
|
|
|
|
ErrorService.shared.handle(error, message: "editProfile.error.saveAccountFailed", showToastr: true)
|
|
|
|
}
|
|
|
|
}
|
2023-03-24 18:44:16 +01:00
|
|
|
|
|
|
|
private func getAvatar() async {
|
|
|
|
do {
|
|
|
|
self.saveDisabled = true
|
|
|
|
|
|
|
|
for item in self.selectedItems {
|
|
|
|
if let data = try await item.loadTransferable(type: Data.self) {
|
|
|
|
self.avatarData = data
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
guard let imageData = self.avatarData else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
guard let image = UIImage(data: imageData) else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
guard let data = image
|
2023-03-25 11:37:02 +01:00
|
|
|
.resized(to: .init(width: 800, height: 800))
|
2023-03-24 18:44:16 +01:00
|
|
|
.getJpegData() else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-03-25 17:01:28 +01:00
|
|
|
withAnimation(.linear) {
|
|
|
|
self.avatarData = data
|
|
|
|
}
|
2023-03-24 18:44:16 +01:00
|
|
|
|
|
|
|
self.saveDisabled = false
|
|
|
|
} catch {
|
|
|
|
ErrorService.shared.handle(error, message: "editProfile.error.loadingAvatarFailed", showToastr: true)
|
|
|
|
}
|
|
|
|
}
|
2023-03-24 17:45:27 +01:00
|
|
|
}
|