wake-ios/wake/View/Upload/MediaUploadView.swift
2025-08-25 15:17:23 +08:00

515 lines
19 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(.horizontal)
.padding(.bottom, -20)
.zIndex(1) //
HStack() {
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: 20,
videoSelectionLimit: 5,
onDismiss: {
//
showMediaPicker = false
},
onUploadProgress: { index, progress in
print("File \(index) upload progress: \(progress * 100)%")
}
)
}
.onChange(of: uploadManager.selectedMedia) { newMedia in
print("onChange1111111", uploadManager.selectedMedia)
//
if !newMedia.isEmpty {
//
uploadManager.startUpload()
}
}
}
// MARK: - Private Methods
private func handleMediaPickerDismiss() {
self.uploadManager.startUpload()
print("handleMediaPickerDismiss1111111", uploadManager.selectedMedia)
// showMediaPicker = false
// //
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
// if !self.uploadManager.selectedMedia.isEmpty {
// self.selectedMedia = self.uploadManager.selectedMedia.first
// self.selectedIndices = [0]
// //
// self.uploadManager.startUpload()
// }
// }
}
private func handleMediaChange(_ newMedia: [MediaType]) {
if newMedia.isEmpty {
selectedMedia = nil
selectedIndices = []
return
}
//
if selectedIndices.isEmpty || selectedIndices.first! >= newMedia.count {
selectedMedia = newMedia.first
selectedIndices = [0]
} else if let selectedIndex = selectedIndices.first, selectedIndex < newMedia.count {
selectedMedia = newMedia[selectedIndex]
}
//
if !newMedia.isEmpty && !isUploading() {
uploadManager.startUpload()
}
}
//
private func isUploading() -> Bool {
return uploadManager.uploadStatus.values.contains { status in
if case .uploading = status {
return true
}
return false
}
}
}
// 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)
.padding(.horizontal)
if !uploadManager.selectedMedia.isEmpty {
//
ScrollView {
LazyVGrid(columns: [
GridItem(.flexible(), spacing: 16),
GridItem(.flexible(), spacing: 16)
], spacing: 16) {
ForEach(0..<uploadManager.selectedMedia.count, id: \.self) { index in
VStack(spacing: 8) {
//
MediaPreview(
media: uploadManager.selectedMedia[index],
uploadManager: uploadManager
)
.frame(height: 150)
.cornerRadius(12)
.shadow(radius: 2)
//
if let status = uploadManager.uploadStatus["\(index)"] {
switch status {
case .uploading(let progress):
VStack(alignment: .leading, spacing: 4) {
Text("Uploading: \(Int(progress * 100))%")
.font(.caption)
.foregroundColor(.gray)
ProgressView(value: progress, total: 1.0)
.progressViewStyle(LinearProgressViewStyle())
.tint(Color.themePrimary)
}
.padding(.horizontal, 8)
case .completed:
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text("Uploaded")
.font(.caption)
.foregroundColor(.gray)
}
case .failed:
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.red)
Text("Upload failed")
.font(.caption)
.foregroundColor(.red)
}
default:
EmptyView()
}
}
}
}
}
.padding()
}
.frame(maxHeight: 400)
} else {
//
UploadPromptView(showMediaPicker: $showMediaPicker)
}
//
Button(action: { showMediaPicker = true }) {
HStack {
Image(systemName: "plus.circle.fill")
Text("Add More")
}
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.themePrimary)
.cornerRadius(10)
.padding(.horizontal)
}
}
.background(Color.white)
.cornerRadius(16)
.padding()
}
}
// 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,
uploadManager: uploadManager,
onTap: {
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 uploadManager: MediaUploadManager?
let onTap: () -> Void
var body: some View {
Button(action: onTap) {
ZStack(alignment: .topTrailing) {
// Main thumbnail content
ZStack {
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)
)
// Upload progress border
if let uploadManager = uploadManager,
let index = uploadManager.selectedMedia.firstIndex(where: { $0 == media }) {
let status = uploadManager.uploadStatus["\(index)"]
if case .uploading(let progress) = status, progress > 0 && progress < 1 {
ZStack {
Circle()
.stroke(
Color.themePrimary.opacity(0.3),
style: StrokeStyle(lineWidth: 2, lineCap: .round)
)
.rotationEffect(.degrees(-90))
.padding(2)
Circle()
.trim(from: 0, to: progress)
.stroke(
Color.themePrimary,
style: StrokeStyle(lineWidth: 2, lineCap: .round)
)
.rotationEffect(.degrees(-90))
.animation(.linear, value: progress)
.padding(2)
}
.frame(width: 20, height: 20)
.offset(x: 30, y: -30)
}
}
}
// 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)
.zIndex(1)
}
// 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)
}
}
.frame(width: 80, height: 80)
.contentShape(Rectangle())
.padding(4)
}
.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
@ObservedObject var uploadManager: MediaUploadManager
private var uploadProgress: Double {
guard let index = uploadManager.selectedMedia.firstIndex(where: { $0 == media }),
case .uploading(let progress) = uploadManager.uploadStatus["\(index)"] else {
return 0
}
return progress
}
var body: some View {
ZStack {
//
Group {
switch media {
case .image(let uiImage):
Image(uiImage: uiImage)
.resizable()
.scaledToFill()
case .video(_, let thumbnail):
if let thumbnail = thumbnail {
Image(uiImage: thumbnail)
.resizable()
.scaledToFill()
.overlay(
Image(systemName: "play.circle.fill")
.font(.system(size: 36))
.foregroundColor(.white)
.shadow(radius: 8)
)
} else {
Color.gray
}
}
}
.aspectRatio(1, contentMode: .fill)
.clipped()
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.themePrimary.opacity(0.3), lineWidth: 1)
)
}
}
}
// MARK: - Preview
struct MediaUploadView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
MediaUploadView()
}
}
}