import SwiftUI import AVKit import os.log /// A view that displays either an image or a video with fullscreen support struct BlindOutcomeView: View { let media: MediaType @Environment(\.presentationMode) var presentationMode @State private var isFullscreen = false @State private var isPlaying = false var body: some View { NavigationView { ZStack { Color.themeTextWhiteSecondary.ignoresSafeArea() VStack(spacing: 0) { // Custom navigation header HStack { Button(action: { // Pop two view controllers to go back two levels if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let window = windowScene.windows.first, let rootVC = window.rootViewController as? UINavigationController { let viewControllers = rootVC.viewControllers if viewControllers.count > 2 { let destinationVC = viewControllers[viewControllers.count - 3] rootVC.popToViewController(destinationVC, animated: true) } else { presentationMode.wrappedValue.dismiss() } } else { presentationMode.wrappedValue.dismiss() } }) { Image(systemName: "chevron.left") .font(.headline) .foregroundColor(Color.themeTextMessageMain) } Spacer() Text("Blind Box") .font(.headline) .foregroundColor(Color.themeTextMessageMain) Spacer() // Invisible spacer to balance the layout HStack(spacing: 4) { Image(systemName: "chevron.left") .opacity(0) Text("Back") .opacity(0) } .padding(.trailing, 8) } .padding(.vertical, 8) .background(Color.themeTextWhiteSecondary) .overlay( Rectangle() .frame(height: 1) .foregroundColor(Color.gray.opacity(0.3)), alignment: .bottom ) Spacer() // Media content ZStack { switch media { case .image(let uiImage): Image(uiImage: uiImage) .resizable() .scaledToFit() .onTapGesture { withAnimation { isFullscreen.toggle() } } case .video(let url, _): // Create an AVPlayer with the video URL let player = AVPlayer(url: url) VideoPlayer(player: player) .onAppear { player.play() isPlaying = true } .onDisappear { player.pause() isPlaying = false } } } .frame(maxWidth: .infinity) .frame(height: UIScreen.main.bounds.height * 0.4) // Takes 40% of screen height .background(Color.black) .padding() // Add some space below navigation bar Spacer() // Button below media VStack(spacing: 16) { Button(action: { // Navigate to the desired view // Replace with your navigation logic print("Button tapped!") }) { Text("Go to Next View") .font(.headline) .foregroundColor(.white) .frame(maxWidth: .infinity) .padding() .background(Color.blue) .cornerRadius(10) } .padding(.horizontal, 40) .padding(.top, 30) Spacer() // Push everything to the top } .frame(maxWidth: .infinity, maxHeight: .infinity) } } .navigationBarBackButtonHidden(true) .statusBar(hidden: isFullscreen) .fullScreenCover(isPresented: $isFullscreen) { if case .video(let url, _) = media { let player = AVPlayer(url: url) FullscreenMediaView(media: media, isPresented: $isFullscreen, isPlaying: $isPlaying, player: player) } else { FullscreenMediaView(media: media, isPresented: $isFullscreen, isPlaying: $isPlaying, player: nil) } } } } } // MARK: - Fullscreen Media View private struct FullscreenMediaView: View { let media: MediaType @Binding var isPresented: Bool @Binding var isPlaying: Bool @State private var showControls = true let player: AVPlayer? var body: some View { ZStack { Color.black.edgesIgnoringSafeArea(.all) // Media content ZStack { switch media { case .image(let uiImage): Image(uiImage: uiImage) .resizable() .scaledToFit() .onTapGesture { withAnimation { showControls.toggle() } } case .video(_, _): if let player = player { VideoPlayer(player: player) .frame(maxWidth: .infinity, maxHeight: .infinity) .onTapGesture { withAnimation { showControls.toggle() } } .overlay( showControls ? VideoControls( isPlaying: $isPlaying, player: player, onClose: { isPresented = false } ) : nil ) } } } .frame(maxWidth: .infinity, maxHeight: .infinity) // Close button (always visible) VStack { HStack { Button(action: { isPresented = false }) { Image(systemName: "xmark") .font(.title2) .foregroundColor(.white) .padding() .background(Color.black.opacity(0.5)) .clipShape(Circle()) } .padding() Spacer() } Spacer() } } .onAppear { if case .video = media { player?.play() isPlaying = true } } .onDisappear { if case .video = media { player?.pause() isPlaying = false } } } } // MARK: - Video Controls private struct VideoControls: View { @Binding var isPlaying: Bool let player: AVPlayer let onClose: () -> Void @State private var currentTime: Double = 0 @State private var duration: Double = 0 @State private var isSeeking = false private let timeFormatter: DateComponentsFormatter = { let formatter = DateComponentsFormatter() formatter.allowedUnits = [.minute, .second] formatter.zeroFormattingBehavior = .pad formatter.unitsStyle = .positional return formatter }() var body: some View { VStack { Spacer() HStack(spacing: 20) { // Play/Pause button Button(action: togglePlayPause) { Image(systemName: isPlaying ? "pause.circle.fill" : "play.circle.fill") .font(.system(size: 30)) .foregroundColor(.white) } // Time slider VStack { Slider( value: $currentTime, in: 0...max(duration, 1), onEditingChanged: { editing in isSeeking = editing if !editing { let targetTime = CMTime(seconds: currentTime, preferredTimescale: 1) player.seek(to: targetTime) } } ) .accentColor(.white) HStack { Text(timeString(from: currentTime)) .font(.caption) .foregroundColor(.white) Spacer() Text(timeString(from: duration)) .font(.caption) .foregroundColor(.white) } } // Fullscreen button Button(action: onClose) { Image(systemName: "xmark.circle.fill") .font(.system(size: 20)) .foregroundColor(.white) } } .padding() .background( LinearGradient( gradient: Gradient(colors: [Color.clear, Color.black.opacity(0.7)]), startPoint: .top, endPoint: .bottom ) ) } .onAppear(perform: setupPlayerObservers) .onDisappear(perform: removePlayerObservers) } private func togglePlayPause() { if isPlaying { player.pause() } else { player.play() } isPlaying.toggle() } private func timeString(from seconds: Double) -> String { guard !seconds.isNaN && !seconds.isInfinite else { return "--:--" } return timeFormatter.string(from: seconds) ?? "--:--" } private func setupPlayerObservers() { // Add time observer to update slider let interval = CMTime(seconds: 0.5, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) _ = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [self] time in guard !isSeeking else { return } currentTime = time.seconds // Update duration if needed if let duration = player.currentItem?.duration.seconds, duration > 0 { self.duration = duration } } // Add observer for when the video ends NotificationCenter.default.addObserver( forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: .main ) { [self] _ in // Loop the video player.seek(to: .zero) player.play() isPlaying = true } } private func removePlayerObservers() { // Remove all observers when the view disappears NotificationCenter.default.removeObserver( self, name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem ) } } // MARK: - Preview struct BlindOutcomeView_Previews: PreviewProvider { static var previews: some View { // Preview with image BlindOutcomeView(media: .image(UIImage(systemName: "photo")!)) // Preview with video if let url = URL(string: "https://example.com/sample.mp4") { BlindOutcomeView(media: .video(url, nil)) } } }