1.vip挽留

This commit is contained in:
澜声世纪 2025-12-08 17:06:30 +08:00
parent 50d1cd9069
commit 41b7bc80c4
21 changed files with 2198 additions and 3 deletions

View File

@ -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

View File

@ -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 = "<group>"; };
85ACDA2B2EE69CD90009B306 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
85ACDA2E2EE6B3760009B306 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
85ACDA302EE6C3640009B306 /* SRVipRetainAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SRVipRetainAlert.swift; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>";
@ -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 */,

View File

@ -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<SRPayAlertModel>) in
continuation.resume(returning: response.data)
}
}
}
}

View File

@ -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

View File

@ -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()
}
}

View File

@ -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)
}
}
}

View File

@ -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")
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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()
}
}
}

View File

@ -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()
}
}
}

View File

@ -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()
}
}
}

View File

@ -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)
}
}
}

View File

@ -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")
}
}
}

View File

@ -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")
}
}
}

View File

@ -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()
}
}
}

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View File

@ -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.<br>2.Daily bonus coins available from the next day.<br>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";