From 96e58806d4660b8c6f0dba091b1ec7cb1a845e67 Mon Sep 17 00:00:00 2001 From: jinyaqiu Date: Fri, 29 Aug 2025 20:44:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A7=86=E9=A2=91=E7=BC=A9=E7=95=A5?= =?UTF-8?q?=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Upload/ImageUploadService.swift | 68 ++++++- .../Upload/ImageUploaderGetID.swift | 189 +++++++----------- wake/View/Components/Upload/MediaUpload.swift | 51 +++-- wake/View/Memories/MemoriesView.swift | 114 +++++++---- wake/View/Upload/MediaUploadView.swift | 37 ++-- 5 files changed, 258 insertions(+), 201 deletions(-) diff --git a/wake/View/Components/Upload/ImageUploadService.swift b/wake/View/Components/Upload/ImageUploadService.swift index f86432c..5dcaba5 100644 --- a/wake/View/Components/Upload/ImageUploadService.swift +++ b/wake/View/Components/Upload/ImageUploadService.swift @@ -199,32 +199,48 @@ public class ImageUploadService { self.uploader.uploadImage( compressedThumbnail, - progress: { _ in }, + progress: { uploadProgress in + // 缩略图上传进度(占总进度的后10%) + let progressInfo = UploadProgress( + current: 90 + Int(uploadProgress * 10), + total: 100, + progress: 0.9 + (uploadProgress * 0.1), + isOriginal: false + ) + progress(progressInfo) + }, completion: { thumbnailResult in switch thumbnailResult { case .success(let thumbnailUploadResult): print("✅ 视频缩略图上传完成, fileId: \(thumbnailUploadResult.fileId)") - let result = MediaUploadResult.video( - video: videoResult, - thumbnail: thumbnailUploadResult + + // 确保返回的视频结果中,preview_file_id 是缩略图的 ID + let finalVideoResult = ImageUploaderGetID.UploadResult( + fileUrl: videoResult.fileUrl, + fileName: videoResult.fileName, + fileSize: videoResult.fileSize, + fileId: videoResult.fileId, + previewFileId: thumbnailUploadResult.fileId // 使用缩略图的ID作为preview_file_id ) - completion(.success(result)) + + completion(.success(.video(video: finalVideoResult, thumbnail: thumbnailUploadResult))) case .failure(let error): print("❌ 视频缩略图上传失败: \(error.localizedDescription)") - completion(.failure(error)) + // 即使缩略图上传失败,也返回视频上传成功 + completion(.success(.video(video: videoResult, thumbnail: nil))) } } ) } else { - let error = NSError(domain: "ImageUploadService", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to compress thumbnail"]) - print("❌ 视频缩略图压缩失败") - completion(.failure(error)) + // 缩略图压缩失败,只返回视频 + completion(.success(.video(video: videoResult, thumbnail: nil))) } case .failure(let error): print("❌ 视频缩略图提取失败: \(error.localizedDescription)") - completion(.failure(error)) + // 缩略图提取失败,只返回视频 + completion(.success(.video(video: videoResult, thumbnail: nil))) } } @@ -352,7 +368,7 @@ public class ImageUploadService { /// 媒体上传结果 public enum MediaUploadResult { case file(ImageUploaderGetID.UploadResult) - case video(video: ImageUploaderGetID.UploadResult, thumbnail: ImageUploaderGetID.UploadResult) + case video(video: ImageUploaderGetID.UploadResult, thumbnail: ImageUploaderGetID.UploadResult?) /// 获取文件ID(对于视频,返回视频文件的ID) public var fileId: String { @@ -363,6 +379,36 @@ public class ImageUploadService { return videoResult.fileId } } + + /// 获取预览文件ID(对于视频,返回缩略图的ID) + public var previewFileId: String? { + switch self { + case .file: + return nil + case .video(_, let thumbnailResult): + return thumbnailResult?.fileId + } + } + + /// 获取文件URL(对于视频,返回视频文件的URL) + public var fileUrl: String { + switch self { + case .file(let result): + return result.fileUrl + case .video(let videoResult, _): + return videoResult.fileUrl + } + } + + /// 获取缩略图URL(如果有) + public var thumbnailUrl: String? { + switch self { + case .file: + return nil + case .video(_, let thumbnailResult): + return thumbnailResult?.fileUrl + } + } } /// 上传进度信息 diff --git a/wake/View/Components/Upload/ImageUploaderGetID.swift b/wake/View/Components/Upload/ImageUploaderGetID.swift index 21c4c7a..764a55f 100644 --- a/wake/View/Components/Upload/ImageUploaderGetID.swift +++ b/wake/View/Components/Upload/ImageUploaderGetID.swift @@ -12,12 +12,14 @@ public class ImageUploaderGetID: ObservableObject { public let fileName: String public let fileSize: Int public let fileId: String + public let previewFileId: String? - public init(fileUrl: String, fileName: String, fileSize: Int, fileId: String) { + public init(fileUrl: String, fileName: String, fileSize: Int, fileId: String, previewFileId: String? = nil) { self.fileUrl = fileUrl self.fileName = fileName self.fileSize = fileSize self.fileId = fileId + self.previewFileId = previewFileId } } @@ -90,7 +92,11 @@ public class ImageUploaderGetID: ObservableObject { } // 2. 获取上传URL - getUploadURL(for: imageData) { [weak self] result in + getUploadURL( + for: imageData, + mimeType: "image/jpeg", + originalFilename: "image_\(UUID().uuidString).jpg" + ) { [weak self] result in switch result { case .success((let fileId, let uploadURL)): print("📤 获取到上传URL,开始上传文件...") @@ -110,7 +116,7 @@ public class ImageUploaderGetID: ObservableObject { // 4. 确认上传 self?.confirmUpload( fileId: fileId, - fileName: "avatar_\(UUID().uuidString).jpg", + fileName: "image_\(UUID().uuidString).jpg", fileSize: imageData.count, completion: completion ) @@ -206,76 +212,6 @@ public class ImageUploaderGetID: ObservableObject { // MARK: - 私有方法 - /// 获取上传URL - private func getUploadURL( - for imageData: Data, - completion: @escaping (Result<(fileId: String, uploadURL: URL), Error>) -> Void - ) { - let fileName = "avatar_\(UUID().uuidString).jpg" - let parameters: [String: Any] = [ - "filename": fileName, - "content_type": "image/jpeg", - "file_size": imageData.count - ] - - let urlString = "\(apiConfig.baseURL)/file/generate-upload-url" - guard let url = URL(string: urlString) else { - completion(.failure(UploadError.invalidURL)) - return - } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.allHTTPHeaderFields = apiConfig.authHeaders - - do { - request.httpBody = try JSONSerialization.data(withJSONObject: parameters) - print("📤 准备上传请求,文件名: \(fileName), 大小: \(Double(imageData.count) / 1024.0) KB") - } catch { - print("❌ 序列化请求参数失败: \(error.localizedDescription)") - completion(.failure(error)) - return - } - - let task = session.dataTask(with: request) { data, response, error in - if let error = error { - completion(.failure(UploadError.uploadFailed(error))) - return - } - - guard let httpResponse = response as? HTTPURLResponse else { - completion(.failure(UploadError.invalidResponse)) - return - } - - guard let data = data else { - completion(.failure(UploadError.invalidResponse)) - return - } - - // 打印调试信息 - if let responseString = String(data: data, encoding: .utf8) { - print("📥 获取上传URL响应: \(responseString)") - } - - do { - guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], - let dataDict = json["data"] as? [String: Any], - let fileId = dataDict["file_id"] as? String, - let uploadURLString = dataDict["upload_url"] as? String, - let uploadURL = URL(string: uploadURLString) else { - throw UploadError.invalidResponse - } - - completion(.success((fileId: fileId, uploadURL: uploadURL))) - } catch { - completion(.failure(UploadError.invalidResponse)) - } - } - - task.resume() - } - /// 获取上传URL private func getUploadURL( for fileData: Data, @@ -291,7 +227,14 @@ public class ImageUploaderGetID: ObservableObject { ] let urlString = "\(apiConfig.baseURL)/file/generate-upload-url" + print("🌐 准备请求上传URL...") + print(" - 目标URL: \(urlString)") + print(" - 文件名: \(fileName)") + print(" - 文件大小: \(Double(fileData.count) / 1024.0) KB") + print(" - MIME类型: \(mimeType)") + guard let url = URL(string: urlString) else { + print("❌ 错误: 无效的URL: \(urlString)") completion(.failure(UploadError.invalidURL)) return } @@ -302,7 +245,9 @@ public class ImageUploaderGetID: ObservableObject { do { request.httpBody = try JSONSerialization.data(withJSONObject: parameters) - print("📤 准备上传请求,文件名: \(fileName), 大小: \(Double(fileData.count) / 1024.0) KB") + if let bodyString = String(data: request.httpBody ?? Data(), encoding: .utf8) { + print("📤 请求体: \(bodyString)") + } } catch { print("❌ 序列化请求参数失败: \(error.localizedDescription)") completion(.failure(error)) @@ -311,37 +256,61 @@ public class ImageUploaderGetID: ObservableObject { let task = session.dataTask(with: request) { data, response, error in if let error = error { + print("❌ 获取上传URL请求失败: \(error.localizedDescription)") completion(.failure(UploadError.uploadFailed(error))) return } guard let httpResponse = response as? HTTPURLResponse else { + print("❌ 无效的服务器响应") completion(.failure(UploadError.invalidResponse)) return } + print("📥 收到上传URL响应") + print(" - 状态码: \(httpResponse.statusCode)") + guard let data = data else { + print("❌ 响应数据为空") completion(.failure(UploadError.invalidResponse)) return } - // 打印调试信息 + // 打印响应头 + print(" - 响应头: \(httpResponse.allHeaderFields)") + + // 打印响应体 if let responseString = String(data: data, encoding: .utf8) { - print("📥 上传URL响应: \(responseString)") + print(" - 响应体: \(responseString)") } do { let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] - guard let code = json?["code"] as? Int, code == 0, - let dataDict = json?["data"] as? [String: Any], + print(" - 解析的JSON: \(String(describing: json))") + + guard let code = json?["code"] as? Int, code == 0 else { + let errorMessage = json?["message"] as? String ?? "未知错误" + print("❌ 服务器返回错误: \(errorMessage)") + completion(.failure(UploadError.serverError(errorMessage))) + return + } + + guard let dataDict = json?["data"] as? [String: Any], let fileId = dataDict["file_id"] as? String, let uploadURLString = dataDict["upload_url"] as? String, let uploadURL = URL(string: uploadURLString) else { - throw UploadError.invalidResponse + print("❌ 响应数据格式错误") + completion(.failure(UploadError.invalidResponse)) + return } + print("✅ 成功获取上传URL") + print(" - 文件ID: \(fileId)") + print(" - 上传URL: \(uploadURLString)") + completion(.success((fileId: fileId, uploadURL: uploadURL))) } catch { + print("❌ 解析响应数据失败: \(error.localizedDescription)") completion(.failure(UploadError.invalidResponse)) } } @@ -421,75 +390,61 @@ public class ImageUploaderGetID: ObservableObject { public func uploadFile( fileData: Data, to uploadURL: URL, - mimeType: String = "application/octet-stream", + mimeType: String, onProgress: @escaping (Double) -> Void, completion: @escaping (Result) -> Void ) -> URLSessionUploadTask { + print("📤 开始上传文件...") + var request = URLRequest(url: uploadURL) request.httpMethod = "PUT" request.setValue(mimeType, forHTTPHeaderField: "Content-Type") - let task = session.uploadTask(with: request, from: fileData) { _, response, error in + let task = session.uploadTask( + with: request, + from: fileData + ) { data, response, error in if let error = error { - completion(.failure(error)) + print("❌ 文件上传失败: \(error.localizedDescription)") + completion(.failure(UploadError.uploadFailed(error))) return } guard let httpResponse = response as? HTTPURLResponse else { + print("❌ 无效的响应") completion(.failure(UploadError.invalidResponse)) return } guard (200...299).contains(httpResponse.statusCode) else { let statusCode = httpResponse.statusCode - completion(.failure(UploadError.serverError("上传失败,状态码: \(statusCode)"))) + print("❌ 服务器返回错误状态码: \(statusCode)") + completion(.failure(UploadError.serverError("HTTP \(statusCode)"))) return } + print("✅ 文件上传成功") completion(.success(())) } // 添加进度观察 - if #available(iOS 11.0, *) { - let progressObserver = task.progress.observe(\.fractionCompleted) { progressValue, _ in - DispatchQueue.main.async { - onProgress(progressValue.fractionCompleted) - } - } - - task.addCompletionHandler { [weak task] in - progressObserver.invalidate() - task?.progress.cancel() - } - } else { - var lastProgress: Double = 0 - let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in - let bytesSent = task.countOfBytesSent - let totalBytes = task.countOfBytesExpectedToSend - let currentProgress = totalBytes > 0 ? Double(bytesSent) / Double(totalBytes) : 0 - - // 只有当进度有显著变化时才回调,避免频繁更新UI - if abs(currentProgress - lastProgress) > 0.01 || currentProgress >= 1.0 { - lastProgress = currentProgress - DispatchQueue.main.async { - onProgress(min(currentProgress, 1.0)) - } - } - - if currentProgress >= 1.0 { - timer.invalidate() - } - } - - task.addCompletionHandler { - timer.invalidate() - } + let progressObserver = task.progress.observe(\.fractionCompleted) { progress, _ in + let percentComplete = progress.fractionCompleted + print("📊 文件上传进度: \(Int(percentComplete * 100))%") + onProgress(percentComplete) } + // 存储观察者以避免提前释放 + objc_setAssociatedObject(task, &AssociatedKeys.progressObserver, progressObserver, .OBJC_ASSOCIATION_RETAIN) + task.resume() return task } + private struct AssociatedKeys { + static var progressObserver = "progressObserver" + } + // MARK: - 文件上传状态 /// 文件上传状态 diff --git a/wake/View/Components/Upload/MediaUpload.swift b/wake/View/Components/Upload/MediaUpload.swift index 8acd520..333059e 100644 --- a/wake/View/Components/Upload/MediaUpload.swift +++ b/wake/View/Components/Upload/MediaUpload.swift @@ -60,7 +60,7 @@ public class MediaUploadManager: ObservableObject { /// 上传状态 @Published public private(set) var uploadStatus: [String: MediaUploadStatus] = [:] /// 上传结果 - @Published public private(set) var uploadResults: [String: String] = [:] // Store fileId as String + @Published public private(set) var uploadResults: [String: UploadResult] = [:] private let uploader = ImageUploadService() // Use ImageUploadService private let logger = Logger(subsystem: "com.yourapp.media", category: "MediaUploadManager") @@ -125,14 +125,8 @@ public class MediaUploadManager: ObservableObject { } /// 获取上传结果 - public func getUploadResults() -> [String: String] { - var results: [String: String] = [:] - for (id, status) in uploadStatus { - if case .completed(let fileId) = status { - results[id] = fileId - } - } - return results + public func getUploadResults() -> [String: UploadResult] { + return uploadResults } /// 检查是否所有上传都已完成 @@ -146,6 +140,21 @@ public class MediaUploadManager: ObservableObject { // MARK: - Private Methods + /// 上传结果 + public struct UploadResult: Codable, Equatable { + public let fileId: String + public let thumbnailId: String? + + public init(fileId: String, thumbnailId: String? = nil) { + self.fileId = fileId + self.thumbnailId = thumbnailId + } + + public static func == (lhs: UploadResult, rhs: UploadResult) -> Bool { + return lhs.fileId == rhs.fileId && lhs.thumbnailId == rhs.thumbnailId + } + } + private func uploadMedia(_ media: MediaType) { logger.info("🔄 开始处理媒体: \(media.id)") @@ -175,9 +184,23 @@ public class MediaUploadManager: ObservableObject { Task { @MainActor in switch result { case .success(let uploadResult): - let fileId = uploadResult.fileId - self.logger.info("✅ 上传成功 (\(media.id)): \(fileId)") - self.uploadResults[media.id] = fileId + // 处理上传结果 + let fileId: String + let thumbnailId: String? + + switch uploadResult { + case .file(let result): + fileId = result.fileId + thumbnailId = nil + case .video(let video, let thumbnail): + fileId = video.fileId + thumbnailId = thumbnail?.fileId + } + + // 保存上传结果 + let result = UploadResult(fileId: fileId, thumbnailId: thumbnailId) + self.uploadResults[media.id] = result + self.logger.info("✅ 上传成功 (\(media.id)): \(fileId), 缩略图ID: \(thumbnailId ?? "无")") self.updateStatus(for: media.id, status: .completed(fileId: fileId)) // 打印上传结果 @@ -205,8 +228,8 @@ public class MediaUploadManager: ObservableObject { let results = self.selectedMedia.compactMap { media -> [String: String]? in guard let result = self.uploadResults[media.id] else { return nil } return [ - "file_id": result, - "preview_file_id": result + "file_id": result.fileId, + "preview_file_id": result.thumbnailId ?? result.fileId ] } diff --git a/wake/View/Memories/MemoriesView.swift b/wake/View/Memories/MemoriesView.swift index 25872a7..08746c8 100644 --- a/wake/View/Memories/MemoriesView.swift +++ b/wake/View/Memories/MemoriesView.swift @@ -8,23 +8,38 @@ struct MaterialResponse: Decodable { struct MaterialData: Decodable { let items: [MemoryItem] + let hasMore: Bool + + enum CodingKeys: String, CodingKey { + case items + case hasMore = "has_more" + } } } struct MemoryItem: Identifiable, Decodable { let id: String - let name: String - let description: String + let name: String? + let description: String? let fileInfo: FileInfo + let previewFileInfo: FileInfo - var title: String { name } - var subtitle: String { description } - var mediaType: MemoryMediaType { .image(fileInfo.url) } - var aspectRatio: CGFloat { 1.0 } // Default to square, adjust based on actual image dimensions if needed + var title: String { name ?? "Untitled" } + var subtitle: String { description ?? "" } + var mediaType: MemoryMediaType { + let url = fileInfo.url.lowercased() + if url.hasSuffix(".mp4") || url.hasSuffix(".mov") { + return .video(url: fileInfo.url, previewUrl: previewFileInfo.url) + } else { + return .image(fileInfo.url) + } + } + var aspectRatio: CGFloat { 1.0 } enum CodingKeys: String, CodingKey { case id, name, description case fileInfo = "file_info" + case previewFileInfo = "preview_file_info" } } @@ -42,7 +57,7 @@ struct FileInfo: Decodable { enum MemoryMediaType: Equatable { case image(String) - case video(String) + case video(url: String, previewUrl: String) } struct MemoriesView: View { @@ -57,28 +72,38 @@ struct MemoriesView: View { var body: some View { NavigationView { - Group { - if isLoading { - ProgressView() - .scaleEffect(1.5) - } else if let error = errorMessage { - Text("Error: \(error)") - .foregroundColor(.red) - } else { - ScrollView { - LazyVGrid(columns: columns, spacing: 4) { - ForEach(memories) { memory in - MemoryCard(memory: memory) - .padding(.horizontal, 2) + ZStack { + Color.themeTextWhiteSecondary.ignoresSafeArea() + + Group { + if isLoading { + ProgressView() + .scaleEffect(1.5) + } else if let error = errorMessage { + Text("Error: \(error)") + .foregroundColor(.red) + } else { + ScrollView { + LazyVGrid(columns: columns, spacing: 4) { + ForEach(memories) { memory in + MemoryCard(memory: memory) + .padding(.horizontal, 2) + } } + .padding(.top, 4) + .padding(.horizontal, 4) } - .padding(.top, 4) - .padding(.horizontal, 4) } } } .navigationTitle("My Memories") .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + EmptyView() + } + } .onAppear { fetchMemories() } @@ -97,9 +122,9 @@ struct MemoriesView: View { switch result { case .success(let response): - print("✅ Successfully fetched \(response.data.items.count) memory items") + print("✅ Successfully fetched \(response.data.items) memory items") response.data.items.forEach { item in - print("📝 Item ID: \(item.id), Title: \(item.name), URL: \(item.fileInfo.url)") + print("📝 Item ID: \(item.id), Title: \(item.name ?? "Untitled"), URL: \(item)") } self.memories = response.data.items case .failure(let error): @@ -135,32 +160,37 @@ struct MemoryCard: View { } } - case .video(let urlString): - if let url = URL(string: urlString) { - VideoPlayer(player: AVPlayer(url: url)) - .aspectRatio(memory.aspectRatio, contentMode: .fill) - .onAppear { - // The video will be shown with a play button overlay - // and will only play when tapped + case .video(let url, let previewUrl): + // Use preview image for video + if let previewUrl = URL(string: previewUrl) { + AsyncImage(url: previewUrl) { phase in + if let image = phase.image { + image + .resizable() + .aspectRatio(contentMode: .fill) + } else if phase.error != nil { + Color.gray.opacity(0.3) + } else { + ProgressView() } + } } else { Color.gray.opacity(0.3) - .aspectRatio(memory.aspectRatio, contentMode: .fill) } } } - .frame(width: (UIScreen.main.bounds.width / 2) - 24, height: (UIScreen.main.bounds.width / 2 - 24) * (1/memory.aspectRatio)) + .frame(width: (UIScreen.main.bounds.width / 2) - 24, + height: (UIScreen.main.bounds.width / 2 - 24) * (1/memory.aspectRatio)) .clipped() .cornerRadius(12) - .overlay( - Group { - if case .video = memory.mediaType { - Image(systemName: "play.circle.fill") - .font(.system(size: 40)) - .foregroundColor(.white.opacity(0.9)) - } - } - ) + + // Show play button for videos + if case .video = memory.mediaType { + Image(systemName: "play.circle.fill") + .font(.system(size: 40)) + .foregroundColor(.white.opacity(0.9)) + .shadow(radius: 3) + } } // Title and Subtitle diff --git a/wake/View/Upload/MediaUploadView.swift b/wake/View/Upload/MediaUploadView.swift index 75ff363..683d69e 100644 --- a/wake/View/Upload/MediaUploadView.swift +++ b/wake/View/Upload/MediaUploadView.swift @@ -1,5 +1,8 @@ import SwiftUI -import AVFoundation +import PhotosUI +import AVKit +import CoreTransferable +import CoreImage.CIFilterBuiltins extension Notification.Name { static let didAddFirstMedia = Notification.Name("didAddFirstMedia") @@ -296,33 +299,33 @@ struct MediaUploadView: View { } /// 处理上传完成 - private func handleUploadCompletion(results: [String: String]) { - uploadedFileIds = results.map { ["file_id": $0.value, "preview_file_id": $0.value] } - uploadComplete = !uploadedFileIds.isEmpty - - // 打印结果到控制台 - if let jsonData = try? JSONSerialization.data(withJSONObject: uploadedFileIds, options: .prettyPrinted), - let jsonString = String(data: jsonData, encoding: .utf8) { - print("📦 上传完成,文件ID列表:") - print(jsonString) + 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() { - // 获取所有已上传文件的ID - let fileIds = uploadManager.uploadResults.map { $0.value } - - guard !fileIds.isEmpty else { + // 获取所有已上传文件的结果 + let uploadResults = uploadManager.uploadResults + guard !uploadResults.isEmpty else { print("⚠️ 没有可用的文件ID") return } // 准备请求参数 - let files = fileIds.map { fileId -> [String: String] in + let files = uploadResults.map { (_, result) -> [String: String] in return [ - "file_id": fileId, - "preview_file_id": fileId + "file_id": result.fileId, + "preview_file_id": result.thumbnailId ?? result.fileId ] }