photo view
This commit is contained in:
parent
947a504f08
commit
9013549362
@ -288,14 +288,29 @@ struct EditProfileView: View {
|
|||||||
|
|
||||||
private func presentAvatarViewer() {
|
private func presentAvatarViewer() {
|
||||||
if let image = avatarImage {
|
if let image = avatarImage {
|
||||||
avatarViewerState = AvatarViewerState(source: .local(image))
|
avatarViewerState = AvatarViewerState(
|
||||||
|
source: .local(image),
|
||||||
|
intrinsicSize: image.size
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let profile,
|
guard let profile,
|
||||||
let fileId = profile.avatars?.current?.fileId,
|
let fileId = profile.avatars?.current?.fileId,
|
||||||
let url = avatarUrl(for: profile, fileId: fileId) else { return }
|
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) {
|
private func handleAvatarDownload(for state: AvatarViewerState) {
|
||||||
@ -389,6 +404,7 @@ struct AvatarViewerState: Identifiable {
|
|||||||
|
|
||||||
let id = UUID()
|
let id = UUID()
|
||||||
let source: Source
|
let source: Source
|
||||||
|
let intrinsicSize: CGSize?
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AvatarViewerError: LocalizedError {
|
enum AvatarViewerError: LocalizedError {
|
||||||
@ -414,6 +430,8 @@ struct AvatarViewerView: View {
|
|||||||
@State private var storedPanOffset: CGSize = .zero
|
@State private var storedPanOffset: CGSize = .zero
|
||||||
@State private var dismissOffset: CGSize = .zero
|
@State private var dismissOffset: CGSize = .zero
|
||||||
@State private var dragMode: DragMode?
|
@State private var dragMode: DragMode?
|
||||||
|
@State private var containerSize: CGSize = .zero
|
||||||
|
@State private var loadedImageSize: CGSize?
|
||||||
|
|
||||||
private enum DragMode {
|
private enum DragMode {
|
||||||
case vertical
|
case vertical
|
||||||
@ -438,6 +456,10 @@ struct AvatarViewerView: View {
|
|||||||
Double(1 - dragProgress * 0.8)
|
Double(1 - dragProgress * 0.8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var effectiveImageSize: CGSize? {
|
||||||
|
loadedImageSize ?? state.intrinsicSize
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.black.opacity(backgroundOpacity).ignoresSafeArea()
|
Color.black.opacity(backgroundOpacity).ignoresSafeArea()
|
||||||
@ -488,27 +510,35 @@ struct AvatarViewerView: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var zoomableContent: some View {
|
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 {
|
switch state.source {
|
||||||
case .local(let image):
|
case .local(let image):
|
||||||
zoomableImage(Image(uiImage: image))
|
zoomableImage(Image(uiImage: image))
|
||||||
case .remote(let url, _, _):
|
.onAppear {
|
||||||
AsyncImage(url: url) { phase in
|
loadedImageSize = image.size
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
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()
|
.scaledToFit()
|
||||||
.offset(currentOffset)
|
.offset(currentOffset)
|
||||||
.scaleEffect(scale, anchor: .center)
|
.scaleEffect(scale, anchor: .center)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
.gesture(dragGesture)
|
.gesture(dragGesture)
|
||||||
.simultaneousGesture(magnificationGesture)
|
.simultaneousGesture(magnificationGesture)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var dragGesture: some Gesture {
|
private var dragGesture: some Gesture {
|
||||||
@ -536,6 +566,7 @@ struct AvatarViewerView: View {
|
|||||||
width: storedPanOffset.width + adjustedTranslation.width,
|
width: storedPanOffset.width + adjustedTranslation.width,
|
||||||
height: storedPanOffset.height + adjustedTranslation.height
|
height: storedPanOffset.height + adjustedTranslation.height
|
||||||
)
|
)
|
||||||
|
panOffset = clampedOffset(panOffset)
|
||||||
} else {
|
} else {
|
||||||
if dragMode == nil {
|
if dragMode == nil {
|
||||||
if abs(value.translation.height) > abs(value.translation.width) {
|
if abs(value.translation.height) > abs(value.translation.width) {
|
||||||
@ -560,10 +591,12 @@ struct AvatarViewerView: View {
|
|||||||
width: value.translation.width / scale,
|
width: value.translation.width / scale,
|
||||||
height: value.translation.height / scale
|
height: value.translation.height / scale
|
||||||
)
|
)
|
||||||
storedPanOffset = CGSize(
|
var newOffset = CGSize(
|
||||||
width: storedPanOffset.width + adjustedTranslation.width,
|
width: storedPanOffset.width + adjustedTranslation.width,
|
||||||
height: storedPanOffset.height + adjustedTranslation.height
|
height: storedPanOffset.height + adjustedTranslation.height
|
||||||
)
|
)
|
||||||
|
newOffset = clampedOffset(newOffset)
|
||||||
|
storedPanOffset = newOffset
|
||||||
} else {
|
} else {
|
||||||
if abs(value.translation.height) > 120 {
|
if abs(value.translation.height) > 120 {
|
||||||
onClose()
|
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 {
|
struct ActivityView: UIViewControllerRepresentable {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user