photo view

This commit is contained in:
cheykrym 2025-12-10 03:18:32 +03:00
parent 947a504f08
commit 9013549362

View File

@ -288,14 +288,29 @@ struct EditProfileView: View {
private func presentAvatarViewer() {
if let image = avatarImage {
avatarViewerState = AvatarViewerState(source: .local(image))
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 }
avatarViewerState = AvatarViewerState(source: .remote(url: url, fileId: fileId, userId: profile.userId.uuidString))
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) {
@ -389,6 +404,7 @@ struct AvatarViewerState: Identifiable {
let id = UUID()
let source: Source
let intrinsicSize: CGSize?
}
enum AvatarViewerError: LocalizedError {
@ -414,6 +430,8 @@ struct AvatarViewerView: View {
@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
@ -438,6 +456,10 @@ struct AvatarViewerView: View {
Double(1 - dragProgress * 0.8)
}
private var effectiveImageSize: CGSize? {
loadedImageSize ?? state.intrinsicSize
}
var body: some View {
ZStack {
Color.black.opacity(backgroundOpacity).ignoresSafeArea()
@ -488,27 +510,35 @@ struct AvatarViewerView: View {
@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))
case .remote(let url, _, _):
AsyncImage(url: url) { phase in
switch phase {
case .empty:
ProgressView()
.progressViewStyle(.circular)
case .success(let image):
zoomableImage(image)
case .failure:
Image(systemName: "person.crop.circle.badge.exclam")
.resizable()
.scaledToFit()
.frame(width: 120, height: 120)
.foregroundColor(.white.opacity(0.7))
@unknown default:
EmptyView()
.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)
}
}
@ -518,9 +548,9 @@ struct AvatarViewerView: View {
.scaledToFit()
.offset(currentOffset)
.scaleEffect(scale, anchor: .center)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.gesture(dragGesture)
.simultaneousGesture(magnificationGesture)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
private var dragGesture: some Gesture {
@ -536,6 +566,7 @@ struct AvatarViewerView: View {
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) {
@ -560,10 +591,12 @@ struct AvatarViewerView: View {
width: value.translation.width / scale,
height: value.translation.height / scale
)
storedPanOffset = CGSize(
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()
@ -596,6 +629,62 @@ struct AvatarViewerView: View {
}
}
}
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 {