add view img

This commit is contained in:
cheykrym 2025-12-10 02:49:56 +03:00
parent 134ba39914
commit 13a1d00934
2 changed files with 204 additions and 1 deletions

View File

@ -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" : {

View File

@ -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) {}
}