wake-ios/wake/View/Upload/MediaUploadView.swift
2025-08-24 16:34:23 +08:00

385 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(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()
}
}
}