diff --git a/wake/ContentView.swift b/wake/ContentView.swift index b6f24cb..0b74bff 100644 --- a/wake/ContentView.swift +++ b/wake/ContentView.swift @@ -2,183 +2,12 @@ import SwiftUI import SwiftData import AVKit -// 添加通知名称 +// MARK: - Notification Names extension Notification.Name { static let blindBoxStatusChanged = Notification.Name("blindBoxStatusChanged") } -private enum BlindBoxAnimationPhase { - case loading - case ready - case opening - case none -} - -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 - case all - } - // 盲盒列表 - struct BlindList: Codable, Identifiable { - let id: Int64 - let boxCode: String - let userId: Int64 - let name: String - let boxType: String - let features: String? - let resultFileId: Int64? - let status: String - let workflowInstanceId: String? - let videoGenerateTime: String? - let createTime: String - let coverFileId: Int64? - let description: String - - enum CodingKeys: String, CodingKey { - case id - case boxCode = "box_code" - case userId = "user_id" - case name - case boxType = "box_type" - case features - case resultFileId = "result_file_id" - case status - case workflowInstanceId = "workflow_instance_id" - case videoGenerateTime = "video_generate_time" - case createTime = "create_time" - case coverFileId = "cover_file_id" - case description - } - } - // 盲盒数量 - struct BlindCount: Codable { - let availableQuantity: Int - - enum CodingKeys: String, CodingKey { - case availableQuantity = "available_quantity" - } - } - - // MARK: - BlindBox Response Model - - struct BlindBoxData: Codable { - let id: Int64 - let boxCode: String - let userId: Int64 - let name: String - let boxType: String - let features: String? - let url: String? - let status: String - let workflowInstanceId: String? - // 视频生成时间 - let videoGenerateTime: String? - let createTime: String - let description: String? - - enum CodingKeys: String, CodingKey { - case id - case boxCode = "box_code" - case userId = "user_id" - case name - case boxType = "box_type" - case features - case url - case status - case workflowInstanceId = "workflow_instance_id" - case videoGenerateTime = "video_generate_time" - case createTime = "create_time" - case description - } - - init(id: Int64, boxCode: String, userId: Int64, name: String, boxType: String, features: String?, url: String?, status: String, workflowInstanceId: String?, videoGenerateTime: String?, createTime: String, description: String?) { - self.id = id - self.boxCode = boxCode - self.userId = userId - self.name = name - self.boxType = boxType - self.features = features - self.url = url - self.status = status - self.workflowInstanceId = workflowInstanceId - self.videoGenerateTime = videoGenerateTime - self.createTime = createTime - self.description = description - } - - init(from listItem: BlindList) { - self.init( - id: listItem.id, - boxCode: listItem.boxCode, - userId: listItem.userId, - name: listItem.name, - boxType: listItem.boxType, - features: listItem.features, - url: nil, - status: listItem.status, - workflowInstanceId: listItem.workflowInstanceId, - videoGenerateTime: listItem.videoGenerateTime, - createTime: listItem.createTime, - description: listItem.description - ) - } - } - let mediaType: BlindBoxMediaType @State private var showModal = false // 控制用户资料弹窗显示 @State private var showSettings = false // 控制设置页面显示 @@ -547,83 +376,40 @@ 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, object: nil ) } - + 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() } - + MediaOverlayView( + videoPlayer: $videoPlayer, + showControls: $showControls, + mediaType: mediaType, + displayImage: displayImage, + scaledWidth: scaledWidth, + scaledHeight: scaledHeight, + scale: scale, + videoURL: videoURL, + imageURL: imageURL, + blindGenerate: blindGenerate, + onBackTap: { + // 导航到BlindOutcomeView + if mediaType == .video, !videoURL.isEmpty, let url = URL(string: videoURL) { + Router.shared.navigate(to: .blindOutcome(media: .video(url, nil), time: blindGenerate?.videoGenerateTime ?? "hhsdshjsjdhn", description:blindGenerate?.description ?? "informationinformationinformationinformationinformationinformation")) } 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) + Router.shared.navigate(to: .blindOutcome(media: .image(image), time: blindGenerate?.videoGenerateTime ?? "hhsdshjsjdhn", description:blindGenerate?.description ?? "informationinformationinformationinformationinformationinformation")) } } - .onTapGesture { - withAnimation(.easeInOut(duration: 0.1)) { - showControls.toggle() - } - } - - // 返回按钮 - if showControls { - VStack { - HStack { - Button(action: { - // 导航到BlindOutcomeView - if mediaType == .video, !videoURL.isEmpty, let url = URL(string: videoURL) { - Router.shared.navigate(to: .blindOutcome(media: .video(url, nil), time: blindGenerate?.videoGenerateTime ?? "hhsdshjsjdhn", description:blindGenerate?.description ?? "informationinformationinformationinformationinformationinformation")) - } else if mediaType == .image, let image = displayImage { - Router.shared.navigate(to: .blindOutcome(media: .image(image), time: blindGenerate?.videoGenerateTime ?? "hhsdshjsjdhn", description:blindGenerate?.description ?? "informationinformationinformationinformationinformationinformation")) - } - }) { - Image(systemName: "chevron.left") - .font(.system(size: 24)) - .foregroundColor(.black) - } - 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() @@ -639,63 +425,15 @@ struct BlindBoxView: View { VStack { VStack(spacing: 20) { if mediaType == .all { - // 顶部导航栏 - HStack { - // 设置按钮 - Button(action: showUserProfile) { - SVGImage(svgName: "User") - .frame(width: 24, height: 24) - .padding(13) // Increases tap area while keeping visual size - .contentShape(Rectangle()) // Makes the padded area tappable - } - .buttonStyle(PlainButtonStyle()) // Prevents the button from affecting the layout - - 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("\(memberProfile?.remainPoints ?? 0)") - .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() - } - } - .padding(.horizontal) - .padding(.top, 20) + BlindBoxNavigationBar( + memberProfile: memberProfile, + showLogin: showLogin, + showUserProfile: showUserProfile + ) } + // 标题 - VStack(alignment: .leading, spacing: 4) { + VStack(alignment: .leading, spacing: 4) { Text("Hi! Click And") Text("Open Your First Box~") } @@ -707,144 +445,30 @@ struct BlindBoxView: View { .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", contentMode: .fit) - // .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 mediaType == .all && !showScalingOverlay { - ZStack { - SVGImage(svgName: "BlindCount") - .frame(width: 100, height: 60) - - Text("\(blindCount?.availableQuantity ?? 0) Boxes") - .font(Typography.font(for: .body, family: .quicksandBold)) - .foregroundColor(.white) - .offset(x: 6, y: -18) + + // 盲盒动画 + BlindBoxAnimationView( + mediaType: mediaType, + animationPhase: animationPhase, + blindCount: blindCount, + blindGenerate: blindGenerate, + scale: scale, + showScalingOverlay: showScalingOverlay, + showMedia: showMedia, + onBoxTap: { + print("点击了盲盒") + withAnimation { + animationPhase = .opening } - .position(x: UIScreen.main.bounds.width * 0.7, - y: UIScreen.main.bounds.height * 0.18) - .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: - ZStack { - GIFView(name: "BlindOpen") - .frame(width: 300, height: 300) - .scaleEffect(scale) - .opacity(showMedia ? 0 : 1) // 当显示媒体时隐藏GIF - .onAppear { - print("开始播放开启动画") - // 初始缩放为1(原始大小) - self.scale = 1.0 - - // 1秒后开始全屏动画 - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - withAnimation(.spring(response: 1.0, dampingFraction: 0.7)) { - // 缩放到全屏 - self.scale = max( - UIScreen.main.bounds.width / 300, - UIScreen.main.bounds.height / 300 - ) * 1.2 - - // 全屏后稍作停留,然后缩小回原始大小 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - withAnimation(.spring(response: 0.8, dampingFraction: 0.7)) { - self.scale = 1.0 - - // 显示媒体内容 - self.showScalingOverlay = true - if mediaType == .video { - loadVideo() - } else if mediaType == .image { - loadImage() - } - - // 标记显示媒体,隐藏GIF - self.showMedia = true - } - } - } - } - } - } - .frame(width: 300, height: 300) - - case .none: - SVGImage(svgName: "BlindNone") - .frame(width: 300, height: 300) - } - } - .offset(y: -50) - .compositingGroup() - .padding() - } - // 只在未显示媒体且未播放动画时显示文字 - if !showScalingOverlay && !showMedia { - VStack(alignment: .leading, spacing: 8) { - // 从变量blindGenerate中获取description - Text(blindGenerate?.videoGenerateTime ?? "hhsdshjsjdhn") - .font(Typography.font(for: .body, family: .quicksandBold)) - .foregroundColor(Color.themeTextMessageMain) - Text(blindGenerate?.description ?? "informationinformationinformationinformationinformationinformation") - .font(.system(size: 14)) - .foregroundColor(Color.themeTextMessageMain) - } - .frame(width: UIScreen.main.bounds.width * 0.70, alignment: .leading) - .padding() - .offset(x: -10, y: UIScreen.main.bounds.height * 0.2) - } - } - .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) - // 打开 - if mediaType == .all { - Button(action: { + + // 控制按钮 + BlindBoxControls( + mediaType: mediaType, + animationPhase: animationPhase, + countdown: countdown, + onButtonTap: { if animationPhase == .ready { // 处理准备就绪状态的操作 // 导航到订阅页面 @@ -852,41 +476,9 @@ struct BlindBoxView: View { } else { showUserProfile() } - }) { - if animationPhase == .loading { - Text("Next: \(countdown.minutes):\(String(format: "%02d", countdown.seconds)).\(String(format: "%02d", countdown.milliseconds))") - .font(Typography.font(for: .body)) - .fontWeight(.bold) - .frame(maxWidth: .infinity) - .padding() - .background(Color.white) - .foregroundColor(.black) - .cornerRadius(32) - .onAppear { - startCountdown() - } - } else if animationPhase == .ready { - Text("Ready") - .font(Typography.font(for: .body)) - .fontWeight(.bold) - .frame(maxWidth: .infinity) - .padding() - .background(Color.themePrimary) - .foregroundColor(Color.themeTextMessageMain) - .cornerRadius(32) - } else { - 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) - } + }, + onCountdownStart: startCountdown + ) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.themeTextWhiteSecondary) @@ -894,6 +486,7 @@ struct BlindBoxView: View { .animation(.spring(response: 0.6, dampingFraction: 0.8), value: showModal) .edgesIgnoringSafeArea(.all) } + // 用户资料弹窗 SlideInModal( isPresented: $showModal, @@ -906,9 +499,9 @@ struct BlindBoxView: View { memberDate: $memberDate ) } - .offset(x: showSettings ? UIScreen.main.bounds.width : 0) + .offset(x: showSettings ? UIScreen.main.bounds.width : 0) .animation(.spring(response: 0.6, dampingFraction: 0.8), value: showSettings) - + // 设置页面遮罩层 ZStack { if showSettings { @@ -917,7 +510,7 @@ struct BlindBoxView: View { .onTapGesture(perform: hideSettings) .transition(.opacity) } - + if showSettings { SettingsView(isPresented: $showSettings) .transition(.move(edge: .leading)) @@ -963,14 +556,3 @@ struct BlindBoxView: View { #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/Models/BlindBoxModels.swift b/wake/Models/BlindBoxModels.swift new file mode 100644 index 0000000..cd280c4 --- /dev/null +++ b/wake/Models/BlindBoxModels.swift @@ -0,0 +1,121 @@ +// MARK: - Blind Box Models +import Foundation + +// MARK: - Blind Box Media Type +enum BlindBoxMediaType { + case video + case image + case all +} + +// MARK: - Blind Box Animation Phase +enum BlindBoxAnimationPhase { + case loading + case ready + case opening + case none +} + +// MARK: - Blind Box Data Models + +struct BlindList: Codable, Identifiable { + let id: Int64 + let boxCode: String + let userId: Int64 + let name: String + let boxType: String + let features: String? + let resultFileId: Int64? + let status: String + let workflowInstanceId: String? + let videoGenerateTime: String? + let createTime: String + let coverFileId: Int64? + let description: String + + enum CodingKeys: String, CodingKey { + case id + case boxCode = "box_code" + case userId = "user_id" + case name + case boxType = "box_type" + case features + case resultFileId = "result_file_id" + case status + case workflowInstanceId = "workflow_instance_id" + case videoGenerateTime = "video_generate_time" + case createTime = "create_time" + case coverFileId = "cover_file_id" + case description + } +} + +struct BlindCount: Codable { + let availableQuantity: Int + + enum CodingKeys: String, CodingKey { + case availableQuantity = "available_quantity" + } +} + +struct BlindBoxData: Codable { + let id: Int64 + let boxCode: String + let userId: Int64 + let name: String + let boxType: String + let features: String? + let url: String? + let status: String + let workflowInstanceId: String? + let videoGenerateTime: String? + let createTime: String + let description: String? + + enum CodingKeys: String, CodingKey { + case id + case boxCode = "box_code" + case userId = "user_id" + case name + case boxType = "box_type" + case features + case url + case status + case workflowInstanceId = "workflow_instance_id" + case videoGenerateTime = "video_generate_time" + case createTime = "create_time" + case description + } + + init(id: Int64, boxCode: String, userId: Int64, name: String, boxType: String, features: String?, url: String?, status: String, workflowInstanceId: String?, videoGenerateTime: String?, createTime: String, description: String?) { + self.id = id + self.boxCode = boxCode + self.userId = userId + self.name = name + self.boxType = boxType + self.features = features + self.url = url + self.status = status + self.workflowInstanceId = workflowInstanceId + self.videoGenerateTime = videoGenerateTime + self.createTime = createTime + self.description = description + } + + init(from listItem: BlindList) { + self.init( + id: listItem.id, + boxCode: listItem.boxCode, + userId: listItem.userId, + name: listItem.name, + boxType: listItem.boxType, + features: listItem.features, + url: nil, + status: listItem.status, + workflowInstanceId: listItem.workflowInstanceId, + videoGenerateTime: listItem.videoGenerateTime, + createTime: listItem.createTime, + description: listItem.description + ) + } +} diff --git a/wake/Utils/Router.swift b/wake/Utils/Router.swift index 6337967..6f6829e 100644 --- a/wake/Utils/Router.swift +++ b/wake/Utils/Router.swift @@ -7,7 +7,7 @@ enum AppRoute: Hashable { case feedbackView case feedbackDetail(type: FeedbackView.FeedbackType) case mediaUpload - case blindBox(mediaType: BlindBoxView.BlindBoxMediaType) + case blindBox(mediaType: BlindBoxMediaType) case blindOutcome(media: MediaType, time: String? = nil, description: String? = nil) case memories case subscribe diff --git a/wake/View/BlindBox/BlindBoxAnimationView.swift b/wake/View/BlindBox/BlindBoxAnimationView.swift new file mode 100644 index 0000000..b5dcae6 --- /dev/null +++ b/wake/View/BlindBox/BlindBoxAnimationView.swift @@ -0,0 +1,105 @@ +// MARK: - Blind Box Animation View +import SwiftUI + +struct BlindBoxAnimationView: View { + let mediaType: BlindBoxMediaType + let animationPhase: BlindBoxAnimationPhase + let blindCount: BlindCount? + let blindGenerate: BlindBoxData? + let scale: CGFloat + let showScalingOverlay: Bool + let showMedia: Bool + let onBoxTap: () -> Void + + var body: some View { + ZStack { + // 1. 背景SVG + if !showScalingOverlay { + SVGImage(svgName: "BlindBg", contentMode: .fit) + .opacity(showScalingOverlay ? 0 : 1) + .animation(.easeOut(duration: 1.5), value: showScalingOverlay) + } + + if mediaType == .all && !showScalingOverlay { + ZStack { + SVGImage(svgName: "BlindCount") + .frame(width: 100, height: 60) + + Text("\(blindCount?.availableQuantity ?? 0) Boxes") + .font(Typography.font(for: .body, family: .quicksandBold)) + .foregroundColor(.white) + .offset(x: 6, y: -18) + } + .position(x: UIScreen.main.bounds.width * 0.7, + y: UIScreen.main.bounds.height * 0.18) + .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) + + 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 { + onBoxTap() + } + } + .frame(width: 300, height: 300) + + case .opening: + ZStack { + GIFView(name: "BlindOpen") + .frame(width: 300, height: 300) + .scaleEffect(scale) + .opacity(showMedia ? 0 : 1) // 当显示媒体时隐藏GIF + } + .frame(width: 300, height: 300) + + case .none: + SVGImage(svgName: "BlindNone") + .frame(width: 300, height: 300) + } + } + .offset(y: -50) + .compositingGroup() + .padding() + } + + // 只在未显示媒体且未播放动画时显示文字 + if !showScalingOverlay && !showMedia { + VStack(alignment: .leading, spacing: 8) { + Text(blindGenerate?.videoGenerateTime ?? "hhsdshjsjdhn") + .font(Typography.font(for: .body, family: .quicksandBold)) + .foregroundColor(Color.themeTextMessageMain) + Text(blindGenerate?.description ?? "informationinformationinformationinformationinformationinformation") + .font(.system(size: 14)) + .foregroundColor(Color.themeTextMessageMain) + } + .frame(width: UIScreen.main.bounds.width * 0.70, alignment: .leading) + .padding() + .offset(x: -10, y: UIScreen.main.bounds.height * 0.2) + } + } + .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) + } +} diff --git a/wake/View/BlindBox/BlindBoxNavigationBar.swift b/wake/View/BlindBox/BlindBoxNavigationBar.swift new file mode 100644 index 0000000..23c89af --- /dev/null +++ b/wake/View/BlindBox/BlindBoxNavigationBar.swift @@ -0,0 +1,40 @@ +// MARK: - Blind Box Navigation Bar +import SwiftUI + +struct BlindBoxNavigationBar: View { + let memberProfile: MemberProfile? + let showLogin: Bool + let showUserProfile: () -> Void + + var body: some View { + HStack { + // 设置按钮 + Button(action: showUserProfile) { + SVGImage(svgName: "User") + .frame(width: 24, height: 24) + .padding(13) // Increases tap area while keeping visual size + .contentShape(Rectangle()) // Makes the padded area tappable + } + .buttonStyle(PlainButtonStyle()) // Prevents the button from affecting the layout + + Spacer() + + NavigationLink(destination: SubscribeView()) { + Text("\(memberProfile?.remainPoints ?? 0)") + .font(Typography.font(for: .subtitle)) + .fontWeight(.bold) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Color.black) + .foregroundColor(.white) + .cornerRadius(16) + } + .padding(.trailing) + .fullScreenCover(isPresented: .constant(showLogin)) { + LoginView() + } + } + .padding(.horizontal) + .padding(.top, 20) + } +} diff --git a/wake/View/BlindBox/MediaViews.swift b/wake/View/BlindBox/MediaViews.swift new file mode 100644 index 0000000..541fdb3 --- /dev/null +++ b/wake/View/BlindBox/MediaViews.swift @@ -0,0 +1,65 @@ +// MARK: - Visual Effect View +import SwiftUI + +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 + } +} + +// MARK: - AV Player Controller +import AVKit + +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 + } +} + +// MARK: - Transparent Video Player +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) {} +}