Compare commits
No commits in common. "9013549362420d01920e604c9fa8225dff08815f" and "f1ec7e637bf14567b49e38a78cf421f0f88a31fe" have entirely different histories.
9013549362
...
f1ec7e637b
@ -76,9 +76,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"1 из 1" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"2FA включена" : {
|
"2FA включена" : {
|
||||||
"comment" : "Заголовок уведомления об успешной активации 2FA"
|
"comment" : "Заголовок уведомления об успешной активации 2FA"
|
||||||
@ -1285,9 +1282,6 @@
|
|||||||
"Не удалось подготовить изображение для загрузки." : {
|
"Не удалось подготовить изображение для загрузки." : {
|
||||||
"comment" : "Avatar encoding error"
|
"comment" : "Avatar encoding error"
|
||||||
},
|
},
|
||||||
"Не удалось подготовить изображение." : {
|
|
||||||
"comment" : "Avatar decoding error"
|
|
||||||
},
|
|
||||||
"Не удалось сериализовать данные запроса." : {
|
"Не удалось сериализовать данные запроса." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -2476,9 +2470,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Скачать" : {
|
|
||||||
"comment" : "Avatar download"
|
|
||||||
},
|
|
||||||
"Скопировано" : {
|
"Скопировано" : {
|
||||||
"comment" : "Заголовок уведомления о копировании"
|
"comment" : "Заголовок уведомления о копировании"
|
||||||
},
|
},
|
||||||
@ -2720,9 +2711,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Удаление аватара пока недоступно." : {
|
|
||||||
"comment" : "Avatar delete placeholder"
|
|
||||||
},
|
|
||||||
"Удаление контакта \"%1$@\" появится позже." : {
|
"Удаление контакта \"%1$@\" появится позже." : {
|
||||||
"comment" : "Contacts delete placeholder message"
|
"comment" : "Contacts delete placeholder message"
|
||||||
},
|
},
|
||||||
@ -2732,9 +2720,6 @@
|
|||||||
"Удалить контакт" : {
|
"Удалить контакт" : {
|
||||||
"comment" : "Contacts context action delete"
|
"comment" : "Contacts context action delete"
|
||||||
},
|
},
|
||||||
"Удалить фото" : {
|
|
||||||
"comment" : "Avatar delete"
|
|
||||||
},
|
|
||||||
"Удалить чат (скоро)" : {
|
"Удалить чат (скоро)" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
|||||||
@ -16,12 +16,8 @@ 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
|
||||||
@ -39,10 +35,6 @@ 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) {
|
||||||
@ -52,15 +44,8 @@ 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("Изменить фото") {
|
||||||
@ -114,7 +99,7 @@ struct EditProfileView: View {
|
|||||||
.navigationTitle("Профиль")
|
.navigationTitle("Профиль")
|
||||||
.onAppear(perform: loadProfile)
|
.onAppear(perform: loadProfile)
|
||||||
.sheet(isPresented: $showImagePicker) {
|
.sheet(isPresented: $showImagePicker) {
|
||||||
ImagePicker(image: $avatarImage, allowsEditing: true)
|
ImagePicker(image: $avatarImage)
|
||||||
}
|
}
|
||||||
.onChange(of: avatarImage) { newValue in
|
.onChange(of: avatarImage) { newValue in
|
||||||
guard let image = newValue else { return }
|
guard let image = newValue else { return }
|
||||||
@ -127,17 +112,6 @@ 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()
|
||||||
@ -188,16 +162,13 @@ struct EditProfileView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var isBusy: Bool {
|
private var isBusy: Bool {
|
||||||
isLoading || isSaving || isUploadingAvatar || isPreparingDownload
|
isLoading || isSaving || isUploadingAvatar
|
||||||
}
|
}
|
||||||
|
|
||||||
private var busyMessage: String {
|
private var busyMessage: String {
|
||||||
if isUploadingAvatar {
|
if isUploadingAvatar {
|
||||||
return "Обновление аватара..."
|
return "Обновление аватара..."
|
||||||
}
|
}
|
||||||
if isPreparingDownload {
|
|
||||||
return "Подготовка изображения..."
|
|
||||||
}
|
|
||||||
if isSaving {
|
if isSaving {
|
||||||
return "Сохранение..."
|
return "Сохранение..."
|
||||||
}
|
}
|
||||||
@ -285,90 +256,15 @@ 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),
|
|
||||||
intrinsicSize: image.size
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let profile,
|
|
||||||
let fileId = profile.avatars?.current?.fileId,
|
|
||||||
let url = avatarUrl(for: profile, fileId: fileId) else { return }
|
|
||||||
let intrinsicSize: CGSize?
|
|
||||||
if let width = profile.avatars?.current?.width,
|
|
||||||
let height = profile.avatars?.current?.height,
|
|
||||||
width > 0,
|
|
||||||
height > 0 {
|
|
||||||
intrinsicSize = CGSize(width: CGFloat(width), height: CGFloat(height))
|
|
||||||
} else {
|
|
||||||
intrinsicSize = nil
|
|
||||||
}
|
|
||||||
avatarViewerState = AvatarViewerState(
|
|
||||||
source: .remote(url: url, fileId: fileId, userId: profile.userId.uuidString),
|
|
||||||
intrinsicSize: intrinsicSize
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
||||||
@Binding var image: UIImage?
|
@Binding var image: UIImage?
|
||||||
var allowsEditing: Bool = false
|
|
||||||
@Environment(\.presentationMode) private var presentationMode
|
@Environment(\.presentationMode) private var presentationMode
|
||||||
|
|
||||||
func makeUIViewController(context: Context) -> UIImagePickerController {
|
func makeUIViewController(context: Context) -> UIImagePickerController {
|
||||||
let picker = UIImagePickerController()
|
let picker = UIImagePickerController()
|
||||||
picker.delegate = context.coordinator
|
picker.delegate = context.coordinator
|
||||||
picker.allowsEditing = allowsEditing
|
|
||||||
return picker
|
return picker
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -386,313 +282,10 @@ struct ImagePicker: UIViewControllerRepresentable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
|
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
|
||||||
if let editedImage = info[.editedImage] as? UIImage {
|
if let uiImage = info[.originalImage] as? UIImage {
|
||||||
parent.image = editedImage
|
|
||||||
} else if let uiImage = info[.originalImage] as? UIImage {
|
|
||||||
parent.image = uiImage
|
parent.image = uiImage
|
||||||
}
|
}
|
||||||
parent.presentationMode.wrappedValue.dismiss()
|
parent.presentationMode.wrappedValue.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AvatarViewerState: Identifiable {
|
|
||||||
enum Source {
|
|
||||||
case local(UIImage)
|
|
||||||
case remote(url: URL, fileId: String, userId: String)
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = UUID()
|
|
||||||
let source: Source
|
|
||||||
let intrinsicSize: CGSize?
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
@State private var scale: CGFloat = 1.0
|
|
||||||
@State private var baseScale: CGFloat = 1.0
|
|
||||||
@State private var panOffset: CGSize = .zero
|
|
||||||
@State private var storedPanOffset: CGSize = .zero
|
|
||||||
@State private var dismissOffset: CGSize = .zero
|
|
||||||
@State private var dragMode: DragMode?
|
|
||||||
@State private var containerSize: CGSize = .zero
|
|
||||||
@State private var loadedImageSize: CGSize?
|
|
||||||
|
|
||||||
private enum DragMode {
|
|
||||||
case vertical
|
|
||||||
case horizontal
|
|
||||||
}
|
|
||||||
|
|
||||||
private var currentOffset: CGSize {
|
|
||||||
scale > 1.05 ? panOffset : dismissOffset
|
|
||||||
}
|
|
||||||
|
|
||||||
private var dragProgress: CGFloat {
|
|
||||||
guard scale <= 1.05 else { return 0 }
|
|
||||||
let progress = min(1, abs(dismissOffset.height) / 220)
|
|
||||||
return progress
|
|
||||||
}
|
|
||||||
|
|
||||||
private var backgroundOpacity: Double {
|
|
||||||
Double(1 - dragProgress * 0.6)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var overlayOpacity: Double {
|
|
||||||
Double(1 - dragProgress * 0.8)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var effectiveImageSize: CGSize? {
|
|
||||||
loadedImageSize ?? state.intrinsicSize
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack {
|
|
||||||
Color.black.opacity(backgroundOpacity).ignoresSafeArea()
|
|
||||||
|
|
||||||
zoomableContent
|
|
||||||
|
|
||||||
topOverlay
|
|
||||||
.opacity(overlayOpacity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var topOverlay: some View {
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
HStack {
|
|
||||||
Button(action: onClose) {
|
|
||||||
Image(systemName: "xmark")
|
|
||||||
.imageScale(.large)
|
|
||||||
}
|
|
||||||
.tint(.white)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Text("1 из 1")
|
|
||||||
.font(.subheadline.weight(.semibold))
|
|
||||||
.foregroundColor(.white.opacity(0.8))
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.top, 24)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var zoomableContent: some View {
|
|
||||||
GeometryReader { proxy in
|
|
||||||
let size = proxy.size
|
|
||||||
Color.clear
|
|
||||||
.onAppear { containerSize = size }
|
|
||||||
.onChange(of: size) { newValue in
|
|
||||||
containerSize = newValue
|
|
||||||
}
|
|
||||||
.overlay {
|
|
||||||
content(for: size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func content(for size: CGSize) -> some View {
|
|
||||||
switch state.source {
|
|
||||||
case .local(let image):
|
|
||||||
zoomableImage(Image(uiImage: image))
|
|
||||||
.onAppear {
|
|
||||||
loadedImageSize = image.size
|
|
||||||
}
|
|
||||||
case .remote(let url, let fileId, let userId):
|
|
||||||
RemoteZoomableImage(url: url, fileId: fileId, userId: userId) { uiImage in
|
|
||||||
loadedImageSize = uiImage.size
|
|
||||||
}
|
|
||||||
.offset(currentOffset)
|
|
||||||
.scaleEffect(scale, anchor: .center)
|
|
||||||
.gesture(dragGesture)
|
|
||||||
.simultaneousGesture(magnificationGesture)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func zoomableImage(_ image: Image) -> some View {
|
|
||||||
image
|
|
||||||
.resizable()
|
|
||||||
.scaledToFit()
|
|
||||||
.offset(currentOffset)
|
|
||||||
.scaleEffect(scale, anchor: .center)
|
|
||||||
.gesture(dragGesture)
|
|
||||||
.simultaneousGesture(magnificationGesture)
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var dragGesture: some Gesture {
|
|
||||||
DragGesture()
|
|
||||||
.onChanged { value in
|
|
||||||
if scale > 1.05 {
|
|
||||||
dismissOffset = .zero
|
|
||||||
let adjustedTranslation = CGSize(
|
|
||||||
width: value.translation.width / scale,
|
|
||||||
height: value.translation.height / scale
|
|
||||||
)
|
|
||||||
panOffset = CGSize(
|
|
||||||
width: storedPanOffset.width + adjustedTranslation.width,
|
|
||||||
height: storedPanOffset.height + adjustedTranslation.height
|
|
||||||
)
|
|
||||||
panOffset = clampedOffset(panOffset)
|
|
||||||
} else {
|
|
||||||
if dragMode == nil {
|
|
||||||
if abs(value.translation.height) > abs(value.translation.width) {
|
|
||||||
dragMode = .vertical
|
|
||||||
} else {
|
|
||||||
dragMode = .horizontal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch dragMode {
|
|
||||||
case .horizontal:
|
|
||||||
let limitedWidth = min(max(value.translation.width, -80), 80)
|
|
||||||
dismissOffset = CGSize(width: limitedWidth, height: 0)
|
|
||||||
case .vertical, .none:
|
|
||||||
dismissOffset = CGSize(width: 0, height: value.translation.height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onEnded { value in
|
|
||||||
if scale > 1.05 {
|
|
||||||
let adjustedTranslation = CGSize(
|
|
||||||
width: value.translation.width / scale,
|
|
||||||
height: value.translation.height / scale
|
|
||||||
)
|
|
||||||
var newOffset = CGSize(
|
|
||||||
width: storedPanOffset.width + adjustedTranslation.width,
|
|
||||||
height: storedPanOffset.height + adjustedTranslation.height
|
|
||||||
)
|
|
||||||
newOffset = clampedOffset(newOffset)
|
|
||||||
storedPanOffset = newOffset
|
|
||||||
} else {
|
|
||||||
if abs(value.translation.height) > 120 {
|
|
||||||
onClose()
|
|
||||||
} else {
|
|
||||||
withAnimation(.spring()) {
|
|
||||||
dismissOffset = .zero
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dragMode = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var magnificationGesture: some Gesture {
|
|
||||||
MagnificationGesture()
|
|
||||||
.onChanged { value in
|
|
||||||
let newScale = baseScale * value
|
|
||||||
scale = min(max(newScale, 1), 4)
|
|
||||||
}
|
|
||||||
.onEnded { _ in
|
|
||||||
baseScale = scale
|
|
||||||
if baseScale <= 1.02 {
|
|
||||||
baseScale = 1
|
|
||||||
withAnimation(.spring()) {
|
|
||||||
scale = 1
|
|
||||||
storedPanOffset = .zero
|
|
||||||
panOffset = .zero
|
|
||||||
dismissOffset = .zero
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func clampedOffset(_ offset: CGSize) -> CGSize {
|
|
||||||
guard scale > 1.01,
|
|
||||||
containerSize != .zero else { return offset }
|
|
||||||
|
|
||||||
let fittedSize = fittedContentSize(in: containerSize)
|
|
||||||
let scaledWidth = fittedSize.width * scale
|
|
||||||
let scaledHeight = fittedSize.height * scale
|
|
||||||
let maxX = max(0, (scaledWidth - containerSize.width) / 2)
|
|
||||||
let maxY = max(0, (scaledHeight - containerSize.height) / 2)
|
|
||||||
|
|
||||||
let clampedX = max(-maxX, min(offset.width, maxX))
|
|
||||||
let clampedY = max(-maxY, min(offset.height, maxY))
|
|
||||||
return CGSize(width: clampedX, height: clampedY)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func fittedContentSize(in container: CGSize) -> CGSize {
|
|
||||||
guard let imageSize = effectiveImageSize,
|
|
||||||
imageSize.width > 0,
|
|
||||||
imageSize.height > 0 else {
|
|
||||||
return container
|
|
||||||
}
|
|
||||||
|
|
||||||
let widthRatio = container.width / imageSize.width
|
|
||||||
let heightRatio = container.height / imageSize.height
|
|
||||||
let ratio = min(widthRatio, heightRatio)
|
|
||||||
return CGSize(width: imageSize.width * ratio, height: imageSize.height * ratio)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct RemoteZoomableImage: View {
|
|
||||||
@StateObject private var loader: ImageLoader
|
|
||||||
let onImageLoaded: (UIImage) -> Void
|
|
||||||
|
|
||||||
init(url: URL, fileId: String, userId: String, onImageLoaded: @escaping (UIImage) -> Void) {
|
|
||||||
_loader = StateObject(wrappedValue: ImageLoader(url: url, fileId: fileId, userId: userId))
|
|
||||||
self.onImageLoaded = onImageLoaded
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Group {
|
|
||||||
if let image = loader.image {
|
|
||||||
Image(uiImage: image)
|
|
||||||
.resizable()
|
|
||||||
.scaledToFit()
|
|
||||||
.onAppear {
|
|
||||||
onImageLoaded(image)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ProgressView()
|
|
||||||
.progressViewStyle(.circular)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
.onAppear(perform: loader.load)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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