wake-ios/wake/View/Owner/UserInfo/AvatarPicker.swift

270 lines
11 KiB
Swift

import SwiftUI
public struct AvatarPicker: View {
@StateObject private var uploadManager = MediaUploadManager()
@State private var showMediaPicker = false
@State private var showImageCapture = false
@State private var isUploading = false
@Binding var selectedImage: UIImage?
@Binding var showUsername: Bool
@Binding var isKeyboardVisible: Bool
@Binding var uploadedFileId: String?
// Animation state
@State private var isAnimating = false
public init(selectedImage: Binding<UIImage?>,
showUsername: Binding<Bool>,
isKeyboardVisible: Binding<Bool>,
uploadedFileId: Binding<String?>) {
self._selectedImage = selectedImage
self._showUsername = showUsername
self._isKeyboardVisible = isKeyboardVisible
self._uploadedFileId = uploadedFileId
}
//
private var scaleFactor: CGFloat {
isKeyboardVisible ? 0.55 : 1.0
}
private var borderWidth: CGFloat {
isKeyboardVisible ? 3 : 4
}
//
private var animation: Animation {
.spring(response: 0.4, dampingFraction: 0.7, blendDuration: 0.3)
}
public var body: some View {
VStack(spacing: 20) {
VStack(spacing: 20) {
// Avatar Image Button
Button(action: {
withAnimation(animation) {
showMediaPicker = true
}
}) {
ZStack {
if let selectedImage = selectedImage {
Image(uiImage: selectedImage)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 225, height: 225)
.clipShape(RoundedRectangle(cornerRadius: 20))
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.themePrimary, lineWidth: borderWidth)
)
.scaleEffect(scaleFactor)
} else {
// Default SVG avatar with animated dashed border
SVGImageHtml(svgName: "IP")
.frame(width: 225, height: 225)
.scaleEffect(scaleFactor)
.contentShape(Rectangle())
.clipShape(RoundedRectangle(cornerRadius: 20 * scaleFactor))
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(style: StrokeStyle(
lineWidth: borderWidth,
lineCap: .round,
dash: [12, 8],
dashPhase: isAnimating ? 40 : 0
))
.foregroundColor(Color.themePrimary)
.scaleEffect(scaleFactor)
)
.onAppear {
withAnimation(Animation.linear(duration: 1.5).repeatForever(autoreverses: false)) {
isAnimating = true
}
}
}
// Upload indicator
if isUploading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .themePrimary))
.scaleEffect(1.5 * scaleFactor)
.frame(width: 225, height: 225)
.scaleEffect(scaleFactor)
.background(Color.black.opacity(0.3))
.clipShape(RoundedRectangle(cornerRadius: 20 * scaleFactor))
.transition(.opacity)
}
}
.frame(width: 225 * scaleFactor, height: 225 * scaleFactor)
.animation(animation, value: isKeyboardVisible)
}
.buttonStyle(ScaleButtonStyle())
// Upload Button (only shown when username is not shown)
if !showUsername {
Button(action: {
withAnimation(animation) {
showMediaPicker = true
}
}) {
Text("Upload from Gallery")
.font(Typography.font(for: .subtitle, family: .inter))
.fontWeight(.regular)
.frame(maxWidth: .infinity)
.padding()
.foregroundColor(.black)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color.themePrimaryLight)
)
.scaleEffect(scaleFactor)
}
.frame(maxWidth: .infinity)
.transition(.opacity.combined(with: .move(edge: .bottom)))
.animation(animation, value: isKeyboardVisible)
}
}
.animation(animation, value: isKeyboardVisible)
.sheet(isPresented: $showMediaPicker) {
MediaPicker(
selectedMedia: Binding(
get: { uploadManager.selectedMedia },
set: { newMedia in
// Only process if we have new media
if !newMedia.isEmpty {
uploadManager.clearAllMedia()
uploadManager.addMedia(newMedia)
// Start upload process
withAnimation(animation) {
isUploading = true
}
uploadManager.startUpload()
print("🔄 Upload started")
}
// Dismiss the picker after processing
showMediaPicker = false
}
),
imageSelectionLimit: 1,
videoSelectionLimit: 0,
allowedMediaTypes: .imagesOnly,
selectionMode: .single,
onDismiss: {
showMediaPicker = false
}
)
}
.onChange(of: uploadManager.uploadStatus) { _, status in
print("🔄 Upload status changed: ", status)
//
let pendingUploads = uploadManager.selectedMedia.filter { media in
guard let status = uploadManager.uploadStatus[media.id] else { return true }
return !status.isCompleted && !status.isUploading
}
if !pendingUploads.isEmpty {
print("🔄 Found \(pendingUploads.count) pending uploads, starting upload...")
uploadManager.startUpload()
}
//
for (mediaId, status) in status {
if case .completed(let fileId) = status {
print("✅ Found completed upload with fileId: ", fileId)
//
if let media = uploadManager.selectedMedia.first(where: { $0.id == mediaId }),
case .image(let image) = media {
print("🖼️ Updating selected image")
DispatchQueue.main.async {
withAnimation(animation) {
self.selectedImage = image
self.uploadedFileId = fileId
self.isUploading = false
}
//
self.uploadManager.clearAllMedia()
}
return
}
}
}
//
let hasFailures = status.values.contains {
if case .failed = $0 { return true }
return false
}
if hasFailures {
print("❌ Some uploads failed")
DispatchQueue.main.async {
withAnimation(animation) {
self.isUploading = false
}
}
}
}
if !showUsername {
Button(action: {
withAnimation(animation) {
showImageCapture = true
}
}) {
Text("Take a Photo")
.font(Typography.font(for: .subtitle, family: .inter))
.fontWeight(.regular)
.frame(maxWidth: .infinity)
.padding()
.foregroundColor(.black)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color.themePrimaryLight)
)
.scaleEffect(scaleFactor)
}
.frame(maxWidth: .infinity)
.animation(animation, value: isKeyboardVisible)
.sheet(isPresented: $showImageCapture) {
CustomCameraView(isPresented: $showImageCapture) { image in
selectedImage = image
uploadManager.clearAllMedia()
uploadManager.addMedia([.image(image)])
withAnimation(animation) {
isUploading = true
}
uploadManager.startUpload()
}
}
}
}
}
}
// Button style for scale effect
private struct ScaleButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
.animation(.easeInOut(duration: 0.2), value: configuration.isPressed)
}
}
// MARK: - Preview
struct AvatarPicker_Previews: PreviewProvider {
static var previews: some View {
AvatarPicker(
selectedImage: .constant(nil),
showUsername: .constant(false),
isKeyboardVisible: .constant(false),
uploadedFileId: .constant(nil)
)
.padding()
.background(Color.gray.opacity(0.1))
.previewLayout(.sizeThatFits)
}
}