diff --git a/wake/View/Upload/MediaUploadView.swift b/wake/View/Upload/MediaUploadView.swift index 70e5836..7d29ee3 100644 --- a/wake/View/Upload/MediaUploadView.swift +++ b/wake/View/Upload/MediaUploadView.swift @@ -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 {