Compare commits

...

4 Commits

Author SHA1 Message Date
jinyaqiu
55255bf0f8 feat: yangshi 2025-09-03 15:43:52 +08:00
jinyaqiu
b1cd957d0c feat: 样式 2025-09-03 14:53:03 +08:00
jinyaqiu
36b95abc37 feat: blind接口解析问题 2025-09-03 14:12:50 +08:00
jinyaqiu
df32ea71bb feat: 样式优化 2025-09-03 11:42:14 +08:00
19 changed files with 757 additions and 93 deletions

View File

@ -1,5 +1,5 @@
{ {
"originHash" : "e8f130fe30ac6cdc940ef06ee1e8535e9f46ffee6aeead1722b9525562f6ce08", "originHash" : "7ea295cc5e3eb8ef644b89ce2b47a7600994b67c8582ee354b643cd63250740d",
"pins" : [ "pins" : [
{ {
"identity" : "alamofire", "identity" : "alamofire",
@ -9,6 +9,51 @@
"revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5", "revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5",
"version" : "5.10.2" "version" : "5.10.2"
} }
},
{
"identity" : "cocoalumberjack",
"kind" : "remoteSourceControl",
"location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git",
"state" : {
"revision" : "a9ed4b6f9bdedce7d77046f43adfb8ce1fd54114",
"version" : "3.9.0"
}
},
{
"identity" : "lottie-spm",
"kind" : "remoteSourceControl",
"location" : "https://github.com/airbnb/lottie-spm.git",
"state" : {
"revision" : "04f2fd18cc9404a0a0917265a449002674f24ec9",
"version" : "4.5.2"
}
},
{
"identity" : "svgkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SVGKit/SVGKit.git",
"state" : {
"revision" : "58152b9f7c85eab239160b36ffdfd364aa43d666",
"version" : "3.0.0"
}
},
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log",
"state" : {
"revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2",
"version" : "1.6.4"
}
},
{
"identity" : "waterfallgrid",
"kind" : "remoteSourceControl",
"location" : "https://github.com/paololeonardi/WaterfallGrid.git",
"state" : {
"revision" : "c7c08652c3540adf8e48409c351879b4caea7e89",
"version" : "1.1.0"
}
} }
], ],
"version" : 3 "version" : 3

View File

@ -1,4 +1,4 @@
<svg width="106" height="66" viewBox="0 0 106 66" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="90" height="66" viewBox="0 0 90 66" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.666667 60C0.666667 62.9455 3.05448 65.3333 6 65.3333C8.94552 65.3333 11.3333 62.9455 11.3333 60C11.3333 57.0545 8.94552 54.6667 6 54.6667C3.05448 54.6667 0.666667 57.0545 0.666667 60ZM22.2802 31.0204L23.152 31.5102L22.2802 31.0204ZM97.5663 30V29H24.0239V30V31H97.5663V30ZM22.2802 31.0204L21.4083 30.5306L5.12816 59.5102L6 60L6.87184 60.4898L23.152 31.5102L22.2802 31.0204ZM24.0239 30V29C22.9395 29 21.9395 29.5852 21.4083 30.5306L22.2802 31.0204L23.152 31.5102C23.3291 31.1951 23.6624 31 24.0239 31V30Z" fill="black"/> <path d="M0.666667 60C0.666667 62.9455 3.05448 65.3333 6 65.3333C8.94552 65.3333 11.3333 62.9455 11.3333 60C11.3333 57.0545 8.94552 54.6667 6 54.6667C3.05448 54.6667 0.666667 57.0545 0.666667 60ZM28.9006 28.8162L29.7066 29.4081L28.9006 28.8162ZM75.5 28V27H30.5126V28V29H75.5V28ZM28.9006 28.8162L28.0946 28.2243L5.194 59.4081L6 60L6.806 60.5919L29.7066 29.4081L28.9006 28.8162ZM30.5126 28V27C29.5577 27 28.6598 27.4546 28.0946 28.2243L28.9006 28.8162L29.7066 29.4081C29.895 29.1515 30.1943 29 30.5126 29V28Z" fill="black"/>
<rect x="16.8433" width="89.1566" height="30" rx="15" fill="black"/> <rect x="15.5" width="74" height="30" rx="15" fill="black"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 705 B

After

Width:  |  Height:  |  Size: 687 B

View File

@ -74,7 +74,7 @@ struct BlindBoxView: View {
case all case all
} }
// //
struct BlindList: Codable, Identifiable { struct BlindList: Codable, Identifiable, Equatable, Hashable {
let id: Int64 let id: Int64
let boxCode: String let boxCode: String
let userId: Int64 let userId: Int64
@ -104,6 +104,40 @@ struct BlindBoxView: View {
case coverFileId = "cover_file_id" case coverFileId = "cover_file_id"
case description case description
} }
// Implement Equatable
static func == (lhs: BlindBoxView.BlindList, rhs: BlindBoxView.BlindList) -> Bool {
return lhs.id == rhs.id &&
lhs.boxCode == rhs.boxCode &&
lhs.userId == rhs.userId &&
lhs.name == rhs.name &&
lhs.boxType == rhs.boxType &&
lhs.features == rhs.features &&
lhs.resultFileId == rhs.resultFileId &&
lhs.status == rhs.status &&
lhs.workflowInstanceId == rhs.workflowInstanceId &&
lhs.videoGenerateTime == rhs.videoGenerateTime &&
lhs.createTime == rhs.createTime &&
lhs.coverFileId == rhs.coverFileId &&
lhs.description == rhs.description
}
// Implement Hashable
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(boxCode)
hasher.combine(userId)
hasher.combine(name)
hasher.combine(boxType)
hasher.combine(features)
hasher.combine(resultFileId)
hasher.combine(status)
hasher.combine(workflowInstanceId)
hasher.combine(videoGenerateTime)
hasher.combine(createTime)
hasher.combine(coverFileId)
hasher.combine(description)
}
} }
// //
struct BlindCount: Codable { struct BlindCount: Codable {
@ -114,22 +148,35 @@ struct BlindBoxView: View {
} }
} }
// MARK: - BlindBox Response Model // MARK: - API Response Wrapper
struct APIResponse<T: Codable>: Codable {
let code: Int
let data: T
let message: String?
struct BlindBoxData: Codable { enum CodingKeys: String, CodingKey {
case code
case data
case message
}
}
// MARK: - BlindBox Response Model
struct BlindBoxData: Codable, Equatable {
let id: Int64 let id: Int64
let boxCode: String let boxCode: String
let userId: Int64 let userId: Int64
let name: String let name: String
let boxType: String let boxType: String
let features: String? let features: String?
let url: String? let resultFileId: Int64?
let status: String let status: String
let workflowInstanceId: String? let workflowInstanceId: String
//
let videoGenerateTime: String? let videoGenerateTime: String?
let createTime: String let createTime: String
let description: String? let coverFileId: Int64?
let url: String?
let description: String
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case id case id
@ -138,29 +185,59 @@ struct BlindBoxView: View {
case name case name
case boxType = "box_type" case boxType = "box_type"
case features case features
case url case resultFileId = "result_file_id"
case status case status
case workflowInstanceId = "workflow_instance_id" case workflowInstanceId = "workflow_instance_id"
case videoGenerateTime = "video_generate_time" case videoGenerateTime = "video_generate_time"
case createTime = "create_time" case createTime = "create_time"
case coverFileId = "cover_file_id"
case url
case description 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?) { init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// Required fields with default values
id = try container.decode(Int64.self, forKey: .id)
boxCode = try container.decodeIfPresent(String.self, forKey: .boxCode) ?? ""
userId = try container.decode(Int64.self, forKey: .userId)
name = try container.decodeIfPresent(String.self, forKey: .name) ?? ""
boxType = try container.decode(String.self, forKey: .boxType)
status = try container.decode(String.self, forKey: .status)
workflowInstanceId = try container.decode(String.self, forKey: .workflowInstanceId)
createTime = try container.decode(String.self, forKey: .createTime)
// Optional fields
features = try container.decodeIfPresent(String.self, forKey: .features)
resultFileId = try container.decodeIfPresent(Int64.self, forKey: .resultFileId)
videoGenerateTime = try container.decodeIfPresent(String.self, forKey: .videoGenerateTime)
coverFileId = try container.decodeIfPresent(Int64.self, forKey: .coverFileId)
url = try container.decodeIfPresent(String.self, forKey: .url)
description = try container.decodeIfPresent(String.self, forKey: .description) ?? ""
}
// Initializer for creating instances manually
init(id: Int64, boxCode: String, userId: Int64, name: String, boxType: String, features: String?,
resultFileId: Int64?, status: String, workflowInstanceId: String, videoGenerateTime: String?,
createTime: String, coverFileId: Int64?, url: String?, description: String) {
self.id = id self.id = id
self.boxCode = boxCode self.boxCode = boxCode
self.userId = userId self.userId = userId
self.name = name self.name = name
self.boxType = boxType self.boxType = boxType
self.features = features self.features = features
self.url = url self.resultFileId = resultFileId
self.status = status self.status = status
self.workflowInstanceId = workflowInstanceId self.workflowInstanceId = workflowInstanceId
self.videoGenerateTime = videoGenerateTime self.videoGenerateTime = videoGenerateTime
self.createTime = createTime self.createTime = createTime
self.coverFileId = coverFileId
self.url = url
self.description = description self.description = description
} }
// Initializer for creating from BlindList
init(from listItem: BlindList) { init(from listItem: BlindList) {
self.init( self.init(
id: listItem.id, id: listItem.id,
@ -169,17 +246,28 @@ struct BlindBoxView: View {
name: listItem.name, name: listItem.name,
boxType: listItem.boxType, boxType: listItem.boxType,
features: listItem.features, features: listItem.features,
url: nil, resultFileId: listItem.resultFileId,
status: listItem.status, status: listItem.status,
workflowInstanceId: listItem.workflowInstanceId, workflowInstanceId: listItem.workflowInstanceId ?? "",
videoGenerateTime: listItem.videoGenerateTime, videoGenerateTime: listItem.videoGenerateTime,
createTime: listItem.createTime, createTime: listItem.createTime,
coverFileId: listItem.coverFileId,
url: nil,
description: listItem.description description: listItem.description
) )
} }
// Equatable conformance
static func == (lhs: BlindBoxData, rhs: BlindBoxData) -> Bool {
return lhs.id == rhs.id &&
lhs.workflowInstanceId == rhs.workflowInstanceId &&
lhs.status == rhs.status
}
} }
let mediaType: BlindBoxMediaType let mediaType: BlindBoxMediaType
private var ids: [String]?
@State private var blindBoxItems: [BlindList]?
@State private var showModal = false // @State private var showModal = false //
@State private var showSettings = false // @State private var showSettings = false //
@State private var isMember = false // @State private var isMember = false //
@ -217,8 +305,9 @@ struct BlindBoxView: View {
// - // -
@Query private var login: [Login] @Query private var login: [Login]
init(mediaType: BlindBoxMediaType) { init(mediaType: BlindBoxMediaType, ids: [String]? = nil) {
self.mediaType = mediaType self.mediaType = mediaType
self.ids = ids
} }
// //
@ -313,6 +402,8 @@ struct BlindBoxView: View {
// none // none
if self.blindList.isEmpty { if self.blindList.isEmpty {
self.animationPhase = .none self.animationPhase = .none
}else{
self.animationPhase = .loading
} }
print("✅ 成功获取 \(self.blindList.count) 个盲盒") print("✅ 成功获取 \(self.blindList.count) 个盲盒")
case .failure(let error): case .failure(let error):
@ -343,9 +434,19 @@ struct BlindBoxView: View {
return return
} }
//
var parameters: [String: Any] = ["box_type": currentBoxType=="Video" ? "Second" : "First"]
//
if let ids = ids, !ids.isEmpty {
let fileIds = ids.compactMap { Int64($0) } // Convert string to Int64
print("material_ids: \(fileIds)")
parameters["material_ids"] = fileIds
}
NetworkService.shared.postWithToken( NetworkService.shared.postWithToken(
path: "/blind_box/generate/mock", path: "/blind_box/generate",
parameters: ["box_type": currentBoxType] parameters: parameters
) { (result: Result<APIResponse<BlindBoxData>, NetworkError>) in ) { (result: Result<APIResponse<BlindBoxData>, NetworkError>) in
DispatchQueue.main.async { DispatchQueue.main.async {
switch result { switch result {
@ -534,7 +635,7 @@ struct BlindBoxView: View {
default: default:
// //
withAnimation { withAnimation {
self.animationPhase = .ready self.animationPhase = .loading
} }
break break
} }
@ -645,7 +746,7 @@ struct BlindBoxView: View {
Button(action: showUserProfile) { Button(action: showUserProfile) {
SVGImage(svgName: "User") SVGImage(svgName: "User")
.frame(width: 24, height: 24) .frame(width: 24, height: 24)
.padding(13) // Increases tap area while keeping visual size .padding(.trailing, 13) // Increases tap area while keeping visual size
.contentShape(Rectangle()) // Makes the padded area tappable .contentShape(Rectangle()) // Makes the padded area tappable
} }
.buttonStyle(PlainButtonStyle()) // Prevents the button from affecting the layout .buttonStyle(PlainButtonStyle()) // Prevents the button from affecting the layout
@ -686,18 +787,25 @@ struct BlindBoxView: View {
.foregroundColor(.white) .foregroundColor(.white)
.cornerRadius(16) .cornerRadius(16)
} }
.padding(.trailing)
.fullScreenCover(isPresented: $showLogin) { .fullScreenCover(isPresented: $showLogin) {
LoginView() LoginView()
} }
} }
.padding(.horizontal)
.padding(.top, 20) .padding(.top, 20)
.padding(.horizontal)
} }
// //
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text("Hi! Click And") Text("Hi! Click And")
Text("Open Your First Box~") HStack {
Text("Open Your")
if mediaType == .video {
Text("Second")
} else if mediaType == .image {
Text("First")
}
Text("Box~")
}
} }
.font(Typography.font(for: .smallLargeTitle)) .font(Typography.font(for: .smallLargeTitle))
.fontWeight(.bold) .fontWeight(.bold)
@ -809,7 +917,7 @@ struct BlindBoxView: View {
.frame(width: 300, height: 300) .frame(width: 300, height: 300)
case .none: case .none:
SVGImage(svgName: "BlindNone") SVGImageHtml(svgName: "BlindNone")
.frame(width: 300, height: 300) .frame(width: 300, height: 300)
} }
} }
@ -839,9 +947,9 @@ struct BlindBoxView: View {
maxHeight: UIScreen.main.bounds.height * 0.65 maxHeight: UIScreen.main.bounds.height * 0.65
) )
.opacity(showScalingOverlay ? 0 : 1) .opacity(showScalingOverlay ? 0 : 1)
.animation(.easeOut(duration: 1.5), value: showScalingOverlay) .animation(.easeOut(duration: 0.5), value: showScalingOverlay)
.offset(y: showScalingOverlay ? -100 : 0) .offset(y: showScalingOverlay ? -100 : 0)
.animation(.easeInOut(duration: 1.5), value: showScalingOverlay) .animation(.easeInOut(duration: 0.5), value: showScalingOverlay)
// //
if mediaType == .all { if mediaType == .all {
Button(action: { Button(action: {

View File

@ -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: BlindBoxView.BlindBoxMediaType, ids: [String]? = nil)
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
@ -16,6 +16,7 @@ enum AppRoute: Hashable {
case about case about
case permissionManagement case permissionManagement
case feedback case feedback
case blindList
@ViewBuilder @ViewBuilder
var view: some View { var view: some View {
@ -30,8 +31,8 @@ enum AppRoute: Hashable {
FeedbackDetailView(feedbackType: type) FeedbackDetailView(feedbackType: type)
case .mediaUpload: case .mediaUpload:
MediaUploadView() MediaUploadView()
case .blindBox(let mediaType): case .blindBox(let mediaType, let ids):
BlindBoxView(mediaType: mediaType) BlindBoxView(mediaType: mediaType, ids: ids)
case .blindOutcome(let media, let time, let description): case .blindOutcome(let media, let time, let description):
BlindOutcomeView(media: media, time: time, description: description) BlindOutcomeView(media: media, time: time, description: description)
case .memories: case .memories:
@ -48,6 +49,8 @@ enum AppRoute: Hashable {
PermissionManagementView() PermissionManagementView()
case .feedback: case .feedback:
FeedbackView() FeedbackView()
case .blindList:
BlindListView()
} }
} }
} }

View File

@ -66,6 +66,8 @@ struct BlindOutcomeView: View {
RoundedRectangle(cornerRadius: 12) RoundedRectangle(cornerRadius: 12)
.fill(Color.white) .fill(Color.white)
.shadow(color: Color.black.opacity(0.1), radius: 8, x: 0, y: 2) .shadow(color: Color.black.opacity(0.1), radius: 8, x: 0, y: 2)
Spacer()
.frame(height: 16)
VStack(spacing: 0) { VStack(spacing: 0) {
switch media { switch media {
@ -75,7 +77,6 @@ struct BlindOutcomeView: View {
.scaledToFit() .scaledToFit()
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.cornerRadius(10) .cornerRadius(10)
.padding(4)
.onTapGesture { .onTapGesture {
withAnimation { withAnimation {
isFullscreen.toggle() isFullscreen.toggle()
@ -84,7 +85,7 @@ struct BlindOutcomeView: View {
case .video(let url, _): case .video(let url, _):
VideoPlayerView(url: url, isPlaying: $isPlaying, player: $player) VideoPlayerView(url: url, isPlaying: $isPlaying, player: $player)
.frame(width: UIScreen.main.bounds.width - 40) .frame(width: UIScreen.main.bounds.width)
.background(Color.clear) .background(Color.clear)
.cornerRadius(10) .cornerRadius(10)
.clipped() .clipped()

View File

@ -154,10 +154,10 @@ struct JoinModal: View {
.font(Typography.font(for: .body, family: .quicksandBold)) .font(Typography.font(for: .body, family: .quicksandBold))
} }
.foregroundColor(.themeTextMessageMain) .foregroundColor(.themeTextMessageMain)
.padding(.vertical, 12) .padding(.vertical, 16)
.padding(.horizontal, 30) .padding(.horizontal, 30)
.background(Color.themePrimary) .background(Color.themePrimary)
.cornerRadius(20) .cornerRadius(32)
} }
.padding(.top, 16) .padding(.top, 16)
// //

View File

@ -196,27 +196,27 @@ struct UserProfileModal: View {
} }
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
// Box //Box
// Button(action: { Button(action: {
// Router.shared.navigate(to: .mediaUpload) Router.shared.navigate(to: .blindList)
// }) { }) {
// HStack(spacing: 16) { HStack(spacing: 16) {
// SVGImage(svgName: "Box") SVGImage(svgName: "Box")
// .foregroundColor(.orange) .foregroundColor(.orange)
// .frame(width: 20, height: 20) .frame(width: 20, height: 20)
// Text("My Blind Box") Text("My Blind Box")
// .font(Typography.font(for: .body)) .font(Typography.font(for: .body))
// .fontWeight(.bold) .fontWeight(.bold)
// .foregroundColor(.themeTextMessageMain) .foregroundColor(.themeTextMessageMain)
// Spacer() Spacer()
// } }
// .padding() .padding()
// .cornerRadius(10) .cornerRadius(10)
// .contentShape(Rectangle()) .contentShape(Rectangle())
// } }
// .buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
// setting // setting
Button(action: { Button(action: {

View File

@ -39,7 +39,7 @@ struct CreditsInfoCard: View {
// MARK: - // MARK: -
private var mainCreditsSection: some View { private var mainCreditsSection: some View {
HStack(spacing: Theme.Spacing.md) { HStack(spacing: Theme.Spacing.sm) {
// //
HStack(spacing: Theme.Spacing.sm) { HStack(spacing: Theme.Spacing.sm) {
Text("Credits:") Text("Credits:")

View File

@ -25,7 +25,7 @@ struct AboutUsView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
// IP Address Section // IP Address Section
VStack(spacing: 12) { VStack(spacing: 12) {
SVGImage(svgName: "AboutIP") SVGImageHtml(svgName: "AboutIP")
.frame(width: 102, height: 102) .frame(width: 102, height: 102)
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@ -33,11 +33,11 @@ struct AboutUsView: View {
// Version & ICP Info // Version & ICP Info
VStack(spacing: 12) { VStack(spacing: 12) {
Text("Version : 1.1.1") Text("Version : 2.0.0")
.font(.system(size: 12)) .font(.system(size: 12))
.foregroundColor(.themeTextMessageMain) .foregroundColor(.themeTextMessageMain)
Text("ICP 备案号: 京ICP备XXXXXXXX号") Text("ICP 备案号: 沪ICP备2025133004号-2A")
.font(.system(size: 12)) .font(.system(size: 12))
.foregroundColor(.themeTextMessageMain) .foregroundColor(.themeTextMessageMain)
} }

View File

@ -121,7 +121,7 @@ struct AccountView: View {
}) { }) {
Text("Confirm") Text("Confirm")
.foregroundColor(.themeTextWhite) .foregroundColor(.themeTextMessage)
.font(.system(size: 12)) .font(.system(size: 12))
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding() .padding()

View File

@ -0,0 +1,483 @@
import SwiftUI
import AVKit
import WaterfallGrid
// MARK: - API Response Models for BlindList
struct BlindListMaterialResponse: Decodable {
let code: Int
let data: [BlindBoxItem]
}
struct BlindBoxItem: Identifiable, Decodable {
let id: Int64
let boxCode: String
let userId: Int64
let name: String
let boxType: String
let features: String?
let resultFile: FileInfo?
let status: String
let workflowInstanceId: Int64?
let videoGenerateTime: String?
let createTime: String
let coverFile: FileInfo?
let description: String
struct FileInfo: Decodable {
let id: String
let fileName: String
let url: String
let metadata: [String: String]?
enum CodingKeys: String, CodingKey {
case id
case fileName = "file_name"
case url
case metadata
}
}
enum CodingKeys: String, CodingKey {
case id
case boxCode = "box_code"
case userId = "user_id"
case name
case boxType = "box_type"
case features
case resultFile = "result_file"
case status
case workflowInstanceId = "workflow_instance_id"
case videoGenerateTime = "video_generate_time"
case createTime = "create_time"
case coverFile = "cover_file"
case description
}
}
enum BlindListMemoryMediaType: Equatable {
case image(String)
case video(url: String, previewUrl: String)
var isVideo: Bool {
if case .video = self { return true }
return false
}
var url: String {
switch self {
case .image(let url):
return url
case .video(let url, _):
return url
}
}
}
// MARK: - View Models
struct BlindListFileInfo {
let id: String
let fileName: String
let url: String
}
struct BlindListMemoryItem: Identifiable {
let id: String
let name: String
let description: String
let fileInfo: BlindListFileInfo
let previewFileInfo: BlindListFileInfo
var title: String { name }
var subtitle: String { description }
var mediaType: BlindListMemoryMediaType {
// Determine media type based on file extension or other criteria
// For now, default to image
return .image(fileInfo.url)
}
var aspectRatio: CGFloat { 1.0 }
}
struct BlindListView: View {
@Environment(\.dismiss) private var dismiss
@State private var memories: [BlindListMemoryItem] = []
@State private var isLoading = false
@State private var errorMessage: String?
@State private var selectedMemory: BlindListMemoryItem? = nil
let columns = [
GridItem(.flexible(), spacing: 1),
GridItem(.flexible(), spacing: 1)
]
var body: some View {
NavigationView {
ZStack {
VStack(spacing: 0) {
//
HStack {
Button(action: {
self.dismiss()
}) {
Image(systemName: "chevron.left")
.foregroundColor(.themeTextMessageMain)
.font(.system(size: 20))
}
Spacer()
Text("我的盲盒")
.foregroundColor(.themeTextMessageMain)
.font(Typography.font(for: .body, family: .quicksandBold))
Spacer()
}
.padding()
.background(Color.themeTextWhiteSecondary)
//
ZStack {
Color.themeTextWhiteSecondary.ignoresSafeArea()
ScrollView {
WaterfallGrid(memories) { memory in
BlindListMemoryCard(memory: memory)
.onTapGesture {
withAnimation(.spring()) {
selectedMemory = memory
}
}
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
}
}
}
//
if let memory = selectedMemory {
BlindListFullScreenMediaView(memory: memory, isPresented: $selectedMemory)
.transition(.opacity)
.zIndex(1)
}
}
}
.navigationBarBackButtonHidden(true)
.onAppear {
fetchList()
}
}
private func fetchList() {
isLoading = true
errorMessage = nil
NetworkService.shared.get(path: "/blind_boxs/query", parameters: nil) { [self] (result: Result<BlindListMaterialResponse, NetworkError>) in
DispatchQueue.main.async { [self] in
self.isLoading = false
switch result {
case .success(let response):
print("✅ Successfully fetched \(response.data.count) blind box items")
// Convert BlindBoxItem to BlindListMemoryItem
self.memories = response.data
.filter { $0.status == "Opened" }
.map { item in
BlindListMemoryItem(
id: String(item.id),
name: item.name,
description: item.description,
fileInfo: BlindListFileInfo(
id: item.resultFile?.id ?? "",
fileName: item.resultFile?.fileName ?? "",
url: item.resultFile?.url ?? ""
),
previewFileInfo: BlindListFileInfo(
id: item.coverFile?.id ?? "",
fileName: item.coverFile?.fileName ?? "",
url: item.coverFile?.url ?? ""
)
)
}
case .failure(let error):
self.errorMessage = error.localizedDescription
print("❌ Failed to fetch blind box items: \(error.localizedDescription)")
}
}
}
}
}
struct BlindListFullScreenMediaView: View {
let memory: BlindListMemoryItem
@Binding var isPresented: BlindListMemoryItem?
@State private var isVideoPlaying = false
@State private var showControls = true
@State private var controlsTimer: Timer? = nil
@State private var imageAspectRatio: CGFloat = 1.0
@State private var isLoading = true
private func loadAspectRatio(from url: URL) {
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
let width = imageProperties[kCGImagePropertyPixelWidth] as? CGFloat,
let height = imageProperties[kCGImagePropertyPixelHeight] as? CGFloat,
height > 0 else {
imageAspectRatio = 16/9
isLoading = false
return
}
imageAspectRatio = width / height
isLoading = false
}
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
ZStack {
GeometryReader { geometry in
switch memory.mediaType {
case .image(let url):
if let imageURL = URL(string: url) {
AsyncImage(url: imageURL) { phase in
switch phase {
case .success(let image):
GeometryReader { geometry in
ZStack {
Color.black
image
.resizable()
.scaledToFit()
.frame(
width: min(geometry.size.width, geometry.size.height * imageAspectRatio),
height: min(geometry.size.height, geometry.size.width / imageAspectRatio)
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.onAppear {
if let uiImage = image.asUIImage() {
let size = uiImage.size
imageAspectRatio = size.width / size.height
isLoading = false
}
}
case .failure(_):
Image(systemName: "exclamationmark.triangle")
.foregroundColor(.red)
case .empty:
ProgressView()
@unknown default:
EmptyView()
}
}
}
case .video(_, let previewUrl):
GeometryReader { geometry in
ZStack {
Color.clear
BlindListVideoPlayer(url: memory.mediaType.url, isPlaying: $isVideoPlaying)
.aspectRatio(imageAspectRatio, contentMode: .fit)
.frame(
width: min(geometry.size.width, geometry.size.height * imageAspectRatio),
height: min(geometry.size.height, geometry.size.width / imageAspectRatio)
)
.onAppear {
if let previewUrl = URL(string: previewUrl) {
loadAspectRatio(from: previewUrl)
}
isVideoPlaying = true
}
.onDisappear {
isVideoPlaying = false
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
}
}
}
VStack {
HStack {
Button(action: {
withAnimation(.spring()) {
isPresented = nil
}
}) {
Image(systemName: "chevron.left")
.font(.system(size: 20, weight: .bold))
.foregroundColor(.white)
.padding(12)
.background(Circle().fill(Color.black.opacity(0.4)))
}
.padding(.leading, 16)
.padding(.top, UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0)
Spacer()
}
Spacer()
}
.zIndex(2)
}
.ignoresSafeArea()
.statusBar(hidden: true)
}
.onTapGesture {
if case .video = memory.mediaType {
withAnimation(.easeInOut) {
showControls.toggle()
}
}
}
.statusBar(hidden: true)
.onAppear {
UIApplication.shared.isIdleTimerDisabled = true
}
.onDisappear {
UIApplication.shared.isIdleTimerDisabled = false
controlsTimer?.invalidate()
}
}
}
struct BlindListVideoPlayer: UIViewControllerRepresentable {
let url: String
@Binding var isPlaying: Bool
func makeUIViewController(context: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
let player = AVPlayer(url: URL(string: url)!)
controller.player = player
controller.showsPlaybackControls = true
controller.videoGravity = .resizeAspect
controller.view.backgroundColor = .clear
controller.view.isOpaque = false
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
if isPlaying {
uiViewController.player?.play()
} else {
uiViewController.player?.pause()
}
}
}
struct BlindListMemoryCard: View {
let memory: BlindListMemoryItem
@State private var aspectRatio: CGFloat = 1.0
@State private var isLoading = true
private func loadAspectRatio(from url: URL) {
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
let width = imageProperties[kCGImagePropertyPixelWidth] as? CGFloat,
let height = imageProperties[kCGImagePropertyPixelHeight] as? CGFloat,
height > 0 else {
aspectRatio = 16/9
isLoading = false
return
}
aspectRatio = width / height
isLoading = false
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
ZStack {
Group {
switch memory.mediaType {
case .image(let url):
if let url = URL(string: url) {
AsyncImage(url: url) { phase in
Group {
if let image = phase.image {
GeometryReader { geometry in
ZStack {
Color.black
image
.resizable()
.scaledToFit()
.frame(
width: min(geometry.size.width, geometry.size.height * aspectRatio),
height: min(geometry.size.height, geometry.size.width / aspectRatio)
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.aspectRatio(aspectRatio, contentMode: aspectRatio > 1 ? .fit : .fill)
.onAppear {
if let uiImage = image.asUIImage() {
let size = uiImage.size
aspectRatio = size.width / size.height
isLoading = false
}
}
} else if phase.error != nil {
Color.gray.opacity(0.3)
} else {
ProgressView()
}
}
}
}
case .video(_, let previewUrl):
if let previewUrl = URL(string: previewUrl) {
AsyncImage(url: previewUrl) { phase in
Group {
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.onAppear {
loadAspectRatio(from: previewUrl)
}
} else if phase.error != nil {
Color.gray.opacity(0.3)
} else {
ProgressView()
}
}
}
} else {
Color.gray.opacity(0.3)
}
}
}
.frame(
width: (UIScreen.main.bounds.width / 2) - 24,
height: (UIScreen.main.bounds.width / 2 - 24) / (isLoading ? 1 : aspectRatio)
)
.clipped()
.cornerRadius(12)
if case .video = memory.mediaType {
Image(systemName: "play.circle.fill")
.font(.system(size: 40))
.foregroundColor(.white.opacity(0.9))
.shadow(radius: 3)
}
}
VStack(alignment: .leading, spacing: 4) {
Text(memory.title)
.font(Typography.font(for: .body, family: .quicksandBold))
.foregroundColor(.themeTextMessageMain)
.lineLimit(1)
Text(memory.subtitle)
.font(.system(size: 14))
.foregroundColor(.themeTextMessageMain)
.lineLimit(2)
}
.padding(.horizontal, 2)
.padding(.bottom, 8)
}
}
}

View File

@ -26,18 +26,24 @@ struct PermissionManagementView: View {
// 1. // 1.
PermissionRow( PermissionRow(
title: "Gallery Permissions", title: "Gallery Permissions",
isEnabled: photoLibraryStatus == .authorized isEnabled: photoLibraryStatus == .authorized,
) { action: {
requestPhotoLibraryPermission() // requestPhotoLibraryPermission()
} },
openSettings: openAppSettings
)
.background(Color.white)
// 2. // 2.
PermissionRow( PermissionRow(
title: "Notification Permissions", title: "Notification Permissions",
isEnabled: notificationStatus == .authorized isEnabled: notificationStatus == .authorized,
) { action: {
requestNotificationPermission() // requestNotificationPermission()
} },
openSettings: openAppSettings
)
.background(Color.white)
} }
.background(Color.white) .background(Color.white)
.cornerRadius(16) .cornerRadius(16)
@ -120,9 +126,11 @@ struct PermissionManagementView: View {
/// ///
private func openAppSettings() { private func openAppSettings() {
if let url = URL(string: UIApplication.openSettingsURLString) { guard let settingsUrl = URL(string: UIApplication.openSettingsURLString),
UIApplication.shared.open(url) UIApplication.shared.canOpenURL(settingsUrl) else {
return
} }
UIApplication.shared.open(settingsUrl)
} }
} }
@ -131,6 +139,14 @@ struct PermissionRow: View {
let title: String // let title: String //
let isEnabled: Bool // let isEnabled: Bool //
let action: () -> Void // let action: () -> Void //
let openSettings: () -> Void //
init(title: String, isEnabled: Bool, action: @escaping () -> Void, openSettings: @escaping () -> Void) {
self.title = title
self.isEnabled = isEnabled
self.action = action
self.openSettings = openSettings
}
var body: some View { var body: some View {
Button(action: action) { Button(action: action) {
@ -143,10 +159,15 @@ struct PermissionRow: View {
Spacer() Spacer()
// //
Toggle("", isOn: .constant(isEnabled)) Toggle("", isOn: .init(
get: { isEnabled },
set: { newValue in
//
openSettings()
}
))
.labelsHidden() .labelsHidden()
.tint(Color.themePrimary) .tint(Color.themePrimary)
.disabled(true)
.onAppear { .onAppear {
// 使 // 使
let themeColor = UIColor(Color.themePrimary) let themeColor = UIColor(Color.themePrimary)
@ -159,13 +180,6 @@ struct PermissionRow: View {
.padding() .padding()
} }
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
//
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(Color(.systemGray6)),
alignment: .bottom
)
} }
} }

View File

@ -60,7 +60,11 @@ struct SettingsView: View {
settingRow( settingRow(
icon: "Suport", icon: "Suport",
title: "Support & Service", title: "Support & Service",
action: {} action: {
if let url = URL(string: "https://work.weixin.qq.com/kfid/kfca0ac87f4e05e8bfd") {
UIApplication.shared.open(url)
}
}
) )
// //

View File

@ -116,7 +116,7 @@ struct UserInfo: View {
// Content VStack // Content VStack
VStack(spacing: 20) { VStack(spacing: 20) {
// Title // Title
Text(showUsername ? "Add Your Avatar" : "What's Your Name?") Text(showUsername ? "What's Your Name?" : "Add Your Avatar")
.font(Typography.font(for: .body, family: .quicksandBold)) .font(Typography.font(for: .body, family: .quicksandBold))
.frame(maxWidth: .infinity, alignment: .center) .frame(maxWidth: .infinity, alignment: .center)

View File

@ -97,6 +97,9 @@ struct SubscriptionStatusBar: View {
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
} }
.frame(height: height) .frame(height: height)
.onTapGesture {
onSubscribeTap?()
}
} }
// MARK: - // MARK: -

View File

@ -94,7 +94,7 @@ struct SubscribeView: View {
// //
legalLinks legalLinks
Spacer(minLength: 100) Spacer(minLength: 10)
} }
} }
.background(Theme.Colors.background) .background(Theme.Colors.background)

View File

@ -328,6 +328,9 @@ struct MediaUploadView: View {
"preview_file_id": result.thumbnailId ?? result.fileId "preview_file_id": result.thumbnailId ?? result.fileId
] ]
} }
let filesID = uploadResults.map { (_, result) -> String in
return result.fileId
}
// POST/material // POST/material
NetworkService.shared.postWithToken( NetworkService.shared.postWithToken(
@ -337,9 +340,9 @@ struct MediaUploadView: View {
switch result { switch result {
case .success: case .success:
print("✅ 素材提交成功") print("✅ 素材提交成功")
// //
DispatchQueue.main.async { DispatchQueue.main.async {
Router.shared.navigate(to: .blindBox(mediaType: .video)) Router.shared.navigate(to: .blindBox(mediaType: .video, ids: filesID))
} }
case .failure(let error): case .failure(let error):
print("❌ 素材提交失败: \(error.localizedDescription)") print("❌ 素材提交失败: \(error.localizedDescription)")