Impressia/Vernissage/Views/EditProfileView.swift

282 lines
10 KiB
Swift
Raw Normal View History

2023-03-24 17:45:27 +01:00
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
2023-03-28 10:35:38 +02:00
// Licensed under the Apache License 2.0.
2023-03-24 17:45:27 +01:00
//
2023-03-24 18:44:16 +01:00
import PhotosUI
2023-03-24 17:45:27 +01:00
import SwiftUI
import PixelfedKit
2023-03-26 08:21:22 +02:00
import HTMLString
2023-03-24 17:45:27 +01:00
struct EditProfileView: View {
@EnvironmentObject private var applicationState: ApplicationState
@EnvironmentObject private var client: Client
@Environment(\.dismiss) private var dismiss
2023-03-26 08:21:22 +02:00
@State private var account: Account?
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-25 17:45:50 +01:00
@State private var isPrivate = false
2023-03-24 18:44:16 +01:00
@State private var avatarData: Data?
2023-03-26 08:21:22 +02:00
@State private var state: ViewState = .loading
2023-03-24 17:45:27 +01:00
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
var body: some View {
2023-03-26 08:21:22 +02:00
switch state {
case .loading:
LoadingIndicator()
.task {
await self.loadData()
}
case .loaded:
if let account = self.account {
self.editForm(account: account)
} else {
NoDataView(imageSystemName: "person.crop.circle", text: "editProfile.error.noProfileData")
}
case .error(let error):
ErrorView(error: error) {
self.state = .loading
await self.loadData()
}
.padding()
}
}
@ViewBuilder
private func editForm(account: Account) -> some View {
2023-03-24 17:45:27 +01:00
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-26 08:21:22 +02:00
Text("@\(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-25 17:45:50 +01:00
Section {
Toggle("editProfile.title.privateAccount", isOn: $isPrivate)
} footer: {
Text("editProfile.title.privateAccountInfo", comment: "Private account info")
}
2023-03-24 17:45:27 +01:00
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
2023-03-25 17:45:50 +01:00
ActionButton(showLoader: true) {
2023-03-26 08:21:22 +02:00
await self.saveProfile(account: account)
2023-03-24 17:45:27 +01:00
} label: {
Text("editProfile.title.save", comment: "Save")
}
.disabled(self.saveDisabled)
.buttonStyle(.borderedProminent)
}
}
.navigationTitle("editProfile.navigationBar.title")
.onAppear {
2023-03-26 08:21:22 +02:00
self.displayName = account.displayName ?? String.empty()
self.website = account.website ?? String.empty()
self.isPrivate = account.locked
2023-03-24 17:45:27 +01:00
2023-03-26 08:21:22 +02:00
// Bio should be set from source property (which is plain text).
if let note = account.source?.note {
self.bio = note.removingHTMLEntities()
} else {
let markdownBio = account.note?.asMarkdown ?? String.empty()
if let attributedString = try? AttributedString(markdown: markdownBio) {
self.bio = String(attributedString.characters)
}
2023-03-24 17:45:27 +01:00
}
}
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-26 08:21:22 +02:00
private func loadData() async {
do {
self.account = try await self.client.accounts?.pixelfedClient.verifyCredentials()
self.state = .loaded
} catch {
if !Task.isCancelled {
ErrorService.shared.handle(error, message: "editProfile.error.loadingAccountFailed", showToastr: true)
self.state = .error(error)
} else {
ErrorService.shared.handle(error, message: "editProfile.error.loadingAccountFailed", showToastr: false)
}
}
}
2023-03-25 17:01:28 +01:00
@MainActor
2023-03-26 08:21:22 +02:00
private func saveProfile(account: Account) async {
2023-03-24 17:45:27 +01:00
do {
2023-03-25 17:01:28 +01:00
_ = try await self.client.accounts?.update(displayName: self.displayName,
bio: self.bio,
website: self.website,
2023-03-25 17:45:50 +01:00
locked: self.isPrivate,
2023-03-25 17:01:28 +01:00
image: nil)
if let avatarData = self.avatarData {
_ = try await self.client.accounts?.avatar(image: avatarData)
2023-03-26 08:21:22 +02:00
if let accountData = AccountDataHandler.shared.getAccountData(accountId: account.id) {
2023-03-25 17:01:28 +01:00
accountData.avatarData = avatarData
self.applicationState.account?.avatarData = avatarData
CoreDataHandler.shared.save()
}
}
2023-03-26 08:21:22 +02:00
let savedAccount = try await self.client.accounts?.account(withId: account.id)
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
}