From d90ed0a1714c2e005f82d0c22ed622b7c7c14758 Mon Sep 17 00:00:00 2001 From: jinyaqiu Date: Thu, 28 Aug 2025 19:34:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=B4=A0=E6=9D=90=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/Utils/Router.swift | 3 + wake/View/Blind/BlindBox.swift | 13 +- wake/View/Components/UserProfileModal.swift | 2 +- wake/View/Memories/MemoriesView.swift | 190 ++++++++++++++++++++ 4 files changed, 200 insertions(+), 8 deletions(-) create mode 100644 wake/View/Memories/MemoriesView.swift diff --git a/wake/Utils/Router.swift b/wake/Utils/Router.swift index 8e3300a..528df05 100644 --- a/wake/Utils/Router.swift +++ b/wake/Utils/Router.swift @@ -9,6 +9,7 @@ enum AppRoute: Hashable { case mediaUpload case blindBox(mediaType: BlindBoxView.BlindBoxMediaType) case blindOutcome(media: MediaType) + case memories @ViewBuilder var view: some View { @@ -27,6 +28,8 @@ enum AppRoute: Hashable { BlindBoxView(mediaType: mediaType) case .blindOutcome(let media): BlindOutcomeView(media: media) + case .memories: + MemoriesView() } } } diff --git a/wake/View/Blind/BlindBox.swift b/wake/View/Blind/BlindBox.swift index 0c3e399..f3c4ab7 100644 --- a/wake/View/Blind/BlindBox.swift +++ b/wake/View/Blind/BlindBox.swift @@ -51,9 +51,9 @@ struct AVPlayerController: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> AVPlayerViewController { let controller = AVPlayerViewController() controller.player = player - controller.showsPlaybackControls = true - controller.entersFullScreenWhenPlaybackBegins = true - controller.exitsFullScreenWhenPlaybackEnds = true + controller.showsPlaybackControls = false + controller.videoGravity = .resizeAspect + controller.view.backgroundColor = .clear return controller } @@ -198,9 +198,8 @@ struct BlindBoxView: View { }) { Image(systemName: "chevron.left.circle.fill") .font(.system(size: 36)) - .foregroundColor(.white) + .foregroundColor(.black) .padding(12) - .background(Color.black.opacity(0.5)) .clipShape(Circle()) } Spacer() @@ -213,8 +212,8 @@ struct BlindBoxView: View { .zIndex(1000) .transition(.opacity) .onAppear { - // 1秒后显示按钮 - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + // 2秒后显示按钮 + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { withAnimation(.easeInOut(duration: 0.3)) { showControls = true } diff --git a/wake/View/Components/UserProfileModal.swift b/wake/View/Components/UserProfileModal.swift index 60b8610..289640d 100644 --- a/wake/View/Components/UserProfileModal.swift +++ b/wake/View/Components/UserProfileModal.swift @@ -185,7 +185,7 @@ struct UserProfileModal: View { // memories Button(action: { - Router.shared.navigate(to: .mediaUpload) + Router.shared.navigate(to: .memories) }) { HStack(spacing: 16) { SVGImage(svgName: "Memory") diff --git a/wake/View/Memories/MemoriesView.swift b/wake/View/Memories/MemoriesView.swift new file mode 100644 index 0000000..66c1378 --- /dev/null +++ b/wake/View/Memories/MemoriesView.swift @@ -0,0 +1,190 @@ +import SwiftUI +import AVKit + +// MARK: - API Response Models +struct MaterialResponse: Decodable { + let code: Int + let data: MaterialData + + struct MaterialData: Decodable { + let items: [MemoryItem] + } +} + +struct MemoryItem: Identifiable, Decodable { + let id: String + let name: String + let description: String + let fileInfo: FileInfo + + var title: String { name } + var subtitle: String { description } + var mediaType: MemoryMediaType { .image(fileInfo.url) } + var aspectRatio: CGFloat { 1.0 } // Default to square, adjust based on actual image dimensions if needed + + enum CodingKeys: String, CodingKey { + case id, name, description + case fileInfo = "file_info" + } +} + +struct FileInfo: Decodable { + let id: String + let fileName: String + let url: String + + enum CodingKeys: String, CodingKey { + case id + case fileName = "file_name" + case url + } +} + +enum MemoryMediaType: Equatable { + case image(String) + case video(String) +} + +struct MemoriesView: View { + @State private var memories: [MemoryItem] = [] + @State private var isLoading = false + @State private var errorMessage: String? + + let columns = [ + GridItem(.flexible(), spacing: 1), + GridItem(.flexible(), spacing: 1) + ] + + var body: some View { + NavigationView { + Group { + if isLoading { + ProgressView() + .scaleEffect(1.5) + } else if let error = errorMessage { + Text("Error: \(error)") + .foregroundColor(.red) + } else { + ScrollView { + LazyVGrid(columns: columns, spacing: 4) { + ForEach(memories) { memory in + MemoryCard(memory: memory) + .padding(.horizontal, 2) + } + } + .padding(.top, 4) + .padding(.horizontal, 4) + } + } + } + .navigationTitle("My Memories") + .navigationBarTitleDisplayMode(.inline) + .onAppear { + fetchMemories() + } + } + } + + private func fetchMemories() { + isLoading = true + errorMessage = nil + + NetworkService.shared.get(path: "/material/list") { [self] (result: Result) in + DispatchQueue.main.async { [self] in + self.isLoading = false + + switch result { + case .success(let response): + print("✅ Successfully fetched \(response.data.items.count) memory items") + response.data.items.forEach { item in + print("📝 Item ID: \(item.id), Title: \(item.name), URL: \(item.fileInfo.url)") + } + self.memories = response.data.items + case .failure(let error): + self.errorMessage = error.localizedDescription + print("❌ Failed to fetch memories: \(error.localizedDescription)") + } + } + } + } +} + +struct MemoryCard: View { + let memory: MemoryItem + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + ZStack { + // Media content + Group { + switch memory.mediaType { + case .image(let urlString): + if let url = URL(string: urlString) { + AsyncImage(url: url) { phase in + if let image = phase.image { + image + .resizable() + .aspectRatio(contentMode: .fill) + } else if phase.error != nil { + Color.gray.opacity(0.3) + } else { + ProgressView() + } + } + } + + case .video(let urlString): + if let url = URL(string: urlString) { + VideoPlayer(player: AVPlayer(url: url)) + .aspectRatio(memory.aspectRatio, contentMode: .fill) + .onAppear { + // The video will be shown with a play button overlay + // and will only play when tapped + } + } else { + Color.gray.opacity(0.3) + .aspectRatio(memory.aspectRatio, contentMode: .fill) + } + } + } + .frame(width: (UIScreen.main.bounds.width / 2) - 24, height: (UIScreen.main.bounds.width / 2 - 24) * (1/memory.aspectRatio)) + .clipped() + .cornerRadius(12) + .overlay( + Group { + if case .video = memory.mediaType { + Image(systemName: "play.circle.fill") + .font(.system(size: 40)) + .foregroundColor(.white.opacity(0.9)) + } + } + ) + } + + // Title and Subtitle + VStack(alignment: .leading, spacing: 1) { + Text(memory.title) + .font(.subheadline) + .lineLimit(1) + + Text(memory.subtitle) + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(1) + } + .padding(.horizontal, 2) + .padding(.bottom, 4) + } + } +} + +// Helper extension to pause video +private extension AVPlayer { + func pause() { + self.pause() + } +} + +#Preview { + MemoriesView() +}