1.vip挽留
This commit is contained in:
parent
50d1cd9069
commit
41b7bc80c4
6
Podfile
6
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
|
||||
|
||||
@ -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 */,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
79
SynthReel/Class/Player/VM/V/SRDetailRecommendCell.swift
Normal file
79
SynthReel/Class/Player/VM/V/SRDetailRecommendCell.swift
Normal 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()
|
||||
}
|
||||
|
||||
}
|
||||
175
SynthReel/Class/Player/VM/V/SRDetailRecommendview.swift
Normal file
175
SynthReel/Class/Player/VM/V/SRDetailRecommendview.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
99
SynthReel/Class/Player/VM/V/SREpSelectorCell.swift
Normal file
99
SynthReel/Class/Player/VM/V/SREpSelectorCell.swift
Normal 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")
|
||||
}
|
||||
|
||||
}
|
||||
211
SynthReel/Class/Player/VM/V/SREpSelectorView.swift
Normal file
211
SynthReel/Class/Player/VM/V/SREpSelectorView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
216
SynthReel/Class/Player/VM/V/SRProgressView.swift
Normal file
216
SynthReel/Class/Player/VM/V/SRProgressView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
26
SynthReel/Class/Player/VM/V/SRRecommendPlayerCell.swift
Normal file
26
SynthReel/Class/Player/VM/V/SRRecommendPlayerCell.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
226
SynthReel/Class/Player/VM/V/SRRecommendPlayerControlView.swift
Normal file
226
SynthReel/Class/Player/VM/V/SRRecommendPlayerControlView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
240
SynthReel/Class/Player/VM/V/SRShortDetailControlView.swift
Normal file
240
SynthReel/Class/Player/VM/V/SRShortDetailControlView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
88
SynthReel/Class/Player/VM/V/SRShortDetailPlayerCell.swift
Normal file
88
SynthReel/Class/Player/VM/V/SRShortDetailPlayerCell.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
120
SynthReel/Class/Player/VM/V/SRVideoLockView.swift
Normal file
120
SynthReel/Class/Player/VM/V/SRVideoLockView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
320
SynthReel/Class/Player/VM/V/SRVideoRechargeView.swift
Normal file
320
SynthReel/Class/Player/VM/V/SRVideoRechargeView.swift
Normal 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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
313
SynthReel/Class/Player/VM/V/SRVipRetainAlert.swift
Normal file
313
SynthReel/Class/Player/VM/V/SRVipRetainAlert.swift
Normal 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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
22
SynthReel/Source/Assets.xcassets/myShort/vipRetainBg.imageset/Contents.json
vendored
Normal file
22
SynthReel/Source/Assets.xcassets/myShort/vipRetainBg.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
SynthReel/Source/Assets.xcassets/myShort/vipRetainBg.imageset/vipRetainBg@2x.png
vendored
Normal file
BIN
SynthReel/Source/Assets.xcassets/myShort/vipRetainBg.imageset/vipRetainBg@2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
BIN
SynthReel/Source/Assets.xcassets/myShort/vipRetainBg.imageset/vipRetainBg@3x.png
vendored
Normal file
BIN
SynthReel/Source/Assets.xcassets/myShort/vipRetainBg.imageset/vipRetainBg@3x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
@ -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";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user