feat: 素材列表
This commit is contained in:
parent
b6178896ec
commit
d90ed0a171
@ -9,6 +9,7 @@ enum AppRoute: Hashable {
|
|||||||
case mediaUpload
|
case mediaUpload
|
||||||
case blindBox(mediaType: BlindBoxView.BlindBoxMediaType)
|
case blindBox(mediaType: BlindBoxView.BlindBoxMediaType)
|
||||||
case blindOutcome(media: MediaType)
|
case blindOutcome(media: MediaType)
|
||||||
|
case memories
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var view: some View {
|
var view: some View {
|
||||||
@ -27,6 +28,8 @@ enum AppRoute: Hashable {
|
|||||||
BlindBoxView(mediaType: mediaType)
|
BlindBoxView(mediaType: mediaType)
|
||||||
case .blindOutcome(let media):
|
case .blindOutcome(let media):
|
||||||
BlindOutcomeView(media: media)
|
BlindOutcomeView(media: media)
|
||||||
|
case .memories:
|
||||||
|
MemoriesView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,9 +51,9 @@ struct AVPlayerController: UIViewControllerRepresentable {
|
|||||||
func makeUIViewController(context: Context) -> AVPlayerViewController {
|
func makeUIViewController(context: Context) -> AVPlayerViewController {
|
||||||
let controller = AVPlayerViewController()
|
let controller = AVPlayerViewController()
|
||||||
controller.player = player
|
controller.player = player
|
||||||
controller.showsPlaybackControls = true
|
controller.showsPlaybackControls = false
|
||||||
controller.entersFullScreenWhenPlaybackBegins = true
|
controller.videoGravity = .resizeAspect
|
||||||
controller.exitsFullScreenWhenPlaybackEnds = true
|
controller.view.backgroundColor = .clear
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,9 +198,8 @@ struct BlindBoxView: View {
|
|||||||
}) {
|
}) {
|
||||||
Image(systemName: "chevron.left.circle.fill")
|
Image(systemName: "chevron.left.circle.fill")
|
||||||
.font(.system(size: 36))
|
.font(.system(size: 36))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.black)
|
||||||
.padding(12)
|
.padding(12)
|
||||||
.background(Color.black.opacity(0.5))
|
|
||||||
.clipShape(Circle())
|
.clipShape(Circle())
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
@ -213,8 +212,8 @@ struct BlindBoxView: View {
|
|||||||
.zIndex(1000)
|
.zIndex(1000)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
// 1秒后显示按钮
|
// 2秒后显示按钮
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||||
withAnimation(.easeInOut(duration: 0.3)) {
|
withAnimation(.easeInOut(duration: 0.3)) {
|
||||||
showControls = true
|
showControls = true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -185,7 +185,7 @@ struct UserProfileModal: View {
|
|||||||
|
|
||||||
// memories
|
// memories
|
||||||
Button(action: {
|
Button(action: {
|
||||||
Router.shared.navigate(to: .mediaUpload)
|
Router.shared.navigate(to: .memories)
|
||||||
}) {
|
}) {
|
||||||
HStack(spacing: 16) {
|
HStack(spacing: 16) {
|
||||||
SVGImage(svgName: "Memory")
|
SVGImage(svgName: "Memory")
|
||||||
|
|||||||
190
wake/View/Memories/MemoriesView.swift
Normal file
190
wake/View/Memories/MemoriesView.swift
Normal file
@ -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<MaterialResponse, NetworkError>) 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()
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user