From 2eee2486e18081e54c773ae09edbf0277f6b8ca7 Mon Sep 17 00:00:00 2001 From: jinyaqiu Date: Sun, 24 Aug 2025 16:34:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=8A=E4=BC=A0=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/Utils/Router.swift | 4 + wake/View/Feedback.swift | 33 ++- wake/View/Upload/MediaUploadView.swift | 384 +++++++++++++++++++++++++ 3 files changed, 404 insertions(+), 17 deletions(-) create mode 100644 wake/View/Upload/MediaUploadView.swift diff --git a/wake/Utils/Router.swift b/wake/Utils/Router.swift index 18e7231..e64f03f 100644 --- a/wake/Utils/Router.swift +++ b/wake/Utils/Router.swift @@ -1,9 +1,11 @@ import SwiftUI +@MainActor enum AppRoute: Hashable { case avatarBox case feedbackView case feedbackDetail(type: FeedbackView.FeedbackType) + case mediaUpload // Add other routes here as needed @ViewBuilder @@ -15,6 +17,8 @@ enum AppRoute: Hashable { FeedbackView() case .feedbackDetail(let type): FeedbackDetailView(feedbackType: type) + case .mediaUpload: + MediaUploadView() } } } diff --git a/wake/View/Feedback.swift b/wake/View/Feedback.swift index 920ffb1..7c47243 100644 --- a/wake/View/Feedback.swift +++ b/wake/View/Feedback.swift @@ -113,24 +113,23 @@ struct FeedbackView: View { } // Continue Button - Button(action: { - if let selected = selectedFeedback { - router.navigate(to: .feedbackDetail(type: selected)) + + Button(action: { + router.navigate(to: .mediaUpload) // or your custom navigation method + }) { + Text("Continue") + .font(.headline) + .foregroundColor(selectedFeedback != nil ? .white : .gray) + .frame(maxWidth: .infinity) + .frame(height: 56) + .background( + RoundedRectangle(cornerRadius: 25) + .fill(selectedFeedback != nil ? + Color.themePrimary : Color(.systemGray5)) + ) } - }) { - Text("Continue") - .font(.headline) - .foregroundColor(selectedFeedback != nil ? .white : .gray) - .frame(maxWidth: .infinity) - .frame(height: 56) - .background( - RoundedRectangle(cornerRadius: 25) - .fill(selectedFeedback != nil ? - Color.themePrimary : Color(.systemGray5)) - ) - .padding(.horizontal, 24) - } - .disabled(selectedFeedback == nil) + .disabled(selectedFeedback == nil) + .padding() } .navigationBarHidden(true) } diff --git a/wake/View/Upload/MediaUploadView.swift b/wake/View/Upload/MediaUploadView.swift new file mode 100644 index 0000000..3a414da --- /dev/null +++ b/wake/View/Upload/MediaUploadView.swift @@ -0,0 +1,384 @@ +import SwiftUI + +// MARK: - Main View +@MainActor +struct MediaUploadView: View { + @StateObject private var uploadManager = MediaUploadManager() + @State private var showMediaPicker = false + @State private var selectedMedia: MediaType? = nil + @State private var selectedIndices: Set = [] + + var body: some View { + VStack(spacing: 24) { + HStack(spacing: 20) { + Text("The upload process will take approximately 2 minutes. Thank you for your patience. ") + .font(Typography.font(for: .caption)) + .foregroundColor(.black) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(6) + .background( + LinearGradient( + gradient: Gradient(colors: [ + Color(red: 1.0, green: 0.97, blue: 0.87), + .white, + Color(red: 1.0, green: 0.97, blue: 0.84) + ]), + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + } + .padding(10) + Spacer() + MainUploadArea( + uploadManager: uploadManager, + showMediaPicker: $showMediaPicker, + selectedMedia: $selectedMedia, + selectedIndices: $selectedIndices + ) + .id("mainUploadArea\(uploadManager.selectedMedia.count)") + + Spacer() + .frame(height: 40) + + // Navigation button + Button(action: { + Router.shared.navigate(to: .avatarBox) + }) { + Text("Continue") + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 56) + .background(Theme.Colors.primary) + .cornerRadius(28) + .padding(.horizontal, 24) + .padding(.top, 16) + } + .buttonStyle(PlainButtonStyle()) + + if !uploadManager.selectedMedia.isEmpty { + ThumbnailScrollView( + uploadManager: uploadManager, + selectedIndices: $selectedIndices, + selectedMedia: $selectedMedia + ) + .id("thumbnailScroll\(uploadManager.selectedMedia.count)") + } + + Spacer() + + if !uploadManager.selectedMedia.isEmpty { + UploadButton(uploadManager: uploadManager) + .id("uploadButton\(uploadManager.selectedMedia.count)") + } + } + .navigationTitle("Complete Your Profile") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + Text("Complete Your Profile") + .font(Typography.font(for: .title2)) + } + ToolbarItem(placement: .navigationBarLeading) { + Button(action: {}) { + EmptyView() + } + } + } + .sheet(isPresented: $showMediaPicker) { + MediaPicker( + selectedMedia: $uploadManager.selectedMedia, + imageSelectionLimit: 10, + videoSelectionLimit: 10, + onDismiss: handleMediaPickerDismiss + ) + } + .onChange(of: uploadManager.selectedMedia) { newMedia in + handleMediaChange(newMedia) + } + } + + // MARK: - Private Methods + + private func handleMediaPickerDismiss() { + showMediaPicker = false + if !uploadManager.selectedMedia.isEmpty && selectedMedia == nil { + selectedMedia = uploadManager.selectedMedia.first + selectedIndices = [0] + } + } + + private func handleMediaChange(_ newMedia: [MediaType]) { + if newMedia.isEmpty { + selectedMedia = nil + selectedIndices = [] + return + } + + // Only update if needed + if selectedIndices.isEmpty || selectedIndices.first! >= newMedia.count { + selectedMedia = newMedia.first + selectedIndices = [0] + } else if let selectedIndex = selectedIndices.first, selectedIndex < newMedia.count { + selectedMedia = newMedia[selectedIndex] + } + } +} + +// MARK: - Main Upload Area + +struct MainUploadArea: View { + @ObservedObject var uploadManager: MediaUploadManager + @Binding var showMediaPicker: Bool + @Binding var selectedMedia: MediaType? + @Binding var selectedIndices: Set + + var body: some View { + VStack(spacing: 16) { + Text("Click to upload 20 images and 5 videos to generate your next blind box.") + .font(Typography.font(for: .body, family: .quicksandRegular)) + .fontWeight(.bold) + .foregroundColor(.black) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + + if let media = selectedMedia { + MediaPreview(media: media) + .frame(height: 300) + .onTapGesture { showMediaPicker = true } + } else { + UploadPromptView(showMediaPicker: $showMediaPicker) + } + } + .padding(.horizontal) + .background(Color.white) + .cornerRadius(16) + } +} + +// MARK: - Upload Prompt View + +struct UploadPromptView: View { + @Binding var showMediaPicker: Bool + + var body: some View { + VStack(spacing: 16) { + SVGImage(svgName: "IP") + .frame(width: 225, height: 225) + .contentShape(Rectangle()) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(style: StrokeStyle( + lineWidth: 3, + lineCap: .round, + dash: [12, 8] + )) + .foregroundColor(Color.themePrimary) + ) + .onTapGesture { + showMediaPicker = true + } + } + .background(Color.white) + .cornerRadius(16) + } +} + +// MARK: - Thumbnail Scroll View + +@MainActor +struct ThumbnailScrollView: View { + @ObservedObject var uploadManager: MediaUploadManager + @Binding var selectedIndices: Set + @Binding var selectedMedia: MediaType? + + // Track the currently selected index directly for faster access + @State private var selectedIndex: Int = 0 + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(0.. Void + + var body: some View { + Button(action: onTap) { + ZStack(alignment: .topTrailing) { + Group { + if let thumbnail = media.thumbnail { + Image(uiImage: thumbnail) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 80, height: 80) + .clipped() + } else { + Color.gray + .frame(width: 80, height: 80) + } + } + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(isSelected ? Color.themePrimary : Color.clear, lineWidth: 2) + ) + + // Selection checkmark + if isSelected && showCheckmark { + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 20)) + .foregroundColor(.white) + .background(Circle().fill(Color.themePrimary)) + .offset(x: 6, y: -6) // Adjusted offset to ensure visibility + .zIndex(1) // Ensure checkmark is above video icon + } + + // Video icon + if media.isVideo { + Image(systemName: "video.fill") + .font(.caption) + .foregroundColor(.white) + .padding(4) + .background(Color.black.opacity(0.6)) + .clipShape(Circle()) + .padding(4) + .offset(x: -4, y: 4) // Slight adjustment for better positioning + } + } + .frame(width: 80, height: 80) + .contentShape(Rectangle()) + .padding(4) // Add padding to prevent clipping + } + .buttonStyle(PlainButtonStyle()) + } +} + +// MARK: - Add More Button + +struct AddMoreButton: View { + @Binding var showMediaPicker: Bool + + var body: some View { + Button(action: { showMediaPicker = true }) { + Image(systemName: "plus") + .font(.title2) + .foregroundColor(.gray) + .frame(width: 80, height: 80) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(style: StrokeStyle( + lineWidth: 1, + dash: [5, 3] + )) + .foregroundColor(.gray) + ) + } + } +} + +// MARK: - Upload Button + +struct UploadButton: View { + @ObservedObject var uploadManager: MediaUploadManager + + var body: some View { + Button(action: uploadManager.startUpload) { + Text("上传") + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 56) + .background(Color.themePrimary) + .cornerRadius(28) + .padding(.horizontal, 40) + .padding(.bottom, 24) + } + } +} + +// MARK: - Media Preview + +struct MediaPreview: View { + let media: MediaType + + var body: some View { + Group { + switch media { + case .image(let uiImage): + Image(uiImage: uiImage) + .resizable() + .scaledToFit() + .cornerRadius(12) + .drawingGroup() // Improves performance for large images + case .video(_, let thumbnail): + if let thumbnail = thumbnail { + Image(uiImage: thumbnail) + .resizable() + .scaledToFit() + .overlay( + Image(systemName: "play.circle.fill") + .font(.system(size: 48)) + .foregroundColor(.white) + .shadow(radius: 10) + ) + .cornerRadius(12) + .drawingGroup() // Improves performance for thumbnails + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.themeTextWhiteSecondary) + .cornerRadius(16) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(Color.themePrimary, lineWidth: 2) + ) + .contentShape(Rectangle()) // Better tap target + } +} + +// MARK: - Preview + +struct MediaUploadView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + MediaUploadView() + } + } +}