382 lines
13 KiB
Swift
382 lines
13 KiB
Swift
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() {
|
|
// 固定的顶部导航栏
|
|
HStack {
|
|
Button(action: {
|
|
Router.shared.pop()
|
|
}) {
|
|
Image(systemName: "chevron.left")
|
|
.font(.system(size: 17, weight: .semibold))
|
|
.foregroundColor(.themeTextMessageMain)
|
|
}
|
|
.padding(.leading, 16)
|
|
|
|
Spacer()
|
|
|
|
Text("Complete Your Profile")
|
|
.font(Typography.font(for: .title2, family: .quicksandBold))
|
|
.foregroundColor(.themeTextMessageMain)
|
|
|
|
Spacer()
|
|
|
|
// 添加一个透明的占位视图来平衡布局
|
|
Color.clear
|
|
.frame(width: 24, height: 24)
|
|
.padding(.trailing, 16)
|
|
}
|
|
.background(Color.themeTextWhiteSecondary)
|
|
.padding(.bottom, -24)
|
|
.zIndex(1) // 确保导航栏在最上层
|
|
// 主体内容
|
|
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()
|
|
|
|
Spacer()
|
|
.frame(height: 20)
|
|
|
|
MainUploadArea(
|
|
uploadManager: uploadManager,
|
|
showMediaPicker: $showMediaPicker,
|
|
selectedMedia: $selectedMedia,
|
|
selectedIndices: $selectedIndices
|
|
)
|
|
.padding()
|
|
.id("mainUploadArea\(uploadManager.selectedMedia.count)")
|
|
|
|
Spacer()
|
|
|
|
// Navigation button
|
|
Button(action: {
|
|
// Router.shared.navigate(to: .avatarBox)
|
|
}) {
|
|
Text("Continue")
|
|
.font(.headline)
|
|
.foregroundColor(uploadManager.selectedMedia.isEmpty ? Color.themeTextMessage : Color.themeTextMessageMain)
|
|
.frame(maxWidth: .infinity)
|
|
.frame(height: 56)
|
|
.background(uploadManager.selectedMedia.isEmpty ? Color.white : Color.themePrimary)
|
|
.cornerRadius(28)
|
|
.padding(.horizontal, 24)
|
|
}
|
|
.buttonStyle(PlainButtonStyle())
|
|
|
|
Spacer()
|
|
}
|
|
.background(Color.themeTextWhiteSecondary)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.navigationBarBackButtonHidden(true)
|
|
.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]
|
|
// Start upload when media picker is dismissed with new media
|
|
uploadManager.startUpload()
|
|
}
|
|
}
|
|
|
|
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]
|
|
}
|
|
|
|
// Auto-upload when new media is added
|
|
if !newMedia.isEmpty && !uploadManager.isUploading {
|
|
uploadManager.startUpload()
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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: .title2, family: .quicksandBold))
|
|
.fontWeight(.bold)
|
|
.foregroundColor(.black)
|
|
.multilineTextAlignment(.center)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding()
|
|
|
|
if let media = selectedMedia {
|
|
MediaPreview(media: media)
|
|
.frame(width: 225, height: 225)
|
|
.onTapGesture { showMediaPicker = true }
|
|
} else {
|
|
UploadPromptView(showMediaPicker: $showMediaPicker)
|
|
}
|
|
if !uploadManager.selectedMedia.isEmpty {
|
|
ThumbnailScrollView(
|
|
uploadManager: uploadManager,
|
|
selectedIndices: $selectedIndices,
|
|
selectedMedia: $selectedMedia
|
|
)
|
|
.id("thumbnailScroll\(uploadManager.selectedMedia.count)")
|
|
}
|
|
}
|
|
.background(Color.white)
|
|
.cornerRadius(16)
|
|
}
|
|
}
|
|
|
|
// MARK: - Upload Prompt View
|
|
|
|
struct UploadPromptView: View {
|
|
@Binding var showMediaPicker: Bool
|
|
|
|
var body: some View {
|
|
Button(action: {
|
|
showMediaPicker = true
|
|
}) {
|
|
SVGImage(svgName: "IP")
|
|
.frame(width: 225, height: 225)
|
|
.contentShape(Rectangle())
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 20)
|
|
.stroke(style: StrokeStyle(
|
|
lineWidth: 5,
|
|
lineCap: .round,
|
|
dash: [12, 8]
|
|
))
|
|
.foregroundColor(Color.themePrimary)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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: - 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()
|
|
}
|
|
}
|
|
}
|