diff --git a/wake/ContentView.swift b/wake/ContentView.swift index b6e7862..b37c578 100644 --- a/wake/ContentView.swift +++ b/wake/ContentView.swift @@ -602,6 +602,12 @@ struct BlindBoxView: View { stopPolling() countdownTimer?.invalidate() countdownTimer = nil + + // Clean up video player + videoPlayer?.pause() + videoPlayer?.replaceCurrentItem(with: nil) + videoPlayer = nil + NotificationCenter.default.removeObserver( self, name: .blindBoxStatusChanged, diff --git a/wake/View/Blind/BlindOutCome.swift b/wake/View/Blind/BlindOutCome.swift index d0880e3..b07430b 100644 --- a/wake/View/Blind/BlindOutCome.swift +++ b/wake/View/Blind/BlindOutCome.swift @@ -10,6 +10,7 @@ struct BlindOutcomeView: View { @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) { @@ -75,6 +76,7 @@ struct BlindOutcomeView: View { Image(uiImage: uiImage) .resizable() .scaledToFit() + .frame(maxWidth: .infinity, maxHeight: .infinity) .cornerRadius(10) .padding(4) .onTapGesture { @@ -84,19 +86,29 @@ struct BlindOutcomeView: View { } case .video(let url, _): - // Create an AVPlayer with the video URL - let player = AVPlayer(url: url) - VideoPlayer(player: player) + VideoPlayerView(url: url, isPlaying: $isPlaying) + .frame(width: UIScreen.main.bounds.width - 40) + .background(Color.clear) .cornerRadius(10) - .padding(4) + .clipped() .onAppear { - player.play() + // Auto-play the video when it appears isPlaying = true } - .onDisappear { - player.pause() - isPlaying = false + .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) { @@ -154,17 +166,6 @@ struct BlindOutcomeView: View { .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) // 额外确保隐藏导航栏 @@ -177,7 +178,16 @@ private struct FullscreenMediaView: View { @Binding var isPresented: Bool @Binding var isPlaying: Bool @State private var showControls = true - let player: AVPlayer? + @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 { @@ -190,29 +200,27 @@ private struct FullscreenMediaView: View { Image(uiImage: uiImage) .resizable() .scaledToFit() + .frame(maxWidth: .infinity, maxHeight: .infinity) .onTapGesture { withAnimation { showControls.toggle() } } - case .video(_, _): - if let player = player { - VideoPlayer(player: player) - .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, - player: player, - onClose: { isPresented = false } - ) : nil - ) - } + } + .overlay( + showControls ? VideoControls( + isPlaying: $isPlaying, + onClose: { isPresented = false } + ) : nil + ) } } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -236,14 +244,16 @@ private struct FullscreenMediaView: View { } .onAppear { if case .video = media { - player?.play() - isPlaying = true + if isPlaying { + // player?.play() + } } } .onDisappear { if case .video = media { - player?.pause() - isPlaying = false + // player?.pause() + // player?.replaceCurrentItem(with: nil) + // player = nil } } } @@ -252,127 +262,87 @@ private struct FullscreenMediaView: View { // 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) + // 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 } - private func togglePlayPause() { + func updateUIView(_ uiView: PlayerView, context: Context) { if isPlaying { - player.pause() + uiView.play() } else { - player.play() + uiView.pause() } - isPlaying.toggle() } +} + +class PlayerView: UIView { + private var player: AVPlayer? + private var playerLayer: AVPlayerLayer? - 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 - } - } + func setupPlayer(url: URL) { + // Remove existing player if any + playerLayer?.removeFromSuperlayer() - // Add observer for when the video ends + // Create new player + let asset = AVAsset(url: url) + let playerItem = AVPlayerItem(asset: asset) + 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( - forName: .AVPlayerItemDidPlayToEndTime, - object: player.currentItem, - queue: .main - ) { [self] _ in - // Loop the video - player.seek(to: .zero) - player.play() - isPlaying = true - } + self, + selector: #selector(playerItemDidReachEnd), + name: .AVPlayerItemDidPlayToEndTime, + object: playerItem + ) } - private func removePlayerObservers() { - // Remove all observers when the view disappears - NotificationCenter.default.removeObserver( - self, - name: .AVPlayerItemDidPlayToEndTime, - object: player.currentItem - ) + func play() { + player?.play() + } + + func pause() { + player?.pause() + } + + @objc private func playerItemDidReachEnd() { + player?.seek(to: .zero) + player?.play() + } + + override func layoutSubviews() { + super.layoutSubviews() + playerLayer?.frame = bounds + } + + deinit { + player?.pause() + player?.replaceCurrentItem(with: nil) + NotificationCenter.default.removeObserver(self) } } diff --git a/wake/WakeApp.swift b/wake/WakeApp.swift index fa935c4..e66f67b 100644 --- a/wake/WakeApp.swift +++ b/wake/WakeApp.swift @@ -46,7 +46,7 @@ struct WakeApp: App { if authState.isAuthenticated { // 已登录:显示主页面 NavigationStack(path: $router.path) { - UserInfo() + BlindBoxView(mediaType: .all) .navigationDestination(for: AppRoute.self) { route in route.view }