feat: memories

This commit is contained in:
jinyaqiu 2025-09-02 20:23:21 +08:00
parent 4836c1f4ae
commit 77c9a855c6

View File

@ -99,32 +99,6 @@ struct MemoriesView: View {
ZStack { ZStack {
Color.themeTextWhiteSecondary.ignoresSafeArea() 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)
// }
// }
// }
// Replace the WaterfallGrid line with this:
ScrollView { ScrollView {
WaterfallGrid(memories) { memory in WaterfallGrid(memories) { memory in
MemoryCard(memory: memory) MemoryCard(memory: memory)
@ -186,7 +160,23 @@ struct FullScreenMediaView: View {
@State private var isVideoPlaying = false @State private var isVideoPlaying = false
@State private var showControls = true @State private var showControls = true
@State private var controlsTimer: Timer? = nil @State private var controlsTimer: Timer? = nil
@State private var player: AVPlayer? = nil @State private var imageAspectRatio: CGFloat = 1.0
@State private var isLoading = true
private func loadAspectRatio(from url: URL) {
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
let width = imageProperties[kCGImagePropertyPixelWidth] as? CGFloat,
let height = imageProperties[kCGImagePropertyPixelHeight] as? CGFloat,
height > 0 else {
imageAspectRatio = 16/9 // Default to 16:9 if we can't determine the aspect ratio
isLoading = false
return
}
imageAspectRatio = width / height
isLoading = false
}
var body: some View { var body: some View {
ZStack { ZStack {
@ -196,18 +186,34 @@ struct FullScreenMediaView: View {
// Media content with back button overlay // Media content with back button overlay
ZStack { ZStack {
// Media content // Media content
GeometryReader { geometry in
switch memory.mediaType { switch memory.mediaType {
case .image(let url): case .image(let url):
if let imageURL = URL(string: url) { if let imageURL = URL(string: url) {
AsyncImage(url: imageURL) { phase in AsyncImage(url: imageURL) { phase in
switch phase { switch phase {
case .success(let image): case .success(let image):
GeometryReader { geometry in
ZStack {
Color.black
image image
.resizable() .resizable()
.scaledToFill() .scaledToFit()
.frame(width: UIScreen.main.bounds.width, .frame(
height: UIScreen.main.bounds.height) width: min(geometry.size.width, geometry.size.height * imageAspectRatio),
.edgesIgnoringSafeArea(.all) height: min(geometry.size.height, geometry.size.width / imageAspectRatio)
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.onAppear {
if let uiImage = image.asUIImage() {
let size = uiImage.size
imageAspectRatio = size.width / size.height
isLoading = false
}
}
case .failure(_): case .failure(_):
Image(systemName: "exclamationmark.triangle") Image(systemName: "exclamationmark.triangle")
.foregroundColor(.red) .foregroundColor(.red)
@ -219,22 +225,27 @@ struct FullScreenMediaView: View {
} }
} }
case .video(let url, let previewUrl): case .video(_, let previewUrl):
if let videoURL = URL(string: url) { GeometryReader { geometry in
VideoPlayer(player: player) ZStack {
Color.clear
VideoPlayer(url: memory.mediaType.url, isPlaying: $isVideoPlaying)
.aspectRatio(imageAspectRatio, contentMode: .fit)
.frame(
width: min(geometry.size.width, geometry.size.height * imageAspectRatio),
height: min(geometry.size.height, geometry.size.width / imageAspectRatio)
)
.onAppear { .onAppear {
self.player = AVPlayer(url: videoURL) if let previewUrl = URL(string: previewUrl) {
self.player?.play() loadAspectRatio(from: previewUrl)
self.isVideoPlaying = true }
isVideoPlaying = true
} }
.onDisappear { .onDisappear {
self.player?.pause() isVideoPlaying = false
self.player = nil
} }
.frame(width: UIScreen.main.bounds.width, }
height: UIScreen.main.bounds.height) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.onTapGesture {
togglePlayPause()
} }
} }
} }
@ -245,13 +256,14 @@ struct FullScreenMediaView: View {
Button(action: { Button(action: {
withAnimation(.spring()) { withAnimation(.spring()) {
isPresented = nil isPresented = nil
pauseVideo() // pauseVideo()
} }
}) { }) {
Image(systemName: "chevron.left") Image(systemName: "chevron.left")
.font(.system(size: 20, weight: .bold)) .font(.system(size: 20, weight: .bold))
.foregroundColor(.white) .foregroundColor(.white)
.padding(12) .padding(12)
.background(Circle().fill(Color.black.opacity(0.4)))
} }
.padding(.leading, 16) .padding(.leading, 16)
.padding(.top, UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0) .padding(.top, UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0)
@ -261,28 +273,6 @@ struct FullScreenMediaView: View {
Spacer() Spacer()
} }
.zIndex(2) // Higher z-index to keep it above media content .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() .ignoresSafeArea()
.statusBar(hidden: true) .statusBar(hidden: true)
@ -290,96 +280,86 @@ struct FullScreenMediaView: View {
.onTapGesture { .onTapGesture {
if case .video = memory.mediaType { if case .video = memory.mediaType {
withAnimation(.easeInOut) { withAnimation(.easeInOut) {
showControls.toggle() // showControls.toggle()
}
if showControls {
resetControlsTimer()
} }
// if showControls {
// resetControlsTimer()
// }
} }
} }
.statusBar(hidden: true) .statusBar(hidden: true)
.onAppear { .onAppear {
UIApplication.shared.isIdleTimerDisabled = true UIApplication.shared.isIdleTimerDisabled = true
if case .video = memory.mediaType { if case .video = memory.mediaType {
setupVideoPlayer() // setupVideoPlayer()
} }
} }
.onDisappear { .onDisappear {
UIApplication.shared.isIdleTimerDisabled = false UIApplication.shared.isIdleTimerDisabled = false
controlsTimer?.invalidate() controlsTimer?.invalidate()
pauseVideo() // pauseVideo()
} }
} }
private func setupVideoPlayer() { // private func setupVideoPlayer() {
if case .video(let url, _) = memory.mediaType, let videoURL = URL(string: url) { // if case .video(let url, _) = memory.mediaType, let videoURL = URL(string: url) {
self.player = AVPlayer(url: videoURL) // // No need to set up player here
self.player?.play() // }
self.isVideoPlaying = true // }
// Add observer for playback end // private func togglePlayPause() {
NotificationCenter.default.addObserver( // if isVideoPlaying {
forName: .AVPlayerItemDidPlayToEndTime, // pauseVideo()
object: self.player?.currentItem, // } else {
queue: .main // playVideo()
) { _ in // }
self.player?.seek(to: .zero) { _ in // withAnimation {
self.player?.play() // showControls = true
} // }
} // resetControlsTimer()
} // }
// private func playVideo() {
// // No need to play video here
// }
// private func pauseVideo() {
// // No need to pause video here
// }
// private func resetControlsTimer() {
// controlsTimer?.invalidate()
// controlsTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in
// withAnimation(.easeInOut) {
// showControls = false
// }
// }
// }
} }
private func togglePlayPause() { struct VideoPlayer: UIViewControllerRepresentable {
if isVideoPlaying { let url: String
pauseVideo() @Binding var isPlaying: Bool
func makeUIViewController(context: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
let player = AVPlayer(url: URL(string: url)!)
controller.player = player
controller.showsPlaybackControls = true
controller.videoGravity = .resizeAspect
// Make the background transparent
controller.view.backgroundColor = .clear
controller.view.isOpaque = false
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
if isPlaying {
uiViewController.player?.play()
} else { } else {
playVideo() uiViewController.player?.pause()
}
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
} }
} }
} }
@ -414,11 +394,21 @@ struct MemoryCard: View {
AsyncImage(url: url) { phase in AsyncImage(url: url) { phase in
Group { Group {
if let image = phase.image { if let image = phase.image {
GeometryReader { geometry in
ZStack {
Color.black
image image
.resizable() .resizable()
.aspectRatio(contentMode: .fill) .scaledToFit()
.frame(
width: min(geometry.size.width, geometry.size.height * aspectRatio),
height: min(geometry.size.height, geometry.size.width / aspectRatio)
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.aspectRatio(aspectRatio, contentMode: aspectRatio > 1 ? .fit : .fill)
.onAppear { .onAppear {
// Get image dimensions
if let uiImage = image.asUIImage() { if let uiImage = image.asUIImage() {
let size = uiImage.size let size = uiImage.size
aspectRatio = size.width / size.height aspectRatio = size.width / size.height
@ -506,6 +496,23 @@ extension View {
} }
} }
// Add this extension to MemoryMediaType to get the URL
private extension MemoryMediaType {
var isVideo: Bool {
if case .video = self { return true }
return false
}
var url: String {
switch self {
case .image(let url):
return url
case .video(let url, _):
return url
}
}
}
#Preview { #Preview {
MemoriesView() MemoriesView()
} }