import SwiftUI import AVKit import os.log 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 @State private var player: AVPlayer? 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, player: $player) .frame(width: UIScreen.main.bounds.width - 40) .background(Color.clear) .cornerRadius(10) .clipped() .onAppear { isPlaying = true } .onDisappear { isPlaying = false player?.pause() } .onTapGesture { withAnimation { showControls.toggle() } } .fullScreenCover(isPresented: $isFullscreen) { FullscreenMediaView(media: media, isPresented: $isFullscreen, isPlaying: $isPlaying, player: player) } } 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) } } .padding(.top, 8) } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .padding(.bottom, 20) } .padding(.horizontal) Spacer() // Button at bottom VStack { Spacer() Button(action: { 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) } } .navigationBarHidden(true) .navigationBarBackButtonHidden(true) .statusBar(hidden: isFullscreen) } .navigationViewStyle(StackNavigationViewStyle()) .navigationBarHidden(true) .overlay( JoinModal(isPresented: $showIPListModal) ) .onDisappear { player?.pause() 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 private let player: AVPlayer? init(media: MediaType, isPresented: Binding, isPlaying: Binding, player: AVPlayer?) { self.media = media self._isPresented = isPresented self._isPlaying = isPlaying self.player = player } var body: some View { ZStack { Color.black.edgesIgnoringSafeArea(.all) ZStack { switch media { case .image(let uiImage): Image(uiImage: uiImage) .resizable() .scaledToFit() .frame(maxWidth: .infinity, maxHeight: .infinity) .onTapGesture { withAnimation { showControls.toggle() } } case .video(_, _): if let player = player { CustomVideoPlayer(player: player) .onAppear { player.play() isPlaying = true } .onDisappear { player.pause() isPlaying = false } } } } 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() } } .onDisappear { player?.pause() } } } // MARK: - Video Player View struct VideoPlayerView: UIViewRepresentable { let url: URL @Binding var isPlaying: Bool @Binding var player: AVPlayer? func makeUIView(context: Context) -> PlayerView { let view = PlayerView() let player = view.setupPlayer(url: url) self.player = player return view } func updateUIView(_ uiView: PlayerView, context: Context) { if isPlaying { uiView.play() } else { uiView.pause() } } } // MARK: - Custom Video Player @available(iOS 14.0, *) struct CustomVideoPlayer: UIViewControllerRepresentable { let player: AVPlayer func makeUIViewController(context: Context) -> AVPlayerViewController { let controller = AVPlayerViewController() controller.player = player controller.showsPlaybackControls = false controller.videoGravity = .resizeAspect return controller } func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) { uiViewController.player = player } } // MARK: - Player View class PlayerView: UIView { private var player: AVPlayer? private var playerLayer: AVPlayerLayer? private var playerItem: AVPlayerItem? private var playerItemObserver: NSKeyValueObservation? @discardableResult func setupPlayer(url: URL) -> AVPlayer { cleanup() let asset = AVAsset(url: url) let playerItem = AVPlayerItem(asset: asset) self.playerItem = playerItem player = AVPlayer(playerItem: playerItem) let playerLayer = AVPlayerLayer(player: player) playerLayer.videoGravity = .resizeAspect layer.addSublayer(playerLayer) self.playerLayer = playerLayer playerLayer.frame = bounds NotificationCenter.default.addObserver( self, selector: #selector(playerItemDidReachEnd), name: .AVPlayerItemDidPlayToEndTime, object: playerItem ) return player! } func play() { player?.play() } func pause() { player?.pause() } private func cleanup() { if let playerItem = playerItem { NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: playerItem) } player?.pause() player?.replaceCurrentItem(with: nil) player = nil playerLayer?.removeFromSuperlayer() playerLayer = nil 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() } }