chore
This commit is contained in:
parent
1026bbb987
commit
1e163ee426
@ -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) {}
|
||||
}
|
||||
|
||||
121
wake/Models/BlindBoxModels.swift
Normal file
121
wake/Models/BlindBoxModels.swift
Normal file
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
105
wake/View/BlindBox/BlindBoxAnimationView.swift
Normal file
105
wake/View/BlindBox/BlindBoxAnimationView.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
40
wake/View/BlindBox/BlindBoxNavigationBar.swift
Normal file
40
wake/View/BlindBox/BlindBoxNavigationBar.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
65
wake/View/BlindBox/MediaViews.swift
Normal file
65
wake/View/BlindBox/MediaViews.swift
Normal file
@ -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) {}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user