feat: 头像选择上传

This commit is contained in:
jinyaqiu 2025-08-21 16:02:27 +08:00
parent 5df804d115
commit 44de40cf83
6 changed files with 165 additions and 103 deletions

BIN
wake/Assets/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -20,6 +20,7 @@ enum TypographyStyle {
case largeTitle // case largeTitle //
case headline // case headline //
case title // case title //
case title2 //
case body // case body //
case subtitle // case subtitle //
case caption // case caption //
@ -39,13 +40,14 @@ struct Typography {
// MARK: - // MARK: -
/// ///
private static let defaultFontFamily: FontFamily = .quicksand private static let defaultFontFamily: FontFamily = .quicksandRegular
/// ///
private static let styleConfig: [TypographyStyle: TypographyConfig] = [ private static let styleConfig: [TypographyStyle: TypographyConfig] = [
.largeTitle: TypographyConfig(size: 32, weight: .heavy, textStyle: .largeTitle), .largeTitle: TypographyConfig(size: 32, weight: .heavy, textStyle: .largeTitle),
.headline: TypographyConfig(size: 24, weight: .bold, textStyle: .headline), .headline: TypographyConfig(size: 24, weight: .bold, textStyle: .headline),
.title: TypographyConfig(size: 20, weight: .semibold, textStyle: .title2), .title: TypographyConfig(size: 20, weight: .semibold, textStyle: .title2),
.title2: TypographyConfig(size: 18, weight: .bold, textStyle: .title2),
.body: TypographyConfig(size: 16, weight: .regular, textStyle: .body), .body: TypographyConfig(size: 16, weight: .regular, textStyle: .body),
.subtitle: TypographyConfig(size: 14, weight: .medium, textStyle: .subheadline), .subtitle: TypographyConfig(size: 14, weight: .medium, textStyle: .subheadline),
.caption: TypographyConfig(size: 12, weight: .regular, textStyle: .caption1), .caption: TypographyConfig(size: 12, weight: .regular, textStyle: .caption1),

View File

@ -0,0 +1,84 @@
import SwiftUI
public struct AvatarPicker: View {
@StateObject private var uploadManager = MediaUploadManager()
@State private var showMediaPicker = false
@State private var isUploading = false
@Binding var selectedImage: UIImage?
public init(selectedImage: Binding<UIImage?>) {
self._selectedImage = selectedImage
}
public var body: some View {
VStack(spacing: 20) {
// Avatar Image
Button(action: {
showMediaPicker = true
}) {
ZStack {
if let selectedImage = selectedImage {
Image(uiImage: selectedImage)
.resizable()
.scaledToFill()
.frame(width: 225, height: 225)
.clipShape(RoundedRectangle(cornerRadius: 20))
} else {
// Default SVG avatar
SVGImage(svgName: "Avatar")
.frame(width: 225, height: 225)
.contentShape(Rectangle())
}
if isUploading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.scaleEffect(1.5)
}
}
}
// Upload button
Button(action: {
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)
)
}
.frame(width: .infinity)
}
.sheet(isPresented: $showMediaPicker) {
MediaPicker(
selectedMedia: $uploadManager.selectedMedia,
imageSelectionLimit: 1,
videoSelectionLimit: 0,
allowedMediaTypes: .imagesOnly,
selectionMode: .single,
onDismiss: {
showMediaPicker = false
if !uploadManager.selectedMedia.isEmpty {
isUploading = true
uploadManager.startUpload()
}
}
)
}
.onChange(of: uploadManager.uploadStatus) { _ in
if let firstMedia = uploadManager.selectedMedia.first,
case .image(let image) = firstMedia,
uploadManager.isAllUploaded {
selectedImage = image
isUploading = false
uploadManager.clearAllMedia()
}
}
}
}

View File

@ -13,9 +13,20 @@ struct UserInfo: View {
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack {
ReturnButton {
print("返回")
}
Spacer()
Text("Complete Your Profile")
.font(Typography.font(for: .title2, family: .quicksandBold))
.foregroundColor(.themeTextMessageMain)
Spacer()
}
.padding()
HStack(spacing: 20) { HStack(spacing: 20) {
Text("Choose a photo as your avatar, and we'll generate a video mystery box for you.") Text("Choose a photo as your avatar, and we'll generate a video mystery box for you.")
.font(Typography.font(for: .small)) .font(Typography.font(for: .caption))
.foregroundColor(.black) .foregroundColor(.black)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 10) .padding(.vertical, 10)
@ -31,59 +42,32 @@ struct UserInfo: View {
) )
) )
} }
.padding(.vertical, 10) .padding(10)
Spacer() Spacer()
VStack(spacing: 20) { VStack(spacing: 20) {
// Title // Title
Text("Add Your Avatar") Text("Add Your Avatar")
.font(Typography.font(for: .title)) .font(Typography.font(for: .body, family: .quicksandBold))
.frame(maxWidth: .infinity, alignment: .center) .frame(maxWidth: .infinity, alignment: .center)
// Avatar // Avatar
ZStack { ZStack {
// Show either the SVG or the uploaded image AvatarPicker(selectedImage: $avatarImage)
if let avatarImage = avatarImage {
Image(uiImage: avatarImage)
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.clipShape(Circle())
} else {
SVGImage(svgName: "Avatar")
.frame(width: 200, height: 200)
}
// Make sure the AvatarUploader is on top and covers the entire area
AvatarUploader(selectedImage: $avatarImage, size: 200)
.contentShape(Rectangle()) // This makes the entire area tappable
}
.frame(width: 200, height: 200)
.padding(.vertical, 20)
// Buttons
Button(action: {
// Action for first button
}) {
Text("Upload from Gallery")
.frame(maxWidth: .infinity)
.padding()
.foregroundColor(.black)
.background(
RoundedRectangle(cornerRadius: 25)
.fill(Color(red: 1.0, green: 0.973, blue: 0.871))
)
} }
.padding(.top, 20)
Button(action: { Button(action: {
// Action for second button // Action for second button
}) { }) {
Text("Take a Photo") Text("Take a Photo")
.font(Typography.font(for: .subtitle, family: .inter))
.fontWeight(.regular)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding() .padding()
.foregroundColor(.black) .foregroundColor(.black)
.background( .background(
RoundedRectangle(cornerRadius: 25) RoundedRectangle(cornerRadius: 16)
.fill(Color(red: 1.0, green: 0.973, blue: 0.871)) .fill(Color.themePrimaryLight)
) )
} }
} }
@ -94,30 +78,22 @@ struct UserInfo: View {
Button(action: { Button(action: {
// Action for next button // Action for next button
}) { }) {
Text("Next") Text("Continue")
.font(Typography.font(for: .body))
.fontWeight(.bold)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding() .padding(16)
.foregroundColor(.black) .foregroundColor(.black)
.background( .background(
RoundedRectangle(cornerRadius: 25) RoundedRectangle(cornerRadius: 25)
.fill(Color(red: 1.0, green: 0.714, blue: 0.271)) .fill(Color.themePrimary)
) )
} }
.padding()
} }
.padding() .padding()
.navigationTitle("Complete Your Profile")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.background(Color(red: 0.98, green: 0.98, blue: 0.98)) // #FAFAFA .background(Color(red: 0.98, green: 0.98, blue: 0.98)) // #FAFAFA
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
dismiss()
}) {
Image(systemName: "chevron.left")
.foregroundColor(.blue)
}
}
}
} }
} }

View File

@ -11,7 +11,20 @@ struct SVGImage: UIViewRepresentable {
webView.scrollView.isScrollEnabled = false webView.scrollView.isScrollEnabled = false
webView.scrollView.contentInsetAdjustmentBehavior = .never webView.scrollView.contentInsetAdjustmentBehavior = .never
if let url = Bundle.main.url(forResource: svgName, withExtension: "svg") { // 1. SVG inDirectory
guard let path = Bundle.main.path(forResource: svgName, ofType: "svg") else {
print("❌ 无法找到 SVG 文件: \(svgName).svg")
//
if let resourcePath = Bundle.main.resourcePath {
print("可用的资源文件: \(try? FileManager.default.contentsOfDirectory(atPath: resourcePath))")
}
return webView
}
// 2. URL
let fileURL = URL(fileURLWithPath: path)
// 3. HTML
let htmlString = """ let htmlString = """
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
@ -24,47 +37,32 @@ struct SVGImage: UIViewRepresentable {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
background-color: transparent;
} }
svg { svg {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block; display: block;
} }
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
</style> </style>
</head> </head>
<body> <body>
<div style="width:100%; height:100%; display:flex; align-items:center; justify-content:center;"> <div style="width:100%; height:100%; display:flex; align-items:center; justify-content:center;">
<img src="\(url.absoluteString)" style="max-width:100%; max-height:100%;" /> <img src="\(fileURL.lastPathComponent)" />
</div> </div>
</body> </body>
</html> </html>
""" """
webView.loadHTMLString(htmlString, baseURL: nil) // 4. HTML
} webView.loadHTMLString(htmlString, baseURL: fileURL.deletingLastPathComponent())
return webView return webView
} }
func updateUIView(_ uiView: WKWebView, context: Context) {} func updateUIView(_ uiView: WKWebView, context: Context) {}
} }
// MARK: - View Modifier for SVG Image
struct SVGImageModifier: ViewModifier {
let size: CGSize
func body(content: Content) -> some View {
content
.frame(width: size.width, height: size.height)
.aspectRatio(contentMode: .fit)
}
}
extension View {
func svgImageStyle(size: CGSize) -> some View {
self.modifier(SVGImageModifier(size: size))
}
}
// Usage:
// SVGImage(svgName: "Avatar")

View File

@ -48,7 +48,9 @@ struct WakeApp: App {
.environmentObject(authState) .environmentObject(authState)
} else { } else {
// //
ContentView() // ContentView()
// .environmentObject(authState)
UserInfo()
.environmentObject(authState) .environmentObject(authState)
} }
} }