feat: uploadmore
This commit is contained in:
parent
607405ba58
commit
f232c70ef1
@ -1,4 +1,5 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
extension Notification.Name {
|
extension Notification.Name {
|
||||||
static let didAddFirstMedia = Notification.Name("didAddFirstMedia")
|
static let didAddFirstMedia = Notification.Name("didAddFirstMedia")
|
||||||
@ -28,14 +29,14 @@ struct MediaUploadView: View {
|
|||||||
|
|
||||||
// 上传提示信息
|
// 上传提示信息
|
||||||
uploadHintView
|
uploadHintView
|
||||||
|
Spacer()
|
||||||
|
.frame(height: uploadManager.selectedMedia.isEmpty ? 80 : 40)
|
||||||
// 主上传区域
|
// 主上传区域
|
||||||
MainUploadArea(
|
MainUploadArea(
|
||||||
uploadManager: uploadManager,
|
uploadManager: uploadManager,
|
||||||
showMediaPicker: $showMediaPicker,
|
showMediaPicker: $showMediaPicker,
|
||||||
selectedMedia: $selectedMedia
|
selectedMedia: $selectedMedia
|
||||||
)
|
)
|
||||||
.padding()
|
|
||||||
.id("mainUploadArea\(uploadManager.selectedMedia.count)")
|
.id("mainUploadArea\(uploadManager.selectedMedia.count)")
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@ -92,7 +93,7 @@ struct MediaUploadView: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.black)
|
.foregroundColor(.black)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(12)
|
.padding(3)
|
||||||
.background(
|
.background(
|
||||||
LinearGradient(
|
LinearGradient(
|
||||||
gradient: Gradient(colors: [
|
gradient: Gradient(colors: [
|
||||||
@ -148,7 +149,7 @@ struct MediaUploadView: View {
|
|||||||
|
|
||||||
// 如果没有选中的媒体,则选中第一个新增的
|
// 如果没有选中的媒体,则选中第一个新增的
|
||||||
if selectedIndices.isEmpty {
|
if selectedIndices.isEmpty {
|
||||||
selectedIndices = [0] // 选择第一个新增的项
|
selectedIndices = [0] // 选择第一个新增项的索引
|
||||||
selectedMedia = newItems.first
|
selectedMedia = newItems.first
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,7 +271,9 @@ struct MainUploadArea: View {
|
|||||||
// MARK: - 视图主体
|
// MARK: - 视图主体
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 16) {
|
VStack() {
|
||||||
|
Spacer()
|
||||||
|
.frame(height: 30)
|
||||||
// 标题
|
// 标题
|
||||||
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.")
|
||||||
.font(Typography.font(for: .title2, family: .quicksandBold))
|
.font(Typography.font(for: .title2, family: .quicksandBold))
|
||||||
@ -278,6 +281,8 @@ struct MainUploadArea: View {
|
|||||||
.foregroundColor(.black)
|
.foregroundColor(.black)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
Spacer()
|
||||||
|
.frame(height: 50)
|
||||||
// 主显示区域
|
// 主显示区域
|
||||||
if let mediaToDisplay = selectedMedia ?? uploadManager.selectedMedia.first {
|
if let mediaToDisplay = selectedMedia ?? uploadManager.selectedMedia.first {
|
||||||
MediaPreview(media: mediaToDisplay, uploadManager: uploadManager)
|
MediaPreview(media: mediaToDisplay, uploadManager: uploadManager)
|
||||||
@ -285,7 +290,7 @@ struct MainUploadArea: View {
|
|||||||
.frame(width: 225, height: 225)
|
.frame(width: 225, height: 225)
|
||||||
.overlay(
|
.overlay(
|
||||||
RoundedRectangle(cornerRadius: 16)
|
RoundedRectangle(cornerRadius: 16)
|
||||||
.stroke(Color.themePrimary, lineWidth: 5) // 使用主题色添加2点宽的实线边框
|
.stroke(Color.themePrimary, lineWidth: 5)
|
||||||
)
|
)
|
||||||
.cornerRadius(16)
|
.cornerRadius(16)
|
||||||
.shadow(radius: 4)
|
.shadow(radius: 4)
|
||||||
@ -296,6 +301,8 @@ struct MainUploadArea: View {
|
|||||||
}
|
}
|
||||||
// 媒体预览区域
|
// 媒体预览区域
|
||||||
mediaPreviewSection
|
mediaPreviewSection
|
||||||
|
Spacer()
|
||||||
|
.frame(height: 10)
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
print("MainUploadArea appeared")
|
print("MainUploadArea appeared")
|
||||||
@ -312,9 +319,8 @@ struct MainUploadArea: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(Color.white)
|
.background(Color.white)
|
||||||
.cornerRadius(16)
|
.cornerRadius(18)
|
||||||
.shadow(radius: 2)
|
.animation(.default, value: selectedMedia?.id)
|
||||||
.animation(.default, value: selectedMedia?.id) // 当 selectedMedia 的 id 变化时添加动画
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 子视图
|
// MARK: - 子视图
|
||||||
@ -323,7 +329,7 @@ struct MainUploadArea: View {
|
|||||||
private var mediaPreviewSection: some View {
|
private var mediaPreviewSection: some View {
|
||||||
Group {
|
Group {
|
||||||
if !uploadManager.selectedMedia.isEmpty {
|
if !uploadManager.selectedMedia.isEmpty {
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 4) {
|
||||||
// 横向滚动的缩略图列表
|
// 横向滚动的缩略图列表
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
LazyHStack(spacing: 10) {
|
LazyHStack(spacing: 10) {
|
||||||
@ -337,9 +343,9 @@ struct MainUploadArea: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
.frame(height: 140)
|
.frame(height: 70)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 8)
|
.padding(.top, 10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -354,16 +360,18 @@ struct MainUploadArea: View {
|
|||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
// 媒体预览
|
// 媒体预览
|
||||||
MediaPreview(media: media, uploadManager: uploadManager)
|
MediaPreview(media: media, uploadManager: uploadManager)
|
||||||
.frame(width: 80, height: 80)
|
.frame(width: 58, height: 58)
|
||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
.shadow(radius: 1)
|
.shadow(radius: 1)
|
||||||
.overlay(
|
.overlay(
|
||||||
// 左上角序号
|
// 左上角序号
|
||||||
ZStack {
|
ZStack(alignment: .topLeading) {
|
||||||
|
// 左上角序号
|
||||||
|
ZStack(alignment: .topLeading) {
|
||||||
Path { path in
|
Path { path in
|
||||||
let radius: CGFloat = 4
|
let radius: CGFloat = 4
|
||||||
let width: CGFloat = 28
|
let width: CGFloat = 14
|
||||||
let height: CGFloat = 18
|
let height: CGFloat = 10
|
||||||
|
|
||||||
// 从左上角开始(带圆角)
|
// 从左上角开始(带圆角)
|
||||||
path.move(to: CGPoint(x: 0, y: radius))
|
path.move(to: CGPoint(x: 0, y: radius))
|
||||||
@ -386,19 +394,51 @@ struct MainUploadArea: View {
|
|||||||
// 闭合路径
|
// 闭合路径
|
||||||
path.closeSubpath()
|
path.closeSubpath()
|
||||||
}
|
}
|
||||||
.fill(Color.white.opacity(0.5))
|
.fill(Color(hex: "BEBEBE").opacity(0.6))
|
||||||
|
|
||||||
// Text
|
|
||||||
Text("\(index + 1)")
|
Text("\(index + 1)")
|
||||||
.font(.system(size: 12, weight: .bold))
|
.font(.system(size: 8, weight: .bold))
|
||||||
.foregroundColor(.black)
|
.foregroundColor(.black)
|
||||||
.frame(width: 28, height: 18)
|
.frame(width: 14, height: 10)
|
||||||
.offset(x: 0, y: -1)
|
.offset(y: -1)
|
||||||
|
}
|
||||||
|
.frame(width: 14, height: 10, 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.frame(width: 28, height: 18)
|
|
||||||
.offset(x: 0, y: 0)
|
|
||||||
.padding([.top, .leading], 0),
|
|
||||||
alignment: .topLeading
|
|
||||||
)
|
)
|
||||||
// 在 mediaItemView 函数中,更新 onTapGesture:
|
// 在 mediaItemView 函数中,更新 onTapGesture:
|
||||||
// .onTapGesture {
|
// .onTapGesture {
|
||||||
@ -429,18 +469,19 @@ struct MainUploadArea: View {
|
|||||||
selectedMedia = nil
|
selectedMedia = nil
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Image(systemName: "xmark.circle.fill")
|
Image(systemName: "xmark")
|
||||||
.font(.system(size: 20))
|
.font(.system(size: 10, weight: .bold))
|
||||||
.foregroundColor(.white.opacity(0.5))
|
.foregroundColor(.black)
|
||||||
|
.frame(width: 12, height: 12)
|
||||||
.background(
|
.background(
|
||||||
Circle()
|
Circle()
|
||||||
.fill(Color.black.opacity(0.5))
|
.fill(Color(hex: "BEBEBE").opacity(0.6))
|
||||||
.frame(width: 20, height: 20)
|
.frame(width: 12, height: 12)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.offset(x: 6, y: -6) // 调整位置,确保完全可见
|
.offset(x: 6, y: -6) // 调整位置,确保完全可见
|
||||||
}
|
}
|
||||||
.padding(4)
|
.padding(.horizontal,4)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -460,7 +501,8 @@ struct MainUploadArea: View {
|
|||||||
ProgressView(value: progress, total: 1.0)
|
ProgressView(value: progress, total: 1.0)
|
||||||
.progressViewStyle(LinearProgressViewStyle())
|
.progressViewStyle(LinearProgressViewStyle())
|
||||||
.frame(height: 3)
|
.frame(height: 3)
|
||||||
.tint(Color.themePrimary)
|
.padding(.horizontal, 4)
|
||||||
|
.padding(.bottom, 2)
|
||||||
}
|
}
|
||||||
.frame(width: 60)
|
.frame(width: 60)
|
||||||
|
|
||||||
@ -486,15 +528,15 @@ struct MainUploadArea: View {
|
|||||||
private var addMoreButton: some View {
|
private var addMoreButton: some View {
|
||||||
Button(action: { showMediaPicker = true }) {
|
Button(action: { showMediaPicker = true }) {
|
||||||
Image(systemName: "plus")
|
Image(systemName: "plus")
|
||||||
.font(.system(size: 18))
|
.font(.system(size: 8, weight: .bold))
|
||||||
.foregroundColor(.black)
|
.foregroundColor(.black)
|
||||||
.frame(width: 80, height: 80)
|
.frame(width: 58, height: 58)
|
||||||
.background(Color.white)
|
.background(Color.white)
|
||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
.overlay(
|
.overlay(
|
||||||
RoundedRectangle(cornerRadius: 8)
|
RoundedRectangle(cornerRadius: 8)
|
||||||
.strokeBorder(style: StrokeStyle(
|
.strokeBorder(style: StrokeStyle(
|
||||||
lineWidth: 3,
|
lineWidth: 2,
|
||||||
dash: [4, 4]
|
dash: [4, 4]
|
||||||
))
|
))
|
||||||
.foregroundColor(Color.themePrimary)
|
.foregroundColor(Color.themePrimary)
|
||||||
@ -518,6 +560,7 @@ struct UploadPromptView: View {
|
|||||||
.frame(width: 225, height: 225)
|
.frame(width: 225, height: 225)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.overlay(
|
.overlay(
|
||||||
|
ZStack {
|
||||||
RoundedRectangle(cornerRadius: 20)
|
RoundedRectangle(cornerRadius: 20)
|
||||||
.stroke(style: StrokeStyle(
|
.stroke(style: StrokeStyle(
|
||||||
lineWidth: 5,
|
lineWidth: 5,
|
||||||
@ -525,6 +568,12 @@ struct UploadPromptView: View {
|
|||||||
dash: [12, 8]
|
dash: [12, 8]
|
||||||
))
|
))
|
||||||
.foregroundColor(Color.themePrimary)
|
.foregroundColor(Color.themePrimary)
|
||||||
|
|
||||||
|
// Add plus icon in the center
|
||||||
|
Image(systemName: "plus")
|
||||||
|
.font(.system(size: 32, weight: .bold))
|
||||||
|
.foregroundColor(.black)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -637,11 +686,8 @@ struct MediaPreview: View {
|
|||||||
ZStack {
|
ZStack {
|
||||||
// 1. 显示图片或视频缩略图
|
// 1. 显示图片或视频缩略图
|
||||||
if let image = image {
|
if let image = image {
|
||||||
|
ZStack(alignment: .bottomTrailing) {
|
||||||
loadedImageView(image)
|
loadedImageView(image)
|
||||||
|
|
||||||
// 视频播放按钮
|
|
||||||
if case .video = media {
|
|
||||||
playButton
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上传进度条(仅当正在上传且进度小于100%时显示)
|
// 上传进度条(仅当正在上传且进度小于100%时显示)
|
||||||
@ -690,14 +736,6 @@ struct MediaPreview: View {
|
|||||||
.transition(.opacity.animation(.easeInOut(duration: 0.2)))
|
.transition(.opacity.animation(.easeInOut(duration: 0.2)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 播放按钮
|
|
||||||
private var playButton: some View {
|
|
||||||
Image(systemName: "play.circle.fill")
|
|
||||||
.font(.system(size: 24))
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.shadow(radius: 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 错误视图
|
/// 错误视图
|
||||||
private func errorView(error: Error) -> some View {
|
private func errorView(error: Error) -> some View {
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
@ -780,6 +818,15 @@ struct MediaPreview: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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: - 扩展
|
// MARK: - 扩展
|
||||||
|
|
||||||
/// 扩展 MediaType 以支持 Identifiable 协议
|
/// 扩展 MediaType 以支持 Identifiable 协议
|
||||||
@ -795,6 +842,14 @@ extension MediaType: Identifiable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TimeInterval {
|
||||||
|
var formattedDuration: String {
|
||||||
|
let minutes = Int(self) / 60
|
||||||
|
let seconds = Int(self) % 60
|
||||||
|
return String(format: "%d:%02d", minutes, seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - 预览
|
// MARK: - 预览
|
||||||
|
|
||||||
struct MediaUploadView_Previews: PreviewProvider {
|
struct MediaUploadView_Previews: PreviewProvider {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user