feat: memories
This commit is contained in:
parent
4836c1f4ae
commit
77c9a855c6
@ -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,46 +186,67 @@ struct FullScreenMediaView: View {
|
|||||||
// Media content with back button overlay
|
// Media content with back button overlay
|
||||||
ZStack {
|
ZStack {
|
||||||
// Media content
|
// Media content
|
||||||
switch memory.mediaType {
|
GeometryReader { geometry in
|
||||||
case .image(let url):
|
|
||||||
if let imageURL = URL(string: url) {
|
switch memory.mediaType {
|
||||||
AsyncImage(url: imageURL) { phase in
|
case .image(let url):
|
||||||
switch phase {
|
if let imageURL = URL(string: url) {
|
||||||
case .success(let image):
|
AsyncImage(url: imageURL) { phase in
|
||||||
image
|
switch phase {
|
||||||
.resizable()
|
case .success(let image):
|
||||||
.scaledToFill()
|
GeometryReader { geometry in
|
||||||
.frame(width: UIScreen.main.bounds.width,
|
ZStack {
|
||||||
height: UIScreen.main.bounds.height)
|
Color.black
|
||||||
.edgesIgnoringSafeArea(.all)
|
image
|
||||||
case .failure(_):
|
.resizable()
|
||||||
Image(systemName: "exclamationmark.triangle")
|
.scaledToFit()
|
||||||
.foregroundColor(.red)
|
.frame(
|
||||||
case .empty:
|
width: min(geometry.size.width, geometry.size.height * imageAspectRatio),
|
||||||
ProgressView()
|
height: min(geometry.size.height, geometry.size.width / imageAspectRatio)
|
||||||
@unknown default:
|
)
|
||||||
EmptyView()
|
}
|
||||||
|
.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 previewUrl):
|
||||||
case .video(let url, let previewUrl):
|
GeometryReader { geometry in
|
||||||
if let videoURL = URL(string: url) {
|
ZStack {
|
||||||
VideoPlayer(player: player)
|
Color.clear
|
||||||
.onAppear {
|
VideoPlayer(url: memory.mediaType.url, isPlaying: $isVideoPlaying)
|
||||||
self.player = AVPlayer(url: videoURL)
|
.aspectRatio(imageAspectRatio, contentMode: .fit)
|
||||||
self.player?.play()
|
.frame(
|
||||||
self.isVideoPlaying = true
|
width: min(geometry.size.width, geometry.size.height * imageAspectRatio),
|
||||||
}
|
height: min(geometry.size.height, geometry.size.width / imageAspectRatio)
|
||||||
.onDisappear {
|
)
|
||||||
self.player?.pause()
|
.onAppear {
|
||||||
self.player = nil
|
if let previewUrl = URL(string: previewUrl) {
|
||||||
}
|
loadAspectRatio(from: previewUrl)
|
||||||
.frame(width: UIScreen.main.bounds.width,
|
}
|
||||||
height: UIScreen.main.bounds.height)
|
isVideoPlaying = true
|
||||||
.onTapGesture {
|
}
|
||||||
togglePlayPause()
|
.onDisappear {
|
||||||
|
isVideoPlaying = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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
|
|
||||||
NotificationCenter.default.addObserver(
|
|
||||||
forName: .AVPlayerItemDidPlayToEndTime,
|
|
||||||
object: self.player?.currentItem,
|
|
||||||
queue: .main
|
|
||||||
) { _ in
|
|
||||||
self.player?.seek(to: .zero) { _ in
|
|
||||||
self.player?.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func togglePlayPause() {
|
// private func togglePlayPause() {
|
||||||
if isVideoPlaying {
|
// if isVideoPlaying {
|
||||||
pauseVideo()
|
// pauseVideo()
|
||||||
} else {
|
// } else {
|
||||||
playVideo()
|
// playVideo()
|
||||||
}
|
// }
|
||||||
withAnimation {
|
// withAnimation {
|
||||||
showControls = true
|
// showControls = true
|
||||||
}
|
// }
|
||||||
resetControlsTimer()
|
// resetControlsTimer()
|
||||||
}
|
// }
|
||||||
|
|
||||||
private func playVideo() {
|
// private func playVideo() {
|
||||||
player?.play()
|
// // No need to play video here
|
||||||
isVideoPlaying = true
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
private func pauseVideo() {
|
// private func pauseVideo() {
|
||||||
player?.pause()
|
// // No need to pause video here
|
||||||
isVideoPlaying = false
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
private func resetControlsTimer() {
|
// private func resetControlsTimer() {
|
||||||
controlsTimer?.invalidate()
|
// controlsTimer?.invalidate()
|
||||||
controlsTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in
|
// controlsTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in
|
||||||
withAnimation(.easeInOut) {
|
// withAnimation(.easeInOut) {
|
||||||
showControls = false
|
// showControls = false
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VideoPlayer: UIViewRepresentable {
|
struct VideoPlayer: UIViewControllerRepresentable {
|
||||||
let player: AVPlayer?
|
let url: String
|
||||||
|
@Binding var isPlaying: Bool
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UIView {
|
func makeUIViewController(context: Context) -> AVPlayerViewController {
|
||||||
let view = UIView()
|
let controller = AVPlayerViewController()
|
||||||
if let player = player {
|
let player = AVPlayer(url: URL(string: url)!)
|
||||||
let playerLayer = AVPlayerLayer(player: player)
|
controller.player = player
|
||||||
playerLayer.frame = UIScreen.main.bounds
|
controller.showsPlaybackControls = true
|
||||||
playerLayer.videoGravity = .resizeAspectFill
|
controller.videoGravity = .resizeAspect
|
||||||
view.layer.addSublayer(playerLayer)
|
|
||||||
}
|
// Make the background transparent
|
||||||
return view
|
controller.view.backgroundColor = .clear
|
||||||
|
controller.view.isOpaque = false
|
||||||
|
|
||||||
|
return controller
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIView(_ uiView: UIView, context: Context) {
|
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
|
||||||
if let player = player, let playerLayer = uiView.layer.sublayers?.first as? AVPlayerLayer {
|
if isPlaying {
|
||||||
playerLayer.player = player
|
uiViewController.player?.play()
|
||||||
playerLayer.frame = UIScreen.main.bounds
|
} else {
|
||||||
|
uiViewController.player?.pause()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -414,17 +394,27 @@ 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 {
|
||||||
image
|
GeometryReader { geometry in
|
||||||
.resizable()
|
ZStack {
|
||||||
.aspectRatio(contentMode: .fill)
|
Color.black
|
||||||
.onAppear {
|
image
|
||||||
// Get image dimensions
|
.resizable()
|
||||||
if let uiImage = image.asUIImage() {
|
.scaledToFit()
|
||||||
let size = uiImage.size
|
.frame(
|
||||||
aspectRatio = size.width / size.height
|
width: min(geometry.size.width, geometry.size.height * aspectRatio),
|
||||||
isLoading = false
|
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 {
|
} else if phase.error != nil {
|
||||||
Color.gray.opacity(0.3)
|
Color.gray.opacity(0.3)
|
||||||
} else {
|
} 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 {
|
#Preview {
|
||||||
MemoriesView()
|
MemoriesView()
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user