feat: 视频缩略图
This commit is contained in:
parent
008778d9a6
commit
96e58806d4
@ -199,32 +199,48 @@ public class ImageUploadService {
|
|||||||
|
|
||||||
self.uploader.uploadImage(
|
self.uploader.uploadImage(
|
||||||
compressedThumbnail,
|
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
|
completion: { thumbnailResult in
|
||||||
switch thumbnailResult {
|
switch thumbnailResult {
|
||||||
case .success(let thumbnailUploadResult):
|
case .success(let thumbnailUploadResult):
|
||||||
print("✅ 视频缩略图上传完成, fileId: \(thumbnailUploadResult.fileId)")
|
print("✅ 视频缩略图上传完成, fileId: \(thumbnailUploadResult.fileId)")
|
||||||
let result = MediaUploadResult.video(
|
|
||||||
video: videoResult,
|
// 确保返回的视频结果中,preview_file_id 是缩略图的 ID
|
||||||
thumbnail: thumbnailUploadResult
|
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):
|
case .failure(let error):
|
||||||
print("❌ 视频缩略图上传失败: \(error.localizedDescription)")
|
print("❌ 视频缩略图上传失败: \(error.localizedDescription)")
|
||||||
completion(.failure(error))
|
// 即使缩略图上传失败,也返回视频上传成功
|
||||||
|
completion(.success(.video(video: videoResult, thumbnail: nil)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let error = NSError(domain: "ImageUploadService", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to compress thumbnail"])
|
// 缩略图压缩失败,只返回视频
|
||||||
print("❌ 视频缩略图压缩失败")
|
completion(.success(.video(video: videoResult, thumbnail: nil)))
|
||||||
completion(.failure(error))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
print("❌ 视频缩略图提取失败: \(error.localizedDescription)")
|
print("❌ 视频缩略图提取失败: \(error.localizedDescription)")
|
||||||
completion(.failure(error))
|
// 缩略图提取失败,只返回视频
|
||||||
|
completion(.success(.video(video: videoResult, thumbnail: nil)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,7 +368,7 @@ public class ImageUploadService {
|
|||||||
/// 媒体上传结果
|
/// 媒体上传结果
|
||||||
public enum MediaUploadResult {
|
public enum MediaUploadResult {
|
||||||
case file(ImageUploaderGetID.UploadResult)
|
case file(ImageUploaderGetID.UploadResult)
|
||||||
case video(video: ImageUploaderGetID.UploadResult, thumbnail: ImageUploaderGetID.UploadResult)
|
case video(video: ImageUploaderGetID.UploadResult, thumbnail: ImageUploaderGetID.UploadResult?)
|
||||||
|
|
||||||
/// 获取文件ID(对于视频,返回视频文件的ID)
|
/// 获取文件ID(对于视频,返回视频文件的ID)
|
||||||
public var fileId: String {
|
public var fileId: String {
|
||||||
@ -363,6 +379,36 @@ public class ImageUploadService {
|
|||||||
return videoResult.fileId
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 上传进度信息
|
/// 上传进度信息
|
||||||
|
|||||||
@ -12,12 +12,14 @@ public class ImageUploaderGetID: ObservableObject {
|
|||||||
public let fileName: String
|
public let fileName: String
|
||||||
public let fileSize: Int
|
public let fileSize: Int
|
||||||
public let fileId: String
|
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.fileUrl = fileUrl
|
||||||
self.fileName = fileName
|
self.fileName = fileName
|
||||||
self.fileSize = fileSize
|
self.fileSize = fileSize
|
||||||
self.fileId = fileId
|
self.fileId = fileId
|
||||||
|
self.previewFileId = previewFileId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +92,11 @@ public class ImageUploaderGetID: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. 获取上传URL
|
// 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 {
|
switch result {
|
||||||
case .success((let fileId, let uploadURL)):
|
case .success((let fileId, let uploadURL)):
|
||||||
print("📤 获取到上传URL,开始上传文件...")
|
print("📤 获取到上传URL,开始上传文件...")
|
||||||
@ -110,7 +116,7 @@ public class ImageUploaderGetID: ObservableObject {
|
|||||||
// 4. 确认上传
|
// 4. 确认上传
|
||||||
self?.confirmUpload(
|
self?.confirmUpload(
|
||||||
fileId: fileId,
|
fileId: fileId,
|
||||||
fileName: "avatar_\(UUID().uuidString).jpg",
|
fileName: "image_\(UUID().uuidString).jpg",
|
||||||
fileSize: imageData.count,
|
fileSize: imageData.count,
|
||||||
completion: completion
|
completion: completion
|
||||||
)
|
)
|
||||||
@ -206,76 +212,6 @@ public class ImageUploaderGetID: ObservableObject {
|
|||||||
|
|
||||||
// MARK: - 私有方法
|
// 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
|
/// 获取上传URL
|
||||||
private func getUploadURL(
|
private func getUploadURL(
|
||||||
for fileData: Data,
|
for fileData: Data,
|
||||||
@ -291,7 +227,14 @@ public class ImageUploaderGetID: ObservableObject {
|
|||||||
]
|
]
|
||||||
|
|
||||||
let urlString = "\(apiConfig.baseURL)/file/generate-upload-url"
|
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 {
|
guard let url = URL(string: urlString) else {
|
||||||
|
print("❌ 错误: 无效的URL: \(urlString)")
|
||||||
completion(.failure(UploadError.invalidURL))
|
completion(.failure(UploadError.invalidURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -302,7 +245,9 @@ public class ImageUploaderGetID: ObservableObject {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
|
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 {
|
} catch {
|
||||||
print("❌ 序列化请求参数失败: \(error.localizedDescription)")
|
print("❌ 序列化请求参数失败: \(error.localizedDescription)")
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
@ -311,37 +256,61 @@ public class ImageUploaderGetID: ObservableObject {
|
|||||||
|
|
||||||
let task = session.dataTask(with: request) { data, response, error in
|
let task = session.dataTask(with: request) { data, response, error in
|
||||||
if let error = error {
|
if let error = error {
|
||||||
|
print("❌ 获取上传URL请求失败: \(error.localizedDescription)")
|
||||||
completion(.failure(UploadError.uploadFailed(error)))
|
completion(.failure(UploadError.uploadFailed(error)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let httpResponse = response as? HTTPURLResponse else {
|
guard let httpResponse = response as? HTTPURLResponse else {
|
||||||
|
print("❌ 无效的服务器响应")
|
||||||
completion(.failure(UploadError.invalidResponse))
|
completion(.failure(UploadError.invalidResponse))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print("📥 收到上传URL响应")
|
||||||
|
print(" - 状态码: \(httpResponse.statusCode)")
|
||||||
|
|
||||||
guard let data = data else {
|
guard let data = data else {
|
||||||
|
print("❌ 响应数据为空")
|
||||||
completion(.failure(UploadError.invalidResponse))
|
completion(.failure(UploadError.invalidResponse))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打印调试信息
|
// 打印响应头
|
||||||
|
print(" - 响应头: \(httpResponse.allHeaderFields)")
|
||||||
|
|
||||||
|
// 打印响应体
|
||||||
if let responseString = String(data: data, encoding: .utf8) {
|
if let responseString = String(data: data, encoding: .utf8) {
|
||||||
print("📥 上传URL响应: \(responseString)")
|
print(" - 响应体: \(responseString)")
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||||
guard let code = json?["code"] as? Int, code == 0,
|
print(" - 解析的JSON: \(String(describing: json))")
|
||||||
let dataDict = json?["data"] as? [String: Any],
|
|
||||||
|
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 fileId = dataDict["file_id"] as? String,
|
||||||
let uploadURLString = dataDict["upload_url"] as? String,
|
let uploadURLString = dataDict["upload_url"] as? String,
|
||||||
let uploadURL = URL(string: uploadURLString) else {
|
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)))
|
completion(.success((fileId: fileId, uploadURL: uploadURL)))
|
||||||
} catch {
|
} catch {
|
||||||
|
print("❌ 解析响应数据失败: \(error.localizedDescription)")
|
||||||
completion(.failure(UploadError.invalidResponse))
|
completion(.failure(UploadError.invalidResponse))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -421,75 +390,61 @@ public class ImageUploaderGetID: ObservableObject {
|
|||||||
public func uploadFile(
|
public func uploadFile(
|
||||||
fileData: Data,
|
fileData: Data,
|
||||||
to uploadURL: URL,
|
to uploadURL: URL,
|
||||||
mimeType: String = "application/octet-stream",
|
mimeType: String,
|
||||||
onProgress: @escaping (Double) -> Void,
|
onProgress: @escaping (Double) -> Void,
|
||||||
completion: @escaping (Result<Void, Error>) -> Void
|
completion: @escaping (Result<Void, Error>) -> Void
|
||||||
) -> URLSessionUploadTask {
|
) -> URLSessionUploadTask {
|
||||||
|
print("📤 开始上传文件...")
|
||||||
|
|
||||||
var request = URLRequest(url: uploadURL)
|
var request = URLRequest(url: uploadURL)
|
||||||
request.httpMethod = "PUT"
|
request.httpMethod = "PUT"
|
||||||
request.setValue(mimeType, forHTTPHeaderField: "Content-Type")
|
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 {
|
if let error = error {
|
||||||
completion(.failure(error))
|
print("❌ 文件上传失败: \(error.localizedDescription)")
|
||||||
|
completion(.failure(UploadError.uploadFailed(error)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let httpResponse = response as? HTTPURLResponse else {
|
guard let httpResponse = response as? HTTPURLResponse else {
|
||||||
|
print("❌ 无效的响应")
|
||||||
completion(.failure(UploadError.invalidResponse))
|
completion(.failure(UploadError.invalidResponse))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard (200...299).contains(httpResponse.statusCode) else {
|
guard (200...299).contains(httpResponse.statusCode) else {
|
||||||
let statusCode = httpResponse.statusCode
|
let statusCode = httpResponse.statusCode
|
||||||
completion(.failure(UploadError.serverError("上传失败,状态码: \(statusCode)")))
|
print("❌ 服务器返回错误状态码: \(statusCode)")
|
||||||
|
completion(.failure(UploadError.serverError("HTTP \(statusCode)")))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print("✅ 文件上传成功")
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加进度观察
|
// 添加进度观察
|
||||||
if #available(iOS 11.0, *) {
|
let progressObserver = task.progress.observe(\.fractionCompleted) { progress, _ in
|
||||||
let progressObserver = task.progress.observe(\.fractionCompleted) { progressValue, _ in
|
let percentComplete = progress.fractionCompleted
|
||||||
DispatchQueue.main.async {
|
print("📊 文件上传进度: \(Int(percentComplete * 100))%")
|
||||||
onProgress(progressValue.fractionCompleted)
|
onProgress(percentComplete)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 存储观察者以避免提前释放
|
||||||
|
objc_setAssociatedObject(task, &AssociatedKeys.progressObserver, progressObserver, .OBJC_ASSOCIATION_RETAIN)
|
||||||
|
|
||||||
task.resume()
|
task.resume()
|
||||||
return task
|
return task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct AssociatedKeys {
|
||||||
|
static var progressObserver = "progressObserver"
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - 文件上传状态
|
// MARK: - 文件上传状态
|
||||||
|
|
||||||
/// 文件上传状态
|
/// 文件上传状态
|
||||||
|
|||||||
@ -60,7 +60,7 @@ public class MediaUploadManager: ObservableObject {
|
|||||||
/// 上传状态
|
/// 上传状态
|
||||||
@Published public private(set) var uploadStatus: [String: MediaUploadStatus] = [:]
|
@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 uploader = ImageUploadService() // Use ImageUploadService
|
||||||
private let logger = Logger(subsystem: "com.yourapp.media", category: "MediaUploadManager")
|
private let logger = Logger(subsystem: "com.yourapp.media", category: "MediaUploadManager")
|
||||||
@ -125,14 +125,8 @@ public class MediaUploadManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 获取上传结果
|
/// 获取上传结果
|
||||||
public func getUploadResults() -> [String: String] {
|
public func getUploadResults() -> [String: UploadResult] {
|
||||||
var results: [String: String] = [:]
|
return uploadResults
|
||||||
for (id, status) in uploadStatus {
|
|
||||||
if case .completed(let fileId) = status {
|
|
||||||
results[id] = fileId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 检查是否所有上传都已完成
|
/// 检查是否所有上传都已完成
|
||||||
@ -146,6 +140,21 @@ public class MediaUploadManager: ObservableObject {
|
|||||||
|
|
||||||
// MARK: - Private Methods
|
// 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) {
|
private func uploadMedia(_ media: MediaType) {
|
||||||
logger.info("🔄 开始处理媒体: \(media.id)")
|
logger.info("🔄 开始处理媒体: \(media.id)")
|
||||||
|
|
||||||
@ -175,9 +184,23 @@ public class MediaUploadManager: ObservableObject {
|
|||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let uploadResult):
|
case .success(let uploadResult):
|
||||||
let fileId = uploadResult.fileId
|
// 处理上传结果
|
||||||
self.logger.info("✅ 上传成功 (\(media.id)): \(fileId)")
|
let fileId: String
|
||||||
self.uploadResults[media.id] = fileId
|
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))
|
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
|
let results = self.selectedMedia.compactMap { media -> [String: String]? in
|
||||||
guard let result = self.uploadResults[media.id] else { return nil }
|
guard let result = self.uploadResults[media.id] else { return nil }
|
||||||
return [
|
return [
|
||||||
"file_id": result,
|
"file_id": result.fileId,
|
||||||
"preview_file_id": result
|
"preview_file_id": result.thumbnailId ?? result.fileId
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,23 +8,38 @@ struct MaterialResponse: Decodable {
|
|||||||
|
|
||||||
struct MaterialData: Decodable {
|
struct MaterialData: Decodable {
|
||||||
let items: [MemoryItem]
|
let items: [MemoryItem]
|
||||||
|
let hasMore: Bool
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case items
|
||||||
|
case hasMore = "has_more"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MemoryItem: Identifiable, Decodable {
|
struct MemoryItem: Identifiable, Decodable {
|
||||||
let id: String
|
let id: String
|
||||||
let name: String
|
let name: String?
|
||||||
let description: String
|
let description: String?
|
||||||
let fileInfo: FileInfo
|
let fileInfo: FileInfo
|
||||||
|
let previewFileInfo: FileInfo
|
||||||
|
|
||||||
var title: String { name }
|
var title: String { name ?? "Untitled" }
|
||||||
var subtitle: String { description }
|
var subtitle: String { description ?? "" }
|
||||||
var mediaType: MemoryMediaType { .image(fileInfo.url) }
|
var mediaType: MemoryMediaType {
|
||||||
var aspectRatio: CGFloat { 1.0 } // Default to square, adjust based on actual image dimensions if needed
|
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 {
|
enum CodingKeys: String, CodingKey {
|
||||||
case id, name, description
|
case id, name, description
|
||||||
case fileInfo = "file_info"
|
case fileInfo = "file_info"
|
||||||
|
case previewFileInfo = "preview_file_info"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,7 +57,7 @@ struct FileInfo: Decodable {
|
|||||||
|
|
||||||
enum MemoryMediaType: Equatable {
|
enum MemoryMediaType: Equatable {
|
||||||
case image(String)
|
case image(String)
|
||||||
case video(String)
|
case video(url: String, previewUrl: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MemoriesView: View {
|
struct MemoriesView: View {
|
||||||
@ -57,28 +72,38 @@ struct MemoriesView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
Group {
|
ZStack {
|
||||||
if isLoading {
|
Color.themeTextWhiteSecondary.ignoresSafeArea()
|
||||||
ProgressView()
|
|
||||||
.scaleEffect(1.5)
|
Group {
|
||||||
} else if let error = errorMessage {
|
if isLoading {
|
||||||
Text("Error: \(error)")
|
ProgressView()
|
||||||
.foregroundColor(.red)
|
.scaleEffect(1.5)
|
||||||
} else {
|
} else if let error = errorMessage {
|
||||||
ScrollView {
|
Text("Error: \(error)")
|
||||||
LazyVGrid(columns: columns, spacing: 4) {
|
.foregroundColor(.red)
|
||||||
ForEach(memories) { memory in
|
} else {
|
||||||
MemoryCard(memory: memory)
|
ScrollView {
|
||||||
.padding(.horizontal, 2)
|
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")
|
.navigationTitle("My Memories")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.navigationBarBackButtonHidden(true)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
fetchMemories()
|
fetchMemories()
|
||||||
}
|
}
|
||||||
@ -97,9 +122,9 @@ struct MemoriesView: View {
|
|||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let response):
|
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
|
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
|
self.memories = response.data.items
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
@ -135,32 +160,37 @@ struct MemoryCard: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case .video(let urlString):
|
case .video(let url, let previewUrl):
|
||||||
if let url = URL(string: urlString) {
|
// Use preview image for video
|
||||||
VideoPlayer(player: AVPlayer(url: url))
|
if let previewUrl = URL(string: previewUrl) {
|
||||||
.aspectRatio(memory.aspectRatio, contentMode: .fill)
|
AsyncImage(url: previewUrl) { phase in
|
||||||
.onAppear {
|
if let image = phase.image {
|
||||||
// The video will be shown with a play button overlay
|
image
|
||||||
// and will only play when tapped
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
} else if phase.error != nil {
|
||||||
|
Color.gray.opacity(0.3)
|
||||||
|
} else {
|
||||||
|
ProgressView()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Color.gray.opacity(0.3)
|
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()
|
.clipped()
|
||||||
.cornerRadius(12)
|
.cornerRadius(12)
|
||||||
.overlay(
|
|
||||||
Group {
|
// Show play button for videos
|
||||||
if case .video = memory.mediaType {
|
if case .video = memory.mediaType {
|
||||||
Image(systemName: "play.circle.fill")
|
Image(systemName: "play.circle.fill")
|
||||||
.font(.system(size: 40))
|
.font(.system(size: 40))
|
||||||
.foregroundColor(.white.opacity(0.9))
|
.foregroundColor(.white.opacity(0.9))
|
||||||
}
|
.shadow(radius: 3)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Title and Subtitle
|
// Title and Subtitle
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import AVFoundation
|
import PhotosUI
|
||||||
|
import AVKit
|
||||||
|
import CoreTransferable
|
||||||
|
import CoreImage.CIFilterBuiltins
|
||||||
|
|
||||||
extension Notification.Name {
|
extension Notification.Name {
|
||||||
static let didAddFirstMedia = Notification.Name("didAddFirstMedia")
|
static let didAddFirstMedia = Notification.Name("didAddFirstMedia")
|
||||||
@ -296,33 +299,33 @@ struct MediaUploadView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 处理上传完成
|
/// 处理上传完成
|
||||||
private func handleUploadCompletion(results: [String: String]) {
|
private func handleUploadCompletion(results: [String: MediaUploadManager.UploadResult]) {
|
||||||
uploadedFileIds = results.map { ["file_id": $0.value, "preview_file_id": $0.value] }
|
// 转换为需要的格式
|
||||||
uploadComplete = !uploadedFileIds.isEmpty
|
let formattedResults = results.map { (_, result) -> [String: String] in
|
||||||
|
return [
|
||||||
// 打印结果到控制台
|
"file_id": result.fileId,
|
||||||
if let jsonData = try? JSONSerialization.data(withJSONObject: uploadedFileIds, options: .prettyPrinted),
|
"preview_file_id": result.thumbnailId ?? result.fileId
|
||||||
let jsonString = String(data: jsonData, encoding: .utf8) {
|
]
|
||||||
print("📦 上传完成,文件ID列表:")
|
|
||||||
print(jsonString)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploadedFileIds = formattedResults
|
||||||
|
uploadComplete = !uploadedFileIds.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 处理继续按钮点击
|
/// 处理继续按钮点击
|
||||||
private func handleContinue() {
|
private func handleContinue() {
|
||||||
// 获取所有已上传文件的ID
|
// 获取所有已上传文件的结果
|
||||||
let fileIds = uploadManager.uploadResults.map { $0.value }
|
let uploadResults = uploadManager.uploadResults
|
||||||
|
guard !uploadResults.isEmpty else {
|
||||||
guard !fileIds.isEmpty else {
|
|
||||||
print("⚠️ 没有可用的文件ID")
|
print("⚠️ 没有可用的文件ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 准备请求参数
|
// 准备请求参数
|
||||||
let files = fileIds.map { fileId -> [String: String] in
|
let files = uploadResults.map { (_, result) -> [String: String] in
|
||||||
return [
|
return [
|
||||||
"file_id": fileId,
|
"file_id": result.fileId,
|
||||||
"preview_file_id": fileId
|
"preview_file_id": result.thumbnailId ?? result.fileId
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user