feat: uploadmore

This commit is contained in:
jinyaqiu 2025-08-26 14:54:09 +08:00
parent 607405ba58
commit f232c70ef1

View File

@ -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,51 +360,85 @@ 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) {
Path { path in //
let radius: CGFloat = 4 ZStack(alignment: .topLeading) {
let width: CGFloat = 28 Path { path in
let height: CGFloat = 18 let radius: CGFloat = 4
let width: CGFloat = 14
let height: CGFloat = 10
// //
path.move(to: CGPoint(x: 0, y: radius)) path.move(to: CGPoint(x: 0, y: radius))
path.addQuadCurve(to: CGPoint(x: radius, y: 0), path.addQuadCurve(to: CGPoint(x: radius, y: 0),
control: CGPoint(x: 0, y: 0)) control: CGPoint(x: 0, y: 0))
// //
path.addLine(to: CGPoint(x: width, y: 0)) path.addLine(to: CGPoint(x: width, y: 0))
// //
path.addLine(to: CGPoint(x: width, y: height - radius)) path.addLine(to: CGPoint(x: width, y: height - radius))
// //
path.addQuadCurve(to: CGPoint(x: width - radius, y: height), path.addQuadCurve(to: CGPoint(x: width - radius, y: height),
control: CGPoint(x: width, y: height)) control: CGPoint(x: width, y: height))
// //
path.addLine(to: CGPoint(x: 0, y: height)) path.addLine(to: CGPoint(x: 0, y: height))
// //
path.closeSubpath() path.closeSubpath()
}
.fill(Color(hex: "BEBEBE").opacity(0.6))
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)") if case .video(let url, _) = media, let videoURL = url as? URL {
.font(.system(size: 12, weight: .bold)) VStack {
.foregroundColor(.black) Spacer()
.frame(width: 28, height: 18) HStack {
.offset(x: 0, y: -1) 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,14 +560,21 @@ struct UploadPromptView: View {
.frame(width: 225, height: 225) .frame(width: 225, height: 225)
.contentShape(Rectangle()) .contentShape(Rectangle())
.overlay( .overlay(
RoundedRectangle(cornerRadius: 20) ZStack {
.stroke(style: StrokeStyle( RoundedRectangle(cornerRadius: 20)
lineWidth: 5, .stroke(style: StrokeStyle(
lineCap: .round, lineWidth: 5,
dash: [12, 8] lineCap: .round,
)) 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 {
loadedImageView(image) ZStack(alignment: .bottomTrailing) {
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 {