import SwiftUI import AVKit // MARK: - API Response Models struct MaterialResponse: Decodable { let code: Int let data: MaterialData struct MaterialData: Decodable { let items: [MemoryItem] let hasMore: Bool enum CodingKeys: String, CodingKey { case items case hasMore = "has_more" } } } struct MemoryItem: Identifiable, Decodable { let id: String let name: String? let description: String? let fileInfo: FileInfo let previewFileInfo: FileInfo var title: String { name ?? "Untitled" } var subtitle: String { description ?? "" } var mediaType: MemoryMediaType { let url = fileInfo.url.lowercased() if url.hasSuffix(".mp4") || url.hasSuffix(".mov") { return .video(url: fileInfo.url, previewUrl: previewFileInfo.url) } else { return .image(fileInfo.url) } } var aspectRatio: CGFloat { 1.0 } enum CodingKeys: String, CodingKey { case id, name, description case fileInfo = "file_info" case previewFileInfo = "preview_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(url: String, previewUrl: String) } struct MemoriesView: View { @Environment(\.dismiss) private var dismiss @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 { VStack(spacing: 0) { // 顶部导航栏 HStack { Button(action: { // 返回上一级 self.dismiss() }) { Image(systemName: "chevron.left") .foregroundColor(.themeTextMessageMain) .font(.system(size: 20)) } Spacer() Text("My Memories") .foregroundColor(.themeTextMessageMain) .font(Typography.font(for: .body, family: .quicksandBold)) Spacer() } .padding() .background(Color.themeTextWhiteSecondary) // 内容区域 ZStack { Color.themeTextWhiteSecondary.ignoresSafeArea() 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) } } } } } } .navigationBarBackButtonHidden(true) .onAppear { fetchMemories() } } private func fetchMemories() { isLoading = true errorMessage = nil let parameters: [String: Any] = ["page": 0] NetworkService.shared.get(path: "/material/list", parameters: parameters) { [self] (result: Result) in DispatchQueue.main.async { [self] in self.isLoading = false switch result { case .success(let response): print("✅ Successfully fetched \(response.data.items) memory items") response.data.items.forEach { item in print("📝 Item ID: \(item.id), Title: \(item.name ?? "Untitled"), URL: \(item)") } 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: 12) { 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 url, let previewUrl): // Use preview image for video if let previewUrl = URL(string: previewUrl) { AsyncImage(url: previewUrl) { phase in if let image = phase.image { image .resizable() .aspectRatio(contentMode: .fill) } else if phase.error != nil { Color.gray.opacity(0.3) } else { ProgressView() } } } else { Color.gray.opacity(0.3) } } } .frame(width: (UIScreen.main.bounds.width / 2) - 24, height: (UIScreen.main.bounds.width / 2 - 24) * (1/memory.aspectRatio)) .clipped() .cornerRadius(12) // Show play button for videos if case .video = memory.mediaType { Image(systemName: "play.circle.fill") .font(.system(size: 40)) .foregroundColor(.white.opacity(0.9)) .shadow(radius: 3) } } // Title and Subtitle VStack(alignment: .leading, spacing: 8) { Text(memory.title) .font(Typography.font(for: .body, family: .quicksandBold)) .foregroundColor(.themeTextMessageMain) .lineLimit(1) Text(memory.subtitle) .font(.system(size: 14)) .foregroundColor(.themeTextMessageMain) } .padding(.horizontal, 2) .padding(.bottom, 8) } } } // Helper extension to pause video private extension AVPlayer { func pause() { self.pause() } } #Preview { MemoriesView() }