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