photo view
This commit is contained in:
parent
947a504f08
commit
9013549362
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user