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 headline //
case title //
case title2 //
case body //
case subtitle //
case caption //
@ -39,13 +40,14 @@ struct Typography {
// MARK: -
///
private static let defaultFontFamily: FontFamily = .quicksand
private static let defaultFontFamily: FontFamily = .quicksandRegular
///
private static let styleConfig: [TypographyStyle: TypographyConfig] = [
.largeTitle: TypographyConfig(size: 32, weight: .heavy, textStyle: .largeTitle),
.headline: TypographyConfig(size: 24, weight: .bold, textStyle: .headline),
.title: TypographyConfig(size: 20, weight: .semibold, textStyle: .title2),
.title2: TypographyConfig(size: 18, weight: .bold, textStyle: .title2),
.body: TypographyConfig(size: 16, weight: .regular, textStyle: .body),
.subtitle: TypographyConfig(size: 14, weight: .medium, textStyle: .subheadline),
.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 {
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) {
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)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 10)
@ -31,59 +42,32 @@ struct UserInfo: View {
)
)
}
.padding(.vertical, 10)
.padding(10)
Spacer()
VStack(spacing: 20) {
// Title
Text("Add Your Avatar")
.font(Typography.font(for: .title))
.font(Typography.font(for: .body, family: .quicksandBold))
.frame(maxWidth: .infinity, alignment: .center)
// Avatar
ZStack {
// Show either the SVG or the uploaded image
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))
)
AvatarPicker(selectedImage: $avatarImage)
}
.padding(.top, 20)
Button(action: {
// Action for second button
}) {
Text("Take a Photo")
.font(Typography.font(for: .subtitle, family: .inter))
.fontWeight(.regular)
.frame(maxWidth: .infinity)
.padding()
.foregroundColor(.black)
.background(
RoundedRectangle(cornerRadius: 25)
.fill(Color(red: 1.0, green: 0.973, blue: 0.871))
RoundedRectangle(cornerRadius: 16)
.fill(Color.themePrimaryLight)
)
}
}
@ -94,30 +78,22 @@ struct UserInfo: View {
Button(action: {
// Action for next button
}) {
Text("Next")
Text("Continue")
.font(Typography.font(for: .body))
.fontWeight(.bold)
.frame(maxWidth: .infinity)
.padding()
.padding(16)
.foregroundColor(.black)
.background(
RoundedRectangle(cornerRadius: 25)
.fill(Color(red: 1.0, green: 0.714, blue: 0.271))
.fill(Color.themePrimary)
)
}
.padding()
}
.padding()
.navigationTitle("Complete Your Profile")
.navigationBarTitleDisplayMode(.inline)
.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.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 = """
<!DOCTYPE html>
<html>
@ -24,47 +37,32 @@ struct SVGImage: UIViewRepresentable {
width: 100%;
height: 100%;
overflow: hidden;
background-color: transparent;
}
svg {
width: 100%;
height: 100%;
display: block;
}
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
</style>
</head>
<body>
<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>
</body>
</html>
"""
webView.loadHTMLString(htmlString, baseURL: nil)
}
// 4. HTML
webView.loadHTMLString(htmlString, baseURL: fileURL.deletingLastPathComponent())
return webView
}
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)
} else {
//
ContentView()
// ContentView()
// .environmentObject(authState)
UserInfo()
.environmentObject(authState)
}
}