播放详情页面,选集功能,播放速率调整

This commit is contained in:
zjx 2025-05-23 16:57:29 +08:00
parent b0e84591d9
commit 4ced649801
73 changed files with 3371 additions and 56 deletions

View File

@ -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 = "<group>"; };
BF0FA7402DDEFBC700C9E5F2 /* UIScrollView+VPRefresh.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScrollView+VPRefresh.swift"; sourceTree = "<group>"; };
BF0FA7442DDF027900C9E5F2 /* VPPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPPlayer.swift; sourceTree = "<group>"; };
BF0FA7492DDF04E200C9E5F2 /* VPPlayerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPPlayerProtocol.swift; sourceTree = "<group>"; };
BF0FA74B2DDF060200C9E5F2 /* VPVideoPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPVideoPlayerViewController.swift; sourceTree = "<group>"; };
BF0FA74D2DDF067E00C9E5F2 /* VPVideoPlayViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPVideoPlayViewModel.swift; sourceTree = "<group>"; };
BF0FA74F2DDF0A9900C9E5F2 /* VPVideoPlayerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPVideoPlayerCell.swift; sourceTree = "<group>"; };
BF0FA7512DDF134700C9E5F2 /* VPVideoPlayerControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPVideoPlayerControlView.swift; sourceTree = "<group>"; };
BF0FA7562DDF159A00C9E5F2 /* VPExploreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPExploreViewController.swift; sourceTree = "<group>"; };
BF0FA7582DDF1C2800C9E5F2 /* VPPlayerProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPPlayerProgressView.swift; sourceTree = "<group>"; };
BF0FA75A2DDF206000C9E5F2 /* VPExplorePlayerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPExplorePlayerCell.swift; sourceTree = "<group>"; };
BF0FA75C2DDF208400C9E5F2 /* VPExplorePlayerControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPExplorePlayerControlView.swift; sourceTree = "<group>"; };
BF0FA75E2DDFFDB000C9E5F2 /* VPDetailPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPDetailPlayerViewController.swift; sourceTree = "<group>"; };
BF0FA7602DDFFE7100C9E5F2 /* VPVideoDetailModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPVideoDetailModel.swift; sourceTree = "<group>"; };
BF0FA7622DE006E700C9E5F2 /* VPDetailPlayerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPDetailPlayerCell.swift; sourceTree = "<group>"; };
BF0FA7642DE00A0E00C9E5F2 /* VPDetailPlayerControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPDetailPlayerControlView.swift; sourceTree = "<group>"; };
BF0FA7662DE0469300C9E5F2 /* VPEpisodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPEpisodeView.swift; sourceTree = "<group>"; };
BF0FA7682DE0502900C9E5F2 /* VPEpisodeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPEpisodeCell.swift; sourceTree = "<group>"; };
BF0FA76A2DE0533400C9E5F2 /* VPEpisodeMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPEpisodeMenuView.swift; sourceTree = "<group>"; };
BF0FA76C2DE053C100C9E5F2 /* VPScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPScrollView.swift; sourceTree = "<group>"; };
BF0FA76E2DE062A700C9E5F2 /* VPRateSelectedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPRateSelectedView.swift; sourceTree = "<group>"; };
BF0FA7702DE062EB00C9E5F2 /* VPVideoRateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPVideoRateModel.swift; sourceTree = "<group>"; };
BF0FA7722DE0671200C9E5F2 /* VPRateSelectedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPRateSelectedCell.swift; sourceTree = "<group>"; };
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 = "<group>"; };
/* End PBXFileReference section */
@ -283,6 +323,7 @@
BF0FA71A2DDC7FF200C9E5F2 /* VPImageView.swift */,
BF0FA7252DDC8F7600C9E5F2 /* VPCollectionView.swift */,
BF0FA7272DDC91F800C9E5F2 /* VPCollectionViewCell.swift */,
BF0FA76C2DE053C100C9E5F2 /* VPScrollView.swift */,
);
path = View;
sourceTree = "<group>";
@ -474,6 +515,7 @@
BF0FA7472DDF03B600C9E5F2 /* Controller */,
BF0FA7462DDF03AD00C9E5F2 /* View */,
BF0FA6FE2DDC660300C9E5F2 /* Model */,
BF0FA7482DDF04B800C9E5F2 /* ViewModel */,
);
path = Player;
sourceTree = "<group>";
@ -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 = "<group>";
@ -539,6 +584,9 @@
BF0FA7422DDF024400C9E5F2 /* Explore */ = {
isa = PBXGroup;
children = (
BF0FA7552DDF158000C9E5F2 /* Controller */,
BF0FA7542DDF157700C9E5F2 /* View */,
BF0FA7532DDF156F00C9E5F2 /* Model */,
);
path = Explore;
sourceTree = "<group>";
@ -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 = "<group>";
@ -561,6 +619,40 @@
BF0FA7472DDF03B600C9E5F2 /* Controller */ = {
isa = PBXGroup;
children = (
BF0FA74B2DDF060200C9E5F2 /* VPVideoPlayerViewController.swift */,
BF0FA75E2DDFFDB000C9E5F2 /* VPDetailPlayerViewController.swift */,
);
path = Controller;
sourceTree = "<group>";
};
BF0FA7482DDF04B800C9E5F2 /* ViewModel */ = {
isa = PBXGroup;
children = (
BF0FA74D2DDF067E00C9E5F2 /* VPVideoPlayViewModel.swift */,
);
path = ViewModel;
sourceTree = "<group>";
};
BF0FA7532DDF156F00C9E5F2 /* Model */ = {
isa = PBXGroup;
children = (
);
path = Model;
sourceTree = "<group>";
};
BF0FA7542DDF157700C9E5F2 /* View */ = {
isa = PBXGroup;
children = (
BF0FA75A2DDF206000C9E5F2 /* VPExplorePlayerCell.swift */,
BF0FA75C2DDF208400C9E5F2 /* VPExplorePlayerControlView.swift */,
);
path = View;
sourceTree = "<group>";
};
BF0FA7552DDF158000C9E5F2 /* Controller */ = {
isa = PBXGroup;
children = (
BF0FA7562DDF159A00C9E5F2 /* VPExploreViewController.swift */,
);
path = Controller;
sourceTree = "<group>";
@ -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 */,

View File

@ -10,7 +10,8 @@ import UIKit
extension AppDelegate {
func appConfig() {
UIButton.vp_Awake()
UIButton.vp_bt_Awake()
UIView.vp_Awake()
}
}

View File

@ -12,7 +12,7 @@ class VPNavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
// self.interactivePopGestureRecognizer?.delegate = self
}
override func pushViewController(_ viewController: UIViewController, animated: Bool) {

View File

@ -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"))

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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<VPVideoDetailModel>) in
completer?(response.data)
}
}
///
static func requestCategoryVideoList(id: String, page: Int, completer: ((_ listModel: VPListModel<VPShortModel>?) -> Void)?) {
@ -26,4 +46,58 @@ class VPVideoAPI: NSObject {
}
}
///
static func requestRecommandsVideo(page: Int, completer: ((_ listModel: VPListModel<VPShortModel>?) -> Void)?) {
var param = VPNetworkParameters(path: "/getRecommands")
param.method = .get
param.parameters = [
"page_size" : 20,
"current_page" : page
]
VPNetwork.request(parameters: param) { (response: VPNetworkResponse<VPListModel<VPShortModel>>) 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<String>) 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")
}

View File

@ -128,6 +128,8 @@ extension VPTabBar {
}
func reload() {
removeAll()
guard let items = self.items else { return }

View File

@ -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

View File

@ -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")
}
}

View File

@ -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
}
}
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

@ -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 {

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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()
}
}

View File

@ -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)
}

View File

@ -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?
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

@ -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()
}
}
}

View File

@ -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()
}
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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()
}
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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)?
}

View File

@ -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)

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 B

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -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"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 B

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

View File

@ -9,3 +9,4 @@
#import <MJRefresh/MJRefresh.h>
#import <ZFPlayer.h>
#import <KTVHTTPCache/KTVHTTPCache.h>
#import <HWPanModal/HWPanModal.h>

View File

@ -10,6 +10,8 @@
"All" = "All";
"Drama Champions" = "Drama Champions";
"Explore" = "Explore";
"EP.%@" = "EP.%@";
"All %@ Episodes" = "All %@ Episodes";

13
app账号信息.txt Normal file
View File

@ -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