播放详情页面,选集功能,播放速率调整
@ -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 */,
|
||||
|
@ -10,7 +10,8 @@ import UIKit
|
||||
extension AppDelegate {
|
||||
|
||||
func appConfig() {
|
||||
UIButton.vp_Awake()
|
||||
UIButton.vp_bt_Awake()
|
||||
UIView.vp_Awake()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ class VPNavigationController: UINavigationController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
||||
// self.interactivePopGestureRecognizer?.delegate = self
|
||||
}
|
||||
|
||||
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
|
||||
|
@ -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"))
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
|
||||
}
|
||||
|
@ -128,6 +128,8 @@ extension VPTabBar {
|
||||
}
|
||||
|
||||
func reload() {
|
||||
removeAll()
|
||||
|
||||
guard let items = self.items else { return }
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
21
Veloria/Base/View/VPScrollView.swift
Normal 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")
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
16
Veloria/Class/Explore/View/VPExplorePlayerCell.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
131
Veloria/Class/Explore/View/VPExplorePlayerControlView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
|
||||
}
|
46
Veloria/Class/Player/Model/VPPlayerProtocol.swift
Normal 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)
|
||||
|
||||
|
||||
}
|
23
Veloria/Class/Player/Model/VPVideoDetailModel.swift
Normal 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?
|
||||
}
|
69
Veloria/Class/Player/Model/VPVideoRateModel.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
16
Veloria/Class/Player/View/VPDetailPlayerCell.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
221
Veloria/Class/Player/View/VPDetailPlayerControlView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
78
Veloria/Class/Player/View/VPEpisodeCell.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
152
Veloria/Class/Player/View/VPEpisodeMenuView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
333
Veloria/Class/Player/View/VPEpisodeView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
212
Veloria/Class/Player/View/VPPlayerProgressView.swift
Normal 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)
|
||||
}
|
||||
}
|
63
Veloria/Class/Player/View/VPRateSelectedCell.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
102
Veloria/Class/Player/View/VPRateSelectedView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
214
Veloria/Class/Player/View/VPVideoPlayerCell.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
299
Veloria/Class/Player/View/VPVideoPlayerControlView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
48
Veloria/Class/Player/ViewModel/VPVideoPlayViewModel.swift
Normal 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)?
|
||||
}
|
@ -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)
|
||||
|
22
Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Symbol@2x.png
vendored
Normal file
After Width: | Height: | Size: 292 B |
BIN
Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Symbol@3x.png
vendored
Normal file
After Width: | Height: | Size: 410 B |
22
Veloria/Source/Assets.xcassets/icon/arrow_right_icon_01.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Frame@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Frame@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
Veloria/Source/Assets.xcassets/icon/arrow_right_icon_01.imageset/Frame@2x.png
vendored
Normal file
After Width: | Height: | Size: 198 B |
BIN
Veloria/Source/Assets.xcassets/icon/arrow_right_icon_01.imageset/Frame@3x.png
vendored
Normal file
After Width: | Height: | Size: 247 B |
22
Veloria/Source/Assets.xcassets/icon/arrow_up_icon_01.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Frame@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Frame@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
Veloria/Source/Assets.xcassets/icon/arrow_up_icon_01.imageset/Frame@2x.png
vendored
Normal file
After Width: | Height: | Size: 250 B |
BIN
Veloria/Source/Assets.xcassets/icon/arrow_up_icon_01.imageset/Frame@3x.png
vendored
Normal file
After Width: | Height: | Size: 275 B |
22
Veloria/Source/Assets.xcassets/icon/close_icon_01.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Frame@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Frame@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
Veloria/Source/Assets.xcassets/icon/close_icon_01.imageset/Frame@2x.png
vendored
Normal file
After Width: | Height: | Size: 500 B |
BIN
Veloria/Source/Assets.xcassets/icon/close_icon_01.imageset/Frame@3x.png
vendored
Normal file
After Width: | Height: | Size: 710 B |
22
Veloria/Source/Assets.xcassets/icon/collect_icon_01.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
Veloria/Source/Assets.xcassets/icon/collect_icon_01.imageset/Vector@2x.png
vendored
Normal file
After Width: | Height: | Size: 820 B |
BIN
Veloria/Source/Assets.xcassets/icon/collect_icon_01.imageset/Vector@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.2 KiB |
22
Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Frame@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Frame@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Frame@2x.png
vendored
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Frame@3x.png
vendored
Normal file
After Width: | Height: | Size: 5.9 KiB |
22
Veloria/Source/Assets.xcassets/icon/ep_icon_01.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Frame@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Frame@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
Veloria/Source/Assets.xcassets/icon/ep_icon_01.imageset/Frame@2x.png
vendored
Normal file
After Width: | Height: | Size: 400 B |
BIN
Veloria/Source/Assets.xcassets/icon/ep_icon_01.imageset/Frame@3x.png
vendored
Normal file
After Width: | Height: | Size: 526 B |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 2.2 KiB |
@ -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"
|
||||
}
|
||||
|
BIN
Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/观看历史.png
vendored
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/观看历史@3x.png
vendored
Normal file
After Width: | Height: | Size: 2.2 KiB |
22
Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Frame 76@2x.png
vendored
Normal file
After Width: | Height: | Size: 305 B |
BIN
Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Frame 76@3x.png
vendored
Normal file
After Width: | Height: | Size: 438 B |
22
Veloria/Source/Assets.xcassets/icon/play_icon_01.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
Veloria/Source/Assets.xcassets/icon/play_icon_01.imageset/Frame 75@2x.png
vendored
Normal file
After Width: | Height: | Size: 544 B |
BIN
Veloria/Source/Assets.xcassets/icon/play_icon_01.imageset/Frame 75@3x.png
vendored
Normal file
After Width: | Height: | Size: 772 B |
@ -9,3 +9,4 @@
|
||||
#import <MJRefresh/MJRefresh.h>
|
||||
#import <ZFPlayer.h>
|
||||
#import <KTVHTTPCache/KTVHTTPCache.h>
|
||||
#import <HWPanModal/HWPanModal.h>
|
||||
|
@ -10,6 +10,8 @@
|
||||
"All" = "All";
|
||||
"Drama Champions" = "Drama Champions";
|
||||
"Explore" = "Explore";
|
||||
"EP.%@" = "EP.%@";
|
||||
"All %@ Episodes" = "All %@ Episodes";
|
||||
|
||||
|
||||
|
||||
|
13
app账号信息.txt
Normal 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
|