diff --git a/wake/View/OnBoarding/SecondBoxUpload.swift b/wake/View/OnBoarding/SecondBoxUpload.swift deleted file mode 100644 index e69de29..0000000 diff --git a/wake/View/Upload/MediaUploadView.swift b/wake/View/Upload/MediaUploadView.swift deleted file mode 100644 index ab0250c..0000000 --- a/wake/View/Upload/MediaUploadView.swift +++ /dev/null @@ -1,719 +0,0 @@ -import SwiftUI -import PhotosUI -import AVKit -import CoreTransferable -import CoreImage.CIFilterBuiltins - -extension Notification.Name { - static let didAddFirstMedia = Notification.Name("didAddFirstMedia") -} -/// 主上传视图 -/// 提供媒体选择、预览和上传功能 -@MainActor -struct MediaUploadView: View { - // MARK: - 属性 - - /// 上传管理器,负责处理上传逻辑 - @StateObject private var uploadManager = MediaUploadManager() - /// 控制媒体选择器的显示/隐藏 - @State private var showMediaPicker = false - /// 当前选中的媒体项 - @State private var selectedMedia: MediaType? = nil - /// 当前选中的媒体索引集合 - @State private var selectedIndices: Set = [] - @State private var mediaPickerSelection: [MediaType] = [] // 添加这个状态变量 - /// 上传完成状态 - @State private var uploadComplete = false - /// 上传完成的文件ID列表 - @State private var uploadedFileIds: [[String: String]] = [] - - // MARK: - 视图主体 - - var body: some View { - VStack(spacing: 0) { - // 顶部导航栏 - topNavigationBar - - // 上传提示信息 - uploadHintView - Spacer() - .frame(height: uploadManager.selectedMedia.isEmpty ? 80 : 40) - // 主上传区域 - MainUploadArea( - uploadManager: uploadManager, - showMediaPicker: $showMediaPicker, - selectedMedia: $selectedMedia - ) - .id("mainUploadArea\(uploadManager.selectedMedia.count)") - - Spacer() - - // // 上传结果展示 - // if uploadComplete && !uploadedFileIds.isEmpty { - // VStack(alignment: .leading) { - // Text("上传完成!") - // .font(.headline) - - // ScrollView { - // ForEach(Array(uploadedFileIds.enumerated()), id: \.offset) { index, fileInfo in - // VStack(alignment: .leading) { - // Text("文件 \(index + 1):") - // .font(.subheadline) - // Text("ID: \(fileInfo["file_id"] ?? "")") - // .font(.caption) - // .foregroundColor(.gray) - // } - // .padding() - // .frame(maxWidth: .infinity, alignment: .leading) - // .background(Color.gray.opacity(0.1)) - // .cornerRadius(8) - // } - // } - // .frame(height: 200) - // } - // .padding() - // } - - // 继续按钮 - continueButton - .padding(.bottom, 24) - } - .background(Color.themeTextWhiteSecondary) - .navigationBarTitleDisplayMode(.inline) - .navigationBarBackButtonHidden(true) - .sheet(isPresented: $showMediaPicker) { - // 媒体选择器 - mediaPickerView - } - .onChange(of: uploadManager.uploadResults) { newResults in - handleUploadCompletion(results: newResults) - } - } - - // MARK: - 子视图 - - /// 顶部导航栏 - private var topNavigationBar: some View { - 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) - .zIndex(1) // 确保导航栏显示在最上层 - } - - /// 上传提示视图 - private var uploadHintView: some View { - HStack (spacing: 6) { - SVGImage(svgName: "Tips") - .frame(width: 16, height: 16) - .padding(.leading,6) - Text("The upload process will take approximately 2 minutes. Thank you for your patience.") - .font(.caption) - .foregroundColor(.black) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(3) - } - .background( - Color.themeTextWhite - .cornerRadius(6) - ) - .padding(.vertical, 8) - .padding(.horizontal) - } - - /// 继续按钮 - private var continueButton: some View { - Button(action: handleContinue) { - 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()) - .disabled(uploadManager.selectedMedia.isEmpty) - } - - /// 媒体选择器视图 - private var mediaPickerView: some View { - MediaPicker( - selectedMedia: Binding( - get: { mediaPickerSelection }, - set: { newSelections in - print("🔄 开始处理用户选择的媒体文件") - print("📌 新选择的媒体数量: \(newSelections.count)") - - // 1. 去重处理:过滤掉已经存在的媒体项 - var uniqueNewMedia: [MediaType] = [] - - for newItem in newSelections { - let isDuplicate = uploadManager.selectedMedia.contains { existingItem in - switch (existingItem, newItem) { - case (.image(let existingImage), .image(let newImage)): - return existingImage.pngData() == newImage.pngData() - case (.video(let existingURL, _), .video(let newURL, _)): - return existingURL == newURL - default: - return false - } - } - - if !isDuplicate { - uniqueNewMedia.append(newItem) - } else { - print("⚠️ 检测到重复文件,已跳过: \(newItem)") - } - } - - // 2. 添加新文件 - if !uniqueNewMedia.isEmpty { - print("✅ 添加 \(uniqueNewMedia.count) 个新文件") - uploadManager.addMedia(uniqueNewMedia) - - // 如果没有当前选中的媒体,则选择第一个新添加的 - if selectedMedia == nil, let firstNewItem = uniqueNewMedia.first { - selectedMedia = firstNewItem - } - - // 开始上传 - uploadManager.startUpload() - } else { - print("ℹ️ 没有新文件需要添加,所有选择的文件都已存在") - } - } - ), - imageSelectionLimit: max(0, 20 - uploadManager.selectedMedia.filter { - if case .image = $0 { return true } - return false - }.count), - videoSelectionLimit: max(0, 5 - uploadManager.selectedMedia.filter { - if case .video = $0 { return true } - return false - }.count), - selectionMode: .multiple, - onDismiss: handleMediaPickerDismiss, - onUploadProgress: { index, progress in - print("文件 \(index) 上传进度: \(progress * 100)%") - } - ) - .onAppear { - // 重置选择状态当选择器出现时 - mediaPickerSelection = [] - } - } - - // MARK: - 私有方法 - - /// 处理媒体选择器关闭事件 - private func handleMediaPickerDismiss() { - showMediaPicker = false - print("媒体选择器关闭 - 开始处理") - - // 如果有选中的媒体,开始上传 - if !uploadManager.selectedMedia.isEmpty { - // 不需要在这里开始上传,因为handleMediaChange会处理 - } - } - - /// 处理媒体变化 - /// - Parameters: - /// - newMedia: 新的媒体数组 - /// - oldMedia: 旧的媒体数组 - private func handleMediaChange(_ newMedia: [MediaType], oldMedia: [MediaType]) { - print("开始处理媒体变化,新数量: \(newMedia.count), 原数量: \(oldMedia.count)") - - // 如果没有变化,直接返回 - guard newMedia != oldMedia else { - print("媒体未发生变化,跳过处理") - return - } - - // 在后台线程处理媒体变化 - DispatchQueue.global(qos: .userInitiated).async { [self] in - // 找出新增的媒体(在newMedia中但不在oldMedia中的项) - let newItems = newMedia.filter { newItem in - !oldMedia.contains { $0.id == newItem.id } - } - - print("检测到\(newItems.count)个新增媒体项") - - // 如果有新增媒体 - if !newItems.isEmpty { - print("准备添加\(newItems.count)个新项...") - - // 在主线程更新UI - DispatchQueue.main.async { [self] in - // 创建新的数组,包含原有媒体和新媒体 - var updatedMedia = uploadManager.selectedMedia - updatedMedia.append(contentsOf: newItems) - - // 更新选中的媒体 - uploadManager.clearAllMedia() - uploadManager.addMedia(updatedMedia) - - // 如果当前没有选中的媒体,则选中第一个新增的媒体 - if selectedIndices.isEmpty && !newItems.isEmpty { - selectedIndices = [oldMedia.count] // 选择第一个新增项的索引 - selectedMedia = newItems.first - } - - // 开始上传新添加的媒体 - uploadManager.startUpload() - print("媒体添加完成,总数量: \(uploadManager.selectedMedia.count)") - } - } - } - } - - /// 检查是否有正在上传的文件 - /// - Returns: 是否正在上传 - private func isUploading() -> Bool { - return uploadManager.uploadStatus.values.contains { status in - if case .uploading = status { return true } - return false - } - } - - /// 处理上传完成 - private func handleUploadCompletion(results: [String: MediaUploadManager.UploadResult]) { - // 转换为需要的格式 - let formattedResults = results.map { (_, result) -> [String: String] in - return [ - "file_id": result.fileId, - "preview_file_id": result.thumbnailId ?? result.fileId - ] - } - - uploadedFileIds = formattedResults - uploadComplete = !uploadedFileIds.isEmpty - } - - /// 处理继续按钮点击 - private func handleContinue() { - // 获取所有已上传文件的结果 - let uploadResults = uploadManager.uploadResults - guard !uploadResults.isEmpty else { - print("⚠️ 没有可用的文件ID") - return - } - - // 准备请求参数 - let files = uploadResults.map { (_, result) -> [String: String] in - return [ - "file_id": result.fileId, - "preview_file_id": result.thumbnailId ?? result.fileId - ] - } - - // 发送POST请求到/material接口 - NetworkService.shared.postWithToken( - path: "/material", - parameters: files - ) { (result: Result) in - switch result { - case .success: - print("✅ 素材提交成功") - // 跳转到盲盒页面 - DispatchQueue.main.async { - Router.shared.navigate(to: .blindBox(mediaType: .video)) - } - case .failure(let error): - print("❌ 素材提交失败: \(error.localizedDescription)") - // 这里可以添加错误处理逻辑,比如显示错误提示 - } - } - } -} - -// MARK: - 主上传区域 - -/// 主上传区域视图 -/// 显示上传提示、媒体预览和添加更多按钮 -struct MainUploadArea: View { - // MARK: - 属性 - - /// 上传管理器 - @ObservedObject var uploadManager: MediaUploadManager - /// 控制媒体选择器的显示/隐藏 - @Binding var showMediaPicker: Bool - /// 当前选中的媒体 - @Binding var selectedMedia: MediaType? - - // MARK: - 视图主体 - - var body: some View { - VStack() { - Spacer() - .frame(height: 30) - // 标题 - 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) - Spacer() - .frame(height: 50) - // 主显示区域 - if let mediaToDisplay = selectedMedia ?? uploadManager.selectedMedia.first { - Button(action: { showMediaPicker = true }) { - MediaPreview(media: mediaToDisplay) - .id(mediaToDisplay.id) - .frame(width: 225, height: 225) - .overlay( - RoundedRectangle(cornerRadius: 16) - .stroke(Color.themePrimary, lineWidth: 5) - ) - .cornerRadius(16) - .padding(.horizontal) - .transition(.opacity) - } - } else { - UploadPromptView(showMediaPicker: $showMediaPicker) - } - // 媒体预览区域 - mediaPreviewSection - Spacer() - .frame(height: 10) - } - .onAppear { - print("MainUploadArea appeared") - print("Selected media count: \(uploadManager.selectedMedia.count)") - - if selectedMedia == nil, let firstMedia = uploadManager.selectedMedia.first { - print("Selecting first media: \(firstMedia.id)") - selectedMedia = firstMedia - } - } - .onReceive(NotificationCenter.default.publisher(for: .didAddFirstMedia)) { notification in - if let media = notification.userInfo?["media"] as? MediaType, selectedMedia == nil { - selectedMedia = media - } - } - .background(Color.white) - .cornerRadius(18) - .animation(.default, value: selectedMedia?.id) - } - - // MARK: - 子视图 - - /// 媒体预览区域 - private var mediaPreviewSection: some View { - Group { - if !uploadManager.selectedMedia.isEmpty { - VStack(spacing: 4) { - // 横向滚动的缩略图列表 - ScrollView(.horizontal, showsIndicators: false) { - LazyHStack(spacing: 10) { - ForEach(Array(uploadManager.selectedMedia.enumerated()), id: \.element.id) { index, media in - mediaItemView(for: media, at: index) - } - // 当没有选择媒体时显示添加更多按钮 - if !uploadManager.selectedMedia.isEmpty { - addMoreButton - } - } - .padding(.horizontal) - } - .frame(height: 70) - } - .padding(.top, 10) - } - } - } - - /// 单个媒体项视图 - /// - Parameters: - /// - media: 媒体项 - /// - index: 索引 - /// - Returns: 媒体项视图 - private func mediaItemView(for media: MediaType, at index: Int) -> some View { - ZStack(alignment: .topTrailing) { - // 媒体预览 - 始终使用本地资源 - MediaPreview(media: media) - .frame(width: 58, height: 58) - .cornerRadius(8) - .shadow(radius: 1) - .overlay( - // 左上角序号 - ZStack(alignment: .topLeading) { - Path { path in - let radius: CGFloat = 4 - let width: CGFloat = 14 - let height: CGFloat = 10 - - // 从左上角开始(带圆角) - path.move(to: CGPoint(x: 0, y: radius)) - path.addQuadCurve(to: CGPoint(x: radius, y: 0), - control: CGPoint(x: 0, y: 0)) - - // 上边缘(右上角保持直角) - path.addLine(to: CGPoint(x: width, y: 0)) - - // 右边缘(右下角保持直角) - path.addLine(to: CGPoint(x: width, y: height - radius)) - - // 右下角圆角 - path.addQuadCurve(to: CGPoint(x: width - radius, y: height), - control: CGPoint(x: width, y: height)) - - // 下边缘(左下角保持直角) - path.addLine(to: CGPoint(x: 0, y: height)) - - // 闭合路径 - path.closeSubpath() - } - .fill(Color(hex: "BEBEBE").opacity(0.6)) - .frame(width: 14, height: 10) - .overlay( - Text("\(index + 1)") - .font(.system(size: 8, weight: .bold)) - .foregroundColor(.black) - .frame(width: 14, height: 10) - .offset(y: -1), - alignment: .topLeading - ) - .padding([.top, .leading], 2) - - // 右下角视频时长 - if case .video(let url, _) = media, let videoURL = url as? URL { - VStack { - Spacer() - HStack { - Spacer() - Text(getVideoDuration(url: videoURL)) - .font(.system(size: 8, weight: .bold)) - .foregroundColor(.black) - .padding(.horizontal, 4) - .frame(height: 10) - .background(Color(hex: "BEBEBE").opacity(0.6)) - .cornerRadius(2) - } - .padding([.trailing, .bottom], 0) - } - }else{ - // 占位 - VStack { - Spacer() - HStack { - Spacer() - Text("占位") - .font(.system(size: 8, weight: .bold)) - .foregroundColor(.black) - .padding(.horizontal, 4) - .frame(height: 10) - .background(Color(hex: "BEBEBE").opacity(0.6)) - .cornerRadius(2) - } - .padding([.trailing, .bottom], 0) - } - .opacity(0) - } - }, - alignment: .topLeading - ) - .onTapGesture { - print("点击了媒体项,索引: \(index)") - withAnimation { - selectedMedia = media - } - } - .contentShape(Rectangle()) - - // 右上角关闭按钮 - Button(action: { - uploadManager.removeMedia(id: media.id) - if selectedMedia == media { - selectedMedia = nil - } - }) { - Image(systemName: "xmark") - .font(.system(size: 8, weight: .bold)) - .foregroundColor(.black) - .frame(width: 12, height: 12) - .background( - Circle() - .fill(Color(hex: "BEBEBE").opacity(0.6)) - .frame(width: 12, height: 12) - ) - } - .offset(x: 6, y: -6) - } - .padding(.horizontal, 4) - .contentShape(Rectangle()) - } - - /// 添加更多按钮 - private var addMoreButton: some View { - Button(action: { showMediaPicker = true }) { - Image(systemName: "plus") - .font(.system(size: 8, weight: .bold)) - .foregroundColor(.black) - .frame(width: 58, height: 58) - .background(Color.white) - .cornerRadius(8) - .overlay( - RoundedRectangle(cornerRadius: 8) - .strokeBorder(style: StrokeStyle( - lineWidth: 2, - dash: [4, 4] - )) - .foregroundColor(Color.themePrimary) - ) - } - } -} - -// MARK: - 上传提示视图 - -/// 上传提示视图 -/// 显示上传区域的占位图和提示 -struct UploadPromptView: View { - /// 控制媒体选择器的显示/隐藏 - @Binding var showMediaPicker: Bool - - var body: some View { - Button(action: { showMediaPicker = true }) { - // 上传图标 - SVGImageHtml(svgName: "IP") - .frame(width: 225, height: 225) - .contentShape(Rectangle()) - .overlay( - ZStack { - RoundedRectangle(cornerRadius: 20) - .stroke(style: StrokeStyle( - lineWidth: 5, - lineCap: .round, - dash: [12, 8] - )) - .foregroundColor(Color.themePrimary) - - // Add plus icon in the center - Image(systemName: "plus") - .font(.system(size: 32, weight: .bold)) - .foregroundColor(.black) - } - ) - } - } -} - -// MARK: - 媒体预览视图 - -/// 媒体预览视图 -/// 显示图片或视频的预览图,始终使用本地资源 -struct MediaPreview: View { - // MARK: - 属性 - - /// 媒体类型 - let media: MediaType - - // MARK: - 计算属性 - - /// 获取要显示的图片 - private var displayImage: UIImage? { - switch media { - case .image(let uiImage): - return uiImage - case .video(_, let thumbnail): - return thumbnail - } - } - - // MARK: - 视图主体 - - var body: some View { - ZStack { - // 1. 显示图片或视频缩略图 - if let image = displayImage { - Image(uiImage: image) - .resizable() - .scaledToFill() - .transition(.opacity.animation(.easeInOut(duration: 0.2))) - } else { - // 2. 加载中的占位图 - Color.gray.opacity(0.1) - } - } - .aspectRatio(1, contentMode: .fill) - .clipped() - .cornerRadius(8) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color.themePrimary.opacity(0.3), lineWidth: 1) - ) - } -} - -private func getVideoDuration(url: URL) -> String { - let asset = AVURLAsset(url: url) - let durationInSeconds = CMTimeGetSeconds(asset.duration) - guard durationInSeconds.isFinite else { return "0:00" } - - let minutes = Int(durationInSeconds) / 60 - let seconds = Int(durationInSeconds) % 60 - return String(format: "%d:%02d", minutes, seconds) -} - -// MARK: - Response Types - -private struct EmptyResponse: Decodable { - // Empty response type for endpoints that don't return data -} - -// MARK: - 扩展 - -/// 扩展 MediaType 以支持 Identifiable 协议 -extension MediaType: Identifiable { - /// 唯一标识符 - public var id: String { - switch self { - case .image(let uiImage): - return "image_\(uiImage.hashValue)" - case .video(let url, _): - return "video_\(url.absoluteString.hashValue)" - } -} -} - -extension TimeInterval { - var formattedDuration: String { - let minutes = Int(self) / 60 - let seconds = Int(self) % 60 - return String(format: "%d:%02d", minutes, seconds) - } -} - -// MARK: - 预览 - -struct MediaUploadView_Previews: PreviewProvider { - static var previews: some View { - NavigationView { - MediaUploadView() - } - } -}