播放器页面完成
@ -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 */,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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"
|
||||
|
||||
|
||||
///用户协议
|
||||
|
90
BeeReel/Base/View/BRPanModalContentView.swift
Normal 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
|
||||
}
|
||||
}
|
21
BeeReel/Base/View/BRScrollView.swift
Normal 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")
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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?()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 }
|
||||
|
74
BeeReel/Class/Player/Model/BRRateModel.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
|
93
BeeReel/Class/Player/View/BRDetailEpButton.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -9,6 +9,7 @@ import UIKit
|
||||
|
||||
class BRDetailPlayerCell: BRPlayerListCell {
|
||||
|
||||
|
||||
override var ControlViewClass: BRPlayerControlView.Type {
|
||||
return BRDetailControlView.self
|
||||
}
|
||||
|
138
BeeReel/Class/Player/View/BREpisodeMenuView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
69
BeeReel/Class/Player/View/BREpisodeSelectorCell.swift
Normal 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")
|
||||
}
|
||||
|
||||
}
|
263
BeeReel/Class/Player/View/BREpisodeSelectorView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
43
BeeReel/Class/Player/View/BRRateSelectorCell.swift
Normal 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")
|
||||
}
|
||||
|
||||
}
|
109
BeeReel/Class/Player/View/BRRateSelectorView.swift
Normal 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()
|
||||
}
|
||||
|
||||
}
|
209
BeeReel/Class/Player/View/BRVideoProgressView.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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: "播放器销毁")
|
||||
}
|
||||
|
@ -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"
|
||||
|
After Width: | Height: | Size: 714 KiB |
22
BeeReel/Sources/Assets.xcassets/icon/arrow_top_icon_01.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
BeeReel/Sources/Assets.xcassets/icon/arrow_top_icon_01.imageset/Frame@2x.png
vendored
Normal file
After Width: | Height: | Size: 268 B |
BIN
BeeReel/Sources/Assets.xcassets/icon/arrow_top_icon_01.imageset/Frame@3x.png
vendored
Normal file
After Width: | Height: | Size: 312 B |
22
BeeReel/Sources/Assets.xcassets/icon/ep_icon_01.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
BeeReel/Sources/Assets.xcassets/icon/ep_icon_01.imageset/Frame@2x.png
vendored
Normal file
After Width: | Height: | Size: 350 B |
BIN
BeeReel/Sources/Assets.xcassets/icon/ep_icon_01.imageset/Frame@3x.png
vendored
Normal file
After Width: | Height: | Size: 485 B |
22
BeeReel/Sources/Assets.xcassets/icon/favorite_icon_02_selected.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
BeeReel/Sources/Assets.xcassets/icon/favorite_icon_02_selected.imageset/Frame@2x.png
vendored
Normal file
After Width: | Height: | Size: 745 B |
BIN
BeeReel/Sources/Assets.xcassets/icon/favorite_icon_02_selected.imageset/Frame@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.1 KiB |
22
BeeReel/Sources/Assets.xcassets/icon/favorite_icon_03.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
BeeReel/Sources/Assets.xcassets/icon/favorite_icon_03.imageset/未选中收藏@2x.png
vendored
Normal file
After Width: | Height: | Size: 859 B |
BIN
BeeReel/Sources/Assets.xcassets/icon/favorite_icon_03.imageset/未选中收藏@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.3 KiB |
22
BeeReel/Sources/Assets.xcassets/icon/favorite_icon_03_selected.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
BeeReel/Sources/Assets.xcassets/icon/favorite_icon_03_selected.imageset/收藏1 2@2x.png
vendored
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
BeeReel/Sources/Assets.xcassets/icon/favorite_icon_03_selected.imageset/收藏1 2@3x.png
vendored
Normal file
After Width: | Height: | Size: 24 KiB |
22
BeeReel/Sources/Assets.xcassets/icon/light_icon_01.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
BeeReel/Sources/Assets.xcassets/icon/light_icon_01.imageset/选中光效@2x.png
vendored
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
BeeReel/Sources/Assets.xcassets/icon/light_icon_01.imageset/选中光效@3x.png
vendored
Normal file
After Width: | Height: | Size: 12 KiB |
22
BeeReel/Sources/Assets.xcassets/image/highlight_top_image.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
BeeReel/Sources/Assets.xcassets/image/highlight_top_image.imageset/Ellipse 13@2x.png
vendored
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
BeeReel/Sources/Assets.xcassets/image/highlight_top_image.imageset/Ellipse 13@3x.png
vendored
Normal file
After Width: | Height: | Size: 136 KiB |
@ -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" : {
|
||||
|