chore
This commit is contained in:
parent
1026bbb987
commit
1e163ee426
@ -2,183 +2,12 @@ import SwiftUI
|
|||||||
import SwiftData
|
import SwiftData
|
||||||
import AVKit
|
import AVKit
|
||||||
|
|
||||||
// 添加通知名称
|
// MARK: - Notification Names
|
||||||
extension Notification.Name {
|
extension Notification.Name {
|
||||||
static let blindBoxStatusChanged = Notification.Name("blindBoxStatusChanged")
|
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 {
|
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
|
let mediaType: BlindBoxMediaType
|
||||||
@State private var showModal = false // 控制用户资料弹窗显示
|
@State private var showModal = false // 控制用户资料弹窗显示
|
||||||
@State private var showSettings = false // 控制设置页面显示
|
@State private var showSettings = false // 控制设置页面显示
|
||||||
@ -561,69 +390,26 @@ struct BlindBoxView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if showScalingOverlay {
|
if showScalingOverlay {
|
||||||
ZStack {
|
MediaOverlayView(
|
||||||
VisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterialLight))
|
videoPlayer: $videoPlayer,
|
||||||
.opacity(0.3)
|
showControls: $showControls,
|
||||||
.edgesIgnoringSafeArea(.all)
|
mediaType: mediaType,
|
||||||
|
displayImage: displayImage,
|
||||||
Group {
|
scaledWidth: scaledWidth,
|
||||||
if mediaType == .video, let player = videoPlayer {
|
scaledHeight: scaledHeight,
|
||||||
// Video Player
|
scale: scale,
|
||||||
AVPlayerController(player: $videoPlayer)
|
videoURL: videoURL,
|
||||||
.frame(width: scaledWidth, height: scaledHeight)
|
imageURL: imageURL,
|
||||||
.opacity(scale == 1 ? 1 : 0.7)
|
blindGenerate: blindGenerate,
|
||||||
.onAppear { player.play() }
|
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 {
|
} else if mediaType == .image, let image = displayImage {
|
||||||
// Image View
|
Router.shared.navigate(to: .blindOutcome(media: .image(image), time: blindGenerate?.videoGenerateTime ?? "hhsdshjsjdhn", description:blindGenerate?.description ?? "informationinformationinformationinformationinformationinformation"))
|
||||||
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, !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)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.animation(.easeInOut(duration: 1.0), value: scale)
|
.animation(.easeInOut(duration: 1.0), value: scale)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
@ -639,61 +425,13 @@ struct BlindBoxView: View {
|
|||||||
VStack {
|
VStack {
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
if mediaType == .all {
|
if mediaType == .all {
|
||||||
// 顶部导航栏
|
BlindBoxNavigationBar(
|
||||||
HStack {
|
memberProfile: memberProfile,
|
||||||
// 设置按钮
|
showLogin: showLogin,
|
||||||
Button(action: showUserProfile) {
|
showUserProfile: 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标题
|
// 标题
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text("Hi! Click And")
|
Text("Hi! Click And")
|
||||||
@ -708,143 +446,29 @@ struct BlindBoxView: View {
|
|||||||
.offset(y: showScalingOverlay ? -UIScreen.main.bounds.height * 0.2 : 0)
|
.offset(y: showScalingOverlay ? -UIScreen.main.bounds.height * 0.2 : 0)
|
||||||
.animation(.easeInOut(duration: 0.5), value: showScalingOverlay)
|
.animation(.easeInOut(duration: 0.5), value: showScalingOverlay)
|
||||||
|
|
||||||
// 盲盒
|
// 盲盒动画
|
||||||
ZStack {
|
BlindBoxAnimationView(
|
||||||
// 1. 背景SVG
|
mediaType: mediaType,
|
||||||
if !showScalingOverlay {
|
animationPhase: animationPhase,
|
||||||
SVGImage(svgName: "BlindBg", contentMode: .fit)
|
blindCount: blindCount,
|
||||||
// .position(x: UIScreen.main.bounds.width / 2,
|
blindGenerate: blindGenerate,
|
||||||
// y: UIScreen.main.bounds.height * 0.325)
|
scale: scale,
|
||||||
.opacity(showScalingOverlay ? 0 : 1)
|
showScalingOverlay: showScalingOverlay,
|
||||||
.animation(.easeOut(duration: 1.5), value: showScalingOverlay)
|
showMedia: showMedia,
|
||||||
}
|
onBoxTap: {
|
||||||
if mediaType == .all && !showScalingOverlay {
|
print("点击了盲盒")
|
||||||
ZStack {
|
withAnimation {
|
||||||
SVGImage(svgName: "BlindCount")
|
animationPhase = .opening
|
||||||
.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)
|
|
||||||
// .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)
|
BlindBoxControls(
|
||||||
.animation(.easeInOut(duration: 1.5), value: showScalingOverlay)
|
mediaType: mediaType,
|
||||||
// 打开
|
animationPhase: animationPhase,
|
||||||
if mediaType == .all {
|
countdown: countdown,
|
||||||
Button(action: {
|
onButtonTap: {
|
||||||
if animationPhase == .ready {
|
if animationPhase == .ready {
|
||||||
// 处理准备就绪状态的操作
|
// 处理准备就绪状态的操作
|
||||||
// 导航到订阅页面
|
// 导航到订阅页面
|
||||||
@ -852,41 +476,9 @@ struct BlindBoxView: View {
|
|||||||
} else {
|
} else {
|
||||||
showUserProfile()
|
showUserProfile()
|
||||||
}
|
}
|
||||||
}) {
|
},
|
||||||
if animationPhase == .loading {
|
onCountdownStart: startCountdown
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.background(Color.themeTextWhiteSecondary)
|
.background(Color.themeTextWhiteSecondary)
|
||||||
@ -894,6 +486,7 @@ struct BlindBoxView: View {
|
|||||||
.animation(.spring(response: 0.6, dampingFraction: 0.8), value: showModal)
|
.animation(.spring(response: 0.6, dampingFraction: 0.8), value: showModal)
|
||||||
.edgesIgnoringSafeArea(.all)
|
.edgesIgnoringSafeArea(.all)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户资料弹窗
|
// 用户资料弹窗
|
||||||
SlideInModal(
|
SlideInModal(
|
||||||
isPresented: $showModal,
|
isPresented: $showModal,
|
||||||
@ -963,14 +556,3 @@ struct BlindBoxView: View {
|
|||||||
#Preview {
|
#Preview {
|
||||||
BlindBoxView(mediaType: .video)
|
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 feedbackView
|
||||||
case feedbackDetail(type: FeedbackView.FeedbackType)
|
case feedbackDetail(type: FeedbackView.FeedbackType)
|
||||||
case mediaUpload
|
case mediaUpload
|
||||||
case blindBox(mediaType: BlindBoxView.BlindBoxMediaType)
|
case blindBox(mediaType: BlindBoxMediaType)
|
||||||
case blindOutcome(media: MediaType, time: String? = nil, description: String? = nil)
|
case blindOutcome(media: MediaType, time: String? = nil, description: String? = nil)
|
||||||
case memories
|
case memories
|
||||||
case subscribe
|
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