Choose avatar.

This commit is contained in:
Marcin Czachursk 2023-03-24 18:44:16 +01:00
parent 9de64db689
commit 537f344cf2
7 changed files with 137 additions and 68 deletions

View File

@ -266,3 +266,4 @@
"editProfile.title.save" = "Save";
"editProfile.title.accountSaved" = "Profile has been updated.";
"editProfile.error.saveAccountFailed" = "Saving profile failed.";
"editProfile.error.loadingAvatarFailed" = "Loading avatar failed.";

View File

@ -266,3 +266,4 @@
"editProfile.title.save" = "Zapisz";
"editProfile.title.accountSaved" = "Profil zaktualizowano.";
"editProfile.error.saveAccountFailed" = "Błąd podczas aktualizacji profilu.";
"editProfile.error.loadingAvatarFailed" = "Błąd podczas wczytywania zdjęcia.";

View File

@ -198,6 +198,7 @@
F8CEEDF829ABADDD00DBED66 /* UIImage+Size.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CEEDF729ABADDD00DBED66 /* UIImage+Size.swift */; };
F8CEEDFA29ABAFD200DBED66 /* ImageFileTranseferable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CEEDF929ABAFD200DBED66 /* ImageFileTranseferable.swift */; };
F8E6D03329CDD52500416CCA /* EditProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E6D03229CDD52500416CCA /* EditProfileView.swift */; };
F8E6D03529CE161B00416CCA /* UIImage+Jpeg.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E6D03429CE161B00416CCA /* UIImage+Jpeg.swift */; };
F8E9391F29C0BCFA002BB3B8 /* ImageContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E9391E29C0BCFA002BB3B8 /* ImageContextMenu.swift */; };
F8E9392129C0DA7E002BB3B8 /* LazyImageState+ImageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E9392029C0DA7E002BB3B8 /* LazyImageState+ImageResponse.swift */; };
F8F6E44229BC58F20004795E /* Vernissage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */; };
@ -412,6 +413,7 @@
F8CEEDF729ABADDD00DBED66 /* UIImage+Size.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Size.swift"; sourceTree = "<group>"; };
F8CEEDF929ABAFD200DBED66 /* ImageFileTranseferable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFileTranseferable.swift; sourceTree = "<group>"; };
F8E6D03229CDD52500416CCA /* EditProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileView.swift; sourceTree = "<group>"; };
F8E6D03429CE161B00416CCA /* UIImage+Jpeg.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Jpeg.swift"; sourceTree = "<group>"; };
F8E9391E29C0BCFA002BB3B8 /* ImageContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContextMenu.swift; sourceTree = "<group>"; };
F8E9392029C0DA7E002BB3B8 /* LazyImageState+ImageResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LazyImageState+ImageResponse.swift"; sourceTree = "<group>"; };
F8EF371429C624DA00669F45 /* Vernissage-006.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-006.xcdatamodel"; sourceTree = "<group>"; };
@ -534,6 +536,7 @@
F8C14393296AF21B001FE31D /* Double+Round.swift */,
F8984E4C296B648000A2610F /* UIImage+Blurhash.swift */,
F8CEEDF729ABADDD00DBED66 /* UIImage+Size.swift */,
F8E6D03429CE161B00416CCA /* UIImage+Jpeg.swift */,
F8996DEA2971D29D0043EEC6 /* View+Transition.swift */,
F88E4D43297E82EB0057491A /* Status+MediaAttachmentType.swift */,
F8864CEE29ACE90B0020C534 /* UIFont+Font.swift */,
@ -1237,6 +1240,7 @@
F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */,
F86167C8297FE781004D1F67 /* AvatarShape.swift in Sources */,
F85D4973296406E700751DF7 /* BottomRight.swift in Sources */,
F8E6D03529CE161B00416CCA /* UIImage+Jpeg.swift in Sources */,
F898DE702972868A004B4A6A /* String+Empty.swift in Sources */,
F86B7218296C27C100EE59EC /* ActionButton.swift in Sources */,
F8FA9917299F7DBD007AB130 /* Client+Media.swift in Sources */,
@ -1274,7 +1278,7 @@
CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 80;
CURRENT_PROJECT_VERSION = 81;
DEVELOPMENT_TEAM = B2U9FEKYP8;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VernissageWidget/Info.plist;
@ -1302,7 +1306,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 80;
CURRENT_PROJECT_VERSION = 81;
DEVELOPMENT_TEAM = B2U9FEKYP8;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VernissageWidget/Info.plist;
@ -1450,7 +1454,7 @@
CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 80;
CURRENT_PROJECT_VERSION = 81;
DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\"";
DEVELOPMENT_TEAM = B2U9FEKYP8;
ENABLE_PREVIEWS = YES;
@ -1490,7 +1494,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 80;
CURRENT_PROJECT_VERSION = 81;
DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\"";
DEVELOPMENT_TEAM = B2U9FEKYP8;
ENABLE_PREVIEWS = YES;

View File

@ -0,0 +1,66 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import SwiftUI
extension UIImage {
public func getJpegData() -> Data? {
#if targetEnvironment(simulator)
// For testing purposes.
let converted = self.convertToExtendedSRGBJpeg()
let filePath = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).jpg")
try? converted?.write(to: filePath)
print(filePath.string)
#endif
// API don't support images over 5K.
if self.size.height > 10_000 || self.size.width > 10_000 {
return self
.resized(to: .init(width: self.size.width / 4, height: self.size.height / 4))
.convertToExtendedSRGBJpeg()
} else if self.size.height > 5000 || self.size.width > 5000 {
return self
.resized(to: .init(width: self.size.width / 2, height: self.size.height / 2))
.convertToExtendedSRGBJpeg()
} else {
return self
.convertToExtendedSRGBJpeg()
}
}
public func convertToExtendedSRGBJpeg() -> Data? {
guard let sourceImage = CIImage(image: self, options: [.applyOrientationProperty: true]) else {
return self.jpegData(compressionQuality: 0.9)
}
// We have to store correct image orientation.
let orientedImage = sourceImage.oriented(forExifOrientation: self.imageOrientation.exifOrientation)
// We dont have to convert images which already are in sRGB color space.
if orientedImage.colorSpace?.name == CGColorSpace.sRGB || orientedImage.colorSpace?.name == CGColorSpace.extendedSRGB {
return self.jpegData(compressionQuality: 0.9)
}
guard let colorSpace = CGColorSpace(name: CGColorSpace.extendedSRGB) else {
return self.jpegData(compressionQuality: 0.9)
}
guard let displayP3 = CGColorSpace(name: CGColorSpace.displayP3) else {
return self.jpegData(compressionQuality: 0.9)
}
// Create Core Image context (with working color space).
let ciContext = CIContext(options: [CIContextOption.workingColorSpace: orientedImage.colorSpace ?? displayP3])
// Creating image with new color space (and preserving colors).
guard let converted = ciContext.jpegRepresentation(of: orientedImage, colorSpace: colorSpace) else {
return self.jpegData(compressionQuality: 0.9)
}
// Returning successfully converted image.
return converted
}
}

View File

@ -52,37 +52,4 @@ extension UIImage {
return UIImage(cgImage: resizedCGImage)
}
func convertToExtendedSRGBJpeg() -> Data? {
guard let sourceImage = CIImage(image: self, options: [.applyOrientationProperty: true]) else {
return self.jpegData(compressionQuality: 0.9)
}
// We have to store correct image orientation.
let orientedImage = sourceImage.oriented(forExifOrientation: self.imageOrientation.exifOrientation)
// We dont have to convert images which already are in sRGB color space.
if orientedImage.colorSpace?.name == CGColorSpace.sRGB || orientedImage.colorSpace?.name == CGColorSpace.extendedSRGB {
return self.jpegData(compressionQuality: 0.9)
}
guard let colorSpace = CGColorSpace(name: CGColorSpace.extendedSRGB) else {
return self.jpegData(compressionQuality: 0.9)
}
guard let displayP3 = CGColorSpace(name: CGColorSpace.displayP3) else {
return self.jpegData(compressionQuality: 0.9)
}
// Create Core Image context (with working color space).
let ciContext = CIContext(options: [CIContextOption.workingColorSpace: orientedImage.colorSpace ?? displayP3])
// Creating image with new color space (and preserving colors).
guard let converted = ciContext.jpegRepresentation(of: orientedImage, colorSpace: colorSpace) else {
return self.jpegData(compressionQuality: 0.9)
}
// Returning successfully converted image.
return converted
}
}

View File

@ -555,7 +555,7 @@ struct ComposeView: View {
return
}
guard let data = self.getJpegData(image: image) else {
guard let data = image.getJpegData() else {
return
}
@ -570,31 +570,7 @@ struct ComposeView: View {
ErrorService.shared.handle(error, message: "compose.error.postingPhotoFailed", showToastr: true)
}
}
private func getJpegData(image: UIImage) -> Data? {
#if targetEnvironment(simulator)
// For testing purposes.
let converted = image.convertToExtendedSRGBJpeg()
let filePath = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).jpg")
try? converted?.write(to: filePath)
print(filePath.string)
#endif
// API don't support images over 5K.
if image.size.height > 10_000 || image.size.width > 10_000 {
return image
.resized(to: .init(width: image.size.width / 4, height: image.size.height / 4))
.convertToExtendedSRGBJpeg()
} else if image.size.height > 5000 || image.size.width > 5000 {
return image
.resized(to: .init(width: image.size.width / 2, height: image.size.height / 2))
.convertToExtendedSRGBJpeg()
} else {
return image
.convertToExtendedSRGBJpeg()
}
}
private func publishStatus() async {
do {
let status = self.createStatus()

View File

@ -4,6 +4,7 @@
// Licensed under the MIT License.
//
import PhotosUI
import SwiftUI
import PixelfedKit
@ -12,9 +13,12 @@ struct EditProfileView: View {
@EnvironmentObject private var client: Client
@Environment(\.dismiss) private var dismiss
@State private var photosPickerVisible = false
@State private var selectedItems: [PhotosPickerItem] = []
@State private var saveDisabled = false
@State var displayName: String = ""
@State var bio: String = ""
@State private var displayName: String = ""
@State private var bio: String = ""
@State private var avatarData: Data?
private let account: Account
@ -29,10 +33,19 @@ struct EditProfileView: View {
Spacer()
VStack {
ZStack {
UserAvatar(accountAvatar: account.avatar, size: .profile)
if let avatarData, let uiAvatar = UIImage(data: avatarData) {
Image(uiImage: uiAvatar)
.resizable()
.clipShape(applicationState.avatarShape.shape())
.aspectRatio(contentMode: .fit)
.frame(width: 96, height: 96)
} else {
UserAvatar(accountAvatar: account.avatar, size: .profile)
}
BottomRight {
Button {
self.photosPickerVisible = true
} label: {
Image(systemName: "person.crop.circle.badge.plus")
.font(.title)
@ -61,7 +74,7 @@ struct EditProfileView: View {
Section("editProfile.title.bio") {
TextField("", text: $bio, axis: .vertical)
.lineLimit(4, reservesSpace: true)
.lineLimit(5, reservesSpace: true)
}
}
.toolbar {
@ -84,15 +97,56 @@ struct EditProfileView: View {
self.bio = String(attributedString.characters)
}
}
.onChange(of: self.selectedItems) { selectedItem in
Task {
await self.getAvatar()
}
}
.photosPicker(isPresented: $photosPickerVisible,
selection: $selectedItems,
maxSelectionCount: 1,
matching: .images)
}
private func saveProfile() async {
do {
_ = try await self.client.accounts?.update(displayName: self.displayName, bio: self.bio, image: nil)
_ = try await self.client.accounts?.update(displayName: self.displayName, bio: self.bio, image: self.avatarData)
ToastrService.shared.showSuccess("editProfile.title.accountSaved", imageSystemName: "person.crop.circle")
dismiss()
} catch {
ErrorService.shared.handle(error, message: "editProfile.error.saveAccountFailed", showToastr: true)
}
}
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
.resized(to: .init(width: 400, height: 400))
.getJpegData() else {
return
}
self.avatarData = data
self.saveDisabled = false
} catch {
ErrorService.shared.handle(error, message: "editProfile.error.loadingAvatarFailed", showToastr: true)
}
}
}