add view img
This commit is contained in:
parent
134ba39914
commit
13a1d00934
@ -1282,6 +1282,9 @@
|
|||||||
"Не удалось подготовить изображение для загрузки." : {
|
"Не удалось подготовить изображение для загрузки." : {
|
||||||
"comment" : "Avatar encoding error"
|
"comment" : "Avatar encoding error"
|
||||||
},
|
},
|
||||||
|
"Не удалось подготовить изображение." : {
|
||||||
|
"comment" : "Avatar decoding error"
|
||||||
|
},
|
||||||
"Не удалось сериализовать данные запроса." : {
|
"Не удалось сериализовать данные запроса." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -2470,6 +2473,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Скачать" : {
|
||||||
|
"comment" : "Avatar download"
|
||||||
|
},
|
||||||
"Скопировано" : {
|
"Скопировано" : {
|
||||||
"comment" : "Заголовок уведомления о копировании"
|
"comment" : "Заголовок уведомления о копировании"
|
||||||
},
|
},
|
||||||
@ -2711,6 +2717,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Удаление аватара пока недоступно." : {
|
||||||
|
"comment" : "Avatar delete placeholder"
|
||||||
|
},
|
||||||
"Удаление контакта \"%1$@\" появится позже." : {
|
"Удаление контакта \"%1$@\" появится позже." : {
|
||||||
"comment" : "Contacts delete placeholder message"
|
"comment" : "Contacts delete placeholder message"
|
||||||
},
|
},
|
||||||
@ -2720,6 +2729,9 @@
|
|||||||
"Удалить контакт" : {
|
"Удалить контакт" : {
|
||||||
"comment" : "Contacts context action delete"
|
"comment" : "Contacts context action delete"
|
||||||
},
|
},
|
||||||
|
"Удалить фото" : {
|
||||||
|
"comment" : "Avatar delete"
|
||||||
|
},
|
||||||
"Удалить чат (скоро)" : {
|
"Удалить чат (скоро)" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
|||||||
@ -16,8 +16,12 @@ struct EditProfileView: View {
|
|||||||
@State private var isLoading = false
|
@State private var isLoading = false
|
||||||
@State private var isSaving = false
|
@State private var isSaving = false
|
||||||
@State private var isUploadingAvatar = false
|
@State private var isUploadingAvatar = false
|
||||||
|
@State private var isPreparingDownload = false
|
||||||
@State private var alertMessage: String?
|
@State private var alertMessage: String?
|
||||||
@State private var showAlert = false
|
@State private var showAlert = false
|
||||||
|
@State private var avatarViewerState: AvatarViewerState?
|
||||||
|
@State private var shareItems: [Any] = []
|
||||||
|
@State private var showShareSheet = false
|
||||||
|
|
||||||
private let profileService = ProfileService()
|
private let profileService = ProfileService()
|
||||||
private let descriptionLimit = 1024
|
private let descriptionLimit = 1024
|
||||||
@ -35,6 +39,10 @@ struct EditProfileView: View {
|
|||||||
.scaledToFill()
|
.scaledToFill()
|
||||||
.frame(width: 120, height: 120)
|
.frame(width: 120, height: 120)
|
||||||
.clipShape(Circle())
|
.clipShape(Circle())
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
presentAvatarViewer()
|
||||||
|
}
|
||||||
} else if let profile = profile,
|
} else if let profile = profile,
|
||||||
let fileId = profile.avatars?.current?.fileId,
|
let fileId = profile.avatars?.current?.fileId,
|
||||||
let url = avatarUrl(for: profile, fileId: fileId) {
|
let url = avatarUrl(for: profile, fileId: fileId) {
|
||||||
@ -44,8 +52,15 @@ struct EditProfileView: View {
|
|||||||
.aspectRatio(contentMode: .fill)
|
.aspectRatio(contentMode: .fill)
|
||||||
.frame(width: 120, height: 120)
|
.frame(width: 120, height: 120)
|
||||||
.clipShape(Circle())
|
.clipShape(Circle())
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
presentAvatarViewer()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
avatarPlaceholder
|
avatarPlaceholder
|
||||||
|
.onTapGesture {
|
||||||
|
presentAvatarViewer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button("Изменить фото") {
|
Button("Изменить фото") {
|
||||||
@ -112,6 +127,17 @@ struct EditProfileView: View {
|
|||||||
} message: { message in
|
} message: { message in
|
||||||
Text(message)
|
Text(message)
|
||||||
}
|
}
|
||||||
|
.fullScreenCover(item: $avatarViewerState) { state in
|
||||||
|
AvatarViewerView(
|
||||||
|
state: state,
|
||||||
|
onClose: { avatarViewerState = nil },
|
||||||
|
onDownload: { handleAvatarDownload(for: state) },
|
||||||
|
onDelete: handleAvatarDeletion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showShareSheet) {
|
||||||
|
ActivityView(activityItems: shareItems)
|
||||||
|
}
|
||||||
|
|
||||||
if isBusy {
|
if isBusy {
|
||||||
Color.black.opacity(0.4).ignoresSafeArea()
|
Color.black.opacity(0.4).ignoresSafeArea()
|
||||||
@ -162,13 +188,16 @@ struct EditProfileView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var isBusy: Bool {
|
private var isBusy: Bool {
|
||||||
isLoading || isSaving || isUploadingAvatar
|
isLoading || isSaving || isUploadingAvatar || isPreparingDownload
|
||||||
}
|
}
|
||||||
|
|
||||||
private var busyMessage: String {
|
private var busyMessage: String {
|
||||||
if isUploadingAvatar {
|
if isUploadingAvatar {
|
||||||
return "Обновление аватара..."
|
return "Обновление аватара..."
|
||||||
}
|
}
|
||||||
|
if isPreparingDownload {
|
||||||
|
return "Подготовка изображения..."
|
||||||
|
}
|
||||||
if isSaving {
|
if isSaving {
|
||||||
return "Сохранение..."
|
return "Сохранение..."
|
||||||
}
|
}
|
||||||
@ -256,6 +285,64 @@ struct EditProfileView: View {
|
|||||||
self.originalDisplayName = loadedName
|
self.originalDisplayName = loadedName
|
||||||
self.originalDescription = loadedBio
|
self.originalDescription = loadedBio
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func presentAvatarViewer() {
|
||||||
|
if let image = avatarImage {
|
||||||
|
avatarViewerState = AvatarViewerState(source: .local(image))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let profile,
|
||||||
|
let fileId = profile.avatars?.current?.fileId,
|
||||||
|
let url = avatarUrl(for: profile, fileId: fileId) else { return }
|
||||||
|
avatarViewerState = AvatarViewerState(source: .remote(url: url, fileId: fileId, userId: profile.userId.uuidString))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleAvatarDownload(for state: AvatarViewerState) {
|
||||||
|
guard !isPreparingDownload else { return }
|
||||||
|
isPreparingDownload = true
|
||||||
|
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let image = try await resolveImage(for: state)
|
||||||
|
await MainActor.run {
|
||||||
|
shareItems = [image]
|
||||||
|
showShareSheet = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
await MainActor.run {
|
||||||
|
alertMessage = error.localizedDescription
|
||||||
|
showAlert = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
isPreparingDownload = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleAvatarDeletion() {
|
||||||
|
alertMessage = NSLocalizedString("Удаление аватара пока недоступно.", comment: "Avatar delete placeholder")
|
||||||
|
showAlert = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resolveImage(for state: AvatarViewerState) async throws -> UIImage {
|
||||||
|
switch state.source {
|
||||||
|
case .local(let image):
|
||||||
|
return image
|
||||||
|
case .remote(let url, let fileId, let userId):
|
||||||
|
if let cached = AvatarCacheService.shared.getImage(forKey: fileId, userId: userId) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
let (data, _) = try await URLSession.shared.data(from: url)
|
||||||
|
guard let image = UIImage(data: data) else {
|
||||||
|
throw AvatarViewerError.imageDecodingFailed
|
||||||
|
}
|
||||||
|
AvatarCacheService.shared.saveImage(image, forKey: fileId, userId: userId)
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ImagePicker: UIViewControllerRepresentable {
|
struct ImagePicker: UIViewControllerRepresentable {
|
||||||
@ -293,3 +380,107 @@ struct ImagePicker: UIViewControllerRepresentable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct AvatarViewerState: Identifiable {
|
||||||
|
enum Source {
|
||||||
|
case local(UIImage)
|
||||||
|
case remote(url: URL, fileId: String, userId: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = UUID()
|
||||||
|
let source: Source
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AvatarViewerError: LocalizedError {
|
||||||
|
case imageDecodingFailed
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .imageDecodingFailed:
|
||||||
|
return NSLocalizedString("Не удалось подготовить изображение.", comment: "Avatar decoding error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AvatarViewerView: View {
|
||||||
|
let state: AvatarViewerState
|
||||||
|
let onClose: () -> Void
|
||||||
|
let onDownload: () -> Void
|
||||||
|
let onDelete: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
NavigationStack {
|
||||||
|
ZStack {
|
||||||
|
Color.black.ignoresSafeArea()
|
||||||
|
avatarContent
|
||||||
|
}
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
Button(action: onClose) {
|
||||||
|
Image(systemName: "xmark")
|
||||||
|
}
|
||||||
|
.tint(.white)
|
||||||
|
}
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Menu {
|
||||||
|
Button(action: onDownload) {
|
||||||
|
Label(NSLocalizedString("Скачать", comment: "Avatar download"), systemImage: "square.and.arrow.down")
|
||||||
|
}
|
||||||
|
Button(role: .destructive, action: onDelete) {
|
||||||
|
Label(NSLocalizedString("Удалить фото", comment: "Avatar delete"), systemImage: "trash")
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "ellipsis.circle")
|
||||||
|
.imageScale(.large)
|
||||||
|
}
|
||||||
|
.tint(.white)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var avatarContent: some View {
|
||||||
|
switch state.source {
|
||||||
|
case .local(let image):
|
||||||
|
Image(uiImage: image)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.padding()
|
||||||
|
case .remote(let url, _, _):
|
||||||
|
AsyncImage(url: url) { phase in
|
||||||
|
switch phase {
|
||||||
|
case .empty:
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(.circular)
|
||||||
|
case .success(let image):
|
||||||
|
image
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.padding()
|
||||||
|
case .failure:
|
||||||
|
Image(systemName: "person.crop.circle.badge.exclam")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(width: 120, height: 120)
|
||||||
|
.foregroundColor(.white.opacity(0.7))
|
||||||
|
@unknown default:
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ActivityView: UIViewControllerRepresentable {
|
||||||
|
let activityItems: [Any]
|
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> UIActivityViewController {
|
||||||
|
UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user