feat: memories
This commit is contained in:
parent
4836c1f4ae
commit
77c9a855c6
@ -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()
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user