287 lines
12 KiB
Swift
287 lines
12 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 {
|
||
// 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)
|
||
}
|
||
} |