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 let time: String? let description: String? @Environment(\.presentationMode) var presentationMode @State private var isFullscreen = false @State private var isPlaying = false @State private var showControls = true @State private var showIPListModal = false init(media: MediaType, time: String? = nil, description: String? = nil) { self.media = media self.time = time self.description = description } 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 VStack(spacing: 16) { ZStack { // 添加白色背景 RoundedRectangle(cornerRadius: 12) .fill(Color.white) .shadow(color: Color.black.opacity(0.1), radius: 8, x: 0, y: 2) VStack(spacing: 0) { switch media { case .image(let uiImage): Image(uiImage: uiImage) .resizable() .scaledToFit() .frame(maxWidth: .infinity, maxHeight: .infinity) .cornerRadius(10) .padding(4) .onTapGesture { withAnimation { isFullscreen.toggle() } } case .video(let url, _): VideoPlayerView(url: url, isPlaying: $isPlaying) .frame(width: UIScreen.main.bounds.width - 40) .background(Color.clear) .cornerRadius(10) .clipped() .onAppear { // Auto-play the video when it appears isPlaying = true } .onTapGesture { withAnimation { showControls.toggle() } } .fullScreenCover(isPresented: $isFullscreen) { FullscreenMediaView(media: media, isPresented: $isFullscreen, isPlaying: $isPlaying, player: nil) } .overlay( showControls ? VideoControls( isPlaying: $isPlaying, onClose: { isFullscreen = false } ) : nil ) } VStack(alignment: .leading, spacing: 8) { if let description = description, !description.isEmpty { VStack(alignment: .leading, spacing: 2) { Text("Description") .font(Typography.font(for: .body, family: .quicksandBold)) .foregroundColor(.themeTextMessageMain) Text(description) .font(.system(size: 12)) .foregroundColor(Color.themeTextMessageMain) .fixedSize(horizontal: false, vertical: true) } .padding(.horizontal, 12) .padding(.bottom, 12) } } .frame(maxWidth: .infinity, alignment: .leading) } .padding(.top, 8) } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .padding(.bottom, 20) } .padding(.horizontal) Spacer() // Button at bottom VStack { Spacer() Button(action: { // 如果携带的类型是video显示弹窗 if case .video = media { withAnimation { showIPListModal = true } } else { Router.shared.navigate(to: .feedbackView) } }) { Text("Continue") .font(.headline) .foregroundColor(.themeTextMessageMain) .frame(maxWidth: .infinity) .padding() .background(Color.themePrimary) .cornerRadius(26) } .padding(.horizontal) } .padding(.bottom, 20) } .onDisappear { // Clean up video player when view disappears if case .video = media { isPlaying = false } } } .navigationBarHidden(true) // 确保隐藏系统导航栏 .navigationBarBackButtonHidden(true) // 确保隐藏系统返回按钮 .statusBar(hidden: isFullscreen) } .navigationViewStyle(StackNavigationViewStyle()) // 确保在iPad上也能正确显示 .navigationBarHidden(true) // 额外确保隐藏导航栏 .overlay( JoinModal(isPresented: $showIPListModal) ) } } // MARK: - Fullscreen Media View private struct FullscreenMediaView: View { let media: MediaType @Binding var isPresented: Bool @Binding var isPlaying: Bool @State private var showControls = true @State private var player: AVPlayer? init(media: MediaType, isPresented: Binding, isPlaying: Binding, player: AVPlayer?) { self.media = media self._isPresented = isPresented self._isPlaying = isPlaying if let player = player { self._player = State(initialValue: player) } } var body: some View { ZStack { Color.black.edgesIgnoringSafeArea(.all) // Media content ZStack { switch media { case .image(let uiImage): Image(uiImage: uiImage) .resizable() .scaledToFit() .frame(maxWidth: .infinity, maxHeight: .infinity) .onTapGesture { withAnimation { showControls.toggle() } } case .video(let url, _): VideoPlayerView(url: url, isPlaying: $isPlaying) .frame(maxWidth: .infinity, maxHeight: .infinity) .onTapGesture { withAnimation { showControls.toggle() } } .overlay( showControls ? VideoControls( isPlaying: $isPlaying, 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 { if isPlaying { // player?.play() } } } .onDisappear { if case .video = media { // player?.pause() // player?.replaceCurrentItem(with: nil) // player = nil } } } } // MARK: - Video Controls private struct VideoControls: View { @Binding var isPlaying: Bool let onClose: () -> Void var body: some View { // Empty view - no controls shown EmptyView() } } // MARK: - Video Player with Dynamic Aspect Ratio struct VideoPlayerView: UIViewRepresentable { let url: URL @Binding var isPlaying: Bool func makeUIView(context: Context) -> PlayerView { let view = PlayerView() view.setupPlayer(url: url) return view } func updateUIView(_ uiView: PlayerView, context: Context) { if isPlaying { uiView.play() } else { uiView.pause() } } } class PlayerView: UIView { private var player: AVPlayer? private var playerLayer: AVPlayerLayer? private var playerItem: AVPlayerItem? private var playerItemObserver: NSKeyValueObservation? func setupPlayer(url: URL) { // Clean up existing resources cleanup() // Create new player let asset = AVAsset(url: url) let playerItem = AVPlayerItem(asset: asset) self.playerItem = playerItem player = AVPlayer(playerItem: playerItem) // Setup player layer let playerLayer = AVPlayerLayer(player: player) playerLayer.videoGravity = .resizeAspect layer.addSublayer(playerLayer) self.playerLayer = playerLayer // Layout playerLayer.frame = bounds // Add observer for video end NotificationCenter.default.addObserver( self, selector: #selector(playerItemDidReachEnd), name: .AVPlayerItemDidPlayToEndTime, object: playerItem ) } func play() { player?.play() } func pause() { player?.pause() } private func cleanup() { // Remove observers if let playerItem = playerItem { NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: playerItem) } // Pause and clean up player player?.pause() player?.replaceCurrentItem(with: nil) player = nil // Remove player layer playerLayer?.removeFromSuperlayer() playerLayer = nil // Release player item playerItem?.cancelPendingSeeks() playerItem?.asset.cancelLoading() playerItem = nil } @objc private func playerItemDidReachEnd() { player?.seek(to: .zero) player?.play() } override func layoutSubviews() { super.layoutSubviews() playerLayer?.frame = bounds } deinit { cleanup() } } // MARK: - Preview struct BlindOutcomeView_Previews: PreviewProvider { static var previews: some View { // Preview with image and details BlindOutcomeView( media: .image(UIImage(systemName: "photo")!), time: "2:30", description: "This is a sample description for the preview. It shows how the text will wrap and display below the media content." ) // Preview with video and details if let url = URL(string: "https://example.com/sample.mp4") { BlindOutcomeView( media: .video(url, nil), time: "1:45", description: "Video content with time and description" ) } } }