播放页面解锁功能开发

This commit is contained in:
zeng 2025-07-26 15:17:18 +08:00
parent c8681c3f1a
commit 688a992aab
39 changed files with 1028 additions and 21 deletions

View File

@ -172,6 +172,10 @@
F39855482E33928400E2D28D /* BRStoreCoinBigCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39855472E33928400E2D28D /* BRStoreCoinBigCell.swift */; };
F398554A2E33929C00E2D28D /* BRStoreCoinCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39855492E33929C00E2D28D /* BRStoreCoinCell.swift */; };
F398554C2E3392C200E2D28D /* BRStoreCoinSmallCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F398554B2E3392C200E2D28D /* BRStoreCoinSmallCell.swift */; };
F398554E2E34699F00E2D28D /* BRVideoLockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F398554D2E34699F00E2D28D /* BRVideoLockView.swift */; };
F39855502E34782200E2D28D /* BRVideoUnlockModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F398554F2E34782200E2D28D /* BRVideoUnlockModel.swift */; };
F39855522E347BDE00E2D28D /* BRVideoRechargeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39855512E347BDE00E2D28D /* BRVideoRechargeView.swift */; };
F39855542E34A49500E2D28D /* VPPayDataRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39855532E34A49500E2D28D /* VPPayDataRequest.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -355,6 +359,10 @@
F39855472E33928400E2D28D /* BRStoreCoinBigCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRStoreCoinBigCell.swift; sourceTree = "<group>"; };
F39855492E33929C00E2D28D /* BRStoreCoinCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRStoreCoinCell.swift; sourceTree = "<group>"; };
F398554B2E3392C200E2D28D /* BRStoreCoinSmallCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRStoreCoinSmallCell.swift; sourceTree = "<group>"; };
F398554D2E34699F00E2D28D /* BRVideoLockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRVideoLockView.swift; sourceTree = "<group>"; };
F398554F2E34782200E2D28D /* BRVideoUnlockModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRVideoUnlockModel.swift; sourceTree = "<group>"; };
F39855512E347BDE00E2D28D /* BRVideoRechargeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRVideoRechargeView.swift; sourceTree = "<group>"; };
F39855532E34A49500E2D28D /* VPPayDataRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPPayDataRequest.swift; sourceTree = "<group>"; };
F70FA1F4169364C4C53534CE /* Pods-BeeReel.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BeeReel.release.xcconfig"; path = "Target Support Files/Pods-BeeReel/Pods-BeeReel.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -816,6 +824,8 @@
BF02B7EA2E2E388800172177 /* BREpisodeMenuView.swift */,
BF02B7F02E2E55E300172177 /* BRRateSelectorView.swift */,
BF02B7F22E2E571600172177 /* BRRateSelectorCell.swift */,
F398554D2E34699F00E2D28D /* BRVideoLockView.swift */,
F39855512E347BDE00E2D28D /* BRVideoRechargeView.swift */,
);
path = View;
sourceTree = "<group>";
@ -827,6 +837,7 @@
BFC676802E122733006659E5 /* BRPlayerProtocol.swift */,
BFC676862E122E36006659E5 /* BRVideoDetailModel.swift */,
BF02B7EE2E2E4BFD00172177 /* BRRateModel.swift */,
F398554F2E34782200E2D28D /* BRVideoUnlockModel.swift */,
);
path = Model;
sourceTree = "<group>";
@ -1073,6 +1084,7 @@
isa = PBXGroup;
children = (
F398553D2E336D3000E2D28D /* BRPayDateModel.swift */,
F39855532E34A49500E2D28D /* VPPayDataRequest.swift */,
);
path = Model;
sourceTree = "<group>";
@ -1207,12 +1219,14 @@
BFC676B12E137D2F006659E5 /* BRPopularPicksViewController.swift in Sources */,
BF02B7FC2E2F262F00172177 /* BRGradientView.swift in Sources */,
BFC676692E0E34DA006659E5 /* BRUserAPI.swift in Sources */,
F398554E2E34699F00E2D28D /* BRVideoLockView.swift in Sources */,
BFC676782E0E9553006659E5 /* BRSpotlightMainBaseCell.swift in Sources */,
BFC676732E0E938B006659E5 /* BRTableView.swift in Sources */,
BFC676932E126A62006659E5 /* BRSpotlightNewMainCell.swift in Sources */,
BFC6768D2E123D6E006659E5 /* AttributedString+BRAdd.swift in Sources */,
BF02B8392E30B30400172177 /* AlignedCollectionViewFlowLayout.swift in Sources */,
BF3A56882E30E0DD009E5CF9 /* BREmpty.swift in Sources */,
F39855542E34A49500E2D28D /* VPPayDataRequest.swift in Sources */,
BF3338F52E1616B200B10F76 /* BRExploreControlView.swift in Sources */,
F398552D2E33126D00E2D28D /* BRMineCoinItemView.swift in Sources */,
BF692B132E0A7B9000A5C2DA /* BRUserInfo.swift in Sources */,
@ -1250,6 +1264,7 @@
BF692B782E0D3A1200A5C2DA /* BRHomeModuleItem.swift in Sources */,
BF692B5A2E0AAADD00A5C2DA /* BRPlayerListCell.swift in Sources */,
BF02B8312E30897700172177 /* BRSearchHomeView.swift in Sources */,
F39855502E34782200E2D28D /* BRVideoUnlockModel.swift in Sources */,
F398554A2E33929C00E2D28D /* BRStoreCoinCell.swift in Sources */,
BF3A568C2E30EBA2009E5CF9 /* BRHomePlayRecordButton.swift in Sources */,
BF692B162E0A7CD600A5C2DA /* BRHUD.swift in Sources */,
@ -1287,6 +1302,7 @@
F39855312E33620200E2D28D /* BRMineStoreCell.swift in Sources */,
BF692B182E0A7D8900A5C2DA /* BRToast.swift in Sources */,
BF692B0E2E0A7AF300A5C2DA /* UserDefaults+BRAdd.swift in Sources */,
F39855522E347BDE00E2D28D /* BRVideoRechargeView.swift in Sources */,
BF02B8082E2F616E00172177 /* BRFavoritesViewController.swift in Sources */,
BF3338FD2E1626B000B10F76 /* BRPlayerControlProtocol.swift in Sources */,
BF692B582E0AAA6F00A5C2DA /* UIScreen+BRAdd.swift in Sources */,

View File

@ -186,4 +186,8 @@ extension UIColor {
static func colorFFB635(alpha: CGFloat = 1) -> UIColor {
return UIColor(rgb: 0xFFB635, alpha: alpha)
}
static func colorB5B5B5(alpha: CGFloat = 1) -> UIColor {
return UIColor(rgb: 0xB5B5B5, alpha: alpha)
}
}

View File

@ -134,6 +134,38 @@ class BRVideoAPI {
}
}
///
static func requestUploadPlayTime(shortPlayId: String, videoId: String, seconds: Int) {
var param = BRNetworkParameters(path: "/uploadHistorySeconds")
param.isLoding = false
param.isToast = false
param.parameters = [
"video_id" : videoId,
"short_play_id" : shortPlayId,
"play_seconds" : seconds
]
BRNetwork.request(parameters: param) { (response: BRNetworkResponse<String>) in
}
}
///
static func requestCoinUnlockVideo(shortPlayId: String, videoId: String, completer: ((_ model: BRVideoUnlockModel?) -> Void)?) {
var param = BRNetworkParameters(path: "/buy_video")
param.isLoding = true
param.parameters = [
"short_play_id" : shortPlayId,
"video_id" : videoId,
]
BRNetwork.request(parameters: param) { (response: BRNetworkResponse<BRVideoUnlockModel>) in
completer?(response.data)
}
}
}

View File

@ -72,9 +72,9 @@ class BRPanModalContentView: HWPanModalContentView {
return false
}
override func minVerticalVelocityToTriggerDismiss() -> CGFloat {
return 0
}
// override func minVerticalVelocityToTriggerDismiss() -> CGFloat {
// return 0
// }
override func showsScrollableVerticalScrollIndicator() -> Bool {
return false

View File

@ -49,6 +49,7 @@ class BRPlayerListViewController: BRViewController {
private(set) lazy var viewModel: BRPlayerViewModel = {
let vm = BRPlayerViewModel()
vm.delegate = self
vm.playerListVC = self
return vm
}()
@ -321,6 +322,14 @@ extension BRPlayerListViewController: BRPlayerViewModelDelegate {
}
func br_playProgressDidChange(viewModel: BRPlayerViewModel, time: TimeInterval) {
}
func br_needUpdateAllData(viewModel: BRPlayerViewModel, scrollTo indexPath: IndexPath?) {
}
}
extension BRPlayerListViewController {

View File

@ -23,6 +23,9 @@ class BRVideoDetailViewController: BRPlayerListViewController {
private var detailArr: [BRVideoDetailModel] = []
///
private var lastUploadTime: TimeInterval = 0
//MARK: UI
private lazy var backButton: UIButton = {
let button = UIButton(type: .custom)
@ -66,8 +69,21 @@ class BRVideoDetailViewController: BRPlayerListViewController {
}
override func play() {
super.play()
BRVideoAPI.requestAddPlayHistory(videoId: self.viewModel.currentPlayer?.videoInfo?.short_play_video_id, shortPlayId: self.viewModel.currentPlayer?.videoInfo?.short_play_id)
let videoInfo = self.viewModel.currentPlayer?.videoInfo
guard videoInfo?.is_lock == true else {
super.play()
BRVideoAPI.requestAddPlayHistory(videoId: self.viewModel.currentPlayer?.videoInfo?.short_play_video_id, shortPlayId: self.viewModel.currentPlayer?.videoInfo?.short_play_id)
return
}
self.pause()
let myCoins = BRLoginManager.manager.userInfo?.totalCoin ?? 0
let coins = videoInfo?.coins ?? 0
if myCoins < coins, self.viewModel.currentPlayer?.hasLastEpisodeUnlocked != true {
self.viewModel.openRechargeView()
}
}
}
@ -116,6 +132,18 @@ extension BRVideoDetailViewController {
self.popUpView = view
}
override func br_playProgressDidChange(viewModel: BRPlayerViewModel, time: TimeInterval) {
if (time >= lastUploadTime + 5 || time < lastUploadTime) && time >= 5 {
lastUploadTime = time
self.viewModel.uploadPlayTime()
}
}
override func br_needUpdateAllData(viewModel: BRPlayerViewModel, scrollTo indexPath: IndexPath?) {
self.requestDetailData(indexPath: indexPath)
}
}
//MARK: -------------- BRPlayerListViewControllerDataSource BRPlayerListViewControllerDelegate --------------
@ -147,6 +175,8 @@ extension BRVideoDetailViewController: BRPlayerListViewControllerDataSource, BRP
return false
}
}
}
extension BRVideoDetailViewController {
@ -169,6 +199,23 @@ extension BRVideoDetailViewController {
self.reloadData { [weak self] in
guard let self = self else { return }
self.play()
var targetIndexPath = IndexPath(row: 0, section: 0)
if let indexPath = indexPath, indexPath.row < (model.episodeList?.count ?? 0) {
targetIndexPath = indexPath
} else if let videoInfo = model.video_info {
var row: Int?
model.episodeList?.enumerated().forEach({
if $1.id == videoInfo.id {
row = $0
}
})
if let row = row {
targetIndexPath = .init(row: row, section: 0)
}
}
self.scrollToItem(indexPath: targetIndexPath, animated: false)
}
}

View File

@ -50,7 +50,7 @@ class BRRateModel: NSObject {
static func getAllRate() -> [BRRateModel] {
return [
BRRateModel(rate: .x0_25),
// BRRateModel(rate: .x0_25),
BRRateModel(rate: .x0_5),
BRRateModel(rate: .x0_75),
BRRateModel(rate: .x1),

View File

@ -0,0 +1,26 @@
//
// BRVideoUnlockModel.swift
// BeeReel
//
// Created by 鸿 on 2025/7/26.
//
import UIKit
import SmartCodable
class BRVideoUnlockModel: BRModel, SmartCodable {
enum ResponseStatus: String, SmartCaseDefaultable {
///
case jump = "jump"
///
case noPlay = "no_play"
///
case notEnough = "not_enough"
///
case success = "success"
}
var status: ResponseStatus?
}

View File

@ -19,6 +19,12 @@ class BRDetailControlView: BRPlayerControlView {
override var videoInfo: BRVideoInfoModel? {
didSet {
epButton.videoInfo = videoInfo
videoLockView.videoInfo = videoInfo
if videoInfo?.is_lock == true {
self.videoLockView.isHidden = false
} else {
self.videoLockView.isHidden = true
}
}
}
@ -108,6 +114,14 @@ class BRDetailControlView: BRPlayerControlView {
return button
}()
private lazy var videoLockView: BRVideoLockView = {
let view = BRVideoLockView()
view.clickUnlockButton = { [weak self] in
self?.viewModel?.clickUnlockButton()
}
return view
}()
deinit {
NotificationCenter.default.removeObserver(self)
@ -133,7 +147,6 @@ class BRDetailControlView: BRPlayerControlView {
}
}
}
extension BRDetailControlView {
@ -198,6 +211,7 @@ extension BRDetailControlView {
addSubview(rateButton)
addSubview(nameLabel)
addSubview(favoriteButton)
addSubview(videoLockView)
progressView.snp.makeConstraints { make in
make.left.equalToSuperview()
@ -233,6 +247,10 @@ extension BRDetailControlView {
make.right.equalToSuperview().offset(-7)
make.bottom.equalTo(progressView.snp.top).offset(-10)
}
videoLockView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
}

View File

@ -12,6 +12,8 @@ class BREpisodeSelectorCell: BRCollectionViewCell {
var model: BRVideoInfoModel? {
didSet {
epLabel.text = model?.episode
lockView.isHidden = !(model?.is_lock ?? false)
}
}
@ -39,6 +41,19 @@ class BREpisodeSelectorCell: BRCollectionViewCell {
return imageView
}()
private lazy var lockView: UIView = {
let view = UIView()
view.backgroundColor = .colorE3FC37()
view.br_setRoundedCorner(topLeft: 0, topRight: 6, bottomLeft: 6, bottomRight: 0)
let icon = UIImageView(image: UIImage(named: "Frame 3"))
view.addSubview(icon)
icon.snp.makeConstraints { make in
make.center.equalToSuperview()
}
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
@ -51,6 +66,7 @@ class BREpisodeSelectorCell: BRCollectionViewCell {
contentView.addSubview(epLabel)
contentView.addSubview(lightImageView)
contentView.addSubview(lockView)
epLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
@ -60,6 +76,12 @@ class BREpisodeSelectorCell: BRCollectionViewCell {
make.right.top.equalToSuperview()
}
lockView.snp.makeConstraints { make in
make.top.right.equalToSuperview()
make.width.equalTo(18)
make.height.equalTo(12)
}
}
@MainActor required init?(coder: NSCoder) {

View File

@ -88,7 +88,17 @@ class BRPlayerControlView: UIView, BRPlayerControlProtocol {
self.viewModel?.switchPlayAndPause()
}
func updatePlayIconState() {
if videoInfo?.is_lock == true {
self.playIconImageView.isHidden = true
} else {
if isCurrent == true, self.viewModel?.isPlaying != true {
self.playIconImageView.isHidden = false
} else {
self.playIconImageView.isHidden = true
}
}
}
}
@ -107,13 +117,6 @@ extension BRPlayerControlView {
extension BRPlayerControlView {
private func updatePlayIconState() {
if isCurrent == true, self.viewModel?.isPlaying != true {
self.playIconImageView.isHidden = false
} else {
self.playIconImageView.isHidden = true
}
}
}

View File

@ -75,6 +75,9 @@ class BRPlayerListCell: BRCollectionViewCell, BRPlayerProtocol {
self.player.seek(toTime: time)
}
func seekTo(time: TimeInterval) {
self.player.seek(toTime: time)
}
var ControlViewClass: BRPlayerControlView.Type {
return BRPlayerControlView.self
@ -144,6 +147,7 @@ extension BRPlayerListCell: BRPlayerDelegate {
} else {
self.controlView.progress = time / player.duration
}
self.viewModel?.playProgressDidChange(time: time)
}
func br_playerInBufferToPlay(_ player: BRPlayer) {
@ -153,4 +157,11 @@ extension BRPlayerListCell: BRPlayerDelegate {
func br_playerBufferingCompleted(_ player: BRPlayer) {
self.controlView.isLoading = false
}
func br_playerReadyToPlay(_ player: BRPlayer) {
let time = TimeInterval(self.videoInfo?.play_seconds ?? 0) / 1000
if time > 1 {
self.seekTo(time: time)
}
}
}

View File

@ -0,0 +1,128 @@
//
// BRVideoLockView.swift
// BeeReel
//
// Created by 鸿 on 2025/7/26.
//
import UIKit
class BRVideoLockView: UIView {
var videoInfo: BRVideoInfoModel? {
didSet {
lockButton.setNeedsUpdateConfiguration()
}
}
var clickUnlockButton: (() -> Void)?
private lazy var lockIconView: UIImageView = {
let view = UIImageView(image: UIImage(named: "Frame 4"))
return view
}()
private lazy var bottomView: UIView = {
let view = UIImageView(image: UIImage(named: "bg"))
view.isUserInteractionEnabled = true
return view
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .fontBold(ofSize: 18)
label.textColor = .colorFFFFFF()
label.text = "Unlock to Continue".localized
return label
}()
private lazy var lockButton: UIButton = {
var config = UIButton.Configuration.plain()
config.background.image = UIImage(named: "bg 1")
config.image = UIImage(named: "Frame 5")
config.imagePadding = 10
let button = UIButton(configuration: config)
button.configurationUpdateHandler = { [weak self] button in
guard let self = self else { return }
let title = "Unlocking costs ## Coins".localizedReplace(text: "\(videoInfo?.coins ?? 0)")
button.configuration?.attributedTitle = AttributedString.br_createAttributedString(string: title, color: .color1C1C1C(), font: .fontRegular(ofSize: 14))
}
button.addTarget(self, action: #selector(handleUnlockButton), for: .touchUpInside)
return button
}()
private lazy var totalCoinsLabel: UILabel = {
let label = UILabel()
label.font = .fontRegular(ofSize: 12)
label.textColor = .colorB5B5B5()
return label
}()
deinit {
NotificationCenter.default.removeObserver(self)
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .color000000(alpha: 0.4)
NotificationCenter.default.addObserver(self, selector: #selector(updateUserInfo), name: BRLoginManager.userInfoUpdateNotification, object: nil)
updateUserInfo()
br_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func updateUserInfo() {
let userInfo = BRLoginManager.manager.userInfo
totalCoinsLabel.text = "Balance: ## Coins".localizedReplace(text: "\(userInfo?.totalCoin ?? 0)")
}
@objc private func handleUnlockButton() {
self.clickUnlockButton?()
}
}
extension BRVideoLockView {
private func br_setupUI() {
addSubview(lockIconView)
addSubview(bottomView)
bottomView.addSubview(titleLabel)
bottomView.addSubview(lockButton)
bottomView.addSubview(totalCoinsLabel)
lockIconView.snp.makeConstraints { make in
make.center.equalToSuperview()
}
bottomView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.height.equalTo(UIScreen.tabbarSafeBottomMargin + 222)
}
titleLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalToSuperview().offset(26)
}
lockButton.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.centerY.equalToSuperview()
make.width.equalTo(260)
make.height.equalTo(48)
}
totalCoinsLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().offset(-(UIScreen.tabbarSafeBottomMargin + 10))
}
}
}

View File

@ -0,0 +1,232 @@
//
// BRVideoRechargeView.swift
// BeeReel
//
// Created by 鸿 on 2025/7/26.
//
import UIKit
class BRVideoRechargeView: BRPanModalContentView {
var buyCoinsFinishBlock: (() -> Void)?
var buyVipFinishBlock: (() -> Void)?
var payDataModel: BRPayDateModel? {
didSet {
self.stackView.br_removeAllArrangedSubview()
self.vipView.list = payDataModel?.list_sub_vip ?? []
self.coinView.list = payDataModel?.list_coins ?? []
if let sort = payDataModel?.sort, sort.count > 0 {
sort.forEach {
if $0 == .vip, payDataModel?.list_sub_vip?.isEmpty == false {
self.stackView.addArrangedSubview(self.vipView)
} else if $0 == .coin, payDataModel?.list_coins?.isEmpty == false {
self.stackView.addArrangedSubview(self.coinView)
}
}
} else {
if payDataModel?.list_sub_vip?.isEmpty == false {
self.stackView.addArrangedSubview(self.vipView)
}
if payDataModel?.list_coins?.isEmpty == false {
self.stackView.addArrangedSubview(self.coinView)
}
}
self.setNeedsLayoutUpdate()
}
}
var unlockCoin: Int? {
didSet {
if let unlockCoin = self.unlockCoin, unlockCoin > 0 {
unlockCoinsView.isHidden = false
} else {
unlockCoinsView.isHidden = true
}
unlockCoinsView.setNeedsUpdateConfiguration()
}
}
var shortPlayId: String? {
didSet {
vipView.shortPlayId = shortPlayId
coinView.shortPlayId = shortPlayId
}
}
var videoId: String? {
didSet {
vipView.videoId = videoId
coinView.videoId = videoId
}
}
private lazy var coinIconView: UIView = {
let imageView = UIImageView(image: UIImage(named: "Frame 6"))
return imageView
}()
private lazy var coinTitleLabel: UILabel = {
let label = UILabel()
label.font = .fontMedium(ofSize: 13)
label.textColor = .colorFFFFFF()
label.text = "My Coins:".localized
return label
}()
private lazy var coinLabel: UILabel = {
let label = UILabel()
label.font = .fontMedium(ofSize: 13)
label.textColor = .colorE3FC37()
return label
}()
private lazy var scrollView: BRScrollView = {
let scrollView = BRScrollView()
scrollView.bounces = false
return scrollView
}()
private lazy var unlockCoinsView: UIButton = {
var config = UIButton.Configuration.plain()
config.background.backgroundColor = .colorE3FC37()
config.image = UIImage(named: "Frame 6")
config.imagePadding = 2
config.imagePlacement = .trailing
config.contentInsets = .init(top: 0, leading: 10, bottom: 0, trailing: 10)
let button = UIButton(configuration: config)
button.layer.cornerRadius = 13
button.layer.masksToBounds = true
button.isUserInteractionEnabled = false
button.configurationUpdateHandler = { [weak self] button in
guard let self = self else { return }
let title = "Unlock:".localized
let coins = " \(unlockCoin ?? 0)"
var attributedTitle = AttributedString.br_createAttributedString(string: title + coins, color: .color1C1C1C(), font: .fontRegular(ofSize: 13))
if let range = attributedTitle.range(of: coins) {
attributedTitle[range].font = UIFont.fontMedium(ofSize: 13)
attributedTitle[range].foregroundColor = UIColor.colorFF7489()
}
button.configuration?.attributedTitle = attributedTitle
}
return button
}()
private lazy var stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = 12
return stackView
}()
private lazy var vipView: BRStoreVipView = {
let view = BRStoreVipView()
view.buyFinishBlock = { [weak self] in
self?.buyVipFinishBlock?()
self?.dismiss(animated: true) {
}
}
return view
}()
private lazy var coinView: BRStoreCoinView = {
let view = BRStoreCoinView()
view.buyFinishBlock = { [weak self] in
self?.buyCoinsFinishBlock?()
self?.dismiss(animated: true) {
}
}
return view
}()
deinit {
NotificationCenter.default.removeObserver(self)
}
override init(frame: CGRect) {
super.init(frame: frame)
NotificationCenter.default.addObserver(self, selector: #selector(updateUserInfo), name: BRLoginManager.userInfoUpdateNotification, object: nil)
self.contentHeight = UIScreen.height - UIScreen.navBarHeight
self.mainScrollView = self.scrollView
br_setupUI()
updateUserInfo()
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func allowsPullDownWhenShortState() -> Bool {
return true
}
override func allowsDragToDismiss() -> Bool {
return true
}
}
extension BRVideoRechargeView {
private func br_setupUI() {
addSubview(scrollView)
scrollView.addSubview(stackView)
addSubview(coinTitleLabel)
addSubview(coinLabel)
addSubview(coinIconView)
addSubview(unlockCoinsView)
scrollView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalToSuperview().offset(50)
}
stackView.snp.makeConstraints { make in
make.top.left.equalToSuperview()
make.width.equalTo(UIScreen.width)
make.bottom.equalToSuperview().offset(-UIScreen.tabbarSafeBottomMargin - 10)
}
coinTitleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15)
make.centerY.equalTo(coinIconView)
}
coinLabel.snp.makeConstraints { make in
make.left.equalTo(coinTitleLabel.snp.right)
make.centerY.equalTo(coinIconView)
}
coinIconView.snp.makeConstraints { make in
make.left.equalTo(coinLabel.snp.right).offset(2)
make.top.equalToSuperview().offset(18)
}
unlockCoinsView.snp.makeConstraints { make in
make.centerY.equalTo(coinIconView)
make.right.equalToSuperview().offset(-15)
make.height.equalTo(26)
}
}
}
extension BRVideoRechargeView {
@objc private func updateUserInfo() {
let userInfo = BRLoginManager.manager.userInfo
coinLabel.text = " \(userInfo?.totalCoin ?? 0)"
}
}

View File

@ -10,6 +10,8 @@ import UIKit
@objc protocol BRPlayerViewModelDelegate {
@objc func br_playProgressDidChange(viewModel: BRPlayerViewModel, time: TimeInterval)
@objc func br_currentVideoPlayFinish(viewModel: BRPlayerViewModel)
@objc func br_switchPlayAndPause(viewModel: BRPlayerViewModel)
@ -17,6 +19,9 @@ import UIKit
@objc func br_onEpisodeView(viewModel: BRPlayerViewModel)
@objc func br_clickRateButton(viewModel: BRPlayerViewModel)
@objc func br_needUpdateAllData(viewModel: BRPlayerViewModel, scrollTo indexPath: IndexPath?)
}
@ -24,6 +29,8 @@ class BRPlayerViewModel: NSObject {
weak var delegate: BRPlayerViewModelDelegate?
weak var playerListVC: BRPlayerListViewController?
@objc dynamic var isPlaying: Bool = true
var currentIndexPath = IndexPath(row: 0, section: 0)
@ -44,12 +51,22 @@ class BRPlayerViewModel: NSObject {
self.currentPlayer?.rate = rateModel.rate.getRate()
}
}
private lazy var payDataRequest = VPPayDataRequest()
}
extension BRPlayerViewModel {
///
func playProgressDidChange(time: TimeInterval) {
if time > 1 {
self.currentPlayer?.videoInfo?.play_seconds = Int(time) * 1000
}
self.delegate?.br_playProgressDidChange(viewModel: self, time: time)
}
func playFinish(player: BRPlayerProtocol) {
guard (player as? UICollectionViewCell) == (currentPlayer as? UICollectionViewCell) else { return }
self.delegate?.br_currentVideoPlayFinish(viewModel: self)
@ -73,7 +90,106 @@ extension BRPlayerViewModel {
///
func seekTo(progress: Float) {
self.currentPlayer?.seekTo(progress: progress)
}
///
func clickUnlockButton() {
unlockVideo { [weak self] finish in
if finish {
self?.playerListVC?.reloadData {
self?.playerListVC?.play()
}
}
}
}
///
func updateAllData(scrollTo indexPath: IndexPath? = nil) {
self.delegate?.br_needUpdateAllData(viewModel: self, scrollTo: indexPath)
}
}
extension BRPlayerViewModel {
func unlockVideo(completer: ((_ finish: Bool) -> Void)?) {
let videoInfo = self.currentPlayer?.videoInfo
guard let shortPlayId = videoInfo?.short_play_id, let videoId = videoInfo?.short_play_video_id else { return }
BRVideoAPI.requestCoinUnlockVideo(shortPlayId: shortPlayId, videoId: videoId) { [weak self] model in
guard let self = self else { return }
guard let model = model else {
completer?(false)
return
}
switch model.status {
case .jump:
BRToast.show(text: "beereel_jump_unlock_error".localized)
completer?(false)
case .noPlay:
BRToast.show(text: "beereel_buy_fail_toast_01".localized)
completer?(false)
case .notEnough:
self.openRechargeView()
completer?(false)
case .success:
//
BRLoginManager.manager.updateUserInfo {
videoInfo?.is_lock = false
completer?(true)
}
default:
completer?(false)
break
}
}
}
///
func openRechargeView() {
guard let videoInfo = self.currentPlayer?.videoInfo else { return }
self.payDataRequest.requestProducts(isLoding: true) { [weak self] model in
guard let self = self else { return }
guard let model = model else { return }
let view = BRVideoRechargeView()
view.shortPlayId = videoInfo.short_play_id
view.videoId = videoInfo.short_play_video_id
view.payDataModel = model
view.unlockCoin = self.currentPlayer?.videoInfo?.coins
view.buyVipFinishBlock = { [weak self] in
guard let self = self else { return }
self.updateAllData(scrollTo: self.currentIndexPath)
}
view.buyCoinsFinishBlock = { [weak self] in
guard let self = self else { return }
self.updateAllData(scrollTo: self.currentIndexPath)
}
view.present(in: nil)
}
}
///
func uploadPlayTime() {
let videoInfo = self.currentPlayer?.videoInfo
let currentTime = self.currentPlayer?.currentTime ?? 0
let duration = self.currentPlayer?.durationTime ?? 0
var time = currentTime
if currentTime >= duration {
time = 0
}
guard let shortPlayId = videoInfo?.short_play_id, let videoId = videoInfo?.short_play_video_id else { return }
//
BRVideoAPI.requestUploadPlayTime(shortPlayId: shortPlayId, videoId: videoId, seconds: Int(time) * 1000)
}
}

View File

@ -11,6 +11,8 @@ class BRStoreViewController: BRViewController {
private var payData: BRPayDateModel?
private lazy var dataRequest = VPPayDataRequest()
private lazy var scrollView: BRScrollView = {
let scrollView = BRScrollView()
return scrollView
@ -95,8 +97,7 @@ extension BRStoreViewController {
extension BRStoreViewController {
private func requestPayData() {
BRStoreAPI.requestPayTemplate { [weak self] model in
self.dataRequest.requestProducts { [weak self] model in
guard let self = self else { return }
guard let model = model else { return }
@ -123,7 +124,6 @@ extension BRStoreViewController {
}
self.stackView.addArrangedSubview(self.tipView)
}
}

View File

@ -0,0 +1,114 @@
//
// VPPayDataRequest.swift
// BeeReel
//
// Created by 鸿 on 2025/7/26.
//
import UIKit
import StoreKit
class VPPayDataRequest: NSObject {
private var oldTemplateModel: BRPayDateModel?
private var completerBlock: ((_ model: BRPayDateModel?) -> Void)?
private var isLoding = false
private var isToast = false
func requestProducts(isLoding: Bool = false, isToast: Bool = true, completer: ((_ model: BRPayDateModel?) -> Void)?) {
self.completerBlock = completer
self.isLoding = isLoding
self.isToast = isToast
if isLoding {
BRHUD.show()
}
BRStoreAPI.requestPayTemplate { [weak self] model in
guard let self = self else { return }
if isLoding {
BRHUD.dismiss()
}
completer?(model)
}
// BRStoreAPI.requestPayTemplate(isToast: isToast) { [weak self] model in
// guard let self = self else { return }
// guard let model = model else {
// if isLoding {
// VPHUD.dismiss()
// }
// self.completerBlock?(nil)
// return
// }
// self.oldTemplateModel = model
//
// var productIdArr: [String] = []
// model.list_sub_vip?.forEach { item in
// productIdArr.append(VPIAPManager.manager.getProductId(templateId: item.ios_template_id) ?? "")
// }
// model.list_coins?.forEach { item in
// productIdArr.append(VPIAPManager.manager.getProductId(templateId: item.ios_template_id) ?? "")
// }
//
// let set = Set(productIdArr)
// let productsRequest = SKProductsRequest(productIdentifiers: set)
// productsRequest.delegate = self
// productsRequest.start()
//
// }
}
}
/*
//MARK: -------------- SKProductsRequestDelegate --------------
extension VPPayTemplateRequest: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
if isLoding {
VPHUD.dismiss()
}
guard let templateModel = self.oldTemplateModel else { return }
let products = response.products
var newCoinList: [VPPayTemplateItem] = []
var newVipList: [VPPayTemplateItem] = []
templateModel.list_coins?.forEach { item in
let productId = VPIAPManager.manager.getProductId(templateId: item.ios_template_id) ?? ""
for product in products {
if productId == product.productIdentifier {
item.price = product.price.stringValue
item.currency = product.priceLocale.currencySymbol
newCoinList.append(item)
break
}
}
}
templateModel.list_sub_vip?.forEach { item in
let productId = VPIAPManager.manager.getProductId(templateId: item.ios_template_id) ?? ""
for product in products {
if productId == product.productIdentifier {
item.price = product.price.stringValue
item.currency = product.priceLocale.currencySymbol
newVipList.append(item)
break
}
}
}
templateModel.list_coins = newCoinList
templateModel.list_sub_vip = newVipList
DispatchQueue.main.async {
self.completerBlock?(templateModel)
}
}
}
*/

View File

@ -9,6 +9,8 @@ import UIKit
class BRStoreCoinView: UIView {
var buyFinishBlock: (() -> Void)?
var list: [BRPayItem] = [] {
didSet {
@ -30,6 +32,9 @@ class BRStoreCoinView: UIView {
}
}
var shortPlayId: String?
var videoId: String?
private var newList: [[BRPayItem]] = []
private lazy var titleLabel: UILabel = {

View File

@ -9,12 +9,17 @@ import UIKit
class BRStoreVipView: UIView {
var buyFinishBlock: (() -> Void)?
var list: [BRPayItem] = [] {
didSet {
self.collectionView.reloadData()
}
}
var shortPlayId: String?
var videoId: String?
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.textColor = .colorFFFFFF()

View File

@ -10,6 +10,8 @@ import SJBaseVideoPlayer
@objc protocol BRPlayerDelegate: NSObjectProtocol {
@objc optional func br_playerReadyToPlay(_ player: BRPlayer)
///
@objc optional func br_playerDurationDidChange(_ player: BRPlayer, duration: TimeInterval)
///
@ -150,8 +152,6 @@ extension BRPlayer {
//
self.player.playbackObserver.timeControlStatusDidChangeExeBlock = { [weak self] player in
guard let self = self else { return }
// , player.reasonForWaitingToPlay == SJWaitingToMinimizeStallsReason
if player.timeControlStatus == .waitingToPlay {//
self.delegate?.br_playerInBufferToPlay?(self)
brLog(message: "=======缓冲中 === \(player.reasonForWaitingToPlay ?? "")")
@ -161,6 +161,14 @@ extension BRPlayer {
}
}
self.player.playbackObserver.assetStatusDidChangeExeBlock = { [weak self] player in
guard let self = self else { return }
brLog(message: "assetStatus === \(player.assetStatus.rawValue)")
if player.assetStatus == .readyToPlay {
self.delegate?.br_playerReadyToPlay?(self)
}
}
//
self.player.playbackObserver.durationDidChangeExeBlock = { [weak self] player in
guard let self = self else { return }

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Frame@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Frame@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 B

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Frame@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Frame@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Frame@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Frame@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Frame@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Frame@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "bg@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "bg@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "bg@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "bg@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@ -34,6 +34,41 @@
}
}
},
"Balance: ## Coins" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Balance: ## Coins"
}
}
}
},
"beereel_buy_fail_toast_01" : {
"comment" : "解锁失败提示",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Purchase failed, please try again later!"
}
}
}
},
"beereel_jump_unlock_error" : {
"comment" : "解锁上一集提示",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "The prequel to this series is not unlocked. Please unlock the prequel before unlocking this series"
}
}
}
},
"beereel_network" : {
"extractionState" : "manual",
"localizations" : {
@ -320,6 +355,17 @@
}
}
},
"My Coins:" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "My Coins:"
}
}
}
},
"New Releases" : {
"extractionState" : "manual",
"localizations" : {
@ -474,6 +520,39 @@
}
}
},
"Unlock to Continue" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unlock to Continue"
}
}
}
},
"Unlock:" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unlock:"
}
}
}
},
"Unlocking costs ## Coins" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unlocking costs ## Coins"
}
}
}
},
"User Agreement" : {
"extractionState" : "manual",
"localizations" : {