diff --git a/wake.xcodeproj/project.pbxproj b/wake.xcodeproj/project.pbxproj
index 40f2a91..c302b59 100644
--- a/wake.xcodeproj/project.pbxproj
+++ b/wake.xcodeproj/project.pbxproj
@@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
AB6693CA2E65C94400BCAAC1 /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = AB6693C92E65C94400BCAAC1 /* SVGKit */; };
AB6693CC2E65C94400BCAAC1 /* SVGKitSwift in Frameworks */ = {isa = PBXBuildFile; productRef = AB6693CB2E65C94400BCAAC1 /* SVGKitSwift */; };
+ AB6695272E67015600BCAAC1 /* WaterfallGrid in Frameworks */ = {isa = PBXBuildFile; productRef = AB6695262E67015600BCAAC1 /* WaterfallGrid */; };
AB8773632E4E04400071CB53 /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = AB8773622E4E040E0071CB53 /* .gitignore */; };
ABC150C12E5DB39A00A1F970 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = ABC150C02E5DB39A00A1F970 /* Lottie */; };
ABE8998E2E533A7100CD7BA6 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = ABE8998D2E533A7100CD7BA6 /* Alamofire */; };
@@ -62,6 +63,7 @@
AB6693CC2E65C94400BCAAC1 /* SVGKitSwift in Frameworks */,
AB6693CA2E65C94400BCAAC1 /* SVGKit in Frameworks */,
ABE8998E2E533A7100CD7BA6 /* Alamofire in Frameworks */,
+ AB6695272E67015600BCAAC1 /* WaterfallGrid in Frameworks */,
ABC150C12E5DB39A00A1F970 /* Lottie in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -120,6 +122,7 @@
ABC150C02E5DB39A00A1F970 /* Lottie */,
AB6693C92E65C94400BCAAC1 /* SVGKit */,
AB6693CB2E65C94400BCAAC1 /* SVGKitSwift */,
+ AB6695262E67015600BCAAC1 /* WaterfallGrid */,
);
productName = wake;
productReference = ABB4E2082E4B75D900660198 /* wake.app */;
@@ -153,6 +156,7 @@
ABE8998C2E533A7100CD7BA6 /* XCRemoteSwiftPackageReference "Alamofire" */,
ABC150BF2E5DB39A00A1F970 /* XCRemoteSwiftPackageReference "lottie-spm" */,
AB6693C82E65C94400BCAAC1 /* XCRemoteSwiftPackageReference "SVGKit" */,
+ AB6695252E67015600BCAAC1 /* XCRemoteSwiftPackageReference "WaterfallGrid" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = ABB4E2092E4B75D900660198 /* Products */;
@@ -409,6 +413,14 @@
minimumVersion = 3.0.0;
};
};
+ AB6695252E67015600BCAAC1 /* XCRemoteSwiftPackageReference "WaterfallGrid" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/paololeonardi/WaterfallGrid.git";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 1.1.0;
+ };
+ };
ABC150BF2E5DB39A00A1F970 /* XCRemoteSwiftPackageReference "lottie-spm" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/airbnb/lottie-spm.git";
@@ -438,6 +450,11 @@
package = AB6693C82E65C94400BCAAC1 /* XCRemoteSwiftPackageReference "SVGKit" */;
productName = SVGKitSwift;
};
+ AB6695262E67015600BCAAC1 /* WaterfallGrid */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = AB6695252E67015600BCAAC1 /* XCRemoteSwiftPackageReference "WaterfallGrid" */;
+ productName = WaterfallGrid;
+ };
ABC150C02E5DB39A00A1F970 /* Lottie */ = {
isa = XCSwiftPackageProductDependency;
package = ABC150BF2E5DB39A00A1F970 /* XCRemoteSwiftPackageReference "lottie-spm" */;
diff --git a/wake.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/wake.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 7ce85c2..39ee57b 100644
--- a/wake.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/wake.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -1,5 +1,6 @@
{
"originHash" : "d4b9379b4bd658fe79a6ae528c96d3386427dfe9d23635a65dad6edf12af85ff",
+ "originHash" : "7ea295cc5e3eb8ef644b89ce2b47a7600994b67c8582ee354b643cd63250740d",
"pins" : [
{
"identity" : "alamofire",
@@ -45,6 +46,15 @@
"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
diff --git a/wake/Assets/Png/logo.png b/wake/Assets/Png/logo.png
new file mode 100644
index 0000000..fc79a01
Binary files /dev/null and b/wake/Assets/Png/logo.png differ
diff --git a/wake/Components/Buttons/ReturnButton.swift b/wake/Components/Buttons/ReturnButton.swift
index 2882b17..e91bb3a 100644
--- a/wake/Components/Buttons/ReturnButton.swift
+++ b/wake/Components/Buttons/ReturnButton.swift
@@ -20,6 +20,8 @@ struct ReturnButton: View {
.font(Typography.font(for: iconSize))
.fontWeight(.medium)
.foregroundColor(iconColor)
+ .padding(10) // 增加内边距来扩大点击区域
+ .contentShape(Rectangle()) // 使整个区域可点击
}
.buttonStyle(PlainButtonStyle())
}
diff --git a/wake/ContentView.swift b/wake/ContentView.swift
index cf44360..b6f24cb 100644
--- a/wake/ContentView.swift
+++ b/wake/ContentView.swift
@@ -182,6 +182,8 @@ struct BlindBoxView: View {
let mediaType: BlindBoxMediaType
@State private var showModal = false // 控制用户资料弹窗显示
@State private var showSettings = false // 控制设置页面显示
+ @State private var isMember = false // 是否是会员
+ @State private var memberDate = "" // 会员到期时间
@State private var showLogin = false
@State private var memberProfile: MemberProfile? = nil
@State private var blindCount: BlindCount? = nil
@@ -275,6 +277,8 @@ struct BlindBoxView: View {
switch result {
case .success(let response):
self.memberProfile = response.data
+ self.isMember = response.data.membershipLevel == "Pioneer"
+ self.memberDate = response.data.membershipEndAt ?? ""
print("✅ 成功获取会员信息:", response.data)
print("✅ 用户ID:", response.data.userInfo.userId)
case .failure(let error):
@@ -641,7 +645,10 @@ struct BlindBoxView: View {
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()
// // 测试质感页面入口
@@ -705,13 +712,9 @@ struct BlindBoxView: View {
ZStack {
// 1. 背景SVG
if !showScalingOverlay {
- SVGImage(svgName: "BlindBg")
- .frame(
- width: UIScreen.main.bounds.width * 1.8,
- height: UIScreen.main.bounds.height * 0.85
- )
- .position(x: UIScreen.main.bounds.width / 2,
- y: UIScreen.main.bounds.height * 0.325)
+ 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)
}
@@ -830,6 +833,7 @@ struct BlindBoxView: View {
.offset(x: -10, y: UIScreen.main.bounds.height * 0.2)
}
}
+ .padding()
.frame(
maxWidth: .infinity,
maxHeight: UIScreen.main.bounds.height * 0.65
@@ -897,10 +901,11 @@ struct BlindBoxView: View {
) {
UserProfileModal(
showModal: $showModal,
- showSettings: $showSettings
+ showSettings: $showSettings,
+ isMember: $isMember,
+ memberDate: $memberDate
)
}
- .shadow(color: .black.opacity(0.3), radius: 10, x: 5, y: 0)
.offset(x: showSettings ? UIScreen.main.bounds.width : 0)
.animation(.spring(response: 0.6, dampingFraction: 0.8), value: showSettings)
diff --git a/wake/Models/OrderInfo.swift b/wake/Models/OrderInfo.swift
new file mode 100644
index 0000000..7cbca93
--- /dev/null
+++ b/wake/Models/OrderInfo.swift
@@ -0,0 +1,101 @@
+import Foundation
+
+/// 订单信息模型
+struct OrderInfo: Codable, Identifiable {
+ let id: String
+ let userId: String
+ let totalAmount: Amount
+ let status: String
+ let items: [OrderItem]
+ let paymentInfo: PaymentInfo?
+ let createdAt: String
+ let updatedAt: String
+ let expiredAt: String
+
+ enum CodingKeys: String, CodingKey {
+ case id
+ case userId = "user_id"
+ case totalAmount = "total_amount"
+ case status
+ case items
+ case paymentInfo = "payment_info"
+ case createdAt = "created_at"
+ case updatedAt = "updated_at"
+ case expiredAt = "expired_at"
+ }
+}
+
+/// 支付信息模型
+struct PaymentInfo: Codable {
+ let id: String
+ let paymentMethod: String
+ let paymentStatus: String
+ let paymentAmount: Amount
+ let transactionId: String?
+ let thirdPartyTransactionId: String?
+ let paidAt: String?
+ let createdAt: String
+ let updatedAt: String
+
+ enum CodingKeys: String, CodingKey {
+ case id
+ case paymentMethod = "payment_method"
+ case paymentStatus = "payment_status"
+ case paymentAmount = "payment_amount"
+ case transactionId = "transaction_id"
+ case thirdPartyTransactionId = "third_party_transaction_id"
+ case paidAt = "paid_at"
+ case createdAt = "created_at"
+ case updatedAt = "updated_at"
+ }
+}
+
+/// 金额模型
+struct Amount: Codable {
+ let amount: String
+ let currency: String
+}
+
+/// 订单项模型
+struct OrderItem: Codable, Identifiable {
+ let id: String
+ let productId: Int
+ let productType: String
+ let productCode: String
+ let productName: String
+ let unitPrice: Amount
+ let discountAmount: Amount
+ let quantity: Int
+ let totalPrice: Amount
+
+ enum CodingKeys: String, CodingKey {
+ case id
+ case productId = "product_id"
+ case productType = "product_type"
+ case productCode = "product_code"
+ case productName = "product_name"
+ case unitPrice = "unit_price"
+ case discountAmount = "discount_amount"
+ case quantity
+ case totalPrice = "total_price"
+ }
+}
+
+/// 订单状态
+enum OrderStatus: Int, Codable {
+ case pending = 0 // 待支付
+ case paid = 1 // 已支付
+ case completed = 2 // 已完成
+ case cancelled = 3 // 已取消
+ case refunded = 4 // 已退款
+
+ var description: String {
+ switch self {
+ case .pending: return "待支付"
+ case .paid: return "已支付"
+ case .completed: return "已完成"
+ case .cancelled: return "已取消"
+ case .refunded: return "已退款"
+ }
+ }
+}
diff --git a/wake/Utils/IAPManager.swift b/wake/Utils/IAPManager.swift
index 27fb2c9..e9b8ae2 100644
--- a/wake/Utils/IAPManager.swift
+++ b/wake/Utils/IAPManager.swift
@@ -34,13 +34,12 @@ final class IAPManager: ObservableObject {
}
// Trigger App Store purchase sheet
- func purchasePioneer() async {
- guard !isPurchasing else { return }
+ func purchasePioneer() async throws -> String {
+ guard !isPurchasing else { throw NSError(domain: "IAPError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Purchase already in progress"]) }
guard let product = pioneerProduct else {
- // Surface an actionable error so the UI can inform the user
- self.errorMessage = "Subscription product unavailable. Please try again later."
- return
+ throw NSError(domain: "IAPError", code: -2, userInfo: [NSLocalizedDescriptionKey: "Subscription product unavailable"])
}
+
isPurchasing = true
defer { isPurchasing = false }
@@ -50,21 +49,26 @@ final class IAPManager: ObservableObject {
case .success(let verification):
switch verification {
case .unverified(_, let error):
- self.errorMessage = "Purchase unverified: \(error.localizedDescription)"
- case .verified(let transaction):
- // Update entitlement for the purchased product
+ throw error
+ case .verified(let transaction):
+ print("🎉 订阅成功!", transaction)
+ print("🔄 交易验证通过 - ID: \(transaction.id), 原始ID: \(transaction.originalID), 产品ID: \(transaction.productID)")
updateEntitlement(from: transaction)
+ let transactionID = String(transaction.id)
+ print("📝 使用交易ID: \(transactionID)")
await transaction.finish()
+ return transactionID
}
case .userCancelled:
- break
+ throw NSError(domain: "IAPError", code: -3, userInfo: [NSLocalizedDescriptionKey: "Purchase was cancelled"])
case .pending:
- break
+ throw NSError(domain: "IAPError", code: -4, userInfo: [NSLocalizedDescriptionKey: "Purchase is pending approval"])
@unknown default:
- break
+ throw NSError(domain: "IAPError", code: -5, userInfo: [NSLocalizedDescriptionKey: "Unknown purchase result"])
}
} catch {
self.errorMessage = "Purchase failed: \(error.localizedDescription)"
+ throw error
}
}
diff --git a/wake/Utils/SVGImage.swift b/wake/Utils/SVGImage.swift
index 78ce50d..324bc52 100644
--- a/wake/Utils/SVGImage.swift
+++ b/wake/Utils/SVGImage.swift
@@ -1,92 +1,148 @@
import SwiftUI
-import WebKit
+import SVGKit
struct SVGImage: UIViewRepresentable {
let svgName: String
- var shouldFill: Bool = false
+ var contentMode: ContentMode = .fit
+ var tintColor: Color?
- func makeUIView(context: Context) -> WKWebView {
- let webView = WKWebView()
- webView.isOpaque = false
- webView.backgroundColor = .clear
- webView.scrollView.isScrollEnabled = false
- webView.scrollView.contentInsetAdjustmentBehavior = .never
-
- guard let path = Bundle.main.path(forResource: svgName, ofType: "svg") else {
- print("❌ 无法找到 SVG 文件: \(svgName).svg")
- return webView
- }
-
- let fileURL = URL(fileURLWithPath: path)
-
- let svgStyle = shouldFill ? """
- width: 100%;
- height: 100%;
- object-fit: cover;
- """ : """
- max-width: 100%;
- max-height: 100%;
- object-fit: contain;
- """
-
- let htmlString = """
-
-
-
-
-
-
-
-
-
-
-
-
- """
-
- webView.loadHTMLString(htmlString, baseURL: fileURL.deletingLastPathComponent())
- return webView
+ private var svgPath: String {
+ return svgName
}
- func updateUIView(_ uiView: WKWebView, context: Context) {}
+ private func createImageView() -> SVGKFastImageView {
+ let emptySVGString = """
+
+ """
+
+ if let data = emptySVGString.data(using: .utf8),
+ let svgImage = SVGKImage(data: data) {
+ let imageView = SVGKFastImageView(svgkImage: svgImage) ?? SVGKFastImageView()
+ imageView.contentMode = .scaleAspectFit
+ imageView.backgroundColor = .clear
+ return imageView
+ }
+
+ let fallbackView = SVGKFastImageView()
+ fallbackView.backgroundColor = .clear
+ return fallbackView
+ }
- func sizeThatFits(_ proposal: ProposedViewSize, uiView: WKWebView, context: Context) -> CGSize? {
+ func makeUIView(context: Context) -> SVGKFastImageView {
+ print("🔄 开始加载SVG: \(svgName)")
+ let imageView = createImageView()
+ loadSVG(into: imageView)
+ configureView(imageView)
+ return imageView
+ }
+
+ private func loadSVG(into imageView: SVGKFastImageView) {
+ guard let path = Bundle.main.path(forResource: svgPath, ofType: "svg") else {
+ print("⚠️ 在main bundle中找不到文件: \(svgPath).svg")
+ return
+ }
+
+ let url = URL(fileURLWithPath: path)
+ guard let svgImage = SVGKImage(contentsOf: url) else {
+ print("❌ 无法从URL创建SVG: \(path)")
+ return
+ }
+
+ // 设置SVG的尺寸为容器大小
+ let containerSize = imageView.bounds.size
+ if containerSize != .zero {
+ svgImage.size = containerSize
+ } else {
+ // 如果容器大小未知,设置一个默认大小
+ svgImage.size = CGSize(width: 100, height: 100)
+ }
+
+ print("✅ 成功加载SVG: \(svgName), 尺寸: \(svgImage.size)")
+
+ DispatchQueue.main.async {
+ imageView.image = svgImage
+ imageView.setNeedsLayout()
+ imageView.layoutIfNeeded()
+ }
+ }
+
+ private func configureView(_ imageView: SVGKFastImageView) {
+ imageView.contentMode = contentMode == .fit ? .scaleAspectFit : .scaleAspectFill
+ imageView.clipsToBounds = true
+ imageView.translatesAutoresizingMaskIntoConstraints = false
+ imageView.backgroundColor = .clear
+
+ // 确保图片视图可以正确缩放
+ imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+
+ if let tintColor = tintColor?.uiColor {
+ imageView.tintColor = tintColor
+ DispatchQueue.main.async {
+ self.applyTintColor(tintColor, to: imageView.layer)
+ }
+ }
+ }
+
+ private func applyTintColor(_ color: UIColor, to layer: CALayer) {
+ if let shapeLayer = layer as? CAShapeLayer {
+ shapeLayer.fillColor = color.cgColor
+ }
+
+ layer.sublayers?.forEach { sublayer in
+ applyTintColor(color, to: sublayer)
+ }
+ }
+
+ func updateUIView(_ uiView: SVGKFastImageView, context: Context) {
+ loadSVG(into: uiView)
+
+ if let tintColor = tintColor?.uiColor {
+ uiView.tintColor = tintColor
+ DispatchQueue.main.async {
+ self.applyTintColor(tintColor, to: uiView.layer)
+ }
+ }
+
+ uiView.contentMode = contentMode == .fit ? .scaleAspectFit : .scaleAspectFill
+ }
+
+ func sizeThatFits(_ proposal: ProposedViewSize, uiView: SVGKFastImageView, context: Context) -> CGSize? {
return nil
}
}
+// MARK: - ContentMode
+extension SVGImage {
+ enum ContentMode {
+ case fit // 保持宽高比,适应容器
+ case fill // 保持宽高比,填充容器(可能被裁剪)
+ }
+}
+
// MARK: - Preview
#Preview {
- VStack {
- Text("Filled SVG")
- SVGImage(svgName: "YourSVGName", shouldFill: true)
- .frame(width: 200, height: 100)
+ VStack(spacing: 20) {
+ Text("IP SVG")
+ SVGImage(svgName: "IP")
+ .frame(width: 100, height: 100)
.background(Color.gray.opacity(0.2))
+ .border(Color.red, width: 1)
- Text("Intrinsic Size SVG")
- SVGImage(svgName: "YourSVGName", shouldFill: false)
- .frame(width: 200, height: 100)
+ Text("Pioneer SVG")
+ SVGImage(svgName: "Pioneer", contentMode: .fill)
+ .frame(width: 100, height: 50)
.background(Color.gray.opacity(0.2))
+ .border(Color.blue, width: 1)
+ .clipped()
}
.padding()
+}
+
+// MARK: - Color Extension
+private extension Color {
+ var uiColor: UIColor {
+ return UIColor(self)
+ }
}
\ No newline at end of file
diff --git a/wake/Utils/SVGImageHtml.swift b/wake/Utils/SVGImageHtml.swift
new file mode 100644
index 0000000..1798b84
--- /dev/null
+++ b/wake/Utils/SVGImageHtml.swift
@@ -0,0 +1,71 @@
+import SwiftUI
+import WebKit
+
+struct SVGImageHtml: UIViewRepresentable {
+ let svgName: String
+
+ func makeUIView(context: Context) -> WKWebView {
+ let webView = WKWebView()
+ webView.isOpaque = false
+ webView.backgroundColor = .clear
+ webView.scrollView.isScrollEnabled = false
+ webView.scrollView.contentInsetAdjustmentBehavior = .never
+
+ // 1. Get the URL for the SVG file
+ guard let path = Bundle.main.path(forResource: svgName, ofType: "svg") else {
+ print("❌ Cannot find SVG file: \(svgName).svg in bundle")
+ return webView
+ }
+
+ let fileURL = URL(fileURLWithPath: path)
+
+ do {
+ // 2. Read the SVG content directly
+ let svgString = try String(contentsOf: fileURL, encoding: .utf8)
+
+ // 3. Create HTML with inline SVG for better reliability
+ let htmlString = """
+
+
+
+
+
+
+
+ \(svgString)
+
+
+ """
+
+ // 4. Load the HTML with base URL as the main bundle's resource path
+ if let resourcePath = Bundle.main.resourceURL {
+ webView.loadHTMLString(htmlString, baseURL: resourcePath)
+ } else {
+ webView.loadHTMLString(htmlString, baseURL: nil)
+ }
+
+ } catch {
+ print("❌ Error loading SVG file: \(error.localizedDescription)")
+ }
+
+ return webView
+ }
+
+ func updateUIView(_ uiView: WKWebView, context: Context) {}
+}
\ No newline at end of file
diff --git a/wake/View/Blind/BlindOutCome.swift b/wake/View/Blind/BlindOutCome.swift
index f4db1a5..cdd4bc9 100644
--- a/wake/View/Blind/BlindOutCome.swift
+++ b/wake/View/Blind/BlindOutCome.swift
@@ -2,7 +2,6 @@ import SwiftUI
import AVKit
import os.log
-/// A view that displays either an image or a video with fullscreen support
struct BlindOutcomeView: View {
let media: MediaType
let time: String?
@@ -12,6 +11,7 @@ struct BlindOutcomeView: View {
@State private var isPlaying = false
@State private var showControls = true
@State private var showIPListModal = false
+ @State private var player: AVPlayer?
init(media: MediaType, time: String? = nil, description: String? = nil) {
self.media = media
@@ -28,7 +28,6 @@ struct BlindOutcomeView: View {
// 自定义导航栏
HStack {
Button(action: {
- // 返回上一级
presentationMode.wrappedValue.dismiss()
}) {
HStack(spacing: 4) {
@@ -47,7 +46,6 @@ struct BlindOutcomeView: View {
Spacer()
- // 占位,保持标题居中
HStack(spacing: 4) {
Image(systemName: "chevron.left")
.opacity(0)
@@ -56,7 +54,7 @@ struct BlindOutcomeView: View {
}
.padding(.vertical, 12)
.background(Color.themeTextWhiteSecondary)
- .zIndex(1) // 确保导航栏在其他内容之上
+ .zIndex(1)
Spacer()
.frame(height: 30)
@@ -65,7 +63,6 @@ struct BlindOutcomeView: View {
GeometryReader { geometry in
VStack(spacing: 16) {
ZStack {
- // 添加白色背景
RoundedRectangle(cornerRadius: 12)
.fill(Color.white)
.shadow(color: Color.black.opacity(0.1), radius: 8, x: 0, y: 2)
@@ -86,48 +83,41 @@ struct BlindOutcomeView: View {
}
case .video(let url, _):
- VideoPlayerView(url: url, isPlaying: $isPlaying)
+ VideoPlayerView(url: url, isPlaying: $isPlaying, player: $player)
.frame(width: UIScreen.main.bounds.width - 40)
.background(Color.clear)
.cornerRadius(10)
.clipped()
.onAppear {
- // Auto-play the video when it appears
isPlaying = true
}
+ .onDisappear {
+ isPlaying = false
+ player?.pause()
+ }
.onTapGesture {
withAnimation {
showControls.toggle()
}
}
.fullScreenCover(isPresented: $isFullscreen) {
- FullscreenMediaView(media: media, isPresented: $isFullscreen, isPlaying: $isPlaying, player: nil)
+ FullscreenMediaView(media: media, isPresented: $isFullscreen, isPlaying: $isPlaying, player: player)
}
- .overlay(
- showControls ? VideoControls(
- isPlaying: $isPlaying,
- onClose: { isFullscreen = false }
- ) : nil
- )
}
- VStack(alignment: .leading, spacing: 8) {
-
- if let description = description, !description.isEmpty {
- VStack(alignment: .leading, spacing: 2) {
- Text("Description")
- .font(Typography.font(for: .body, family: .quicksandBold))
- .foregroundColor(.themeTextMessageMain)
- Text(description)
- .font(.system(size: 12))
- .foregroundColor(Color.themeTextMessageMain)
- .fixedSize(horizontal: false, vertical: true)
- }
- .padding(.horizontal, 12)
- .padding(.bottom, 12)
+ if let description = description, !description.isEmpty {
+ VStack(alignment: .leading, spacing: 2) {
+ Text("Description")
+ .font(Typography.font(for: .body, family: .quicksandBold))
+ .foregroundColor(.themeTextMessageMain)
+ Text(description)
+ .font(.system(size: 12))
+ .foregroundColor(Color.themeTextMessageMain)
+ .fixedSize(horizontal: false, vertical: true)
}
+ .padding(.horizontal, 12)
+ .padding(.bottom, 12)
}
- .frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.top, 8)
}
@@ -136,12 +126,13 @@ struct BlindOutcomeView: View {
.padding(.bottom, 20)
}
.padding(.horizontal)
+
Spacer()
+
// Button at bottom
VStack {
Spacer()
Button(action: {
- // 如果携带的类型是video显示弹窗
if case .video = media {
withAnimation {
showIPListModal = true
@@ -162,22 +153,20 @@ struct BlindOutcomeView: View {
}
.padding(.bottom, 20)
}
- .onDisappear {
- // Clean up video player when view disappears
- if case .video = media {
- isPlaying = false
- }
- }
}
- .navigationBarHidden(true) // 确保隐藏系统导航栏
- .navigationBarBackButtonHidden(true) // 确保隐藏系统返回按钮
+ .navigationBarHidden(true)
+ .navigationBarBackButtonHidden(true)
.statusBar(hidden: isFullscreen)
}
- .navigationViewStyle(StackNavigationViewStyle()) // 确保在iPad上也能正确显示
- .navigationBarHidden(true) // 额外确保隐藏导航栏
+ .navigationViewStyle(StackNavigationViewStyle())
+ .navigationBarHidden(true)
.overlay(
JoinModal(isPresented: $showIPListModal)
)
+ .onDisappear {
+ player?.pause()
+ player = nil
+ }
}
}
@@ -187,22 +176,19 @@ private struct FullscreenMediaView: View {
@Binding var isPresented: Bool
@Binding var isPlaying: Bool
@State private var showControls = true
- @State private var player: AVPlayer?
+ private let player: AVPlayer?
init(media: MediaType, isPresented: Binding, isPlaying: Binding, player: AVPlayer?) {
self.media = media
self._isPresented = isPresented
self._isPlaying = isPlaying
- if let player = player {
- self._player = State(initialValue: player)
- }
+ self.player = player
}
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
- // Media content
ZStack {
switch media {
case .image(let uiImage):
@@ -216,25 +202,21 @@ private struct FullscreenMediaView: View {
}
}
- case .video(let url, _):
- VideoPlayerView(url: url, isPlaying: $isPlaying)
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- .onTapGesture {
- withAnimation {
- showControls.toggle()
+ case .video(_, _):
+ if let player = player {
+ CustomVideoPlayer(player: player)
+ .onAppear {
+ player.play()
+ isPlaying = true
}
- }
- .overlay(
- showControls ? VideoControls(
- isPlaying: $isPlaying,
- onClose: { isPresented = false }
- ) : nil
- )
+ .onDisappear {
+ player.pause()
+ isPlaying = false
+ }
+ }
}
}
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- // Close button (always visible)
VStack {
HStack {
Button(action: { isPresented = false }) {
@@ -251,42 +233,22 @@ private struct FullscreenMediaView: View {
Spacer()
}
}
- .onAppear {
- if case .video = media {
- if isPlaying {
- // player?.play()
- }
- }
- }
.onDisappear {
- if case .video = media {
- // player?.pause()
- // player?.replaceCurrentItem(with: nil)
- // player = nil
- }
+ player?.pause()
}
}
}
-// MARK: - Video Controls
-private struct VideoControls: View {
- @Binding var isPlaying: Bool
- let onClose: () -> Void
-
- var body: some View {
- // Empty view - no controls shown
- EmptyView()
- }
-}
-
-// MARK: - Video Player with Dynamic Aspect Ratio
+// MARK: - Video Player View
struct VideoPlayerView: UIViewRepresentable {
let url: URL
@Binding var isPlaying: Bool
+ @Binding var player: AVPlayer?
func makeUIView(context: Context) -> PlayerView {
let view = PlayerView()
- view.setupPlayer(url: url)
+ let player = view.setupPlayer(url: url)
+ self.player = player
return view
}
@@ -299,39 +261,56 @@ struct VideoPlayerView: UIViewRepresentable {
}
}
+// MARK: - Custom Video Player
+@available(iOS 14.0, *)
+struct CustomVideoPlayer: UIViewControllerRepresentable {
+ let player: AVPlayer
+
+ func makeUIViewController(context: Context) -> AVPlayerViewController {
+ let controller = AVPlayerViewController()
+ controller.player = player
+ controller.showsPlaybackControls = false
+ controller.videoGravity = .resizeAspect
+ return controller
+ }
+
+ func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
+ uiViewController.player = player
+ }
+}
+
+// MARK: - Player View
class PlayerView: UIView {
private var player: AVPlayer?
private var playerLayer: AVPlayerLayer?
private var playerItem: AVPlayerItem?
private var playerItemObserver: NSKeyValueObservation?
- func setupPlayer(url: URL) {
- // Clean up existing resources
+ @discardableResult
+ func setupPlayer(url: URL) -> AVPlayer {
cleanup()
- // Create new player
let asset = AVAsset(url: url)
let playerItem = AVPlayerItem(asset: asset)
self.playerItem = playerItem
player = AVPlayer(playerItem: playerItem)
- // Setup player layer
let playerLayer = AVPlayerLayer(player: player)
playerLayer.videoGravity = .resizeAspect
layer.addSublayer(playerLayer)
self.playerLayer = playerLayer
- // Layout
playerLayer.frame = bounds
- // Add observer for video end
NotificationCenter.default.addObserver(
self,
selector: #selector(playerItemDidReachEnd),
name: .AVPlayerItemDidPlayToEndTime,
object: playerItem
)
+
+ return player!
}
func play() {
@@ -343,21 +322,17 @@ class PlayerView: UIView {
}
private func cleanup() {
- // Remove observers
if let playerItem = playerItem {
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: playerItem)
}
- // Pause and clean up player
player?.pause()
player?.replaceCurrentItem(with: nil)
player = nil
- // Remove player layer
playerLayer?.removeFromSuperlayer()
playerLayer = nil
- // Release player item
playerItem?.cancelPendingSeeks()
playerItem?.asset.cancelLoading()
playerItem = nil
@@ -376,25 +351,4 @@ class PlayerView: UIView {
deinit {
cleanup()
}
-}
-
-// MARK: - Preview
-struct BlindOutcomeView_Previews: PreviewProvider {
- static var previews: some View {
- // Preview with image and details
- BlindOutcomeView(
- media: .image(UIImage(systemName: "photo")!),
- time: "2:30",
- description: "This is a sample description for the preview. It shows how the text will wrap and display below the media content."
- )
-
- // Preview with video and details
- if let url = URL(string: "https://example.com/sample.mp4") {
- BlindOutcomeView(
- media: .video(url, nil),
- time: "1:45",
- description: "Video content with time and description"
- )
- }
- }
-}
+}
\ No newline at end of file
diff --git a/wake/View/Blind/JoinModal.swift b/wake/View/Blind/JoinModal.swift
index 222faef..2a21e5c 100644
--- a/wake/View/Blind/JoinModal.swift
+++ b/wake/View/Blind/JoinModal.swift
@@ -22,7 +22,7 @@ struct JoinModal: View {
// IP Image peeking from top
HStack {
// Make sure you have an image named "IP" in your assets
- SVGImage(svgName: "IP1")
+ SVGImageHtml(svgName: "IP1")
.frame(width: 116, height: 65)
.offset(x: 30)
Spacer()
diff --git a/wake/View/Components/SheetModal.swift b/wake/View/Components/SheetModal.swift
index d464001..c69e0b8 100644
--- a/wake/View/Components/SheetModal.swift
+++ b/wake/View/Components/SheetModal.swift
@@ -8,7 +8,7 @@ struct SlideInModal: View {
// 动画配置 - 更慢的动画
private let animation = Animation.spring(
response: 0.8, // 增加响应时间使动画更慢
- dampingFraction: 0.6, // 减少阻尼系数使弹跳更明显
+ dampingFraction: 1, // 减少阻尼系数使弹跳更明显
blendDuration: 0.8 // 增加混合时间使过渡更平滑
)
@@ -28,21 +28,35 @@ struct SlideInModal: View {
}
}
- // 弹窗内容
- VStack(spacing: 0) {
- // 顶部安全区域占位
- Color.clear
- .frame(height: UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0)
-
- // 内容区域
- content()
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- .padding(.bottom, UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0)
+ // 添加一个额外的容器来承载阴影
+ ZStack(alignment: .leading) {
+ // 弹窗内容
+ VStack(spacing: 0) {
+ // 顶部安全区域占位
+ Color.clear
+ .frame(height: UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0)
+
+ // 内容区域
+ content()
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .padding(.bottom, UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0)
+ }
+ .frame(width: UIScreen.main.bounds.width * 0.8)
+ .frame(maxHeight: .infinity)
+ .background(Color(.systemBackground))
+ .edgesIgnoringSafeArea(.vertical)
}
- .frame(width: UIScreen.main.bounds.width * 0.8)
- .frame(maxHeight: .infinity)
- .background(Color(.systemBackground))
- .edgesIgnoringSafeArea(.vertical)
+ // 在这里应用阴影
+ .background(
+ RoundedRectangle(cornerRadius: 0)
+ .fill(Color(.systemBackground))
+ .shadow(
+ color: .black.opacity(0.3),
+ radius: 10,
+ x: 5,
+ y: 0
+ )
+ )
.offset(x: isPresented ? 0 : -UIScreen.main.bounds.width)
.zIndex(2)
.transition(.move(edge: .leading))
diff --git a/wake/View/Components/Upload/ImageUploaderGetID.swift b/wake/View/Components/Upload/ImageUploaderGetID.swift
index 764a55f..dff7dbd 100644
--- a/wake/View/Components/Upload/ImageUploaderGetID.swift
+++ b/wake/View/Components/Upload/ImageUploaderGetID.swift
@@ -305,8 +305,8 @@ public class ImageUploaderGetID: ObservableObject {
}
print("✅ 成功获取上传URL")
- print(" - 文件ID: \(fileId)")
- print(" - 上传URL: \(uploadURLString)")
+ print(" ❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️ - 文件ID: \(fileId)")
+ print(" - 上传URL: \(uploadURLString)")
completion(.success((fileId: fileId, uploadURL: uploadURL)))
} catch {
diff --git a/wake/View/Components/UserProfileModal.swift b/wake/View/Components/UserProfileModal.swift
index c2345dd..9c5a23c 100644
--- a/wake/View/Components/UserProfileModal.swift
+++ b/wake/View/Components/UserProfileModal.swift
@@ -25,10 +25,20 @@ struct APIResponse: Codable {
struct UserProfileModal: View {
@Binding var showModal: Bool
@Binding var showSettings: Bool
+ @Binding var isMember: Bool
+ @Binding var memberDate: String
@State private var userProfile: UserProfile?
@State private var isLoading = false
@State private var errorMessage: String?
@State private var isCopied = false
+ @State private var isContentReady = false
+
+ init(showModal: Binding, showSettings: Binding, isMember: Binding, memberDate: Binding) {
+ self._showModal = showModal
+ self._showSettings = showSettings
+ self._isMember = isMember
+ self._memberDate = memberDate
+ }
var body: some View {
VStack(spacing: 20) {
@@ -42,10 +52,35 @@ struct UserProfileModal: View {
Text(error)
.foregroundColor(.red)
.padding()
+ } else if isContentReady, let userProfile = userProfile {
+ userProfileView(userProfile: userProfile)
+ .opacity(isContentReady ? 1 : 0)
+ .animation(.easeInOut(duration: 0.3), value: isContentReady)
+ } else {
+ // Empty view with same dimensions to prevent layout shifts
+ Color.clear
+ .frame(height: UIScreen.main.bounds.height * 0.7)
}
-
+ }
+ .frame(width: UIScreen.main.bounds.width * 0.8)
+ .background(Color.themeTextWhiteSecondary)
+ .edgesIgnoringSafeArea(.all)
+ .onAppear {
+ fetchUserInfo()
+ // Delay content appearance to sync with modal animation
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
+ withAnimation {
+ isContentReady = true
+ }
+ }
+ }
+ }
+
+ @ViewBuilder
+ private func userProfileView(userProfile: UserProfile) -> some View {
+ VStack(spacing: 20) {
HStack(alignment: .center, spacing: 16) {
- if let avatarUrl = userProfile?.avatarUrl, !avatarUrl.isEmpty, let url = URL(string: avatarUrl) {
+ if let avatarUrl = userProfile.avatarUrl, !avatarUrl.isEmpty, let url = URL(string: avatarUrl) {
AsyncImage(url: url) { phase in
switch phase {
case .success(let image):
@@ -62,6 +97,9 @@ struct UserProfileModal: View {
.foregroundColor(.blue)
}
}
+ .onTapGesture {
+ Router.shared.navigate(to: .userInfo)
+ }
} else {
Image(systemName: "person.circle.fill")
.resizable()
@@ -71,12 +109,12 @@ struct UserProfileModal: View {
}
VStack(alignment: .leading, spacing: 10) {
- Text(userProfile?.nickname ?? "Name")
+ Text(userProfile.nickname)
.font(Typography.font(for: .body))
.fontWeight(.bold)
.foregroundColor(.themeTextMessageMain)
HStack(spacing: 4) {
- Text("ID: \(userProfile?.userId ?? "")")
+ Text("ID: \(userProfile.userId)")
.font(.system(size: 14))
.foregroundColor(.themeTextMessageMain)
.lineLimit(1)
@@ -84,21 +122,7 @@ struct UserProfileModal: View {
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: 120)
- Button(action: {
- print("Copy ID button tapped")
- UIPasteboard.general.string = userProfile?.userId
- print("Copied to clipboard:", userProfile?.userId ?? "nil")
- withAnimation {
- isCopied = true
- // Reset after 2 seconds
- DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
- withAnimation {
- isCopied = false
- print("Reset copy button state")
- }
- }
- }
- }) {
+ Button(action: copyUserId) {
if isCopied {
Image(systemName: "checkmark")
.foregroundColor(.themePrimary)
@@ -109,8 +133,8 @@ struct UserProfileModal: View {
.font(.system(size: 12))
.foregroundColor(.themeTextMessageMain)
.animation(.easeInOut, value: isCopied)
- .contentShape(Rectangle()) // Make the entire button area tappable
- .frame(width: 24, height: 24) // Ensure minimum touch target size
+ .contentShape(Rectangle())
+ .frame(width: 24, height: 24)
}
}
@@ -124,43 +148,8 @@ struct UserProfileModal: View {
)
.padding(.horizontal)
- Button(action: {
- Router.shared.navigate(to: .subscribe)
- }) {
- ZStack(alignment: .center) {
- // SVG背景 - 确保铺满整个区域
- SVGImage(svgName: "Pioneer")
- .scaledToFill()
- .frame(maxWidth: .infinity, maxHeight: .infinity)
-
- // 内容区域
- VStack(alignment: .leading, spacing: 6) {
- Text("Pioneer")
- .font(Typography.font(for: .title3))
- .fontWeight(.bold)
- .foregroundColor(.themeTextMessageMain)
- .padding(.top, 6)
-
- Text("Expires on :")
- .font(.system(size: 12))
- .foregroundColor(.themeTextMessageMain)
- .padding(.top, 2)
-
- Text("March 15, 2025")
- .font(.system(size: 12))
- .fontWeight(.bold)
- .foregroundColor(.themeTextMessageMain)
- }
- .frame(maxWidth: .infinity, alignment: .leading)
- .padding(.leading, 32)
- // 可以添加内边距使内容不紧贴边缘
- .padding(.vertical, 12)
- }
- .frame(height: 112)
- .frame(maxWidth: .infinity)
- .cornerRadius(16)
- .clipped() // 确保内容不会超出边界
- }
+ // 当前订阅状态卡片
+ currentSubscriptionCard
VStack(spacing: 12) {
// upload
@@ -208,26 +197,26 @@ struct UserProfileModal: View {
.buttonStyle(PlainButtonStyle())
// Box
- Button(action: {
- Router.shared.navigate(to: .mediaUpload)
- }) {
- HStack(spacing: 16) {
- SVGImage(svgName: "Box")
- .foregroundColor(.orange)
- .frame(width: 20, height: 20)
+ // Button(action: {
+ // Router.shared.navigate(to: .mediaUpload)
+ // }) {
+ // HStack(spacing: 16) {
+ // SVGImage(svgName: "Box")
+ // .foregroundColor(.orange)
+ // .frame(width: 20, height: 20)
- Text("My Blind Box")
- .font(Typography.font(for: .body))
- .fontWeight(.bold)
- .foregroundColor(.themeTextMessageMain)
+ // Text("My Blind Box")
+ // .font(Typography.font(for: .body))
+ // .fontWeight(.bold)
+ // .foregroundColor(.themeTextMessageMain)
- Spacer()
- }
- .padding()
- .cornerRadius(10)
- .contentShape(Rectangle())
- }
- .buttonStyle(PlainButtonStyle())
+ // Spacer()
+ // }
+ // .padding()
+ // .cornerRadius(10)
+ // .contentShape(Rectangle())
+ // }
+ // .buttonStyle(PlainButtonStyle())
// setting
Button(action: {
@@ -262,14 +251,44 @@ struct UserProfileModal: View {
.padding(.horizontal)
Spacer()
}
- .frame(width: UIScreen.main.bounds.width * 0.8)
- .background(Color.themeTextWhiteSecondary)
- .edgesIgnoringSafeArea(.all)
- .onAppear {
- fetchUserInfo()
- }
}
+ private func copyUserId() {
+ UIPasteboard.general.string = userProfile?.userId
+ withAnimation {
+ isCopied = true
+ DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
+ withAnimation {
+ isCopied = false
+ }
+ }
+ }
+ }
+
+ // MARK: - 当前订阅状态卡片
+ private var currentSubscriptionCard: some View {
+ let status: SubscriptionStatus = {
+ if isMember {
+ let dateFormatter = ISO8601DateFormatter()
+ dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
+ let expiryDate = dateFormatter.date(from: memberDate) ?? Date()
+ return .pioneer(expiryDate: expiryDate)
+ } else {
+ return .free
+ }
+ }()
+
+ return SubscriptionStatusBar(
+ status: status,
+ height: 112,
+ onSubscribeTap: {
+ // 跳转到订阅页面
+ Router.shared.navigate(to: .subscribe)
+ }
+ )
+ .padding(.horizontal, Theme.Spacing.xl)
+ }
+
private func fetchUserInfo() {
isLoading = true
errorMessage = nil
@@ -295,5 +314,5 @@ struct UserProfileModal: View {
}
#Preview {
- UserProfileModal(showModal: .constant(true), showSettings: .constant(false))
+ UserProfileModal(showModal: .constant(true), showSettings: .constant(false), isMember: .constant(true), memberDate: .constant(""))
}
diff --git a/wake/View/Credits/CreditsInfoCard.swift b/wake/View/Credits/CreditsInfoCard.swift
index c6654d2..509625b 100644
--- a/wake/View/Credits/CreditsInfoCard.swift
+++ b/wake/View/Credits/CreditsInfoCard.swift
@@ -32,8 +32,8 @@ struct CreditsInfoCard: View {
mainCreditsSection
}
.buttonStyle(PlainButtonStyle())
- .background(Theme.Colors.primaryLight)
- .cornerRadius(Theme.CornerRadius.extraLarge)
+ .background(Color.themeTextWhite)
+ .cornerRadius(Theme.CornerRadius.round)
// .shadow(color: Theme.Shadows.small, radius: Theme.Shadows.cardShadow.radius, x: Theme.Shadows.cardShadow.x, y: Theme.Shadows.cardShadow.y)
}
diff --git a/wake/View/Memories/MemoriesView.swift b/wake/View/Memories/MemoriesView.swift
index 194de72..cb2bde6 100644
--- a/wake/View/Memories/MemoriesView.swift
+++ b/wake/View/Memories/MemoriesView.swift
@@ -1,5 +1,6 @@
import SwiftUI
import AVKit
+import WaterfallGrid
// MARK: - API Response Models
struct MaterialResponse: Decodable {
@@ -27,7 +28,7 @@ struct MemoryItem: Identifiable, Decodable {
var title: String { name ?? "Untitled" }
var subtitle: String { description ?? "" }
var mediaType: MemoryMediaType {
- let url = fileInfo.url.lowercased()
+ let url = fileInfo.fileName.lowercased()
if url.hasSuffix(".mp4") || url.hasSuffix(".mov") {
return .video(url: fileInfo.url, previewUrl: previewFileInfo.url)
} else {
@@ -98,30 +99,17 @@ struct MemoriesView: View {
ZStack {
Color.themeTextWhiteSecondary.ignoresSafeArea()
- Group {
- if isLoading {
- ProgressView()
- .scaleEffect(1.5)
- } else if let error = errorMessage {
- Text("Error: \(error)")
- .foregroundColor(.red)
- } else {
- ScrollView {
- LazyVGrid(columns: columns, spacing: 4) {
- ForEach(memories) { memory in
- MemoryCard(memory: memory)
- .padding(.horizontal, 2)
- .onTapGesture {
- withAnimation(.spring()) {
- selectedMemory = memory
- }
- }
+ ScrollView {
+ WaterfallGrid(memories) { memory in
+ MemoryCard(memory: memory)
+ .onTapGesture {
+ withAnimation(.spring()) {
+ selectedMemory = memory
}
}
- .padding(.top, 4)
- .padding(.horizontal, 4)
- }
}
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
}
}
}
@@ -172,7 +160,23 @@ struct FullScreenMediaView: View {
@State private var isVideoPlaying = false
@State private var showControls = true
@State private var controlsTimer: Timer? = nil
- @State private var player: AVPlayer? = 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 // Default to 16:9 if we can't determine the aspect ratio
+ isLoading = false
+ return
+ }
+
+ imageAspectRatio = width / height
+ isLoading = false
+ }
var body: some View {
ZStack {
@@ -182,46 +186,67 @@ struct FullScreenMediaView: View {
// Media content with back button overlay
ZStack {
// Media content
- switch memory.mediaType {
- case .image(let url):
- if let imageURL = URL(string: url) {
- AsyncImage(url: imageURL) { phase in
- switch phase {
- case .success(let image):
- image
- .resizable()
- .scaledToFill()
- .frame(width: UIScreen.main.bounds.width,
- height: UIScreen.main.bounds.height)
- .edgesIgnoringSafeArea(.all)
- case .failure(_):
- Image(systemName: "exclamationmark.triangle")
- .foregroundColor(.red)
- case .empty:
- ProgressView()
- @unknown default:
- EmptyView()
+ 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 url, let previewUrl):
- if let videoURL = URL(string: url) {
- VideoPlayer(player: player)
- .onAppear {
- self.player = AVPlayer(url: videoURL)
- self.player?.play()
- self.isVideoPlaying = true
- }
- .onDisappear {
- self.player?.pause()
- self.player = nil
- }
- .frame(width: UIScreen.main.bounds.width,
- height: UIScreen.main.bounds.height)
- .onTapGesture {
- togglePlayPause()
+
+ case .video(_, let previewUrl):
+ GeometryReader { geometry in
+ ZStack {
+ Color.clear
+ VideoPlayer(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)
+ }
}
}
@@ -231,13 +256,14 @@ struct FullScreenMediaView: View {
Button(action: {
withAnimation(.spring()) {
isPresented = nil
- pauseVideo()
+ // pauseVideo()
}
}) {
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)
@@ -247,28 +273,6 @@ struct FullScreenMediaView: View {
Spacer()
}
.zIndex(2) // Higher z-index to keep it above media content
-
- // Video controls overlay (only for video)
- if case .video = memory.mediaType, showControls {
- VStack {
- Spacer()
- // Play/pause button
- Button(action: {
- togglePlayPause()
- }) {
- Image(systemName: isVideoPlaying ? "pause.circle.fill" : "play.circle.fill")
- .font(.system(size: 70))
- .foregroundColor(.white.opacity(0.9))
- .shadow(radius: 3)
- }
- .padding(.bottom, 30)
- }
- .transition(.opacity)
- .onAppear {
- resetControlsTimer()
- }
- .zIndex(3) // Highest z-index for controls
- }
}
.ignoresSafeArea()
.statusBar(hidden: true)
@@ -276,120 +280,146 @@ struct FullScreenMediaView: View {
.onTapGesture {
if case .video = memory.mediaType {
withAnimation(.easeInOut) {
- showControls.toggle()
- }
- if showControls {
- resetControlsTimer()
+ // showControls.toggle()
}
+ // if showControls {
+ // resetControlsTimer()
+ // }
}
}
.statusBar(hidden: true)
.onAppear {
UIApplication.shared.isIdleTimerDisabled = true
if case .video = memory.mediaType {
- setupVideoPlayer()
+ // setupVideoPlayer()
}
}
.onDisappear {
UIApplication.shared.isIdleTimerDisabled = false
controlsTimer?.invalidate()
- pauseVideo()
+ // pauseVideo()
}
}
- private func setupVideoPlayer() {
- if case .video(let url, _) = memory.mediaType, let videoURL = URL(string: url) {
- self.player = AVPlayer(url: videoURL)
- self.player?.play()
- self.isVideoPlaying = true
-
- // Add observer for playback end
- NotificationCenter.default.addObserver(
- forName: .AVPlayerItemDidPlayToEndTime,
- object: self.player?.currentItem,
- queue: .main
- ) { _ in
- self.player?.seek(to: .zero) { _ in
- self.player?.play()
- }
- }
- }
- }
+ // private func setupVideoPlayer() {
+ // if case .video(let url, _) = memory.mediaType, let videoURL = URL(string: url) {
+ // // No need to set up player here
+ // }
+ // }
- private func togglePlayPause() {
- if isVideoPlaying {
- pauseVideo()
- } else {
- playVideo()
- }
- withAnimation {
- showControls = true
- }
- resetControlsTimer()
- }
+ // private func togglePlayPause() {
+ // if isVideoPlaying {
+ // pauseVideo()
+ // } else {
+ // playVideo()
+ // }
+ // withAnimation {
+ // showControls = true
+ // }
+ // resetControlsTimer()
+ // }
- private func playVideo() {
- player?.play()
- isVideoPlaying = true
- }
+ // private func playVideo() {
+ // // No need to play video here
+ // }
- private func pauseVideo() {
- player?.pause()
- isVideoPlaying = false
- }
+ // private func pauseVideo() {
+ // // No need to pause video here
+ // }
- private func resetControlsTimer() {
- controlsTimer?.invalidate()
- controlsTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in
- withAnimation(.easeInOut) {
- showControls = false
- }
- }
- }
+ // private func resetControlsTimer() {
+ // controlsTimer?.invalidate()
+ // controlsTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in
+ // withAnimation(.easeInOut) {
+ // showControls = false
+ // }
+ // }
+ // }
}
-struct VideoPlayer: UIViewRepresentable {
- let player: AVPlayer?
+struct VideoPlayer: UIViewControllerRepresentable {
+ let url: String
+ @Binding var isPlaying: Bool
- func makeUIView(context: Context) -> UIView {
- let view = UIView()
- if let player = player {
- let playerLayer = AVPlayerLayer(player: player)
- playerLayer.frame = UIScreen.main.bounds
- playerLayer.videoGravity = .resizeAspectFill
- view.layer.addSublayer(playerLayer)
- }
- return view
+ func makeUIViewController(context: Context) -> AVPlayerViewController {
+ let controller = AVPlayerViewController()
+ let player = AVPlayer(url: URL(string: url)!)
+ controller.player = player
+ controller.showsPlaybackControls = true
+ controller.videoGravity = .resizeAspect
+
+ // Make the background transparent
+ controller.view.backgroundColor = .clear
+ controller.view.isOpaque = false
+
+ return controller
}
- func updateUIView(_ uiView: UIView, context: Context) {
- if let player = player, let playerLayer = uiView.layer.sublayers?.first as? AVPlayerLayer {
- playerLayer.player = player
- playerLayer.frame = UIScreen.main.bounds
+ func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
+ if isPlaying {
+ uiViewController.player?.play()
+ } else {
+ uiViewController.player?.pause()
}
}
}
struct MemoryCard: View {
let memory: MemoryItem
+ @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 // Default to 16:9 if we can't determine the aspect ratio
+ isLoading = false
+ return
+ }
+
+ aspectRatio = width / height
+ isLoading = false
+ }
var body: some View {
- VStack(alignment: .leading, spacing: 16) {
+ VStack(alignment: .leading, spacing: 8) {
ZStack {
- // Media content
Group {
switch memory.mediaType {
case .image(let url):
if let url = URL(string: url) {
AsyncImage(url: url) { phase in
- if let image = phase.image {
- image
- .resizable()
- .aspectRatio(contentMode: .fill)
- } else if phase.error != nil {
- Color.gray.opacity(0.3)
- } else {
- ProgressView()
+ 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()
+ }
}
}
}
@@ -397,14 +427,19 @@ struct MemoryCard: View {
case .video(_, let previewUrl):
if let previewUrl = URL(string: previewUrl) {
AsyncImage(url: previewUrl) { phase in
- if let image = phase.image {
- image
- .resizable()
- .aspectRatio(contentMode: .fill)
- } else if phase.error != nil {
- Color.gray.opacity(0.3)
- } else {
- ProgressView()
+ 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 {
@@ -412,12 +447,13 @@ struct MemoryCard: View {
}
}
}
- .frame(width: (UIScreen.main.bounds.width / 2) - 24,
- height: (UIScreen.main.bounds.width / 2 - 24) * (1/memory.aspectRatio))
+ .frame(
+ width: (UIScreen.main.bounds.width / 2) - 24,
+ height: (UIScreen.main.bounds.width / 2 - 24) / (isLoading ? 1 : aspectRatio)
+ )
.clipped()
.cornerRadius(12)
- // Show play button for videos
if case .video = memory.mediaType {
Image(systemName: "play.circle.fill")
.font(.system(size: 40))
@@ -426,8 +462,7 @@ struct MemoryCard: View {
}
}
- // Title and Subtitle
- VStack(alignment: .leading, spacing: 16) {
+ VStack(alignment: .leading, spacing: 4) {
Text(memory.title)
.font(Typography.font(for: .body, family: .quicksandBold))
.foregroundColor(.themeTextMessageMain)
@@ -444,6 +479,40 @@ struct MemoryCard: View {
}
}
+// Add this extension to get UIImage from Image
+extension View {
+ func asUIImage() -> UIImage? {
+ let controller = UIHostingController(rootView: self)
+ let view = controller.view
+
+ let targetSize = controller.view.intrinsicContentSize
+ view?.bounds = CGRect(origin: .zero, size: targetSize)
+ view?.backgroundColor = .clear
+
+ let renderer = UIGraphicsImageRenderer(size: targetSize)
+ return renderer.image { _ in
+ view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
+ }
+ }
+}
+
+// Add this extension to MemoryMediaType to get the URL
+private extension MemoryMediaType {
+ 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
+ }
+ }
+}
+
#Preview {
MemoriesView()
}
diff --git a/wake/View/Owner/UserInfo/AvatarPicker.swift b/wake/View/Owner/UserInfo/AvatarPicker.swift
index 0b1d657..ed7dcd4 100644
--- a/wake/View/Owner/UserInfo/AvatarPicker.swift
+++ b/wake/View/Owner/UserInfo/AvatarPicker.swift
@@ -50,18 +50,17 @@ public struct AvatarPicker: View {
if let selectedImage = selectedImage {
Image(uiImage: selectedImage)
.resizable()
- .scaledToFill()
+ .aspectRatio(contentMode: .fill)
.frame(width: 225, height: 225)
- .scaleEffect(scaleFactor)
- .clipShape(RoundedRectangle(cornerRadius: 20 * scaleFactor))
+ .clipShape(RoundedRectangle(cornerRadius: 20))
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.themePrimary, lineWidth: borderWidth)
- .scaleEffect(scaleFactor)
)
+ .scaleEffect(scaleFactor)
} else {
// Default SVG avatar with animated dashed border
- SVGImage(svgName: "IP")
+ SVGImageHtml(svgName: "IP")
.frame(width: 225, height: 225)
.scaleEffect(scaleFactor)
.contentShape(Rectangle())
diff --git a/wake/View/Subscribe/Components/SubscriptionStatusBar.swift b/wake/View/Subscribe/Components/SubscriptionStatusBar.swift
index 8040ee4..8137093 100644
--- a/wake/View/Subscribe/Components/SubscriptionStatusBar.swift
+++ b/wake/View/Subscribe/Components/SubscriptionStatusBar.swift
@@ -53,50 +53,50 @@ enum SubscriptionStatus {
struct SubscriptionStatusBar: View {
let status: SubscriptionStatus
let onSubscribeTap: (() -> Void)?
+ private let height: CGFloat
- init(status: SubscriptionStatus, onSubscribeTap: (() -> Void)? = nil) {
+ init(status: SubscriptionStatus, height: CGFloat? = nil, onSubscribeTap: (() -> Void)? = nil) {
self.status = status
+ self.height = height ?? 155 // 默认高度为155
self.onSubscribeTap = onSubscribeTap
}
var body: some View {
- ZStack(alignment: .topLeading) {
+ ZStack(alignment: .leading) {
// Background SVG - First layer
- SVGImage(svgName: status.backgroundImageName, shouldFill: true)
+ SVGImage(svgName: status.backgroundImageName)
.frame(maxWidth: .infinity, minHeight: 120)
.clipped()
- // Content - Second layer
+ // Main content container
VStack(alignment: .leading, spacing: 0) {
- Spacer()
-
- // Subscription title
+ // Title - Centered vertically
Text(status.title)
.font(.system(size: 28, weight: .bold, design: .rounded))
.foregroundColor(status.textColor)
- .padding(.leading, 24)
+ .frame(maxHeight: .infinity, alignment: .center) // Center vertically
+ .padding(.leading, 12)
+ .padding(.top, height < 155 ? 30 : 40)
- // Expiry date or subscribe button
+ // Expiry date - Bottom left
if case .pioneer(let expiryDate) = status {
VStack(alignment: .leading, spacing: 4) {
- Text("Expires on:")
- .font(.system(size: 14, weight: .medium))
- .foregroundColor(status.textColor.opacity(0.9))
+ Text("Expires on :")
+ .font(.system(size: 12))
+ .foregroundColor(.themeTextMessageMain)
Text(formatDate(expiryDate))
- .font(.system(size: 16, weight: .semibold))
- .foregroundColor(status.textColor)
+ .font(.system(size: 12))
+ .fontWeight(.bold)
+ .foregroundColor(.themeTextMessageMain)
}
- .padding(.leading, 24)
- .padding(.bottom, 20)
+ .padding(.leading, 12)
+ .padding(.bottom, 12)
}
}
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
}
- .frame(height: 155)
- .frame(maxWidth: .infinity)
- .cornerRadius(20)
- .clipped()
+ .frame(height: height)
}
// MARK: - 日期格式化
diff --git a/wake/View/Subscribe/SubscribeView.swift b/wake/View/Subscribe/SubscribeView.swift
index 1c30a7a..02a886b 100644
--- a/wake/View/Subscribe/SubscribeView.swift
+++ b/wake/View/Subscribe/SubscribeView.swift
@@ -7,6 +7,7 @@
import SwiftUI
import StoreKit
+import Network
// MARK: - 订阅计划枚举
enum SubscriptionPlan: String, CaseIterable {
@@ -46,6 +47,7 @@ struct SubscribeView: View {
@State private var showErrorAlert = false
@State private var errorText = ""
@State private var memberProfile: MemberProfile?
+ @State private var showSuccessAlert = false
// 功能对比数据
private let features = [
@@ -61,7 +63,6 @@ struct SubscribeView: View {
dismiss()
}
.background(Color.themeTextWhiteSecondary)
- .padding(.bottom, Theme.Spacing.lg)
ScrollView {
VStack(spacing: 0) {
@@ -123,12 +124,17 @@ struct SubscribeView: View {
} message: {
Text(errorText)
}
+ .alert("Purchase Success", isPresented: $showSuccessAlert) {
+ Button("OK", role: .cancel) { }
+ } message: {
+ Text("购买成功!")
+ }
}
// MARK: - 当前订阅状态卡片
private var currentSubscriptionCard: some View {
let status: SubscriptionStatus = {
- if memberProfile?.membershipLevel == "pioneer" {
+ if memberProfile?.membershipLevel == "Pioneer" {
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let expiryDate = memberProfile.flatMap { dateFormatter.date(from: $0.membershipEndAt) } ?? Date()
@@ -139,7 +145,7 @@ struct SubscribeView: View {
}()
return SubscriptionStatusBar(
- status: .pioneer(expiryDate: Date()),
+ status: status,
onSubscribeTap: {
// 订阅操作
handleSubscribe()
@@ -232,6 +238,9 @@ struct SubscribeView: View {
HStack(spacing: 8) {
Button(action: {
// 打开服务条款
+ if let url = URL(string: "https://memorywake.com/privacy-policy") {
+ UIApplication.shared.open(url)
+ }
}) {
Text("Terms of Service")
.underline()
@@ -242,6 +251,9 @@ struct SubscribeView: View {
Button(action: {
// 打开隐私政策
+ if let url = URL(string: "https://memorywake.com/privacy-policy") {
+ UIApplication.shared.open(url)
+ }
}) {
Text("Privacy Policy")
.underline()
@@ -250,13 +262,24 @@ struct SubscribeView: View {
Text("|")
.foregroundColor(.secondary)
+ // Button(action: {
+ // Task { await store.restorePurchases() }
+ // }) {
+ // Text("Restore Purchase")
+ // .underline()
+ // }
Button(action: {
- Task { await store.restorePurchases() }
- }) {
- Text("Restore Purchase")
- .underline()
+ // 打开隐私政策
+ if let url = URL(string: "https://memorywake.com/privacy-policy") {
+ UIApplication.shared.open(url)
+ }
+ }) {
+ Text("AI Usage Guidelines")
+ .underline()
+ }
+
}
- }
+
.font(Typography.font(for: .caption, family: .quicksandRegular))
.foregroundColor(.secondary)
.padding(.top, Theme.Spacing.sm)
@@ -264,7 +287,286 @@ struct SubscribeView: View {
// MARK: - 订阅处理
private func handleSubscribe() {
- Task { await store.purchasePioneer() }
+ isLoading = true
+ Task {
+ do {
+ print("🔄 开始订阅流程...")
+
+ // 1. 调用后端创建订单
+ print("🔄 正在创建订单...")
+ let orderInfo = try await createOrder()
+
+ // 2. 根据创建订单返回的id来调用创建支付接口
+ print("🔄 正在创建支付...")
+ let paymentInfo = try await createPayment(orderId: orderInfo.id)
+
+ // 3. 使用订单信息进行应用内购买
+ print("🔄 开始苹果内购流程...")
+ do {
+ // 发起苹果内购
+ let transactionId = try await store.purchasePioneer()
+ print("✅ 苹果内购成功,交易ID: \(transactionId)")
+
+ // 4. 通知服务器支付成功
+ print("🔄 正在通知服务器支付处理中...")
+ _ = try await notifyPaymentProcessing(
+ transactionId: paymentInfo.transactionId ?? paymentInfo.id,
+ // thirdPartyTransactionId: transactionId
+ )
+
+ print("🔄 正在通知服务器支付成功...")
+ _ = try await notifyPaymentSuccess(
+ transactionId: paymentInfo.transactionId ?? paymentInfo.id,
+ // thirdPartyTransactionId: transactionId
+ )
+
+ print("✅ 订阅流程完成")
+
+ // 5. 成功后关闭页面
+ await MainActor.run {
+ self.isLoading = false
+ self.dismiss()
+ }
+
+ } catch let purchaseError as NSError {
+ print("❌ 苹果内购失败: \(purchaseError.localizedDescription)")
+
+ // 通知服务器支付失败
+ print("🔄 正在通知服务器支付失败...")
+ _ = try? await notifyPaymentFailure(
+ transactionId: paymentInfo.transactionId ?? paymentInfo.id,
+ reason: purchaseError.localizedDescription
+ )
+
+ // 重新抛出错误以便外部处理
+ throw purchaseError
+ }
+
+ } catch let error as NSError {
+ print("❌ 订阅失败: \(error.localizedDescription)")
+
+ // 根据错误类型显示不同的错误信息
+ var errorMessage = error.localizedDescription
+
+ if error.domain == "NetworkError" {
+ errorMessage = "网络连接失败,请检查您的网络设置"
+ } else if error.domain == "APIError" {
+ errorMessage = "请求失败,请稍后重试 (错误码: \(error.code))"
+ } else if error.domain == NSURLErrorDomain {
+ switch error.code {
+ case NSURLErrorNotConnectedToInternet, NSURLErrorNetworkConnectionLost:
+ errorMessage = "网络连接已断开,请检查您的网络设置"
+ case NSURLErrorTimedOut:
+ errorMessage = "请求超时,请稍后重试"
+ case NSURLErrorCannotConnectToHost, NSURLErrorCannotFindHost:
+ errorMessage = "无法连接到服务器,请稍后重试"
+ default:
+ errorMessage = "网络错误: \(error.localizedDescription)"
+ }
+ }
+
+ // 在主线程更新UI
+ await MainActor.run {
+ self.isLoading = false
+ self.errorText = errorMessage
+ self.showErrorAlert = true
+ print("❌ 错误提示: \(errorMessage)")
+ }
+ }
+ }
+ }
+
+ // 创建订单
+ private func createOrder() async throws -> OrderInfo {
+ return try await withCheckedThrowingContinuation { continuation in
+ let parameters: [String: Any] = [
+ "items": [
+ [
+ "product_item_id": 5,
+ "quantity": 1
+ ]
+ ]
+ ]
+
+ print("🔄 开始创建订单请求,参数:\(parameters)")
+
+ // 检查网络连接
+ let monitor = NWPathMonitor()
+ let queue = DispatchQueue(label: "NetworkMonitor")
+ monitor.pathUpdateHandler = { path in
+ if path.status == .satisfied {
+ // 网络可用,继续执行网络请求
+ NetworkService.shared.postWithToken(
+ path: "/order/create",
+ parameters: parameters
+ ) { (result: Result, NetworkError>) in
+ switch result {
+ case .success(let response):
+ print("✅ 请求成功,状态码:\(response.code)")
+ print("📦 返回数据:\(String(describing: response.data))")
+
+ if response.code == 0 {
+ continuation.resume(returning: response.data)
+ } else {
+ let errorMessage = "创建订单失败,状态码:\(response.code)"
+ print("❌ \(errorMessage)")
+ continuation.resume(throwing: NSError(
+ domain: "APIError",
+ code: response.code,
+ userInfo: [NSLocalizedDescriptionKey: errorMessage]
+ ))
+ }
+ case .failure(let error):
+ print("❌ 请求异常:\(error.localizedDescription)")
+ print("🔍 错误详情:\(error)")
+ if let urlError = error as? URLError {
+ print("🌐 URL错误: \(urlError.code.rawValue) - \(urlError.localizedDescription)")
+ print("🔗 失败URL: \(urlError.failingURL?.absoluteString ?? "未知")")
+ }
+ continuation.resume(throwing: error)
+ }
+ }
+ } else {
+ // 网络不可用,抛出错误
+ let errorMessage = "网络连接不可用,请检查网络设置"
+ print("❌ \(errorMessage)")
+ continuation.resume(throwing: NSError(domain: "NetworkError", code: 0, userInfo: [NSLocalizedDescriptionKey: errorMessage]))
+ }
+ }
+ monitor.start(queue: queue)
+ }
+ }
+ // 创建支付
+ private func createPayment(orderId: String) async throws -> PaymentInfo {
+ return try await withCheckedThrowingContinuation { continuation in
+ let parameters: [String: Any] = [
+ "order_id": orderId,
+ "payment_method": "ApplePay"
+ ]
+
+ print("🔄 开始创建支付请求,参数:\(parameters)")
+
+ NetworkService.shared.postWithToken(
+ path: "/order/pay",
+ parameters: parameters
+ ) { (result: Result, NetworkError>) in
+ switch result {
+ case .success(let response):
+ print("✅ 请求成功,状态码:\(response.code)")
+ print("📦 返回数据:\(String(describing: response.data))")
+
+ if response.code == 0 {
+ continuation.resume(returning: response.data)
+ } else {
+ let errorMessage = "创建支付失败,状态码:\(response.code)"
+ print("❌ \(errorMessage)")
+ continuation.resume(throwing: NSError(
+ domain: "APIError",
+ code: response.code,
+ userInfo: [NSLocalizedDescriptionKey: errorMessage]
+ ))
+ }
+ case .failure(let error):
+ print("❌ 请求异常:\(error.localizedDescription)")
+ print("🔍 错误详情:\(error)")
+ if let urlError = error as? URLError {
+ print("🌐 URL错误: \(urlError.code.rawValue) - \(urlError.localizedDescription)")
+ print("🔗 失败URL: \(urlError.failingURL?.absoluteString ?? "未知")")
+ }
+ continuation.resume(throwing: error)
+ }
+ }
+ }
+ }
+
+ // MARK: - 支付结果处理
+
+ /// 通知服务器支付处理中
+ /// - Parameter transactionId: 交易ID
+ /// - Parameter thirdPartyTransactionId: 第三方交易ID,可选
+ private func notifyPaymentProcessing(transactionId: String, thirdPartyTransactionId: String? = nil) async throws -> Bool {
+ return try await withCheckedThrowingContinuation { continuation in
+ var parameters: [String: Any] = ["transaction_id": transactionId]
+
+ // 只有在提供了第三方交易ID时才添加到参数中
+ if let thirdPartyId = thirdPartyTransactionId {
+ parameters["third_party_transaction_id"] = thirdPartyId
+ }
+
+ print("🔄 通知服务器支付处理中,参数:\(parameters)")
+
+ NetworkService.shared.postWithToken(
+ path: "/order/pay-processing",
+ parameters: parameters
+ ) { (result: Result, NetworkError>) in
+ switch result {
+ case .success(let response):
+ print("✅ 支付处理通知发送成功,状态码:\(response.code)")
+ continuation.resume(returning: response.code == 0)
+ case .failure(let error):
+ print("❌ 支付处理通知发送失败:\(error.localizedDescription)")
+ continuation.resume(throwing: error)
+ }
+ }
+ }
+ }
+
+ /// 通知服务器支付成功
+ /// - Parameter transactionId: 交易ID
+ /// - Parameter thirdPartyTransactionId: 第三方交易ID,可选
+ private func notifyPaymentSuccess(transactionId: String, thirdPartyTransactionId: String? = nil) async throws -> Bool {
+ return try await withCheckedThrowingContinuation { continuation in
+ var parameters: [String: Any] = ["transaction_id": transactionId]
+
+ // 只有在提供了第三方交易ID时才添加到参数中
+ if let thirdPartyId = thirdPartyTransactionId {
+ parameters["third_party_transaction_id"] = thirdPartyId
+ }
+
+ print("🔄 通知服务器支付成功,参数:\(parameters)")
+
+ NetworkService.shared.postWithToken(
+ path: "/order/pay-success",
+ parameters: parameters
+ ) { (result: Result, NetworkError>) in
+ switch result {
+ case .success(let response):
+ print("✅ 支付成功通知发送成功,状态码:\(response.code)")
+ continuation.resume(returning: response.code == 0)
+ case .failure(let error):
+ print("❌ 支付成功通知发送失败:\(error.localizedDescription)")
+ continuation.resume(throwing: error)
+ }
+ }
+ }
+ }
+
+ /// 通知服务器支付失败
+ /// - Parameter transactionId: 交易ID
+ /// - Parameter reason: 失败原因
+ private func notifyPaymentFailure(transactionId: String, reason: String) async throws -> Bool {
+ return try await withCheckedThrowingContinuation { continuation in
+ let parameters: [String: Any] = [
+ "transaction_id": transactionId,
+ "reason": reason
+ ]
+
+ print("🔄 通知服务器支付失败,参数:\(parameters)")
+
+ NetworkService.shared.postWithToken(
+ path: "/order/pay-failure",
+ parameters: parameters
+ ) { (result: Result, NetworkError>) in
+ switch result {
+ case .success(let response):
+ print("✅ 支付失败通知发送成功,状态码:\(response.code)")
+ continuation.resume(returning: response.code == 0)
+ case .failure(let error):
+ print("❌ 支付失败通知发送失败:\(error.localizedDescription)")
+ continuation.resume(throwing: error)
+ }
+ }
+ }
}
// MARK: - Helper Methods
diff --git a/wake/View/Upload/MediaUploadView.swift b/wake/View/Upload/MediaUploadView.swift
index 683d69e..ab0250c 100644
--- a/wake/View/Upload/MediaUploadView.swift
+++ b/wake/View/Upload/MediaUploadView.swift
@@ -599,7 +599,7 @@ struct UploadPromptView: View {
var body: some View {
Button(action: { showMediaPicker = true }) {
// 上传图标
- SVGImage(svgName: "IP")
+ SVGImageHtml(svgName: "IP")
.frame(width: 225, height: 225)
.contentShape(Rectangle())
.overlay(