diff --git a/BeeReel.xcodeproj/project.pbxproj b/BeeReel.xcodeproj/project.pbxproj index 7a15c39..78a5cbb 100644 --- a/BeeReel.xcodeproj/project.pbxproj +++ b/BeeReel.xcodeproj/project.pbxproj @@ -9,6 +9,16 @@ /* Begin PBXBuildFile section */ 440A41A6E6A22A02807AE759 /* Pods_BeeReel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 899B3015B03D5E1A5A6507EB /* Pods_BeeReel.framework */; }; 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 */; }; + BF3338EC2E154BFE00B10F76 /* BRPlayerControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3338EB2E154BFE00B10F76 /* BRPlayerControlView.swift */; }; + BF3338F02E15569600B10F76 /* BRExploreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3338EF2E15569600B10F76 /* BRExploreViewController.swift */; }; + BF3338F32E16169A00B10F76 /* BRExplorePlayerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3338F22E16169A00B10F76 /* BRExplorePlayerCell.swift */; }; + BF3338F52E1616B200B10F76 /* BRExploreControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3338F42E1616B200B10F76 /* BRExploreControlView.swift */; }; + BF3338F72E16176900B10F76 /* BRDetailPlayerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3338F62E16176900B10F76 /* BRDetailPlayerCell.swift */; }; + BF3338F92E16178700B10F76 /* BRDetailControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3338F82E16178700B10F76 /* BRDetailControlView.swift */; }; + BF3338FB2E161CF900B10F76 /* NSNumber+BRAdd.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3338FA2E161CF000B10F76 /* NSNumber+BRAdd.swift */; }; + BF3338FD2E1626B000B10F76 /* BRPlayerControlProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3338FC2E1626A500B10F76 /* BRPlayerControlProtocol.swift */; }; BF692AEB2E0A475D00A5C2DA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF692AE12E0A475D00A5C2DA /* AppDelegate.swift */; }; BF692AEC2E0A475D00A5C2DA /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF692AE82E0A475D00A5C2DA /* SceneDelegate.swift */; }; BF692AEE2E0A475D00A5C2DA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF692AE22E0A475D00A5C2DA /* Assets.xcassets */; }; @@ -104,6 +114,16 @@ 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 = ""; }; 899B3015B03D5E1A5A6507EB /* Pods_BeeReel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_BeeReel.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BF0DBDD02E0D4E150035F6B4 /* BRTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRTabBar.swift; sourceTree = ""; }; + BF3338E72E15218F00B10F76 /* UINavigationBar+BRAdd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+BRAdd.swift"; sourceTree = ""; }; + BF3338E92E152B8100B10F76 /* BRPlayerCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRPlayerCache.swift; sourceTree = ""; }; + BF3338EB2E154BFE00B10F76 /* BRPlayerControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRPlayerControlView.swift; sourceTree = ""; }; + BF3338EF2E15569600B10F76 /* BRExploreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRExploreViewController.swift; sourceTree = ""; }; + BF3338F22E16169A00B10F76 /* BRExplorePlayerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRExplorePlayerCell.swift; sourceTree = ""; }; + BF3338F42E1616B200B10F76 /* BRExploreControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRExploreControlView.swift; sourceTree = ""; }; + BF3338F62E16176900B10F76 /* BRDetailPlayerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRDetailPlayerCell.swift; sourceTree = ""; }; + BF3338F82E16178700B10F76 /* BRDetailControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRDetailControlView.swift; sourceTree = ""; }; + BF3338FA2E161CF000B10F76 /* NSNumber+BRAdd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSNumber+BRAdd.swift"; sourceTree = ""; }; + BF3338FC2E1626A500B10F76 /* BRPlayerControlProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRPlayerControlProtocol.swift; sourceTree = ""; }; BF692AC92E0A475500A5C2DA /* BeeReel.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BeeReel.app; sourceTree = BUILT_PRODUCTS_DIR; }; BF692AE12E0A475D00A5C2DA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; BF692AE22E0A475D00A5C2DA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -233,6 +253,32 @@ path = Pods; sourceTree = ""; }; + BF3338ED2E15566C00B10F76 /* Explore */ = { + isa = PBXGroup; + children = ( + BF3338EE2E15567200B10F76 /* Controller */, + BF3338F12E16167B00B10F76 /* View */, + ); + path = Explore; + sourceTree = ""; + }; + BF3338EE2E15567200B10F76 /* Controller */ = { + isa = PBXGroup; + children = ( + BF3338EF2E15569600B10F76 /* BRExploreViewController.swift */, + ); + path = Controller; + sourceTree = ""; + }; + BF3338F12E16167B00B10F76 /* View */ = { + isa = PBXGroup; + children = ( + BF3338F22E16169A00B10F76 /* BRExplorePlayerCell.swift */, + BF3338F42E1616B200B10F76 /* BRExploreControlView.swift */, + ); + path = View; + sourceTree = ""; + }; BF692AC02E0A475500A5C2DA = { isa = PBXGroup; children = ( @@ -302,6 +348,7 @@ BF692AF52E0A47D400A5C2DA /* Class */ = { isa = PBXGroup; children = ( + BF3338ED2E15566C00B10F76 /* Explore */, BF692B682E0BC78C00A5C2DA /* Home */, BF692B4A2E0AA84C00A5C2DA /* Player */, ); @@ -370,6 +417,8 @@ BF692B0C2E0A7A9A00A5C2DA /* Extension */ = { isa = PBXGroup; children = ( + BF3338FA2E161CF000B10F76 /* NSNumber+BRAdd.swift */, + BF3338E72E15218F00B10F76 /* UINavigationBar+BRAdd.swift */, BFC676BD2E13A8DD006659E5 /* UIScrollView+BRRefresh.swift */, BFC6768C2E123D67006659E5 /* AttributedString+BRAdd.swift */, BFC6767A2E0E9736006659E5 /* UIStackView+BRAdd.swift */, @@ -444,6 +493,7 @@ isa = PBXGroup; children = ( BF692B462E0A9B7400A5C2DA /* BRPlayer.swift */, + BF3338E92E152B8100B10F76 /* BRPlayerCache.swift */, ); path = Player; sourceTree = ""; @@ -472,6 +522,9 @@ isa = PBXGroup; children = ( BF692B592E0AAADD00A5C2DA /* BRPlayerListCell.swift */, + BF3338EB2E154BFE00B10F76 /* BRPlayerControlView.swift */, + BF3338F62E16176900B10F76 /* BRDetailPlayerCell.swift */, + BF3338F82E16178700B10F76 /* BRDetailControlView.swift */, ); path = View; sourceTree = ""; @@ -479,6 +532,7 @@ BF692B4E2E0AA88600A5C2DA /* Model */ = { isa = PBXGroup; children = ( + BF3338FC2E1626A500B10F76 /* BRPlayerControlProtocol.swift */, BFC676802E122733006659E5 /* BRPlayerProtocol.swift */, BFC676862E122E36006659E5 /* BRVideoDetailModel.swift */, ); @@ -800,6 +854,7 @@ BFC676732E0E938B006659E5 /* BRTableView.swift in Sources */, BFC676932E126A62006659E5 /* BRSpotlightNewMainCell.swift in Sources */, BFC6768D2E123D6E006659E5 /* AttributedString+BRAdd.swift in Sources */, + BF3338F52E1616B200B10F76 /* BRExploreControlView.swift in Sources */, BF692B132E0A7B9000A5C2DA /* BRUserInfo.swift in Sources */, BF692B042E0A76D200A5C2DA /* BRLoginManager.swift in Sources */, BFC6769D2E129794006659E5 /* BRHomeTop10ViewController.swift in Sources */, @@ -808,6 +863,7 @@ BF692B342E0A87C800A5C2DA /* UIDevice+BRAdd.swift in Sources */, BF692B3E2E0A8D2300A5C2DA /* BRTabBarController.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 */, @@ -818,6 +874,8 @@ BF692B782E0D3A1200A5C2DA /* BRHomeModuleItem.swift in Sources */, BF692B5A2E0AAADD00A5C2DA /* BRPlayerListCell.swift in Sources */, BF692B162E0A7CD600A5C2DA /* BRHUD.swift in Sources */, + BF3338F72E16176900B10F76 /* BRDetailPlayerCell.swift in Sources */, + BF3338EA2E152B8100B10F76 /* BRPlayerCache.swift in Sources */, BFC676952E126BBF006659E5 /* BRSpotlightNewCell.swift in Sources */, BF692B402E0A8FA100A5C2DA /* UIColor+BRAdd.swift in Sources */, BF692B102E0A7B4300A5C2DA /* BRUserDefaultsKey.swift in Sources */, @@ -830,6 +888,7 @@ BFC676812E122733006659E5 /* BRPlayerProtocol.swift in Sources */, BFC676BC2E138ABB006659E5 /* BRNewReleasesViewController.swift in Sources */, BF692B5F2E0B812800A5C2DA /* BRTabBarItemContainer.swift in Sources */, + BF3338EC2E154BFE00B10F76 /* BRPlayerControlView.swift in Sources */, BF692B652E0BC53900A5C2DA /* CGMutablePath+BRRoundedCorner.swift in Sources */, BFC676752E0E93B3006659E5 /* BRTableViewCell.swift in Sources */, BF78108B2E0D4EB3007DEEBC /* BRURLPath.swift in Sources */, @@ -839,16 +898,20 @@ BFC676BE2E13A8EB006659E5 /* UIScrollView+BRRefresh.swift in Sources */, BF692B182E0A7D8900A5C2DA /* BRToast.swift in Sources */, BF692B0E2E0A7AF300A5C2DA /* UserDefaults+BRAdd.swift in Sources */, + BF3338FD2E1626B000B10F76 /* BRPlayerControlProtocol.swift in Sources */, BF692B582E0AAA6F00A5C2DA /* UIScreen+BRAdd.swift in Sources */, BF692B1F2E0A804600A5C2DA /* BRLocalizedManager.swift in Sources */, BF692B612E0B814F00A5C2DA /* BRTabBarItemContentView.swift in Sources */, BF692B012E0A74A200A5C2DA /* BRDefine.swift in Sources */, BFC6767B2E0E973B006659E5 /* UIStackView+BRAdd.swift in Sources */, + BF3338F32E16169A00B10F76 /* BRExplorePlayerCell.swift in Sources */, + BF3338F92E16178700B10F76 /* BRDetailControlView.swift in Sources */, BFC676AB2E1372BD006659E5 /* BRHomeTop3Cell.swift in Sources */, BFC676892E122FDD006659E5 /* BRVideoAPI.swift in Sources */, BFC6769B2E1285C5006659E5 /* BRPagerViewTransformer.swift in Sources */, BFC676832E122CC5006659E5 /* BRPlayerViewModel.swift in Sources */, BF692B672E0BC6C700A5C2DA /* AppDelegate+BRConfig.swift in Sources */, + BF3338FB2E161CF900B10F76 /* NSNumber+BRAdd.swift in Sources */, BF692B222E0A820D00A5C2DA /* String+BRAdd.swift in Sources */, BF692B632E0B9D4800A5C2DA /* BRTabBarItem.swift in Sources */, BFC6768B2E123690006659E5 /* BRVideoRevolutionManager.swift in Sources */, @@ -867,6 +930,7 @@ BF692B732E0D397700A5C2DA /* BRHomeAPI.swift in Sources */, BF692B7A2E0D3BD300A5C2DA /* BRShortModel.swift in Sources */, BFC676712E0E9234006659E5 /* BRSpotlightViewViewController.swift in Sources */, + BF3338F02E15569600B10F76 /* BRExploreViewController.swift in Sources */, BF0DBDD12E0D4E150035F6B4 /* BRTabBar.swift in Sources */, BF692B562E0AA92100A5C2DA /* BRCollectionViewCell.swift in Sources */, BF692B072E0A771C00A5C2DA /* BRModel.swift in Sources */, diff --git a/BeeReel/Base/Controller/BRTabBarController.swift b/BeeReel/Base/Controller/BRTabBarController.swift index 0273498..8922c36 100644 --- a/BeeReel/Base/Controller/BRTabBarController.swift +++ b/BeeReel/Base/Controller/BRTabBarController.swift @@ -71,7 +71,7 @@ extension BRTabBarController { private func br_setup() { let nav1 = createNavigationController(viewController: BRHomeViewController(), title: "首页".localized, image: UIImage(named: "tabbar_icon_01"), selectedImage: UIImage(named: "tabbar_icon_01_selected")) - let nav2 = createNavigationController(viewController: BRViewController(), title: "推荐".localized, image: UIImage(named: "tabbar_icon_02"), selectedImage: UIImage(named: "tabbar_icon_02_selected")) + let nav2 = createNavigationController(viewController: BRExploreViewController(), title: "推荐".localized, image: UIImage(named: "tabbar_icon_02"), selectedImage: UIImage(named: "tabbar_icon_02_selected")) let nav3 = createNavigationController(viewController: BRViewController(), title: "首页".localized, image: UIImage(named: "tabbar_icon_03"), selectedImage: UIImage(named: "tabbar_icon_03_selected")) let nav4 = createNavigationController(viewController: BRViewController(), title: "首页".localized, image: UIImage(named: "tabbar_icon_04"), selectedImage: UIImage(named: "tabbar_icon_04_selected")) diff --git a/BeeReel/Base/Controller/BRViewController.swift b/BeeReel/Base/Controller/BRViewController.swift index 92025e9..5fa4396 100644 --- a/BeeReel/Base/Controller/BRViewController.swift +++ b/BeeReel/Base/Controller/BRViewController.swift @@ -25,6 +25,14 @@ class BRViewController: UIViewController { self.isViewDidLoad = true self.view.backgroundColor = .backgroundColor() + if let navi = navigationController { + if navi.visibleViewController == self { + if navi.viewControllers.count > 1 { + configNavigationBack() + } + } + } + } @@ -56,3 +64,69 @@ class BRViewController: UIViewController { completer?() } } + + +extension UIViewController { + func configNavigationBack(_ imageName: String = "nav_back_icon_01") { + let image = UIImage(named: imageName) + + let leftBarButtonItem = UIBarButtonItem(image: image, style: .plain ,target: self,action: #selector(handleNavBack)) + navigationItem.leftBarButtonItem = leftBarButtonItem + } + + @objc func handleNavBack() { + self.br_toLastViewController(animated: true) + } + + func br_toLastViewController(animated: Bool) { + if self.navigationController != nil + { + if self.navigationController?.viewControllers.count == 1 + { + self.dismiss(animated: animated, completion: nil) + } else { + self.navigationController?.popViewController(animated: animated) + } + } + else if self.presentingViewController != nil { + self.dismiss(animated: animated, completion: nil) + } + } +} + +extension UIViewController { + ///设置导航默认样式 + func br_setNavigationNormalStyle(backgroundColor: UIColor = .clear, + isTranslucent: Bool = true, + prefersLargeTitles: Bool = false) + { + self.br_setNavigationBackgroundColor(color: backgroundColor, isTranslucent: isTranslucent) + self.br_setNavigationTitleStyle() + self.navigationController?.navigationBar.prefersLargeTitles = prefersLargeTitles + } + + ///设置导航背景色 + func br_setNavigationBackgroundColor(color: UIColor?, isTranslucent: Bool = false) { + guard let nav = navigationController else { return } + if nav.visibleViewController == self { + nav.navigationBar.br_setBackgroundColor(backgroundColor: color) + nav.navigationBar.br_setTranslucent(isTranslucent: isTranslucent) + } + } + + ///设置标题样式 + func br_setNavigationTitleStyle(color: UIColor? = nil, font: UIFont? = nil) { + guard let nav = navigationController else { return } + if nav.visibleViewController == self { + //标题样式 + var titleTextAttributes = UINavigationBar.br_normalTitleTextAttributes + if let color = color { + titleTextAttributes[NSAttributedString.Key.foregroundColor] = color + } + if let font = font { + titleTextAttributes[NSAttributedString.Key.font] = font + } + nav.navigationBar.br_setTitleTextAttributes(titleTextAttributes: titleTextAttributes) + } + } +} diff --git a/BeeReel/Base/Extension/NSNumber+BRAdd.swift b/BeeReel/Base/Extension/NSNumber+BRAdd.swift new file mode 100644 index 0000000..7928ec7 --- /dev/null +++ b/BeeReel/Base/Extension/NSNumber+BRAdd.swift @@ -0,0 +1,39 @@ +// +// NSNumber+BRAdd.swift +// BeeReel +// +// Created by 湖南秦九 on 2025/7/3. +// + +extension NSNumber { + + func br_toString(maximumFractionDigits: Int = 10, minimumFractionDigits: Int? = nil, roundingMode: NumberFormatter.RoundingMode? = nil) -> String { + let formatter = NumberFormatter() + formatter.minimumIntegerDigits = 1 + formatter.maximumFractionDigits = maximumFractionDigits + if let minimumFractionDigits = minimumFractionDigits { + formatter.minimumFractionDigits = minimumFractionDigits + } + if let roundingMode = roundingMode { + formatter.roundingMode = roundingMode + } + formatter.numberStyle = .none + return formatter.string(from: self) ?? "0" + } +} + + +extension Int { + func br_formatTimeGroup() -> (String, String, String) { + let seconds = self + + var s: String = "00" + var m: String = "00" + var h: String = "00" + s = String(format: "%02d", Int(Int(seconds) % 60)) + m = String(format: "%02d", Int(seconds / 60) % 60) + h = String(format: "%02d", Int(seconds / 3600)) + + return (h, m, s) + } +} diff --git a/BeeReel/Base/Extension/UIColor+BRAdd.swift b/BeeReel/Base/Extension/UIColor+BRAdd.swift index 63b8d65..ffb6194 100644 --- a/BeeReel/Base/Extension/UIColor+BRAdd.swift +++ b/BeeReel/Base/Extension/UIColor+BRAdd.swift @@ -58,4 +58,8 @@ extension UIColor { static func colorE3FC37(alpha: CGFloat = 1) -> UIColor { return UIColor(rgb: 0xE3FC37, alpha: alpha) } + + static func color5A5D45(alpha: CGFloat = 1) -> UIColor { + return UIColor(rgb: 0x5A5D45, alpha: alpha) + } } diff --git a/BeeReel/Base/Extension/UINavigationBar+BRAdd.swift b/BeeReel/Base/Extension/UINavigationBar+BRAdd.swift new file mode 100644 index 0000000..e48742d --- /dev/null +++ b/BeeReel/Base/Extension/UINavigationBar+BRAdd.swift @@ -0,0 +1,88 @@ +// +// UINavigationBar+BRAdd.swift +// BeeReel +// +// Created by 湖南秦九 on 2025/7/2. +// + +import UIKit + +extension UINavigationBar { + + static let br_normalTitleFont = UIFont.fontBold(ofSize: 16) + static var br_normalTitleColor: UIColor { + get { + return .colorFFFFFF() + } + } + + /** + 默认标题样式 + */ + static var br_normalTitleTextAttributes: [NSAttributedString.Key : Any] { + get { + return [ + NSAttributedString.Key.font : br_normalTitleFont, + NSAttributedString.Key.foregroundColor : br_normalTitleColor + ] + } + } + + /** + 默认背景色 + */ + static var br_normalBackgroundColor: UIColor { + get { + return .black + } + } + + @available(iOS 13.0, *) + static let navBarAppearance: UINavigationBarAppearance = { + let navBarAppearance = UINavigationBarAppearance() + navBarAppearance.configureWithOpaqueBackground() + // 背景色 + navBarAppearance.backgroundColor = br_normalBackgroundColor + // 去掉半透明效果 + navBarAppearance.backgroundEffect = nil + // 去除导航栏阴影(如果不设置clear,导航栏底下会有一条阴影线) + navBarAppearance.shadowColor = UIColor.clear + // 字体颜色 + navBarAppearance.titleTextAttributes = br_normalTitleTextAttributes + + return navBarAppearance + }() + + + func br_setTranslucent(isTranslucent: Bool) { + self.isTranslucent = isTranslucent + } + + func br_setBackgroundColor(backgroundColor: UIColor?) { + if #available(iOS 15.0, *) { + UINavigationBar.navBarAppearance.backgroundColor = backgroundColor + self.standardAppearance = UINavigationBar.navBarAppearance + self.scrollEdgeAppearance = UINavigationBar.navBarAppearance + } + + if let backgroundColor = backgroundColor { + self.setBackgroundImage(UIImage(color: backgroundColor), for: .default) + } else { + self.setBackgroundImage(UIImage(), for: .default) + } + } + + func br_setTitleTextAttributes(titleTextAttributes: [NSAttributedString.Key : Any]?) { + + if #available(iOS 15.0, *) { + if let titleTextAttributes = titleTextAttributes { + UINavigationBar.navBarAppearance.titleTextAttributes = titleTextAttributes + } + self.scrollEdgeAppearance = UINavigationBar.navBarAppearance + self.standardAppearance = UINavigationBar.navBarAppearance + } else { + + self.titleTextAttributes = titleTextAttributes + } + } +} diff --git a/BeeReel/Base/Network/API/BRVideoAPI.swift b/BeeReel/Base/Network/API/BRVideoAPI.swift index 782ee7e..0eb1911 100644 --- a/BeeReel/Base/Network/API/BRVideoAPI.swift +++ b/BeeReel/Base/Network/API/BRVideoAPI.swift @@ -66,6 +66,28 @@ class BRVideoAPI { } } } + + ///推荐短剧 + static func requestExploreVideo(page: Int, revolution: BRShortModel.VideoRevolution? = nil, completer: ((_ listModel: BRListModel?) -> Void)?) { + + var parameters: [String : Any] = [ + "page_size" : 20, + "current_page" : page + ] + + if let revolution = revolution?.rawValue { + parameters["revolution"] = revolution + } + + var param = BRNetworkParameters(path: "/getRecommands") + param.method = .get + param.parameters = parameters + + + BRNetwork.request(parameters: param) { (response: BRNetworkResponse>) in + completer?(response.data) + } + } } diff --git a/BeeReel/Class/Explore/Controller/BRExploreViewController.swift b/BeeReel/Class/Explore/Controller/BRExploreViewController.swift new file mode 100644 index 0000000..45b366c --- /dev/null +++ b/BeeReel/Class/Explore/Controller/BRExploreViewController.swift @@ -0,0 +1,123 @@ +// +// BRExploreViewController.swift +// BeeReel +// +// Created by 湖南秦九 on 2025/7/2. +// + +import UIKit + +class BRExploreViewController: BRPlayerListViewController { + + + private lazy var dataArr: [BRShortModel] = [] + private lazy var page: Int = 1 + private var pagination: BRListPaginationModel? + + override var CellClass: BRPlayerListCell.Type { + return BRExplorePlayerCell.self + } + + override var contentSize: CGSize { + let width = UIScreen.width - 30 + let height = UIScreen.height - UIScreen.customTabBarHeight - 40 - 50 - UIScreen.statusBarHeight + return CGSize(width: width, height: height) + } + + override func viewDidLoad() { + super.viewDidLoad() + self.edgesForExtendedLayout = .all + self.delegate = self + self.dataSource = self + + self.requestDataArr(page: 1) + + br_setupUI() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(true, animated: true) + } + + + func addDataArr(dataArr: [BRShortModel]) { + guard dataArr.count > 0 else { return } + + var indexPaths: [IndexPath] = [] + var startRow = self.dataArr.count + + dataArr.forEach { _ in + indexPaths.append(IndexPath(row: startRow, section: 0)) + startRow += 1 + } + self.dataArr += dataArr + + CATransaction.setCompletionBlock(nil) + CATransaction.begin() + self.collectionView.insertItems(at: indexPaths) + CATransaction.commit() + } +} + +extension BRExploreViewController { + + private func br_setupUI() { + self.view.backgroundColor = .color1C1C1C() + self.collectionView.layer.cornerRadius = 20 + self.collectionView.layer.masksToBounds = true + + self.collectionView.snp.remakeConstraints { make in + make.left.equalToSuperview().offset(15) + make.top.equalToSuperview().offset(UIScreen.statusBarHeight + 50) + make.width.equalTo(contentSize.width) + make.height.equalTo(contentSize.height) + } + } + +} + + +//MARK: -------------- BRPlayerListViewControllerDelegate BRPlayerListViewControllerDataSource -------------- +extension BRExploreViewController: BRPlayerListViewControllerDelegate, BRPlayerListViewControllerDataSource { + + func br_playerListViewController(_ viewController: BRPlayerListViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, oldCell: UICollectionViewCell) -> UICollectionViewCell { + let model = dataArr[indexPath.row] + if let cell = oldCell as? BRPlayerListCell { + cell.shortModel = model + cell.videoInfo = model.video_info + } + + return oldCell + } + + func br_playerListViewController(_ viewController: BRPlayerListViewController, _ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return dataArr.count + } +} + + +extension BRExploreViewController { + + private func requestDataArr(page: Int, completer: (() -> Void)? = nil) { + + BRVideoAPI.requestExploreVideo(page: page, revolution: BRVideoRevolutionManager.manager.revolution) { [weak self] listModel in + guard let self = self else { return } + self.collectionView.isHidden = false + if let listModel = listModel, let list = listModel.list { + if page == 1 { + self.dataArr = list + self.clearData() + self.reloadData() + } else { + self.addDataArr(dataArr: list) + } + self.pagination = listModel.pagination + } + completer?() + } + + } + +} + diff --git a/BeeReel/Class/Explore/View/BRExploreControlView.swift b/BeeReel/Class/Explore/View/BRExploreControlView.swift new file mode 100644 index 0000000..380ee5d --- /dev/null +++ b/BeeReel/Class/Explore/View/BRExploreControlView.swift @@ -0,0 +1,96 @@ +// +// BRExploreControlView.swift +// BeeReel +// +// Created by 湖南秦九 on 2025/7/3. +// + +import UIKit + +class BRExploreControlView: BRPlayerControlView { + + + override var videoInfo: BRVideoInfoModel? { + didSet { + } + } + + override var shortModel: BRShortModel? { + didSet { + hotView.setNeedsUpdateConfiguration() + } + } + + private lazy var hotView: UIButton = { + var config = UIButton.Configuration.plain() + config.image = UIImage(named: "hot_icon_03") + config.imagePadding = 3 + config.imagePlacement = .leading + config.contentInsets = .init(top: 0, leading: 10, bottom: 0, trailing: 10) + + let button = UIButton(configuration: config) + button.isUserInteractionEnabled = false + button.backgroundColor = .color5A5D45(alpha: 0.2) + button.layer.cornerRadius = 17 + button.layer.masksToBounds = true + button.configurationUpdateHandler = { [weak self] button in + guard let self = self else { return } + let count = shortModel?.watch_total ?? 0 + var string = "\(count)" + if count >= 1000 { + let num = NSNumber(value: Float(count) / 1000) + string = num.br_toString(maximumFractionDigits: 1, minimumFractionDigits: 0) + "k" + } + button.configuration?.attributedTitle = AttributedString.br_createAttributedString(string: string, color: .colorFFFFFF(), font: .fontMedium(ofSize: 12)) + } + return button + }() + + private lazy var fullButton: UIButton = { + let button = UIButton(type: .custom) + button.setImage(UIImage(named: "full_icon_01"), for: .normal) + button.setBackgroundImage(UIImage(color: .color5A5D45(alpha: 0.2)), for: .normal) + button.layer.cornerRadius = 17 + button.layer.masksToBounds = true + button.addTarget(self, action: #selector(handleFullButton), for: .touchUpInside) + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + br_setupUI() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func handleFullButton() { + let vc = BRVideoDetailViewController() + vc.shortPlayId = self.shortModel?.short_play_id + self.viewController?.navigationController?.pushViewController(vc, animated: true) + } + +} + +extension BRExploreControlView { + + private func br_setupUI() { + addSubview(hotView) + addSubview(fullButton) + + hotView.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-10) + make.top.equalToSuperview().offset(10) + make.height.equalTo(34) + } + + fullButton.snp.makeConstraints { make in + make.left.equalToSuperview().offset(10) + make.centerY.equalTo(hotView) + make.width.equalTo(40) + make.height.equalTo(34) + } + } + +} diff --git a/BeeReel/Class/Explore/View/BRExplorePlayerCell.swift b/BeeReel/Class/Explore/View/BRExplorePlayerCell.swift new file mode 100644 index 0000000..e908931 --- /dev/null +++ b/BeeReel/Class/Explore/View/BRExplorePlayerCell.swift @@ -0,0 +1,17 @@ +// +// BRExplorePlayerCell.swift +// BeeReel +// +// Created by 湖南秦九 on 2025/7/3. +// + +import UIKit + +class BRExplorePlayerCell: BRPlayerListCell { + + override var ControlViewClass: BRPlayerControlView.Type { + return BRExploreControlView.self + } + + +} diff --git a/BeeReel/Class/Player/Controller/BRPlayerListViewController.swift b/BeeReel/Class/Player/Controller/BRPlayerListViewController.swift index 6d5d456..df80a45 100644 --- a/BeeReel/Class/Player/Controller/BRPlayerListViewController.swift +++ b/BeeReel/Class/Player/Controller/BRPlayerListViewController.swift @@ -6,6 +6,7 @@ // import UIKit +import SJMediaCacheServer @objc protocol BRPlayerListViewControllerDelegate { @@ -44,11 +45,18 @@ class BRPlayerListViewController: BRViewController { return BRPlayerListCell.self } - private(set) lazy var viewModel = BRPlayerViewModel() + private(set) lazy var viewModel: BRPlayerViewModel = { + let vm = BRPlayerViewModel() + vm.delegate = self + return vm + }() weak var delegate: BRPlayerListViewControllerDelegate? weak var dataSource: BRPlayerListViewControllerDataSource? + ///预加载 + private var prePrefetchTask: MCSPrefetchTask? + private var nextPrefetchTask: MCSPrefetchTask? private lazy var collectionViewLayout: UICollectionViewLayout = { let layout = UICollectionViewFlowLayout() @@ -67,13 +75,19 @@ class BRPlayerListViewController: BRViewController { collectionView.showsHorizontalScrollIndicator = false collectionView.bounces = false collectionView.scrollsToTop = false - collectionView.register(CellClass.self, forCellWithReuseIdentifier: "cell") + collectionView.register(CellClass.self, forCellWithReuseIdentifier: "playerCell") return collectionView }() + deinit { + self.prePrefetchTask?.cancel() + self.nextPrefetchTask?.cancel() + self.collectionView.removeFromSuperview() + } + override func viewDidLoad() { super.viewDidLoad() - + self.statusBarStyle = .lightContent br_setupUI() } @@ -81,6 +95,30 @@ class BRPlayerListViewController: BRViewController { super.viewWillAppear(animated) } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if self.viewModel.isPlaying { + self.viewModel.currentPlayer?.start() + } + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + self.viewModel.currentPlayer?.pause() + + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in + guard let self = self else { return } + self.scrollDidEnd(self.collectionView) + } + } + + ///返回上个视频路径 需要子类重写 + var previousVideoUrl: String? { return nil } + ///返回下个视频路径 + var nextVideoUrl: String? { return nil } + func play() { if self.isDidAppear { self.viewModel.currentPlayer?.start() @@ -92,6 +130,47 @@ class BRPlayerListViewController: BRViewController { // self.loadMoreData() // } } + + func reloadData(completion: (() -> Void)? = nil) { + + UIView.performWithoutAnimation { + self.collectionView.reloadData() + } + + self.collectionView.performBatchUpdates(nil) { [weak self] finish in + guard let self = self else { return } + let cell = self.collectionView.cellForItem(at: viewModel.currentIndexPath) as? BRPlayerProtocol + self.viewModel.currentPlayer = cell + + completion?() + } + } + + func scrollToItem(indexPath: IndexPath, animated: Bool = true, completer: (() -> Void)? = nil) { + + UIView.performWithoutAnimation { + self.collectionView.scrollToItem(at: indexPath, at: .top, animated: animated); + } + + self.collectionView.performBatchUpdates(nil) { [weak self] _ in + guard let self = self else { return } + if !animated { + if viewModel.currentIndexPath != indexPath { + self.skip(indexPath: indexPath) + } else { + self.play() + } + } + completer?() + } + } + + func clearData() { + self.viewModel.currentPlayer = nil + self.viewModel.currentIndexPath = .init(row: 0, section: 0) + self.collectionView.contentOffset = .init(x: 0, y: 0) + self.collectionView.reloadData() + } } @@ -113,7 +192,7 @@ extension BRPlayerListViewController { extension BRPlayerListViewController: UICollectionViewDelegate, UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - var cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) + var cell = collectionView.dequeueReusableCell(withReuseIdentifier: "playerCell", for: indexPath) if let newCell = self.dataSource?.br_playerListViewController(self, collectionView, cellForItemAt: indexPath, oldCell: cell) { cell = newCell } @@ -123,10 +202,25 @@ extension BRPlayerListViewController: UICollectionViewDelegate, UICollectionView } } + if self.viewModel.currentPlayer == nil, indexPath == viewModel.currentIndexPath, let playerProtocol = cell as? BRPlayerProtocol { + viewModel.currentIndexPath = indexPath + self.viewModel.currentPlayer = playerProtocol + didChangeIndexPathForVisible() + } return cell } + func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + + self.prePrefetchTask?.cancel() + self.nextPrefetchTask?.cancel() + + self.prePrefetchTask = self.prefetchTask(url: self.previousVideoUrl) + self.nextPrefetchTask = self.prefetchTask(url: self.nextVideoUrl) + + } + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { if let count = self.dataSource?.br_playerListViewController(self, collectionView, numberOfItemsInSection: section) { return count @@ -134,6 +228,82 @@ extension BRPlayerListViewController: UICollectionViewDelegate, UICollectionView return 0 } } + + //滑动停止 + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + scrollDidEnd(scrollView) + brLog(message: "scrollViewDidEndDecelerating+++++++++++++++") + } + + func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { + scrollDidEnd(scrollView) + brLog(message: "scrollViewDidEndScrollingAnimation+++++++++++++++") + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { +// if !decelerate { +// scrollDidEnd(scrollView) +// } + brLog(message: "scrollViewDidEndDragging+++++++++++++++\(decelerate)") + } + + private func scrollDidEnd(_ scrollView: UIScrollView) { + let offsetY = scrollView.contentOffset.y + let indexPaths = self.collectionView.indexPathsForVisibleItems + for indexPath in indexPaths { + guard let cell = self.collectionView.cellForItem(at: indexPath) else { continue } + if floor(offsetY) == floor(cell.frame.origin.y) { + if viewModel.currentIndexPath != indexPath { + self.skip(indexPath: indexPath) + } + } + } + } + + private func skip(indexPath: IndexPath) { + guard let currentPlayer = self.collectionView.cellForItem(at: indexPath) as? BRPlayerProtocol else { return } + viewModel.currentIndexPath = indexPath + self.viewModel.currentPlayer = currentPlayer + didChangeIndexPathForVisible() + self.play() + } +} + +//MARK: -------------- BRPlayerViewModelDelegate -------------- +extension BRPlayerListViewController: BRPlayerViewModelDelegate { + + func br_currentVideoPlayFinish(viewModel: BRPlayerViewModel) { + scrollToNextEpisode() + } + + +} + +extension BRPlayerListViewController { + + ///滑动至下一级 + private func scrollToNextEpisode() { + + var contentOffset = self.collectionView.contentOffset + + if hasNextEpisode() { + contentOffset.y = floor(contentOffset.y + self.contentSize.height) + self.collectionView.setContentOffset(contentOffset, animated: true) + } else { + self.viewModel.currentPlayer?.replay() + } + } + + + ///是否还有下一级 + private func hasNextEpisode() -> Bool { + let contentOffset = self.collectionView.contentOffset + let contentSize = self.collectionView.contentSize + if contentOffset.y >= contentSize.height - self.contentSize.height { + return false + } + return true + } } @@ -154,3 +324,11 @@ extension BRPlayerListViewController { self.delegate?.br_playerListViewController?(self, didChangeIndexPathForVisible: viewModel.currentIndexPath) } } + +//MARK: -------------- 预加载 -------------- +extension BRPlayerListViewController { + + private func prefetchTask(url: String?) -> MCSPrefetchTask? { + return BRPlayerCache.prefetch(urlString: url) + } +} diff --git a/BeeReel/Class/Player/Controller/BRVideoDetailViewController.swift b/BeeReel/Class/Player/Controller/BRVideoDetailViewController.swift index dc1c04e..50cdaa6 100644 --- a/BeeReel/Class/Player/Controller/BRVideoDetailViewController.swift +++ b/BeeReel/Class/Player/Controller/BRVideoDetailViewController.swift @@ -10,23 +10,91 @@ import UIKit class BRVideoDetailViewController: BRPlayerListViewController { + override var contentSize: CGSize { + return .init(width: UIScreen.width, height: UIScreen.height) + } + + override var CellClass: BRPlayerListCell.Type { + return BRDetailPlayerCell.self + } + var shortPlayId: String? var activityId: String? private var detailModel: BRVideoDetailModel? + + + //MARK: UI属性 + private lazy var backButton: UIButton = { + let button = UIButton(type: .custom) + button.setImage(UIImage(named: "nav_back_icon_01"), for: .normal) + button.addTarget(self, action: #selector(handleNavBack), for: .touchUpInside) + return button + }() override func viewDidLoad() { super.viewDidLoad() - + self.delegate = self + self.dataSource = self self.requestDetailData() + + br_setupUI() } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(true, animated: true) + } + override var previousVideoUrl: String? { + let index = self.viewModel.currentIndexPath.row - 1 + guard index > 0 else { return nil } + return detailModel?.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 + } } +extension BRVideoDetailViewController { + + private func br_setupUI() { + view.addSubview(backButton) + + backButton.snp.makeConstraints { make in + make.left.equalToSuperview().offset(15) + make.top.equalToSuperview().offset(UIScreen.statusBarHeight + 10) + } + } + +} + + + +//MARK: -------------- BRPlayerListViewControllerDataSource BRPlayerListViewControllerDelegate -------------- +extension BRVideoDetailViewController: BRPlayerListViewControllerDataSource, BRPlayerListViewControllerDelegate { + + func br_playerListViewController(_ viewController: BRPlayerListViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, oldCell: UICollectionViewCell) -> UICollectionViewCell { + + if let cell = oldCell as? BRPlayerListCell { + cell.videoInfo = self.detailModel?.episodeList?[indexPath.row] + cell.shortModel = self.detailModel?.shortPlayInfo + } + + return oldCell + } + + func br_playerListViewController(_ viewController: BRPlayerListViewController, _ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return self.detailModel?.episodeList?.count ?? 0 + } + +} extension BRVideoDetailViewController { @@ -43,32 +111,30 @@ extension BRVideoDetailViewController { guard let self = self else { return } guard let model = model else { return } self.detailModel = model -// self.videoNameLabel.text = model.shortPlayInfo?.name - /* self.reloadData { [weak self] in guard let self = self else { return } - - if let indexPath = indexPath { - self.scrollToItem(indexPath: indexPath, animated: false) - - } else if let videoInfo = self.detailModel?.video_info { - var row: Int? - self.detailModel?.episodeList?.enumerated().forEach({ - if $1.id == videoInfo.id { - row = $0 - } - }) - if let row = row { - self.scrollToItem(indexPath: .init(row: row, section: 0), animated: false) - } else { - self.scrollToItem(indexPath: .init(row: 0, section: 0), animated: false) - } - } else { - self.scrollToItem(indexPath: .init(row: 0, section: 0), animated: false) - } + self.play() +// if let indexPath = indexPath { +// self.scrollToItem(indexPath: indexPath, animated: false) +// +// } else if let videoInfo = self.detailModel?.video_info { +// var row: Int? +// self.detailModel?.episodeList?.enumerated().forEach({ +// if $1.id == videoInfo.id { +// row = $0 +// } +// }) +// if let row = row { +// self.scrollToItem(indexPath: .init(row: row, section: 0), animated: false) +// } else { +// self.scrollToItem(indexPath: .init(row: 0, section: 0), animated: false) +// } +// } else { +// self.scrollToItem(indexPath: .init(row: 0, section: 0), animated: false) +// } } - */ + } } diff --git a/BeeReel/Class/Player/Model/BRPlayerControlProtocol.swift b/BeeReel/Class/Player/Model/BRPlayerControlProtocol.swift new file mode 100644 index 0000000..80f4bd9 --- /dev/null +++ b/BeeReel/Class/Player/Model/BRPlayerControlProtocol.swift @@ -0,0 +1,16 @@ +// +// BRPlayerControlProtocol.swift +// BeeReel +// +// Created by 湖南秦九 on 2025/7/3. +// + + +import UIKit + +@objc protocol BRPlayerControlProtocol { + + ///点击事件 + func singleTapEvent() + +} diff --git a/BeeReel/Class/Player/Model/BRPlayerProtocol.swift b/BeeReel/Class/Player/Model/BRPlayerProtocol.swift index 54ba3b7..528198d 100644 --- a/BeeReel/Class/Player/Model/BRPlayerProtocol.swift +++ b/BeeReel/Class/Player/Model/BRPlayerProtocol.swift @@ -33,6 +33,9 @@ import UIKit ///暂停播放 func pause() + ///停止播放 + func stop() + ///从头播放 func replay() diff --git a/BeeReel/Class/Player/View/BRDetailControlView.swift b/BeeReel/Class/Player/View/BRDetailControlView.swift new file mode 100644 index 0000000..d833cea --- /dev/null +++ b/BeeReel/Class/Player/View/BRDetailControlView.swift @@ -0,0 +1,20 @@ +// +// BRDetailControlView.swift +// BeeReel +// +// Created by 湖南秦九 on 2025/7/3. +// + +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 + } + */ + +} diff --git a/BeeReel/Class/Player/View/BRDetailPlayerCell.swift b/BeeReel/Class/Player/View/BRDetailPlayerCell.swift new file mode 100644 index 0000000..8975b8d --- /dev/null +++ b/BeeReel/Class/Player/View/BRDetailPlayerCell.swift @@ -0,0 +1,16 @@ +// +// BRDetailPlayerCell.swift +// BeeReel +// +// Created by 湖南秦九 on 2025/7/3. +// + +import UIKit + +class BRDetailPlayerCell: BRPlayerListCell { + + override var ControlViewClass: BRPlayerControlView.Type { + return BRDetailControlView.self + } + +} diff --git a/BeeReel/Class/Player/View/BRPlayerControlView.swift b/BeeReel/Class/Player/View/BRPlayerControlView.swift new file mode 100644 index 0000000..1f454e9 --- /dev/null +++ b/BeeReel/Class/Player/View/BRPlayerControlView.swift @@ -0,0 +1,49 @@ +// +// BRPlayerControlView.swift +// BeeReel +// +// Created by 湖南秦九 on 2025/7/2. +// + +import UIKit + + +class BRPlayerControlView: UIView, BRPlayerControlProtocol { + + + + + + weak var viewModel: BRPlayerViewModel? { + didSet { + + } + } + + var shortModel: BRShortModel? { + didSet { + + } + } + + var videoInfo: BRVideoInfoModel? { + didSet { + + } + } + + + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +//MARK: BRPlayerControlProtocol + func singleTapEvent() { + + } +} diff --git a/BeeReel/Class/Player/View/BRPlayerListCell.swift b/BeeReel/Class/Player/View/BRPlayerListCell.swift index 667d694..1db766a 100644 --- a/BeeReel/Class/Player/View/BRPlayerListCell.swift +++ b/BeeReel/Class/Player/View/BRPlayerListCell.swift @@ -10,14 +10,22 @@ import UIKit class BRPlayerListCell: BRCollectionViewCell, BRPlayerProtocol { - var viewModel: BRPlayerViewModel? { + weak var viewModel: BRPlayerViewModel? { didSet { - + self.controlView.viewModel = viewModel + } + } + + var shortModel: BRShortModel? { + didSet { + self.controlView.shortModel = shortModel + self.player.coverImageView?.br_setImage(url: shortModel?.image_url) } } var videoInfo: BRVideoInfoModel? { didSet { + self.controlView.videoInfo = videoInfo player.setPlayUrl(url: videoInfo?.video_url ?? "") } } @@ -35,11 +43,15 @@ class BRPlayerListCell: BRCollectionViewCell, BRPlayerProtocol { } func start() { - + player.start() } func pause() { - + player.pause() + } + + func stop() { + player.stop() } func replay() { @@ -51,9 +63,15 @@ class BRPlayerListCell: BRCollectionViewCell, BRPlayerProtocol { } + var ControlViewClass: BRPlayerControlView.Type { + return BRPlayerControlView.self + } + private lazy var player: BRPlayer = { - let player = BRPlayer() + let player = BRPlayer(controlView: nil) player.playerView = self.playerView + player.delegate = self + return player }() @@ -62,6 +80,11 @@ class BRPlayerListCell: BRCollectionViewCell, BRPlayerProtocol { return view }() + private lazy var controlView: BRPlayerControlView = { + let view = ControlViewClass.init() + return view + }() + override init(frame: CGRect) { super.init(frame: frame) @@ -78,10 +101,21 @@ extension BRPlayerListCell { private func br_setupUI() { contentView.addSubview(playerView) + playerView.snp.makeConstraints { make in make.edges.equalToSuperview() } + + } + +} + +//MARK: -------------- BRPlayerDelegate -------------- +extension BRPlayerListCell: BRPlayerDelegate { + + func br_playerDidPlayFinish(_ player: BRPlayer) { + self.viewModel?.playFinish(player: self) } } diff --git a/BeeReel/Class/Player/ViewModel/BRPlayerViewModel.swift b/BeeReel/Class/Player/ViewModel/BRPlayerViewModel.swift index 04d063b..6b1643b 100644 --- a/BeeReel/Class/Player/ViewModel/BRPlayerViewModel.swift +++ b/BeeReel/Class/Player/ViewModel/BRPlayerViewModel.swift @@ -7,14 +7,22 @@ import UIKit + +@objc protocol BRPlayerViewModelDelegate { + + @objc optional func br_currentVideoPlayFinish(viewModel: BRPlayerViewModel) +} + + class BRPlayerViewModel: NSObject { + weak var delegate: BRPlayerViewModelDelegate? @objc dynamic var isPlaying: Bool = true var currentIndexPath = IndexPath(row: 0, section: 0) - var currentPlayer: BRPlayerProtocol? { + var currentPlayer: BRPlayerProtocol? { didSet { oldValue?.isCurrent = false oldValue?.pause() @@ -24,3 +32,14 @@ class BRPlayerViewModel: NSObject { } } } + + + +extension BRPlayerViewModel { + + func playFinish(player: BRPlayerProtocol) { + guard (player as? UICollectionViewCell) == (currentPlayer as? UICollectionViewCell) else { return } + self.delegate?.br_currentVideoPlayFinish?(viewModel: self) + } + +} diff --git a/BeeReel/Lib/Player/BRPlayer.swift b/BeeReel/Lib/Player/BRPlayer.swift index 5349a4b..8e9308f 100644 --- a/BeeReel/Lib/Player/BRPlayer.swift +++ b/BeeReel/Lib/Player/BRPlayer.swift @@ -9,15 +9,51 @@ import UIKit import SJBaseVideoPlayer -class BRPlayer { +@objc protocol BRPlayerDelegate: NSObjectProtocol { + ///更新当前总进度 + @objc optional func br_playerDurationDidChange(_ player: BRPlayer, duration: TimeInterval) + ///更新当前进度 + @objc optional func br_playerCurrentTimeDidChange(_ player: BRPlayer, time: TimeInterval) + + ///播放状态变化 +// @objc optional func vp_player(_ player: VPPlayer, playStateDidChanged state: VPPlayer.PlayState) + + ///加载状态发生变化 +// @objc optional func vp_player(_ player: VPPlayer, loadStateDidChange state: VPPlayer.LoadState) + + ///播放时间发生变化 +// @objc optional func vp_playTimeChanged(_ player: BRPlayer, currentTime: Int, duration: Int) + + ///显示首帧 +// @objc optional func br_firstRenderedStart(_ player: BRPlayer) + + ///准备完成 +// @objc optional func br_playerReadyToPlay(_ player: BRPlayer) + + ///播放完成 + @objc optional func br_playerDidPlayFinish(_ player: BRPlayer) + + ///缓冲完成 +// @objc optional func br_playLoadingEnd(_ player: BRPlayer) +} + +class BRPlayer: NSObject { private lazy var player: SJBaseVideoPlayer = { let player = SJBaseVideoPlayer() player.autoplayWhenSetNewAsset = false + player.resumePlaybackWhenAppDidEnterForeground = false + player.accurateSeeking = true + player.videoGravity = .resizeAspectFill +// player.disableVolumeSetting = true + player.rotationManager?.isDisabledAutorotation = true + player.controlLayerDataSource = self return player }() - var playerView: UIView? { + weak var delegate: BRPlayerDelegate? + + weak var playerView: UIView? { didSet { playerView?.addSubview(player.view) player.view.snp.makeConstraints { make in @@ -25,7 +61,14 @@ class BRPlayer { } } } + + private(set) weak var br_controlView: BRPlayerControlProtocol?; + var coverImageView: UIImageView? { + return self.player.presentView.placeholderImageView + } + + ///精确到秒 var duration: TimeInterval { return self.player.duration } @@ -34,14 +77,23 @@ class BRPlayer { return self.player.currentTime } - init() { + deinit { + brLog(message: "播放器销毁") + } + + init(controlView: BRPlayerControlProtocol?) { + super.init() + self.br_controlView = controlView + setupPlayer() } func setPlayUrl(url: String) { self.stop() guard let url = URL(string: url) else { return } - let asset = SJVideoPlayerURLAsset(url: url) + guard let proxyUrl = BRPlayerCache.proxyURL(url: url) else { return } + + let asset = SJVideoPlayerURLAsset(url: proxyUrl) self.player.urlAsset = asset } @@ -53,22 +105,34 @@ class BRPlayer { self.player.pause() } + ///用户暂停 + func pauseForUser() { + self.player.pauseForUser() + } + func stop() { self.player.stop() } func seek(toTime: Int) { - + self.player.seek(toTime: TimeInterval(toTime)) } } extension BRPlayer { private func setupPlayer() { + //设置支持的手势 + self.player.gestureController.supportedGestureTypes = .singleTap + self.player.gestureController.singleTapHandler = { [weak self] _, _ in + guard let self = self else { return } + self.br_controlView?.singleTapEvent() + } + //播放完成回调 self.player.playbackObserver.playbackDidFinishExeBlock = { [weak self] player in guard let self = self else { return } - + self.delegate?.br_playerDidPlayFinish?(self) } //播放状态改变 self.player.playbackObserver.playbackStatusDidChangeExeBlock = { [weak self] player in @@ -78,13 +142,23 @@ extension BRPlayer { //播放时长改变 self.player.playbackObserver.durationDidChangeExeBlock = { [weak self] player in guard let self = self else { return } - + self.delegate?.br_playerDurationDidChange?(self, duration: player.duration) } //播放进度改变 self.player.playbackObserver.currentTimeDidChangeExeBlock = { [weak self] player in guard let self = self else { return } - + self.delegate?.br_playerCurrentTimeDidChange?(self, time: player.currentTime) } + + + } } + +//MARK: -------------- SJVideoPlayerControlLayerDataSource -------------- +extension BRPlayer: SJVideoPlayerControlLayerDataSource { + func controlView() -> UIView! { + return self.br_controlView as? UIView + } +} diff --git a/BeeReel/Lib/Player/BRPlayerCache.swift b/BeeReel/Lib/Player/BRPlayerCache.swift new file mode 100644 index 0000000..92982be --- /dev/null +++ b/BeeReel/Lib/Player/BRPlayerCache.swift @@ -0,0 +1,28 @@ +// +// BRPlayerCache.swift +// BeeReel +// +// Created by 湖南秦九 on 2025/7/2. +// + +import UIKit +import SJMediaCacheServer + +class BRPlayerCache { + + static func proxyURL(url: URL) -> URL? { + return SJMediaCacheServer.shared().proxyURL(from: url) + } + + ///预加载数据 + static func prefetch(url: URL?) -> MCSPrefetchTask? { + guard let url = url else { return nil } + return SJMediaCacheServer.shared().prefetch(with: url, prefetchSize: 1 * 1024 * 1024) + } + + static func prefetch(urlString: String?) -> MCSPrefetchTask? { + guard let str = urlString else { return nil } + return prefetch(url: URL(string: str)) + } + +} diff --git a/BeeReel/Sources/Assets.xcassets/icon/full_icon_01.imageset/Contents.json b/BeeReel/Sources/Assets.xcassets/icon/full_icon_01.imageset/Contents.json new file mode 100644 index 0000000..5c4d3b1 --- /dev/null +++ b/BeeReel/Sources/Assets.xcassets/icon/full_icon_01.imageset/Contents.json @@ -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 + } +} diff --git a/BeeReel/Sources/Assets.xcassets/icon/full_icon_01.imageset/Frame@2x.png b/BeeReel/Sources/Assets.xcassets/icon/full_icon_01.imageset/Frame@2x.png new file mode 100644 index 0000000..01560e6 Binary files /dev/null and b/BeeReel/Sources/Assets.xcassets/icon/full_icon_01.imageset/Frame@2x.png differ diff --git a/BeeReel/Sources/Assets.xcassets/icon/full_icon_01.imageset/Frame@3x.png b/BeeReel/Sources/Assets.xcassets/icon/full_icon_01.imageset/Frame@3x.png new file mode 100644 index 0000000..f053f0b Binary files /dev/null and b/BeeReel/Sources/Assets.xcassets/icon/full_icon_01.imageset/Frame@3x.png differ diff --git a/BeeReel/Sources/Assets.xcassets/icon/hot_icon_03.imageset/Contents.json b/BeeReel/Sources/Assets.xcassets/icon/hot_icon_03.imageset/Contents.json new file mode 100644 index 0000000..5c4d3b1 --- /dev/null +++ b/BeeReel/Sources/Assets.xcassets/icon/hot_icon_03.imageset/Contents.json @@ -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 + } +} diff --git a/BeeReel/Sources/Assets.xcassets/icon/hot_icon_03.imageset/Frame@2x.png b/BeeReel/Sources/Assets.xcassets/icon/hot_icon_03.imageset/Frame@2x.png new file mode 100644 index 0000000..e55493e Binary files /dev/null and b/BeeReel/Sources/Assets.xcassets/icon/hot_icon_03.imageset/Frame@2x.png differ diff --git a/BeeReel/Sources/Assets.xcassets/icon/hot_icon_03.imageset/Frame@3x.png b/BeeReel/Sources/Assets.xcassets/icon/hot_icon_03.imageset/Frame@3x.png new file mode 100644 index 0000000..9b6a785 Binary files /dev/null and b/BeeReel/Sources/Assets.xcassets/icon/hot_icon_03.imageset/Frame@3x.png differ diff --git a/BeeReel/Sources/Assets.xcassets/icon/nav_back_icon_01.imageset/Contents.json b/BeeReel/Sources/Assets.xcassets/icon/nav_back_icon_01.imageset/Contents.json new file mode 100644 index 0000000..725eae5 --- /dev/null +++ b/BeeReel/Sources/Assets.xcassets/icon/nav_back_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame 1498@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame 1498@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/BeeReel/Sources/Assets.xcassets/icon/nav_back_icon_01.imageset/Frame 1498@2x.png b/BeeReel/Sources/Assets.xcassets/icon/nav_back_icon_01.imageset/Frame 1498@2x.png new file mode 100644 index 0000000..375883e Binary files /dev/null and b/BeeReel/Sources/Assets.xcassets/icon/nav_back_icon_01.imageset/Frame 1498@2x.png differ diff --git a/BeeReel/Sources/Assets.xcassets/icon/nav_back_icon_01.imageset/Frame 1498@3x.png b/BeeReel/Sources/Assets.xcassets/icon/nav_back_icon_01.imageset/Frame 1498@3x.png new file mode 100644 index 0000000..0a7bd76 Binary files /dev/null and b/BeeReel/Sources/Assets.xcassets/icon/nav_back_icon_01.imageset/Frame 1498@3x.png differ diff --git a/Podfile b/Podfile index 40b4c58..4178169 100644 --- a/Podfile +++ b/Podfile @@ -23,6 +23,7 @@ target 'BeeReel' do pod 'Toast' #吐司提示 # pod 'YYKit' #工具类 pod 'SJVideoPlayer' #播放器 + pod 'SJMediaCacheServer' #播放器缓存 pod 'WMZPageController' #分页控制器 pod 'YYCategories' pod 'YYText'