feat: memories
This commit is contained in:
parent
0c75e1b446
commit
84cc5d207f
@ -65,6 +65,7 @@ struct MemoriesView: View {
|
||||
@State private var memories: [MemoryItem] = []
|
||||
@State private var isLoading = false
|
||||
@State private var errorMessage: String?
|
||||
@State private var selectedMemory: MemoryItem? = nil
|
||||
|
||||
let columns = [
|
||||
GridItem(.flexible(), spacing: 1),
|
||||
@ -73,51 +74,64 @@ struct MemoriesView: View {
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
VStack(spacing: 0) {
|
||||
// 顶部导航栏
|
||||
HStack {
|
||||
Button(action: {
|
||||
// 返回上一级
|
||||
self.dismiss()
|
||||
}) {
|
||||
Image(systemName: "chevron.left")
|
||||
ZStack {
|
||||
VStack(spacing: 0) {
|
||||
// Top navigation bar
|
||||
HStack {
|
||||
Button(action: {
|
||||
self.dismiss()
|
||||
}) {
|
||||
Image(systemName: "chevron.left")
|
||||
.foregroundColor(.themeTextMessageMain)
|
||||
.font(.system(size: 20))
|
||||
}
|
||||
Spacer()
|
||||
Text("My Memories")
|
||||
.foregroundColor(.themeTextMessageMain)
|
||||
.font(.system(size: 20))
|
||||
.font(Typography.font(for: .body, family: .quicksandBold))
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
Text("My Memories")
|
||||
.foregroundColor(.themeTextMessageMain)
|
||||
.font(Typography.font(for: .body, family: .quicksandBold))
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.background(Color.themeTextWhiteSecondary)
|
||||
|
||||
// 内容区域
|
||||
ZStack {
|
||||
Color.themeTextWhiteSecondary.ignoresSafeArea()
|
||||
.padding()
|
||||
.background(Color.themeTextWhiteSecondary)
|
||||
|
||||
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)
|
||||
// Content area
|
||||
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)
|
||||
.onTapGesture {
|
||||
withAnimation(.spring()) {
|
||||
selectedMemory = memory
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 4)
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
.padding(.top, 4)
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Full Screen Modal
|
||||
if let memory = selectedMemory {
|
||||
FullScreenMediaView(memory: memory, isPresented: $selectedMemory)
|
||||
.transition(.opacity)
|
||||
.zIndex(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarBackButtonHidden(true)
|
||||
@ -152,17 +166,221 @@ struct MemoriesView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct FullScreenMediaView: View {
|
||||
let memory: MemoryItem
|
||||
@Binding var isPresented: MemoryItem?
|
||||
@State private var isVideoPlaying = false
|
||||
@State private var showControls = true
|
||||
@State private var controlsTimer: Timer? = nil
|
||||
@State private var player: AVPlayer? = nil
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Background
|
||||
Color.black.ignoresSafeArea()
|
||||
|
||||
// Media content with back button overlay
|
||||
ZStack {
|
||||
// Media content
|
||||
switch memory.mediaType {
|
||||
case .image(let url):
|
||||
if let imageURL = URL(string: url) {
|
||||
AsyncImage(url: imageURL) { phase in
|
||||
switch phase {
|
||||
case .success(let image):
|
||||
image
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: UIScreen.main.bounds.width,
|
||||
height: UIScreen.main.bounds.height)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
case .failure(_):
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.foregroundColor(.red)
|
||||
case .empty:
|
||||
ProgressView()
|
||||
@unknown default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case .video(let url, let previewUrl):
|
||||
if let videoURL = URL(string: url) {
|
||||
VideoPlayer(player: player)
|
||||
.onAppear {
|
||||
self.player = AVPlayer(url: videoURL)
|
||||
self.player?.play()
|
||||
self.isVideoPlaying = true
|
||||
}
|
||||
.onDisappear {
|
||||
self.player?.pause()
|
||||
self.player = nil
|
||||
}
|
||||
.frame(width: UIScreen.main.bounds.width,
|
||||
height: UIScreen.main.bounds.height)
|
||||
.onTapGesture {
|
||||
togglePlayPause()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Back button - Always visible at the top-left of the device screen
|
||||
VStack {
|
||||
HStack {
|
||||
Button(action: {
|
||||
withAnimation(.spring()) {
|
||||
isPresented = nil
|
||||
pauseVideo()
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 20, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
.padding(12)
|
||||
}
|
||||
.padding(.leading, 16)
|
||||
.padding(.top, UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.zIndex(2) // Higher z-index to keep it above media content
|
||||
|
||||
// Video controls overlay (only for video)
|
||||
if case .video = memory.mediaType, showControls {
|
||||
VStack {
|
||||
Spacer()
|
||||
// Play/pause button
|
||||
Button(action: {
|
||||
togglePlayPause()
|
||||
}) {
|
||||
Image(systemName: isVideoPlaying ? "pause.circle.fill" : "play.circle.fill")
|
||||
.font(.system(size: 70))
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.shadow(radius: 3)
|
||||
}
|
||||
.padding(.bottom, 30)
|
||||
}
|
||||
.transition(.opacity)
|
||||
.onAppear {
|
||||
resetControlsTimer()
|
||||
}
|
||||
.zIndex(3) // Highest z-index for controls
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.statusBar(hidden: true)
|
||||
}
|
||||
.onTapGesture {
|
||||
if case .video = memory.mediaType {
|
||||
withAnimation(.easeInOut) {
|
||||
showControls.toggle()
|
||||
}
|
||||
if showControls {
|
||||
resetControlsTimer()
|
||||
}
|
||||
}
|
||||
}
|
||||
.statusBar(hidden: true)
|
||||
.onAppear {
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
if case .video = memory.mediaType {
|
||||
setupVideoPlayer()
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
controlsTimer?.invalidate()
|
||||
pauseVideo()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupVideoPlayer() {
|
||||
if case .video(let url, _) = memory.mediaType, let videoURL = URL(string: url) {
|
||||
self.player = AVPlayer(url: videoURL)
|
||||
self.player?.play()
|
||||
self.isVideoPlaying = true
|
||||
|
||||
// Add observer for playback end
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: .AVPlayerItemDidPlayToEndTime,
|
||||
object: self.player?.currentItem,
|
||||
queue: .main
|
||||
) { _ in
|
||||
self.player?.seek(to: .zero) { _ in
|
||||
self.player?.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func togglePlayPause() {
|
||||
if isVideoPlaying {
|
||||
pauseVideo()
|
||||
} else {
|
||||
playVideo()
|
||||
}
|
||||
withAnimation {
|
||||
showControls = true
|
||||
}
|
||||
resetControlsTimer()
|
||||
}
|
||||
|
||||
private func playVideo() {
|
||||
player?.play()
|
||||
isVideoPlaying = true
|
||||
}
|
||||
|
||||
private func pauseVideo() {
|
||||
player?.pause()
|
||||
isVideoPlaying = false
|
||||
}
|
||||
|
||||
private func resetControlsTimer() {
|
||||
controlsTimer?.invalidate()
|
||||
controlsTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in
|
||||
withAnimation(.easeInOut) {
|
||||
showControls = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct VideoPlayer: UIViewRepresentable {
|
||||
let player: AVPlayer?
|
||||
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
let view = UIView()
|
||||
if let player = player {
|
||||
let playerLayer = AVPlayerLayer(player: player)
|
||||
playerLayer.frame = UIScreen.main.bounds
|
||||
playerLayer.videoGravity = .resizeAspectFill
|
||||
view.layer.addSublayer(playerLayer)
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIView, context: Context) {
|
||||
if let player = player, let playerLayer = uiView.layer.sublayers?.first as? AVPlayerLayer {
|
||||
playerLayer.player = player
|
||||
playerLayer.frame = UIScreen.main.bounds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MemoryCard: View {
|
||||
let memory: MemoryItem
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
ZStack {
|
||||
// Media content
|
||||
Group {
|
||||
switch memory.mediaType {
|
||||
case .image(let urlString):
|
||||
if let url = URL(string: urlString) {
|
||||
case .image(let url):
|
||||
if let url = URL(string: url) {
|
||||
AsyncImage(url: url) { phase in
|
||||
if let image = phase.image {
|
||||
image
|
||||
@ -176,8 +394,7 @@ struct MemoryCard: View {
|
||||
}
|
||||
}
|
||||
|
||||
case .video(let url, let previewUrl):
|
||||
// Use preview image for video
|
||||
case .video(_, let previewUrl):
|
||||
if let previewUrl = URL(string: previewUrl) {
|
||||
AsyncImage(url: previewUrl) { phase in
|
||||
if let image = phase.image {
|
||||
@ -210,7 +427,7 @@ struct MemoryCard: View {
|
||||
}
|
||||
|
||||
// Title and Subtitle
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text(memory.title)
|
||||
.font(Typography.font(for: .body, family: .quicksandBold))
|
||||
.foregroundColor(.themeTextMessageMain)
|
||||
@ -219,6 +436,7 @@ struct MemoryCard: View {
|
||||
Text(memory.subtitle)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.themeTextMessageMain)
|
||||
.lineLimit(2)
|
||||
}
|
||||
.padding(.horizontal, 2)
|
||||
.padding(.bottom, 8)
|
||||
@ -226,13 +444,6 @@ struct MemoryCard: View {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper extension to pause video
|
||||
private extension AVPlayer {
|
||||
func pause() {
|
||||
self.pause()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MemoriesView()
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user