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 {
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 {
WaterfallGrid(memories) { memory in
MemoryCard(memory: memory)
@ -186,7 +160,23 @@ struct FullScreenMediaView: View {
@State private var isVideoPlaying = false
@State private var showControls = true
@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 {
ZStack {
@ -196,46 +186,67 @@ struct FullScreenMediaView: View {
// 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()
GeometryReader { geometry in
switch memory.mediaType {
case .image(let url):
if let imageURL = URL(string: url) {
AsyncImage(url: imageURL) { phase in
switch phase {
case .success(let image):
GeometryReader { geometry in
ZStack {
Color.black
image
.resizable()
.scaledToFit()
.frame(
width: min(geometry.size.width, geometry.size.height * imageAspectRatio),
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(_):
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()
case .video(_, let previewUrl):
GeometryReader { geometry in
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 {
if let previewUrl = URL(string: previewUrl) {
loadAspectRatio(from: previewUrl)
}
isVideoPlaying = true
}
.onDisappear {
isVideoPlaying = false
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
}
}
}
@ -245,13 +256,14 @@ struct FullScreenMediaView: View {
Button(action: {
withAnimation(.spring()) {
isPresented = nil
pauseVideo()
// pauseVideo()
}
}) {
Image(systemName: "chevron.left")
.font(.system(size: 20, weight: .bold))
.foregroundColor(.white)
.padding(12)
.background(Circle().fill(Color.black.opacity(0.4)))
}
.padding(.leading, 16)
.padding(.top, UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0)
@ -261,28 +273,6 @@ struct FullScreenMediaView: View {
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)
@ -290,96 +280,86 @@ struct FullScreenMediaView: View {
.onTapGesture {
if case .video = memory.mediaType {
withAnimation(.easeInOut) {
showControls.toggle()
}
if showControls {
resetControlsTimer()
// showControls.toggle()
}
// if showControls {
// resetControlsTimer()
// }
}
}
.statusBar(hidden: true)
.onAppear {
UIApplication.shared.isIdleTimerDisabled = true
if case .video = memory.mediaType {
setupVideoPlayer()
// setupVideoPlayer()
}
}
.onDisappear {
UIApplication.shared.isIdleTimerDisabled = false
controlsTimer?.invalidate()
pauseVideo()
// 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 setupVideoPlayer() {
// if case .video(let url, _) = memory.mediaType, let videoURL = URL(string: url) {
// // No need to set up player here
// }
// }
private func togglePlayPause() {
if isVideoPlaying {
pauseVideo()
} else {
playVideo()
}
withAnimation {
showControls = true
}
resetControlsTimer()
}
// private func togglePlayPause() {
// if isVideoPlaying {
// pauseVideo()
// } else {
// playVideo()
// }
// withAnimation {
// showControls = true
// }
// resetControlsTimer()
// }
private func playVideo() {
player?.play()
isVideoPlaying = true
}
// private func playVideo() {
// // No need to play video here
// }
private func pauseVideo() {
player?.pause()
isVideoPlaying = false
}
// 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 resetControlsTimer() {
// controlsTimer?.invalidate()
// controlsTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in
// withAnimation(.easeInOut) {
// showControls = false
// }
// }
// }
}
struct VideoPlayer: UIViewRepresentable {
let player: AVPlayer?
struct VideoPlayer: UIViewControllerRepresentable {
let url: String
@Binding var isPlaying: Bool
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 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 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
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
if isPlaying {
uiViewController.player?.play()
} else {
uiViewController.player?.pause()
}
}
}
@ -414,17 +394,27 @@ struct MemoryCard: View {
AsyncImage(url: url) { phase in
Group {
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.onAppear {
// Get image dimensions
if let uiImage = image.asUIImage() {
let size = uiImage.size
aspectRatio = size.width / size.height
isLoading = false
}
GeometryReader { geometry in
ZStack {
Color.black
image
.resizable()
.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 {
if let uiImage = image.asUIImage() {
let size = uiImage.size
aspectRatio = size.width / size.height
isLoading = false
}
}
} else if phase.error != nil {
Color.gray.opacity(0.3)
} else {
@ -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 {
MemoriesView()
}