feat: uploadmore
This commit is contained in:
parent
9a432f65ac
commit
6229ddcd6c
@ -334,9 +334,19 @@ public class ImageUploadService {
|
|||||||
// MARK: - Supporting Types
|
// MARK: - Supporting Types
|
||||||
|
|
||||||
/// 媒体类型
|
/// 媒体类型
|
||||||
public enum MediaType {
|
public enum MediaType: Equatable {
|
||||||
case image(UIImage)
|
case image(UIImage)
|
||||||
case video(URL, UIImage?)
|
case video(URL, UIImage?)
|
||||||
|
|
||||||
|
// 确保 id 计算属性存在
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case .image(let uiImage):
|
||||||
|
return "image_\(uiImage.hashValue)"
|
||||||
|
case .video(let url, _):
|
||||||
|
return "video_\(url.absoluteString.hashValue)"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 媒体上传结果
|
/// 媒体上传结果
|
||||||
|
|||||||
@ -46,7 +46,27 @@ public class MediaUploadManager: ObservableObject {
|
|||||||
|
|
||||||
/// 添加上传媒体
|
/// 添加上传媒体
|
||||||
public func addMedia(_ media: [MediaType]) {
|
public func addMedia(_ media: [MediaType]) {
|
||||||
selectedMedia.append(contentsOf: media)
|
print("Adding \(media.count) media items")
|
||||||
|
let newMedia = media.filter { newItem in
|
||||||
|
!selectedMedia.contains { $0.id == newItem.id }
|
||||||
|
}
|
||||||
|
print("After filtering duplicates: \(newMedia.count) new items")
|
||||||
|
|
||||||
|
let isFirstMedia = selectedMedia.isEmpty
|
||||||
|
selectedMedia.append(contentsOf: newMedia)
|
||||||
|
|
||||||
|
for item in newMedia {
|
||||||
|
uploadStatus[item.id] = .pending
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是第一次添加媒体,发送通知
|
||||||
|
if isFirstMedia, let firstMedia = newMedia.first {
|
||||||
|
NotificationCenter.default.post(
|
||||||
|
name: .didAddFirstMedia,
|
||||||
|
object: nil,
|
||||||
|
userInfo: ["media": firstMedia]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 移除指定索引的媒体
|
/// 移除指定索引的媒体
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
extension Notification.Name {
|
||||||
|
static let didAddFirstMedia = Notification.Name("didAddFirstMedia")
|
||||||
|
}
|
||||||
/// 主上传视图
|
/// 主上传视图
|
||||||
/// 提供媒体选择、预览和上传功能
|
/// 提供媒体选择、预览和上传功能
|
||||||
@MainActor
|
@MainActor
|
||||||
@ -30,8 +33,7 @@ struct MediaUploadView: View {
|
|||||||
MainUploadArea(
|
MainUploadArea(
|
||||||
uploadManager: uploadManager,
|
uploadManager: uploadManager,
|
||||||
showMediaPicker: $showMediaPicker,
|
showMediaPicker: $showMediaPicker,
|
||||||
selectedMedia: $selectedMedia,
|
selectedMedia: $selectedMedia
|
||||||
selectedIndices: $selectedIndices
|
|
||||||
)
|
)
|
||||||
.padding()
|
.padding()
|
||||||
.id("mainUploadArea\(uploadManager.selectedMedia.count)")
|
.id("mainUploadArea\(uploadManager.selectedMedia.count)")
|
||||||
@ -262,12 +264,10 @@ struct MainUploadArea: View {
|
|||||||
@Binding var showMediaPicker: Bool
|
@Binding var showMediaPicker: Bool
|
||||||
/// 当前选中的媒体
|
/// 当前选中的媒体
|
||||||
@Binding var selectedMedia: MediaType?
|
@Binding var selectedMedia: MediaType?
|
||||||
/// 当前选中的媒体索引
|
|
||||||
@Binding var selectedIndices: Set<Int>
|
|
||||||
|
|
||||||
// MARK: - 视图主体
|
// MARK: - 视图主体
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
// 标题
|
// 标题
|
||||||
Text("Click to upload 20 images and 5 videos to generate your next blind box.")
|
Text("Click to upload 20 images and 5 videos to generate your next blind box.")
|
||||||
@ -276,16 +276,43 @@ struct MainUploadArea: View {
|
|||||||
.foregroundColor(.black)
|
.foregroundColor(.black)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
// 主显示区域
|
||||||
// 上传提示视图
|
if let mediaToDisplay = selectedMedia ?? uploadManager.selectedMedia.first {
|
||||||
UploadPromptView(showMediaPicker: $showMediaPicker)
|
MediaPreview(media: mediaToDisplay, uploadManager: uploadManager)
|
||||||
|
.id(mediaToDisplay.id)
|
||||||
|
.frame(width: 225, height: 225)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 16)
|
||||||
|
.stroke(Color.themePrimary, lineWidth: 5) // 使用主题色添加2点宽的实线边框
|
||||||
|
)
|
||||||
|
.cornerRadius(16)
|
||||||
|
.shadow(radius: 4)
|
||||||
|
.padding(.horizontal)
|
||||||
|
.transition(.opacity)
|
||||||
|
} else {
|
||||||
|
UploadPromptView(showMediaPicker: $showMediaPicker)
|
||||||
|
}
|
||||||
// 媒体预览区域
|
// 媒体预览区域
|
||||||
mediaPreviewSection
|
mediaPreviewSection
|
||||||
}
|
}
|
||||||
|
.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)
|
.background(Color.white)
|
||||||
.cornerRadius(16)
|
.cornerRadius(16)
|
||||||
.shadow(radius: 2)
|
.shadow(radius: 2)
|
||||||
|
.animation(.default, value: selectedMedia?.id) // 当 selectedMedia 的 id 变化时添加动画
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 子视图
|
// MARK: - 子视图
|
||||||
@ -371,20 +398,31 @@ struct MainUploadArea: View {
|
|||||||
.padding([.top, .leading], 0),
|
.padding([.top, .leading], 0),
|
||||||
alignment: .topLeading
|
alignment: .topLeading
|
||||||
)
|
)
|
||||||
|
// 在 mediaItemView 函数中,更新 onTapGesture:
|
||||||
|
// .onTapGesture {
|
||||||
|
// print("Tapped media at index: \(index)") // 添加日志
|
||||||
|
// withAnimation {
|
||||||
|
// selectedIndices = [index]
|
||||||
|
// selectedMedia = media
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// .contentShape(Rectangle()) // 确保整个区域都可点击
|
||||||
|
// 在 mediaItemView 中,更新 onTapGesture:
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
// 更新选中的媒体
|
print("点击了媒体项,索引: \(index)")
|
||||||
selectedIndices = [index]
|
withAnimation {
|
||||||
selectedMedia = media
|
selectedMedia = media // 直接更新选中的媒体
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.contentShape(Rectangle()) // 确保整个区域都可点击
|
||||||
}
|
}
|
||||||
|
|
||||||
// 右上角关闭按钮
|
// 右上角关闭按钮
|
||||||
Button(action: {
|
Button(action: {
|
||||||
// 移除选中的媒体项
|
// 移除选中的媒体项
|
||||||
uploadManager.selectedMedia.remove(at: index)
|
uploadManager.selectedMedia.remove(at: index)
|
||||||
// 更新选中状态
|
// 重置选中的媒体
|
||||||
if selectedIndices.contains(index) {
|
if selectedMedia == media {
|
||||||
selectedIndices = []
|
|
||||||
selectedMedia = nil
|
selectedMedia = nil
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
@ -594,7 +632,7 @@ struct MediaPreview: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
// 显示图片或错误状态
|
// 1. 显示图片或视频缩略图
|
||||||
if let image = image {
|
if let image = image {
|
||||||
loadedImageView(image)
|
loadedImageView(image)
|
||||||
|
|
||||||
@ -602,16 +640,30 @@ struct MediaPreview: View {
|
|||||||
if case .video = media {
|
if case .video = media {
|
||||||
playButton
|
playButton
|
||||||
}
|
}
|
||||||
} else if case .failure(let error) = loadState {
|
|
||||||
// 加载失败状态
|
// 上传进度条(仅当正在上传且进度小于100%时显示)
|
||||||
errorView(error: error)
|
if isUploading && uploadProgress < 1.0 {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
ProgressView(value: uploadProgress, total: 1.0)
|
||||||
|
.progressViewStyle(LinearProgressViewStyle(tint: .white))
|
||||||
|
.frame(height: 2)
|
||||||
|
.padding(.horizontal, 4)
|
||||||
|
.padding(.bottom, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 初始加载时显示占位图
|
// 2. 加载中的占位图
|
||||||
placeholderView
|
placeholderView
|
||||||
.onAppear {
|
.onAppear {
|
||||||
loadImage()
|
loadImage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. 显示错误状态(如果有)
|
||||||
|
if case .failure(let error) = loadState, image == nil {
|
||||||
|
errorView(error: error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.aspectRatio(1, contentMode: .fill)
|
.aspectRatio(1, contentMode: .fill)
|
||||||
.clipped()
|
.clipped()
|
||||||
@ -622,18 +674,12 @@ struct MediaPreview: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 子视图
|
// 加载中的占位图
|
||||||
|
|
||||||
/// 加载中的占位图
|
|
||||||
private var placeholderView: some View {
|
private var placeholderView: some View {
|
||||||
Color.gray.opacity(0.1)
|
Color.gray.opacity(0.1)
|
||||||
.overlay(
|
|
||||||
ProgressView()
|
|
||||||
.progressViewStyle(CircularProgressViewStyle())
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 加载完成的图片视图
|
// 加载完成的图片视图
|
||||||
private func loadedImageView(_ image: UIImage) -> some View {
|
private func loadedImageView(_ image: UIImage) -> some View {
|
||||||
Image(uiImage: image)
|
Image(uiImage: image)
|
||||||
.resizable()
|
.resizable()
|
||||||
@ -736,15 +782,15 @@ struct MediaPreview: View {
|
|||||||
/// 扩展 MediaType 以支持 Identifiable 协议
|
/// 扩展 MediaType 以支持 Identifiable 协议
|
||||||
extension MediaType: Identifiable {
|
extension MediaType: Identifiable {
|
||||||
/// 唯一标识符
|
/// 唯一标识符
|
||||||
public var id: String {
|
public var id: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .image(let uiImage):
|
case .image(let uiImage):
|
||||||
return "image_\(uiImage.hashValue)"
|
return "image_\(uiImage.hashValue)"
|
||||||
case .video(let url, _):
|
case .video(let url, _):
|
||||||
return "video_\(url.absoluteString)"
|
return "video_\(url.absoluteString.hashValue)"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - 预览
|
// MARK: - 预览
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user