From 538507f5ecad246db39f86271886fbac148823cb Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Mon, 1 Sep 2025 13:24:12 +0800 Subject: [PATCH] feat: bug --- wake.xcodeproj/project.pbxproj | 17 + .../xcshareddata/swiftpm/Package.resolved | 10 + wake/Assets/Png/logo.png | Bin 0 -> 62271 bytes wake/Components/Buttons/ReturnButton.swift | 2 + wake/ContentView.swift | 23 +- wake/Models/OrderInfo.swift | 101 +++++ wake/Utils/IAPManager.swift | 26 +- wake/Utils/SVGImage.swift | 200 ++++++--- wake/Utils/SVGImageHtml.swift | 71 +++ wake/View/Blind/BlindOutCome.swift | 188 +++----- wake/View/Blind/JoinModal.swift | 2 +- wake/View/Components/SheetModal.swift | 44 +- .../Upload/ImageUploaderGetID.swift | 4 +- wake/View/Components/UserProfileModal.swift | 185 ++++---- wake/View/Credits/CreditsInfoCard.swift | 4 +- wake/View/Memories/MemoriesView.swift | 413 ++++++++++-------- wake/View/Owner/UserInfo/AvatarPicker.swift | 9 +- .../Components/SubscriptionStatusBar.swift | 42 +- wake/View/Subscribe/SubscribeView.swift | 320 +++++++++++++- wake/View/Upload/MediaUploadView.swift | 2 +- 20 files changed, 1143 insertions(+), 520 deletions(-) create mode 100644 wake/Assets/Png/logo.png create mode 100644 wake/Models/OrderInfo.swift create mode 100644 wake/Utils/SVGImageHtml.swift 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 0000000000000000000000000000000000000000..fc79a01a1fe1d9f3f949b2c5f3f0486e943bf490 GIT binary patch literal 62271 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 - - - """ - - 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(

4oYSKy4D-IF6RpR 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 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 = """ - - - - - - - -