feat: 上传页面
This commit is contained in:
parent
4e97f8ebb8
commit
2eee2486e1
@ -1,9 +1,11 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
enum AppRoute: Hashable {
|
enum AppRoute: Hashable {
|
||||||
case avatarBox
|
case avatarBox
|
||||||
case feedbackView
|
case feedbackView
|
||||||
case feedbackDetail(type: FeedbackView.FeedbackType)
|
case feedbackDetail(type: FeedbackView.FeedbackType)
|
||||||
|
case mediaUpload
|
||||||
// Add other routes here as needed
|
// Add other routes here as needed
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@ -15,6 +17,8 @@ enum AppRoute: Hashable {
|
|||||||
FeedbackView()
|
FeedbackView()
|
||||||
case .feedbackDetail(let type):
|
case .feedbackDetail(let type):
|
||||||
FeedbackDetailView(feedbackType: type)
|
FeedbackDetailView(feedbackType: type)
|
||||||
|
case .mediaUpload:
|
||||||
|
MediaUploadView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -113,24 +113,23 @@ struct FeedbackView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Continue Button
|
// Continue Button
|
||||||
Button(action: {
|
|
||||||
if let selected = selectedFeedback {
|
Button(action: {
|
||||||
router.navigate(to: .feedbackDetail(type: selected))
|
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))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}) {
|
.disabled(selectedFeedback == nil)
|
||||||
Text("Continue")
|
.padding()
|
||||||
.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)
|
|
||||||
}
|
}
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
}
|
}
|
||||||
|
|||||||
384
wake/View/Upload/MediaUploadView.swift
Normal file
384
wake/View/Upload/MediaUploadView.swift
Normal file
@ -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<Int> = []
|
||||||
|
|
||||||
|
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<Int>
|
||||||
|
|
||||||
|
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<Int>
|
||||||
|
@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..<uploadManager.selectedMedia.count, id: \.self) { index in
|
||||||
|
let media = uploadManager.selectedMedia[index]
|
||||||
|
ThumbnailView(
|
||||||
|
media: media,
|
||||||
|
isSelected: selectedIndex == index,
|
||||||
|
showCheckmark: true
|
||||||
|
) {
|
||||||
|
// Directly update the selection without animation for immediate response
|
||||||
|
selectedIndex = index
|
||||||
|
selectedMedia = media
|
||||||
|
selectedIndices = [index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddMoreButton(showMediaPicker: .constant(true))
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
.frame(height: 100)
|
||||||
|
.onAppear {
|
||||||
|
// Initialize selection when view appears
|
||||||
|
if !uploadManager.selectedMedia.isEmpty {
|
||||||
|
selectedMedia = uploadManager.selectedMedia[selectedIndex]
|
||||||
|
selectedIndices = [selectedIndex]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Thumbnail View
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct ThumbnailView: View {
|
||||||
|
let media: MediaType
|
||||||
|
let isSelected: Bool
|
||||||
|
let showCheckmark: Bool
|
||||||
|
let onTap: () -> 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user