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 @State private var showIPListModal = false var body: some View { NavigationView { ZStack { Color.themeTextWhiteSecondary.ignoresSafeArea() VStack(spacing: 0) { // 自定义导航栏 HStack { Button(action: { // 返回上一级 presentationMode.wrappedValue.dismiss() }) { HStack(spacing: 4) { Image(systemName: "chevron.left") .font(.headline) } .foregroundColor(Color.themeTextMessageMain) } .padding(.leading, 16) Spacer() Text("Blind Box") .font(.headline) .foregroundColor(Color.themeTextMessageMain) Spacer() // 占位,保持标题居中 HStack(spacing: 4) { Image(systemName: "chevron.left") .opacity(0) } .padding(.trailing, 16) } .padding(.vertical, 12) .background(Color.themeTextWhiteSecondary) .zIndex(1) // 确保导航栏在其他内容之上 Spacer() .frame(height: 30) // Media content GeometryReader { geometry in ZStack { // 添加白色背景 RoundedRectangle(cornerRadius: 12) .fill(Color.white) .shadow(color: Color.black.opacity(0.1), radius: 8, x: 0, y: 2) switch media { case .image(let uiImage): Image(uiImage: uiImage) .resizable() .scaledToFit() .cornerRadius(10) .padding(4) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .onTapGesture { withAnimation { isFullscreen.toggle() } } case .video(let url, _): // Create an AVPlayer with the video URL let player = AVPlayer(url: url) VideoPlayer(player: player) .cornerRadius(10) .padding(4) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .onAppear { player.play() isPlaying = true } .onDisappear { player.pause() isPlaying = false } } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .padding(.bottom, 20) } .frame(height: UIScreen.main.bounds.height / 2) .padding(.horizontal) Spacer() // Button below media VStack(spacing: 16) { Button(action: { // 如果携带的类型是video显示弹窗 if case .video = media { withAnimation { showIPListModal = true } } else { Router.shared.navigate(to: .mediaUpload) } }) { Text("Continue") .font(.headline) .foregroundColor(.themeTextMessageMain) .frame(maxWidth: .infinity) .padding() .background(Color.themePrimary) .cornerRadius(26) } .padding() } .frame(maxWidth: .infinity, maxHeight: .infinity) } } .navigationBarHidden(true) // 确保隐藏系统导航栏 .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) } } .overlay( JoinModal(isPresented: $showIPListModal) ) } .navigationViewStyle(StackNavigationViewStyle()) // 确保在iPad上也能正确显示 .navigationBarHidden(true) // 额外确保隐藏导航栏 } } // 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)) } } }