From 817cd23c4a1f3f4977f4db6ad0237ffbba7d20c1 Mon Sep 17 00:00:00 2001 From: jinyaqiu Date: Fri, 29 Aug 2025 16:30:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=9B=B2=E7=9B=92=E4=B8=BB=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/ContentView.swift | 570 ++++++++++++++++++-------------- wake/Utils/Router.swift | 3 - wake/View/Blind/BlindBox.swift | 354 -------------------- wake/View/Blind/JoinModal.swift | 2 +- wake/WakeApp.swift | 45 ++- 5 files changed, 338 insertions(+), 636 deletions(-) diff --git a/wake/ContentView.swift b/wake/ContentView.swift index 7487812..01b2f72 100644 --- a/wake/ContentView.swift +++ b/wake/ContentView.swift @@ -2,309 +2,369 @@ import SwiftUI import SwiftData import AVKit -// MARK: - 自定义过渡动画 -extension AnyTransition { - /// 创建从左向右的滑动过渡动画 - static var slideFromLeading: AnyTransition { - .asymmetric( - insertion: .move(edge: .trailing).combined(with: .opacity), // 从右侧滑入 - removal: .move(edge: .leading).combined(with: .opacity) // 向左侧滑出 - ) - } +// MARK: - Constants +private enum MediaURLs { + static let videoURL = "https://cdn.memorywake.com/users/7363409620351717377/files/7366657553935241216/39C069E1-7C3E-4261-8486-12058F855B38.mov" + static let imageURL = "https://cdn.fairclip.cn/files/7343228671693557760/20250604-164000.jpg" + static let VideoBlindURL = "https://cdn.memorywake.com/users/7363409620351717377/files/7366658779259211776/AD970D28-9D1E-4817-A245-F11967441B8F.mp4" } -// MARK: - Video Player View -struct VideoPlayerView: View { - let player: AVPlayer - @Binding var isFullscreen: Bool - - var body: some View { - ZStack(alignment: .bottomTrailing) { - VideoPlayer(player: player) - .frame(width: 300, height: 200) - .cornerRadius(12) - .overlay( - RoundedRectangle(cornerRadius: 12) - .stroke(Color.gray.opacity(0.3), lineWidth: 1) - ) - - // 全屏按钮 - Button(action: { - isFullscreen = true - player.play() - }) { - Image(systemName: "arrow.up.left.and.arrow.down.right") - .font(.system(size: 20)) - .foregroundColor(.white) - .padding(8) - .background(Color.black.opacity(0.6)) - .clipShape(Circle()) - } - .padding(16) - } - .frame(width: 300, height: 200) - .onTapGesture { - player.play() - } - .onAppear { - player.play() - } - .fullScreenCover(isPresented: $isFullscreen) { - ZStack(alignment: .topLeading) { - // 全屏视频播放器 - VideoPlayer(player: player) - .edgesIgnoringSafeArea(.all) - .onAppear { player.play() } - - // 关闭按钮 - Button(action: { - isFullscreen = false - player.pause() - }) { - Image(systemName: "xmark.circle.fill") - .font(.title) - .foregroundColor(.white) - .padding() - .background(Color.black.opacity(0.4)) - .clipShape(Circle()) - } - .padding(.top, 50) - .padding(.leading, 20) - } - .background(Color.black.edgesIgnoringSafeArea(.all)) - .statusBar(hidden: true) - } - } +private enum BlindBoxAnimationPhase { + case loading + case ready + case opening +} + +extension Notification.Name { + static let navigateToMediaViewer = Notification.Name("navigateToMediaViewer") } // MARK: - 主视图 -struct ContentView: View { - // MARK: - 状态属性 - @State private var showModal = false // 控制用户资料弹窗显示 - @State private var showSettings = false // 控制设置页面显示 - @State private var contentOffset: CGFloat = 0 // 内容偏移量 - @State private var showLogin = false - @State private var animateGradient = false - @State private var showLottieAnimation = true // 控制Lottie动画显示 - @State private var showVideoPlayer = false // 控制视频播放器显示 - @State private var isVideoFullscreen = false // 控制视频全屏状态 +struct VisualEffectView: UIViewRepresentable { + var effect: UIVisualEffect? - let timer = Timer.publish(every: 0.02, on: .main, in: .common).autoconnect() + 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 + } - // 获取模型上下文 - @Environment(\.modelContext) private var modelContext + func updateUIView(_ uiView: UIVisualEffectView, context: Context) { + // No need to update the effect + } +} + +struct AVPlayerController: UIViewControllerRepresentable { + @Binding var player: AVPlayer? - // 查询数据 - 简单查询 - @Query private var login: [Login] + func makeUIViewController(context: Context) -> AVPlayerViewController { + let controller = AVPlayerViewController() + controller.player = player + controller.showsPlaybackControls = false + controller.videoGravity = .resizeAspect + controller.view.backgroundColor = .clear + return controller + } - // 视频播放器 - private let player: AVPlayer? + func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) { + uiViewController.player = player + } +} + +struct BlindBoxView: View { + enum BlindBoxMediaType { + case video + case image + case all + } - init() { - // 使用远程视频URL - if let videoURL = URL(string: "https://cdn.fairclip.cn/files/7342843896868769793/飞书20250617-144935.mp4") { - self.player = AVPlayer(url: videoURL) - } else { - self.player = nil - print("Error: Invalid video URL") + let mediaType: BlindBoxMediaType + @State private var showLottieAnimation = true + @State private var showScalingOverlay = false + @State private var animationPhase: BlindBoxAnimationPhase = .loading + @State private var scale: CGFloat = 0.1 + @State private var videoPlayer: AVPlayer? + @State private var showControls = false + @State private var isAnimating = true + @State private var aspectRatio: CGFloat = 1.0 + @State private var isPortrait: Bool = false + @State private var displayImage: UIImage? + + init(mediaType: BlindBoxMediaType) { + self.mediaType = mediaType + } + + private func loadMedia() { + switch mediaType { + case .video: + loadVideo() + case .image: + loadImage() + case .all: + loadData() + } + } + + private func loadData() { + guard let url = URL(string: MediaURLs.imageURL) else { return } + URLSession.shared.dataTask(with: url) { data, _, _ in + if let data = data, let image = UIImage(data: data) { + DispatchQueue.main.async { + self.displayImage = image + self.aspectRatio = image.size.width / image.size.height + self.isPortrait = image.size.height > image.size.width + } + } + }.resume() + } + + private func loadImage() { + guard let url = URL(string: MediaURLs.imageURL) else { return } + URLSession.shared.dataTask(with: url) { data, _, _ in + if let data = data, let image = UIImage(data: data) { + DispatchQueue.main.async { + self.displayImage = image + self.aspectRatio = image.size.width / image.size.height + self.isPortrait = image.size.height > image.size.width + } + } + }.resume() + } + + private func loadVideo() { + guard let url = URL(string: MediaURLs.videoURL) 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 + isPortrait = height > width + } + + player.isMuted = true + self.videoPlayer = player + } + + private func startScalingAnimation() { + self.scale = 0.1 + self.showScalingOverlay = true + + withAnimation(.spring(response: 2.0, dampingFraction: 0.5, blendDuration: 0.8)) { + self.scale = 1.0 + } + } + + // MARK: - Computed Properties + private var scaledWidth: CGFloat { + if isPortrait { + return UIScreen.main.bounds.height * scale * 1/aspectRatio + } else { + return UIScreen.main.bounds.width * scale + } + } + + private var scaledHeight: CGFloat { + if isPortrait { + return UIScreen.main.bounds.height * scale + } else { + return UIScreen.main.bounds.width * scale * 1/aspectRatio } } - // MARK: - 主体视图 var body: some View { - NavigationView { - ZStack { - // 全局背景颜色背景色 - Color.themeTextWhiteSecondary.ignoresSafeArea() - - // 主内容区域 - VStack { - VStack(spacing: 20) { - // 顶部导航栏 - HStack { - // 设置按钮 - Button(action: showUserProfile) { - SVGImage(svgName: "User") - .frame(width: 24, height: 24) + ZStack { + Color.themeTextWhiteSecondary.ignoresSafeArea() + + if showScalingOverlay { + ZStack { + VisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterialLight)) + .opacity(0.3) + .edgesIgnoringSafeArea(.all) + + Group { + if mediaType == .video, let player = videoPlayer { + // Video Player + AVPlayerController(player: $videoPlayer) + .frame(width: scaledWidth, height: scaledHeight) + .opacity(scale == 1 ? 1 : 0.7) + .onAppear { player.play() } + + } else if mediaType == .image, let image = displayImage { + // Image View + Image(uiImage: image) + .resizable() + .scaledToFit() + .frame(width: scaledWidth, height: scaledHeight) + .opacity(scale == 1 ? 1 : 0.7) + } + } + .onTapGesture { + withAnimation(.easeInOut(duration: 0.1)) { + showControls.toggle() + } + } + + // 返回按钮 + if showControls { + VStack { + HStack { + Button(action: { + // 导航到BlindOutcomeView + if mediaType == .video, let videoURL = URL(string: MediaURLs.videoURL) { + Router.shared.navigate(to: .blindOutcome(media: .video(videoURL, nil))) + } else if mediaType == .image, let image = displayImage { + Router.shared.navigate(to: .blindOutcome(media: .image(image))) + } + }) { + Image(systemName: "chevron.left.circle.fill") + .font(.system(size: 36)) + .foregroundColor(.black) + .padding(12) + .clipShape(Circle()) + } + Spacer() } - Spacer() - // // 测试质感页面入口 - // NavigationLink(destination: TestView()) { - // Text("TestView") - // .font(.subheadline) - // .padding(.horizontal, 12) - // .padding(.vertical, 6) - // .background(Color.brown) - // .foregroundColor(.white) - // .cornerRadius(8) - // } - - // // 订阅测试按钮 - // NavigationLink(destination: SubscribeView()) { - // Text("Subscribe") - // .font(.subheadline) - // .padding(.horizontal, 12) - // .padding(.vertical, 6) - // .background(Color.orange) - // .foregroundColor(.white) - // .cornerRadius(8) - // } - // .padding(.trailing) - // .fullScreenCover(isPresented: $showLogin) { - // LoginView() - // } - NavigationLink(destination: SubscribeView()) { - Text("3290") - .font(Typography.font(for: .subtitle)) - .fontWeight(.bold) - .padding(.horizontal, 12) - .padding(.vertical, 6) - .background(Color.black) - .foregroundColor(.white) - .cornerRadius(16) - } - .padding(.trailing) - .fullScreenCover(isPresented: $showLogin) { - LoginView() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(.top, 50) + .padding(.leading, 20) + .zIndex(1000) + .transition(.opacity) + .onAppear { + // 2秒后显示按钮 + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + withAnimation(.easeInOut(duration: 0.3)) { + showControls = true + } } } - .padding(.horizontal) - .padding(.top, 20) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .animation(.easeInOut(duration: 1.0), value: scale) + .ignoresSafeArea() + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + withAnimation(.spring(response: 2.5, dampingFraction: 0.6, blendDuration: 1.0)) { + self.scale = 1.0 + } + } + } + } else { + // Original content + VStack { + VStack(spacing: 20) { // 标题 VStack(alignment: .leading, spacing: 4) { Text("Hi! Click And") - Text("Open Your Box~") + 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) + // 盲盒 - // 添加SVG背景图片 ZStack { // 1. 背景SVG - SVGImage(svgName: "BlindBg") - .frame( - width: UIScreen.main.bounds.width * 1.8, - height: UIScreen.main.bounds.height * 0.65 - ) - .position(x: UIScreen.main.bounds.width / 2, - y: UIScreen.main.bounds.height * 0.325) - - // 2. Lottie动画层 - if showLottieAnimation { - GIFView(name: "Blind") { - // 点击事件处理 - Router.shared.navigate(to: .blindBox(mediaType: .video)) - } - .frame(width: 300, height: 300) + if !showScalingOverlay { + SVGImage(svgName: "BlindBoxBg") + .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) } - - // 3. 视频播放器 - if showVideoPlayer, let player = player { - VideoPlayerView(player: player, isFullscreen: $isVideoFullscreen) - } else if showVideoPlayer { - Text("Video not found") - .foregroundColor(.red) + if !showScalingOverlay { + VStack(spacing: 20) { + switch animationPhase { + case .loading: + GIFView(name: "BlindLoading") + .frame(width: 300, height: 300) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 6) { + withAnimation { + animationPhase = .ready + } + } + } + + case .ready: + ZStack { + GIFView(name: "BlindReady") + .frame(width: 300, height: 300) + + // Add a transparent overlay to capture taps + Color.clear + .contentShape(Rectangle()) // Make the entire area tappable + .frame(width: 300, height: 300) + .onTapGesture { + print("点击了盲盒") + withAnimation { + animationPhase = .opening + } + } + } + .frame(width: 300, height: 300) + + case .opening: + GIFView(name: "BlindOpen") + .frame(width: 300, height: 300) + .onAppear { + self.loadMedia() + // Start animation after media is loaded + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { + self.startScalingAnimation() + } + } + } + } + .compositingGroup() + .padding() } } .frame( maxWidth: .infinity, maxHeight: UIScreen.main.bounds.height * 0.65 ) - .clipped() - // 打开 - Button(action: showUserProfile) { - Text("Go to Buy") - .font(Typography.font(for: .body)) - .fontWeight(.bold) - .frame(maxWidth: .infinity) - .padding() - .background(Color.themePrimary) - .foregroundColor(Color.themeTextMessageMain) - .cornerRadius(32) - } - .padding(.horizontal) + .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) - .offset(x: showModal ? UIScreen.main.bounds.width * 0.8 : 0) - .animation(.spring(response: 0.6, dampingFraction: 0.8), value: showModal) .edgesIgnoringSafeArea(.all) } - - // 用户资料弹窗 - SlideInModal( - isPresented: $showModal, - onDismiss: hideUserProfile - ) { - UserProfileModal( - showModal: $showModal, - showSettings: $showSettings - ) - } - .offset(x: showSettings ? UIScreen.main.bounds.width : 0) - .animation(.spring(response: 0.6, dampingFraction: 0.8), value: showSettings) - - // 设置页面遮罩层 - ZStack { - if showSettings { - Color.black.opacity(0.3) - .edgesIgnoringSafeArea(.all) - .onTapGesture(perform: hideSettings) - .transition(.opacity) - } - - if showSettings { - SettingsView(isPresented: $showSettings) - .transition(.move(edge: .leading)) - .zIndex(1) - } - } - .animation(.spring(response: 0.6, dampingFraction: 0.8), value: showSettings) } - .background(Color.themeTextWhiteSecondary) - .navigationBarHidden(true) - .navigationBarBackButtonHidden(true) } - .navigationViewStyle(StackNavigationViewStyle()) - .navigationBarHidden(true) .navigationBarBackButtonHidden(true) } - - /// 显示用户资料弹窗 - private func showUserProfile() { - withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) { - // print("登录记录数量: \(login.count)") - // for (index, item) in login.enumerated() { - // print("记录 \(index + 1): 邮箱=\(item.email), 姓名=\(item.name)") - // } - print("当前登录记录:") - for (index, item) in login.enumerated() { - print("记录 \(index + 1): 邮箱=\(item.email), 姓名=\(item.name)") - } - showModal.toggle() - } - } - - /// 隐藏用户资料弹窗 - private func hideUserProfile() { - withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) { - showModal = false - } - } - - /// 隐藏设置页面 - private func hideSettings() { - withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) { - showSettings = false - } - } } // MARK: - 预览 #Preview { - ContentView() + BlindBoxView(mediaType: .video) +} + +struct TransparentVideoPlayer: UIViewRepresentable { + func makeUIView(context: Context) -> UIView { + let view = UIView() + view.backgroundColor = .clear + view.isOpaque = false + return view + } + + func updateUIView(_ uiView: UIView, context: Context) {} } diff --git a/wake/Utils/Router.swift b/wake/Utils/Router.swift index 428840d..0230790 100644 --- a/wake/Utils/Router.swift +++ b/wake/Utils/Router.swift @@ -12,7 +12,6 @@ enum AppRoute: Hashable { case memories case subscribe case userInfo - case content @ViewBuilder var view: some View { @@ -37,8 +36,6 @@ enum AppRoute: Hashable { SubscribeView() case .userInfo: UserInfo() - case .content: - ContentView() } } } diff --git a/wake/View/Blind/BlindBox.swift b/wake/View/Blind/BlindBox.swift index 606d357..e69de29 100644 --- a/wake/View/Blind/BlindBox.swift +++ b/wake/View/Blind/BlindBox.swift @@ -1,354 +0,0 @@ -import SwiftUI -import SwiftData -import AVKit - -// MARK: - Constants -private enum MediaURLs { - static let videoURL = "https://cdn.memorywake.com/users/7363409620351717377/files/7366657553935241216/39C069E1-7C3E-4261-8486-12058F855B38.mov" - static let imageURL = "https://cdn.fairclip.cn/files/7343228671693557760/20250604-164000.jpg" - static let VideoBlindURL = "https://cdn.memorywake.com/users/7363409620351717377/files/7366658779259211776/AD970D28-9D1E-4817-A245-F11967441B8F.mp4" -} - -private enum BlindBoxAnimationPhase { - case loading - case ready - case opening -} - -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 = false - controller.videoGravity = .resizeAspect - controller.view.backgroundColor = .clear - return controller - } - - func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) { - uiViewController.player = player - } -} - -struct BlindBoxView: View { - enum BlindBoxMediaType { - case video - case image - } - - let mediaType: BlindBoxMediaType - @State private var showLottieAnimation = true - @State private var showScalingOverlay = false - @State private var animationPhase: BlindBoxAnimationPhase = .loading - @State private var scale: CGFloat = 0.1 - @State private var videoPlayer: AVPlayer? - @State private var showControls = false - @State private var isAnimating = true - @State private var aspectRatio: CGFloat = 1.0 - @State private var isPortrait: Bool = false - @State private var displayImage: UIImage? - - init(mediaType: BlindBoxMediaType) { - self.mediaType = mediaType - } - - private func loadMedia() { - switch mediaType { - case .video: - loadVideo() - case .image: - loadImage() - } - } - - private func loadImage() { - guard let url = URL(string: MediaURLs.imageURL) else { return } - URLSession.shared.dataTask(with: url) { data, _, _ in - if let data = data, let image = UIImage(data: data) { - DispatchQueue.main.async { - self.displayImage = image - self.aspectRatio = image.size.width / image.size.height - self.isPortrait = image.size.height > image.size.width - } - } - }.resume() - } - - private func loadVideo() { - guard let url = URL(string: MediaURLs.videoURL) 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 - isPortrait = height > width - } - - player.isMuted = true - self.videoPlayer = player - } - - private func startScalingAnimation() { - self.scale = 0.1 - self.showScalingOverlay = true - - withAnimation(.spring(response: 2.0, dampingFraction: 0.5, blendDuration: 0.8)) { - self.scale = 1.0 - } - } - - // MARK: - Computed Properties - private var scaledWidth: CGFloat { - if isPortrait { - return UIScreen.main.bounds.height * scale * 1/aspectRatio - } else { - return UIScreen.main.bounds.width * scale - } - } - - private var scaledHeight: CGFloat { - if isPortrait { - return UIScreen.main.bounds.height * scale - } else { - return UIScreen.main.bounds.width * scale * 1/aspectRatio - } - } - - var body: some View { - ZStack { - Color.themeTextWhiteSecondary.ignoresSafeArea() - - if showScalingOverlay { - ZStack { - VisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterialLight)) - .opacity(0.3) - .edgesIgnoringSafeArea(.all) - - Group { - if mediaType == .video, let player = videoPlayer { - // Video Player - AVPlayerController(player: $videoPlayer) - .frame(width: scaledWidth, height: scaledHeight) - .opacity(scale == 1 ? 1 : 0.7) - .onAppear { player.play() } - - } else if mediaType == .image, let image = displayImage { - // Image View - Image(uiImage: image) - .resizable() - .scaledToFit() - .frame(width: scaledWidth, height: scaledHeight) - .opacity(scale == 1 ? 1 : 0.7) - } - } - .onTapGesture { - withAnimation(.easeInOut(duration: 0.1)) { - showControls.toggle() - } - } - - // 返回按钮 - if showControls { - VStack { - HStack { - Button(action: { - // 导航到BlindOutcomeView - if mediaType == .video, let videoURL = URL(string: MediaURLs.videoURL) { - Router.shared.navigate(to: .blindOutcome(media: .video(videoURL, nil))) - } else if mediaType == .image, let image = displayImage { - Router.shared.navigate(to: .blindOutcome(media: .image(image))) - } - }) { - Image(systemName: "chevron.left.circle.fill") - .font(.system(size: 36)) - .foregroundColor(.black) - .padding(12) - .clipShape(Circle()) - } - Spacer() - } - Spacer() - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .padding(.top, 50) - .padding(.leading, 20) - .zIndex(1000) - .transition(.opacity) - .onAppear { - // 2秒后显示按钮 - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - withAnimation(.easeInOut(duration: 0.3)) { - showControls = true - } - } - } - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .animation(.easeInOut(duration: 1.0), value: scale) - .ignoresSafeArea() - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - withAnimation(.spring(response: 2.5, dampingFraction: 0.6, blendDuration: 1.0)) { - self.scale = 1.0 - } - } - } - } else { - // Original content - 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: "BlindBoxBg") - .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 { - VStack(spacing: 20) { - switch animationPhase { - case .loading: - GIFView(name: "BlindLoading") - .frame(width: 300, height: 300) - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 6) { - withAnimation { - animationPhase = .ready - } - } - } - - case .ready: - ZStack { - GIFView(name: "BlindReady") - .frame(width: 300, height: 300) - - // Add a transparent overlay to capture taps - Color.clear - .contentShape(Rectangle()) // Make the entire area tappable - .frame(width: 300, height: 300) - .onTapGesture { - print("点击了盲盒") - withAnimation { - animationPhase = .opening - } - } - } - .frame(width: 300, height: 300) - - case .opening: - GIFView(name: "BlindOpen") - .frame(width: 300, height: 300) - .onAppear { - self.loadMedia() - // Start animation after media is loaded - DispatchQueue.main.asyncAfter(deadline: .now() + 5) { - self.startScalingAnimation() - } - } - } - } - .compositingGroup() - .padding() - } - } - .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) - } - } - } - .navigationBarBackButtonHidden(true) - } -} - -// MARK: - 预览 -#Preview { - BlindBoxView(mediaType: .video) -} - -struct TransparentVideoPlayer: UIViewRepresentable { - func makeUIView(context: Context) -> UIView { - let view = UIView() - view.backgroundColor = .clear - view.isOpaque = false - return view - } - - func updateUIView(_ uiView: UIView, context: Context) {} -} diff --git a/wake/View/Blind/JoinModal.swift b/wake/View/Blind/JoinModal.swift index 0d31259..04822ff 100644 --- a/wake/View/Blind/JoinModal.swift +++ b/wake/View/Blind/JoinModal.swift @@ -35,7 +35,7 @@ struct JoinModal: View { Spacer() Button(action: { withAnimation { - Router.shared.navigate(to: .content) + Router.shared.navigate(to: .blindBox(mediaType: .all)) } }) { Image(systemName: "xmark") diff --git a/wake/WakeApp.swift b/wake/WakeApp.swift index 6b1ebdb..0b024b0 100644 --- a/wake/WakeApp.swift +++ b/wake/WakeApp.swift @@ -32,32 +32,31 @@ struct WakeApp: App { var body: some Scene { WindowGroup { - NavigationStack(path: $router.path) { - ZStack { - if showSplash { - // 显示启动页 - SplashView() - .environmentObject(authState) - .onAppear { - // 启动页显示时检查token有效性 - checkTokenValidity() - } - } else { - // 根据登录状态显示不同视图 - if authState.isAuthenticated { - // 已登录:显示主页面 - ContentView() - .environmentObject(authState) - } else { - // 未登录:显示登录界面 - LoginView() - .environmentObject(authState) + ZStack { + if showSplash { + // 显示启动页 + SplashView() + .environmentObject(authState) + .onAppear { + // 启动页显示时检查token有效性 + checkTokenValidity() } + } else { + // 根据登录状态显示不同视图 + if authState.isAuthenticated { + // 已登录:显示主页面 + NavigationStack(path: $router.path) { + BlindBoxView(mediaType: .all) + .navigationDestination(for: AppRoute.self) { route in + route.view + } + } + } else { + // 未登录:显示登录界面 + LoginView() + .environmentObject(authState) } } - .navigationDestination(for: AppRoute.self) { route in - route.view - } } .environmentObject(router) .environmentObject(authState)