feat: 盲盒成果页

This commit is contained in:
jinyaqiu 2025-09-01 16:06:08 +08:00
parent c5cb87b90b
commit 34b6fa7894
3 changed files with 124 additions and 148 deletions

View File

@ -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,

View File

@ -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<Bool>, isPlaying: Binding<Bool>, 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)
}
}

View File

@ -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
}