播放器页面完成

This commit is contained in:
zjx 2025-07-21 19:58:36 +08:00
parent 108bf3141b
commit 698f1b941d
50 changed files with 1782 additions and 70 deletions

View File

@ -8,6 +8,16 @@
/* Begin PBXBuildFile section */
440A41A6E6A22A02807AE759 /* Pods_BeeReel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 899B3015B03D5E1A5A6507EB /* Pods_BeeReel.framework */; };
BF02B7E12E2DE64200172177 /* BRVideoProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF02B7E02E2DE64200172177 /* BRVideoProgressView.swift */; };
BF02B7E32E2E08BD00172177 /* BRDetailEpButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF02B7E22E2E08BD00172177 /* BRDetailEpButton.swift */; };
BF02B7E52E2E1E6100172177 /* BREpisodeSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF02B7E42E2E1E6100172177 /* BREpisodeSelectorView.swift */; };
BF02B7E72E2E1F0500172177 /* BRPanModalContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF02B7E62E2E1F0500172177 /* BRPanModalContentView.swift */; };
BF02B7E92E2E29E900172177 /* BREpisodeSelectorCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF02B7E82E2E29E900172177 /* BREpisodeSelectorCell.swift */; };
BF02B7EB2E2E388800172177 /* BREpisodeMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF02B7EA2E2E388800172177 /* BREpisodeMenuView.swift */; };
BF02B7ED2E2E390500172177 /* BRScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF02B7EC2E2E390500172177 /* BRScrollView.swift */; };
BF02B7EF2E2E4BFD00172177 /* BRRateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF02B7EE2E2E4BFD00172177 /* BRRateModel.swift */; };
BF02B7F12E2E55E300172177 /* BRRateSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF02B7F02E2E55E300172177 /* BRRateSelectorView.swift */; };
BF02B7F32E2E571600172177 /* BRRateSelectorCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF02B7F22E2E571600172177 /* BRRateSelectorCell.swift */; };
BF0DBDD12E0D4E150035F6B4 /* BRTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0DBDD02E0D4E150035F6B4 /* BRTabBar.swift */; };
BF3338E82E15219500B10F76 /* UINavigationBar+BRAdd.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3338E72E15218F00B10F76 /* UINavigationBar+BRAdd.swift */; };
BF3338EA2E152B8100B10F76 /* BRPlayerCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3338E92E152B8100B10F76 /* BRPlayerCache.swift */; };
@ -113,6 +123,16 @@
/* Begin PBXFileReference section */
86290EBFA8B93C91B3BAD835 /* Pods-ShortBox.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShortBox.debug.xcconfig"; path = "Target Support Files/Pods-ShortBox/Pods-ShortBox.debug.xcconfig"; sourceTree = "<group>"; };
899B3015B03D5E1A5A6507EB /* Pods_BeeReel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_BeeReel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BF02B7E02E2DE64200172177 /* BRVideoProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRVideoProgressView.swift; sourceTree = "<group>"; };
BF02B7E22E2E08BD00172177 /* BRDetailEpButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRDetailEpButton.swift; sourceTree = "<group>"; };
BF02B7E42E2E1E6100172177 /* BREpisodeSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BREpisodeSelectorView.swift; sourceTree = "<group>"; };
BF02B7E62E2E1F0500172177 /* BRPanModalContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRPanModalContentView.swift; sourceTree = "<group>"; };
BF02B7E82E2E29E900172177 /* BREpisodeSelectorCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BREpisodeSelectorCell.swift; sourceTree = "<group>"; };
BF02B7EA2E2E388800172177 /* BREpisodeMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BREpisodeMenuView.swift; sourceTree = "<group>"; };
BF02B7EC2E2E390500172177 /* BRScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRScrollView.swift; sourceTree = "<group>"; };
BF02B7EE2E2E4BFD00172177 /* BRRateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRRateModel.swift; sourceTree = "<group>"; };
BF02B7F02E2E55E300172177 /* BRRateSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRRateSelectorView.swift; sourceTree = "<group>"; };
BF02B7F22E2E571600172177 /* BRRateSelectorCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRRateSelectorCell.swift; sourceTree = "<group>"; };
BF0DBDD02E0D4E150035F6B4 /* BRTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRTabBar.swift; sourceTree = "<group>"; };
BF3338E72E15218F00B10F76 /* UINavigationBar+BRAdd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+BRAdd.swift"; sourceTree = "<group>"; };
BF3338E92E152B8100B10F76 /* BRPlayerCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRPlayerCache.swift; sourceTree = "<group>"; };
@ -525,6 +545,13 @@
BF3338EB2E154BFE00B10F76 /* BRPlayerControlView.swift */,
BF3338F62E16176900B10F76 /* BRDetailPlayerCell.swift */,
BF3338F82E16178700B10F76 /* BRDetailControlView.swift */,
BF02B7E02E2DE64200172177 /* BRVideoProgressView.swift */,
BF02B7E22E2E08BD00172177 /* BRDetailEpButton.swift */,
BF02B7E42E2E1E6100172177 /* BREpisodeSelectorView.swift */,
BF02B7E82E2E29E900172177 /* BREpisodeSelectorCell.swift */,
BF02B7EA2E2E388800172177 /* BREpisodeMenuView.swift */,
BF02B7F02E2E55E300172177 /* BRRateSelectorView.swift */,
BF02B7F22E2E571600172177 /* BRRateSelectorCell.swift */,
);
path = View;
sourceTree = "<group>";
@ -535,6 +562,7 @@
BF3338FC2E1626A500B10F76 /* BRPlayerControlProtocol.swift */,
BFC676802E122733006659E5 /* BRPlayerProtocol.swift */,
BFC676862E122E36006659E5 /* BRVideoDetailModel.swift */,
BF02B7EE2E2E4BFD00172177 /* BRRateModel.swift */,
);
path = Model;
sourceTree = "<group>";
@ -557,6 +585,8 @@
BFC6766C2E0E3A8D006659E5 /* BRImageView.swift */,
BFC676722E0E938B006659E5 /* BRTableView.swift */,
BFC676742E0E93B3006659E5 /* BRTableViewCell.swift */,
BF02B7E62E2E1F0500172177 /* BRPanModalContentView.swift */,
BF02B7EC2E2E390500172177 /* BRScrollView.swift */,
);
path = View;
sourceTree = "<group>";
@ -845,6 +875,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BF02B7F32E2E571600172177 /* BRRateSelectorCell.swift in Sources */,
BFC676992E1280E3006659E5 /* BRSpotlightRecommandCell.swift in Sources */,
BFC676A42E129D60006659E5 /* BRHomeTop10Cell.swift in Sources */,
BF692B3C2E0A8D0200A5C2DA /* BRNavigationController.swift in Sources */,
@ -862,14 +893,17 @@
BF692B242E0A825B00A5C2DA /* BRCryptorService.swift in Sources */,
BF692B342E0A87C800A5C2DA /* UIDevice+BRAdd.swift in Sources */,
BF692B3E2E0A8D2300A5C2DA /* BRTabBarController.swift in Sources */,
BF02B7E12E2DE64200172177 /* BRVideoProgressView.swift in Sources */,
BF692B542E0AA8FA00A5C2DA /* BRCollectionView.swift in Sources */,
BF3338E82E15219500B10F76 /* UINavigationBar+BRAdd.swift in Sources */,
BF692B472E0A9B7900A5C2DA /* BRPlayer.swift in Sources */,
BF692B6E2E0BD4CB00A5C2DA /* BRHomeHeaderView.swift in Sources */,
BF692AFA2E0A6F0900A5C2DA /* BRNetwork.swift in Sources */,
BF02B7E52E2E1E6100172177 /* BREpisodeSelectorView.swift in Sources */,
BF692B6B2E0BC85300A5C2DA /* BRHomeViewController.swift in Sources */,
BF692B7C2E0D3C1300A5C2DA /* BRVideoInfoModel.swift in Sources */,
BFC6766D2E0E3A8D006659E5 /* BRImageView.swift in Sources */,
BF02B7ED2E2E390500172177 /* BRScrollView.swift in Sources */,
BFC6766F2E0E3B5C006659E5 /* UIImageView+BRAdd.swift in Sources */,
BF692B782E0D3A1200A5C2DA /* BRHomeModuleItem.swift in Sources */,
BF692B5A2E0AAADD00A5C2DA /* BRPlayerListCell.swift in Sources */,
@ -901,7 +935,9 @@
BF3338FD2E1626B000B10F76 /* BRPlayerControlProtocol.swift in Sources */,
BF692B582E0AAA6F00A5C2DA /* UIScreen+BRAdd.swift in Sources */,
BF692B1F2E0A804600A5C2DA /* BRLocalizedManager.swift in Sources */,
BF02B7E92E2E29E900172177 /* BREpisodeSelectorCell.swift in Sources */,
BF692B612E0B814F00A5C2DA /* BRTabBarItemContentView.swift in Sources */,
BF02B7F12E2E55E300172177 /* BRRateSelectorView.swift in Sources */,
BF692B012E0A74A200A5C2DA /* BRDefine.swift in Sources */,
BFC6767B2E0E973B006659E5 /* UIStackView+BRAdd.swift in Sources */,
BF3338F32E16169A00B10F76 /* BRExplorePlayerCell.swift in Sources */,
@ -910,6 +946,7 @@
BFC676892E122FDD006659E5 /* BRVideoAPI.swift in Sources */,
BFC6769B2E1285C5006659E5 /* BRPagerViewTransformer.swift in Sources */,
BFC676832E122CC5006659E5 /* BRPlayerViewModel.swift in Sources */,
BF02B7EF2E2E4BFD00172177 /* BRRateModel.swift in Sources */,
BF692B672E0BC6C700A5C2DA /* AppDelegate+BRConfig.swift in Sources */,
BF3338FB2E161CF900B10F76 /* NSNumber+BRAdd.swift in Sources */,
BF692B222E0A820D00A5C2DA /* String+BRAdd.swift in Sources */,
@ -924,7 +961,9 @@
BFC676972E127D3C006659E5 /* BRSpotlightRecommandMainCell.swift in Sources */,
BFC6767F2E121A72006659E5 /* BRSpotlightHotCell.swift in Sources */,
BFC6767D2E0E9809006659E5 /* BRSpotlightHotMainCell.swift in Sources */,
BF02B7EB2E2E388800172177 /* BREpisodeMenuView.swift in Sources */,
BFC676662E0E2C8E006659E5 /* WMZBannerFadeLayout.m in Sources */,
BF02B7E72E2E1F0500172177 /* BRPanModalContentView.swift in Sources */,
BFC676872E122E36006659E5 /* BRVideoDetailModel.swift in Sources */,
BFC676672E0E2C8E006659E5 /* WMZBannerParam.m in Sources */,
BF692B732E0D397700A5C2DA /* BRHomeAPI.swift in Sources */,
@ -933,6 +972,7 @@
BF3338F02E15569600B10F76 /* BRExploreViewController.swift in Sources */,
BF0DBDD12E0D4E150035F6B4 /* BRTabBar.swift in Sources */,
BF692B562E0AA92100A5C2DA /* BRCollectionViewCell.swift in Sources */,
BF02B7E32E2E08BD00172177 /* BRDetailEpButton.swift in Sources */,
BF692B072E0A771C00A5C2DA /* BRModel.swift in Sources */,
BF692B752E0D39D000A5C2DA /* BRListModel.swift in Sources */,
BFC676B92E1385FC006659E5 /* BRPopularPicksSmallCell.swift in Sources */,

View File

@ -66,4 +66,12 @@ extension UIColor {
static func colorFFFDF9(alpha: CGFloat = 1) -> UIColor {
return UIColor(rgb: 0xFFFDF9, alpha: alpha)
}
static func colorDDDDDD(alpha: CGFloat = 1) -> UIColor {
return UIColor(rgb: 0xDDDDDD, alpha: alpha)
}
static func color747474(alpha: CGFloat = 1) -> UIColor {
return UIColor(rgb: 0x747474, alpha: alpha)
}
}

View File

@ -42,7 +42,7 @@ extension UIView {
}
}
///
func setRoundedCorner(topLeft: CGFloat, topRight: CGFloat, bottomLeft: CGFloat, bottomRight: CGFloat) {
func br_setRoundedCorner(topLeft: CGFloat, topRight: CGFloat, bottomLeft: CGFloat, bottomRight: CGFloat) {
//
self.roundedCorner = BRRoundedCorner(topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight)
_updateRoundedCorner()

View File

@ -7,11 +7,11 @@
let BRBaseURL = "https://api-qjwl168.qjwl168.com"
let BRURLPathPrefix = "/velo"
let BRBaseURL = "https://api-breeltv.breeltv.com"
let BRURLPathPrefix = "/reel"
let BRWebBaseURL = "https://www.qjwl168.com"
let BRCampaignWebURL = "https://campaign.qjwl168.com"
let BRWebBaseURL = "https://www.breeltv.com"
let BRCampaignWebURL = "https://campaign.breeltv.com"
///

View File

@ -0,0 +1,90 @@
//
// BRPanModalContentView.swift
// BeeReel
//
// Created by on 2025/7/21.
//
import UIKit
import HWPanModal
class BRPanModalContentView: HWPanModalContentView {
var contentHeight = UIScreen.height * (2 / 3)
var mainScrollView: UIScrollView?
///UI contentSize
func setNeedsLayoutUpdate() {
self.panModalSetNeedsLayoutUpdate()
}
private(set) lazy var topImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "highlight_top_image"))
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .color1C1C1C()
addSubview(topImageView)
topImageView.snp.makeConstraints { make in
make.top.equalToSuperview()
make.centerX.equalToSuperview()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: HWPanModalPresentable
override func panScrollable() -> UIScrollView? {
return mainScrollView
}
override func longFormHeight() -> PanModalHeight {
return PanModalHeightMake(.content, contentHeight)
}
override func showDragIndicator() -> Bool {
return false
}
override func backgroundConfig() -> HWBackgroundConfig {
let config = HWBackgroundConfig()
config.backgroundAlpha = 0.0
return config
}
override func allowsDragToDismiss() -> Bool {
return false
}
///
override func allowsTapBackgroundToDismiss() -> Bool {
return true
}
override func allowsPullDownWhenShortState() -> Bool {
return false
}
override func minVerticalVelocityToTriggerDismiss() -> CGFloat {
return 0
}
override func showsScrollableVerticalScrollIndicator() -> Bool {
return false
}
override func springDamping() -> CGFloat {
return 1
}
override func cornerRadius() -> CGFloat {
return 24
}
}

View File

@ -0,0 +1,21 @@
//
// BRScrollView.swift
// BeeReel
//
// Created by on 2025/7/21.
//
import UIKit
class BRScrollView: UIScrollView {
override init(frame: CGRect) {
super.init(frame: frame)
self.contentInsetAdjustmentBehavior = .never
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -32,7 +32,7 @@ class BRTabBar: UITabBar {
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .color1C1C1C()
self.setRoundedCorner(topLeft: 30, topRight: 30, bottomLeft: 0, bottomRight: 0)
self.br_setRoundedCorner(topLeft: 30, topRight: 30, bottomLeft: 0, bottomRight: 0)
addSubview(topImageView)

View File

@ -47,14 +47,18 @@ class BRExploreViewController: BRPlayerListViewController {
button.setImage(UIImage(named: "episode_icon_01"), for: .normal)
button.setContentHuggingPriority(.required, for: .horizontal)
button.setContentCompressionResistancePriority(.required, for: .horizontal)
button.addTarget(self, action: #selector(handleEpisodeButton), for: .touchUpInside)
return button
}()
private lazy var favoriteButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "favorite_icon_02"), for: .normal)
button.setImage(UIImage(named: "favorite_icon_02_selected"), for: .selected)
button.setImage(UIImage(named: "favorite_icon_02_selected"), for: [.selected, .highlighted])
button.setContentHuggingPriority(.required, for: .horizontal)
button.setContentCompressionResistancePriority(.required, for: .horizontal)
button.addTarget(self, action: #selector(handleFavoriteButton), for: .touchUpInside)
return button
}()
@ -143,18 +147,42 @@ extension BRExploreViewController {
}
extension BRExploreViewController {
@objc private func handleFavoriteButton() {
let shortModel = self.viewModel.currentPlayer?.shortModel
guard let shortPlayId = shortModel?.short_play_id else { return }
let isFavorite = !(shortModel?.is_collect ?? false)
BRVideoAPI.requestFavorite(isFavorite: isFavorite, shortPlayId: shortPlayId, videoId: shortModel?.short_play_video_id) {
}
}
@objc private func handleEpisodeButton() {
let vc = BRVideoDetailViewController()
vc.shortPlayId = self.viewModel.currentPlayer?.shortModel?.short_play_id
self.navigationController?.pushViewController(vc, animated: true)
}
}
//MARK: -------------- BRPlayerListViewControllerDelegate BRPlayerListViewControllerDataSource --------------
extension BRExploreViewController: BRPlayerListViewControllerDelegate, BRPlayerListViewControllerDataSource {
func br_playerListViewController(_ viewController: BRPlayerListViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, oldCell: UICollectionViewCell) -> UICollectionViewCell {
func br_playerListViewController(_ viewController: BRPlayerListViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let model = dataArr[indexPath.row]
if let cell = oldCell as? BRPlayerListCell {
cell.shortModel = model
cell.videoInfo = model.video_info
}
return oldCell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! BRExplorePlayerCell
cell.videoInfo = model.video_info
cell.shortModel = model
cell.didChangeFavoriteState = { [weak self] in
self?.favoriteButton.isSelected = self?.viewModel.currentPlayer?.shortModel?.is_collect ?? true
}
return cell
}
func br_playerListViewController(_ viewController: BRPlayerListViewController, _ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
@ -163,6 +191,7 @@ extension BRExploreViewController: BRPlayerListViewControllerDelegate, BRPlayerL
func br_playerListViewController(_ viewController: BRPlayerListViewController, didChangeIndexPathForVisible indexPath: IndexPath) {
videoNameLabel.text = self.viewModel.currentPlayer?.shortModel?.name
favoriteButton.isSelected = self.viewModel.currentPlayer?.shortModel?.is_collect ?? true
}
}
@ -176,9 +205,11 @@ extension BRExploreViewController {
self.collectionView.isHidden = false
if let listModel = listModel, let list = listModel.list {
if page == 1 {
self.dataArr = list
self.clearData()
self.reloadData()
self.dataArr = list
self.reloadData { [weak self] in
self?.play()
}
} else {
self.addDataArr(dataArr: list)
}

View File

@ -13,5 +13,30 @@ class BRExplorePlayerCell: BRPlayerListCell {
return BRExploreControlView.self
}
var didChangeFavoriteState: (() -> Void)?
deinit {
NotificationCenter.default.removeObserver(self)
}
override init(frame: CGRect) {
super.init(frame: frame)
NotificationCenter.default.addObserver(self, selector: #selector(updateShortFavoriteStateNotification), name: BRVideoAPI.updateShortFavoriteStateNotification, object: nil)
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func updateShortFavoriteStateNotification(sender: Notification) {
guard let userInfo = sender.userInfo else { return }
guard let shortPlayId = userInfo["id"] as? String else { return }
guard let state = userInfo["state"] as? Bool else { return }
guard shortPlayId == self.shortModel?.short_play_id else { return }
self.shortModel?.is_collect = state
self.didChangeFavoriteState?()
}
}

View File

@ -29,10 +29,12 @@ import SJMediaCacheServer
@objc protocol BRPlayerListViewControllerDataSource {
func br_playerListViewController(_ viewController: BRPlayerListViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, oldCell: UICollectionViewCell) -> UICollectionViewCell
func br_playerListViewController(_ viewController: BRPlayerListViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
func br_playerListViewController(_ viewController: BRPlayerListViewController, _ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
@objc optional func br_numberOfSections(in viewController: BRPlayerListViewController) -> Int
}
class BRPlayerListViewController: BRViewController {
@ -75,7 +77,7 @@ class BRPlayerListViewController: BRViewController {
collectionView.showsHorizontalScrollIndicator = false
collectionView.bounces = false
collectionView.scrollsToTop = false
collectionView.register(CellClass.self, forCellWithReuseIdentifier: "playerCell")
collectionView.register(CellClass.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
@ -88,6 +90,7 @@ class BRPlayerListViewController: BRViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.statusBarStyle = .lightContent
self.view.backgroundColor = .color1C1C1C()
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActiveNotification), name: UIApplication.didBecomeActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willResignActiveNotification), name: UIApplication.willResignActiveNotification, object: nil)
@ -201,9 +204,11 @@ extension BRPlayerListViewController {
extension BRPlayerListViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
var cell = collectionView.dequeueReusableCell(withReuseIdentifier: "playerCell", for: indexPath)
if let newCell = self.dataSource?.br_playerListViewController(self, collectionView, cellForItemAt: indexPath, oldCell: cell) {
var cell: UICollectionViewCell
if let newCell = self.dataSource?.br_playerListViewController(self, collectionView, cellForItemAt: indexPath) {
cell = newCell
} else {
cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
}
if let cell = cell as? BRPlayerListCell {
if cell.viewModel == nil {
@ -238,6 +243,14 @@ extension BRPlayerListViewController: UICollectionViewDelegate, UICollectionView
}
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
if let num = self.dataSource?.br_numberOfSections?(in: self) {
return num
} else {
return 1
}
}
//
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
scrollDidEnd(scrollView)
@ -293,6 +306,14 @@ extension BRPlayerListViewController: BRPlayerViewModelDelegate {
}
}
func br_onEpisodeView(viewModel: BRPlayerViewModel) {
}
func br_clickRateButton(viewModel: BRPlayerViewModel) {
}
}
extension BRPlayerListViewController {

View File

@ -21,8 +21,7 @@ class BRVideoDetailViewController: BRPlayerListViewController {
var shortPlayId: String?
var activityId: String?
private var detailModel: BRVideoDetailModel?
private var detailArr: [BRVideoDetailModel] = []
//MARK: UI
private lazy var backButton: UIButton = {
@ -51,13 +50,15 @@ class BRVideoDetailViewController: BRPlayerListViewController {
override var previousVideoUrl: String? {
let index = self.viewModel.currentIndexPath.row - 1
guard index > 0 else { return nil }
return detailModel?.episodeList?[index].video_url
let model = self.detailArr[self.viewModel.currentIndexPath.section]
return model.episodeList?[index].video_url
}
override var nextVideoUrl: String? {
let index = self.viewModel.currentIndexPath.row + 1
guard index < (detailModel?.episodeList?.count ?? 0) else { return nil }
return detailModel?.episodeList?[index].video_url
let model = self.detailArr[self.viewModel.currentIndexPath.section]
guard index < (model.episodeList?.count ?? 0) else { return nil }
return model.episodeList?[index].video_url
}
}
@ -75,25 +76,56 @@ extension BRVideoDetailViewController {
}
//MARK: -------------- BRPlayerViewModelDelegate --------------
//
extension BRVideoDetailViewController {
///
override func br_onEpisodeView(viewModel: BRPlayerViewModel) {
let indexPath = self.viewModel.currentIndexPath
let model = self.detailArr[indexPath.section]
let view = BREpisodeSelectorView()
view.shortModel = model.shortPlayInfo
view.epList = model.episodeList ?? []
view.index = viewModel.currentIndexPath.row
view.didSelectedIndex = { [weak self] index in
self?.scrollToItem(indexPath: IndexPath(row: index, section: indexPath.section), animated: false)
}
view.present(in: nil)
}
override func br_clickRateButton(viewModel: BRPlayerViewModel) {
let view = BRRateSelectorView()
view.didSelectedRate = { [weak self] model in
guard let self = self else { return }
self.viewModel.rateModel = model
}
view.show()
}
}
//MARK: -------------- BRPlayerListViewControllerDataSource BRPlayerListViewControllerDelegate --------------
extension BRVideoDetailViewController: BRPlayerListViewControllerDataSource, BRPlayerListViewControllerDelegate {
func br_playerListViewController(_ viewController: BRPlayerListViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, oldCell: UICollectionViewCell) -> UICollectionViewCell {
func br_playerListViewController(_ viewController: BRPlayerListViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = oldCell as? BRPlayerListCell {
cell.videoInfo = self.detailModel?.episodeList?[indexPath.row]
cell.shortModel = self.detailModel?.shortPlayInfo
}
let model = self.detailArr[indexPath.section]
return oldCell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! BRDetailPlayerCell
cell.videoInfo = model.episodeList?[indexPath.row]
cell.shortModel = model.shortPlayInfo
return cell
}
func br_playerListViewController(_ viewController: BRPlayerListViewController, _ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.detailModel?.episodeList?.count ?? 0
return self.detailArr[section].episodeList?.count ?? 0
}
func br_numberOfSections(in viewController: BRPlayerListViewController) -> Int {
return self.detailArr.count
}
}
extension BRVideoDetailViewController {
@ -110,7 +142,8 @@ extension BRVideoDetailViewController {
guard let self = self else { return }
guard let model = model else { return }
self.detailModel = model
self.detailArr.removeAll()
self.detailArr.append(model)
self.reloadData { [weak self] in
guard let self = self else { return }

View File

@ -0,0 +1,74 @@
//
// BRRateModel.swift
// BeeReel
//
// Created by on 2025/7/21.
//
import UIKit
class BRRateModel: NSObject {
enum Rate: String {
case x0_25 = "0.25"
case x0_5 = "0.5"
case x0_75 = "0.75"
case x1 = "1.0"
case x1_25 = "1.25"
case x1_5 = "1.5"
case x1_75 = "1.75"
case x2 = "2.0"
func getRate() -> Float {
switch self {
case .x0_25:
return 0.25
case .x0_5:
return 0.5
case .x0_75:
return 0.75
case .x1:
return 1
case .x1_25:
return 1.25
case .x1_5:
return 1.5
case .x1_75:
return 1.75
case .x2:
return 2
}
}
}
static func getAllRate() -> [BRRateModel] {
return [
BRRateModel(rate: .x0_25),
BRRateModel(rate: .x0_5),
BRRateModel(rate: .x0_75),
BRRateModel(rate: .x1),
BRRateModel(rate: .x1_25),
BRRateModel(rate: .x1_5),
BRRateModel(rate: .x1_75),
BRRateModel(rate: .x2)
]
}
private(set) var rate: Rate = .x1
init(rate: Rate) {
super.init()
self.rate = rate
}
func formatString() -> String {
return self.rate.rawValue
}
}

View File

@ -9,12 +9,227 @@ import UIKit
class BRDetailControlView: BRPlayerControlView {
/*
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
override var viewModel: BRPlayerViewModel? {
didSet {
viewModel?.addObserver(self, forKeyPath: "rateModel", context: nil)
rateButton.setTitle("\(self.viewModel?.rateModel.formatString() ?? "")x", for: .normal)
}
}
override var videoInfo: BRVideoInfoModel? {
didSet {
epButton.videoInfo = videoInfo
}
}
override var shortModel: BRShortModel? {
didSet {
epButton.shortModel = shortModel
nameLabel.text = shortModel?.name
favoriteButton.isSelected = self.shortModel?.is_collect ?? false
}
}
override var progress: CGFloat {
didSet {
progressView.progress = progress
}
}
///
override var isLoading: Bool {
didSet {
progressView.isLoading = isLoading
}
}
override var currentTime: TimeInterval {
didSet {
updateProgressLabel()
}
}
override var durationTime: TimeInterval {
didSet {
updateProgressLabel()
}
}
//MARK: UI
private lazy var progressView: BRVideoProgressView = {
let view = BRVideoProgressView()
view.insets = .init(top: 18, left: 15, bottom: 0, right: 18)
return view
}()
private lazy var progressLabel: UILabel = {
let label = UILabel()
label.isUserInteractionEnabled = false
label.font = .fontRegular(ofSize: 12)
return label
}()
private lazy var epButton: BRDetailEpButton = {
let button = BRDetailEpButton()
button.addTarget(self, action: #selector(handleEpButton), for: .touchUpInside)
return button
}()
private lazy var rateButton: UIButton = {
let button = UIButton(type: .custom)
button.backgroundColor = epButton.backgroundColor
button.layer.cornerRadius = 15
button.layer.masksToBounds = true
button.titleLabel?.font = .fontRegular(ofSize: 13)
button.setTitleColor(.colorFFFFFF(), for: .normal)
button.setTitle("1.0x", for: .normal)
button.addTarget(self, action: #selector(handleRateButton), for: .touchUpInside)
return button
}()
private lazy var nameLabel: UILabel = {
let label = UILabel()
label.font = .fontMedium(ofSize: 14)
label.textColor = .colorFFFDF9()
return label
}()
private lazy var favoriteButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "favorite_icon_03"), for: .normal)
button.setImage(UIImage(named: "favorite_icon_03_selected"), for: .selected)
button.setImage(UIImage(named: "favorite_icon_03_selected"), for: [.selected, .highlighted])
button.addTarget(self, action: #selector(handleFavoriteButton), for: .touchUpInside)
return button
}()
deinit {
NotificationCenter.default.removeObserver(self)
self.viewModel?.removeObserver(self, forKeyPath: "rateModel")
}
override init(frame: CGRect) {
super.init(frame: frame)
NotificationCenter.default.addObserver(self, selector: #selector(updateShortFavoriteStateNotification), name: BRVideoAPI.updateShortFavoriteStateNotification, object: nil)
br_setupUI()
}
@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?) {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
if keyPath == "rateModel" {
rateButton.setTitle("\(self.viewModel?.rateModel.formatString() ?? "")x", for: .normal)
}
}
}
extension BRDetailControlView {
private func updateProgressLabel() {
let currentTime = Int(self.currentTime).br_formatTimeGroup()
let durationTime = Int(self.durationTime).br_formatTimeGroup()
let string = NSMutableAttributedString()
let str1 = NSMutableAttributedString(string: "\(currentTime.1):\(currentTime.2)/")
str1.yy_color = .colorFFFFFF(alpha: 0.9)
string.append(str1)
let str2 = NSMutableAttributedString(string: "\(durationTime.1):\(durationTime.2)")
str2.yy_color = .colorDDDDDD(alpha: 0.9)
string.append(str2)
progressLabel.attributedText = string
}
@objc private func handleFavoriteButton() {
guard let shortPlayId = self.shortModel?.short_play_id else { return }
let isFavorite = !(self.shortModel?.is_collect ?? false)
let videoId = self.videoInfo?.short_play_video_id
BRVideoAPI.requestFavorite(isFavorite: isFavorite, shortPlayId: shortPlayId, videoId: videoId) {
}
}
@objc private func updateShortFavoriteStateNotification(sender: Notification) {
guard let userInfo = sender.userInfo else { return }
guard let shortPlayId = userInfo["id"] as? String else { return }
guard let state = userInfo["state"] as? Bool else { return }
guard shortPlayId == self.shortModel?.short_play_id else { return }
self.shortModel?.is_collect = state
favoriteButton.isSelected = self.shortModel?.is_collect ?? false
}
@objc private func handleEpButton() {
self.viewModel?.clickEpButton()
}
@objc private func handleRateButton() {
self.viewModel?.clickRateButton()
}
}
extension BRDetailControlView {
private func br_setupUI() {
addSubview(progressView)
progressView.addSubview(progressLabel)
addSubview(epButton)
addSubview(rateButton)
addSubview(nameLabel)
addSubview(favoriteButton)
progressView.snp.makeConstraints { make in
make.left.equalToSuperview()
make.right.equalToSuperview()
make.bottom.equalTo(epButton.snp.top).offset(-10)
}
progressLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15)
make.top.equalToSuperview()
}
epButton.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15)
make.bottom.equalToSuperview().offset(-(UIScreen.tabbarSafeBottomMargin + 10))
make.height.equalTo(30)
}
rateButton.snp.makeConstraints { make in
make.centerY.height.equalTo(epButton)
make.right.equalToSuperview().offset(-15)
make.left.equalTo(epButton.snp.right).offset(10)
make.width.equalTo(50)
}
nameLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15)
make.right.lessThanOrEqualToSuperview().offset(-70)
make.bottom.equalTo(progressView.snp.top).offset(-18)
}
favoriteButton.snp.makeConstraints { make in
make.right.equalToSuperview().offset(-7)
make.bottom.equalTo(progressView.snp.top).offset(-10)
}
}
*/
}

View File

@ -0,0 +1,93 @@
//
// BRDetailEpButton.swift
// BeeReel
//
// Created by on 2025/7/21.
//
import UIKit
class BRDetailEpButton: UIControl {
var videoInfo: BRVideoInfoModel? {
didSet {
currentEpLabel.text = "EP.##".localizedReplace(text: videoInfo?.episode ?? "0")
}
}
var shortModel: BRShortModel? {
didSet {
totalEpLabel.text = "All ## Episodes".localizedReplace(text: "\(shortModel?.episode_total ?? 0)")
}
}
private lazy var iconImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "ep_icon_01"))
return imageView
}()
private lazy var currentEpLabel: UILabel = {
let label = UILabel()
label.font = .fontRegular(ofSize: 13)
label.textColor = .colorFFFFFF()
return label
}()
private lazy var totalEpLabel: UILabel = {
let label = UILabel()
label.textColor = .colorD3D3D3()
label.font = .fontRegular(ofSize: 13)
return label
}()
private lazy var indicatorImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "arrow_top_icon_01"))
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .color1C1C1C(alpha: 0.6)
layer.cornerRadius = 15
layer.masksToBounds = true
br_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension BRDetailEpButton {
private func br_setupUI() {
addSubview(iconImageView)
addSubview(currentEpLabel)
addSubview(indicatorImageView)
addSubview(totalEpLabel)
iconImageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview().offset(10)
}
currentEpLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalTo(iconImageView.snp.right).offset(6)
}
indicatorImageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview().offset(-10)
}
totalEpLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalTo(indicatorImageView.snp.left).offset(-4)
}
}
}

View File

@ -9,6 +9,7 @@ import UIKit
class BRDetailPlayerCell: BRPlayerListCell {
override var ControlViewClass: BRPlayerControlView.Type {
return BRDetailControlView.self
}

View File

@ -0,0 +1,138 @@
//
// BREpisodeMenuView.swift
// BeeReel
//
// Created by on 2025/7/21.
//
import UIKit
class BREpisodeMenuView: UIView {
override var intrinsicContentSize: CGSize {
return CGSize(width: UIScreen.width, height: 35)
}
var didSelectedIndex: ((_ index: Int) -> Void)?
var dataArr: [String] = [] {
didSet {
self.reloadData()
}
}
var selectedIndex: Int = 0 {
didSet {
self.buttonArr.forEach {
$0.isSelected = $0.tag == selectedIndex
}
}
}
private lazy var buttonArr: [UIButton] = []
//MARK: UI
private lazy var scrollView: BRScrollView = {
let scrollView = BRScrollView()
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
return scrollView
}()
override init(frame: CGRect) {
super.init(frame: frame)
br_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func reloadData() {
buttonArr.forEach {
$0.removeFromSuperview()
}
buttonArr.removeAll()
let count = self.dataArr.count
var previousButton: UIButton?
dataArr.enumerated().forEach {
let normalStrig = NSMutableAttributedString(string: $1)
normalStrig.yy_color = .colorD3D3D3()
normalStrig.yy_font = .fontRegular(ofSize: 14)
let selectedString = NSMutableAttributedString(string: $1)
selectedString.yy_color = .colorE3FC37()
selectedString.yy_font = .fontMedium(ofSize: 14)
let button = UIButton(type: .custom)
button.tag = $0
button.setAttributedTitle(normalStrig, for: .normal)
button.setAttributedTitle(selectedString, for: .selected)
button.setAttributedTitle(selectedString, for: [.selected, .highlighted])
button.addTarget(self, action: #selector(handleButton), for: .touchUpInside)
button.isSelected = $0 == selectedIndex
self.scrollView.addSubview(button)
self.buttonArr.append(button)
if previousButton == nil {
button.snp.makeConstraints { make in
make.top.left.equalToSuperview()
make.height.equalTo(35)
}
} else if let previousButton = previousButton, count - 1 == $0 {
button.snp.makeConstraints { make in
make.top.equalToSuperview()
make.left.equalTo(previousButton.snp.right).offset(41)
make.height.equalTo(35)
make.right.equalToSuperview()
}
} else if let previousButton = previousButton {
button.snp.makeConstraints { make in
make.top.equalToSuperview()
make.left.equalTo(previousButton.snp.right).offset(41)
make.height.equalTo(35)
}
}
if let previousButton = previousButton {
let lineView = UIView()
lineView.backgroundColor = .colorD3D3D3()
self.scrollView.addSubview(lineView)
lineView.snp.makeConstraints { make in
make.height.equalTo(12)
make.width.equalTo(1)
make.centerY.equalTo(previousButton)
make.left.equalTo(previousButton.snp.right).offset(20)
}
}
previousButton = button
}
}
@objc private func handleButton(sender: UIButton) {
self.selectedIndex = sender.tag
self.didSelectedIndex?(self.selectedIndex)
}
}
extension BREpisodeMenuView {
private func br_setupUI() {
addSubview(scrollView)
scrollView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
make.bottom.equalToSuperview()
}
}
}

View File

@ -0,0 +1,69 @@
//
// BREpisodeSelectorCell.swift
// BeeReel
//
// Created by on 2025/7/21.
//
import UIKit
class BREpisodeSelectorCell: BRCollectionViewCell {
var model: BRVideoInfoModel? {
didSet {
epLabel.text = model?.episode
}
}
var br_isSelected: Bool = false {
didSet {
if br_isSelected {
self.contentView.layer.borderColor = UIColor.colorE3FC37().cgColor
lightImageView.isHidden = false
} else {
self.contentView.layer.borderColor = UIColor.clear.cgColor
lightImageView.isHidden = true
}
}
}
private lazy var epLabel: UILabel = {
let label = UILabel()
label.font = .fontRegular(ofSize: 14)
label.textColor = .colorFFFFFF()
return label
}()
private lazy var lightImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "light_icon_01"))
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .colorFFFFFF(alpha: 0.1)
contentView.layer.cornerRadius = 6
contentView.layer.masksToBounds = true
contentView.layer.borderWidth = 1
contentView.addSubview(epLabel)
contentView.addSubview(lightImageView)
epLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
}
lightImageView.snp.makeConstraints { make in
make.right.top.equalToSuperview()
}
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,263 @@
//
// BREpisodeSelectorView.swift
// BeeReel
//
// Created by on 2025/7/21.
//
import UIKit
class BREpisodeSelectorView: BRPanModalContentView {
var shortModel: BRShortModel? {
didSet {
coverImageView.br_setImage(url: shortModel?.image_url)
nameLabel.text = shortModel?.name
tagLabel.text = shortModel?.category?.first
contentLabel.text = shortModel?.br_description
}
}
var epList: [BRVideoInfoModel] = [] {
didSet {
self.collectionView.reloadData()
var menuDataArr = [String]()
let totalEpisode = epList.count
var index = 0
var remainingEpisodes = totalEpisode
while remainingEpisodes > 0 {
let minIndex = index * 30
var maxIndex = minIndex + 29
if maxIndex >= epList.count {
maxIndex = epList.count - 1
}
let minEpisode = epList[minIndex].episode ?? "0"
let maxEpisode = epList[maxIndex].episode ?? "0"
if minEpisode == maxEpisode {
menuDataArr.append("\(minEpisode)")
} else {
menuDataArr.append("\(minEpisode)-\(maxEpisode)")
}
remainingEpisodes -= 30
index += 1
}
self.menuView.dataArr = menuDataArr
}
}
var index: Int = 0 {
didSet {
self.collectionView.reloadData()
}
}
var didSelectedIndex: ((_ index: Int) -> Void)?
private var isDecelerating = false
private var isDragging = false
private lazy var coverImageView: BRImageView = {
let imageView = BRImageView()
imageView.layer.cornerRadius = 6
imageView.layer.masksToBounds = true
return imageView
}()
private lazy var nameLabel: UILabel = {
let label = UILabel()
label.font = .fontMedium(ofSize: 14)
label.textColor = .colorFFFFFF()
return label
}()
private lazy var tagLabel: UILabel = {
let label = UILabel()
label.font = .fontRegular(ofSize: 12)
label.textColor = .color899D00()
return label
}()
private lazy var contentLabel: UILabel = {
let label = UILabel()
label.font = .fontRegular(ofSize: 10)
label.textColor = .colorD3D3D3()
label.numberOfLines = 3
return label
}()
private lazy var collectionViewLayout: UICollectionViewFlowLayout = {
let cellWidth = floor((UIScreen.width - 9 * 4 - 30) / 5)
let layout = UICollectionViewFlowLayout()
layout.itemSize = .init(width: cellWidth, height: 54)
layout.minimumLineSpacing = 9
layout.minimumInteritemSpacing = 9
layout.sectionInset = .init(top: 0, left: 15, bottom: 0, right: 15)
return layout
}()
private lazy var collectionView: BRCollectionView = {
let collectionView = BRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.contentInset = .init(top: 0, left: 0, bottom: UIScreen.tabbarSafeBottomMargin, right: 0)
collectionView.register(BREpisodeSelectorCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
private lazy var menuView: BREpisodeMenuView = {
let view = BREpisodeMenuView()
view.didSelectedIndex = { [weak self] index in
guard let self = self else { return }
var row = 0
if index > 0 {
row = index * 30 + 10
let count = self.epList.count
if row >= count {
row = count - 1
}
}
let indexPath = IndexPath.init(row: row, section: 0)
self.collectionView.scrollToItem(at: indexPath, at: .centeredVertically, animated: true)
}
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.mainScrollView = collectionView
br_setupUI()
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension BREpisodeSelectorView {
private func br_setupUI() {
addSubview(coverImageView)
addSubview(nameLabel)
addSubview(tagLabel)
addSubview(contentLabel)
addSubview(collectionView)
addSubview(menuView)
coverImageView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15)
make.top.equalToSuperview().offset(30)
make.width.equalTo(64)
make.height.equalTo(85)
}
nameLabel.snp.makeConstraints { make in
make.top.equalTo(coverImageView)
make.left.equalTo(coverImageView.snp.right).offset(10)
make.right.lessThanOrEqualToSuperview().offset(-15)
}
tagLabel.snp.makeConstraints { make in
make.left.equalTo(nameLabel)
make.top.equalTo(nameLabel.snp.bottom).offset(11)
}
contentLabel.snp.makeConstraints { make in
make.left.equalTo(nameLabel)
make.top.equalTo(tagLabel.snp.bottom).offset(10)
make.right.lessThanOrEqualToSuperview().offset(-15)
}
collectionView.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.bottom.equalToSuperview()
make.top.equalToSuperview().offset(167)
}
menuView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15)
make.right.equalToSuperview().offset(-15)
make.top.equalTo(coverImageView.snp.bottom).offset(13)
}
}
}
//MARK: -------------- UICollectionViewDelegate & UICollectionViewDataSource --------------
extension BREpisodeSelectorView: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return epList.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! BREpisodeSelectorCell
cell.model = epList[indexPath.row]
cell.br_isSelected = indexPath.row == index
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.row == index { return }
self.didSelectedIndex?(indexPath.row)
self.dismiss(animated: true) {
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if isDragging || isDecelerating {
updateMuneSelectedIndex()
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
isDecelerating = false
updateMuneSelectedIndex()
}
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
isDecelerating = true
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
isDragging = true
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
isDragging = false
}
func updateMuneSelectedIndex() {
let indexPathArr = collectionView.indexPathsForVisibleItems
var minRow = epList.count - 1
var maxRow = 0
for indexPath in indexPathArr {
if indexPath.row < minRow {
minRow = indexPath.row
}
if indexPath.row > maxRow {
maxRow = indexPath.row
}
}
let selectedIndex = maxRow / 30
if menuView.selectedIndex != selectedIndex {
menuView.selectedIndex = selectedIndex
}
}
}

View File

@ -11,9 +11,6 @@ import UIKit
class BRPlayerControlView: UIView, BRPlayerControlProtocol {
weak var viewModel: BRPlayerViewModel? {
didSet {
self.viewModel?.addObserver(self, forKeyPath: "isPlaying", context: nil)
@ -32,6 +29,24 @@ class BRPlayerControlView: UIView, BRPlayerControlProtocol {
}
}
///
var panProgressFinishBlock: ((_ progress: CGFloat) -> Void)?
///0-1
var progress: CGFloat = 0 {
didSet {
}
}
///
var isLoading = false {
didSet {
}
}
var durationTime: TimeInterval = 0
var currentTime: TimeInterval = 0
private lazy var playIconImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "play_icon_05"))
return imageView

View File

@ -40,7 +40,11 @@ class BRPlayerListCell: BRCollectionViewCell, BRPlayerProtocol {
var currentPosition: Int = 0
var rate: Float = 1
var rate: Float = 1 {
didSet {
self.player.rate = rate
}
}
func prepare() {
@ -122,4 +126,18 @@ extension BRPlayerListCell: BRPlayerDelegate {
self.viewModel?.playFinish(player: self)
}
func br_playerDurationDidChange(_ player: BRPlayer, duration: TimeInterval) {
self.controlView.durationTime = duration
}
func br_playerCurrentTimeDidChange(_ player: BRPlayer, time: TimeInterval) {
self.controlView.currentTime = time
if player.duration <= 0 {
self.controlView.progress = 0
} else {
self.controlView.progress = time / player.duration
}
}
}

View File

@ -0,0 +1,43 @@
//
// BRRateSelectorCell.swift
// BeeReel
//
// Created by on 2025/7/21.
//
import UIKit
class BRRateSelectorCell: BRTableViewCell {
private(set) lazy var titleLabel: UILabel = {
let label = UILabel()
label.textColor = .colorFFFFFF()
return label
}()
private lazy var bgView: UIView = {
let view = UIView()
view.br_addEffectView(style: .dark)
return view
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
// self.contentView.br_addEffectView(style: .dark)
contentView.addSubview(bgView)
contentView.addSubview(titleLabel)
bgView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
titleLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
}
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,109 @@
//
// BRRateSelectorView.swift
// BeeReel
//
// Created by on 2025/7/21.
//
import UIKit
class BRRateSelectorView: UIView {
var didSelectedRate: ((_ model: BRRateModel) -> Void)?
private lazy var arr = BRRateModel.getAllRate()
private lazy var tableView: BRTableView = {
let tableView = BRTableView(frame: .zero, style: .plain)
tableView.delegate = self
tableView.dataSource = self
tableView.rowHeight = 44
tableView.layer.cornerRadius = 10
tableView.layer.masksToBounds = true
tableView.isScrollEnabled = false
tableView.separatorColor = .color747474()
tableView.separatorInset = .init(top: 0, left: 0, bottom: 0, right: 0)
tableView.register(BRRateSelectorCell.self, forCellReuseIdentifier: "cell")
return tableView
}()
override init(frame: CGRect) {
super.init(frame: frame)
br_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
self.dismiss()
}
func show() {
BRAppTool.keyWindow?.addSubview(self)
self.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
func dismiss() {
self.removeFromSuperview()
}
}
extension BRRateSelectorView {
private func br_setupUI() {
addSubview(tableView)
tableView.snp.makeConstraints { make in
make.right.equalToSuperview().offset(-15)
make.bottom.equalToSuperview().offset(-(UIScreen.tabbarSafeBottomMargin + 45))
make.width.equalTo(100)
make.height.equalTo((arr.count + 1) * 44)
}
}
}
//MARK: -------------- UITableViewDelegate & UITableViewDataSource --------------
extension BRRateSelectorView: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! BRRateSelectorCell
if indexPath.row == 0 {
cell.titleLabel.text = "Speed".localized
cell.titleLabel.font = .fontMedium(ofSize: 14)
} else {
cell.titleLabel.text = arr[indexPath.row - 1].formatString()
cell.titleLabel.font = .fontRegular(ofSize: 14)
}
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arr.count + 1
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard indexPath.row > 0 else { return }
let model = arr[indexPath.row - 1]
self.didSelectedRate?(model)
self.dismiss()
}
}

View File

@ -0,0 +1,209 @@
//
// BRVideoProgressView.swift
// BeeReel
//
// Created by on 2025/7/21.
//
import UIKit
class BRVideoProgressView: 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 = .colorFFFFFF(alpha: 0.2)
var currentProgress: UIColor = .colorFFFFFF()
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: 15, bottom: 0, right: 15) {
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 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()
}
}
}
extension BRVideoProgressView {
@objc func handlePanGesture(sender: UIPanGestureRecognizer) {
switch sender.state {
case .began:
self.isPaning = true
self.tempProgress = self.progress
sender.setTranslation(CGPoint(x: 0, y: 0), in: self)
self.panStart?()
case .changed:
let point = sender.translation(in: self)
let offsetX = point.x / (self.width - self.insets.left - self.insets.right)
self.panProgress = self.tempProgress + offsetX
if self.panProgress < 0 {
self.panProgress = 0
}
self.panChange?(self.panProgress)
setNeedsDisplay()
default:
self.isPaning = false
self.panFinish?(self.panProgress)
self.panProgress = 0
}
}
@objc func handleTapGesture(sender: UITapGestureRecognizer) {
let point = sender.location(in: self)
let offsetX = (point.x - self.insets.left) / (self.width - self.insets.left - self.insets.right)
self.panFinish?(offsetX)
}
}

View File

@ -10,9 +10,13 @@ import UIKit
@objc protocol BRPlayerViewModelDelegate {
@objc optional func br_currentVideoPlayFinish(viewModel: BRPlayerViewModel)
@objc func br_currentVideoPlayFinish(viewModel: BRPlayerViewModel)
@objc optional func br_switchPlayAndPause(viewModel: BRPlayerViewModel)
@objc func br_switchPlayAndPause(viewModel: BRPlayerViewModel)
@objc func br_onEpisodeView(viewModel: BRPlayerViewModel)
@objc func br_clickRateButton(viewModel: BRPlayerViewModel)
}
@ -30,7 +34,14 @@ class BRPlayerViewModel: NSObject {
oldValue?.pause()
self.currentPlayer?.isCurrent = true
// self.currentPlayer?.rate = rateModel.rate.getRate()
self.currentPlayer?.rate = rateModel.rate.getRate()
}
}
///
@objc dynamic lazy var rateModel = BRRateModel(rate: .x1) {
didSet {
self.currentPlayer?.rate = rateModel.rate.getRate()
}
}
}
@ -41,13 +52,22 @@ extension BRPlayerViewModel {
func playFinish(player: BRPlayerProtocol) {
guard (player as? UICollectionViewCell) == (currentPlayer as? UICollectionViewCell) else { return }
self.delegate?.br_currentVideoPlayFinish?(viewModel: self)
self.delegate?.br_currentVideoPlayFinish(viewModel: self)
}
///
func switchPlayAndPause() {
self.delegate?.br_switchPlayAndPause?(viewModel: self)
self.delegate?.br_switchPlayAndPause(viewModel: self)
}
///
func clickEpButton() {
self.delegate?.br_onEpisodeView(viewModel: self)
}
///
func clickRateButton() {
self.delegate?.br_clickRateButton(viewModel: self)
}
}

View File

@ -77,6 +77,15 @@ class BRPlayer: NSObject {
return self.player.currentTime
}
var rate: Float {
get {
return self.player.rate
}
set {
self.player.rate = newValue
}
}
deinit {
brLog(message: "播放器销毁")
}

View File

@ -1,28 +1,7 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"filename" : "GleeStream 1.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 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: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 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: 350 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 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: 745 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 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: 859 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 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: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

View File

@ -1,6 +1,17 @@
{
"sourceLanguage" : "en",
"strings" : {
"All ## Episodes" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "All ## Episodes"
}
}
}
},
"Browse Genres" : {
"extractionState" : "manual",
"localizations" : {
@ -23,6 +34,17 @@
}
}
},
"EP.##" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "EP.##"
}
}
}
},
"Fresh Stories" : {
"extractionState" : "manual",
"localizations" : {
@ -56,6 +78,17 @@
}
}
},
"Speed" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Speed"
}
}
}
},
"Spotlight" : {
"extractionState" : "manual",
"localizations" : {

View File

@ -29,5 +29,6 @@ target 'BeeReel' do
pod 'YYText'
pod 'FSPagerView' #banner
pod 'MJRefresh' #刷新控件
pod 'HWPanModal' #底部弹出控制器
end