feat: 盲盒成果页
This commit is contained in:
parent
c5cb87b90b
commit
34b6fa7894
@ -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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user