add view img
This commit is contained in:
parent
134ba39914
commit
13a1d00934
@ -1282,6 +1282,9 @@
|
||||
"Не удалось подготовить изображение для загрузки." : {
|
||||
"comment" : "Avatar encoding error"
|
||||
},
|
||||
"Не удалось подготовить изображение." : {
|
||||
"comment" : "Avatar decoding error"
|
||||
},
|
||||
"Не удалось сериализовать данные запроса." : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -2470,6 +2473,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Скачать" : {
|
||||
"comment" : "Avatar download"
|
||||
},
|
||||
"Скопировано" : {
|
||||
"comment" : "Заголовок уведомления о копировании"
|
||||
},
|
||||
@ -2711,6 +2717,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Удаление аватара пока недоступно." : {
|
||||
"comment" : "Avatar delete placeholder"
|
||||
},
|
||||
"Удаление контакта \"%1$@\" появится позже." : {
|
||||
"comment" : "Contacts delete placeholder message"
|
||||
},
|
||||
@ -2720,6 +2729,9 @@
|
||||
"Удалить контакт" : {
|
||||
"comment" : "Contacts context action delete"
|
||||
},
|
||||
"Удалить фото" : {
|
||||
"comment" : "Avatar delete"
|
||||
},
|
||||
"Удалить чат (скоро)" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
|
||||
@ -16,8 +16,12 @@ struct EditProfileView: View {
|
||||
@State private var isLoading = false
|
||||
@State private var isSaving = false
|
||||
@State private var isUploadingAvatar = false
|
||||
@State private var isPreparingDownload = false
|
||||
@State private var alertMessage: String?
|
||||
@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 descriptionLimit = 1024
|
||||
@ -35,6 +39,10 @@ struct EditProfileView: View {
|
||||
.scaledToFill()
|
||||
.frame(width: 120, height: 120)
|
||||
.clipShape(Circle())
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
presentAvatarViewer()
|
||||
}
|
||||
} else if let profile = profile,
|
||||
let fileId = profile.avatars?.current?.fileId,
|
||||
let url = avatarUrl(for: profile, fileId: fileId) {
|
||||
@ -44,8 +52,15 @@ struct EditProfileView: View {
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 120, height: 120)
|
||||
.clipShape(Circle())
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
presentAvatarViewer()
|
||||
}
|
||||
} else {
|
||||
avatarPlaceholder
|
||||
.onTapGesture {
|
||||
presentAvatarViewer()
|
||||
}
|
||||
}
|
||||
|
||||
Button("Изменить фото") {
|
||||
@ -112,6 +127,17 @@ struct EditProfileView: View {
|
||||
} message: { message in
|
||||
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 {
|
||||
Color.black.opacity(0.4).ignoresSafeArea()
|
||||
@ -162,13 +188,16 @@ struct EditProfileView: View {
|
||||
}
|
||||
|
||||
private var isBusy: Bool {
|
||||
isLoading || isSaving || isUploadingAvatar
|
||||
isLoading || isSaving || isUploadingAvatar || isPreparingDownload
|
||||
}
|
||||
|
||||
private var busyMessage: String {
|
||||
if isUploadingAvatar {
|
||||
return "Обновление аватара..."
|
||||
}
|
||||
if isPreparingDownload {
|
||||
return "Подготовка изображения..."
|
||||
}
|
||||
if isSaving {
|
||||
return "Сохранение..."
|
||||
}
|
||||
@ -256,6 +285,64 @@ struct EditProfileView: View {
|
||||
self.originalDisplayName = loadedName
|
||||
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 {
|
||||
@ -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