feat: 盲盒成果页
This commit is contained in:
parent
c5cb87b90b
commit
34b6fa7894
@ -602,6 +602,12 @@ struct BlindBoxView: View {
|
|||||||
stopPolling()
|
stopPolling()
|
||||||
countdownTimer?.invalidate()
|
countdownTimer?.invalidate()
|
||||||
countdownTimer = nil
|
countdownTimer = nil
|
||||||
|
|
||||||
|
// Clean up video player
|
||||||
|
videoPlayer?.pause()
|
||||||
|
videoPlayer?.replaceCurrentItem(with: nil)
|
||||||
|
videoPlayer = nil
|
||||||
|
|
||||||
NotificationCenter.default.removeObserver(
|
NotificationCenter.default.removeObserver(
|
||||||
self,
|
self,
|
||||||
name: .blindBoxStatusChanged,
|
name: .blindBoxStatusChanged,
|
||||||
|
|||||||
@ -10,6 +10,7 @@ struct BlindOutcomeView: View {
|
|||||||
@Environment(\.presentationMode) var presentationMode
|
@Environment(\.presentationMode) var presentationMode
|
||||||
@State private var isFullscreen = false
|
@State private var isFullscreen = false
|
||||||
@State private var isPlaying = false
|
@State private var isPlaying = false
|
||||||
|
@State private var showControls = true
|
||||||
@State private var showIPListModal = false
|
@State private var showIPListModal = false
|
||||||
|
|
||||||
init(media: MediaType, time: String? = nil, description: String? = nil) {
|
init(media: MediaType, time: String? = nil, description: String? = nil) {
|
||||||
@ -75,6 +76,7 @@ struct BlindOutcomeView: View {
|
|||||||
Image(uiImage: uiImage)
|
Image(uiImage: uiImage)
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.padding(4)
|
.padding(4)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
@ -84,19 +86,29 @@ struct BlindOutcomeView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case .video(let url, _):
|
case .video(let url, _):
|
||||||
// Create an AVPlayer with the video URL
|
VideoPlayerView(url: url, isPlaying: $isPlaying)
|
||||||
let player = AVPlayer(url: url)
|
.frame(width: UIScreen.main.bounds.width - 40)
|
||||||
VideoPlayer(player: player)
|
.background(Color.clear)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.padding(4)
|
.clipped()
|
||||||
.onAppear {
|
.onAppear {
|
||||||
player.play()
|
// Auto-play the video when it appears
|
||||||
isPlaying = true
|
isPlaying = true
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onTapGesture {
|
||||||
player.pause()
|
withAnimation {
|
||||||
isPlaying = false
|
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) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
@ -154,17 +166,6 @@ struct BlindOutcomeView: View {
|
|||||||
.navigationBarHidden(true) // 确保隐藏系统导航栏
|
.navigationBarHidden(true) // 确保隐藏系统导航栏
|
||||||
.navigationBarBackButtonHidden(true) // 确保隐藏系统返回按钮
|
.navigationBarBackButtonHidden(true) // 确保隐藏系统返回按钮
|
||||||
.statusBar(hidden: isFullscreen)
|
.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上也能正确显示
|
.navigationViewStyle(StackNavigationViewStyle()) // 确保在iPad上也能正确显示
|
||||||
.navigationBarHidden(true) // 额外确保隐藏导航栏
|
.navigationBarHidden(true) // 额外确保隐藏导航栏
|
||||||
@ -177,7 +178,16 @@ private struct FullscreenMediaView: View {
|
|||||||
@Binding var isPresented: Bool
|
@Binding var isPresented: Bool
|
||||||
@Binding var isPlaying: Bool
|
@Binding var isPlaying: Bool
|
||||||
@State private var showControls = true
|
@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 {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
@ -190,29 +200,27 @@ private struct FullscreenMediaView: View {
|
|||||||
Image(uiImage: uiImage)
|
Image(uiImage: uiImage)
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
showControls.toggle()
|
showControls.toggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case .video(_, _):
|
case .video(let url, _):
|
||||||
if let player = player {
|
VideoPlayerView(url: url, isPlaying: $isPlaying)
|
||||||
VideoPlayer(player: player)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.onTapGesture {
|
||||||
.onTapGesture {
|
withAnimation {
|
||||||
withAnimation {
|
showControls.toggle()
|
||||||
showControls.toggle()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.overlay(
|
}
|
||||||
showControls ? VideoControls(
|
.overlay(
|
||||||
isPlaying: $isPlaying,
|
showControls ? VideoControls(
|
||||||
player: player,
|
isPlaying: $isPlaying,
|
||||||
onClose: { isPresented = false }
|
onClose: { isPresented = false }
|
||||||
) : nil
|
) : nil
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
@ -236,14 +244,16 @@ private struct FullscreenMediaView: View {
|
|||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if case .video = media {
|
if case .video = media {
|
||||||
player?.play()
|
if isPlaying {
|
||||||
isPlaying = true
|
// player?.play()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
if case .video = media {
|
if case .video = media {
|
||||||
player?.pause()
|
// player?.pause()
|
||||||
isPlaying = false
|
// player?.replaceCurrentItem(with: nil)
|
||||||
|
// player = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,127 +262,87 @@ private struct FullscreenMediaView: View {
|
|||||||
// MARK: - Video Controls
|
// MARK: - Video Controls
|
||||||
private struct VideoControls: View {
|
private struct VideoControls: View {
|
||||||
@Binding var isPlaying: Bool
|
@Binding var isPlaying: Bool
|
||||||
let player: AVPlayer
|
|
||||||
let onClose: () -> Void
|
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 {
|
var body: some View {
|
||||||
VStack {
|
// Empty view - no controls shown
|
||||||
Spacer()
|
EmptyView()
|
||||||
|
}
|
||||||
HStack(spacing: 20) {
|
}
|
||||||
// Play/Pause button
|
|
||||||
Button(action: togglePlayPause) {
|
// MARK: - Video Player with Dynamic Aspect Ratio
|
||||||
Image(systemName: isPlaying ? "pause.circle.fill" : "play.circle.fill")
|
struct VideoPlayerView: UIViewRepresentable {
|
||||||
.font(.system(size: 30))
|
let url: URL
|
||||||
.foregroundColor(.white)
|
@Binding var isPlaying: Bool
|
||||||
}
|
|
||||||
|
func makeUIView(context: Context) -> PlayerView {
|
||||||
// Time slider
|
let view = PlayerView()
|
||||||
VStack {
|
view.setupPlayer(url: url)
|
||||||
Slider(
|
return view
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func togglePlayPause() {
|
func updateUIView(_ uiView: PlayerView, context: Context) {
|
||||||
if isPlaying {
|
if isPlaying {
|
||||||
player.pause()
|
uiView.play()
|
||||||
} else {
|
} 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 {
|
func setupPlayer(url: URL) {
|
||||||
guard !seconds.isNaN && !seconds.isInfinite else { return "--:--" }
|
// Remove existing player if any
|
||||||
return timeFormatter.string(from: seconds) ?? "--:--"
|
playerLayer?.removeFromSuperlayer()
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(
|
NotificationCenter.default.addObserver(
|
||||||
forName: .AVPlayerItemDidPlayToEndTime,
|
self,
|
||||||
object: player.currentItem,
|
selector: #selector(playerItemDidReachEnd),
|
||||||
queue: .main
|
name: .AVPlayerItemDidPlayToEndTime,
|
||||||
) { [self] _ in
|
object: playerItem
|
||||||
// Loop the video
|
)
|
||||||
player.seek(to: .zero)
|
|
||||||
player.play()
|
|
||||||
isPlaying = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func removePlayerObservers() {
|
func play() {
|
||||||
// Remove all observers when the view disappears
|
player?.play()
|
||||||
NotificationCenter.default.removeObserver(
|
}
|
||||||
self,
|
|
||||||
name: .AVPlayerItemDidPlayToEndTime,
|
func pause() {
|
||||||
object: player.currentItem
|
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 {
|
if authState.isAuthenticated {
|
||||||
// 已登录:显示主页面
|
// 已登录:显示主页面
|
||||||
NavigationStack(path: $router.path) {
|
NavigationStack(path: $router.path) {
|
||||||
UserInfo()
|
BlindBoxView(mediaType: .all)
|
||||||
.navigationDestination(for: AppRoute.self) { route in
|
.navigationDestination(for: AppRoute.self) { route in
|
||||||
route.view
|
route.view
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user