详情推荐

This commit is contained in:
zeng 2025-07-29 17:43:43 +08:00
parent 219b70a6f0
commit a75d9395be
25 changed files with 791 additions and 14 deletions

View File

@ -205,6 +205,10 @@
F398558B2E37792700E2D28D /* Date+BRAdd.swift in Sources */ = {isa = PBXBuildFile; fileRef = F398558A2E37792200E2D28D /* Date+BRAdd.swift */; };
F398558E2E37857D00E2D28D /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = F398558D2E37857D00E2D28D /* FirebaseMessaging */; };
F39855902E37862200E2D28D /* BRSettingAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F398558F2E37861D00E2D28D /* BRSettingAPI.swift */; };
F39855922E37999900E2D28D /* BRVideoDetailRecommendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39855912E37999900E2D28D /* BRVideoDetailRecommendView.swift */; };
F39855942E379D9600E2D28D /* BRVideoDetailRecommendCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39855932E379D9600E2D28D /* BRVideoDetailRecommendCell.swift */; };
F39855962E38A27500E2D28D /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = F39855952E38A27500E2D28D /* GoogleService-Info.plist */; };
F39855982E38BB3500E2D28D /* BRVideoDetailRecommendTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39855972E38BB3500E2D28D /* BRVideoDetailRecommendTransformer.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -420,6 +424,11 @@
F39855882E37732600E2D28D /* BRGuideViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRGuideViewController.swift; sourceTree = "<group>"; };
F398558A2E37792200E2D28D /* Date+BRAdd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+BRAdd.swift"; sourceTree = "<group>"; };
F398558F2E37861D00E2D28D /* BRSettingAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRSettingAPI.swift; sourceTree = "<group>"; };
F39855912E37999900E2D28D /* BRVideoDetailRecommendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRVideoDetailRecommendView.swift; sourceTree = "<group>"; };
F39855932E379D9600E2D28D /* BRVideoDetailRecommendCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRVideoDetailRecommendCell.swift; sourceTree = "<group>"; };
F39855952E38A27500E2D28D /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
F39855972E38BB3500E2D28D /* BRVideoDetailRecommendTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRVideoDetailRecommendTransformer.swift; sourceTree = "<group>"; };
F39855992E38CBE800E2D28D /* BeeReel.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = BeeReel.entitlements; 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 */
@ -644,6 +653,7 @@
BF692AEA2E0A475D00A5C2DA /* BeeReel */ = {
isa = PBXGroup;
children = (
F39855992E38CBE800E2D28D /* BeeReel.entitlements */,
BF692AF32E0A47B500A5C2DA /* Delegate */,
BF692AF42E0A47CA00A5C2DA /* Base */,
BF692AF52E0A47D400A5C2DA /* Class */,
@ -657,6 +667,7 @@
BF692AF22E0A478D00A5C2DA /* Sources */ = {
isa = PBXGroup;
children = (
F39855952E38A27500E2D28D /* GoogleService-Info.plist */,
F39855432E33840500E2D28D /* AaHouDiHei-Regular.ttf */,
BF692AE22E0A475D00A5C2DA /* Assets.xcassets */,
BF692AE32E0A475D00A5C2DA /* Info.plist */,
@ -898,6 +909,9 @@
BF02B7F22E2E571600172177 /* BRRateSelectorCell.swift */,
F398554D2E34699F00E2D28D /* BRVideoLockView.swift */,
F39855512E347BDE00E2D28D /* BRVideoRechargeView.swift */,
F39855912E37999900E2D28D /* BRVideoDetailRecommendView.swift */,
F39855932E379D9600E2D28D /* BRVideoDetailRecommendCell.swift */,
F39855972E38BB3500E2D28D /* BRVideoDetailRecommendTransformer.swift */,
);
path = View;
sourceTree = "<group>";
@ -1249,6 +1263,7 @@
files = (
BF692AEE2E0A475D00A5C2DA /* Assets.xcassets in Resources */,
BF692AF02E0A475D00A5C2DA /* LaunchScreen.storyboard in Resources */,
F39855962E38A27500E2D28D /* GoogleService-Info.plist in Resources */,
F39855442E33840500E2D28D /* AaHouDiHei-Regular.ttf in Resources */,
BF692B442E0A910E00A5C2DA /* Localizable.xcstrings in Resources */,
);
@ -1325,10 +1340,12 @@
BFC6768D2E123D6E006659E5 /* AttributedString+BRAdd.swift in Sources */,
BF02B8392E30B30400172177 /* AlignedCollectionViewFlowLayout.swift in Sources */,
BF3A56882E30E0DD009E5CF9 /* BREmpty.swift in Sources */,
F39855942E379D9600E2D28D /* BRVideoDetailRecommendCell.swift in Sources */,
F39855542E34A49500E2D28D /* BRPayDataRequest.swift in Sources */,
BF3338F52E1616B200B10F76 /* BRExploreControlView.swift in Sources */,
F398552D2E33126D00E2D28D /* BRMineCoinItemView.swift in Sources */,
BF692B132E0A7B9000A5C2DA /* BRUserInfo.swift in Sources */,
F39855982E38BB3500E2D28D /* BRVideoDetailRecommendTransformer.swift in Sources */,
BF692B042E0A76D200A5C2DA /* BRLoginManager.swift in Sources */,
BFC6769D2E129794006659E5 /* BRHomeTop10ViewController.swift in Sources */,
F398556C2E3717A500E2D28D /* BRWalletHeaderItemView.swift in Sources */,
@ -1475,6 +1492,7 @@
BF692B7A2E0D3BD300A5C2DA /* BRShortModel.swift in Sources */,
F39855822E376CB000E2D28D /* BROpenAppModel.swift in Sources */,
BFC676712E0E9234006659E5 /* BRSpotlightViewViewController.swift in Sources */,
F39855922E37999900E2D28D /* BRVideoDetailRecommendView.swift in Sources */,
BF3338F02E15569600B10F76 /* BRExploreViewController.swift in Sources */,
BF0DBDD12E0D4E150035F6B4 /* BRTabBar.swift in Sources */,
BF692B562E0AA92100A5C2DA /* BRCollectionViewCell.swift in Sources */,
@ -1522,6 +1540,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = BeeReel/BeeReel.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 8NNUR9HPV3;
@ -1561,6 +1580,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = BeeReel/BeeReel.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 8NNUR9HPV3;

View File

@ -206,4 +206,12 @@ extension UIColor {
static func color7ECD5E(alpha: CGFloat = 1) -> UIColor {
return UIColor(rgb: 0x7ECD5E, alpha: alpha)
}
static func color576300(alpha: CGFloat = 1) -> UIColor {
return UIColor(rgb: 0x576300, alpha: alpha)
}
static func color499C20(alpha: CGFloat = 1) -> UIColor {
return UIColor(rgb: 0x499C20, alpha: alpha)
}
}

View File

@ -32,6 +32,17 @@ class BRVideoAPI {
}
}
///
static func requestDetailsRecommand(completer: ((_ list: [BRShortModel]?) -> Void)?) {
var param = BRNetworkParameters(path: "/getDetailsRecommand")
param.method = .get
BRNetwork.request(parameters: param) { (response: BRNetworkResponse<BRListModel<BRShortModel>>) in
completer?(response.data?.list)
}
}
///
static func requestFavorite(isFavorite: Bool, shortPlayId: String, videoId: String?, isLoding: Bool = true, success: (() -> Void)?, failure: (() -> Void)? = nil) {

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.associated-domains</key>
<array/>
<key>keychain-access-groups</key>
<array/>
</dict>
</plist>

View File

@ -23,6 +23,10 @@ class BRVideoDetailViewController: BRPlayerListViewController {
private var detailArr: [BRVideoDetailModel] = []
///
private lazy var isAllowShowRecommand = false
private var recommandTimer: Timer?
///
private var lastUploadTime: TimeInterval = 0
@ -30,7 +34,7 @@ class BRVideoDetailViewController: BRPlayerListViewController {
private lazy var backButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "nav_back_icon_01"), for: .normal)
button.addTarget(self, action: #selector(handleNavBack), for: .touchUpInside)
button.addTarget(self, action: #selector(handleBackButton), for: .touchUpInside)
return button
}()
@ -44,6 +48,7 @@ class BRVideoDetailViewController: BRPlayerListViewController {
self.fd_interactivePopDisabled = true
self.requestDetailData()
self.viewModel.asyncGetRecommandDataArr()
br_setupUI()
}
@ -86,9 +91,24 @@ class BRVideoDetailViewController: BRPlayerListViewController {
}
}
override func handleNavBack() {
super.handleNavBack()
@objc private func handleBackButton() {
if !self.viewModel.recommandList.isEmpty, isAllowShowRecommand {
self.pause()
let view = BRVideoDetailRecommendView()
view.videoList = self.viewModel.recommandList
view.didDismissBlock = { [weak self] in
self?.handleNavBack()
}
view.didSelectedVideo = { [weak self] model in
guard let self = self else { return }
self.shortPlayId = model.short_play_id
self.requestDetailData()
}
view.present(in: nil)
} else {
self.handleNavBack()
}
}
}
@ -102,7 +122,13 @@ extension BRVideoDetailViewController {
make.top.equalToSuperview().offset(UIScreen.statusBarHeight + 10)
}
}
}
extension BRVideoDetailViewController {
@objc private func handleRecommandTimer() {
self.isAllowShowRecommand = true
}
}
//MARK: -------------- BRPlayerViewModelDelegate --------------
@ -161,6 +187,13 @@ extension BRVideoDetailViewController: BRPlayerListViewControllerDataSource, BRP
cell.videoInfo = model.episodeList?[indexPath.row]
cell.shortModel = model.shortPlayInfo
let upRow = indexPath.row - 1
if upRow >= 0, let videoInfo = model.episodeList?[upRow], videoInfo.is_lock == true {
cell.hasLastEpisodeUnlocked = true
} else {
cell.hasLastEpisodeUnlocked = false
}
return cell
}
@ -188,8 +221,11 @@ extension BRVideoDetailViewController {
private func requestDetailData(indexPath: IndexPath? = nil) {
guard let shortPlayId = shortPlayId else { return }
isAllowShowRecommand = false
recommandTimer?.invalidate()
recommandTimer = nil
recommandTimer = Timer.scheduledTimer(timeInterval: 6, target: YYTextWeakProxy(target: self), selector: #selector(handleRecommandTimer), userInfo: nil, repeats: false)
BRHUD.show(containerView: self.view)
BRVideoAPI.requestVideoDetail(shortPlayId: shortPlayId, activityId: activityId) { [weak self] model in

View File

@ -37,6 +37,12 @@ class BRDetailControlView: BRPlayerControlView {
}
}
override var hasLastEpisodeUnlocked: Bool {
didSet {
self.videoLockView.hasLastEpisodeUnlocked = hasLastEpisodeUnlocked
}
}
override var progress: CGFloat {
didSet {
progressView.progress = progress

View File

@ -29,6 +29,8 @@ class BRPlayerControlView: UIView, BRPlayerControlProtocol {
}
}
var hasLastEpisodeUnlocked: Bool = false
///
var panProgressFinishBlock: ((_ progress: CGFloat) -> Void)?

View File

@ -30,6 +30,12 @@ class BRPlayerListCell: BRCollectionViewCell, BRPlayerProtocol {
}
}
var hasLastEpisodeUnlocked: Bool = false {
didSet {
self.controlView.hasLastEpisodeUnlocked = hasLastEpisodeUnlocked
}
}
var isCurrent: Bool = false {
didSet {
self.controlView.isCurrent = isCurrent

View File

@ -0,0 +1,115 @@
//
// BRVideoDetailRecommendCell.swift
// BeeReel
//
// Created by 鸿 on 2025/7/28.
//
import UIKit
import FSPagerView
class BRVideoDetailRecommendCell: FSPagerViewCell {
var model: BRShortModel? {
didSet {
coverImageView.br_setImage(url: model?.image_url)
categoryLabel.text = model?.category?.first
self.player.coverImageView?.br_setImage(url: model?.image_url)
player.setPlayUrl(url: model?.video_url ?? "")
}
}
var isCurrent: Bool = false
private lazy var bgView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "Rectangle 80"))
return imageView
}()
private lazy var player: BRPlayer = {
let player = BRPlayer(controlView: nil)
player.playerView = self.playerView
// player.delegate = self
return player
}()
private lazy var playerView: UIView = {
let view = UIView()
view.layer.cornerRadius = 10
view.layer.masksToBounds = true
view.layer.borderColor = UIColor.colorFFFFFF().cgColor
view.layer.borderWidth = 2
return view
}()
private lazy var coverImageView: BRImageView = {
let imageView = BRImageView()
imageView.layer.cornerRadius = 10
imageView.layer.masksToBounds = true
imageView.layer.borderColor = UIColor.colorFFFFFF().cgColor
imageView.layer.borderWidth = 2
imageView.isHidden = true
return imageView
}()
private lazy var categoryLabel: UILabel = {
let label = UILabel()
label.font = .fontMedium(ofSize: 10)
label.textColor = .color576300()
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
br_setupUI()
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func play() {
self.player.start()
}
func pause() {
self.player.pause()
}
}
extension BRVideoDetailRecommendCell {
private func br_setupUI() {
contentView.addSubview(bgView)
contentView.addSubview(playerView)
contentView.addSubview(coverImageView)
contentView.addSubview(categoryLabel)
bgView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
playerView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(5)
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().offset(-5)
make.top.equalToSuperview().offset(19)
}
coverImageView.snp.makeConstraints { make in
make.edges.equalTo(playerView)
}
categoryLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(7)
make.top.equalToSuperview().offset(4)
make.width.lessThanOrEqualTo(65)
}
}
}

View File

@ -0,0 +1,214 @@
//
// BRVideoDetailRecommendTransformer.swift
// BeeReel
//
// Created by 鸿 on 2025/7/29.
//
import UIKit
import FSPagerView
class BRVideoDetailRecommendTransformer: FSPagerViewTransformer {
override func applyTransform(to attributes: FSPagerViewLayoutAttributes) {
guard let pagerView = self.pagerView else {
return
}
let position = attributes.position
let scrollDirection = pagerView.scrollDirection
let itemSpacing = (scrollDirection == .horizontal ? attributes.bounds.width : attributes.bounds.height) + self.proposedInteritemSpacing()
switch self.type {
case .crossFading:
var zIndex = 0
var alpha: CGFloat = 0
var transform = CGAffineTransform.identity
switch scrollDirection {
case .horizontal:
transform.tx = -itemSpacing * position
case .vertical:
transform.ty = -itemSpacing * position
}
if (abs(position) < 1) { // [-1,1]
// Use the default slide transition when moving to the left page
alpha = 1 - abs(position)
zIndex = 1
} else { // (1,+Infinity]
// This page is way off-screen to the right.
alpha = 0
zIndex = Int.min
}
attributes.alpha = alpha
attributes.transform = transform
attributes.zIndex = zIndex
case .zoomOut:
var alpha: CGFloat = 0
var transform = CGAffineTransform.identity
switch position {
case -CGFloat.greatestFiniteMagnitude ..< -1 : // [-Infinity,-1)
// This page is way off-screen to the left.
alpha = 0
case -1 ... 1 : // [-1,1]
// Modify the default slide transition to shrink the page as well
let scaleFactor = max(self.minimumScale, 1 - abs(position))
transform.a = scaleFactor
transform.d = scaleFactor
switch scrollDirection {
case .horizontal:
let vertMargin = attributes.bounds.height * (1 - scaleFactor) / 2;
let horzMargin = itemSpacing * (1 - scaleFactor) / 2;
transform.tx = position < 0 ? (horzMargin - vertMargin*2) : (-horzMargin + vertMargin*2)
case .vertical:
let horzMargin = attributes.bounds.width * (1 - scaleFactor) / 2;
let vertMargin = itemSpacing * (1 - scaleFactor) / 2;
transform.ty = position < 0 ? (vertMargin - horzMargin*2) : (-vertMargin + horzMargin*2)
}
// Fade the page relative to its size.
alpha = self.minimumAlpha + (scaleFactor-self.minimumScale)/(1-self.minimumScale)*(1-self.minimumAlpha)
case 1 ... CGFloat.greatestFiniteMagnitude : // (1,+Infinity]
// This page is way off-screen to the right.
alpha = 0
default:
break
}
attributes.alpha = alpha
attributes.transform = transform
case .depth:
var transform = CGAffineTransform.identity
var zIndex = 0
var alpha: CGFloat = 0.0
switch position {
case -CGFloat.greatestFiniteMagnitude ..< -1: // [-Infinity,-1)
// This page is way off-screen to the left.
alpha = 0
zIndex = 0
case -1 ... 0: // [-1,0]
// Use the default slide transition when moving to the left page
alpha = 1
transform.tx = 0
transform.a = 1
transform.d = 1
zIndex = 1
case 0 ..< 1: // (0,1)
// Fade the page out.
alpha = CGFloat(1.0) - position
// Counteract the default slide transition
switch scrollDirection {
case .horizontal:
transform.tx = itemSpacing * -position
case .vertical:
transform.ty = itemSpacing * -position
}
// Scale the page down (between minimumScale and 1)
let scaleFactor = self.minimumScale
+ (1.0 - self.minimumScale) * (1.0 - abs(position));
transform.a = scaleFactor
transform.d = scaleFactor
zIndex = 0
case 1 ... CGFloat.greatestFiniteMagnitude: // [1,+Infinity)
// This page is way off-screen to the right.
alpha = 0
zIndex = 0
default:
break
}
attributes.alpha = alpha
attributes.transform = transform
attributes.zIndex = zIndex
case .overlap,.linear:
guard scrollDirection == .horizontal else {
// This type doesn't support vertical mode
return
}
let scale = max(1 - (1-self.minimumScale) * abs(position), self.minimumScale)
let translate = 1 - (scale - self.minimumScale) / (1 - self.minimumScale);
var transform = CATransform3DIdentity
transform = CATransform3DTranslate(transform, 0, 28 * translate, 0);
transform = CATransform3DScale(transform, scale, scale, 1.0);
attributes.transform3D = transform
let alpha = (self.minimumAlpha + (1-abs(position))*(1-self.minimumAlpha))
attributes.alpha = alpha
let zIndex = (1-abs(position)) * 10
attributes.zIndex = Int(zIndex)
case .coverFlow:
guard scrollDirection == .horizontal else {
// This type doesn't support vertical mode
return
}
let position = min(max(-position,-1) ,1)
let rotation = sin(position*(.pi)*0.5)*(.pi)*0.25*1.5
let translationZ = -itemSpacing * 0.5 * abs(position)
var transform3D = CATransform3DIdentity
transform3D.m34 = -0.002
transform3D = CATransform3DRotate(transform3D, rotation, 0, 1, 0)
transform3D = CATransform3DTranslate(transform3D, 0, 0, translationZ)
attributes.zIndex = 100 - Int(abs(position))
attributes.transform3D = transform3D
case .ferrisWheel, .invertedFerrisWheel:
guard scrollDirection == .horizontal else {
// This type doesn't support vertical mode
return
}
// http://ronnqvi.st/translate-rotate-translate/
var zIndex = 0
var transform = CGAffineTransform.identity
switch position {
case -5 ... 5:
let itemSpacing = attributes.bounds.width+self.proposedInteritemSpacing()
let count: CGFloat = 14
let circle: CGFloat = .pi * 2.0
let radius = itemSpacing * count / circle
let ty = radius * (self.type == .ferrisWheel ? 1 : -1)
let theta = circle / count
let rotation = position * theta * (self.type == .ferrisWheel ? 1 : -1)
transform = transform.translatedBy(x: -position*itemSpacing, y: ty)
transform = transform.rotated(by: rotation)
transform = transform.translatedBy(x: 0, y: -ty)
zIndex = Int((4.0-abs(position)*10))
default:
break
}
attributes.alpha = abs(position) < 0.5 ? 1 : self.minimumAlpha
attributes.transform = transform
attributes.zIndex = zIndex
case .cubic:
switch position {
case -CGFloat.greatestFiniteMagnitude ... -1:
attributes.alpha = 0
case -1 ..< 1:
attributes.alpha = 1
attributes.zIndex = Int((1-position) * CGFloat(10))
let direction: CGFloat = position < 0 ? 1 : -1
let theta = position * .pi * 0.5 * (scrollDirection == .horizontal ? 1 : -1)
let radius = scrollDirection == .horizontal ? attributes.bounds.width : attributes.bounds.height
var transform3D = CATransform3DIdentity
transform3D.m34 = -0.002
switch scrollDirection {
case .horizontal:
// ForwardX -> RotateY -> BackwardX
attributes.center.x += direction*radius*0.5 // ForwardX
transform3D = CATransform3DRotate(transform3D, theta, 0, 1, 0) // RotateY
transform3D = CATransform3DTranslate(transform3D,-direction*radius*0.5, 0, 0) // BackwardX
case .vertical:
// ForwardY -> RotateX -> BackwardY
attributes.center.y += direction*radius*0.5 // ForwardY
transform3D = CATransform3DRotate(transform3D, theta, 1, 0, 0) // RotateX
transform3D = CATransform3DTranslate(transform3D,0, -direction*radius*0.5, 0) // BackwardY
}
attributes.transform3D = transform3D
case 1 ... CGFloat.greatestFiniteMagnitude:
attributes.alpha = 0
default:
attributes.alpha = 0
attributes.zIndex = 0
}
}
}
open override func proposedInteritemSpacing() -> CGFloat {
return pagerView?.interitemSpacing ?? 0
}
}

View File

@ -0,0 +1,194 @@
//
// BRVideoDetailRecommendView.swift
// BeeReel
//
// Created by 鸿 on 2025/7/28.
//
import UIKit
import FSPagerView
class BRVideoDetailRecommendView: BRPanModalContentView {
var didDismissBlock: (() -> Void)?
var didSelectedVideo: ((_ model: BRShortModel) -> Void)?
var videoList: [BRShortModel] = [] {
didSet {
self.bannerView.reloadData()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
self?.updateCurrentData()
}
}
}
private var currentCell: BRVideoDetailRecommendCell? {
didSet {
oldValue?.isCurrent = false
oldValue?.pause()
currentCell?.isCurrent = true
currentCell?.play()
}
}
private var isClickWatchButton = false
private lazy var videoNameLabel: UILabel = {
let label = UILabel()
label.font = .fontBold(ofSize: 18)
label.textColor = .colorFFFFFF()
return label
}()
private lazy var bannerView: FSPagerView = {
// let transformer = BRVideoDetailRecommendTransformer(type: .linear)
// transformer.minimumScale = 0.8
let view = FSPagerView()
view.transformer = BRVideoDetailRecommendTransformer(type: .linear)
view.transformer?.minimumScale = 0.75
view.transformer?.minimumAlpha = 1
// view.decelerationDistance = FSPagerView.automaticDistance
view.itemSize = .init(width: 180, height: 244)
view.isInfinite = true
view.delegate = self
view.dataSource = self
view.interitemSpacing = 25
view.register(BRVideoDetailRecommendCell.self, forCellWithReuseIdentifier: "cell")
return view
}()
private lazy var iconImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "箭头"))
return imageView
}()
private lazy var watchButton: UIButton = {
let button = UIButton(type: .custom)
button.setBackgroundImage(UIImage(named: "bg 1"), for: .normal)
button.layer.cornerRadius = 24
button.layer.borderWidth = 1
button.layer.borderColor = UIColor.color499C20(alpha: 0.6).cgColor
button.layer.shadowOpacity = 1
button.layer.shadowRadius = 15
button.layer.shadowOffset = .init(width: 0, height: 0)
button.setTitle("Watch Now".localized, for: .normal)
button.setTitleColor(.color1C1C1C(), for: .normal)
button.titleLabel?.font = .fontMedium(ofSize: 15)
button.addTarget(self, action: #selector(handleWatchButton), for: .touchUpInside)
return button
}()
deinit {
NotificationCenter.default.removeObserver(self)
}
override init(frame: CGRect) {
super.init(frame: frame)
self.contentHeight = 433 + UIScreen.tabbarSafeBottomMargin
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActiveNotification), name: UIApplication.didBecomeActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willResignActiveNotification), name: UIApplication.willResignActiveNotification, object: nil)
br_setupUI()
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func panModalDidDismissed() {
super.panModalDidDismissed()
if !isClickWatchButton {
self.didDismissBlock?()
}
}
///
private func updateCurrentData() {
guard let cell = self.bannerView.cellForItem(at: self.bannerView.currentIndex) as? BRVideoDetailRecommendCell else { return }
self.currentCell = cell
let model = cell.model
self.videoNameLabel.text = model?.name
}
@objc private func handleWatchButton() {
self.isClickWatchButton = true
self.dismiss(animated: true) {
}
self.didSelectedVideo?(videoList[self.bannerView.currentIndex])
}
}
extension BRVideoDetailRecommendView {
private func br_setupUI() {
addSubview(videoNameLabel)
addSubview(bannerView)
addSubview(iconImageView)
addSubview(watchButton)
videoNameLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.right.lessThanOrEqualToSuperview().offset(-15)
make.top.equalToSuperview().offset(26)
}
bannerView.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.top.equalTo(74)
make.height.equalTo(244)
}
iconImageView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.centerY.equalTo(bannerView.snp.bottom).offset(5)
}
watchButton.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().offset(-(UIScreen.tabbarSafeBottomMargin + 6))
make.width.equalTo(260)
make.height.equalTo(48)
}
}
}
extension BRVideoDetailRecommendView: FSPagerViewDelegate, FSPagerViewDataSource {
func numberOfItems(in pagerView: FSPagerView) -> Int {
return videoList.count
}
func pagerView(_ pagerView: FSPagerView, cellForItemAt index: Int) -> FSPagerViewCell {
let cell = pagerView.dequeueReusableCell(withReuseIdentifier: "cell", at: index) as! BRVideoDetailRecommendCell
cell.model = videoList[index]
return cell
}
func pagerViewDidEndDecelerating(_ pagerView: FSPagerView) {
self.updateCurrentData()
}
}
extension BRVideoDetailRecommendView {
@objc func didBecomeActiveNotification() {
self.currentCell?.play()
}
@objc func willResignActiveNotification() {
self.currentCell?.pause()
}
}

View File

@ -9,6 +9,7 @@ import UIKit
class BRVideoLockView: UIView {
var clickUnlockButton: (() -> Void)?
var videoInfo: BRVideoInfoModel? {
didSet {
@ -16,7 +17,12 @@ class BRVideoLockView: UIView {
}
}
var clickUnlockButton: (() -> Void)?
var hasLastEpisodeUnlocked = false {
didSet {
lockButton.setNeedsUpdateConfiguration()
}
}
private lazy var lockIconView: UIImageView = {
let view = UIImageView(image: UIImage(named: "Frame 4"))
@ -47,7 +53,13 @@ class BRVideoLockView: UIView {
button.configurationUpdateHandler = { [weak self] button in
guard let self = self else { return }
let title = "Unlocking costs ## Coins".localizedReplace(text: "\(videoInfo?.coins ?? 0)")
let title: String
if hasLastEpisodeUnlocked {
title = "beereel_video_lock_tip_text".localized
} else {
title = "Unlocking costs ## Coins".localizedReplace(text: "\(videoInfo?.coins ?? 0)")
}
button.configuration?.attributedTitle = AttributedString.br_createAttributedString(string: title, color: .color1C1C1C(), font: .fontRegular(ofSize: 14))
}

View File

@ -31,6 +31,9 @@ class BRPlayerViewModel: NSObject {
weak var playerListVC: BRPlayerListViewController?
///
private(set) lazy var recommandList: [BRShortModel] = []
@objc dynamic var isPlaying: Bool = true
var currentIndexPath = IndexPath(row: 0, section: 0)
@ -192,4 +195,14 @@ extension BRPlayerViewModel {
BRVideoAPI.requestUploadPlayTime(shortPlayId: shortPlayId, videoId: videoId, seconds: Int(time) * 1000)
}
///
func asyncGetRecommandDataArr() {
BRVideoAPI.requestDetailsRecommand { [weak self] list in
guard let self = self else { return }
if let list = list {
self.recommandList = list
}
}
}
}

View File

@ -70,9 +70,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
extension SceneDelegate {
private func startApp() {
// let hasOpenApp = UserDefaults.standard.object(forKey: kBRHasBeenOpenedAPPDefaultsKey) as? Bool
//
// if hasOpenApp != true {
let hasOpenApp = UserDefaults.standard.object(forKey: kBRHasBeenOpenedAPPDefaultsKey) as? Bool
if hasOpenApp != true {
let guideVc = BRGuideViewController()
guideVc.clickStartButton = { [weak self] in
guard let self = self else { return }
@ -81,9 +81,9 @@ extension SceneDelegate {
}
window?.rootViewController = guideVc
window?.makeKeyAndVisible()
// } else {
// openApp()
// }
} else {
openApp()
}
}
private func openApp() {

View File

@ -61,6 +61,8 @@ class BRPlayer: NSObject {
return self.player.presentView.placeholderImageView
}
var isLoop = false
///
var duration: TimeInterval {
return self.player.duration
@ -142,7 +144,11 @@ extension BRPlayer {
//
self.player.playbackObserver.playbackDidFinishExeBlock = { [weak self] player in
guard let self = self else { return }
self.delegate?.br_playerDidPlayFinish?(self)
if self.isLoop {
self.replay()
} else {
self.delegate?.br_playerDidPlayFinish?(self)
}
}
//
self.player.playbackObserver.playbackStatusDidChangeExeBlock = { [weak self] player in

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>AIzaSyDpxwKO6oHYZEvxJOnJVfdCJbBDyUAZzBU</string>
<key>GCM_SENDER_ID</key>
<string>75119644836</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.breeltv.beereel</string>
<key>PROJECT_ID</key>
<string>beereel-6843e</string>
<key>STORAGE_BUCKET</key>
<string>beereel-6843e.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:75119644836:ios:216e375865f2e011a25631</string>
</dict>
</plist>

View File

@ -2,6 +2,17 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>beereelapp</string>
</array>
</dict>
</array>
<key>FirebaseAppDelegateProxyEnabled</key>
<false/>
<key>UIAppFonts</key>
@ -25,5 +36,10 @@
</array>
</dict>
</dict>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
<string>fetch</string>
</array>
</dict>
</plist>

View File

@ -102,6 +102,18 @@
}
}
},
"beereel_video_lock_tip_text" : {
"comment" : "请解锁上一集",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Please unlock the previous episode"
}
}
}
},
"Browse Genres" : {
"extractionState" : "manual",
"localizations" : {
@ -806,6 +818,17 @@
}
}
},
"Watch Now" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Watch Now"
}
}
}
},
"week" : {
"extractionState" : "manual",
"localizations" : {

View File

@ -82,7 +82,16 @@
attributes.transform3D = CATransform3DMakeScale(1.0, 1.0, 1.0);
}
}else{
attributes.transform3D = CATransform3DMakeScale(1.0, zoom, 1.0);
CGFloat scaleFactor = 1 - self.param.wScaleFactor;
CGFloat translate = 1 - (zoom - scaleFactor) / (1 - scaleFactor);
CATransform3D transform = CATransform3DIdentity;
transform = CATransform3DTranslate(transform, 0, 45 * translate, 0);
transform = CATransform3DScale(transform, zoom, zoom, 1.0);
attributes.transform3D = transform;
// CATransform3D transform3D = CATransform3DMakeScale(zoom, zoom, 1.0);
//
// attributes.transform3D = CATransform3DTranslate(transform3D, 0, 30, 0);
}
if (self.param.wAlpha<1) {
CGFloat collectionCenter = self.collectionView.frame.size.width / 2 ;