diff --git a/Veloria.xcodeproj/project.pbxproj b/Veloria.xcodeproj/project.pbxproj index 47a1303..99b3cef 100644 --- a/Veloria.xcodeproj/project.pbxproj +++ b/Veloria.xcodeproj/project.pbxproj @@ -77,6 +77,26 @@ BF0FA73F2DDEF26E00C9E5F2 /* VPHomeSearchButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA73E2DDEF26E00C9E5F2 /* VPHomeSearchButton.swift */; }; BF0FA7412DDEFBC700C9E5F2 /* UIScrollView+VPRefresh.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7402DDEFBC700C9E5F2 /* UIScrollView+VPRefresh.swift */; }; BF0FA7452DDF027900C9E5F2 /* VPPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7442DDF027900C9E5F2 /* VPPlayer.swift */; }; + BF0FA74A2DDF04E200C9E5F2 /* VPPlayerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7492DDF04E200C9E5F2 /* VPPlayerProtocol.swift */; }; + BF0FA74C2DDF060200C9E5F2 /* VPVideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA74B2DDF060200C9E5F2 /* VPVideoPlayerViewController.swift */; }; + BF0FA74E2DDF067E00C9E5F2 /* VPVideoPlayViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA74D2DDF067E00C9E5F2 /* VPVideoPlayViewModel.swift */; }; + BF0FA7502DDF0A9900C9E5F2 /* VPVideoPlayerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA74F2DDF0A9900C9E5F2 /* VPVideoPlayerCell.swift */; }; + BF0FA7522DDF134700C9E5F2 /* VPVideoPlayerControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7512DDF134700C9E5F2 /* VPVideoPlayerControlView.swift */; }; + BF0FA7572DDF159B00C9E5F2 /* VPExploreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7562DDF159A00C9E5F2 /* VPExploreViewController.swift */; }; + BF0FA7592DDF1C2800C9E5F2 /* VPPlayerProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7582DDF1C2800C9E5F2 /* VPPlayerProgressView.swift */; }; + BF0FA75B2DDF206000C9E5F2 /* VPExplorePlayerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA75A2DDF206000C9E5F2 /* VPExplorePlayerCell.swift */; }; + BF0FA75D2DDF208400C9E5F2 /* VPExplorePlayerControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA75C2DDF208400C9E5F2 /* VPExplorePlayerControlView.swift */; }; + BF0FA75F2DDFFDB000C9E5F2 /* VPDetailPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA75E2DDFFDB000C9E5F2 /* VPDetailPlayerViewController.swift */; }; + BF0FA7612DDFFE7100C9E5F2 /* VPVideoDetailModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7602DDFFE7100C9E5F2 /* VPVideoDetailModel.swift */; }; + BF0FA7632DE006E700C9E5F2 /* VPDetailPlayerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7622DE006E700C9E5F2 /* VPDetailPlayerCell.swift */; }; + BF0FA7652DE00A0E00C9E5F2 /* VPDetailPlayerControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7642DE00A0E00C9E5F2 /* VPDetailPlayerControlView.swift */; }; + BF0FA7672DE0469300C9E5F2 /* VPEpisodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7662DE0469300C9E5F2 /* VPEpisodeView.swift */; }; + BF0FA7692DE0502900C9E5F2 /* VPEpisodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7682DE0502900C9E5F2 /* VPEpisodeCell.swift */; }; + BF0FA76B2DE0533400C9E5F2 /* VPEpisodeMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA76A2DE0533400C9E5F2 /* VPEpisodeMenuView.swift */; }; + BF0FA76D2DE053C100C9E5F2 /* VPScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA76C2DE053C100C9E5F2 /* VPScrollView.swift */; }; + BF0FA76F2DE062A700C9E5F2 /* VPRateSelectedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA76E2DE062A700C9E5F2 /* VPRateSelectedView.swift */; }; + BF0FA7712DE062EB00C9E5F2 /* VPVideoRateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7702DE062EB00C9E5F2 /* VPVideoRateModel.swift */; }; + BF0FA7732DE0671200C9E5F2 /* VPRateSelectedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7722DE0671200C9E5F2 /* VPRateSelectedCell.swift */; }; F939C04AD4003BA127F15C28 /* Pods_Veloria.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 34F57E87E765BF8D72A43DCA /* Pods_Veloria.framework */; }; /* End PBXBuildFile section */ @@ -160,6 +180,26 @@ BF0FA73E2DDEF26E00C9E5F2 /* VPHomeSearchButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPHomeSearchButton.swift; sourceTree = ""; }; BF0FA7402DDEFBC700C9E5F2 /* UIScrollView+VPRefresh.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScrollView+VPRefresh.swift"; sourceTree = ""; }; BF0FA7442DDF027900C9E5F2 /* VPPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPPlayer.swift; sourceTree = ""; }; + BF0FA7492DDF04E200C9E5F2 /* VPPlayerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPPlayerProtocol.swift; sourceTree = ""; }; + BF0FA74B2DDF060200C9E5F2 /* VPVideoPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPVideoPlayerViewController.swift; sourceTree = ""; }; + BF0FA74D2DDF067E00C9E5F2 /* VPVideoPlayViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPVideoPlayViewModel.swift; sourceTree = ""; }; + BF0FA74F2DDF0A9900C9E5F2 /* VPVideoPlayerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPVideoPlayerCell.swift; sourceTree = ""; }; + BF0FA7512DDF134700C9E5F2 /* VPVideoPlayerControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPVideoPlayerControlView.swift; sourceTree = ""; }; + BF0FA7562DDF159A00C9E5F2 /* VPExploreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPExploreViewController.swift; sourceTree = ""; }; + BF0FA7582DDF1C2800C9E5F2 /* VPPlayerProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPPlayerProgressView.swift; sourceTree = ""; }; + BF0FA75A2DDF206000C9E5F2 /* VPExplorePlayerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPExplorePlayerCell.swift; sourceTree = ""; }; + BF0FA75C2DDF208400C9E5F2 /* VPExplorePlayerControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPExplorePlayerControlView.swift; sourceTree = ""; }; + BF0FA75E2DDFFDB000C9E5F2 /* VPDetailPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPDetailPlayerViewController.swift; sourceTree = ""; }; + BF0FA7602DDFFE7100C9E5F2 /* VPVideoDetailModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPVideoDetailModel.swift; sourceTree = ""; }; + BF0FA7622DE006E700C9E5F2 /* VPDetailPlayerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPDetailPlayerCell.swift; sourceTree = ""; }; + BF0FA7642DE00A0E00C9E5F2 /* VPDetailPlayerControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPDetailPlayerControlView.swift; sourceTree = ""; }; + BF0FA7662DE0469300C9E5F2 /* VPEpisodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPEpisodeView.swift; sourceTree = ""; }; + BF0FA7682DE0502900C9E5F2 /* VPEpisodeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPEpisodeCell.swift; sourceTree = ""; }; + BF0FA76A2DE0533400C9E5F2 /* VPEpisodeMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPEpisodeMenuView.swift; sourceTree = ""; }; + BF0FA76C2DE053C100C9E5F2 /* VPScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPScrollView.swift; sourceTree = ""; }; + BF0FA76E2DE062A700C9E5F2 /* VPRateSelectedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPRateSelectedView.swift; sourceTree = ""; }; + BF0FA7702DE062EB00C9E5F2 /* VPVideoRateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPVideoRateModel.swift; sourceTree = ""; }; + BF0FA7722DE0671200C9E5F2 /* VPRateSelectedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPRateSelectedCell.swift; sourceTree = ""; }; E0BDA3570E00C90877E45AA0 /* Pods-VideoPlayer.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-VideoPlayer.debug.xcconfig"; path = "Target Support Files/Pods-VideoPlayer/Pods-VideoPlayer.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -283,6 +323,7 @@ BF0FA71A2DDC7FF200C9E5F2 /* VPImageView.swift */, BF0FA7252DDC8F7600C9E5F2 /* VPCollectionView.swift */, BF0FA7272DDC91F800C9E5F2 /* VPCollectionViewCell.swift */, + BF0FA76C2DE053C100C9E5F2 /* VPScrollView.swift */, ); path = View; sourceTree = ""; @@ -474,6 +515,7 @@ BF0FA7472DDF03B600C9E5F2 /* Controller */, BF0FA7462DDF03AD00C9E5F2 /* View */, BF0FA6FE2DDC660300C9E5F2 /* Model */, + BF0FA7482DDF04B800C9E5F2 /* ViewModel */, ); path = Player; sourceTree = ""; @@ -481,8 +523,11 @@ BF0FA6FE2DDC660300C9E5F2 /* Model */ = { isa = PBXGroup; children = ( + BF0FA7492DDF04E200C9E5F2 /* VPPlayerProtocol.swift */, BF0FA6FF2DDC665300C9E5F2 /* VPShortModel.swift */, BF0FA7012DDC667C00C9E5F2 /* VPVideoInfoModel.swift */, + BF0FA7602DDFFE7100C9E5F2 /* VPVideoDetailModel.swift */, + BF0FA7702DE062EB00C9E5F2 /* VPVideoRateModel.swift */, ); path = Model; sourceTree = ""; @@ -539,6 +584,9 @@ BF0FA7422DDF024400C9E5F2 /* Explore */ = { isa = PBXGroup; children = ( + BF0FA7552DDF158000C9E5F2 /* Controller */, + BF0FA7542DDF157700C9E5F2 /* View */, + BF0FA7532DDF156F00C9E5F2 /* Model */, ); path = Explore; sourceTree = ""; @@ -554,6 +602,16 @@ BF0FA7462DDF03AD00C9E5F2 /* View */ = { isa = PBXGroup; children = ( + BF0FA74F2DDF0A9900C9E5F2 /* VPVideoPlayerCell.swift */, + BF0FA7512DDF134700C9E5F2 /* VPVideoPlayerControlView.swift */, + BF0FA7582DDF1C2800C9E5F2 /* VPPlayerProgressView.swift */, + BF0FA7622DE006E700C9E5F2 /* VPDetailPlayerCell.swift */, + BF0FA7642DE00A0E00C9E5F2 /* VPDetailPlayerControlView.swift */, + BF0FA7662DE0469300C9E5F2 /* VPEpisodeView.swift */, + BF0FA7682DE0502900C9E5F2 /* VPEpisodeCell.swift */, + BF0FA76A2DE0533400C9E5F2 /* VPEpisodeMenuView.swift */, + BF0FA76E2DE062A700C9E5F2 /* VPRateSelectedView.swift */, + BF0FA7722DE0671200C9E5F2 /* VPRateSelectedCell.swift */, ); path = View; sourceTree = ""; @@ -561,6 +619,40 @@ BF0FA7472DDF03B600C9E5F2 /* Controller */ = { isa = PBXGroup; children = ( + BF0FA74B2DDF060200C9E5F2 /* VPVideoPlayerViewController.swift */, + BF0FA75E2DDFFDB000C9E5F2 /* VPDetailPlayerViewController.swift */, + ); + path = Controller; + sourceTree = ""; + }; + BF0FA7482DDF04B800C9E5F2 /* ViewModel */ = { + isa = PBXGroup; + children = ( + BF0FA74D2DDF067E00C9E5F2 /* VPVideoPlayViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + BF0FA7532DDF156F00C9E5F2 /* Model */ = { + isa = PBXGroup; + children = ( + ); + path = Model; + sourceTree = ""; + }; + BF0FA7542DDF157700C9E5F2 /* View */ = { + isa = PBXGroup; + children = ( + BF0FA75A2DDF206000C9E5F2 /* VPExplorePlayerCell.swift */, + BF0FA75C2DDF208400C9E5F2 /* VPExplorePlayerControlView.swift */, + ); + path = View; + sourceTree = ""; + }; + BF0FA7552DDF158000C9E5F2 /* Controller */ = { + isa = PBXGroup; + children = ( + BF0FA7562DDF159A00C9E5F2 /* VPExploreViewController.swift */, ); path = Controller; sourceTree = ""; @@ -686,6 +778,7 @@ files = ( 1B056E742DDB2DD7007EE38D /* UIView+VPAdd.swift in Sources */, BF0FA6F12DDC600200C9E5F2 /* VPNetwork.swift in Sources */, + BF0FA7502DDF0A9900C9E5F2 /* VPVideoPlayerCell.swift in Sources */, 1B056E572DDACC6B007EE38D /* VPHomePageViewController.swift in Sources */, BF0FA72A2DDC922F00C9E5F2 /* VPHomeRecommandCell.swift in Sources */, BF0FA7322DDEBD6400C9E5F2 /* AppDelegate+Config.swift in Sources */, @@ -695,6 +788,7 @@ BF0FA7262DDC8F7600C9E5F2 /* VPCollectionView.swift in Sources */, BF0FA71D2DDC807200C9E5F2 /* UIImageView+VPAdd.swift in Sources */, BF0FA6DC2DDC5CD700C9E5F2 /* VPTokenModel.swift in Sources */, + BF0FA7572DDF159B00C9E5F2 /* VPExploreViewController.swift in Sources */, 1B056E772DDB3641007EE38D /* VPTabBarItemNormalVew.swift in Sources */, 1B056E4B2DDAC6BA007EE38D /* VPDefine.swift in Sources */, 1B056E702DDB019B007EE38D /* VPTabBarItemContainer.swift in Sources */, @@ -703,22 +797,33 @@ 1B056E6C2DDADAA1007EE38D /* VPTabBar.swift in Sources */, BF0FA72E2DDD7DD400C9E5F2 /* VPHomeRankingCell.swift in Sources */, BF0FA7302DDEBB1600C9E5F2 /* UIButton+VPAdd.swift in Sources */, + BF0FA74C2DDF060200C9E5F2 /* VPVideoPlayerViewController.swift in Sources */, + BF0FA76D2DE053C100C9E5F2 /* VPScrollView.swift in Sources */, 1B056E5D2DDACD8E007EE38D /* UIFont+VPAdd.swift in Sources */, BF0FA7282DDC91F800C9E5F2 /* VPCollectionViewCell.swift in Sources */, + BF0FA7672DE0469300C9E5F2 /* VPEpisodeView.swift in Sources */, BF0FA6F92DDC64E700C9E5F2 /* VPHomeAPI.swift in Sources */, BF0FA6F42DDC604500C9E5F2 /* VPHUD.swift in Sources */, BF0FA7222DDC859D00C9E5F2 /* NSNumber+VPAdd.swift in Sources */, BF0FA73B2DDED1C700C9E5F2 /* VPHomeListCell.swift in Sources */, + BF0FA7592DDF1C2800C9E5F2 /* VPPlayerProgressView.swift in Sources */, BF0FA71B2DDC7FF200C9E5F2 /* VPImageView.swift in Sources */, + BF0FA7522DDF134700C9E5F2 /* VPVideoPlayerControlView.swift in Sources */, 1B056E2C2DDAC0FD007EE38D /* SceneDelegate.swift in Sources */, + BF0FA75D2DDF208400C9E5F2 /* VPExplorePlayerControlView.swift in Sources */, 1B056E462DDAC370007EE38D /* UIScreen+VPAdd.swift in Sources */, + BF0FA7692DE0502900C9E5F2 /* VPEpisodeCell.swift in Sources */, BF0FA73D2DDED2D000C9E5F2 /* VPVideoAPI.swift in Sources */, BF0FA6EE2DDC5F8700C9E5F2 /* JXUUID.m in Sources */, BF0FA7052DDC67AC00C9E5F2 /* VPHomeViewModel.swift in Sources */, BF0FA6EF2DDC5F8700C9E5F2 /* PDKeyChain.m in Sources */, + BF0FA75F2DDFFDB000C9E5F2 /* VPDetailPlayerViewController.swift in Sources */, + BF0FA7732DE0671200C9E5F2 /* VPRateSelectedCell.swift in Sources */, 1B056E4D2DDAC7C1007EE38D /* VPTabBarController.swift in Sources */, BF0FA6DA2DDC5CB600C9E5F2 /* VPLoginManager.swift in Sources */, + BF0FA74A2DDF04E200C9E5F2 /* VPPlayerProtocol.swift in Sources */, BF0FA72C2DDD7B7300C9E5F2 /* VPHomeRankingContentCell.swift in Sources */, + BF0FA7652DE00A0E00C9E5F2 /* VPDetailPlayerControlView.swift in Sources */, BF0FA7342DDEC74500C9E5F2 /* VPCategoryModel.swift in Sources */, 1B056E4F2DDAC7FC007EE38D /* VPNavigationController.swift in Sources */, 1B056E3F2DDAC2DB007EE38D /* VPCryptorService.swift in Sources */, @@ -728,25 +833,32 @@ BF0FA70E2DDC6ACC00C9E5F2 /* VPHomeItemContentCell.swift in Sources */, BF0FA6D72DDC5BE100C9E5F2 /* VPURLPath.swift in Sources */, 1B056E5B2DDACD80007EE38D /* UIColor+VPAdd.swift in Sources */, + BF0FA76B2DE0533400C9E5F2 /* VPEpisodeMenuView.swift in Sources */, 1B056E6A2DDAD0BF007EE38D /* VPLocalizedManager.swift in Sources */, BF0FA7242DDC888F00C9E5F2 /* VPHomeRecommandContentCell.swift in Sources */, BF0FA7192DDC7F4900C9E5F2 /* VPHomeBannerCell.swift in Sources */, + BF0FA7712DE062EB00C9E5F2 /* VPVideoRateModel.swift in Sources */, BF0FA7022DDC667C00C9E5F2 /* VPVideoInfoModel.swift in Sources */, + BF0FA76F2DE062A700C9E5F2 /* VPRateSelectedView.swift in Sources */, 1B056E792DDB365A007EE38D /* VPTabBarItemSelectedView.swift in Sources */, 1B056E722DDB022F007EE38D /* VPTabBarItem.swift in Sources */, 1B056E412DDAC30A007EE38D /* VPModel.swift in Sources */, BF0FA70A2DDC69C800C9E5F2 /* VPTableViewCell.swift in Sources */, 1B056E442DDAC355007EE38D /* UIDevice+VPAdd.swift in Sources */, + BF0FA7632DE006E700C9E5F2 /* VPDetailPlayerCell.swift in Sources */, BF0FA73F2DDEF26E00C9E5F2 /* VPHomeSearchButton.swift in Sources */, 1B056E512DDACBE5007EE38D /* VPViewController.swift in Sources */, BF0FA7122DDC6D2C00C9E5F2 /* VPHomeModuleItem.swift in Sources */, BF0FA7162DDC78FF00C9E5F2 /* ZKCycleScrollViewFlowLayout.swift in Sources */, BF0FA7172DDC78FF00C9E5F2 /* ZKCycleScrollView.swift in Sources */, + BF0FA7612DDFFE7100C9E5F2 /* VPVideoDetailModel.swift in Sources */, BF0FA6D52DDC5B5D00C9E5F2 /* VPApi.swift in Sources */, BF0FA6FC2DDC657500C9E5F2 /* VPHomeDataModel.swift in Sources */, BF0FA7392DDECF8900C9E5F2 /* VPHomeListViewController.swift in Sources */, BF0FA6F62DDC616300C9E5F2 /* VPToast.swift in Sources */, 1B056E492DDAC3DF007EE38D /* VPAppTool.swift in Sources */, + BF0FA74E2DDF067E00C9E5F2 /* VPVideoPlayViewModel.swift in Sources */, + BF0FA75B2DDF206000C9E5F2 /* VPExplorePlayerCell.swift in Sources */, BF0FA7412DDEFBC700C9E5F2 /* UIScrollView+VPRefresh.swift in Sources */, BF0FA70C2DDC6A3800C9E5F2 /* VPHomeBannerContentCell.swift in Sources */, BF0FA7102DDC6CA200C9E5F2 /* VPListModel.swift in Sources */, diff --git a/Veloria/AppDelegate/AppDelegate+Config.swift b/Veloria/AppDelegate/AppDelegate+Config.swift index 99a2ee6..38bc9e2 100644 --- a/Veloria/AppDelegate/AppDelegate+Config.swift +++ b/Veloria/AppDelegate/AppDelegate+Config.swift @@ -10,7 +10,8 @@ import UIKit extension AppDelegate { func appConfig() { - UIButton.vp_Awake() + UIButton.vp_bt_Awake() + UIView.vp_Awake() } } diff --git a/Veloria/Base/Controller/VPNavigationController.swift b/Veloria/Base/Controller/VPNavigationController.swift index 598a8cc..cc991d8 100644 --- a/Veloria/Base/Controller/VPNavigationController.swift +++ b/Veloria/Base/Controller/VPNavigationController.swift @@ -12,7 +12,7 @@ class VPNavigationController: UINavigationController { override func viewDidLoad() { super.viewDidLoad() - +// self.interactivePopGestureRecognizer?.delegate = self } override func pushViewController(_ viewController: UIViewController, animated: Bool) { diff --git a/Veloria/Base/Controller/VPTabBarController.swift b/Veloria/Base/Controller/VPTabBarController.swift index 61c1655..a25fec9 100644 --- a/Veloria/Base/Controller/VPTabBarController.swift +++ b/Veloria/Base/Controller/VPTabBarController.swift @@ -87,7 +87,7 @@ extension VPTabBarController { let nav1 = createNavigationController(viewController: VPHomePageViewController(), title: "Home".localized, image: UIImage(named: "tabbar_icon_01"), selectedImage: UIImage(named: "tabbar_icon_01_selected")) - let nav2 = createNavigationController(viewController: VPHomePageViewController(), title: "Explore".localized, image: UIImage(named: "tabbar_icon_02"), selectedImage: UIImage(named: "tabbar_icon_02_selected")) + let nav2 = createNavigationController(viewController: VPExploreViewController(), title: "Explore".localized, image: UIImage(named: "tabbar_icon_02"), selectedImage: UIImage(named: "tabbar_icon_02_selected")) let nav3 = createNavigationController(viewController: VPHomePageViewController(), title: "Home".localized, image: UIImage(named: "tabbar_icon_03"), selectedImage: UIImage(named: "tabbar_icon_03_selected")) let nav4 = createNavigationController(viewController: VPHomePageViewController(), title: "Home".localized, image: UIImage(named: "tabbar_icon_03"), selectedImage: UIImage(named: "tabbar_icon_04_selected")) diff --git a/Veloria/Base/Controller/VPViewController.swift b/Veloria/Base/Controller/VPViewController.swift index e91333e..842f2df 100644 --- a/Veloria/Base/Controller/VPViewController.swift +++ b/Veloria/Base/Controller/VPViewController.swift @@ -14,11 +14,22 @@ class VPViewController: UIViewController { return imageView }() + private(set) var isViewDidLoad = false + private(set) var isDidAppear = false + private(set) var hasViewDidAppear = false + override func viewDidLoad() { super.viewDidLoad() - + self.isViewDidLoad = true view.backgroundColor = .backgroundColor() + if let navi = navigationController { + if navi.visibleViewController == self { + if navi.viewControllers.count > 1 { + configNavigationBack() + } + } + } view.addSubview(bgImageView) @@ -27,9 +38,57 @@ class VPViewController: UIViewController { } } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + isDidAppear = true + hasViewDidAppear = true + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + isDidAppear = false + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + isDidAppear = false + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + func handleHeaderRefresh(_ completer: (() -> Void)?) {} func handleFooterRefresh(_ completer: (() -> Void)?) {} } + +extension VPViewController { + func configNavigationBack(_ imageName: String = "arrow_left_icon_01") { + let image = UIImage(named: imageName) + + let leftBarButtonItem = UIBarButtonItem(image: image, style: .plain ,target: self,action: #selector(handleBack)) + navigationItem.leftBarButtonItem = leftBarButtonItem + } + + @objc func handleBack() { + self.vp_toLastViewController(animated: true) + } + + func vp_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) + } + } +} diff --git a/Veloria/Base/Extension/NSNumber+VPAdd.swift b/Veloria/Base/Extension/NSNumber+VPAdd.swift index 3ab4489..a0e716f 100644 --- a/Veloria/Base/Extension/NSNumber+VPAdd.swift +++ b/Veloria/Base/Extension/NSNumber+VPAdd.swift @@ -23,3 +23,19 @@ extension NSNumber { return formatter.string(from: self) ?? "0" } } + + +extension Int { + func 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/Veloria/Base/Extension/UIButton+VPAdd.swift b/Veloria/Base/Extension/UIButton+VPAdd.swift index 52f7b91..149b176 100644 --- a/Veloria/Base/Extension/UIButton+VPAdd.swift +++ b/Veloria/Base/Extension/UIButton+VPAdd.swift @@ -9,23 +9,23 @@ import UIKit extension UIButton { fileprivate struct AssociatedKeys { - static var vp_gradientLayer: Int? - static var vp_gradientBorder: Int? - static var vp_gradientBorderShapeLayer: Int? + static var bt_gradientLayer: Int? + static var bt_gradientBorder: Int? + static var bt_gradientBorderShapeLayer: Int? } - @objc public static func vp_Awake() { - vp_swizzled_instanceMethod("vp", oldClass: self, oldSelector: "layoutSubviews", newClass: self) + @objc public static func vp_bt_Awake() { + vp_swizzled_instanceMethod("vp_bt", oldClass: self, oldSelector: "layoutSubviews", newClass: self) } - @objc func vp_layoutSubviews() { - vp_layoutSubviews() - vp_gradientLayer?.frame = self.bounds + @objc func vp_bt_layoutSubviews() { + vp_bt_layoutSubviews() + bt_gradientLayer?.frame = self.bounds - vp_gradientBorder?.frame = self.bounds - let path = UIBezierPath(roundedRect: bounds.insetBy(dx: 2, dy: 2), cornerRadius: layer.cornerRadius) - vp_gradientBorderShapeLayer?.path = path.cgPath + bt_gradientBorder?.frame = self.bounds + let path = UIBezierPath(roundedRect: bounds.insetBy(dx: 0.5, dy: 0.5), cornerRadius: layer.cornerRadius) + bt_gradientBorderShapeLayer?.path = path.cgPath } @@ -33,61 +33,61 @@ extension UIButton { } extension UIButton { - private var vp_gradientLayer: CAGradientLayer? { + private var bt_gradientLayer: CAGradientLayer? { get { - return objc_getAssociatedObject(self, &AssociatedKeys.vp_gradientLayer) as? CAGradientLayer + return objc_getAssociatedObject(self, &AssociatedKeys.bt_gradientLayer) as? CAGradientLayer } set { - objc_setAssociatedObject(self, &AssociatedKeys.vp_gradientLayer, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + objc_setAssociatedObject(self, &AssociatedKeys.bt_gradientLayer, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } - private var vp_gradientBorder: CAGradientLayer? { + private var bt_gradientBorder: CAGradientLayer? { get { - return objc_getAssociatedObject(self, &AssociatedKeys.vp_gradientBorder) as? CAGradientLayer + return objc_getAssociatedObject(self, &AssociatedKeys.bt_gradientBorder) as? CAGradientLayer } set { - objc_setAssociatedObject(self, &AssociatedKeys.vp_gradientBorder, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + objc_setAssociatedObject(self, &AssociatedKeys.bt_gradientBorder, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } - private var vp_gradientBorderShapeLayer: CAShapeLayer? { + private var bt_gradientBorderShapeLayer: CAShapeLayer? { get { - return objc_getAssociatedObject(self, &AssociatedKeys.vp_gradientBorderShapeLayer) as? CAShapeLayer + return objc_getAssociatedObject(self, &AssociatedKeys.bt_gradientBorderShapeLayer) as? CAShapeLayer } set { - objc_setAssociatedObject(self, &AssociatedKeys.vp_gradientBorderShapeLayer, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + objc_setAssociatedObject(self, &AssociatedKeys.bt_gradientBorderShapeLayer, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } ///设置渐变色 - func vp_setGradient() { - if vp_gradientLayer == nil { + func bt_setGradient() { + if bt_gradientLayer == nil { let gLayer = CAGradientLayer() gLayer.colors = [UIColor.color05CEA0().cgColor, UIColor.color7C174F().cgColor] gLayer.locations = [0, 0.8] gLayer.startPoint = .init(x: 0, y: 0.3) gLayer.endPoint = .init(x: 1, y: 0.8) - vp_gradientLayer = gLayer + bt_gradientLayer = gLayer } - if let layer = vp_gradientLayer { + if let layer = bt_gradientLayer { self.layer.addSublayer(layer) self.titleLabel?.layer.zPosition = 1 } } ///删除渐变层 - func vp_removeGradient() { - vp_gradientLayer?.removeFromSuperlayer() + func bt_removeGradient() { + bt_gradientLayer?.removeFromSuperlayer() } ///设置渐变边框 - func vp_setGradientBorder() { - if vp_gradientBorder == nil { + func bt_setGradientBorder() { + if bt_gradientBorder == nil { let gLayer = CAGradientLayer() gLayer.colors = [UIColor.color05CEA0().cgColor, UIColor.color7C174F().cgColor] gLayer.locations = [0, 0.8] @@ -100,18 +100,18 @@ extension UIButton { shapeLayer.strokeColor = UIColor.black.cgColor gLayer.mask = shapeLayer - vp_gradientBorderShapeLayer = shapeLayer - vp_gradientBorder = gLayer + bt_gradientBorderShapeLayer = shapeLayer + bt_gradientBorder = gLayer } - if let layer = vp_gradientBorder { + if let layer = bt_gradientBorder { self.layer.addSublayer(layer) self.titleLabel?.layer.zPosition = 1 } } ///删除渐变边框 - func vp_removeGradientBorder() { - vp_gradientBorder?.removeFromSuperlayer() + func bt_removeGradientBorder() { + bt_gradientBorder?.removeFromSuperlayer() } diff --git a/Veloria/Base/Extension/UIColor+VPAdd.swift b/Veloria/Base/Extension/UIColor+VPAdd.swift index c1bf02f..22f90d3 100644 --- a/Veloria/Base/Extension/UIColor+VPAdd.swift +++ b/Veloria/Base/Extension/UIColor+VPAdd.swift @@ -49,4 +49,24 @@ extension UIColor { static func colorFFFFFB(alpha: CGFloat = 1) -> UIColor { return UIColor(rgb: 0xFFFFFB, alpha: alpha) } + + static func color949494(alpha: CGFloat = 1) -> UIColor { + return UIColor(rgb: 0x949494, alpha: alpha) + } + + static func colorBEBEBE(alpha: CGFloat = 1) -> UIColor { + return UIColor(rgb: 0xBEBEBE, alpha: alpha) + } + + static func colorFFBD36(alpha: CGFloat = 1) -> UIColor { + return UIColor(rgb: 0xFFBD36, alpha: alpha) + } + + static func color545458(alpha: CGFloat = 1) -> UIColor { + return UIColor(rgb: 0x545458, alpha: alpha) + } + + static func color1C2D2F(alpha: CGFloat = 1) -> UIColor { + return UIColor(rgb: 0x1C2D2F, alpha: alpha) + } } diff --git a/Veloria/Base/Extension/UIView+VPAdd.swift b/Veloria/Base/Extension/UIView+VPAdd.swift index 6e66be5..1702402 100644 --- a/Veloria/Base/Extension/UIView+VPAdd.swift +++ b/Veloria/Base/Extension/UIView+VPAdd.swift @@ -10,4 +10,110 @@ import SnapKit extension UIView { + fileprivate struct AssociatedKeys { + static var vp_effect: Int? + static var vp_circulars: Int? + static var vp_gradientBorder: Int? + static var vp_gradientBorderShapeLayer: Int? + } + + + @objc public static func vp_Awake() { + vp_swizzled_instanceMethod("vp", oldClass: self, oldSelector: "layoutSubviews", newClass: self) + } + + @objc func vp_layoutSubviews() { + vp_layoutSubviews() + + if let effectView = effectView, effectView.frame != self.bounds { + effectView.frame = self.bounds + } + + if let border = vp_gradientBorder { + border.frame = self.bounds + let path = UIBezierPath(roundedRect: bounds.insetBy(dx: 0.5, dy: 0.5), cornerRadius: layer.cornerRadius) + vp_gradientBorderShapeLayer?.path = path.cgPath + } + } +} + + +//MARK: -------------- 模糊效果 -------------- +extension UIView { + private var effectView: UIVisualEffectView? { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.vp_effect) as? UIVisualEffectView + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.vp_effect, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + ///添加模糊效果 + func addEffectView(style: UIBlurEffect.Style = .dark) { + if self.effectView == nil { + let blur = UIBlurEffect(style: style) + let effectView = UIVisualEffectView(effect: blur) + effectView.isUserInteractionEnabled = false + self.addSubview(effectView) + self.sendSubviewToBack(effectView) + + self.effectView = effectView + } + } + ///删除模糊效果 + func removeEffectView() { + self.effectView?.removeFromSuperview() + self.effectView = nil + } +} + +//MARK: -------------- 渐变边框 -------------- +extension UIView { + private var vp_gradientBorder: CAGradientLayer? { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.vp_gradientBorder) as? CAGradientLayer + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.vp_gradientBorder, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + private var vp_gradientBorderShapeLayer: CAShapeLayer? { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.vp_gradientBorderShapeLayer) as? CAShapeLayer + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.vp_gradientBorderShapeLayer, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + ///设置渐变边框 + func vp_setGradientBorder() { + if vp_gradientBorder == nil { + let gLayer = CAGradientLayer() + gLayer.colors = [UIColor.color05CEA0().cgColor, UIColor.color7C174F().cgColor] + gLayer.locations = [0, 0.8] + gLayer.startPoint = .init(x: 0, y: 0.3) + gLayer.endPoint = .init(x: 1, y: 0.8) + + let shapeLayer = CAShapeLayer() + shapeLayer.lineWidth = 1 + shapeLayer.fillColor = UIColor.clear.cgColor + shapeLayer.strokeColor = UIColor.black.cgColor + gLayer.mask = shapeLayer + + vp_gradientBorderShapeLayer = shapeLayer + vp_gradientBorder = gLayer + } + + if let layer = vp_gradientBorder { + self.layer.addSublayer(layer) + } + } + + ///删除渐变边框 + func vp_removeGradientBorder() { + vp_gradientBorder?.removeFromSuperlayer() + } } diff --git a/Veloria/Base/Networking/API/VPVideoAPI.swift b/Veloria/Base/Networking/API/VPVideoAPI.swift index cb3d0e6..bdbc230 100644 --- a/Veloria/Base/Networking/API/VPVideoAPI.swift +++ b/Veloria/Base/Networking/API/VPVideoAPI.swift @@ -9,6 +9,26 @@ import UIKit class VPVideoAPI: NSObject { + ///获取视频详情 + static func requestVideoDetail(shortPlayId: String, activityId: String? = nil, completer: ((_ model: VPVideoDetailModel?) -> Void)?) { + var parameters: [String : Any] = [ + "short_play_id" : shortPlayId, + "video_id" : "0" + ] + + if let activityId = activityId { + parameters["activity_id"] = activityId + } + + var param = VPNetworkParameters(path: "/getVideoDetails") + param.method = .get + param.parameters = parameters + + VPNetwork.request(parameters: param) { (response: VPNetworkResponse) in + completer?(response.data) + } + } + ///获取分类短剧 static func requestCategoryVideoList(id: String, page: Int, completer: ((_ listModel: VPListModel?) -> Void)?) { @@ -26,4 +46,58 @@ class VPVideoAPI: NSObject { } } + ///推荐短剧 + static func requestRecommandsVideo(page: Int, completer: ((_ listModel: VPListModel?) -> Void)?) { + + var param = VPNetworkParameters(path: "/getRecommands") + param.method = .get + param.parameters = [ + "page_size" : 20, + "current_page" : page + ] + + VPNetwork.request(parameters: param) { (response: VPNetworkResponse>) in + completer?(response.data) + } + } + + ///收藏短剧 + static func requestCollectShort(isCollect: Bool, shortPlayId: String, videoId: String?, success: (() -> Void)?, failure: (() -> Void)? = nil) { + let path: String + if isCollect { + path = "/collect" + } else { + path = "/cancelCollect" + } + + var parameters: [String : Any] = [ + "short_play_id" : shortPlayId, + ] + + if let videoId = videoId { + parameters["video_id"] = videoId + } + + var param = VPNetworkParameters(path: path) + param.isLoding = true + param.parameters = parameters + + VPNetwork.request(parameters: param) { (response: VPNetworkResponse) in + if response.code == VPNetworkCodeSucceed { + success?() + NotificationCenter.default.post(name: VPVideoAPI.updateShortCollectStateNotification, object: nil, userInfo: [ + "state" : isCollect, + "id" : shortPlayId, + ]) + } else { + failure?() + } + } + } +} + +extension VPVideoAPI { + ///更新短剧关注状态 [ "state" : isCollect, "id" : shortPlayId,] + @objc static let updateShortCollectStateNotification = NSNotification.Name(rawValue: "VPVideoAPI.updateShortCollectStateNotification") + } diff --git a/Veloria/Base/View/TabBar/VPTabBar.swift b/Veloria/Base/View/TabBar/VPTabBar.swift index f61e085..5404c43 100644 --- a/Veloria/Base/View/TabBar/VPTabBar.swift +++ b/Veloria/Base/View/TabBar/VPTabBar.swift @@ -128,6 +128,8 @@ extension VPTabBar { } func reload() { + removeAll() + guard let items = self.items else { return } diff --git a/Veloria/Base/View/TabBar/VPTabBarItemSelectedView.swift b/Veloria/Base/View/TabBar/VPTabBarItemSelectedView.swift index 455add5..d4e8028 100644 --- a/Veloria/Base/View/TabBar/VPTabBarItemSelectedView.swift +++ b/Veloria/Base/View/TabBar/VPTabBarItemSelectedView.swift @@ -50,8 +50,8 @@ class VPTabBarItemSelectedView: VPGradientView { super.init(frame: frame) colors = [UIColor.color7C174F().cgColor, UIColor.color05CEA0().cgColor] locations = [0, 1] - startPoint = .init(x: 0, y: 0.5) - endPoint = .init(x: 1, y: 0.5) + startPoint = .init(x: 0, y: 0.3) + endPoint = .init(x: 1, y: 0.8) layer.cornerRadius = VPTabBar.itemMinWidth / 2 layer.masksToBounds = true diff --git a/Veloria/Base/View/VPScrollView.swift b/Veloria/Base/View/VPScrollView.swift new file mode 100644 index 0000000..45276e6 --- /dev/null +++ b/Veloria/Base/View/VPScrollView.swift @@ -0,0 +1,21 @@ +// +// VPScrollView.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit + +class VPScrollView: UIScrollView { + + override init(frame: CGRect) { + super.init(frame: frame) + self.contentInsetAdjustmentBehavior = .never + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/Veloria/Class/Explore/Controller/VPExploreViewController.swift b/Veloria/Class/Explore/Controller/VPExploreViewController.swift new file mode 100644 index 0000000..2997f8c --- /dev/null +++ b/Veloria/Class/Explore/Controller/VPExploreViewController.swift @@ -0,0 +1,90 @@ +// +// VPExploreViewController.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/22. +// + +import UIKit + +class VPExploreViewController: VPVideoPlayerViewController { + + override var PlayerCellClass: VPVideoPlayerCell.Type { + return VPExplorePlayerCell.self + } + + var pagination: VPListPaginationModel? + + + override func viewDidLoad() { + super.viewDidLoad() + self.delegate = self + self.dataSource = self + + + requestDataArr(page: 1) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(true, animated: true) + } + + +} + +//MARK: -------------- VPPlayerListViewControllerDelegate -------------- +extension VPExploreViewController: VPPlayerListViewControllerDelegate { + func vp_playerViewControllerLoadMoreData(playerViewController: VPVideoPlayerViewController) { + guard let pagination = self.pagination else { return } + guard let page = self.pagination?.current_page else { return } + let pageSize = pagination.page_size ?? 0 + if pagination.page_total ?? 0 <= pageSize * page { + return + } + self.requestDataArr(page: page + 1) + } +} + +//MARK: -------------- VPPlayerListViewControllerDataSource -------------- +extension VPExploreViewController: VPPlayerListViewControllerDataSource { + + func vp_playerListViewController(_ viewController: VPVideoPlayerViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, oldCell: UICollectionViewCell) -> UICollectionViewCell { + + if let cell = oldCell as? VPVideoPlayerCell { + if let model = dataArr[indexPath.row] as? VPShortModel { + cell.shortModel = model + cell.videoInfo = model.video_info + } + cell.isLoop = false + } + return oldCell + } + + func vp_playerListViewController(_ viewController: VPVideoPlayerViewController, _ collectionView: UICollectionView, numberOfItemsInSection section: Int, oldNumber: Int) -> Int { + return oldNumber + } + +} + +extension VPExploreViewController { + + private func requestDataArr(page: Int) { + + VPVideoAPI.requestRecommandsVideo(page: page) { [weak self] listModel in + guard let self = self else { return } + if let listModel = listModel, let list = listModel.list { + if page == 1 { + self.setDataArr(dataArr: list) { [weak self] in + self?.play() + } + } else { + self.addDataArr(dataArr: list) + } + self.pagination = listModel.pagination + } + } + + } + +} diff --git a/Veloria/Class/Explore/View/VPExplorePlayerCell.swift b/Veloria/Class/Explore/View/VPExplorePlayerCell.swift new file mode 100644 index 0000000..2187783 --- /dev/null +++ b/Veloria/Class/Explore/View/VPExplorePlayerCell.swift @@ -0,0 +1,16 @@ +// +// VPExplorePlayerCell.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/22. +// + +import UIKit + +class VPExplorePlayerCell: VPVideoPlayerCell { + + override var ControlViewClass: VPVideoPlayerControlView.Type { + return VPExplorePlayerControlView.self + } + +} diff --git a/Veloria/Class/Explore/View/VPExplorePlayerControlView.swift b/Veloria/Class/Explore/View/VPExplorePlayerControlView.swift new file mode 100644 index 0000000..ed69dd0 --- /dev/null +++ b/Veloria/Class/Explore/View/VPExplorePlayerControlView.swift @@ -0,0 +1,131 @@ +// +// VPExplorePlayerControlView.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/22. +// + +import UIKit + +class VPExplorePlayerControlView: VPVideoPlayerControlView { + + override var videoInfo: VPVideoInfoModel? { + didSet { + epLabel.text = String(format: "EP.%@".localized, videoInfo?.episode ?? "0") + + } + } + + override var shortModel: VPShortModel? { + didSet { + allView.setTitle(String(format: "All %@ Episodes".localized, "\(shortModel?.episode_total ?? 0)"), for: .normal) + videoNameLabel.text = shortModel?.name + } + } + + + private lazy var moreButton: UIControl = { + let button = UIButton(type: .custom) + button.backgroundColor = .color000000(alpha: 0.2) + button.addTarget(self, action: #selector(handleMoreButton), for: .touchUpInside) + return button + }() + + private lazy var epIconImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "ep_icon_01")) + return imageView + }() + + private lazy var epLabel: UILabel = { + let label = UILabel() + label.font = .fontRegular(ofSize: 12) + label.textColor = .colorFFFFFF() + return label + }() + + private lazy var allView: UIButton = { + let view = JXButton(type: .custom) + view.isUserInteractionEnabled = false + view.jx_font = .fontRegular(ofSize: 12) + view.titleDirection = .left + view.space = 6 + view.setTitleColor(.colorFFFFFB(), for: .normal) + view.setImage(UIImage(named: "arrow_right_icon_01"), for: .normal) + return view + }() + + private lazy var videoNameLabel: UILabel = { + let label = UILabel() + label.font = .fontMedium(ofSize: 14) + label.textColor = .colorFFFFFF() + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + vp_setupUI() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +extension VPExplorePlayerControlView { + + @objc private func handleMoreButton() { + let vc = VPDetailPlayerViewController() + vc.shortPlayId = self.shortModel?.short_play_id + self.viewController?.navigationController?.pushViewController(vc, animated: true) + } + +} + +extension VPExplorePlayerControlView { + + private func vp_setupUI() { + self.progressView.insets = .init(top: 30, left: 15, bottom: 6, right: 15) + + addSubview(videoNameLabel) + self.progressView.addSubview(moreButton) + moreButton.addSubview(epIconImageView) + moreButton.addSubview(epLabel) + moreButton.addSubview(allView) + + self.progressView.snp.remakeConstraints { make in + make.left.equalToSuperview() + make.centerX.equalToSuperview() + make.bottom.equalToSuperview() + } + + videoNameLabel.snp.makeConstraints { make in + make.left.equalToSuperview().offset(15) + make.bottom.equalTo(self.progressView.snp.top).offset(-15) + make.right.lessThanOrEqualToSuperview().offset(-120) + } + + moreButton.snp.makeConstraints { make in + make.left.right.top.equalToSuperview() + make.height.equalTo(30) + } + + epIconImageView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalToSuperview().offset(15) + } + + epLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalTo(epIconImageView.snp.right).offset(5) + } + + allView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.right.equalToSuperview().offset(-15) + } + } + + +} diff --git a/Veloria/Class/Home/Controller/VPHomeListViewController.swift b/Veloria/Class/Home/Controller/VPHomeListViewController.swift index e7e9fce..c046b98 100644 --- a/Veloria/Class/Home/Controller/VPHomeListViewController.swift +++ b/Veloria/Class/Home/Controller/VPHomeListViewController.swift @@ -32,7 +32,7 @@ class VPHomeListViewController: VPViewController, WMZPageProtocol { collectionView.delegate = self collectionView.dataSource = self collectionView.contentInset = .init(top: 0, left: 0, bottom: UIScreen.customTabBarHeight + 10, right: 0) - collectionView.vp_addRefreshBackFooter(insetBottom: collectionView.contentInset.bottom) { [weak self] in + collectionView.vp_addRefreshBackFooter(insetBottom: 0) { [weak self] in self?.handleFooterRefresh(nil) } collectionView.register(VPHomeListCell.self, forCellWithReuseIdentifier: "cell") @@ -93,6 +93,13 @@ extension VPHomeListViewController: UICollectionViewDelegate, UICollectionViewDa return self.dataArr.count } + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let model = dataArr[indexPath.row] + let vc = VPDetailPlayerViewController() + vc.shortPlayId = model.short_play_id + self.navigationController?.pushViewController(vc, animated: true) + } + } extension VPHomeListViewController { diff --git a/Veloria/Class/Home/Controller/VPHomePageViewController.swift b/Veloria/Class/Home/Controller/VPHomePageViewController.swift index a4dc9f7..1973aaa 100644 --- a/Veloria/Class/Home/Controller/VPHomePageViewController.swift +++ b/Veloria/Class/Home/Controller/VPHomePageViewController.swift @@ -149,11 +149,11 @@ class VPHomePageViewController: VPViewController { private func setButtonState(button: UIButton) { button.layer.masksToBounds = true if button.isSelected { - button.vp_setGradient() - button.vp_removeGradientBorder() + button.bt_setGradient() + button.bt_removeGradientBorder() } else { - button.vp_removeGradient() - button.vp_setGradientBorder() + button.bt_removeGradient() + button.bt_setGradientBorder() } } diff --git a/Veloria/Class/Home/View/VPHomeBannerContentCell.swift b/Veloria/Class/Home/View/VPHomeBannerContentCell.swift index 3e6203c..e1119b7 100644 --- a/Veloria/Class/Home/View/VPHomeBannerContentCell.swift +++ b/Veloria/Class/Home/View/VPHomeBannerContentCell.swift @@ -71,4 +71,12 @@ extension VPHomeBannerContentCell: ZKCycleScrollViewDataSource, ZKCycleScrollVie cell.model = self.item?.list?[index] return cell } + + func cycleScrollView(_ cycleScrollView: ZKCycleScrollView, didSelectItemAt index: Int) { + guard let model = self.item?.list?[index] else { return } + + let vc = VPDetailPlayerViewController() + vc.shortPlayId = model.short_play_id + self.viewController?.navigationController?.pushViewController(vc, animated: true) + } } diff --git a/Veloria/Class/Home/View/VPHomeRankingContentCell.swift b/Veloria/Class/Home/View/VPHomeRankingContentCell.swift index c8e60a3..b64ec12 100644 --- a/Veloria/Class/Home/View/VPHomeRankingContentCell.swift +++ b/Veloria/Class/Home/View/VPHomeRankingContentCell.swift @@ -105,4 +105,10 @@ extension VPHomeRankingContentCell: UICollectionViewDelegate, UICollectionViewDa return item?.list?.count ?? 0 } + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let model = item?.list?[indexPath.row] else { return } + let vc = VPDetailPlayerViewController() + vc.shortPlayId = model.short_play_id + self.viewController?.navigationController?.pushViewController(vc, animated: true) + } } diff --git a/Veloria/Class/Home/View/VPHomeRecommandContentCell.swift b/Veloria/Class/Home/View/VPHomeRecommandContentCell.swift index 13107c2..225dc63 100644 --- a/Veloria/Class/Home/View/VPHomeRecommandContentCell.swift +++ b/Veloria/Class/Home/View/VPHomeRecommandContentCell.swift @@ -97,4 +97,11 @@ extension VPHomeRecommandContentCell: UICollectionViewDelegate, UICollectionView func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return self.item?.list?.count ?? 0 } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let model = item?.list?[indexPath.row] else { return } + let vc = VPDetailPlayerViewController() + vc.shortPlayId = model.short_play_id + self.viewController?.navigationController?.pushViewController(vc, animated: true) + } } diff --git a/Veloria/Class/Player/Controller/VPDetailPlayerViewController.swift b/Veloria/Class/Player/Controller/VPDetailPlayerViewController.swift new file mode 100644 index 0000000..7bb46ba --- /dev/null +++ b/Veloria/Class/Player/Controller/VPDetailPlayerViewController.swift @@ -0,0 +1,166 @@ +// +// VPDetailPlayerViewController.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit + +class VPDetailPlayerViewController: VPVideoPlayerViewController { + + override var PlayerCellClass: VPVideoPlayerCell.Type { + return VPDetailPlayerCell.self + } + + override var contentSize: CGSize { + return .init(width: UIScreen.width, height: UIScreen.height) + } + + var shortPlayId: String? + var activityId: String? + + private var detailModel: VPVideoDetailModel? + + //MARK: UI属性 + private lazy var backButton: UIButton = { + let button = UIButton(type: .custom) + button.setImage(UIImage(named: "arrow_left_icon_01"), for: .normal) + button.addTarget(self, action: #selector(handleBack), for: .touchUpInside) + return button + }() + + private lazy var videoNameLabel: UILabel = { + let label = UILabel() + label.font = .fontMedium(ofSize: 16) + label.textColor = .colorFFFFFF() + return label + }() + + ///选集 + private weak var episodeView: VPEpisodeView? + + override func viewDidLoad() { + super.viewDidLoad() + self.delegate = self + self.dataSource = self + requestDetailData() + + vp_setupUI() + vp_addAction() + } + + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(true, animated: true) + } + +} + +extension VPDetailPlayerViewController { + + private func vp_setupUI() { + view.addSubview(backButton) + view.addSubview(videoNameLabel) + + backButton.snp.makeConstraints { make in + make.left.equalToSuperview() + make.top.equalToSuperview().offset(UIScreen.statusBarHeight) + make.width.equalTo(48) + make.height.equalTo(44) + } + + videoNameLabel.snp.makeConstraints { make in + make.centerY.equalTo(backButton) + make.left.equalTo(backButton.snp.right) + make.right.lessThanOrEqualToSuperview().offset(-150) + } + + } + + private func vp_addAction() { + self.viewModel.handleEpisode = { [weak self] in + self?.onEpisode() + } + + } +} + +extension VPDetailPlayerViewController { + + private func onEpisode() { + let view = VPEpisodeView() + view.dataArr = detailModel?.episodeList ?? [] + view.shortModel = detailModel?.shortPlayInfo + view.currentIndex = self.currentIndexPath.row + view.didSelectedIndex = { [weak self] (index) in + self?.scrollToItem(indexPath: IndexPath(row: index, section: 0), animated: false) + } + view.present(in: nil) + self.episodeView = view + } + +} + +//MARK: -------------- VPPlayerListViewControllerDataSource -------------- +extension VPDetailPlayerViewController: VPPlayerListViewControllerDataSource { + func vp_playerListViewController(_ viewController: VPVideoPlayerViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, oldCell: UICollectionViewCell) -> UICollectionViewCell { + guard let cell = oldCell as? VPDetailPlayerCell else { return oldCell } + cell.shortModel = detailModel?.shortPlayInfo + cell.videoInfo = detailModel?.episodeList?[indexPath.row] + cell.isLoop = false + + return cell + } + + func vp_playerListViewController(_ viewController: VPVideoPlayerViewController, _ collectionView: UICollectionView, numberOfItemsInSection section: Int, oldNumber: Int) -> Int { + return self.detailModel?.episodeList?.count ?? 0 + } + + +} + +//MARK: -------------- VPPlayerListViewControllerDelegate -------------- +extension VPDetailPlayerViewController: VPPlayerListViewControllerDelegate { + func vp_playerListViewController(_ viewController: VPVideoPlayerViewController, didChangeIndexPathForVisible indexPath: IndexPath) { + self.episodeView?.currentIndex = indexPath.row + } +} + + + +extension VPDetailPlayerViewController { + + private func requestDetailData() { + guard let shortPlayId = shortPlayId else { return } + + VPVideoAPI.requestVideoDetail(shortPlayId: shortPlayId, activityId: activityId) { [weak self] model in + 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 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/Veloria/Class/Player/Controller/VPVideoPlayerViewController.swift b/Veloria/Class/Player/Controller/VPVideoPlayerViewController.swift new file mode 100644 index 0000000..f19084f --- /dev/null +++ b/Veloria/Class/Player/Controller/VPVideoPlayerViewController.swift @@ -0,0 +1,382 @@ +// +// VPVideoPlayerViewController.swift +// Veloria +// +// Created by Veloria on 2025/5/22. +// + +import UIKit + +@objc protocol VPPlayerListViewControllerDelegate { + + ///加载新数据 + @objc optional func vp_playerViewControllerLoadNewDataV2(playerViewController: VPVideoPlayerViewController) + + ///将要加载更多数据 + @objc optional func vp_playerViewControllerShouldLoadMoreData(playerViewController: VPVideoPlayerViewController) -> Bool + ///加载更多数据 + @objc optional func vp_playerViewControllerLoadMoreData(playerViewController: VPVideoPlayerViewController) + ///向上加载更多数据 + @objc optional func vp_playerViewControllerLoadUpMoreData(playerViewController: VPVideoPlayerViewController) + + ///当前展示的发生变化 + @objc optional func vp_playerListViewController(_ viewController: VPVideoPlayerViewController, didChangeIndexPathForVisible indexPath: IndexPath) + +} + +@objc protocol VPPlayerListViewControllerDataSource { + + + func vp_playerListViewController(_ viewController: VPVideoPlayerViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, oldCell: UICollectionViewCell) -> UICollectionViewCell + + func vp_playerListViewController(_ viewController: VPVideoPlayerViewController, _ collectionView: UICollectionView, numberOfItemsInSection section: Int, oldNumber: Int) -> Int + + + +} + +class VPVideoPlayerViewController: VPViewController { + + var contentSize: CGSize { + return CGSize(width: UIScreen.width, height: UIScreen.height - UIScreen.customTabBarHeight) + } + + var PlayerCellClass: VPVideoPlayerCell.Type { + return VPVideoPlayerCell.self + } + + weak var delegate: VPPlayerListViewControllerDelegate? + weak var dataSource: VPPlayerListViewControllerDataSource? + + private(set) var viewModel = VPVideoPlayViewModel() + + private(set) var dataArr: [Any] = [] + + private(set) var currentIndexPath = IndexPath(row: 0, section: 0) + + ///自动下一级 + var autoNextEpisode = true + + private lazy var collectionViewLayout: UICollectionViewLayout = { + let layout = UICollectionViewFlowLayout() + layout.itemSize = contentSize + layout.minimumInteritemSpacing = 0 + layout.minimumLineSpacing = 0 + return layout + }() + + private(set) lazy var collectionView: VPCollectionView = { + let collectionView = VPCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) + collectionView.delegate = self + collectionView.dataSource = self + collectionView.isPagingEnabled = true + collectionView.showsVerticalScrollIndicator = false + collectionView.showsHorizontalScrollIndicator = false + collectionView.bounces = false + collectionView.scrollsToTop = false +// PlayerCellClass.registerCell(collectionView: collectionView) + collectionView.register(PlayerCellClass.self, forCellWithReuseIdentifier: "cell") + return collectionView + }() + + deinit { + NotificationCenter.default.removeObserver(self) + } + + override func viewDidLoad() { + super.viewDidLoad() + NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActiveNotification), name: UIApplication.didBecomeActiveNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(willResignActiveNotification), name: UIApplication.willResignActiveNotification, object: nil) + + do { + try? KTVHTTPCache.proxyStart() + } + + vp_setupUI() + vp_addActio() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + if getDataCount() > 0 && self.viewModel.isPlaying { + self.viewModel.currentPlayer?.start() +// self.play() + } + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + self.viewModel.currentPlayer?.pause() + ///处理切换页面后,滑动代理scrollViewDidEndDecelerating不执行的问题 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in + guard let self = self else { return } + self.scrollDidEnd(self.collectionView) + } + } + + func play() { + if self.isDidAppear { + self.viewModel.currentPlayer?.start() + } + + self.viewModel.isPlaying = true + + if getDataCount() - currentIndexPath.row <= 2 { + self.loadMoreData() + } + +// if isFirstPlay { +// isFirstPlay = false +// let offset = self.collectionView.contentOffset.y + 0.2 +// self.collectionView.setContentOffset(CGPoint(x: 0, y: offset), animated: false) +// +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { +// let offset = self.collectionView.contentOffset.y +// self.collectionView.setContentOffset(CGPoint(x: 0, y: floor(offset)), animated: false) +// } +// } + } + + func pause() { + self.viewModel.isPlaying = false + self.viewModel.currentPlayer?.pause() + } + + func setDataArr(dataArr: [Any], completer: (() -> Void)?) { + self.dataArr = dataArr + reloadData(completion: completer) + } + + func addDataArr(dataArr: [Any]) { + 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() + } + + func reloadData(completion: (() -> Void)? = nil) { + CATransaction.setCompletionBlock { [weak self] in + guard let self = self else { return } + let cell = self.collectionView.cellForItem(at: self.currentIndexPath) as? VPVideoPlayerCell + self.viewModel.currentPlayer = cell + + completion?() + } + CATransaction.begin() + self.collectionView.reloadData() + CATransaction.commit() + } + + func scrollToItem(indexPath: IndexPath, animated: Bool = true, completer: (() -> Void)? = nil) { + CATransaction.setCompletionBlock { [weak self] in + guard let self = self else { return } + if !animated { + if self.currentIndexPath != indexPath { + self.skip(indexPath: indexPath) + } else { + self.play() + } + } + completer?() + } + CATransaction.begin() + self.collectionView.scrollToItem(at: indexPath, at: .top, animated: animated); + CATransaction.commit() + } + + ///当前播放完成 子类可重写 + func currentPlayFinish() { + if self.autoNextEpisode { + scrollToNextEpisode() + } + } + + ///当前播放进度变更 子类可重写 + func currentPlayTimeDidChange(time: Int) { + + } +} + +extension VPVideoPlayerViewController { + func getDataCount() -> Int { + return self.collectionView(self.collectionView, numberOfItemsInSection: 0) + } + + ///点击播放或暂停 + private func clickPauseOrPlay() { + if self.viewModel.isPlaying { + self.pause() + } else { + self.play() + } + } + + + ///滑动至下一级 + 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 + } + + +} + + +extension VPVideoPlayerViewController { + private func vp_setupUI() { + view.addSubview(collectionView) + + collectionView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalToSuperview() + make.width.equalTo(self.contentSize.width) + make.height.equalTo(self.contentSize.height) + } + } + + private func vp_addActio() { + self.viewModel.handlePauseOrPlay = { [weak self] in + self?.clickPauseOrPlay() + } + + self.viewModel.handlePlayFinish = { [weak self] in + self?.currentPlayFinish() + } + + self.viewModel.handlePlayTimeDidChange = { [weak self] time in + self?.currentPlayTimeDidChange(time: time) + } + } +} + + +//MARK: -------------- UICollectionViewDelegate UICollectionViewDataSource -------------- +extension VPVideoPlayerViewController: UICollectionViewDelegate, UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + var cell: UICollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) + + if let newCell = self.dataSource?.vp_playerListViewController(self, collectionView, cellForItemAt: indexPath, oldCell: cell) { + cell = newCell + } + + if let cell = cell as? VPVideoPlayerCell { + if cell.viewModel == nil { + cell.viewModel = viewModel + } + } + + if self.viewModel.currentPlayer == nil, indexPath == currentIndexPath, let playerProtocol = cell as? VPPlayerProtocol { + self.currentIndexPath = indexPath + self.viewModel.currentPlayer = playerProtocol + didChangeIndexPathForVisible() + } + + + return cell + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + let count = dataArr.count + if let newCount = self.dataSource?.vp_playerListViewController(self, collectionView, numberOfItemsInSection: section, oldNumber: count) { + return newCount + } else { + return count + } + } + + //滑动停止 + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + scrollDidEnd(scrollView) + } + + func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { + scrollDidEnd(scrollView) + } + + 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 self.currentIndexPath != indexPath { + self.skip(indexPath: indexPath) + } + } + } + } + + private func skip(indexPath: IndexPath) { + currentIndexPath = indexPath + guard let currentPlayer = self.collectionView.cellForItem(at: indexPath) as? VPPlayerProtocol else { return } + self.viewModel.currentPlayer = currentPlayer +// currentCell = self.collectionView.cellForItem(at: indexPath) as? BCListPlayerCell + didChangeIndexPathForVisible() + self.play() + } +} + + +extension VPVideoPlayerViewController { + + private func loadMoreData() { + let isLoad = self.delegate?.vp_playerViewControllerShouldLoadMoreData?(playerViewController: self) + if isLoad != false { + self.delegate?.vp_playerViewControllerLoadMoreData?(playerViewController: self) + } + } + + private func loadUpMoreData() { + self.delegate?.vp_playerViewControllerLoadUpMoreData?(playerViewController: self) + } + + private func didChangeIndexPathForVisible() { + self.delegate?.vp_playerListViewController?(self, didChangeIndexPathForVisible: self.currentIndexPath) + } +} + +//MARK: -------------- APP生命周期 -------------- +extension VPVideoPlayerViewController { + + @objc func didBecomeActiveNotification() { + if getDataCount() > 0 && self.viewModel.isPlaying && isDidAppear { + self.viewModel.currentPlayer?.start() + } + } + + @objc func willResignActiveNotification() { + self.viewModel.currentPlayer?.pause() + } + + +} diff --git a/Veloria/Class/Player/Model/VPPlayerProtocol.swift b/Veloria/Class/Player/Model/VPPlayerProtocol.swift new file mode 100644 index 0000000..861cbe7 --- /dev/null +++ b/Veloria/Class/Player/Model/VPPlayerProtocol.swift @@ -0,0 +1,46 @@ +// +// VPPlayerProtocol.swift +// Veloria +// +// Created by Veloria on 2025/5/22. +// + +import UIKit + +@objc protocol VPPlayerProtocol: NSObjectProtocol { + + ///播放完成 + var playerFinishHadle: (() -> Void)? { get set } + +// var model: Any? { get set } +// var videoInfo: VPVideoInfoModel? { get set } + + var isCurrent: Bool { get set } + + ///上一集是否加锁 + @objc optional var hasLockUpEpisode: Bool { get set } + + ///总进度 + var duration: Int { get } + ///当前进度 + var currentPosition: Int { get } + + var rate: Float { get set } + + ///播放准备 + func prepare() + + ///开始播放 + func start() + + ///暂停播放 + func pause() + + ///从头播放 + func replay() + + ///设置进度 + func seekToTime(toTime: Int) + + +} diff --git a/Veloria/Class/Player/Model/VPVideoDetailModel.swift b/Veloria/Class/Player/Model/VPVideoDetailModel.swift new file mode 100644 index 0000000..dcb7cdd --- /dev/null +++ b/Veloria/Class/Player/Model/VPVideoDetailModel.swift @@ -0,0 +1,23 @@ +// +// VPVideoDetailModel.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit +import SmartCodable + +class VPVideoDetailModel: VPModel, SmartCodable { + var business_model: String? + var video_info: VPVideoInfoModel? + var shortPlayInfo: VPShortModel? + var episodeList: [VPVideoInfoModel]? + var is_collect: Bool? + var show_share_coin: Int? + var share_coin: Int? + var install_coins: Int? + var revolution: Int? + var unlock_video_ad_count: Int? + var discount: Int? +} diff --git a/Veloria/Class/Player/Model/VPVideoRateModel.swift b/Veloria/Class/Player/Model/VPVideoRateModel.swift new file mode 100644 index 0000000..ed5a097 --- /dev/null +++ b/Veloria/Class/Player/Model/VPVideoRateModel.swift @@ -0,0 +1,69 @@ +// +// VPVideoRateModel.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit + +class VPVideoRateModel: NSObject { + + enum Rate: String { + case x0_5 = "0.5x" + case x0_75 = "0.75x" + case x1 = "1.0x" + case x1_25 = "1.25x" + case x1_5 = "1.5x" + case x1_75 = "1.75x" + case x2 = "2.0x" + + func getRate() -> Float { + switch self { + 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() -> [VPVideoRateModel] { + return [ + VPVideoRateModel(rate: .x0_75), + VPVideoRateModel(rate: .x1), + VPVideoRateModel(rate: .x1_25), + VPVideoRateModel(rate: .x1_5), + VPVideoRateModel(rate: .x1_75), + VPVideoRateModel(rate: .x2) + ] + } + + var rate: Rate = .x1 + + init(rate: Rate) { + super.init() + self.rate = rate + } + + func formatString() -> String { + return self.rate.rawValue + } + +} diff --git a/Veloria/Class/Player/View/VPDetailPlayerCell.swift b/Veloria/Class/Player/View/VPDetailPlayerCell.swift new file mode 100644 index 0000000..a632c92 --- /dev/null +++ b/Veloria/Class/Player/View/VPDetailPlayerCell.swift @@ -0,0 +1,16 @@ +// +// VPDetailPlayerCell.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit + +class VPDetailPlayerCell: VPVideoPlayerCell { + + override var ControlViewClass: VPVideoPlayerControlView.Type { + return VPDetailPlayerControlView.self + } + +} diff --git a/Veloria/Class/Player/View/VPDetailPlayerControlView.swift b/Veloria/Class/Player/View/VPDetailPlayerControlView.swift new file mode 100644 index 0000000..cfe0d7b --- /dev/null +++ b/Veloria/Class/Player/View/VPDetailPlayerControlView.swift @@ -0,0 +1,221 @@ +// +// VPDetailPlayerControlView.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit + +class VPDetailPlayerControlView: VPVideoPlayerControlView { + + override var viewModel: VPVideoPlayViewModel? { + didSet { + self.viewModel?.addObserver(self, forKeyPath: "rateModel", options: .new, context: nil) + + rateButton.setTitle(self.viewModel?.rateModel.formatString(), for: .normal) + } + } + + + override var videoInfo: VPVideoInfoModel? { + didSet { + epView.setTitle(String(format: "EP.%@".localized, "\(videoInfo?.episode ?? "0")"), for: .normal) + } + } + + override var shortModel: VPShortModel? { + didSet { + allEpView.setTitle(String(format: "All %@ Episodes".localized, "\(shortModel?.episode_total ?? 0)"), for: .normal) + } + } + + override var durationTime: Int { + didSet { + updateTimeLabel() + } + } + + override var currentTime: Int { + didSet { + updateTimeLabel() + } + } + + //MARK: -------------- UI属性 -------------- + private lazy var bottomView: VPGradientView = { + let view = VPGradientView() + view.isUserInteractionEnabled = false + view.colors = [UIColor.color000000(alpha: 0).cgColor, UIColor.color000000(alpha: 0.5).cgColor, UIColor.color000000(alpha: 1).cgColor] + view.locations = [0, 0.5, 1] + view.startPoint = .init(x: 0.5, y: 0) + view.endPoint = .init(x: 0.5, y: 1) + return view + }() + + private lazy var epBgView: UIView = { + let view = UIButton(type: .custom) + view.setBackgroundImage(UIImage(color: .color949494(alpha: 0.4)), for: .normal) + view.layer.cornerRadius = 15 + view.layer.masksToBounds = true + view.addTarget(self, action: #selector(handleEpisodesButton), for: .touchUpInside) + return view + }() + + private lazy var epView: UIButton = { + let view = JXButton(type: .custom) + view.isUserInteractionEnabled = false + view.titleDirection = .right + view.jx_font = .fontRegular(ofSize: 13) + view.space = 5 + view.setTitleColor(.colorFFFFFF(), for: .normal) + view.setImage(UIImage(named: "ep_icon_01"), for: .normal) + return view + }() + + private lazy var allEpView: UIButton = { + let view = JXButton(type: .custom) + view.isUserInteractionEnabled = false + view.titleDirection = .left + view.jx_font = .fontRegular(ofSize: 13) + view.space = 4 + view.setTitleColor(.colorBEBEBE(), for: .normal) + view.setImage(UIImage(named: "arrow_up_icon_01"), for: .normal) + return view + }() + + private lazy var rateButton: UIButton = { + let button = UIButton(type: .custom) + button.setBackgroundImage(UIImage(color: .color949494(alpha: 0.4)), for: .normal) + button.layer.cornerRadius = 15 + button.layer.masksToBounds = true + button.setTitleColor(.colorFFFFFF(), for: .normal) + button.titleLabel?.font = .fontRegular(ofSize: 13) + button.addTarget(self, action: #selector(handleRateButton), for: .touchUpInside) + return button + }() + + private lazy var timeLabel: UILabel = { + let label = UILabel() + label.font = .fontRegular(ofSize: 12) + label.textColor = .colorFFFFFB(alpha: 0.9) + label.isUserInteractionEnabled = false + return label + }() + + ///倍速选择 + private lazy var rateSelectedView: VPRateSelectedView = { + let view = VPRateSelectedView() + view.didSelected = { [weak self] model in + guard let self = self else { return } + self.viewModel?.rateModel = model + } + return view + }() + + deinit { + self.viewModel?.removeObserver(self, forKeyPath: "rateModel") + } + + override init(frame: CGRect) { + super.init(frame: frame) + + vp_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(), for: .normal) + } + } +} + +extension VPDetailPlayerControlView { + + private func updateTimeLabel() { + let currentTime = self.currentTime.formatTimeGroup() + let durationTime = self.durationTime.formatTimeGroup() + + timeLabel.text = "\(currentTime.1):\(currentTime.2)/\(durationTime.1):\(durationTime.2)" + } + + @objc private func handleEpisodesButton() { + self.viewModel?.handleEpisode?() + } + + @objc private func handleRateButton() { + addSubview(rateSelectedView) + rateSelectedView.currentRateModel = self.viewModel?.rateModel + + rateSelectedView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + +} + +extension VPDetailPlayerControlView { + + private func vp_setupUI() { + progressView.lineWidth = 3 + progressView.insets = .init(top: 20, left: 15, bottom: 10, right: 15) + + addSubview(bottomView) + addSubview(epBgView) + epBgView.addSubview(epView) + epBgView.addSubview(allEpView) + addSubview(rateButton) + addSubview(timeLabel) + + self.sendSubviewToBack(self.bottomView) + + progressView.snp.remakeConstraints { make in + make.left.right.equalToSuperview() + make.bottom.equalTo(epBgView.snp.top) + } + + rightToolView.snp.updateConstraints { make in + make.bottom.equalToSuperview().offset(-(UIScreen.tabbarSafeBottomMargin + 150)) + } + + bottomView.snp.makeConstraints { make in + make.left.right.bottom.equalToSuperview() + make.height.equalTo(UIScreen.tabbarSafeBottomMargin + 100) + } + + epBgView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(15) + make.bottom.equalToSuperview().offset(-(UIScreen.tabbarSafeBottomMargin + 10)) + make.height.equalTo(30) + } + + epView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(10) + make.centerY.equalToSuperview() + } + + allEpView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.right.equalToSuperview().offset(-10) + } + + rateButton.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-15) + make.left.equalTo(epBgView.snp.right).offset(10) + make.height.top.equalTo(epBgView) + make.width.equalTo(50) + } + + timeLabel.snp.makeConstraints { make in + make.left.equalToSuperview().offset(15) + make.bottom.equalTo(epBgView.snp.top).offset(-16) + } + } + +} diff --git a/Veloria/Class/Player/View/VPEpisodeCell.swift b/Veloria/Class/Player/View/VPEpisodeCell.swift new file mode 100644 index 0000000..ba2545f --- /dev/null +++ b/Veloria/Class/Player/View/VPEpisodeCell.swift @@ -0,0 +1,78 @@ +// +// VPEpisodeCell.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit + +class VPEpisodeCell: VPCollectionViewCell { + + + var videoInfoModel: VPVideoInfoModel? { + didSet { + numLabel.text = videoInfoModel?.episode + } + } + + var vp_isSelected: Bool = false { + didSet { + if vp_isSelected { + contentView.vp_setGradientBorder() + contentView.backgroundColor = .color05CEA0(alpha: 0.1) + } else { + contentView.vp_removeGradientBorder() + contentView.backgroundColor = .colorFFFFFF(alpha: 0.1) + } + } + } + + private lazy var bgView: UIView = { + let view = UIView() + view.vp_setGradientBorder() + view.layer.cornerRadius = 6 + view.layer.masksToBounds = true + return view + }() + + + + private lazy var numLabel: UILabel = { + let label = UILabel() + label.font = .fontRegular(ofSize: 14) + label.textColor = .colorFFFFFF() + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + vp_setupUI() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +extension VPEpisodeCell { + + private func vp_setupUI() { + contentView.layer.cornerRadius = 6 + contentView.layer.masksToBounds = true + +// contentView.addSubview(bgView) + contentView.addSubview(numLabel) + +// bgView.snp.makeConstraints { make in +// make.edges.equalToSuperview() +// } + + numLabel.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } + +} diff --git a/Veloria/Class/Player/View/VPEpisodeMenuView.swift b/Veloria/Class/Player/View/VPEpisodeMenuView.swift new file mode 100644 index 0000000..ec0cdea --- /dev/null +++ b/Veloria/Class/Player/View/VPEpisodeMenuView.swift @@ -0,0 +1,152 @@ +// +// VPEpisodeMenuView.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit + +class VPEpisodeMenuView: 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 + } + self.progressSlide() + } + } + + + private lazy var buttonArr: [UIButton] = [] + + //MARK: UI属性 + private lazy var progressView: VPGradientView = { + let view = VPGradientView() + view.colors = [UIColor.color05CEA0().cgColor, UIColor.color7C174F().cgColor] + view.startPoint = .init(x: 0, y: 0.3) + view.endPoint = .init(x: 1, y: 0.8) + view.locations = [0, 1] + view.layer.cornerRadius = 1 + view.layer.masksToBounds = true + return view + }() + + private lazy var scrollView: VPScrollView = { + let scrollView = VPScrollView() + scrollView.showsVerticalScrollIndicator = false + scrollView.showsHorizontalScrollIndicator = false + return scrollView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + _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.color = .colorFFFFFF(alpha: 0.6) + normalStrig.font = .fontMedium(ofSize: 14) + + let selectedString = NSMutableAttributedString(string: $1) + selectedString.color = .colorFFFFFF() + selectedString.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(27) + 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(27) + make.height.equalTo(35) + } + } + + previousButton = button + } + + progressSlide() + } + + private func progressSlide() { + if self.selectedIndex >= self.buttonArr.count { return } + let currentButton = self.buttonArr[self.selectedIndex] + + self.progressView.snp.remakeConstraints { make in + make.bottom.width.equalTo(currentButton) + make.centerX.equalTo(currentButton) + make.height.equalTo(2) + } + } + + + @objc private func handleButton(sender: UIButton) { + self.selectedIndex = sender.tag + self.didSelectedIndex?(self.selectedIndex) + } + +} + +extension VPEpisodeMenuView { + + private func _setupUI() { + addSubview(scrollView) + scrollView.addSubview(progressView) + + scrollView.snp.makeConstraints { make in + make.left.right.top.equalToSuperview() + make.bottom.equalToSuperview() + } + } + +} diff --git a/Veloria/Class/Player/View/VPEpisodeView.swift b/Veloria/Class/Player/View/VPEpisodeView.swift new file mode 100644 index 0000000..e66ae84 --- /dev/null +++ b/Veloria/Class/Player/View/VPEpisodeView.swift @@ -0,0 +1,333 @@ +// +// VPEpisodeView.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit + +class VPEpisodeView: HWPanModalContentView { + + var currentIndex: Int = 0 { + didSet { + self.collectionView.reloadData() + } + } + + var shortModel: VPShortModel? { + didSet { + coverImageView.vp_setImage(url: shortModel?.image_url) + videoNameLabel.text = shortModel?.name + if let category = shortModel?.category?.first, !category.isEmpty { + tagView.setTitle(category, for: .normal) + tagView.isHidden = false + } else { + tagView.isHidden = true + } + desLabel.text = shortModel?.sp_description + } + } + + var dataArr: [VPVideoInfoModel] = [] { + didSet { + self.collectionView.reloadData() + + var menuDataArr = [String]() + let totalEpisode = dataArr.count + var index = 0 + var remainingEpisodes = totalEpisode + + while remainingEpisodes > 0 { + let minIndex = index * 30 + var maxIndex = minIndex + 29 + if maxIndex >= dataArr.count { + maxIndex = dataArr.count - 1 + } + + let minEpisode = dataArr[minIndex].episode ?? "0" + let maxEpisode = dataArr[maxIndex].episode ?? "0" + + if minEpisode == maxEpisode { + menuDataArr.append("\(minEpisode)") + } else { + menuDataArr.append("\(minEpisode)-\(maxEpisode)") + } + + remainingEpisodes -= 30 + index += 1 + } + + self.menuView.dataArr = menuDataArr + } + } + + var didSelectedIndex: ((_ index: Int) -> Void)? + + var isDecelerating = false + var isDragging = false + + //MARK: UI属性 + private lazy var bgView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "bg_image_01")) + return imageView + }() + + private lazy var closeButton: UIButton = { + let button = UIButton(type: .custom) + button.setImage(UIImage(named: "close_icon_01"), for: .normal) + button.addTarget(self, action: #selector(handleCloseButton), for: .touchUpInside) + return button + }() + + private lazy var coverImageView: VPImageView = { + let imageView = VPImageView() + imageView.layer.cornerRadius = 6 + imageView.layer.masksToBounds = true + return imageView + }() + + private lazy var videoNameLabel: UILabel = { + let label = UILabel() + label.font = .fontMedium(ofSize: 15) + label.textColor = .colorFFFFFF() + label.numberOfLines = 2 + return label + }() + + private lazy var tagView: JXButton = { + let view = JXButton(type: .custom) + view.isUserInteractionEnabled = false + view.backgroundColor = .colorFFFFFF(alpha: 0.1) + view.leftAndRightMargin = 6 + view.layer.cornerRadius = 3 + view.layer.masksToBounds = true + view.jx_font = .fontRegular(ofSize: 12) + view.setTitleColor(.colorAFAFAF(), for: .normal) + return view + }() + + private lazy var desLabel: UILabel = { + let label = UILabel() + label.font = .fontRegular(ofSize: 12) + label.textColor = .colorFFFFFF(alpha: 0.6) + label.numberOfLines = 3 + return label + }() + + private lazy var lineView: UIView = { + let view = UIView() + view.backgroundColor = .color545458(alpha: 0.45) + return view + }() + + private lazy var collectionViewLayout: UICollectionViewFlowLayout = { + let itemWidth = floor((UIScreen.width - 8 * 4 - 30) / 5) + + let layout = UICollectionViewFlowLayout() + layout.itemSize = .init(width: itemWidth, height: 54) + layout.minimumLineSpacing = 9 + layout.minimumInteritemSpacing = 8 + layout.sectionInset = .init(top: 0, left: 15, bottom: 0, right: 15) + return layout + }() + + private lazy var collectionView: VPCollectionView = { + let collectionView = VPCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) + collectionView.delegate = self + collectionView.dataSource = self + collectionView.contentInset = .init(top: 0, left: 0, bottom: UIScreen.tabbarSafeBottomMargin, right: 0) + collectionView.register(VPEpisodeCell.self, forCellWithReuseIdentifier: "cell") + return collectionView + }() + + private lazy var menuView: VPEpisodeMenuView = { + let view = VPEpisodeMenuView() + 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.dataArr.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) + vp_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func handleCloseButton() { + self.dismiss(animated: true) { + + } + } + + //MARK: HWPanModalPresentable + override func panScrollable() -> UIScrollView? { + return collectionView + } + + override func longFormHeight() -> PanModalHeight { + return PanModalHeightMake(.content, UIScreen.height * (2 / 3)) + } + + override func showDragIndicator() -> Bool { + return false + } + + override func backgroundConfig() -> HWBackgroundConfig { + let config = HWBackgroundConfig() + config.backgroundAlpha = 0.6 + return config + } +} + +extension VPEpisodeView { + + private func vp_setupUI() { + addSubview(bgView) + addSubview(closeButton) + addSubview(coverImageView) + addSubview(videoNameLabel) + addSubview(tagView) + addSubview(desLabel) + addSubview(lineView) + addSubview(menuView) + addSubview(collectionView) + + bgView.snp.makeConstraints { make in + make.left.right.top.equalToSuperview() + } + + closeButton.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-5) + make.top.equalToSuperview().offset(5) + make.width.equalTo(40) + make.height.equalTo(40) + } + + coverImageView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(15) + make.top.equalToSuperview().offset(45) + make.width.equalTo(64) + make.height.equalTo(82) + } + + videoNameLabel.snp.makeConstraints { make in + make.left.equalTo(coverImageView.snp.right).offset(10) + make.centerY.equalTo(coverImageView.snp.top).offset(20) + make.right.lessThanOrEqualToSuperview().offset(-15) + } + + tagView.snp.makeConstraints { make in + make.left.equalTo(videoNameLabel) + make.bottom.equalTo(coverImageView).offset(-12) + } + + desLabel.snp.makeConstraints { make in + make.left.equalToSuperview().offset(15) + make.right.lessThanOrEqualToSuperview().offset(-15) + make.top.equalTo(coverImageView.snp.bottom).offset(10) + } + + lineView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(15) + make.centerX.equalToSuperview() + make.top.equalTo(desLabel.snp.bottom).offset(46) + make.height.equalTo(1) + } + + menuView.snp.makeConstraints { make in + make.left.right.equalTo(self.lineView) + make.bottom.equalTo(self.lineView).offset(0.5) + } + + collectionView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.bottom.equalToSuperview() + make.top.equalTo(lineView.snp.bottom).offset(14) + } + + } + +} + +//MARK: -------------- UICollectionViewDelegate UICollectionViewDataSource -------------- +extension VPEpisodeView: UICollectionViewDelegate, UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! VPEpisodeCell + cell.videoInfoModel = self.dataArr[indexPath.row] + cell.vp_isSelected = indexPath.row == currentIndex + return cell + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return self.dataArr.count + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard indexPath.row != currentIndex else { 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 = dataArr.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 + } + } +} diff --git a/Veloria/Class/Player/View/VPPlayerProgressView.swift b/Veloria/Class/Player/View/VPPlayerProgressView.swift new file mode 100644 index 0000000..2ace123 --- /dev/null +++ b/Veloria/Class/Player/View/VPPlayerProgressView.swift @@ -0,0 +1,212 @@ +// +// VPPlayerProgressView.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/22. +// + +import UIKit + +class VPPlayerProgressView: 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 = 2 + + ///加载中状态 + var isLoading = false { + didSet { + if isLoading { + if gradientTimer == nil { + gradientTimer = Timer.scheduledTimer(timeInterval: 0.05, target: YYWeakProxy(target: self), selector: #selector(handleGradientTimer), userInfo: nil, repeats: true) + } + } else { + gradientTimer?.invalidate() + gradientTimer = nil + } + } + } + + var insets: UIEdgeInsets = .init(top: 0, left: 0, bottom: 0, right: 0) { + 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 height = rect.height + + let progressX = insets.left + let progressY = insets.top +// let progressY = height - lineWidth + 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 y = height - lineWidth + + ///绘制进度 + 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 VPPlayerProgressView { + + @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.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.width + self.panFinish?(offsetX) + } +} diff --git a/Veloria/Class/Player/View/VPRateSelectedCell.swift b/Veloria/Class/Player/View/VPRateSelectedCell.swift new file mode 100644 index 0000000..9795575 --- /dev/null +++ b/Veloria/Class/Player/View/VPRateSelectedCell.swift @@ -0,0 +1,63 @@ +// +// VPRateSelectedCell.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit + +class VPRateSelectedCell: VPCollectionViewCell { + + var model: VPVideoRateModel? { + didSet { + label.text = model?.formatString() + } + } + + var vp_isSelected: Bool = false { + didSet { + if vp_isSelected { + contentView.vp_setGradientBorder() + contentView.backgroundColor = .color1C2D2F(alpha: 0.6) + + } else { + contentView.vp_removeGradientBorder() + contentView.backgroundColor = .color000000(alpha: 0.3) + } + } + } + + private lazy var label: UILabel = { + let label = UILabel() + label.font = .fontRegular(ofSize: 14) + label.textColor = .colorFFFFFF() + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + vp_setupUI() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension VPRateSelectedCell { + + private func vp_setupUI() { + contentView.layer.cornerRadius = 6 + contentView.layer.masksToBounds = true + + + + contentView.addSubview(label) + + label.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } + +} diff --git a/Veloria/Class/Player/View/VPRateSelectedView.swift b/Veloria/Class/Player/View/VPRateSelectedView.swift new file mode 100644 index 0000000..473651a --- /dev/null +++ b/Veloria/Class/Player/View/VPRateSelectedView.swift @@ -0,0 +1,102 @@ +// +// VPRateSelectedView.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit + +class VPRateSelectedView: UIView { + + var currentRateModel: VPVideoRateModel? { + didSet { + collectionView.reloadData() + } + } + + var didSelected: ((_ rateModel: VPVideoRateModel) -> Void)? + + private lazy var rateArr: [VPVideoRateModel] = VPVideoRateModel.getAllRate() + + private lazy var collectionViewLayout: UICollectionViewFlowLayout = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.itemSize = .init(width: 70, height: 54) + layout.minimumLineSpacing = 10 + layout.sectionInset = .init(top: 0, left: 15, bottom: 0, right: 15) + return layout + }() + + private lazy var collectionView: VPCollectionView = { + let collectionView = VPCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) + collectionView.delegate = self + collectionView.dataSource = self + collectionView.register(VPRateSelectedCell.self, forCellWithReuseIdentifier: "cell") + collectionView.showsHorizontalScrollIndicator = false + return collectionView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + let tap = UITapGestureRecognizer(target: self, action: #selector(handleDismiss)) + tap.delegate = self + self.addGestureRecognizer(tap) + vp_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func handleDismiss() { + self.removeFromSuperview() + } +} + +extension VPRateSelectedView { + + private func vp_setupUI() { + addSubview(collectionView) + + collectionView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.bottom.equalToSuperview().offset(-(UIScreen.tabbarSafeBottomMargin + 85)) + make.height.equalTo(54) + } + } + +} + +//MARK: -------------- UICollectionViewDelegate UICollectionViewDataSource -------------- +extension VPRateSelectedView: UICollectionViewDelegate, UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let model = rateArr[indexPath.row] + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! VPRateSelectedCell + cell.model = model + cell.vp_isSelected = model.rate == currentRateModel?.rate + return cell + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return rateArr.count + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let model = rateArr[indexPath.row] + self.didSelected?(model) + self.handleDismiss() + } +} + +//MARK: -------------- UIGestureRecognizerDelegate -------------- +extension VPRateSelectedView: UIGestureRecognizerDelegate { + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + if touch.view != self { + return false + } else { + return true + } + } +} diff --git a/Veloria/Class/Player/View/VPVideoPlayerCell.swift b/Veloria/Class/Player/View/VPVideoPlayerCell.swift new file mode 100644 index 0000000..74ceefc --- /dev/null +++ b/Veloria/Class/Player/View/VPVideoPlayerCell.swift @@ -0,0 +1,214 @@ +// +// VPVideoPlayerCell.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/22. +// + +import UIKit + +class VPVideoPlayerCell: VPCollectionViewCell, VPPlayerProtocol { + + var ControlViewClass: VPVideoPlayerControlView.Type { + return VPVideoPlayerControlView.self + } + + + weak var viewModel: VPVideoPlayViewModel? { + didSet { + self.controlView.viewModel = self.viewModel + } + } + + var shortModel: VPShortModel? { + didSet { + self.controlView.shortModel = shortModel + coverImageView.vp_setImage(url: shortModel?.image_url) + } + } + + var videoInfo: VPVideoInfoModel? { + didSet { + self.controlView.progress = 0 + self.controlView.currentTime = 0 + self.controlView.durationTime = 0 + + self.controlView.videoInfo = videoInfo + + player.setPlayUrl(url: videoInfo?.video_url ?? "") + + } + } + + var isLoop = true { + didSet { + player.isLoop = isLoop + } + } + + + private lazy var player: VPPlayer = { + let player = VPPlayer() + player.playerView = playerView + player.delegate = self + return player + }() + + //MARK: UI属性 + private lazy var playerView: UIView = { + let view = UIView() + return view + }() + + private lazy var coverImageView: VPImageView = { + let imageView = VPImageView() + return imageView + }() + + private lazy var controlView: VPVideoPlayerControlView = { + let view = ControlViewClass.init() + view.panProgressFinishBlock = { [weak self] progress in + guard let self = self else { return } + let duration = CGFloat(self.player.duration) + let toTime = progress * duration + self.player.seekToTime(toTime: Int(toTime)) + } + return view + }() + + + override init(frame: CGRect) { + super.init(frame: frame) + + vp_setupUI() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + //MARK: VPPlayerProtocol + var playerFinishHadle: (() -> Void)? + + var isCurrent: Bool = false { + didSet { + controlView.isCurrent = isCurrent + if !isCurrent { +// self.player.replay() +// self.player.seekToTime(toTime: 0) + self.coverImageView.isHidden = false + } + } + } + + var duration: Int { + get { + return player.duration + } + } + + var currentPosition: Int { + get { + player.currentPosition + } + } + + var rate: Float { + set { + player.rate = newValue + } + get { + player.rate + } + } + + func prepare() { + + } + + func start() { + player.start() + } + + func pause() { + player.pause() + } + + func replay() { + player.replay() + } + + func seekToTime(toTime: Int) { + player.seekToTime(toTime: toTime) + } + + + + +} + +extension VPVideoPlayerCell { + + private func vp_setupUI() { + contentView.addSubview(playerView) + contentView.addSubview(coverImageView) + contentView.addSubview(controlView) + + coverImageView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + playerView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + controlView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + +} + +//MARK: -------------- SPPlayerDelegate -------------- +extension VPVideoPlayerCell: VPPlayerDelegate { + + func vp_playCompletion(_ player: VPPlayer) { +// self.playerFinishHadle?() + self.viewModel?.handlePlayFinish?() + } + + func vp_playLoadingEnd(_ player: VPPlayer) { + + } + + func vp_playTimeChanged(_ player: VPPlayer, currentTime: Int, duration: Int) { + controlView.progress = CGFloat(currentTime) / CGFloat(duration) + controlView.currentTime = currentTime + controlView.durationTime = duration + self.viewModel?.handlePlayTimeDidChange?(currentTime) + } + + func vp_firstRenderedStart(_ player: VPPlayer) { + + } + + func vp_player(_ player: VPPlayer, playStateDidChanged state: VPPlayer.PlayState) { + if state == .playing { + self.coverImageView.isHidden = true + } + } + + func vp_player(_ player: VPPlayer, loadStateDidChange state: VPPlayer.LoadState) { + if state == .prepare || state == .stalled { + self.controlView.isLoading = true + } else { + self.controlView.isLoading = false + } + } + + func vp_playerReadyToPlay(_ player: VPPlayer) { + self.seekToTime(toTime: (videoInfo?.play_seconds ?? 0) / 1000) + } + +} diff --git a/Veloria/Class/Player/View/VPVideoPlayerControlView.swift b/Veloria/Class/Player/View/VPVideoPlayerControlView.swift new file mode 100644 index 0000000..59cce59 --- /dev/null +++ b/Veloria/Class/Player/View/VPVideoPlayerControlView.swift @@ -0,0 +1,299 @@ +// +// VPVideoPlayerControlView.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/22. +// + +import UIKit + +class VPVideoPlayerControlView: UIView { + + weak var viewModel: VPVideoPlayViewModel? { + didSet { + viewModel?.addObserver(self, forKeyPath: "isPlaying", context: nil) + } + } + + var shortModel: VPShortModel? { + didSet { + updateCollectButtonState() + } + } + + var videoInfo: VPVideoInfoModel? { + didSet { + + } + } + + + ///滑动进度条 + var panProgressFinishBlock: ((_ progress: CGFloat) -> Void)? + + ///0-1 + var progress: CGFloat = 0 { + didSet { + progressView.progress = progress + } + } + + ///加载中状态 + var isLoading = false { + didSet { + progressView.isLoading = isLoading + } + } + + var durationTime: Int = 0 + var currentTime: Int = 0 + + var isCurrent: Bool = false { + didSet { + updatePlayIconState() + } + } + + private var hiddenPlayButtonTimer: Timer? + + //MARK: UI属性 + private(set) lazy var progressView: VPPlayerProgressView = { + let view = VPPlayerProgressView() + view.panStart = { [weak self] in + guard let self = self else { return } + self.panProgressStart() + } + view.panChange = { [weak self] progress in + guard let self = self else { return } + self.panProgressChange(progress: progress) + } + + view.panFinish = { [weak self] progress in + guard let self = self else { return } + self.panProgressFinish(progress: progress) + } + return view + }() + + private(set) lazy var rightToolView: UIStackView = { + let view = UIStackView(arrangedSubviews: [collectButton]) + view.axis = .vertical + view.spacing = 28 + return view + }() + + ///收藏按钮 + private lazy var collectButton: UIButton = { + let button = createRightButton(title: "0", image: UIImage(named: "collect_icon_01"), selectedImage: UIImage(named: "collect_icon_01_selected")) + button.addTarget(self, action: #selector(handleCollectButton), for: .touchUpInside) + return button + }() + + private lazy var playButton: UIControl = { + let button = UIControl() +// let button = UIButton(type: .custom) +// button.setImage(UIImage(named: "pause_icon_01"), for: .normal) +// button.setImage(UIImage(named: "play_icon_01"), for: .selected) + button.addEffectView(style: .light) + button.addTarget(self, action: #selector(handlePlayButton), for: .touchUpInside) + button.layer.cornerRadius = 6 + button.layer.masksToBounds = true + button.isHidden = true + return button + }() + + private lazy var playIconImageView: UIImageView = { + let imageView = UIImageView() + return imageView + }() + + deinit { + viewModel?.removeObserver(self, forKeyPath: "isPlaying") + NotificationCenter.default.removeObserver(self) + } + + override init(frame: CGRect) { + super.init(frame: frame) + NotificationCenter.default.addObserver(self, selector: #selector(updateShortCollectStateNotification), name: VPVideoAPI.updateShortCollectStateNotification, object: nil) + + let tap = UITapGestureRecognizer(target: self, action: #selector(handleScreen)) + tap.delegate = self + self.addGestureRecognizer(tap) + + vp_setupUI() + } + + 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?) { + if keyPath == "isPlaying" { + updatePlayIconState() + } + } + + func createRightButton(title: String?, selectedTitle: String? = nil, image: UIImage?, selectedImage: UIImage? = nil) -> UIButton { + let button = JXButton(type: .custom) + button.titleDirection = .down + button.space = 8 + button.setImage(image, for: .normal) + button.setImage(selectedImage, for: .selected) + button.setImage(selectedImage, for: [.selected, .highlighted]) + button.setTitle(title, for: .normal); + button.setTitle(selectedTitle, for: .selected); + button.setTitle(selectedTitle, for: [.selected, .highlighted]) + button.setTitleColor(.colorFFFFFF(), for: .normal) + button.setTitleColor(.colorFFBD36(), for: .selected) + button.setTitleColor(.colorFFBD36(), for: [.selected, .highlighted]) + button.jx_font = .fontRegular(ofSize: 10) + return button + } + + + + func updatePlayIconState() { + let isPlaying = self.viewModel?.isPlaying ?? false + if isCurrent { + if isPlaying { + playIconImageView.image = UIImage(named: "pause_icon_01") + hiddenPlayButtonTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(handleHiddenPlayButtonTimer), userInfo: nil, repeats: false) + } else { + playIconImageView.image = UIImage(named: "play_icon_01") + hiddenPlayButtonTimer?.invalidate() + hiddenPlayButtonTimer = nil + } + } else { + playButton.isHidden = true + playIconImageView.image = UIImage(named: "pause_icon_01") + } + } +} + +extension VPVideoPlayerControlView { + @objc private func handlePlayButton() { + self.hadlePlayAndOrPaused() + } + + @objc func handleScreen() { + if self.viewModel?.isPlaying != true { return } + + self.playButton.isHidden = !self.playButton.isHidden + + if self.playButton.isHidden { + self.hiddenPlayButtonTimer?.invalidate() + self.hiddenPlayButtonTimer = nil + } else { + self.hiddenPlayButtonTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(handleHiddenPlayButtonTimer), userInfo: nil, repeats: false) + } + } + + @objc private func hadlePlayAndOrPaused() { + self.viewModel?.handlePauseOrPlay?() + } + + @objc private func handleHiddenPlayButtonTimer() { + if self.viewModel?.isPlaying != true { return } + + self.playButton.isHidden = true + hiddenPlayButtonTimer?.invalidate() + hiddenPlayButtonTimer = nil + } + + @objc private func handleCollectButton() { + guard let shortPlayId = self.videoInfo?.short_play_id else { return } + guard let videoId = self.videoInfo?.short_play_video_id else { return } + + let isCollect = !(self.shortModel?.is_collect ?? false) + + VPVideoAPI.requestCollectShort(isCollect: isCollect, shortPlayId: shortPlayId, videoId: videoId) { [weak self] in + guard let self = self else { return } + var count = self.shortModel?.collect_total ?? 0 + if isCollect { + count += 1 + } else { + count -= 1 + } + if count < 0 { + count = 0 + } + self.shortModel?.collect_total = count + } + } + + @objc private func updateShortCollectStateNotification(sender: Notification) { + guard let userInfo = sender.userInfo else { return } + guard let shortPlayId = userInfo["id"] as? String else { return } + guard let isCollect = userInfo["state"] as? Bool else { return } + guard shortPlayId == self.videoInfo?.short_play_id else { return } + + self.shortModel?.is_collect = isCollect; + updateCollectButtonState() + + } + + private func updateCollectButtonState() { + self.collectButton.isSelected = self.shortModel?.is_collect ?? false + self.collectButton.setTitle("\(self.shortModel?.collect_total ?? 0)", for: .normal) + } + + +} + +extension VPVideoPlayerControlView { + + private func vp_setupUI() { + addSubview(progressView) + + addSubview(rightToolView) + addSubview(playButton) + playButton.addSubview(playIconImageView) + + rightToolView.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-15) + make.bottom.equalToSuperview().offset(-110) + } + + playButton.snp.makeConstraints { make in + make.center.equalToSuperview() + make.width.equalTo(70) + make.height.equalTo(50) + } + + playIconImageView.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } +} + +extension VPVideoPlayerControlView { + ///滑动进度开始 + private func panProgressStart() { + + + } + + ///滑动进度中 + private func panProgressChange(progress: CGFloat) { + + } + + ///滑动进度结束 + private func panProgressFinish(progress: CGFloat) { + self.panProgressFinishBlock?(progress) + } + + +} + +//MARK: -------------- UIGestureRecognizerDelegate -------------- +extension VPVideoPlayerControlView: UIGestureRecognizerDelegate { + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + if touch.view != self { + return false + } else { + return true + } + } +} diff --git a/Veloria/Class/Player/ViewModel/VPVideoPlayViewModel.swift b/Veloria/Class/Player/ViewModel/VPVideoPlayViewModel.swift new file mode 100644 index 0000000..517550b --- /dev/null +++ b/Veloria/Class/Player/ViewModel/VPVideoPlayViewModel.swift @@ -0,0 +1,48 @@ +// +// VPVideoPlayViewModel.swift +// Veloria +// +// Created by Veloria on 2025/5/22. +// + +import UIKit + +class VPVideoPlayViewModel: NSObject { + + @objc dynamic var isPlaying: Bool = true + + + var currentPlayer: VPPlayerProtocol? { + didSet { + oldValue?.isCurrent = false + oldValue?.pause() + +// self.currentPlayer?.playerFinishHadle = { [weak self] in +// self?.handlePlayFinish?() +// } + self.currentPlayer?.isCurrent = true + self.currentPlayer?.rate = rateModel.rate.getRate() + } + } + + ///倍速播放 + @objc dynamic lazy var rateModel = VPVideoRateModel(rate: .x1) { + didSet { + self.currentPlayer?.rate = rateModel.rate.getRate() + } + } + + ///设置进度 + func seekToTime(toTime: Int) { + self.currentPlayer?.seekToTime(toTime: toTime) + } + + ///点暂停或播放 + var handlePauseOrPlay: (() -> Void)? + ///播放完成 + var handlePlayFinish: (() -> Void)? + ///播放进度变更 + var handlePlayTimeDidChange: ((_ time: Int) -> Void)? + ///选集 + var handleEpisode: (() -> Void)? +} diff --git a/Veloria/Libs/LocalizedManager/VPLocalizedManager.swift b/Veloria/Libs/LocalizedManager/VPLocalizedManager.swift index 92e1725..f40124e 100644 --- a/Veloria/Libs/LocalizedManager/VPLocalizedManager.swift +++ b/Veloria/Libs/LocalizedManager/VPLocalizedManager.swift @@ -37,16 +37,17 @@ class VPLocalizedManager: NSObject { // 获取当前语言代码(如果用户未手动设置,则返回系统语言) var currentLocalizedKey: String { get { - var key = (UserDefaults.standard.string(forKey: LocalizedUserDefaultsKey) ?? Locale.preferredLanguages.first) ?? "en" - if key.contains("zh-Hans") { - key = "zh" - } else if key.contains("zh-Hant") { - key = "zh_hk" - } else { - let arr = key.components(separatedBy: "-") - key = arr.first ?? "en" - } - return key +// var key = (UserDefaults.standard.string(forKey: LocalizedUserDefaultsKey) ?? Locale.preferredLanguages.first) ?? "en" +// if key.contains("zh-Hans") { +// key = "zh" +// } else if key.contains("zh-Hant") { +// key = "zh_hk" +// } else { +// let arr = key.components(separatedBy: "-") +// key = arr.first ?? "en" +// } +// return key + return "en" } set { UserDefaults.standard.set(newValue, forKey: LocalizedUserDefaultsKey) diff --git a/Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Contents.json b/Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Contents.json new file mode 100644 index 0000000..cf16d81 --- /dev/null +++ b/Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Symbol@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Symbol@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Symbol@2x.png b/Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Symbol@2x.png new file mode 100644 index 0000000..d8456c6 Binary files /dev/null and b/Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Symbol@2x.png differ diff --git a/Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Symbol@3x.png b/Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Symbol@3x.png new file mode 100644 index 0000000..93bb348 Binary files /dev/null and b/Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Symbol@3x.png differ diff --git a/Veloria/Source/Assets.xcassets/icon/arrow_right_icon_01.imageset/Contents.json b/Veloria/Source/Assets.xcassets/icon/arrow_right_icon_01.imageset/Contents.json new file mode 100644 index 0000000..5c4d3b1 --- /dev/null +++ b/Veloria/Source/Assets.xcassets/icon/arrow_right_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/Veloria/Source/Assets.xcassets/icon/arrow_right_icon_01.imageset/Frame@2x.png b/Veloria/Source/Assets.xcassets/icon/arrow_right_icon_01.imageset/Frame@2x.png new file mode 100644 index 0000000..fde74ff Binary files /dev/null and b/Veloria/Source/Assets.xcassets/icon/arrow_right_icon_01.imageset/Frame@2x.png differ diff --git a/Veloria/Source/Assets.xcassets/icon/arrow_right_icon_01.imageset/Frame@3x.png b/Veloria/Source/Assets.xcassets/icon/arrow_right_icon_01.imageset/Frame@3x.png new file mode 100644 index 0000000..425bc1a Binary files /dev/null and b/Veloria/Source/Assets.xcassets/icon/arrow_right_icon_01.imageset/Frame@3x.png differ diff --git a/Veloria/Source/Assets.xcassets/icon/arrow_up_icon_01.imageset/Contents.json b/Veloria/Source/Assets.xcassets/icon/arrow_up_icon_01.imageset/Contents.json new file mode 100644 index 0000000..5c4d3b1 --- /dev/null +++ b/Veloria/Source/Assets.xcassets/icon/arrow_up_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/Veloria/Source/Assets.xcassets/icon/arrow_up_icon_01.imageset/Frame@2x.png b/Veloria/Source/Assets.xcassets/icon/arrow_up_icon_01.imageset/Frame@2x.png new file mode 100644 index 0000000..f4a9ea4 Binary files /dev/null and b/Veloria/Source/Assets.xcassets/icon/arrow_up_icon_01.imageset/Frame@2x.png differ diff --git a/Veloria/Source/Assets.xcassets/icon/arrow_up_icon_01.imageset/Frame@3x.png b/Veloria/Source/Assets.xcassets/icon/arrow_up_icon_01.imageset/Frame@3x.png new file mode 100644 index 0000000..7c5fae3 Binary files /dev/null and b/Veloria/Source/Assets.xcassets/icon/arrow_up_icon_01.imageset/Frame@3x.png differ diff --git a/Veloria/Source/Assets.xcassets/icon/close_icon_01.imageset/Contents.json b/Veloria/Source/Assets.xcassets/icon/close_icon_01.imageset/Contents.json new file mode 100644 index 0000000..5c4d3b1 --- /dev/null +++ b/Veloria/Source/Assets.xcassets/icon/close_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/Veloria/Source/Assets.xcassets/icon/close_icon_01.imageset/Frame@2x.png b/Veloria/Source/Assets.xcassets/icon/close_icon_01.imageset/Frame@2x.png new file mode 100644 index 0000000..650dda2 Binary files /dev/null and b/Veloria/Source/Assets.xcassets/icon/close_icon_01.imageset/Frame@2x.png differ diff --git a/Veloria/Source/Assets.xcassets/icon/close_icon_01.imageset/Frame@3x.png b/Veloria/Source/Assets.xcassets/icon/close_icon_01.imageset/Frame@3x.png new file mode 100644 index 0000000..3bb19de Binary files /dev/null and b/Veloria/Source/Assets.xcassets/icon/close_icon_01.imageset/Frame@3x.png differ diff --git a/Veloria/Source/Assets.xcassets/icon/collect_icon_01.imageset/Contents.json b/Veloria/Source/Assets.xcassets/icon/collect_icon_01.imageset/Contents.json new file mode 100644 index 0000000..6a4d508 --- /dev/null +++ b/Veloria/Source/Assets.xcassets/icon/collect_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Vector@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Vector@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Veloria/Source/Assets.xcassets/icon/collect_icon_01.imageset/Vector@2x.png b/Veloria/Source/Assets.xcassets/icon/collect_icon_01.imageset/Vector@2x.png new file mode 100644 index 0000000..bfdc876 Binary files /dev/null and b/Veloria/Source/Assets.xcassets/icon/collect_icon_01.imageset/Vector@2x.png differ diff --git a/Veloria/Source/Assets.xcassets/icon/collect_icon_01.imageset/Vector@3x.png b/Veloria/Source/Assets.xcassets/icon/collect_icon_01.imageset/Vector@3x.png new file mode 100644 index 0000000..fa3e1cf Binary files /dev/null and b/Veloria/Source/Assets.xcassets/icon/collect_icon_01.imageset/Vector@3x.png differ diff --git a/Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Contents.json b/Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Contents.json new file mode 100644 index 0000000..5c4d3b1 --- /dev/null +++ b/Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.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/Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Frame@2x.png b/Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Frame@2x.png new file mode 100644 index 0000000..80037c2 Binary files /dev/null and b/Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Frame@2x.png differ diff --git a/Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Frame@3x.png b/Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Frame@3x.png new file mode 100644 index 0000000..2f08eaa Binary files /dev/null and b/Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Frame@3x.png differ diff --git a/Veloria/Source/Assets.xcassets/icon/ep_icon_01.imageset/Contents.json b/Veloria/Source/Assets.xcassets/icon/ep_icon_01.imageset/Contents.json new file mode 100644 index 0000000..5c4d3b1 --- /dev/null +++ b/Veloria/Source/Assets.xcassets/icon/ep_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/Veloria/Source/Assets.xcassets/icon/ep_icon_01.imageset/Frame@2x.png b/Veloria/Source/Assets.xcassets/icon/ep_icon_01.imageset/Frame@2x.png new file mode 100644 index 0000000..f8ce659 Binary files /dev/null and b/Veloria/Source/Assets.xcassets/icon/ep_icon_01.imageset/Frame@2x.png differ diff --git a/Veloria/Source/Assets.xcassets/icon/ep_icon_01.imageset/Frame@3x.png b/Veloria/Source/Assets.xcassets/icon/ep_icon_01.imageset/Frame@3x.png new file mode 100644 index 0000000..ba06160 Binary files /dev/null and b/Veloria/Source/Assets.xcassets/icon/ep_icon_01.imageset/Frame@3x.png differ diff --git a/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/Component 20@2x.png b/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/Component 20@2x.png deleted file mode 100644 index b9e80c2..0000000 Binary files a/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/Component 20@2x.png and /dev/null differ diff --git a/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/Component 20@3x.png b/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/Component 20@3x.png deleted file mode 100644 index 111dd77..0000000 Binary files a/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/Component 20@3x.png and /dev/null differ diff --git a/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/Contents.json b/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/Contents.json index 5987f00..a26ca3a 100644 --- a/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/Contents.json +++ b/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/Contents.json @@ -5,12 +5,12 @@ "scale" : "1x" }, { - "filename" : "Component 20@2x.png", + "filename" : "观看历史.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "Component 20@3x.png", + "filename" : "观看历史@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/观看历史.png b/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/观看历史.png new file mode 100644 index 0000000..bfd8b36 Binary files /dev/null and b/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/观看历史.png differ diff --git a/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/观看历史@3x.png b/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/观看历史@3x.png new file mode 100644 index 0000000..e939db7 Binary files /dev/null and b/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/观看历史@3x.png differ diff --git a/Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Contents.json b/Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Contents.json new file mode 100644 index 0000000..a2f740e --- /dev/null +++ b/Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame 76@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame 76@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Frame 76@2x.png b/Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Frame 76@2x.png new file mode 100644 index 0000000..0cdc78d Binary files /dev/null and b/Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Frame 76@2x.png differ diff --git a/Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Frame 76@3x.png b/Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Frame 76@3x.png new file mode 100644 index 0000000..c1d4372 Binary files /dev/null and b/Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Frame 76@3x.png differ diff --git a/Veloria/Source/Assets.xcassets/icon/play_icon_01.imageset/Contents.json b/Veloria/Source/Assets.xcassets/icon/play_icon_01.imageset/Contents.json new file mode 100644 index 0000000..9aa1b8f --- /dev/null +++ b/Veloria/Source/Assets.xcassets/icon/play_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame 75@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame 75@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Veloria/Source/Assets.xcassets/icon/play_icon_01.imageset/Frame 75@2x.png b/Veloria/Source/Assets.xcassets/icon/play_icon_01.imageset/Frame 75@2x.png new file mode 100644 index 0000000..7b80b70 Binary files /dev/null and b/Veloria/Source/Assets.xcassets/icon/play_icon_01.imageset/Frame 75@2x.png differ diff --git a/Veloria/Source/Assets.xcassets/icon/play_icon_01.imageset/Frame 75@3x.png b/Veloria/Source/Assets.xcassets/icon/play_icon_01.imageset/Frame 75@3x.png new file mode 100644 index 0000000..be533bf Binary files /dev/null and b/Veloria/Source/Assets.xcassets/icon/play_icon_01.imageset/Frame 75@3x.png differ diff --git a/Veloria/Source/Veloria-Bridging-Header.h b/Veloria/Source/Veloria-Bridging-Header.h index 07194d1..00efc57 100644 --- a/Veloria/Source/Veloria-Bridging-Header.h +++ b/Veloria/Source/Veloria-Bridging-Header.h @@ -9,3 +9,4 @@ #import #import #import +#import diff --git a/Veloria/Source/en.lproj/Localizable.strings b/Veloria/Source/en.lproj/Localizable.strings index d760936..f0cdf23 100644 --- a/Veloria/Source/en.lproj/Localizable.strings +++ b/Veloria/Source/en.lproj/Localizable.strings @@ -10,6 +10,8 @@ "All" = "All"; "Drama Champions" = "Drama Champions"; "Explore" = "Explore"; +"EP.%@" = "EP.%@"; +"All %@ Episodes" = "All %@ Episodes"; diff --git a/app账号信息.txt b/app账号信息.txt new file mode 100644 index 0000000..2080a9d --- /dev/null +++ b/app账号信息.txt @@ -0,0 +1,13 @@ +湖南秦九开发者账号信息 +认证人 :张文祺 +认证设备 :Mac Mini +手机型号 :MU9D3CH/A  +设备序列号:K73J20237W +手机号码 :18173178983 +企业邮箱 :app@qjwl168.com密码:1q1w1e1r +D-U-N-S :616751820 +公司主体 :Hunan Qinjiu Network Technology Co., Ltd. +公司地址 :9010, Xijing Apartment Sanqi Shopping Plaza, No.383, Jinxing M. Road +认证官网 :https://www.qjwl168.com +账号 :hn.qinjiu.developer@icloud.com +密码 :Discover2024 \ No newline at end of file