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

287 lines
12 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 {
// SwiftUI + 线 + 线
ZStack {
RoundedRectangle(cornerRadius: 20)
.fill(Color.white)
.frame(width: 225, height: 225)
Image(systemName: "plus")
.font(.system(size: 32, weight: .bold))
.foregroundColor(.black)
}
.scaleEffect(scaleFactor)
.contentShape(Rectangle())
.clipShape(RoundedRectangle(cornerRadius: 20 * scaleFactor))
.overlay(alignment: .bottomTrailing) {
Image("IP")
.resizable()
.scaledToFit()
.frame(width: 80, height: 80)
.padding(.bottom, 10)
.padding(.trailing, 18)
}
.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)
.padding(.top, Theme.Spacing.md)
}
}
.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()
}
}
.padding(.bottom, Theme.Spacing.md)
}
}
}
}
// 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)
}
}