diff --git a/Podfile b/Podfile index 6cf9cae..cc03db5 100644 --- a/Podfile +++ b/Podfile @@ -33,4 +33,10 @@ target 'SynthReel' do pod 'HWPanModal' pod 'LYEmptyView' pod 'ZLPhotoBrowser' + + # AdMob SDK + pod 'Google-Mobile-Ads-SDK' + + # AppLovin SDK + pod 'AppLovinSDK' end diff --git a/SynthReel.xcodeproj/project.pbxproj b/SynthReel.xcodeproj/project.pbxproj index 99b0fc8..ecdf632 100644 --- a/SynthReel.xcodeproj/project.pbxproj +++ b/SynthReel.xcodeproj/project.pbxproj @@ -195,6 +195,7 @@ 3779D0612ECF1CB8006B1698 /* SRShortHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3779D0602ECF1CB8006B1698 /* SRShortHeaderView.swift */; }; 47BB39E2DD30787FA591F8EB /* Pods_SynthReel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F9255BF4D4B1CFDDB5CFFB43 /* Pods_SynthReel.framework */; }; 85ACDA2F2EE6B3760009B306 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 85ACDA2E2EE6B3760009B306 /* GoogleService-Info.plist */; }; + 85ACDA312EE6C3640009B306 /* SRVipRetainAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ACDA302EE6C3640009B306 /* SRVipRetainAlert.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -499,6 +500,7 @@ 85ACDA292EE69CD90009B306 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 85ACDA2B2EE69CD90009B306 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 85ACDA2E2EE6B3760009B306 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 85ACDA302EE6C3640009B306 /* SRVipRetainAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SRVipRetainAlert.swift; sourceTree = ""; }; AA88214030574193B51DE563 /* Pods-SynthReel.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SynthReel.release.xcconfig"; path = "Target Support Files/Pods-SynthReel/Pods-SynthReel.release.xcconfig"; sourceTree = ""; }; F9255BF4D4B1CFDDB5CFFB43 /* Pods_SynthReel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SynthReel.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -684,7 +686,6 @@ isa = PBXGroup; children = ( 03B1A9202ECB2F25006C353F /* VC */, - 03B1A9242ECBFF03006C353F /* V */, 03B1A8EB2EC72C0E006C353F /* M */, 03B1A9232ECBFEF9006C353F /* VM */, ); @@ -714,6 +715,7 @@ isa = PBXGroup; children = ( 03B1A9252ECBFF31006C353F /* SRShortPlayerViewModel.swift */, + 03B1A9242ECBFF03006C353F /* V */, 03980F4E2ECEB91C0006E317 /* SRRecommendPlayerViewModel.swift */, ); path = VM; @@ -733,6 +735,7 @@ 3754ACD62ED82774009EBCAD /* SRDetailRecommendCell.swift */, 3754AD192EDD745A009EBCAD /* SRVideoLockView.swift */, 3754B0302EE2C9D4009EBCAD /* SRVideoRechargeView.swift */, + 85ACDA302EE6C3640009B306 /* SRVipRetainAlert.swift */, ); path = V; sourceTree = ""; @@ -1773,6 +1776,7 @@ 03B1A9302ECC10D1006C353F /* SRSearchViewController.swift in Sources */, 03B1A8432EC5CB99006C353F /* SRTabBarController.swift in Sources */, 3754AD3E2EDD9C88009EBCAD /* SRLogin+FB.swift in Sources */, + 85ACDA312EE6C3640009B306 /* SRVipRetainAlert.swift in Sources */, 03B1A8EF2EC72C78006C353F /* SRShortModel.swift in Sources */, 03B1A8ED2EC72C1F006C353F /* SRHomeModuleItem.swift in Sources */, 370D2F102ED4534500571E77 /* SRUserViewController.swift in Sources */, diff --git a/SynthReel/Base/API/SRStoreAPI.swift b/SynthReel/Base/API/SRStoreAPI.swift index 9c50331..f29a01c 100644 --- a/SynthReel/Base/API/SRStoreAPI.swift +++ b/SynthReel/Base/API/SRStoreAPI.swift @@ -89,5 +89,14 @@ class SRStoreAPI: NSObject { } } + static func requestVipRetainPayInfo() async -> SRPayAlertModel? { + await withCheckedContinuation { continuation in + var param = SRNetwork.Parameters(path: "/getRetainVipPaySetting") + param.method = .get + SRNetwork.request(parameters: param) { (response: SRNetwork.Response) in + continuation.resume(returning: response.data) + } + } + } } diff --git a/SynthReel/Class/Player/VM/SRShortPlayerViewModel.swift b/SynthReel/Class/Player/VM/SRShortPlayerViewModel.swift index 1c9ae9c..c8ad3e7 100644 --- a/SynthReel/Class/Player/VM/SRShortPlayerViewModel.swift +++ b/SynthReel/Class/Player/VM/SRShortPlayerViewModel.swift @@ -203,12 +203,32 @@ extension SRShortPlayerViewModel { } view.didDismissHandle = { [weak self] in guard let self = self else { return } -// self._showVipRetainAlert(videoInfo) + self._showVipRetainAlert(videoInfo) } view.present(in: nil) self.popView = view } + private func _showVipRetainAlert(_ videoInfo: SRVideoInfoModel) { + + + payDataRequest = SRPayDataRequest() + + payDataRequest?.requestVipRetainPayInfo { [weak self] model in + guard let self = self else { return } + guard let model = model else { return } + let view = SRVipRetainAlert() + view.model = model + view.videoInfo = videoInfo + view.buyFinishHandle = { [weak self] in + guard let self = self else { return } + Task { + await self.requestShortDetail(indexPath: self.currentIndexPath) + } + } + view.show(in: SRTool.keyWindow) + } + } @objc private func handleRecommandTimer() { self.isShowRecommand = true diff --git a/SynthReel/Class/Player/VM/V/SRDetailRecommendCell.swift b/SynthReel/Class/Player/VM/V/SRDetailRecommendCell.swift new file mode 100644 index 0000000..4fdd924 --- /dev/null +++ b/SynthReel/Class/Player/VM/V/SRDetailRecommendCell.swift @@ -0,0 +1,79 @@ +// +// SRDetailRecommendCell.swift +// SynthReel +// +// Created by CSGY on 2025/11/27. +// Copyright © 2025 SR. All rights reserved. +// + +import UIKit +import FSPagerView +import JXPlayer + +class SRDetailRecommendCell: FSPagerViewCell { + + var model: SRShortModel? { + didSet { + player.coverImageView?.sr_setImage(model?.image_url) + player.setPlayUrl(url: model?.video_url ?? "") + } + } + + /// ① 新增背景图 + private lazy var bgImageView: UIImageView = { + let iv = UIImageView() + iv.image = .homeViralHitsCell + iv.contentMode = .scaleAspectFill + iv.clipsToBounds = true + return iv + }() + + + private lazy var player: JXPlayer = { + let player = JXPlayer(controlView: nil) + player.playerView = self.playerView + player.isLoop = true + return player + }() + + private lazy var playerView: UIView = { + let view = UIView() + view.isUserInteractionEnabled = false + return view + }() + + + + override init(frame: CGRect) { + super.init(frame: frame) + contentView.addSubview(bgImageView) + bgImageView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + // 播放器画面 + contentView.addSubview(playerView) + playerView.snp.makeConstraints { make in +// make.edges.equalToSuperview() + make.edges.equalTo(UIEdgeInsets(top: 15, left: 8, bottom: 15, right: 8)) + } +// addSubview(playerView) +// +// playerView.snp.makeConstraints { make in +// make.edges.equalToSuperview() +// } + } + + @MainActor required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func play() { + self.player.start() + } + + func pause() { + self.player.pause() + } + +} diff --git a/SynthReel/Class/Player/VM/V/SRDetailRecommendview.swift b/SynthReel/Class/Player/VM/V/SRDetailRecommendview.swift new file mode 100644 index 0000000..31eac76 --- /dev/null +++ b/SynthReel/Class/Player/VM/V/SRDetailRecommendview.swift @@ -0,0 +1,175 @@ +// +// SRDetailRecommendview.swift +// SynthReel +// +// Created by CSGY on 2025/11/27. +// Copyright © 2025 SR. All rights reserved. +// + +import UIKit +import FSPagerView + +class SRDetailRecommendview: SRBaseAlert { + + var clickCloseButton: (() -> Void)? + var didSelectedVideo: ((_ model: SRShortModel) -> Void)? + + var dataArr: [SRShortModel] = [] { + didSet { + self.pagerView.reloadData() + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in + self?.updateCurrentData() + } + } + } + + private weak var currentCell: SRDetailRecommendCell? { + didSet { + oldValue?.pause() + + currentCell?.play() + } + } + + private lazy var titleLabel: SRLabel = { + let label = SRLabel() + label.font = .font(ofSize: 16, weight: .init(900)) + label.textColors = [UIColor._4_CFFD_4.cgColor, UIColor._51_D_4_FF.cgColor] + label.textStartPoint = .init(x: 0.5, y: 0) + label.textEndPoint = .init(x: 0.5, y: 1) + label.text = "Keep the Drama Going".localized + return label + }() + + private lazy var bgView: UIImageView = { + let view = UIImageView(image: UIImage(named: "recommendBg")) + view.isUserInteractionEnabled = true + return view + }() + + private lazy var cancelButton: UIButton = { + let button = UIButton(type: .custom) + button.setImage(UIImage(named: "Close"), for: .normal) + button.addTarget(self, action: #selector(handleCancelButton), for: .touchUpInside) + return button + }() + + private lazy var pagerView: FSPagerView = { + let transformer = SRPagerViewTransformer(type: .linear) + transformer.minimumScale = 1 + + let view = FSPagerView() + view.itemSize = .init(width: 132, height: 185) + view.transformer = transformer + view.delegate = self + view.dataSource = self + view.isInfinite = true + view.interitemSpacing = 8 + view.register(SRDetailRecommendCell.self, forCellWithReuseIdentifier: "cell") + return view + }() + + override init(frame: CGRect) { + super.init(frame: frame) + contentWidth = UIScreen.width + self.closeButton.isHidden = true + + self.contentView.backgroundColor = .clear + self.contentView.layer.cornerRadius = 0 + self.contentView.layer.masksToBounds = false + + fa_setupLayout() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + @objc private func handleCancelButton() { + self.dismiss() + self.clickCloseButton?() + } + + ///更新当前数据 + private func updateCurrentData() { + guard let cell = self.pagerView.cellForItem(at: self.pagerView.currentIndex) as? SRDetailRecommendCell else { return } + + self.currentCell = cell +// let model = cell.model +// self.videoNameLabel.text = model?.name + } + +} + +extension SRDetailRecommendview { + + private func fa_setupLayout() { + contentView.addSubview(bgView) + contentView.addSubview(cancelButton) + bgView.addSubview(titleLabel) + bgView.addSubview(pagerView) + + + bgView.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(15) + make.top.equalToSuperview().offset(-25) +// make.bottom.e() + } + + titleLabel.snp.makeConstraints { make in + make.top.equalTo(17) + make.centerX.equalToSuperview() + } + + pagerView.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(10) + make.bottom.equalTo(-17) + make.top.equalTo(titleLabel.snp.bottom).offset(10) + } + + cancelButton.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.top.equalTo(bgView.snp.bottom).offset(30) + make.bottom.equalToSuperview() + } + + + } + +} + +//MARK: FSPagerViewDelegate FSPagerViewDataSource +extension SRDetailRecommendview: FSPagerViewDelegate, FSPagerViewDataSource { + + func pagerView(_ pagerView: FSPagerView, cellForItemAt index: Int) -> FSPagerViewCell { + let cell = pagerView.dequeueReusableCell(withReuseIdentifier: "cell", at: index) as! SRDetailRecommendCell + cell.model = self.dataArr[index] + return cell + } + + func numberOfItems(in pagerView: FSPagerView) -> Int { + return self.dataArr.count + } + + func pagerView(_ pagerView: FSPagerView, didSelectItemAt index: Int) { + didSelectedVideo?(self.dataArr[index]) + self.dismiss() + } + + func pagerViewDidEndDecelerating(_ pagerView: FSPagerView) { + self.updateCurrentData() + } + +} + +extension SRDetailRecommendview { + + class PageControl: UIPageControl{ + override func size(forNumberOfPages pageCount: Int) -> CGSize { + return .init(width: 4, height: 4) + } + } + +} diff --git a/SynthReel/Class/Player/VM/V/SREpSelectorCell.swift b/SynthReel/Class/Player/VM/V/SREpSelectorCell.swift new file mode 100644 index 0000000..bb17e5f --- /dev/null +++ b/SynthReel/Class/Player/VM/V/SREpSelectorCell.swift @@ -0,0 +1,99 @@ +// +// SREpSelectorCell.swift +// SynthReel +// +// Created by 湖北秦九 on 2025/11/19. +// Copyright © 2025 SR. All rights reserved. +// + +import UIKit +import SnapKit + +class SREpSelectorCell: UICollectionViewCell { + + + var model: SRVideoInfoModel? { + didSet { + numLabel.text = model?.episode + lockImageview.isHidden = !(model?.is_lock ?? true) + } + } + + var sr_isSelected: Bool = false { + didSet { + if sr_isSelected { + numLabel.textColors = [UIColor.srGreen.cgColor, UIColor.srBlue.cgColor] + boderView.isHidden = false + } else { + numLabel.textColors = [UIColor.white.cgColor, UIColor.white.cgColor] + boderView.isHidden = true + } + } + } + + lazy var numLabel: SRLabel = { + let label = SRLabel() + label.font = .font(ofSize: 14, weight: .regular) + label.textStartPoint = .init(x: 0.5, y: 0) + label.textEndPoint = .init(x: 0.5, y: 1) + return label + }() + + lazy var boderView: SRGradientView = { + let view = SRGradientView() + view.colors = [UIColor.srGreen.cgColor, UIColor.srBlue.cgColor] + view.startPoint = .init(x: 0.5, y: 0) + view.endPoint = .init(x: 0.5, y: 1) + view.layer.cornerRadius = 10 + view.layer.masksToBounds = true + return view + }() + + lazy var lockImageview = UIImageView.init(image: .lock) + + lazy var boderLayer: CAShapeLayer = { + let layer = CAShapeLayer() + return layer + }() + + override init(frame: CGRect) { + super.init(frame: frame) + contentView.layer.cornerRadius = 10 + contentView.layer.masksToBounds = true + contentView.backgroundColor = ._1_B_1_B_1_B + boderLayer.fillColor = contentView.backgroundColor?.cgColor // 设置为透明填充,实现镂空效果 + + + contentView.addSubview(boderView) + boderView.layer.addSublayer(boderLayer) + contentView.addSubview(numLabel) + contentView.addSubview(lockImageview) + + numLabel.snp.makeConstraints { make in + make.center.equalToSuperview() + } + + boderView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + lockImageview.snp.makeConstraints { make in + make.right.top.equalToSuperview().inset(4) + make.width.height.equalTo(12) + } + } + + + override func layoutSubviews() { + super.layoutSubviews() + let size = self.bounds.size + let boderWidth: CGFloat = 1 + + boderLayer.path = UIBezierPath(roundedRect: .init(x: boderWidth, y: boderWidth, width: size.width - boderWidth * 2, height: size.height - boderWidth * 2), cornerRadius: 9.5).cgPath + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/SynthReel/Class/Player/VM/V/SREpSelectorView.swift b/SynthReel/Class/Player/VM/V/SREpSelectorView.swift new file mode 100644 index 0000000..5ca7123 --- /dev/null +++ b/SynthReel/Class/Player/VM/V/SREpSelectorView.swift @@ -0,0 +1,211 @@ +// +// SREpSelectorView.swift +// SynthReel +// +// Created by 湖北秦九 on 2025/11/18. +// Copyright © 2025 SR. All rights reserved. +// + +import UIKit +import HWPanModal +import SnapKit + +class SREpSelectorView: SRPanModalContentView { + + var didSelected: ((_ index: Int) -> Void)? + + var model: SRShortDetailModel? { + didSet { + coverImageView.sr_setImage(model?.shortPlayInfo?.image_url) + shortNameLabel.text = model?.shortPlayInfo?.name + desLabel.text = model?.shortPlayInfo?.sr_description + + subtitleLabel.text = "all_episodes_text".localizedReplace(text: "\(model?.shortPlayInfo?.episode_total ?? 0)") + + if let text = model?.shortPlayInfo?.category?.first, text.count > 0 { + cagetoryLabel.text = "#" + text + } else { + cagetoryLabel.text = "" + } + self.collectionView.reloadData() + } + } + var selectedIndex: Int = 0 { + didSet { + self.collectionView.reloadData() + } + } + + lazy var coverBgView = UIImageView(image: UIImage(named: "ep_cover_bg_image")) + lazy var coverImageView: UIImageView = { + let imageView = SRImageView() + imageView.layer.cornerRadius = 2 + return imageView + }() + + lazy var shortNameLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 15, weight: .semibold) + label.textColor = .srBlue + return label + }() + + lazy var cagetoryLabel: SRLabel = { + let label = SRLabel() + label.font = .font(ofSize: 12, weight: .regular) + label.textColors = [UIColor.srGreen.cgColor, UIColor.srBlue.cgColor] + label.textStartPoint = .init(x: 0.5, y: 0) + label.textEndPoint = .init(x: 0.5, y: 1) + return label + }() + + lazy var desLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 12, weight: .regular) + label.textColor = .A_6_A_6_A_6 + label.numberOfLines = 3 + return label + }() + + lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 15, weight: .medium) + label.textColor = .white + label.text = "Select Episode".localized + return label + }() + + lazy var subtitleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 12, weight: .regular) + label.textColor = .CCCCCC + return label + }() + + lazy var collectionViewLayout: UICollectionViewFlowLayout = { + let itemWidth = (UIScreen.width - 30 - 40) / 5 + + let layout = UICollectionViewFlowLayout() + layout.minimumLineSpacing = 10 + layout.minimumInteritemSpacing = 10 + layout.sectionInset = .init(top: 0, left: 15, bottom: 0, right: 15) + layout.itemSize = .init(width: floor(itemWidth), height: 50) + return layout + }() + + lazy var collectionView: SRCollectionView = { + let collectionView = SRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) + collectionView.delegate = self + collectionView.dataSource = self + collectionView.showsVerticalScrollIndicator = false + collectionView.contentInset = .init(top: 0, left: 0, bottom: UIScreen.safeBottom + 10, right: 0) + collectionView.register(SREpSelectorCell.self, forCellWithReuseIdentifier: "cell") + return collectionView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + sr_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + + +extension SREpSelectorView { + + private func sr_setupUI() { + addSubview(coverBgView) + addSubview(coverImageView) + addSubview(shortNameLabel) + addSubview(cagetoryLabel) + addSubview(desLabel) + addSubview(titleLabel) + addSubview(subtitleLabel) + addSubview(collectionView) + + coverBgView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(15) + make.top.equalToSuperview().offset(18) + } + + coverImageView.snp.makeConstraints { make in + make.center.equalTo(coverBgView) + make.width.equalTo(63) + make.height.equalTo(84) + } + + shortNameLabel.snp.makeConstraints { make in + make.left.equalTo(coverImageView.snp.right).offset(10) + make.top.equalToSuperview().offset(24) + make.right.lessThanOrEqualToSuperview().offset(-15) + } + + cagetoryLabel.snp.makeConstraints { make in + make.left.equalTo(shortNameLabel) + make.top.equalTo(shortNameLabel.snp.bottom).offset(8) + } + + desLabel.snp.makeConstraints { make in + make.left.equalTo(shortNameLabel) + make.right.lessThanOrEqualToSuperview().offset(-15) + make.top.equalTo(shortNameLabel.snp.bottom).offset(32) + } + + titleLabel.snp.makeConstraints { make in + make.left.equalToSuperview().offset(15) + make.top.equalTo(coverBgView.snp.bottom).offset(16) + } + + subtitleLabel.snp.makeConstraints { make in + make.centerY.equalTo(titleLabel) + make.left.equalTo(titleLabel.snp.right).offset(3) + } + + collectionView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.top.equalToSuperview().offset(166) + make.bottom.equalToSuperview() + } + } + +} + +//MARK: UICollectionViewDelegate UICollectionViewDataSource +extension SREpSelectorView: UICollectionViewDelegate, UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! SREpSelectorCell + cell.model = self.model?.episodeList?[indexPath.row] + cell.sr_isSelected = indexPath.row == self.selectedIndex + return cell + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return self.model?.episodeList?.count ?? 0 + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + + guard let epList = self.model?.episodeList else { return } + if self.selectedIndex == indexPath.row { return } + + let lastIndex = indexPath.row - 1 + var lastIsLock = false + if lastIndex > 0 && lastIndex < epList.count { + let lastModel = epList[lastIndex] + lastIsLock = lastModel.is_lock ?? false + } + if lastIsLock { + SRToast.show(text: "buy_fail_toast_02".localized) + return + } + + self.didSelected?(indexPath.row) + Task { + await self.dismiss(animated: true) + } + } +} diff --git a/SynthReel/Class/Player/VM/V/SRProgressView.swift b/SynthReel/Class/Player/VM/V/SRProgressView.swift new file mode 100644 index 0000000..6e9976b --- /dev/null +++ b/SynthReel/Class/Player/VM/V/SRProgressView.swift @@ -0,0 +1,216 @@ +// +// SRProgressView.swift +// SynthReel +// +// Created by 湖北秦九 on 2025/11/18. +// Copyright © 2025 SR. All rights reserved. +// + +import UIKit +import YYText +import YYCategories + +class SRProgressView: UIView { + + ///滑动开始 + var panStart: (() -> Void)? + + ///滑动中 + var panChange: ((_ progress: CGFloat) -> Void)? + + ///滑动完成回调 + var panFinish: ((_ progress: CGFloat) -> Void)? + + var progress: CGFloat = 0 { + didSet { + if !isPaning { + setNeedsDisplay() + } + } + } + + ///用来记录滑动时的当前进度 + private var tempProgress: CGFloat = 0 + + ///滑动进度 + private var panProgress: CGFloat = 0 + + var progressColor = UIColor.white.withAlphaComponent(0.2) + var currentProgress = UIColor.white + + var lineWidth: CGFloat = 3 + + ///加载中状态 + var isLoading = false { + didSet { + if isLoading { + if gradientTimer == nil { + gradientTimer = Timer.scheduledTimer(timeInterval: 0.05, target: YYTextWeakProxy(target: self), selector: #selector(handleGradientTimer), userInfo: nil, repeats: true) + } + } else { + gradientTimer?.invalidate() + gradientTimer = nil + } + } + } + + var insets: UIEdgeInsets = .init(top: 0, left: 0, bottom: 0, right: 0) { + didSet { + self.invalidateIntrinsicContentSize() + setNeedsDisplay() + } + } + + private(set) lazy var panGesture: UIPanGestureRecognizer = { + let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(sender:))) + return pan + }() + + private(set) lazy var tagGesture: UITapGestureRecognizer = { + let tap = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(sender:))) + return tap + }() + + ///是否在滑动中 + private var isPaning: Bool = false + + private var gradientTimer: Timer? + + private var gradientValue: CGFloat = 0 + + override var intrinsicContentSize: CGSize { + return .init(width: UIScreen.width, height: lineWidth + insets.top + insets.bottom) + } + + override init(frame: CGRect) { + super.init(frame: frame) +// self.backgroundColor = progressColor + self.backgroundColor = .clear + + self.addGestureRecognizer(panGesture) + self.addGestureRecognizer(tagGesture) + + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + setNeedsDisplay() + } + + @objc private func handleGradientTimer() { + gradientValue += 0.1 + if gradientValue > 1 { + gradientValue = 0 + } + setNeedsDisplay() + } + + override func draw(_ rect: CGRect) { + super.draw(rect) + guard let context = UIGraphicsGetCurrentContext() else { return } + let width = rect.width + + let progressX = insets.left + let progressY = insets.top + let progressWidth = width - insets.left - insets.right + + if isLoading, !isPaning { + // 定义颜色空间 + let colorSpace = CGColorSpaceCreateDeviceRGB() + let colors: [CGColor] = [ + UIColor.clear.cgColor, + UIColor.white.cgColor, + UIColor.clear.cgColor + ] + let locations: [CGFloat] = [0.0, gradientValue, 1.0] + + guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: locations) else { + return + } + + let gradientRect = CGRect(x: progressX, + y: progressY, + width: progressWidth, + height: lineWidth) + + // 定义渐变的起点和终点 + let startPoint = CGPoint(x: rect.minX, y: rect.minY) + let endPoint = CGPoint(x: rect.maxX, y: rect.maxY) + + // 裁剪到渐变区域 + context.saveGState() + context.clip(to: gradientRect) + + // 绘制渐变 + context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: []) + } else { + var progress = self.progress + if self.isPaning { + progress = self.panProgress + } + let currentProgressWidth = progressWidth * progress + + ///绘制进度 + let progressPath = UIBezierPath(roundedRect: CGRect(x: progressX, y: progressY, width: progressWidth, height: lineWidth), cornerRadius: lineWidth / 2) + context.addPath(progressPath.cgPath) + context.setFillColor(progressColor.cgColor) + context.fillPath() + + ///绘制当前进度 + let currentPath = UIBezierPath(roundedRect: CGRect(x: progressX, y: progressY, width: progressWidth * progress, height: lineWidth), cornerRadius: lineWidth / 2) + context.addPath(currentPath.cgPath) + context.setFillColor(currentProgress.cgColor) + context.fillPath() + + ///绘制一个点 + let path = UIBezierPath(arcCenter: .init(x: currentProgressWidth + progressX, y: progressY + lineWidth / 2), radius: 3, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true) + context.addPath(path.cgPath) + context.setFillColor(currentProgress.cgColor) + context.fillPath() + } + + + } + +} + +extension SRProgressView { + @objc func handlePanGesture(sender: UIPanGestureRecognizer) { + + switch sender.state { + case .began: + self.isPaning = true + self.tempProgress = self.progress + sender.setTranslation(CGPoint(x: 0, y: 0), in: self) + self.panStart?() + + case .changed: + let point = sender.translation(in: self) + let offsetX = point.x / (self.width - self.insets.left - self.insets.right) + self.panProgress = self.tempProgress + offsetX + if self.panProgress < 0 { + self.panProgress = 0 + } + self.panChange?(self.panProgress) + setNeedsDisplay() + + default: + self.isPaning = false + self.panFinish?(self.panProgress) + + self.panProgress = 0 + } + } + + @objc func handleTapGesture(sender: UITapGestureRecognizer) { + let point = sender.location(in: self) + let offsetX = (point.x - self.insets.left) / (self.width - self.insets.left - self.insets.right) + self.panFinish?(offsetX) + } +} diff --git a/SynthReel/Class/Player/VM/V/SRRecommendPlayerCell.swift b/SynthReel/Class/Player/VM/V/SRRecommendPlayerCell.swift new file mode 100644 index 0000000..3b77847 --- /dev/null +++ b/SynthReel/Class/Player/VM/V/SRRecommendPlayerCell.swift @@ -0,0 +1,26 @@ +// +// SRRecommendPlayerCell.swift +// SynthReel +// +// Created by 湖北秦九 on 2025/11/20. +// Copyright © 2025 SR. All rights reserved. +// + +import UIKit +import JXPlayer + +class SRRecommendPlayerCell: JXPlayerListCell { + + override var ControlViewClass: JXPlayerListControlView.Type { + return SRRecommendPlayerControlView.self + } + + override var model: Any? { + didSet { + let model = self.model as? SRShortModel + let videoInfo = model?.video_info + self.player.setPlayUrl(url: videoInfo?.video_url ?? "") + self.player.coverImageView?.sr_setImage(model?.image_url) + } + } +} diff --git a/SynthReel/Class/Player/VM/V/SRRecommendPlayerControlView.swift b/SynthReel/Class/Player/VM/V/SRRecommendPlayerControlView.swift new file mode 100644 index 0000000..3eec91e --- /dev/null +++ b/SynthReel/Class/Player/VM/V/SRRecommendPlayerControlView.swift @@ -0,0 +1,226 @@ +// +// SRRecommendPlayerControlView.swift +// SynthReel +// +// Created by 湖北秦九 on 2025/11/20. +// Copyright © 2025 SR. All rights reserved. +// + +import UIKit +import JXPlayer +import SnapKit +import YYCategories + +class SRRecommendPlayerControlView: JXPlayerListControlView { + + override var viewModel: JXPlayerListViewModel? { + didSet { + self.viewModel?.addObserver(self, forKeyPath: "isPlaying", context: nil) + } + } + + override var model: Any? { + didSet { + let model = model as! SRShortModel + + shortNameLabel.text = model.name + + stackView.sr_removeAllArrangedSubview() + if let text = model.category?.first, text.count > 0 { + categoryLabel.text = "#" + text + stackView.addArrangedSubview(categoryLabel) + } + + if let text = model.sr_description, text.count > 0 { + desLabel.text = text + stackView.addArrangedSubview(desLabel) + } + + } + } + + override var isCurrent: Bool { + didSet { + updatePlayerViewStatus() + } + } + + lazy var controlerView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "short_progress_bg_image")) + imageView.isUserInteractionEnabled = true + return imageView + }() + + lazy var shortNameLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .semibold) + label.textColor = .srBlue + return label + }() + + lazy var stackView: UIStackView = { + let view = UIStackView() + view.axis = .vertical + view.spacing = 8 + return view + }() + + lazy var categoryLabel: SRLabel = { + let label = SRLabel() + label.font = .font(ofSize: 11, weight: .regular) + label.textColors = [UIColor.srGreen.cgColor, UIColor.srBlue.cgColor] + label.textStartPoint = .init(x: 0.5, y: 0) + label.textEndPoint = .init(x: 0.5, y: 1) + return label + }() + + lazy var desLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 11, weight: .regular) + label.textColor = .A_6_A_6_A_6 + label.numberOfLines = 2 + return label + }() + + lazy var epBgView: UIView = { + let view = SRGradientView() + view.colors = [UIColor._51_D_4_FF.withAlphaComponent(0.5).cgColor, UIColor._4_CFFD_4.withAlphaComponent(0.1).cgColor] + view.startPoint = .init(x: 0, y: 0.5) + view.endPoint = .init(x: 1, y: 0.5) + view.layer.cornerRadius = 2 + view.layer.masksToBounds = true + let tap = UITapGestureRecognizer { [weak self] _ in + guard let self = self else { return } + let vc = SRDetailPlayerViewController() + vc.shortId = (self.model as? SRShortModel)?.short_play_id + self.viewController?.navigationController?.pushViewController(vc, animated: true) + } + view.addGestureRecognizer(tap) + return view + }() + + lazy var epIconImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "ep_icon_02")) + imageView.setContentHuggingPriority(.required, for: .horizontal) + imageView.setContentCompressionResistancePriority(.required, for: .horizontal) + return imageView + }() + + lazy var epTextLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 12, weight: .regular) + label.textColor = .white + label.text = "recommend_ep_text".localized + return label + }() + + lazy var indicatorImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "arrow_right_icon_02")) + imageView.setContentHuggingPriority(.required, for: .horizontal) + imageView.setContentCompressionResistancePriority(.required, for: .horizontal) + return imageView + }() + + lazy var playerImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "play_icon_02")) + imageView.isHidden = true + return imageView + }() + + deinit { + self.viewModel?.removeObserver(self, forKeyPath: "isPlaying") + } + + override init(frame: CGRect) { + super.init(frame: frame) + sr_setupUI() + + let tap = UITapGestureRecognizer { [weak self] _ in + guard let self = self else { return } + self.viewModel?.userSwitchPlayAndPause() + } + self.addGestureRecognizer(tap) + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == "isPlaying" { + updatePlayerViewStatus() + } + } + + + func updatePlayerViewStatus() { + if self.viewModel?.isPlaying == true || !isCurrent { + playerImageView.isHidden = true + } else { + playerImageView.isHidden = false + } + + } + +} + +extension SRRecommendPlayerControlView { + + private func sr_setupUI() { + addSubview(controlerView) + controlerView.addSubview(shortNameLabel) + controlerView.addSubview(stackView) + controlerView.addSubview(epBgView) + epBgView.addSubview(epIconImageView) + epBgView.addSubview(epTextLabel) + epBgView.addSubview(indicatorImageView) + addSubview(playerImageView) + + controlerView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(15) + make.centerX.equalToSuperview() + make.bottom.equalToSuperview().offset(-10) + } + + shortNameLabel.snp.makeConstraints { make in + make.left.equalToSuperview().offset(12) + make.right.lessThanOrEqualToSuperview().offset(-12) + make.top.equalToSuperview().offset(13) + } + + stackView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(12) + make.right.lessThanOrEqualToSuperview().offset(-12) + make.top.equalTo(shortNameLabel.snp.bottom).offset(8) + make.bottom.equalToSuperview().offset(-52) + } + + epBgView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(12) + make.right.equalToSuperview().offset(-12) + make.bottom.equalToSuperview().offset(-18) + make.height.equalTo(26) + } + + epIconImageView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalToSuperview().offset(8) + } + + epTextLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalTo(epIconImageView.snp.right).offset(4) + make.right.lessThanOrEqualTo(self.indicatorImageView.snp.left).offset(-5) + } + + indicatorImageView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.right.equalToSuperview().offset(-12) + } + + playerImageView.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } + +} diff --git a/SynthReel/Class/Player/VM/V/SRShortDetailControlView.swift b/SynthReel/Class/Player/VM/V/SRShortDetailControlView.swift new file mode 100644 index 0000000..d2f0f9b --- /dev/null +++ b/SynthReel/Class/Player/VM/V/SRShortDetailControlView.swift @@ -0,0 +1,240 @@ +// +// SRShortDetailControlView.swift +// SynthReel +// +// Created by 湖北秦九 on 2025/11/18. +// Copyright © 2025 SR. All rights reserved. +// + +import UIKit +import JXPlayer +import SnapKit + +class SRShortDetailControlView: JXPlayerListControlView { + + var sr_viewModel: SRShortPlayerViewModel? { + return self.viewModel as? SRShortPlayerViewModel + } + + override var viewModel: JXPlayerListViewModel? { + didSet { + self.viewModel?.addObserver(self, forKeyPath: "isPlaying", context: nil) + } + } + + var shortModel: SRShortModel? { + didSet { + titleLabel.text = shortModel?.name + collectButton.isSelected = shortModel?.is_collect == true + } + } + + override var durationTime: TimeInterval { + didSet { + updateProgress() + let (_, m, s) = Int(durationTime).formatTimeGroup() + totalTimeLabel.text = "\(m):\(s)" + } + } + + override var currentTime: TimeInterval { + didSet { + updateProgress() + let (_, m, s) = Int(currentTime).formatTimeGroup() + currentTimeLabel.text = "\(m):\(s)" + } + } + + override var isLoading: Bool { + didSet { + progressView.isLoading = isLoading + } + } + + lazy var progressBgView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "short_progress_bg_image")) + imageView.isUserInteractionEnabled = true + return imageView + }() + + lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .medium) + label.textColor = .srBlue + return label + }() + + lazy var progressView: SRProgressView = { + let view = SRProgressView() + view.insets = .init(top: 10, left: 5, bottom: 10, right: 5) + view.panFinish = { [weak self] progress in + guard let self = self else { return } + self.viewModel?.seekTo(Float(progress)) + } + return view + }() + + lazy var totalTimeLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 10, weight: .regular) + label.textColor = .DFDFDF + label.text = "00:00" + return label + }() + + lazy var currentTimeLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 10, weight: .regular) + label.textColor = .DFDFDF + label.text = "00:00" + return label + }() + + lazy var epButton: UIButton = { + let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in + guard let self = self else { return } + self.sr_viewModel?.onEpSelectorView() + })) + button.setImage(UIImage(named: "ep_icon_01"), for: .normal) + return button + }() + + lazy var collectButton: UIButton = { + let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in + guard let self = self else { return } + guard let shortId = self.shortModel?.short_play_id else { return } + let videoId = (self.model as? SRVideoInfoModel)?.short_play_video_id + let isCollect = !(self.shortModel?.is_collect ?? false) + + Task { + await SRShortApi.requestShortCollect(shortId: shortId, videoId: videoId, isCollect: isCollect) + } + })) + button.setImage(UIImage(named: "collect_icon_01"), for: .normal) + button.setImage(UIImage(named: "collect_icon_01_selected"), for: .selected) + button.setImage(UIImage(named: "collect_icon_01_selected"), for: [.selected, .highlighted]) + return button + }() + + + lazy var playerImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "play_icon_02")) + imageView.isHidden = true + return imageView + }() + + deinit { + self.viewModel?.removeObserver(self, forKeyPath: "isPlaying") + NotificationCenter.default.removeObserver(self) + } + + override init(frame: CGRect) { + super.init(frame: frame) + NotificationCenter.default.addObserver(self, selector: #selector(updateShortCollectStateNotification), name: SRShortApi.updateShortCollectStateNotification, object: nil) + + let tap = UITapGestureRecognizer { [weak self] _ in + guard let self = self else { return } + self.viewModel?.userSwitchPlayAndPause() + } + self.addGestureRecognizer(tap) + + sr_setupUI() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func updateShortCollectStateNotification(sender: Notification) { + guard let userInfo = sender.userInfo else { return } + guard let shortId = userInfo["id"] as? String else { return } + guard let state = userInfo["state"] as? Bool else { return } + guard shortId == self.shortModel?.short_play_id else { return } + self.shortModel?.is_collect = state + + collectButton.isSelected = state + } + + private func updateProgress() { + guard durationTime > 0 else { + progressView.progress = 0 + return + } + progressView.progress = currentTime / durationTime + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == "isPlaying" { + updatePlayerViewStatus() + } + } + + + func updatePlayerViewStatus() { + if self.viewModel?.isPlaying == true || !isCurrent { + playerImageView.isHidden = true + } else { + playerImageView.isHidden = false + } + + } + +} + +extension SRShortDetailControlView { + + private func sr_setupUI() { + addSubview(progressBgView) + progressBgView.addSubview(titleLabel) + progressBgView.addSubview(progressView) + progressBgView.addSubview(totalTimeLabel) + progressBgView.addSubview(currentTimeLabel) + addSubview(epButton) + addSubview(collectButton) + addSubview(playerImageView) + + progressBgView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(15) + make.centerX.equalToSuperview() + make.bottom.equalToSuperview().offset(-(UIScreen.safeBottom + 5)) + make.height.equalTo(88) + } + + titleLabel.snp.makeConstraints { make in + make.centerY.equalTo(progressBgView.snp.top).offset(23) + make.left.equalToSuperview().offset(9) + make.right.lessThanOrEqualToSuperview().offset(-9) + } + + progressView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(4) + make.right.equalToSuperview().offset(-6) + make.bottom.equalToSuperview().offset(-30) + } + + totalTimeLabel.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-11) + make.bottom.equalToSuperview().offset(-24) + } + + currentTimeLabel.snp.makeConstraints { make in + make.left.equalToSuperview().offset(9) + make.bottom.equalToSuperview().offset(-24) + } + + epButton.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-15) + make.bottom.equalTo(progressBgView.snp.top).offset(-44) + } + + collectButton.snp.makeConstraints { make in + make.centerX.equalTo(epButton) + make.bottom.equalTo(epButton.snp.top).offset(-25) + } + + playerImageView.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } + +} diff --git a/SynthReel/Class/Player/VM/V/SRShortDetailPlayerCell.swift b/SynthReel/Class/Player/VM/V/SRShortDetailPlayerCell.swift new file mode 100644 index 0000000..c9e553e --- /dev/null +++ b/SynthReel/Class/Player/VM/V/SRShortDetailPlayerCell.swift @@ -0,0 +1,88 @@ +// +// SRShortDetailPlayerCell.swift +// SynthReel +// +// Created by 湖北秦九 on 2025/11/18. +// Copyright © 2025 SR. All rights reserved. +// + +import UIKit +import JXPlayer + +class SRShortDetailPlayerCell: JXPlayerListCell { + + override var ControlViewClass: JXPlayerListControlView.Type { + return SRShortDetailControlView.self + } + + var sr_controlView: SRShortDetailControlView { + return self.controlView as! SRShortDetailControlView + } + + var sr_viewModel: SRShortPlayerViewModel? { + return self.viewModel as? SRShortPlayerViewModel + } + + var hasLastEpisodeUnlocked: Bool = false { + didSet { + self.lockView.hasLastEpisodeUnlocked = hasLastEpisodeUnlocked + } + } + + private lazy var lockView: SRVideoLockView = { + let view = SRVideoLockView() + view.clickUnlockButton = { [weak self] in + Task { + await self?.sr_viewModel?.handleUnlockVideo() + } + } + + view.adUnlockButton = { [weak self] in + Task { + await self?.sr_viewModel?.handleAdUnlockVideo() + } + } + return view + }() + + override var model: Any? { + didSet { + let model = self.model as? SRVideoInfoModel + self.player.setPlayUrl(url: model?.video_url ?? "") + + self.lockView.isHidden = !(model?.is_lock ?? true) + lockView.videoInfo = model + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + sr_setupLayout() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var shortModel: SRShortModel? { + didSet { + self.sr_controlView.shortModel = shortModel + self.player.coverImageView?.sr_setImage(shortModel?.image_url) + } + } + +} + + +extension SRShortDetailPlayerCell { + + private func sr_setupLayout() { + addSubview(lockView) + + lockView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + +} diff --git a/SynthReel/Class/Player/VM/V/SRVideoLockView.swift b/SynthReel/Class/Player/VM/V/SRVideoLockView.swift new file mode 100644 index 0000000..c90f6d2 --- /dev/null +++ b/SynthReel/Class/Player/VM/V/SRVideoLockView.swift @@ -0,0 +1,120 @@ +// +// SRVideoLockView.swift +// SynthReel +// +// Created by CSGY on 2025/12/1. +// Copyright © 2025 SR. All rights reserved. +// + +import UIKit + +class SRVideoLockView: UIView { + + var clickUnlockButton: (() -> Void)? + + var adUnlockButton: (() -> Void)? + + var videoInfo: SRVideoInfoModel? { + didSet { + unlockButton.setNeedsUpdateConfiguration() + } + } + + var hasLastEpisodeUnlocked = false { + didSet { + unlockButton.setNeedsUpdateConfiguration() + } + } + + lazy var unlockStackView: UIStackView = { + let stack = UIStackView() + stack.axis = .vertical + stack.spacing = 12 + stack.distribution = .fillEqually + return stack + }() + + private lazy var unlockButton: UIButton = { + var config = UIButton.Configuration.plain() + config.image = UIImage(named: "lock") + config.imagePadding = 6 + // 设置背景图片(使用 UIImage 作为背景) + config.background.image = UIImage(named: "unlockButtonBg") + config.background.imageContentMode = .scaleToFill // 让背景图铺满按钮 + + let button = UIButton(configuration: config, primaryAction: UIAction(handler: { [weak self] _ in + guard let self = self else { return } + self.clickUnlockButton?() + })) + button.configurationUpdateHandler = { [weak self] button in + guard let self = self else { return } + let attributeContainer = AttributeContainer([ + .font : UIFont.font(ofSize: 14, weight: .medium), + .foregroundColor : UIColor._51_D_4_FF + ]) + if hasLastEpisodeUnlocked { + button.configuration?.attributedTitle = .init("video_lock_tip_text".localized, attributes: attributeContainer) + } else { + button.configuration?.attributedTitle = .init("synthreel_unlocking_coins_notice".localizedReplace(text: "\(videoInfo?.coins ?? 0)"), attributes: attributeContainer) + } + } + return button + }() + + private lazy var adlockButton: UIButton = { + var config = UIButton.Configuration.plain() + config.image = UIImage(named: "adlock") + config.imagePadding = 6 + // 设置背景图片(使用 UIImage 作为背景) + config.background.image = UIImage(named: "unlockButtonBg") + config.background.imageContentMode = .scaleToFill // 让背景图铺满按钮 + let attr = AttributeContainer([ + .font: UIFont.font(ofSize: 14, weight: .medium), + .foregroundColor: UIColor.white + ]) + config.attributedTitle = AttributedString("Watch 2ads to unlock".localized, attributes: attr) + + let button = UIButton(configuration: config, primaryAction: UIAction(handler: { [weak self] _ in + guard let self = self else { return } + self.adUnlockButton?() + })) + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = ._000000.withAlphaComponent(0.6) + sr_setupLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +extension SRVideoLockView { + + private func sr_setupLayout() { + addSubview(unlockStackView) + + unlockStackView.addArrangedSubview(unlockButton) + unlockStackView.addArrangedSubview(adlockButton) + + + unlockStackView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(45) + make.right.equalToSuperview().offset(-45) + make.height.equalTo(43 * 2 + 30) // 两个按钮 + 间距 + make.centerY.equalToSuperview() // 可自定义 + } + + unlockButton.snp.makeConstraints { make in + make.height.equalTo(43) + } + + adlockButton.snp.makeConstraints { make in + make.height.equalTo(43) + } + } +} diff --git a/SynthReel/Class/Player/VM/V/SRVideoRechargeView.swift b/SynthReel/Class/Player/VM/V/SRVideoRechargeView.swift new file mode 100644 index 0000000..9730eb9 --- /dev/null +++ b/SynthReel/Class/Player/VM/V/SRVideoRechargeView.swift @@ -0,0 +1,320 @@ +// +// SRVideoRechargeView.swift +// SynthReel +// +// Created by CSGY on 2025/12/5. +// Copyright © 2025 SR. All rights reserved. +// + +import UIKit + +class SRVideoRechargeView: SRPanModalContentView { + + var buyFinishHandle: (() -> Void)? + var didDismissHandle: (() -> Void)? + + var model: SRPayDateModel? { + didSet { + self.stackView.sr_removeAllArrangedSubview() + self.vipView.dataArr = model?.list_sub_vip ?? [] + self.coinsView.setDataArr(model?.list_coins ?? []) + + if let sort = model?.sort, sort.count > 0 { + sort.forEach { + if $0 == .vip, model?.list_sub_vip?.isEmpty == false { + self.stackView.addArrangedSubview(self.vipView) + } else if $0 == .coin, model?.list_coins?.isEmpty == false { + self.stackView.addArrangedSubview(self.coinsView) + } + } + } else { + if model?.list_sub_vip?.isEmpty == false { + self.stackView.addArrangedSubview(self.vipView) + } + if model?.list_coins?.isEmpty == false { + self.stackView.addArrangedSubview(self.coinsView) + } + } + + self.stackView.addArrangedSubview(self.tipView) + + self.setNeedsLayoutUpdate() + } + } + + var videoInfo: SRVideoInfoModel? { + didSet { + self.coinsView.videoId = videoInfo?.short_play_video_id + self.coinsView.shortPlayId = videoInfo?.short_play_id + self.vipView.videoId = videoInfo?.short_play_video_id + self.vipView.shortPlayId = videoInfo?.short_play_id + videoCoinsView.coins = videoInfo?.coins ?? 0 + + self.requestRestore() + } + } + + private lazy var closeButton: UIButton = { + let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in + guard let self = self else { return } +// FAStatAPI.requestEventStat(orderCode: nil, shortPlayId: self.videoInfo?.short_play_id, videoId: self.videoInfo?.short_play_video_id, eventKey: .payTemplateDialog, errorMsg: nil, otherParamenters: [ +// "event_name" : "pay cancel" +// ]) + Task { + await self.dismiss(animated: true) + } + self.didDismissHandle?() + })) + button.setImage(UIImage(named: "Close"), for: .normal) + return button + }() + + private lazy var scrollView: SRScrollView = { + let scrollView = SRScrollView() +// scrollView.clipsToBounds = false + return scrollView + }() + + private lazy var stackView: UIStackView = { + let view = UIStackView() + view.axis = .vertical + view.spacing = 18 + return view + }() + + private lazy var coinsView: SRStoreCoinsView = { + let view = SRStoreCoinsView() + view.buyFinishHandle = { [weak self] in + self?.buyFinishHandle?() + Task { + await self?.dismiss(animated: true) + } + } + return view + }() + + private lazy var vipView: SRStoreVipView = { + let view = SRStoreVipView() + view.buyFinishHandle = { [weak self] in + self?.buyFinishHandle?() + Task { + await self?.dismiss(animated: true) + } + } + return view + }() + + private lazy var videoCoinsView: SRVideoRechargeView.CoinsView = { + let view = SRVideoRechargeView.CoinsView() + view.title = "synthreel_price".localized + ":" + return view + }() + + private lazy var totalCoinsView: SRVideoRechargeView.CoinsView = { + let view = SRVideoRechargeView.CoinsView() + view.title = "synthreel_balance".localized + ":" + view.coins = SRLogin.manager.userInfo?.totalCoins ?? 0 + return view + }() + + private lazy var tipView: UIView = { + let view = UIView() + view.addSubview(tipTitleLabel) + view.addSubview(tipTextLabel) + + tipTitleLabel.snp.makeConstraints { make in + make.top.equalToSuperview().offset(7) + make.left.equalToSuperview().offset(16) + } + + tipTextLabel.snp.makeConstraints { make in + make.left.equalToSuperview().offset(16) + make.right.lessThanOrEqualToSuperview().offset(-16) + make.top.equalTo(tipTitleLabel.snp.bottom).offset(4) + make.bottom.equalToSuperview() + } + return view + }() + + private lazy var tipTitleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 12, weight: .medium) + label.textColor = .white + label.text = "synthreel_tips".localized + return label + }() + + private lazy var tipTextLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 10, weight: .regular) + label.textColor = .white + label.text = "store_tips".localized + label.numberOfLines = 0 + return label + }() + + deinit { + NotificationCenter.default.removeObserver(self) + } + + override init(frame: CGRect) { + super.init(frame: frame) + NotificationCenter.default.addObserver(self, selector: #selector(userInfoUpdateNotification), name: SRLogin.userInfoUpdateNotification, object: nil) + self.contentHeight = UIScreen.height - UIScreen.safeTop +// self.backgroundColor = ._000000.withAlphaComponent(0.6) + self.backgroundColor = .clear + self.mainScrollView = self.scrollView + fa_setupLayout() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func userInfoUpdateNotification() { + totalCoinsView.coins = SRLogin.manager.userInfo?.totalCoins ?? 0 + } + + override func present(in view: UIView?) { + super.present(in: view) +// self.hw_contentView.sr_addEffectView(style: .dark) +// self.hw_contentView.sr_setRoundedCorner(topLeft: 24, topRight: 24, bottomLeft: 0, bottomRight: 0) + } + + override func allowsTapBackgroundToDismiss() -> Bool { + return false + } +} + +extension SRVideoRechargeView { + + private func fa_setupLayout() { + addSubview(closeButton) + addSubview(videoCoinsView) + addSubview(totalCoinsView) +// addSubview(titleLabel) + addSubview(scrollView) + scrollView.addSubview(stackView) + + closeButton.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-16) + make.top.equalToSuperview().offset(12) + } + + videoCoinsView.snp.makeConstraints { make in + make.centerY.equalTo(closeButton) + make.left.equalToSuperview().offset(16) + } + + totalCoinsView.snp.makeConstraints { make in + make.centerY.equalTo(closeButton) + make.left.equalTo(videoCoinsView.snp.right).offset(15) + } + +// titleLabel.snp.makeConstraints { make in +// make.left.equalToSuperview().offset(16) +// make.top.equalToSuperview().offset(39) +// } + + scrollView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.top.equalToSuperview().offset(74) + make.bottom.equalToSuperview() + } + + stackView.snp.makeConstraints { make in + make.left.centerX.equalToSuperview() + make.top.equalToSuperview() + make.bottom.equalToSuperview().offset(-(UIScreen.safeBottom + 10)) + } + } + +} + +extension SRVideoRechargeView { + + @objc private func requestRestore() { + guard let shortPlayId = self.videoInfo?.short_play_id, let videoId = self.videoInfo?.short_play_video_id else { return } + + SRIapManager.manager.restore(isLoding: false, shortPlayId: shortPlayId, videoId: videoId) { [weak self] isFinish, buyType in + if isFinish { + Task { + await SRLogin.manager.requestUserInfo(completer: nil) + self?.buyFinishHandle?() + await self?.dismiss(animated: true) + } + } + } + } + +} + + +extension SRVideoRechargeView { + + class CoinsView: UIView { + var title: String? { + didSet { + titleLabel.text = title + } + } + + var coins: Int = 0 { + didSet { + coinsLabel.text = "\(coins)" + } + } + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .regular) + label.textColor = .white + return label + }() + + private lazy var iconImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "bigCoins")) + return imageView + }() + + private lazy var coinsLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .bold) + label.textColor = .white + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + addSubview(titleLabel) + addSubview(iconImageView) + addSubview(coinsLabel) + + titleLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.bottom.lessThanOrEqualToSuperview() + make.left.equalToSuperview() + } + + iconImageView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.bottom.lessThanOrEqualToSuperview() + make.left.equalTo(titleLabel.snp.right).offset(6) + } + + coinsLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.bottom.lessThanOrEqualToSuperview() + make.left.equalTo(iconImageView.snp.right).offset(4) + make.right.equalToSuperview() + } + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + } + +} diff --git a/SynthReel/Class/Player/VM/V/SRVipRetainAlert.swift b/SynthReel/Class/Player/VM/V/SRVipRetainAlert.swift new file mode 100644 index 0000000..060a167 --- /dev/null +++ b/SynthReel/Class/Player/VM/V/SRVipRetainAlert.swift @@ -0,0 +1,313 @@ +// +// SRVipRetainAlert.swift +// SynthReel +// +// Created by 澜声世纪 on 2025/12/8. +// Copyright © 2025 SR. All rights reserved. +// + +import UIKit + +class SRVipRetainAlert: SRBaseAlert { + + var buyFinishHandle: (() -> Void)? + + var model: SRPayAlertModel? { + didSet { + let payItem = model?.info + titleView.text = payItem?.getVipTitle() + itemView.payItem = payItem + } + } + + var videoInfo: SRVideoInfoModel? + + + private lazy var titleView: SRLabel = { + let label = SRLabel() + label.font = .font(ofSize: 24, weight: .init(900)) + label.text = "fableo_weekly_refill".localized.uppercased() + label.textColors = [UIColor._4_CFFD_4.cgColor, UIColor._51_D_4_FF.cgColor] + label.textStartPoint = .init(x: 0.5, y: 0) + label.textEndPoint = .init(x: 0.5, y: 1) + return label + }() + + private lazy var itemView: ItemView = { + let view = ItemView() + return view + }() + + private lazy var textLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 18, weight: .bold).withBoldItalic() + label.textColor = .white + label.text = "vip_retain_alert_text".localized + label.textAlignment = .center + label.numberOfLines = 0 + return label + }() + + private lazy var buyButton: UIButton = { + let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in + guard let self = self else { return } + guard let payItem = self.model?.info else { return } + SRIapManager.manager.start(model: payItem, shortPlayId: self.videoInfo?.short_play_id, videoId: self.videoInfo?.short_play_video_id) { [weak self] finish in + guard let self = self else { return } + if finish { + Task { + await SRAccountManager.manager.updateUserInfo() + self.dismiss() + self.buyFinishHandle?() + } + } + } + + })) + + button.setTitle("synthreel_buy_now".localized, for: .normal) + button.setTitleColor(.white, for: .normal) + button.titleLabel?.font = .font(ofSize: 14, weight: .bold) + button.setBackgroundImage(.vipRetainBg, for: .normal) + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + contentWidth = UIScreen.width - 55 + contentView.backgroundColor = .clear + + fa_setupLayout() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +extension SRVipRetainAlert { + + private func fa_setupLayout() { + contentView.addSubview(titleView) + contentView.addSubview(itemView) + contentView.addSubview(textLabel) + contentView.addSubview(buyButton) + + titleView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.centerX.equalToSuperview() + } + + itemView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.top.equalTo(titleView.snp.bottom).offset(12) + make.height.equalTo(84) + } + + textLabel.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.right.lessThanOrEqualToSuperview().offset(-10) + make.top.equalTo(itemView.snp.bottom).offset(12) + } + + buyButton.snp.makeConstraints { make in + make.left.equalToSuperview().offset(20) + make.centerX.equalToSuperview() + make.top.equalTo(textLabel.snp.bottom).offset(12) + make.height.equalTo(48) + make.bottom.equalToSuperview() + } + + } + +} + +extension SRVipRetainAlert { + + + + class ItemView: UIView { + + var payItem: SRPayItem? { + didSet { + nameLabel.text = payItem?.getVipTitle() + textLabel.text = "vip_tip_01".localized + + priceView.setNeedsUpdateConfiguration() + } + } + + private lazy var bgView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "coinPackCell")) + return imageView + }() +// private lazy var bgView: FAGradientView = { +// let view = FAGradientView() +// view.fa_colors = [UIColor._524_B_8_E.cgColor, UIColor._303265.cgColor] +// view.fa_locations = [0, 1] +// view.fa_startPoint = .init(x: 0, y: 0.5) +// view.fa_endPoint = .init(x: 1, y: 0.5) +// view.layer.cornerRadius = 12 +// view.layer.masksToBounds = true +// view.layer.borderWidth = 1 +// view.layer.borderColor = UIColor.E_5_E_5_E_5.cgColor +// return view +// }() + + private lazy var bgIconImageView1: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "coin_attachment_01")) + return imageView + }() + + private lazy var bgIconImageView2: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "coin_attachment_02")) + return imageView + }() + private lazy var bgIconImageView3 = UIImageView(image: UIImage(named: "coin_attachment_04")) + private lazy var bgIconImageView4 = UIImageView(image: UIImage(named: "coin_attachment_05")) + + private lazy var vipIconImageView = UIImageView(image: UIImage(named: "皇冠-金")) + + private lazy var nameLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 18, weight: .bold) + label.textColor = .white + return label + }() + + private lazy var textLabel: UILabel = { + let label = SRLabel() + label.textColor = .white + label.font = .font(ofSize: 12, weight: .regular) + return label + }() + + private lazy var priceView: UIButton = { + var config = UIButton.Configuration.plain() + config.titleAlignment = .center + config.titlePadding = 0 + config.contentInsets = .init(top: 0, leading: 10, bottom: 0, trailing: 10) + config.background.image = .coinStackBg + let button = UIButton(configuration: config) + button.isUserInteractionEnabled = false + button.configurationUpdateHandler = { [weak self] button in + guard let self = self else { return } + + let currency = self.payItem?.currency ?? "" + let timeText = payItem?.getTimeString() ?? "" + let oldPrice = self.payItem?.price ?? "" + var discountPrice: String? = nil + + if self.payItem?.discount_type == 1, let introductoryPrice = self.payItem?.introductionaryOffer { + discountPrice = introductoryPrice.price.stringValue + } else if self.payItem?.discount_type == 2, let discount = self.payItem?.promotionalOffers?.first { + discountPrice = discount.price.stringValue + } + + if let discountPrice = discountPrice { + + let priceString = AttributedString("\(currency)\(discountPrice)", attributes: AttributeContainer([ + .font : UIFont.font(ofSize: 18, weight: .bold), + .foregroundColor : UIColor(hexString: "#FFE600") + ])) + + + button.configuration?.attributedTitle = priceString + + var subtitle = AttributedString("\(currency)\(oldPrice)", attributes: AttributeContainer([ + .font : UIFont.font(ofSize: 12, weight: .regular), + .foregroundColor : UIColor.white.withAlphaComponent(0.05), + .strikethroughStyle: NSUnderlineStyle.double.rawValue, + .strikethroughColor: UIColor.white.withAlphaComponent(0.05) + ])) + + button.configuration?.attributedSubtitle = subtitle + + } else { + + button.configuration?.attributedTitle = AttributedString("\(currency)\(oldPrice)", attributes: AttributeContainer([ + .font : UIFont.font(ofSize: 18, weight: .bold), + .foregroundColor : UIColor(hexString: "#FFE600") + ])) + + button.configuration?.attributedSubtitle = AttributedString("/\(timeText)", attributes: AttributeContainer([ + .font : UIFont.font(ofSize: 12, weight: .regular), + .foregroundColor : UIColor.black.withAlphaComponent(0.5) + ])) + } + + } + + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + nameLabel.text = "Weekly VIP" + + + addSubview(bgView) + bgView.addSubview(bgIconImageView4) + bgView.addSubview(bgIconImageView3) + bgView.addSubview(bgIconImageView1) + bgView.addSubview(bgIconImageView2) + bgView.addSubview(vipIconImageView) + bgView.addSubview(nameLabel) + bgView.addSubview(textLabel) + bgView.addSubview(priceView) + + bgView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + bgIconImageView1.snp.makeConstraints { make in + make.left.equalToSuperview().offset(0) + make.top.equalToSuperview().offset(0) + } + + bgIconImageView2.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-26) + make.centerY.equalToSuperview() + } + + bgIconImageView3.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.centerX.equalTo(bgIconImageView2) + } + + bgIconImageView4.snp.makeConstraints { make in + make.left.bottom.equalToSuperview() + } + + vipIconImageView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(16) + make.top.equalToSuperview().offset(18) + } + + nameLabel.snp.makeConstraints { make in + make.centerY.equalTo(vipIconImageView) + make.left.equalTo(vipIconImageView.snp.right).offset(4) + } + + textLabel.snp.makeConstraints { make in + make.left.equalTo(vipIconImageView) + make.bottom.equalToSuperview().offset(-18) + } + + priceView.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-12) + make.height.equalTo(48) + make.bottom.equalTo(-8) + make.width.greaterThanOrEqualTo(88) + } + + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + } + +} diff --git a/SynthReel/Libs/SRAppstore/SRPayDataRequest.swift b/SynthReel/Libs/SRAppstore/SRPayDataRequest.swift index 961c538..411ca8b 100644 --- a/SynthReel/Libs/SRAppstore/SRPayDataRequest.swift +++ b/SynthReel/Libs/SRAppstore/SRPayDataRequest.swift @@ -58,6 +58,26 @@ class SRPayDataRequest: NSObject { } } + + ///挽留信息 + func requestVipRetainPayInfo(completer: ((_ model: SRPayAlertModel?) -> Void)?) { + self.completerBlock = nil + self.payAlertBlock = completer + + Task { + guard let model = await SRStoreAPI.requestVipRetainPayInfo() else { + self.payAlertBlock?(nil) + return + } + self.payAlertModel = model + let productId = SRIapManager.manager.getProductId(templateId: model.info?.ios_template_id) ?? "" + + let set = Set([productId]) + let productsRequest = SKProductsRequest(productIdentifiers: set) + productsRequest.delegate = self + productsRequest.start() + } + } } diff --git a/SynthReel/Source/Assets.xcassets/myShort/vipRetainBg.imageset/Contents.json b/SynthReel/Source/Assets.xcassets/myShort/vipRetainBg.imageset/Contents.json new file mode 100644 index 0000000..c629f58 --- /dev/null +++ b/SynthReel/Source/Assets.xcassets/myShort/vipRetainBg.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "vipRetainBg@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "vipRetainBg@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SynthReel/Source/Assets.xcassets/myShort/vipRetainBg.imageset/vipRetainBg@2x.png b/SynthReel/Source/Assets.xcassets/myShort/vipRetainBg.imageset/vipRetainBg@2x.png new file mode 100644 index 0000000..e73c391 Binary files /dev/null and b/SynthReel/Source/Assets.xcassets/myShort/vipRetainBg.imageset/vipRetainBg@2x.png differ diff --git a/SynthReel/Source/Assets.xcassets/myShort/vipRetainBg.imageset/vipRetainBg@3x.png b/SynthReel/Source/Assets.xcassets/myShort/vipRetainBg.imageset/vipRetainBg@3x.png new file mode 100644 index 0000000..c331dd7 Binary files /dev/null and b/SynthReel/Source/Assets.xcassets/myShort/vipRetainBg.imageset/vipRetainBg@3x.png differ diff --git a/SynthReel/Source/en.lproj/Localizable.strings b/SynthReel/Source/en.lproj/Localizable.strings index af0926b..a54326b 100644 --- a/SynthReel/Source/en.lproj/Localizable.strings +++ b/SynthReel/Source/en.lproj/Localizable.strings @@ -84,6 +84,7 @@ "synthreel_dailu_bonuses" = "Daily Bonuses"; "synthreel_coin_bag_buy_tip_title" = "How Do I Receive Coins?"; "synthreel_continue" = "Claim Now"; +"vip_retain_alert_text" = "Unlock every show you love!"; "coins_pack_tips" = "1.Coins are delivered instantly upon purchase.
2.Daily bonus coins available from the next day.
3.All coins will be revoked when the subscription expires, including both initial and daily coins."; @@ -91,7 +92,7 @@ "synthreel_balance" = "balance"; "synthreel_continue" = "Claim Now"; "synthreel_success" = "success"; - +"synthreel_buy_now" = "Buy Now"; "pay_error_1" = "You are already a member!"; "pay_error_2" = "Invalid in-app purchase"; "pay_error_3" = "Payment has been cancelled";