import SwiftUI import SwiftData import AVKit extension Notification.Name { static let navigateToMediaViewer = Notification.Name("navigateToMediaViewer") } // MARK: - 主视图 struct VisualEffectView: UIViewRepresentable { var effect: UIVisualEffect? func makeUIView(context: Context) -> UIVisualEffectView { let view = UIVisualEffectView(effect: nil) // Use a simpler approach without animator let blurEffect = UIBlurEffect(style: .systemUltraThinMaterialLight) // Create a custom blur effect with reduced intensity let blurView = UIVisualEffectView(effect: blurEffect) blurView.alpha = 0.3 // Reduce intensity // Add a white background with low opacity for better frosted effect let backgroundView = UIView() backgroundView.backgroundColor = UIColor.white.withAlphaComponent(0.1) backgroundView.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.contentView.addSubview(backgroundView) view.contentView.addSubview(blurView) blurView.frame = view.bounds blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight] return view } func updateUIView(_ uiView: UIVisualEffectView, context: Context) { // No need to update the effect } } struct AVPlayerController: UIViewControllerRepresentable { @Binding var player: AVPlayer? func makeUIViewController(context: Context) -> AVPlayerViewController { let controller = AVPlayerViewController() controller.player = player controller.showsPlaybackControls = true controller.entersFullScreenWhenPlaybackBegins = true controller.exitsFullScreenWhenPlaybackEnds = true return controller } func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) { uiViewController.player = player } } struct BlindBoxView: View { @State private var showLottieAnimation = true // 控制Lottie动画显示 @State private var showScalingOverlay = false @State private var scale: CGFloat = 0.1 @State private var mediaToShow: MediaType? @State private var videoPlayer: AVPlayer? @State private var showControls = false // Add this line for controlling visibility @State private var aspectRatio: CGFloat = 1.0 @State private var isPortraitVideo: Bool = false private func startScalingAnimation() { // Start from 10% size self.scale = 0.1 self.showScalingOverlay = true // Slower animation with spring effect withAnimation(.spring(response: 2.0, dampingFraction: 0.5, blendDuration: 0.8)) { self.scale = 1.0 // Scale enough to cover the screen } } private func loadVideo() { guard let url = URL(string: "https://cdn.fairclip.cn/files/7329556154558844929/doubao-seedance-1-0-lite-i2v-2100199406-02174750234303300000000000000000000ffffac15403eacd2fe.mp4") else { return } let asset = AVAsset(url: url) let playerItem = AVPlayerItem(asset: asset) let player = AVPlayer(playerItem: playerItem) // 获取视频轨道以确定宽高比 let videoTracks = asset.tracks(withMediaType: .video) if let videoTrack = videoTracks.first { let size = videoTrack.naturalSize.applying(videoTrack.preferredTransform) let width = abs(size.width) let height = abs(size.height) // 计算宽高比 aspectRatio = width / height isPortraitVideo = height > width } player.isMuted = true self.videoPlayer = player } var body: some View { ZStack { // 全局背景颜色背景色 Color.themeTextWhiteSecondary.ignoresSafeArea() // 主内容区域 VStack { VStack(spacing: 20) { // 标题 VStack(alignment: .leading, spacing: 4) { Text("Hi! Click And") Text("Open Your First Box~") } .font(Typography.font(for: .smallLargeTitle)) .fontWeight(.bold) .foregroundColor(Color.themeTextMessageMain) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal) .opacity(showScalingOverlay ? 0 : 1) .offset(y: showScalingOverlay ? -UIScreen.main.bounds.height * 0.2 : 0) .animation(.easeInOut(duration: 0.5), value: showScalingOverlay) // 盲盒 ZStack { // 1. 背景SVG if !showScalingOverlay { SVGImage(svgName: "BlindBg") .frame( width: UIScreen.main.bounds.width * 1.8, height: UIScreen.main.bounds.height * 0.85 ) .position(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height * 0.325) .opacity(showScalingOverlay ? 0 : 1) .animation(.easeOut(duration: 1.5), value: showScalingOverlay) } if !showScalingOverlay { LottieView(name: "data", loopMode: .loop) .frame(width: 200, height: 200) .position(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height * 0.325) .opacity(showScalingOverlay ? 0 : 1) .animation(.easeOut(duration: 0.5), value: showScalingOverlay) } } .frame( maxWidth: .infinity, maxHeight: UIScreen.main.bounds.height * 0.65 ) .opacity(showScalingOverlay ? 0 : 1) .animation(.easeOut(duration: 1.5), value: showScalingOverlay) .offset(y: showScalingOverlay ? -100 : 0) .animation(.easeInOut(duration: 1.5), value: showScalingOverlay) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.themeTextWhiteSecondary) .edgesIgnoringSafeArea(.all) } // Scaling Overlay if showScalingOverlay { ZStack { // Frosted glass background with custom transparency VisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterialLight)) .edgesIgnoringSafeArea(.all) // Video Player if let player = videoPlayer { ZStack(alignment: .topLeading) { AVPlayerController(player: $videoPlayer) .frame( width: isPortraitVideo ? UIScreen.main.bounds.height * scale * 1/aspectRatio : UIScreen.main.bounds.width * scale, height: isPortraitVideo ? UIScreen.main.bounds.height * scale : UIScreen.main.bounds.width * scale * 1/aspectRatio ) .opacity(scale == 1 ? 1 : 0.7) .onTapGesture { withAnimation(.easeInOut(duration: 0.1)) { showControls.toggle() } } // Back Button - Always on top if showControls { Button(action: { if let media = mediaToShow { if let url = URL(string: "https://cdn.fairclip.cn/files/7348219809961742336/c5ca6151-91d3-483e-b7e7-c37f2cb69dc0.png") { URLSession.shared.dataTask(with: url) { data, response, error in if let data = data, let image = UIImage(data: data) { let media = MediaType.image(image) DispatchQueue.main.async { Router.shared.navigate(to: .blindOutcome(media: media)) } } }.resume() } } }) { Image(systemName: "chevron.left.circle.fill") .font(.system(size: 36)) .foregroundColor(.white) .padding(12) .clipShape(Circle()) } .padding(.top, 50) .padding(.leading, 20) .zIndex(1000) // Ensure it's on top .transition(.opacity) } } } else { // Fallback color in case video fails to load Color.red .frame(width: UIScreen.main.bounds.width * scale, height: UIScreen.main.bounds.height * scale) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .animation(.easeInOut(duration: 1.0), value: scale) .ignoresSafeArea() .onTapGesture { withAnimation(.easeInOut(duration: 0.1)) { showControls.toggle() } } .onAppear { // Start animation after a small delay to ensure view is loaded DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { withAnimation(.spring(response: 2.5, dampingFraction: 0.6, blendDuration: 1.0)) { self.scale = 1.0 } } } } } .navigationBarBackButtonHidden(true) .onAppear { // Load video first self.loadVideo() // Then load image if let url = URL(string: "https://cdn.fairclip.cn/files/7348219809961742336/c5ca6151-91d3-483e-b7e7-c37f2cb69dc0.png") { URLSession.shared.dataTask(with: url) { data, response, error in if let data = data, let image = UIImage(data: data) { let media = MediaType.image(image) self.mediaToShow = media // Start scaling animation after 5 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 5) { self.startScalingAnimation() } } }.resume() } } } } // MARK: - 预览 #Preview { BlindBoxView() }