From 4ced649801cfb59a6d3a89afe7735ae629125dba Mon Sep 17 00:00:00 2001 From: zjx Date: Fri, 23 May 2025 16:57:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=92=AD=E6=94=BE=E8=AF=A6=E6=83=85=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=EF=BC=8C=E9=80=89=E9=9B=86=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E9=80=9F=E7=8E=87=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Veloria.xcodeproj/project.pbxproj | 112 +++++ Veloria/AppDelegate/AppDelegate+Config.swift | 3 +- .../Controller/VPNavigationController.swift | 2 +- .../Base/Controller/VPTabBarController.swift | 2 +- .../Base/Controller/VPViewController.swift | 61 ++- Veloria/Base/Extension/NSNumber+VPAdd.swift | 16 + Veloria/Base/Extension/UIButton+VPAdd.swift | 66 +-- Veloria/Base/Extension/UIColor+VPAdd.swift | 20 + Veloria/Base/Extension/UIView+VPAdd.swift | 106 +++++ Veloria/Base/Networking/API/VPVideoAPI.swift | 74 ++++ Veloria/Base/View/TabBar/VPTabBar.swift | 2 + .../TabBar/VPTabBarItemSelectedView.swift | 4 +- Veloria/Base/View/VPScrollView.swift | 21 + .../Controller/VPExploreViewController.swift | 90 +++++ .../Explore/View/VPExplorePlayerCell.swift | 16 + .../View/VPExplorePlayerControlView.swift | 131 ++++++ .../Controller/VPHomeListViewController.swift | 9 +- .../Controller/VPHomePageViewController.swift | 8 +- .../Home/View/VPHomeBannerContentCell.swift | 8 + .../Home/View/VPHomeRankingContentCell.swift | 6 + .../View/VPHomeRecommandContentCell.swift | 7 + .../VPDetailPlayerViewController.swift | 166 ++++++++ .../VPVideoPlayerViewController.swift | 382 ++++++++++++++++++ .../Class/Player/Model/VPPlayerProtocol.swift | 46 +++ .../Player/Model/VPVideoDetailModel.swift | 23 ++ .../Class/Player/Model/VPVideoRateModel.swift | 69 ++++ .../Player/View/VPDetailPlayerCell.swift | 16 + .../View/VPDetailPlayerControlView.swift | 221 ++++++++++ Veloria/Class/Player/View/VPEpisodeCell.swift | 78 ++++ .../Class/Player/View/VPEpisodeMenuView.swift | 152 +++++++ Veloria/Class/Player/View/VPEpisodeView.swift | 333 +++++++++++++++ .../Player/View/VPPlayerProgressView.swift | 212 ++++++++++ .../Player/View/VPRateSelectedCell.swift | 63 +++ .../Player/View/VPRateSelectedView.swift | 102 +++++ .../Class/Player/View/VPVideoPlayerCell.swift | 214 ++++++++++ .../View/VPVideoPlayerControlView.swift | 299 ++++++++++++++ .../ViewModel/VPVideoPlayViewModel.swift | 48 +++ .../LocalizedManager/VPLocalizedManager.swift | 21 +- .../arrow_left_icon_01.imageset/Contents.json | 22 + .../arrow_left_icon_01.imageset/Symbol@2x.png | Bin 0 -> 292 bytes .../arrow_left_icon_01.imageset/Symbol@3x.png | Bin 0 -> 410 bytes .../Contents.json | 22 + .../arrow_right_icon_01.imageset/Frame@2x.png | Bin 0 -> 198 bytes .../arrow_right_icon_01.imageset/Frame@3x.png | Bin 0 -> 247 bytes .../arrow_up_icon_01.imageset/Contents.json | 22 + .../arrow_up_icon_01.imageset/Frame@2x.png | Bin 0 -> 250 bytes .../arrow_up_icon_01.imageset/Frame@3x.png | Bin 0 -> 275 bytes .../icon/close_icon_01.imageset/Contents.json | 22 + .../icon/close_icon_01.imageset/Frame@2x.png | Bin 0 -> 500 bytes .../icon/close_icon_01.imageset/Frame@3x.png | Bin 0 -> 710 bytes .../collect_icon_01.imageset/Contents.json | 22 + .../collect_icon_01.imageset/Vector@2x.png | Bin 0 -> 820 bytes .../collect_icon_01.imageset/Vector@3x.png | Bin 0 -> 1261 bytes .../Contents.json | 22 + .../Frame@2x.png | Bin 0 -> 3157 bytes .../Frame@3x.png | Bin 0 -> 6041 bytes .../icon/ep_icon_01.imageset/Contents.json | 22 + .../icon/ep_icon_01.imageset/Frame@2x.png | Bin 0 -> 400 bytes .../icon/ep_icon_01.imageset/Frame@3x.png | Bin 0 -> 526 bytes .../Component 20@2x.png | Bin 1543 -> 0 bytes .../Component 20@3x.png | Bin 2243 -> 0 bytes .../history_icon_01.imageset/Contents.json | 4 +- .../history_icon_01.imageset/观看历史.png | Bin 0 -> 1550 bytes .../history_icon_01.imageset/观看历史@3x.png | Bin 0 -> 2298 bytes .../icon/pause_icon_01.imageset/Contents.json | 22 + .../pause_icon_01.imageset/Frame 76@2x.png | Bin 0 -> 305 bytes .../pause_icon_01.imageset/Frame 76@3x.png | Bin 0 -> 438 bytes .../icon/play_icon_01.imageset/Contents.json | 22 + .../play_icon_01.imageset/Frame 75@2x.png | Bin 0 -> 544 bytes .../play_icon_01.imageset/Frame 75@3x.png | Bin 0 -> 772 bytes Veloria/Source/Veloria-Bridging-Header.h | 1 + Veloria/Source/en.lproj/Localizable.strings | 2 + app账号信息.txt | 13 + 73 files changed, 3371 insertions(+), 56 deletions(-) create mode 100644 Veloria/Base/View/VPScrollView.swift create mode 100644 Veloria/Class/Explore/Controller/VPExploreViewController.swift create mode 100644 Veloria/Class/Explore/View/VPExplorePlayerCell.swift create mode 100644 Veloria/Class/Explore/View/VPExplorePlayerControlView.swift create mode 100644 Veloria/Class/Player/Controller/VPDetailPlayerViewController.swift create mode 100644 Veloria/Class/Player/Controller/VPVideoPlayerViewController.swift create mode 100644 Veloria/Class/Player/Model/VPPlayerProtocol.swift create mode 100644 Veloria/Class/Player/Model/VPVideoDetailModel.swift create mode 100644 Veloria/Class/Player/Model/VPVideoRateModel.swift create mode 100644 Veloria/Class/Player/View/VPDetailPlayerCell.swift create mode 100644 Veloria/Class/Player/View/VPDetailPlayerControlView.swift create mode 100644 Veloria/Class/Player/View/VPEpisodeCell.swift create mode 100644 Veloria/Class/Player/View/VPEpisodeMenuView.swift create mode 100644 Veloria/Class/Player/View/VPEpisodeView.swift create mode 100644 Veloria/Class/Player/View/VPPlayerProgressView.swift create mode 100644 Veloria/Class/Player/View/VPRateSelectedCell.swift create mode 100644 Veloria/Class/Player/View/VPRateSelectedView.swift create mode 100644 Veloria/Class/Player/View/VPVideoPlayerCell.swift create mode 100644 Veloria/Class/Player/View/VPVideoPlayerControlView.swift create mode 100644 Veloria/Class/Player/ViewModel/VPVideoPlayViewModel.swift create mode 100644 Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Contents.json create mode 100644 Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Symbol@2x.png create mode 100644 Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Symbol@3x.png create mode 100644 Veloria/Source/Assets.xcassets/icon/arrow_right_icon_01.imageset/Contents.json create mode 100644 Veloria/Source/Assets.xcassets/icon/arrow_right_icon_01.imageset/Frame@2x.png create mode 100644 Veloria/Source/Assets.xcassets/icon/arrow_right_icon_01.imageset/Frame@3x.png create mode 100644 Veloria/Source/Assets.xcassets/icon/arrow_up_icon_01.imageset/Contents.json create mode 100644 Veloria/Source/Assets.xcassets/icon/arrow_up_icon_01.imageset/Frame@2x.png create mode 100644 Veloria/Source/Assets.xcassets/icon/arrow_up_icon_01.imageset/Frame@3x.png create mode 100644 Veloria/Source/Assets.xcassets/icon/close_icon_01.imageset/Contents.json create mode 100644 Veloria/Source/Assets.xcassets/icon/close_icon_01.imageset/Frame@2x.png create mode 100644 Veloria/Source/Assets.xcassets/icon/close_icon_01.imageset/Frame@3x.png create mode 100644 Veloria/Source/Assets.xcassets/icon/collect_icon_01.imageset/Contents.json create mode 100644 Veloria/Source/Assets.xcassets/icon/collect_icon_01.imageset/Vector@2x.png create mode 100644 Veloria/Source/Assets.xcassets/icon/collect_icon_01.imageset/Vector@3x.png create mode 100644 Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Contents.json create mode 100644 Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Frame@2x.png create mode 100644 Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Frame@3x.png create mode 100644 Veloria/Source/Assets.xcassets/icon/ep_icon_01.imageset/Contents.json create mode 100644 Veloria/Source/Assets.xcassets/icon/ep_icon_01.imageset/Frame@2x.png create mode 100644 Veloria/Source/Assets.xcassets/icon/ep_icon_01.imageset/Frame@3x.png delete mode 100644 Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/Component 20@2x.png delete mode 100644 Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/Component 20@3x.png create mode 100644 Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/观看历史.png create mode 100644 Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/观看历史@3x.png create mode 100644 Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Contents.json create mode 100644 Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Frame 76@2x.png create mode 100644 Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Frame 76@3x.png create mode 100644 Veloria/Source/Assets.xcassets/icon/play_icon_01.imageset/Contents.json create mode 100644 Veloria/Source/Assets.xcassets/icon/play_icon_01.imageset/Frame 75@2x.png create mode 100644 Veloria/Source/Assets.xcassets/icon/play_icon_01.imageset/Frame 75@3x.png create mode 100644 app账号信息.txt diff --git a/Veloria.xcodeproj/project.pbxproj b/Veloria.xcodeproj/project.pbxproj index 47a1303..99b3cef 100644 --- a/Veloria.xcodeproj/project.pbxproj +++ b/Veloria.xcodeproj/project.pbxproj @@ -77,6 +77,26 @@ BF0FA73F2DDEF26E00C9E5F2 /* VPHomeSearchButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA73E2DDEF26E00C9E5F2 /* VPHomeSearchButton.swift */; }; BF0FA7412DDEFBC700C9E5F2 /* UIScrollView+VPRefresh.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7402DDEFBC700C9E5F2 /* UIScrollView+VPRefresh.swift */; }; BF0FA7452DDF027900C9E5F2 /* VPPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7442DDF027900C9E5F2 /* VPPlayer.swift */; }; + BF0FA74A2DDF04E200C9E5F2 /* VPPlayerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7492DDF04E200C9E5F2 /* VPPlayerProtocol.swift */; }; + BF0FA74C2DDF060200C9E5F2 /* VPVideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA74B2DDF060200C9E5F2 /* VPVideoPlayerViewController.swift */; }; + BF0FA74E2DDF067E00C9E5F2 /* VPVideoPlayViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA74D2DDF067E00C9E5F2 /* VPVideoPlayViewModel.swift */; }; + BF0FA7502DDF0A9900C9E5F2 /* VPVideoPlayerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA74F2DDF0A9900C9E5F2 /* VPVideoPlayerCell.swift */; }; + BF0FA7522DDF134700C9E5F2 /* VPVideoPlayerControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7512DDF134700C9E5F2 /* VPVideoPlayerControlView.swift */; }; + BF0FA7572DDF159B00C9E5F2 /* VPExploreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7562DDF159A00C9E5F2 /* VPExploreViewController.swift */; }; + BF0FA7592DDF1C2800C9E5F2 /* VPPlayerProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7582DDF1C2800C9E5F2 /* VPPlayerProgressView.swift */; }; + BF0FA75B2DDF206000C9E5F2 /* VPExplorePlayerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA75A2DDF206000C9E5F2 /* VPExplorePlayerCell.swift */; }; + BF0FA75D2DDF208400C9E5F2 /* VPExplorePlayerControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA75C2DDF208400C9E5F2 /* VPExplorePlayerControlView.swift */; }; + BF0FA75F2DDFFDB000C9E5F2 /* VPDetailPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA75E2DDFFDB000C9E5F2 /* VPDetailPlayerViewController.swift */; }; + BF0FA7612DDFFE7100C9E5F2 /* VPVideoDetailModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7602DDFFE7100C9E5F2 /* VPVideoDetailModel.swift */; }; + BF0FA7632DE006E700C9E5F2 /* VPDetailPlayerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7622DE006E700C9E5F2 /* VPDetailPlayerCell.swift */; }; + BF0FA7652DE00A0E00C9E5F2 /* VPDetailPlayerControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7642DE00A0E00C9E5F2 /* VPDetailPlayerControlView.swift */; }; + BF0FA7672DE0469300C9E5F2 /* VPEpisodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7662DE0469300C9E5F2 /* VPEpisodeView.swift */; }; + BF0FA7692DE0502900C9E5F2 /* VPEpisodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7682DE0502900C9E5F2 /* VPEpisodeCell.swift */; }; + BF0FA76B2DE0533400C9E5F2 /* VPEpisodeMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA76A2DE0533400C9E5F2 /* VPEpisodeMenuView.swift */; }; + BF0FA76D2DE053C100C9E5F2 /* VPScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA76C2DE053C100C9E5F2 /* VPScrollView.swift */; }; + BF0FA76F2DE062A700C9E5F2 /* VPRateSelectedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA76E2DE062A700C9E5F2 /* VPRateSelectedView.swift */; }; + BF0FA7712DE062EB00C9E5F2 /* VPVideoRateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7702DE062EB00C9E5F2 /* VPVideoRateModel.swift */; }; + BF0FA7732DE0671200C9E5F2 /* VPRateSelectedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0FA7722DE0671200C9E5F2 /* VPRateSelectedCell.swift */; }; F939C04AD4003BA127F15C28 /* Pods_Veloria.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 34F57E87E765BF8D72A43DCA /* Pods_Veloria.framework */; }; /* End PBXBuildFile section */ @@ -160,6 +180,26 @@ BF0FA73E2DDEF26E00C9E5F2 /* VPHomeSearchButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPHomeSearchButton.swift; sourceTree = ""; }; BF0FA7402DDEFBC700C9E5F2 /* UIScrollView+VPRefresh.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScrollView+VPRefresh.swift"; sourceTree = ""; }; BF0FA7442DDF027900C9E5F2 /* VPPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPPlayer.swift; sourceTree = ""; }; + BF0FA7492DDF04E200C9E5F2 /* VPPlayerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPPlayerProtocol.swift; sourceTree = ""; }; + BF0FA74B2DDF060200C9E5F2 /* VPVideoPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPVideoPlayerViewController.swift; sourceTree = ""; }; + BF0FA74D2DDF067E00C9E5F2 /* VPVideoPlayViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPVideoPlayViewModel.swift; sourceTree = ""; }; + BF0FA74F2DDF0A9900C9E5F2 /* VPVideoPlayerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPVideoPlayerCell.swift; sourceTree = ""; }; + BF0FA7512DDF134700C9E5F2 /* VPVideoPlayerControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPVideoPlayerControlView.swift; sourceTree = ""; }; + BF0FA7562DDF159A00C9E5F2 /* VPExploreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPExploreViewController.swift; sourceTree = ""; }; + BF0FA7582DDF1C2800C9E5F2 /* VPPlayerProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPPlayerProgressView.swift; sourceTree = ""; }; + BF0FA75A2DDF206000C9E5F2 /* VPExplorePlayerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPExplorePlayerCell.swift; sourceTree = ""; }; + BF0FA75C2DDF208400C9E5F2 /* VPExplorePlayerControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPExplorePlayerControlView.swift; sourceTree = ""; }; + BF0FA75E2DDFFDB000C9E5F2 /* VPDetailPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPDetailPlayerViewController.swift; sourceTree = ""; }; + BF0FA7602DDFFE7100C9E5F2 /* VPVideoDetailModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPVideoDetailModel.swift; sourceTree = ""; }; + BF0FA7622DE006E700C9E5F2 /* VPDetailPlayerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPDetailPlayerCell.swift; sourceTree = ""; }; + BF0FA7642DE00A0E00C9E5F2 /* VPDetailPlayerControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPDetailPlayerControlView.swift; sourceTree = ""; }; + BF0FA7662DE0469300C9E5F2 /* VPEpisodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPEpisodeView.swift; sourceTree = ""; }; + BF0FA7682DE0502900C9E5F2 /* VPEpisodeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPEpisodeCell.swift; sourceTree = ""; }; + BF0FA76A2DE0533400C9E5F2 /* VPEpisodeMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPEpisodeMenuView.swift; sourceTree = ""; }; + BF0FA76C2DE053C100C9E5F2 /* VPScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPScrollView.swift; sourceTree = ""; }; + BF0FA76E2DE062A700C9E5F2 /* VPRateSelectedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPRateSelectedView.swift; sourceTree = ""; }; + BF0FA7702DE062EB00C9E5F2 /* VPVideoRateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPVideoRateModel.swift; sourceTree = ""; }; + BF0FA7722DE0671200C9E5F2 /* VPRateSelectedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPRateSelectedCell.swift; sourceTree = ""; }; E0BDA3570E00C90877E45AA0 /* Pods-VideoPlayer.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-VideoPlayer.debug.xcconfig"; path = "Target Support Files/Pods-VideoPlayer/Pods-VideoPlayer.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -283,6 +323,7 @@ BF0FA71A2DDC7FF200C9E5F2 /* VPImageView.swift */, BF0FA7252DDC8F7600C9E5F2 /* VPCollectionView.swift */, BF0FA7272DDC91F800C9E5F2 /* VPCollectionViewCell.swift */, + BF0FA76C2DE053C100C9E5F2 /* VPScrollView.swift */, ); path = View; sourceTree = ""; @@ -474,6 +515,7 @@ BF0FA7472DDF03B600C9E5F2 /* Controller */, BF0FA7462DDF03AD00C9E5F2 /* View */, BF0FA6FE2DDC660300C9E5F2 /* Model */, + BF0FA7482DDF04B800C9E5F2 /* ViewModel */, ); path = Player; sourceTree = ""; @@ -481,8 +523,11 @@ BF0FA6FE2DDC660300C9E5F2 /* Model */ = { isa = PBXGroup; children = ( + BF0FA7492DDF04E200C9E5F2 /* VPPlayerProtocol.swift */, BF0FA6FF2DDC665300C9E5F2 /* VPShortModel.swift */, BF0FA7012DDC667C00C9E5F2 /* VPVideoInfoModel.swift */, + BF0FA7602DDFFE7100C9E5F2 /* VPVideoDetailModel.swift */, + BF0FA7702DE062EB00C9E5F2 /* VPVideoRateModel.swift */, ); path = Model; sourceTree = ""; @@ -539,6 +584,9 @@ BF0FA7422DDF024400C9E5F2 /* Explore */ = { isa = PBXGroup; children = ( + BF0FA7552DDF158000C9E5F2 /* Controller */, + BF0FA7542DDF157700C9E5F2 /* View */, + BF0FA7532DDF156F00C9E5F2 /* Model */, ); path = Explore; sourceTree = ""; @@ -554,6 +602,16 @@ BF0FA7462DDF03AD00C9E5F2 /* View */ = { isa = PBXGroup; children = ( + BF0FA74F2DDF0A9900C9E5F2 /* VPVideoPlayerCell.swift */, + BF0FA7512DDF134700C9E5F2 /* VPVideoPlayerControlView.swift */, + BF0FA7582DDF1C2800C9E5F2 /* VPPlayerProgressView.swift */, + BF0FA7622DE006E700C9E5F2 /* VPDetailPlayerCell.swift */, + BF0FA7642DE00A0E00C9E5F2 /* VPDetailPlayerControlView.swift */, + BF0FA7662DE0469300C9E5F2 /* VPEpisodeView.swift */, + BF0FA7682DE0502900C9E5F2 /* VPEpisodeCell.swift */, + BF0FA76A2DE0533400C9E5F2 /* VPEpisodeMenuView.swift */, + BF0FA76E2DE062A700C9E5F2 /* VPRateSelectedView.swift */, + BF0FA7722DE0671200C9E5F2 /* VPRateSelectedCell.swift */, ); path = View; sourceTree = ""; @@ -561,6 +619,40 @@ BF0FA7472DDF03B600C9E5F2 /* Controller */ = { isa = PBXGroup; children = ( + BF0FA74B2DDF060200C9E5F2 /* VPVideoPlayerViewController.swift */, + BF0FA75E2DDFFDB000C9E5F2 /* VPDetailPlayerViewController.swift */, + ); + path = Controller; + sourceTree = ""; + }; + BF0FA7482DDF04B800C9E5F2 /* ViewModel */ = { + isa = PBXGroup; + children = ( + BF0FA74D2DDF067E00C9E5F2 /* VPVideoPlayViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + BF0FA7532DDF156F00C9E5F2 /* Model */ = { + isa = PBXGroup; + children = ( + ); + path = Model; + sourceTree = ""; + }; + BF0FA7542DDF157700C9E5F2 /* View */ = { + isa = PBXGroup; + children = ( + BF0FA75A2DDF206000C9E5F2 /* VPExplorePlayerCell.swift */, + BF0FA75C2DDF208400C9E5F2 /* VPExplorePlayerControlView.swift */, + ); + path = View; + sourceTree = ""; + }; + BF0FA7552DDF158000C9E5F2 /* Controller */ = { + isa = PBXGroup; + children = ( + BF0FA7562DDF159A00C9E5F2 /* VPExploreViewController.swift */, ); path = Controller; sourceTree = ""; @@ -686,6 +778,7 @@ files = ( 1B056E742DDB2DD7007EE38D /* UIView+VPAdd.swift in Sources */, BF0FA6F12DDC600200C9E5F2 /* VPNetwork.swift in Sources */, + BF0FA7502DDF0A9900C9E5F2 /* VPVideoPlayerCell.swift in Sources */, 1B056E572DDACC6B007EE38D /* VPHomePageViewController.swift in Sources */, BF0FA72A2DDC922F00C9E5F2 /* VPHomeRecommandCell.swift in Sources */, BF0FA7322DDEBD6400C9E5F2 /* AppDelegate+Config.swift in Sources */, @@ -695,6 +788,7 @@ BF0FA7262DDC8F7600C9E5F2 /* VPCollectionView.swift in Sources */, BF0FA71D2DDC807200C9E5F2 /* UIImageView+VPAdd.swift in Sources */, BF0FA6DC2DDC5CD700C9E5F2 /* VPTokenModel.swift in Sources */, + BF0FA7572DDF159B00C9E5F2 /* VPExploreViewController.swift in Sources */, 1B056E772DDB3641007EE38D /* VPTabBarItemNormalVew.swift in Sources */, 1B056E4B2DDAC6BA007EE38D /* VPDefine.swift in Sources */, 1B056E702DDB019B007EE38D /* VPTabBarItemContainer.swift in Sources */, @@ -703,22 +797,33 @@ 1B056E6C2DDADAA1007EE38D /* VPTabBar.swift in Sources */, BF0FA72E2DDD7DD400C9E5F2 /* VPHomeRankingCell.swift in Sources */, BF0FA7302DDEBB1600C9E5F2 /* UIButton+VPAdd.swift in Sources */, + BF0FA74C2DDF060200C9E5F2 /* VPVideoPlayerViewController.swift in Sources */, + BF0FA76D2DE053C100C9E5F2 /* VPScrollView.swift in Sources */, 1B056E5D2DDACD8E007EE38D /* UIFont+VPAdd.swift in Sources */, BF0FA7282DDC91F800C9E5F2 /* VPCollectionViewCell.swift in Sources */, + BF0FA7672DE0469300C9E5F2 /* VPEpisodeView.swift in Sources */, BF0FA6F92DDC64E700C9E5F2 /* VPHomeAPI.swift in Sources */, BF0FA6F42DDC604500C9E5F2 /* VPHUD.swift in Sources */, BF0FA7222DDC859D00C9E5F2 /* NSNumber+VPAdd.swift in Sources */, BF0FA73B2DDED1C700C9E5F2 /* VPHomeListCell.swift in Sources */, + BF0FA7592DDF1C2800C9E5F2 /* VPPlayerProgressView.swift in Sources */, BF0FA71B2DDC7FF200C9E5F2 /* VPImageView.swift in Sources */, + BF0FA7522DDF134700C9E5F2 /* VPVideoPlayerControlView.swift in Sources */, 1B056E2C2DDAC0FD007EE38D /* SceneDelegate.swift in Sources */, + BF0FA75D2DDF208400C9E5F2 /* VPExplorePlayerControlView.swift in Sources */, 1B056E462DDAC370007EE38D /* UIScreen+VPAdd.swift in Sources */, + BF0FA7692DE0502900C9E5F2 /* VPEpisodeCell.swift in Sources */, BF0FA73D2DDED2D000C9E5F2 /* VPVideoAPI.swift in Sources */, BF0FA6EE2DDC5F8700C9E5F2 /* JXUUID.m in Sources */, BF0FA7052DDC67AC00C9E5F2 /* VPHomeViewModel.swift in Sources */, BF0FA6EF2DDC5F8700C9E5F2 /* PDKeyChain.m in Sources */, + BF0FA75F2DDFFDB000C9E5F2 /* VPDetailPlayerViewController.swift in Sources */, + BF0FA7732DE0671200C9E5F2 /* VPRateSelectedCell.swift in Sources */, 1B056E4D2DDAC7C1007EE38D /* VPTabBarController.swift in Sources */, BF0FA6DA2DDC5CB600C9E5F2 /* VPLoginManager.swift in Sources */, + BF0FA74A2DDF04E200C9E5F2 /* VPPlayerProtocol.swift in Sources */, BF0FA72C2DDD7B7300C9E5F2 /* VPHomeRankingContentCell.swift in Sources */, + BF0FA7652DE00A0E00C9E5F2 /* VPDetailPlayerControlView.swift in Sources */, BF0FA7342DDEC74500C9E5F2 /* VPCategoryModel.swift in Sources */, 1B056E4F2DDAC7FC007EE38D /* VPNavigationController.swift in Sources */, 1B056E3F2DDAC2DB007EE38D /* VPCryptorService.swift in Sources */, @@ -728,25 +833,32 @@ BF0FA70E2DDC6ACC00C9E5F2 /* VPHomeItemContentCell.swift in Sources */, BF0FA6D72DDC5BE100C9E5F2 /* VPURLPath.swift in Sources */, 1B056E5B2DDACD80007EE38D /* UIColor+VPAdd.swift in Sources */, + BF0FA76B2DE0533400C9E5F2 /* VPEpisodeMenuView.swift in Sources */, 1B056E6A2DDAD0BF007EE38D /* VPLocalizedManager.swift in Sources */, BF0FA7242DDC888F00C9E5F2 /* VPHomeRecommandContentCell.swift in Sources */, BF0FA7192DDC7F4900C9E5F2 /* VPHomeBannerCell.swift in Sources */, + BF0FA7712DE062EB00C9E5F2 /* VPVideoRateModel.swift in Sources */, BF0FA7022DDC667C00C9E5F2 /* VPVideoInfoModel.swift in Sources */, + BF0FA76F2DE062A700C9E5F2 /* VPRateSelectedView.swift in Sources */, 1B056E792DDB365A007EE38D /* VPTabBarItemSelectedView.swift in Sources */, 1B056E722DDB022F007EE38D /* VPTabBarItem.swift in Sources */, 1B056E412DDAC30A007EE38D /* VPModel.swift in Sources */, BF0FA70A2DDC69C800C9E5F2 /* VPTableViewCell.swift in Sources */, 1B056E442DDAC355007EE38D /* UIDevice+VPAdd.swift in Sources */, + BF0FA7632DE006E700C9E5F2 /* VPDetailPlayerCell.swift in Sources */, BF0FA73F2DDEF26E00C9E5F2 /* VPHomeSearchButton.swift in Sources */, 1B056E512DDACBE5007EE38D /* VPViewController.swift in Sources */, BF0FA7122DDC6D2C00C9E5F2 /* VPHomeModuleItem.swift in Sources */, BF0FA7162DDC78FF00C9E5F2 /* ZKCycleScrollViewFlowLayout.swift in Sources */, BF0FA7172DDC78FF00C9E5F2 /* ZKCycleScrollView.swift in Sources */, + BF0FA7612DDFFE7100C9E5F2 /* VPVideoDetailModel.swift in Sources */, BF0FA6D52DDC5B5D00C9E5F2 /* VPApi.swift in Sources */, BF0FA6FC2DDC657500C9E5F2 /* VPHomeDataModel.swift in Sources */, BF0FA7392DDECF8900C9E5F2 /* VPHomeListViewController.swift in Sources */, BF0FA6F62DDC616300C9E5F2 /* VPToast.swift in Sources */, 1B056E492DDAC3DF007EE38D /* VPAppTool.swift in Sources */, + BF0FA74E2DDF067E00C9E5F2 /* VPVideoPlayViewModel.swift in Sources */, + BF0FA75B2DDF206000C9E5F2 /* VPExplorePlayerCell.swift in Sources */, BF0FA7412DDEFBC700C9E5F2 /* UIScrollView+VPRefresh.swift in Sources */, BF0FA70C2DDC6A3800C9E5F2 /* VPHomeBannerContentCell.swift in Sources */, BF0FA7102DDC6CA200C9E5F2 /* VPListModel.swift in Sources */, diff --git a/Veloria/AppDelegate/AppDelegate+Config.swift b/Veloria/AppDelegate/AppDelegate+Config.swift index 99a2ee6..38bc9e2 100644 --- a/Veloria/AppDelegate/AppDelegate+Config.swift +++ b/Veloria/AppDelegate/AppDelegate+Config.swift @@ -10,7 +10,8 @@ import UIKit extension AppDelegate { func appConfig() { - UIButton.vp_Awake() + UIButton.vp_bt_Awake() + UIView.vp_Awake() } } diff --git a/Veloria/Base/Controller/VPNavigationController.swift b/Veloria/Base/Controller/VPNavigationController.swift index 598a8cc..cc991d8 100644 --- a/Veloria/Base/Controller/VPNavigationController.swift +++ b/Veloria/Base/Controller/VPNavigationController.swift @@ -12,7 +12,7 @@ class VPNavigationController: UINavigationController { override func viewDidLoad() { super.viewDidLoad() - +// self.interactivePopGestureRecognizer?.delegate = self } override func pushViewController(_ viewController: UIViewController, animated: Bool) { diff --git a/Veloria/Base/Controller/VPTabBarController.swift b/Veloria/Base/Controller/VPTabBarController.swift index 61c1655..a25fec9 100644 --- a/Veloria/Base/Controller/VPTabBarController.swift +++ b/Veloria/Base/Controller/VPTabBarController.swift @@ -87,7 +87,7 @@ extension VPTabBarController { let nav1 = createNavigationController(viewController: VPHomePageViewController(), title: "Home".localized, image: UIImage(named: "tabbar_icon_01"), selectedImage: UIImage(named: "tabbar_icon_01_selected")) - let nav2 = createNavigationController(viewController: VPHomePageViewController(), title: "Explore".localized, image: UIImage(named: "tabbar_icon_02"), selectedImage: UIImage(named: "tabbar_icon_02_selected")) + let nav2 = createNavigationController(viewController: VPExploreViewController(), title: "Explore".localized, image: UIImage(named: "tabbar_icon_02"), selectedImage: UIImage(named: "tabbar_icon_02_selected")) let nav3 = createNavigationController(viewController: VPHomePageViewController(), title: "Home".localized, image: UIImage(named: "tabbar_icon_03"), selectedImage: UIImage(named: "tabbar_icon_03_selected")) let nav4 = createNavigationController(viewController: VPHomePageViewController(), title: "Home".localized, image: UIImage(named: "tabbar_icon_03"), selectedImage: UIImage(named: "tabbar_icon_04_selected")) diff --git a/Veloria/Base/Controller/VPViewController.swift b/Veloria/Base/Controller/VPViewController.swift index e91333e..842f2df 100644 --- a/Veloria/Base/Controller/VPViewController.swift +++ b/Veloria/Base/Controller/VPViewController.swift @@ -14,11 +14,22 @@ class VPViewController: UIViewController { return imageView }() + private(set) var isViewDidLoad = false + private(set) var isDidAppear = false + private(set) var hasViewDidAppear = false + override func viewDidLoad() { super.viewDidLoad() - + self.isViewDidLoad = true view.backgroundColor = .backgroundColor() + if let navi = navigationController { + if navi.visibleViewController == self { + if navi.viewControllers.count > 1 { + configNavigationBack() + } + } + } view.addSubview(bgImageView) @@ -27,9 +38,57 @@ class VPViewController: UIViewController { } } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + isDidAppear = true + hasViewDidAppear = true + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + isDidAppear = false + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + isDidAppear = false + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + func handleHeaderRefresh(_ completer: (() -> Void)?) {} func handleFooterRefresh(_ completer: (() -> Void)?) {} } + +extension VPViewController { + func configNavigationBack(_ imageName: String = "arrow_left_icon_01") { + let image = UIImage(named: imageName) + + let leftBarButtonItem = UIBarButtonItem(image: image, style: .plain ,target: self,action: #selector(handleBack)) + navigationItem.leftBarButtonItem = leftBarButtonItem + } + + @objc func handleBack() { + self.vp_toLastViewController(animated: true) + } + + func vp_toLastViewController(animated: Bool) { + if self.navigationController != nil + { + if self.navigationController?.viewControllers.count == 1 + { + self.dismiss(animated: animated, completion: nil) + } else { + self.navigationController?.popViewController(animated: animated) + } + } + else if self.presentingViewController != nil { + self.dismiss(animated: animated, completion: nil) + } + } +} diff --git a/Veloria/Base/Extension/NSNumber+VPAdd.swift b/Veloria/Base/Extension/NSNumber+VPAdd.swift index 3ab4489..a0e716f 100644 --- a/Veloria/Base/Extension/NSNumber+VPAdd.swift +++ b/Veloria/Base/Extension/NSNumber+VPAdd.swift @@ -23,3 +23,19 @@ extension NSNumber { return formatter.string(from: self) ?? "0" } } + + +extension Int { + func formatTimeGroup() -> (String, String, String) { + let seconds = self + + var s: String = "00" + var m: String = "00" + var h: String = "00" + s = String(format: "%02d", Int(Int(seconds) % 60)) + m = String(format: "%02d", Int(seconds / 60) % 60) + h = String(format: "%02d", Int(seconds / 3600)) + + return (h, m, s) + } +} diff --git a/Veloria/Base/Extension/UIButton+VPAdd.swift b/Veloria/Base/Extension/UIButton+VPAdd.swift index 52f7b91..149b176 100644 --- a/Veloria/Base/Extension/UIButton+VPAdd.swift +++ b/Veloria/Base/Extension/UIButton+VPAdd.swift @@ -9,23 +9,23 @@ import UIKit extension UIButton { fileprivate struct AssociatedKeys { - static var vp_gradientLayer: Int? - static var vp_gradientBorder: Int? - static var vp_gradientBorderShapeLayer: Int? + static var bt_gradientLayer: Int? + static var bt_gradientBorder: Int? + static var bt_gradientBorderShapeLayer: Int? } - @objc public static func vp_Awake() { - vp_swizzled_instanceMethod("vp", oldClass: self, oldSelector: "layoutSubviews", newClass: self) + @objc public static func vp_bt_Awake() { + vp_swizzled_instanceMethod("vp_bt", oldClass: self, oldSelector: "layoutSubviews", newClass: self) } - @objc func vp_layoutSubviews() { - vp_layoutSubviews() - vp_gradientLayer?.frame = self.bounds + @objc func vp_bt_layoutSubviews() { + vp_bt_layoutSubviews() + bt_gradientLayer?.frame = self.bounds - vp_gradientBorder?.frame = self.bounds - let path = UIBezierPath(roundedRect: bounds.insetBy(dx: 2, dy: 2), cornerRadius: layer.cornerRadius) - vp_gradientBorderShapeLayer?.path = path.cgPath + bt_gradientBorder?.frame = self.bounds + let path = UIBezierPath(roundedRect: bounds.insetBy(dx: 0.5, dy: 0.5), cornerRadius: layer.cornerRadius) + bt_gradientBorderShapeLayer?.path = path.cgPath } @@ -33,61 +33,61 @@ extension UIButton { } extension UIButton { - private var vp_gradientLayer: CAGradientLayer? { + private var bt_gradientLayer: CAGradientLayer? { get { - return objc_getAssociatedObject(self, &AssociatedKeys.vp_gradientLayer) as? CAGradientLayer + return objc_getAssociatedObject(self, &AssociatedKeys.bt_gradientLayer) as? CAGradientLayer } set { - objc_setAssociatedObject(self, &AssociatedKeys.vp_gradientLayer, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + objc_setAssociatedObject(self, &AssociatedKeys.bt_gradientLayer, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } - private var vp_gradientBorder: CAGradientLayer? { + private var bt_gradientBorder: CAGradientLayer? { get { - return objc_getAssociatedObject(self, &AssociatedKeys.vp_gradientBorder) as? CAGradientLayer + return objc_getAssociatedObject(self, &AssociatedKeys.bt_gradientBorder) as? CAGradientLayer } set { - objc_setAssociatedObject(self, &AssociatedKeys.vp_gradientBorder, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + objc_setAssociatedObject(self, &AssociatedKeys.bt_gradientBorder, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } - private var vp_gradientBorderShapeLayer: CAShapeLayer? { + private var bt_gradientBorderShapeLayer: CAShapeLayer? { get { - return objc_getAssociatedObject(self, &AssociatedKeys.vp_gradientBorderShapeLayer) as? CAShapeLayer + return objc_getAssociatedObject(self, &AssociatedKeys.bt_gradientBorderShapeLayer) as? CAShapeLayer } set { - objc_setAssociatedObject(self, &AssociatedKeys.vp_gradientBorderShapeLayer, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + objc_setAssociatedObject(self, &AssociatedKeys.bt_gradientBorderShapeLayer, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } ///设置渐变色 - func vp_setGradient() { - if vp_gradientLayer == nil { + func bt_setGradient() { + if bt_gradientLayer == nil { let gLayer = CAGradientLayer() gLayer.colors = [UIColor.color05CEA0().cgColor, UIColor.color7C174F().cgColor] gLayer.locations = [0, 0.8] gLayer.startPoint = .init(x: 0, y: 0.3) gLayer.endPoint = .init(x: 1, y: 0.8) - vp_gradientLayer = gLayer + bt_gradientLayer = gLayer } - if let layer = vp_gradientLayer { + if let layer = bt_gradientLayer { self.layer.addSublayer(layer) self.titleLabel?.layer.zPosition = 1 } } ///删除渐变层 - func vp_removeGradient() { - vp_gradientLayer?.removeFromSuperlayer() + func bt_removeGradient() { + bt_gradientLayer?.removeFromSuperlayer() } ///设置渐变边框 - func vp_setGradientBorder() { - if vp_gradientBorder == nil { + func bt_setGradientBorder() { + if bt_gradientBorder == nil { let gLayer = CAGradientLayer() gLayer.colors = [UIColor.color05CEA0().cgColor, UIColor.color7C174F().cgColor] gLayer.locations = [0, 0.8] @@ -100,18 +100,18 @@ extension UIButton { shapeLayer.strokeColor = UIColor.black.cgColor gLayer.mask = shapeLayer - vp_gradientBorderShapeLayer = shapeLayer - vp_gradientBorder = gLayer + bt_gradientBorderShapeLayer = shapeLayer + bt_gradientBorder = gLayer } - if let layer = vp_gradientBorder { + if let layer = bt_gradientBorder { self.layer.addSublayer(layer) self.titleLabel?.layer.zPosition = 1 } } ///删除渐变边框 - func vp_removeGradientBorder() { - vp_gradientBorder?.removeFromSuperlayer() + func bt_removeGradientBorder() { + bt_gradientBorder?.removeFromSuperlayer() } diff --git a/Veloria/Base/Extension/UIColor+VPAdd.swift b/Veloria/Base/Extension/UIColor+VPAdd.swift index c1bf02f..22f90d3 100644 --- a/Veloria/Base/Extension/UIColor+VPAdd.swift +++ b/Veloria/Base/Extension/UIColor+VPAdd.swift @@ -49,4 +49,24 @@ extension UIColor { static func colorFFFFFB(alpha: CGFloat = 1) -> UIColor { return UIColor(rgb: 0xFFFFFB, alpha: alpha) } + + static func color949494(alpha: CGFloat = 1) -> UIColor { + return UIColor(rgb: 0x949494, alpha: alpha) + } + + static func colorBEBEBE(alpha: CGFloat = 1) -> UIColor { + return UIColor(rgb: 0xBEBEBE, alpha: alpha) + } + + static func colorFFBD36(alpha: CGFloat = 1) -> UIColor { + return UIColor(rgb: 0xFFBD36, alpha: alpha) + } + + static func color545458(alpha: CGFloat = 1) -> UIColor { + return UIColor(rgb: 0x545458, alpha: alpha) + } + + static func color1C2D2F(alpha: CGFloat = 1) -> UIColor { + return UIColor(rgb: 0x1C2D2F, alpha: alpha) + } } diff --git a/Veloria/Base/Extension/UIView+VPAdd.swift b/Veloria/Base/Extension/UIView+VPAdd.swift index 6e66be5..1702402 100644 --- a/Veloria/Base/Extension/UIView+VPAdd.swift +++ b/Veloria/Base/Extension/UIView+VPAdd.swift @@ -10,4 +10,110 @@ import SnapKit extension UIView { + fileprivate struct AssociatedKeys { + static var vp_effect: Int? + static var vp_circulars: Int? + static var vp_gradientBorder: Int? + static var vp_gradientBorderShapeLayer: Int? + } + + + @objc public static func vp_Awake() { + vp_swizzled_instanceMethod("vp", oldClass: self, oldSelector: "layoutSubviews", newClass: self) + } + + @objc func vp_layoutSubviews() { + vp_layoutSubviews() + + if let effectView = effectView, effectView.frame != self.bounds { + effectView.frame = self.bounds + } + + if let border = vp_gradientBorder { + border.frame = self.bounds + let path = UIBezierPath(roundedRect: bounds.insetBy(dx: 0.5, dy: 0.5), cornerRadius: layer.cornerRadius) + vp_gradientBorderShapeLayer?.path = path.cgPath + } + } +} + + +//MARK: -------------- 模糊效果 -------------- +extension UIView { + private var effectView: UIVisualEffectView? { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.vp_effect) as? UIVisualEffectView + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.vp_effect, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + ///添加模糊效果 + func addEffectView(style: UIBlurEffect.Style = .dark) { + if self.effectView == nil { + let blur = UIBlurEffect(style: style) + let effectView = UIVisualEffectView(effect: blur) + effectView.isUserInteractionEnabled = false + self.addSubview(effectView) + self.sendSubviewToBack(effectView) + + self.effectView = effectView + } + } + ///删除模糊效果 + func removeEffectView() { + self.effectView?.removeFromSuperview() + self.effectView = nil + } +} + +//MARK: -------------- 渐变边框 -------------- +extension UIView { + private var vp_gradientBorder: CAGradientLayer? { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.vp_gradientBorder) as? CAGradientLayer + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.vp_gradientBorder, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + private var vp_gradientBorderShapeLayer: CAShapeLayer? { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.vp_gradientBorderShapeLayer) as? CAShapeLayer + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.vp_gradientBorderShapeLayer, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + ///设置渐变边框 + func vp_setGradientBorder() { + if vp_gradientBorder == nil { + let gLayer = CAGradientLayer() + gLayer.colors = [UIColor.color05CEA0().cgColor, UIColor.color7C174F().cgColor] + gLayer.locations = [0, 0.8] + gLayer.startPoint = .init(x: 0, y: 0.3) + gLayer.endPoint = .init(x: 1, y: 0.8) + + let shapeLayer = CAShapeLayer() + shapeLayer.lineWidth = 1 + shapeLayer.fillColor = UIColor.clear.cgColor + shapeLayer.strokeColor = UIColor.black.cgColor + gLayer.mask = shapeLayer + + vp_gradientBorderShapeLayer = shapeLayer + vp_gradientBorder = gLayer + } + + if let layer = vp_gradientBorder { + self.layer.addSublayer(layer) + } + } + + ///删除渐变边框 + func vp_removeGradientBorder() { + vp_gradientBorder?.removeFromSuperlayer() + } } diff --git a/Veloria/Base/Networking/API/VPVideoAPI.swift b/Veloria/Base/Networking/API/VPVideoAPI.swift index cb3d0e6..bdbc230 100644 --- a/Veloria/Base/Networking/API/VPVideoAPI.swift +++ b/Veloria/Base/Networking/API/VPVideoAPI.swift @@ -9,6 +9,26 @@ import UIKit class VPVideoAPI: NSObject { + ///获取视频详情 + static func requestVideoDetail(shortPlayId: String, activityId: String? = nil, completer: ((_ model: VPVideoDetailModel?) -> Void)?) { + var parameters: [String : Any] = [ + "short_play_id" : shortPlayId, + "video_id" : "0" + ] + + if let activityId = activityId { + parameters["activity_id"] = activityId + } + + var param = VPNetworkParameters(path: "/getVideoDetails") + param.method = .get + param.parameters = parameters + + VPNetwork.request(parameters: param) { (response: VPNetworkResponse) in + completer?(response.data) + } + } + ///获取分类短剧 static func requestCategoryVideoList(id: String, page: Int, completer: ((_ listModel: VPListModel?) -> Void)?) { @@ -26,4 +46,58 @@ class VPVideoAPI: NSObject { } } + ///推荐短剧 + static func requestRecommandsVideo(page: Int, completer: ((_ listModel: VPListModel?) -> Void)?) { + + var param = VPNetworkParameters(path: "/getRecommands") + param.method = .get + param.parameters = [ + "page_size" : 20, + "current_page" : page + ] + + VPNetwork.request(parameters: param) { (response: VPNetworkResponse>) in + completer?(response.data) + } + } + + ///收藏短剧 + static func requestCollectShort(isCollect: Bool, shortPlayId: String, videoId: String?, success: (() -> Void)?, failure: (() -> Void)? = nil) { + let path: String + if isCollect { + path = "/collect" + } else { + path = "/cancelCollect" + } + + var parameters: [String : Any] = [ + "short_play_id" : shortPlayId, + ] + + if let videoId = videoId { + parameters["video_id"] = videoId + } + + var param = VPNetworkParameters(path: path) + param.isLoding = true + param.parameters = parameters + + VPNetwork.request(parameters: param) { (response: VPNetworkResponse) in + if response.code == VPNetworkCodeSucceed { + success?() + NotificationCenter.default.post(name: VPVideoAPI.updateShortCollectStateNotification, object: nil, userInfo: [ + "state" : isCollect, + "id" : shortPlayId, + ]) + } else { + failure?() + } + } + } +} + +extension VPVideoAPI { + ///更新短剧关注状态 [ "state" : isCollect, "id" : shortPlayId,] + @objc static let updateShortCollectStateNotification = NSNotification.Name(rawValue: "VPVideoAPI.updateShortCollectStateNotification") + } diff --git a/Veloria/Base/View/TabBar/VPTabBar.swift b/Veloria/Base/View/TabBar/VPTabBar.swift index f61e085..5404c43 100644 --- a/Veloria/Base/View/TabBar/VPTabBar.swift +++ b/Veloria/Base/View/TabBar/VPTabBar.swift @@ -128,6 +128,8 @@ extension VPTabBar { } func reload() { + removeAll() + guard let items = self.items else { return } diff --git a/Veloria/Base/View/TabBar/VPTabBarItemSelectedView.swift b/Veloria/Base/View/TabBar/VPTabBarItemSelectedView.swift index 455add5..d4e8028 100644 --- a/Veloria/Base/View/TabBar/VPTabBarItemSelectedView.swift +++ b/Veloria/Base/View/TabBar/VPTabBarItemSelectedView.swift @@ -50,8 +50,8 @@ class VPTabBarItemSelectedView: VPGradientView { super.init(frame: frame) colors = [UIColor.color7C174F().cgColor, UIColor.color05CEA0().cgColor] locations = [0, 1] - startPoint = .init(x: 0, y: 0.5) - endPoint = .init(x: 1, y: 0.5) + startPoint = .init(x: 0, y: 0.3) + endPoint = .init(x: 1, y: 0.8) layer.cornerRadius = VPTabBar.itemMinWidth / 2 layer.masksToBounds = true diff --git a/Veloria/Base/View/VPScrollView.swift b/Veloria/Base/View/VPScrollView.swift new file mode 100644 index 0000000..45276e6 --- /dev/null +++ b/Veloria/Base/View/VPScrollView.swift @@ -0,0 +1,21 @@ +// +// VPScrollView.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit + +class VPScrollView: UIScrollView { + + override init(frame: CGRect) { + super.init(frame: frame) + self.contentInsetAdjustmentBehavior = .never + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/Veloria/Class/Explore/Controller/VPExploreViewController.swift b/Veloria/Class/Explore/Controller/VPExploreViewController.swift new file mode 100644 index 0000000..2997f8c --- /dev/null +++ b/Veloria/Class/Explore/Controller/VPExploreViewController.swift @@ -0,0 +1,90 @@ +// +// VPExploreViewController.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/22. +// + +import UIKit + +class VPExploreViewController: VPVideoPlayerViewController { + + override var PlayerCellClass: VPVideoPlayerCell.Type { + return VPExplorePlayerCell.self + } + + var pagination: VPListPaginationModel? + + + override func viewDidLoad() { + super.viewDidLoad() + self.delegate = self + self.dataSource = self + + + requestDataArr(page: 1) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(true, animated: true) + } + + +} + +//MARK: -------------- VPPlayerListViewControllerDelegate -------------- +extension VPExploreViewController: VPPlayerListViewControllerDelegate { + func vp_playerViewControllerLoadMoreData(playerViewController: VPVideoPlayerViewController) { + guard let pagination = self.pagination else { return } + guard let page = self.pagination?.current_page else { return } + let pageSize = pagination.page_size ?? 0 + if pagination.page_total ?? 0 <= pageSize * page { + return + } + self.requestDataArr(page: page + 1) + } +} + +//MARK: -------------- VPPlayerListViewControllerDataSource -------------- +extension VPExploreViewController: VPPlayerListViewControllerDataSource { + + func vp_playerListViewController(_ viewController: VPVideoPlayerViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, oldCell: UICollectionViewCell) -> UICollectionViewCell { + + if let cell = oldCell as? VPVideoPlayerCell { + if let model = dataArr[indexPath.row] as? VPShortModel { + cell.shortModel = model + cell.videoInfo = model.video_info + } + cell.isLoop = false + } + return oldCell + } + + func vp_playerListViewController(_ viewController: VPVideoPlayerViewController, _ collectionView: UICollectionView, numberOfItemsInSection section: Int, oldNumber: Int) -> Int { + return oldNumber + } + +} + +extension VPExploreViewController { + + private func requestDataArr(page: Int) { + + VPVideoAPI.requestRecommandsVideo(page: page) { [weak self] listModel in + guard let self = self else { return } + if let listModel = listModel, let list = listModel.list { + if page == 1 { + self.setDataArr(dataArr: list) { [weak self] in + self?.play() + } + } else { + self.addDataArr(dataArr: list) + } + self.pagination = listModel.pagination + } + } + + } + +} diff --git a/Veloria/Class/Explore/View/VPExplorePlayerCell.swift b/Veloria/Class/Explore/View/VPExplorePlayerCell.swift new file mode 100644 index 0000000..2187783 --- /dev/null +++ b/Veloria/Class/Explore/View/VPExplorePlayerCell.swift @@ -0,0 +1,16 @@ +// +// VPExplorePlayerCell.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/22. +// + +import UIKit + +class VPExplorePlayerCell: VPVideoPlayerCell { + + override var ControlViewClass: VPVideoPlayerControlView.Type { + return VPExplorePlayerControlView.self + } + +} diff --git a/Veloria/Class/Explore/View/VPExplorePlayerControlView.swift b/Veloria/Class/Explore/View/VPExplorePlayerControlView.swift new file mode 100644 index 0000000..ed69dd0 --- /dev/null +++ b/Veloria/Class/Explore/View/VPExplorePlayerControlView.swift @@ -0,0 +1,131 @@ +// +// VPExplorePlayerControlView.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/22. +// + +import UIKit + +class VPExplorePlayerControlView: VPVideoPlayerControlView { + + override var videoInfo: VPVideoInfoModel? { + didSet { + epLabel.text = String(format: "EP.%@".localized, videoInfo?.episode ?? "0") + + } + } + + override var shortModel: VPShortModel? { + didSet { + allView.setTitle(String(format: "All %@ Episodes".localized, "\(shortModel?.episode_total ?? 0)"), for: .normal) + videoNameLabel.text = shortModel?.name + } + } + + + private lazy var moreButton: UIControl = { + let button = UIButton(type: .custom) + button.backgroundColor = .color000000(alpha: 0.2) + button.addTarget(self, action: #selector(handleMoreButton), for: .touchUpInside) + return button + }() + + private lazy var epIconImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "ep_icon_01")) + return imageView + }() + + private lazy var epLabel: UILabel = { + let label = UILabel() + label.font = .fontRegular(ofSize: 12) + label.textColor = .colorFFFFFF() + return label + }() + + private lazy var allView: UIButton = { + let view = JXButton(type: .custom) + view.isUserInteractionEnabled = false + view.jx_font = .fontRegular(ofSize: 12) + view.titleDirection = .left + view.space = 6 + view.setTitleColor(.colorFFFFFB(), for: .normal) + view.setImage(UIImage(named: "arrow_right_icon_01"), for: .normal) + return view + }() + + private lazy var videoNameLabel: UILabel = { + let label = UILabel() + label.font = .fontMedium(ofSize: 14) + label.textColor = .colorFFFFFF() + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + vp_setupUI() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +extension VPExplorePlayerControlView { + + @objc private func handleMoreButton() { + let vc = VPDetailPlayerViewController() + vc.shortPlayId = self.shortModel?.short_play_id + self.viewController?.navigationController?.pushViewController(vc, animated: true) + } + +} + +extension VPExplorePlayerControlView { + + private func vp_setupUI() { + self.progressView.insets = .init(top: 30, left: 15, bottom: 6, right: 15) + + addSubview(videoNameLabel) + self.progressView.addSubview(moreButton) + moreButton.addSubview(epIconImageView) + moreButton.addSubview(epLabel) + moreButton.addSubview(allView) + + self.progressView.snp.remakeConstraints { make in + make.left.equalToSuperview() + make.centerX.equalToSuperview() + make.bottom.equalToSuperview() + } + + videoNameLabel.snp.makeConstraints { make in + make.left.equalToSuperview().offset(15) + make.bottom.equalTo(self.progressView.snp.top).offset(-15) + make.right.lessThanOrEqualToSuperview().offset(-120) + } + + moreButton.snp.makeConstraints { make in + make.left.right.top.equalToSuperview() + make.height.equalTo(30) + } + + epIconImageView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalToSuperview().offset(15) + } + + epLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalTo(epIconImageView.snp.right).offset(5) + } + + allView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.right.equalToSuperview().offset(-15) + } + } + + +} diff --git a/Veloria/Class/Home/Controller/VPHomeListViewController.swift b/Veloria/Class/Home/Controller/VPHomeListViewController.swift index e7e9fce..c046b98 100644 --- a/Veloria/Class/Home/Controller/VPHomeListViewController.swift +++ b/Veloria/Class/Home/Controller/VPHomeListViewController.swift @@ -32,7 +32,7 @@ class VPHomeListViewController: VPViewController, WMZPageProtocol { collectionView.delegate = self collectionView.dataSource = self collectionView.contentInset = .init(top: 0, left: 0, bottom: UIScreen.customTabBarHeight + 10, right: 0) - collectionView.vp_addRefreshBackFooter(insetBottom: collectionView.contentInset.bottom) { [weak self] in + collectionView.vp_addRefreshBackFooter(insetBottom: 0) { [weak self] in self?.handleFooterRefresh(nil) } collectionView.register(VPHomeListCell.self, forCellWithReuseIdentifier: "cell") @@ -93,6 +93,13 @@ extension VPHomeListViewController: UICollectionViewDelegate, UICollectionViewDa return self.dataArr.count } + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let model = dataArr[indexPath.row] + let vc = VPDetailPlayerViewController() + vc.shortPlayId = model.short_play_id + self.navigationController?.pushViewController(vc, animated: true) + } + } extension VPHomeListViewController { diff --git a/Veloria/Class/Home/Controller/VPHomePageViewController.swift b/Veloria/Class/Home/Controller/VPHomePageViewController.swift index a4dc9f7..1973aaa 100644 --- a/Veloria/Class/Home/Controller/VPHomePageViewController.swift +++ b/Veloria/Class/Home/Controller/VPHomePageViewController.swift @@ -149,11 +149,11 @@ class VPHomePageViewController: VPViewController { private func setButtonState(button: UIButton) { button.layer.masksToBounds = true if button.isSelected { - button.vp_setGradient() - button.vp_removeGradientBorder() + button.bt_setGradient() + button.bt_removeGradientBorder() } else { - button.vp_removeGradient() - button.vp_setGradientBorder() + button.bt_removeGradient() + button.bt_setGradientBorder() } } diff --git a/Veloria/Class/Home/View/VPHomeBannerContentCell.swift b/Veloria/Class/Home/View/VPHomeBannerContentCell.swift index 3e6203c..e1119b7 100644 --- a/Veloria/Class/Home/View/VPHomeBannerContentCell.swift +++ b/Veloria/Class/Home/View/VPHomeBannerContentCell.swift @@ -71,4 +71,12 @@ extension VPHomeBannerContentCell: ZKCycleScrollViewDataSource, ZKCycleScrollVie cell.model = self.item?.list?[index] return cell } + + func cycleScrollView(_ cycleScrollView: ZKCycleScrollView, didSelectItemAt index: Int) { + guard let model = self.item?.list?[index] else { return } + + let vc = VPDetailPlayerViewController() + vc.shortPlayId = model.short_play_id + self.viewController?.navigationController?.pushViewController(vc, animated: true) + } } diff --git a/Veloria/Class/Home/View/VPHomeRankingContentCell.swift b/Veloria/Class/Home/View/VPHomeRankingContentCell.swift index c8e60a3..b64ec12 100644 --- a/Veloria/Class/Home/View/VPHomeRankingContentCell.swift +++ b/Veloria/Class/Home/View/VPHomeRankingContentCell.swift @@ -105,4 +105,10 @@ extension VPHomeRankingContentCell: UICollectionViewDelegate, UICollectionViewDa return item?.list?.count ?? 0 } + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let model = item?.list?[indexPath.row] else { return } + let vc = VPDetailPlayerViewController() + vc.shortPlayId = model.short_play_id + self.viewController?.navigationController?.pushViewController(vc, animated: true) + } } diff --git a/Veloria/Class/Home/View/VPHomeRecommandContentCell.swift b/Veloria/Class/Home/View/VPHomeRecommandContentCell.swift index 13107c2..225dc63 100644 --- a/Veloria/Class/Home/View/VPHomeRecommandContentCell.swift +++ b/Veloria/Class/Home/View/VPHomeRecommandContentCell.swift @@ -97,4 +97,11 @@ extension VPHomeRecommandContentCell: UICollectionViewDelegate, UICollectionView func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return self.item?.list?.count ?? 0 } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let model = item?.list?[indexPath.row] else { return } + let vc = VPDetailPlayerViewController() + vc.shortPlayId = model.short_play_id + self.viewController?.navigationController?.pushViewController(vc, animated: true) + } } diff --git a/Veloria/Class/Player/Controller/VPDetailPlayerViewController.swift b/Veloria/Class/Player/Controller/VPDetailPlayerViewController.swift new file mode 100644 index 0000000..7bb46ba --- /dev/null +++ b/Veloria/Class/Player/Controller/VPDetailPlayerViewController.swift @@ -0,0 +1,166 @@ +// +// VPDetailPlayerViewController.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit + +class VPDetailPlayerViewController: VPVideoPlayerViewController { + + override var PlayerCellClass: VPVideoPlayerCell.Type { + return VPDetailPlayerCell.self + } + + override var contentSize: CGSize { + return .init(width: UIScreen.width, height: UIScreen.height) + } + + var shortPlayId: String? + var activityId: String? + + private var detailModel: VPVideoDetailModel? + + //MARK: UI属性 + private lazy var backButton: UIButton = { + let button = UIButton(type: .custom) + button.setImage(UIImage(named: "arrow_left_icon_01"), for: .normal) + button.addTarget(self, action: #selector(handleBack), for: .touchUpInside) + return button + }() + + private lazy var videoNameLabel: UILabel = { + let label = UILabel() + label.font = .fontMedium(ofSize: 16) + label.textColor = .colorFFFFFF() + return label + }() + + ///选集 + private weak var episodeView: VPEpisodeView? + + override func viewDidLoad() { + super.viewDidLoad() + self.delegate = self + self.dataSource = self + requestDetailData() + + vp_setupUI() + vp_addAction() + } + + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(true, animated: true) + } + +} + +extension VPDetailPlayerViewController { + + private func vp_setupUI() { + view.addSubview(backButton) + view.addSubview(videoNameLabel) + + backButton.snp.makeConstraints { make in + make.left.equalToSuperview() + make.top.equalToSuperview().offset(UIScreen.statusBarHeight) + make.width.equalTo(48) + make.height.equalTo(44) + } + + videoNameLabel.snp.makeConstraints { make in + make.centerY.equalTo(backButton) + make.left.equalTo(backButton.snp.right) + make.right.lessThanOrEqualToSuperview().offset(-150) + } + + } + + private func vp_addAction() { + self.viewModel.handleEpisode = { [weak self] in + self?.onEpisode() + } + + } +} + +extension VPDetailPlayerViewController { + + private func onEpisode() { + let view = VPEpisodeView() + view.dataArr = detailModel?.episodeList ?? [] + view.shortModel = detailModel?.shortPlayInfo + view.currentIndex = self.currentIndexPath.row + view.didSelectedIndex = { [weak self] (index) in + self?.scrollToItem(indexPath: IndexPath(row: index, section: 0), animated: false) + } + view.present(in: nil) + self.episodeView = view + } + +} + +//MARK: -------------- VPPlayerListViewControllerDataSource -------------- +extension VPDetailPlayerViewController: VPPlayerListViewControllerDataSource { + func vp_playerListViewController(_ viewController: VPVideoPlayerViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, oldCell: UICollectionViewCell) -> UICollectionViewCell { + guard let cell = oldCell as? VPDetailPlayerCell else { return oldCell } + cell.shortModel = detailModel?.shortPlayInfo + cell.videoInfo = detailModel?.episodeList?[indexPath.row] + cell.isLoop = false + + return cell + } + + func vp_playerListViewController(_ viewController: VPVideoPlayerViewController, _ collectionView: UICollectionView, numberOfItemsInSection section: Int, oldNumber: Int) -> Int { + return self.detailModel?.episodeList?.count ?? 0 + } + + +} + +//MARK: -------------- VPPlayerListViewControllerDelegate -------------- +extension VPDetailPlayerViewController: VPPlayerListViewControllerDelegate { + func vp_playerListViewController(_ viewController: VPVideoPlayerViewController, didChangeIndexPathForVisible indexPath: IndexPath) { + self.episodeView?.currentIndex = indexPath.row + } +} + + + +extension VPDetailPlayerViewController { + + private func requestDetailData() { + guard let shortPlayId = shortPlayId else { return } + + VPVideoAPI.requestVideoDetail(shortPlayId: shortPlayId, activityId: activityId) { [weak self] model in + guard let self = self else { return } + guard let model = model else { return } + self.detailModel = model + self.videoNameLabel.text = model.shortPlayInfo?.name + + self.reloadData { [weak self] in + guard let self = self else { return } + + if let videoInfo = self.detailModel?.video_info { + var row: Int? + self.detailModel?.episodeList?.enumerated().forEach({ + if $1.id == videoInfo.id { + row = $0 + } + }) + if let row = row { + self.scrollToItem(indexPath: .init(row: row, section: 0), animated: false) + } else { + self.scrollToItem(indexPath: .init(row: 0, section: 0), animated: false) + } + } else { + self.scrollToItem(indexPath: .init(row: 0, section: 0), animated: false) + } + } + } + } + +} diff --git a/Veloria/Class/Player/Controller/VPVideoPlayerViewController.swift b/Veloria/Class/Player/Controller/VPVideoPlayerViewController.swift new file mode 100644 index 0000000..f19084f --- /dev/null +++ b/Veloria/Class/Player/Controller/VPVideoPlayerViewController.swift @@ -0,0 +1,382 @@ +// +// VPVideoPlayerViewController.swift +// Veloria +// +// Created by Veloria on 2025/5/22. +// + +import UIKit + +@objc protocol VPPlayerListViewControllerDelegate { + + ///加载新数据 + @objc optional func vp_playerViewControllerLoadNewDataV2(playerViewController: VPVideoPlayerViewController) + + ///将要加载更多数据 + @objc optional func vp_playerViewControllerShouldLoadMoreData(playerViewController: VPVideoPlayerViewController) -> Bool + ///加载更多数据 + @objc optional func vp_playerViewControllerLoadMoreData(playerViewController: VPVideoPlayerViewController) + ///向上加载更多数据 + @objc optional func vp_playerViewControllerLoadUpMoreData(playerViewController: VPVideoPlayerViewController) + + ///当前展示的发生变化 + @objc optional func vp_playerListViewController(_ viewController: VPVideoPlayerViewController, didChangeIndexPathForVisible indexPath: IndexPath) + +} + +@objc protocol VPPlayerListViewControllerDataSource { + + + func vp_playerListViewController(_ viewController: VPVideoPlayerViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, oldCell: UICollectionViewCell) -> UICollectionViewCell + + func vp_playerListViewController(_ viewController: VPVideoPlayerViewController, _ collectionView: UICollectionView, numberOfItemsInSection section: Int, oldNumber: Int) -> Int + + + +} + +class VPVideoPlayerViewController: VPViewController { + + var contentSize: CGSize { + return CGSize(width: UIScreen.width, height: UIScreen.height - UIScreen.customTabBarHeight) + } + + var PlayerCellClass: VPVideoPlayerCell.Type { + return VPVideoPlayerCell.self + } + + weak var delegate: VPPlayerListViewControllerDelegate? + weak var dataSource: VPPlayerListViewControllerDataSource? + + private(set) var viewModel = VPVideoPlayViewModel() + + private(set) var dataArr: [Any] = [] + + private(set) var currentIndexPath = IndexPath(row: 0, section: 0) + + ///自动下一级 + var autoNextEpisode = true + + private lazy var collectionViewLayout: UICollectionViewLayout = { + let layout = UICollectionViewFlowLayout() + layout.itemSize = contentSize + layout.minimumInteritemSpacing = 0 + layout.minimumLineSpacing = 0 + return layout + }() + + private(set) lazy var collectionView: VPCollectionView = { + let collectionView = VPCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) + collectionView.delegate = self + collectionView.dataSource = self + collectionView.isPagingEnabled = true + collectionView.showsVerticalScrollIndicator = false + collectionView.showsHorizontalScrollIndicator = false + collectionView.bounces = false + collectionView.scrollsToTop = false +// PlayerCellClass.registerCell(collectionView: collectionView) + collectionView.register(PlayerCellClass.self, forCellWithReuseIdentifier: "cell") + return collectionView + }() + + deinit { + NotificationCenter.default.removeObserver(self) + } + + override func viewDidLoad() { + super.viewDidLoad() + NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActiveNotification), name: UIApplication.didBecomeActiveNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(willResignActiveNotification), name: UIApplication.willResignActiveNotification, object: nil) + + do { + try? KTVHTTPCache.proxyStart() + } + + vp_setupUI() + vp_addActio() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + if getDataCount() > 0 && self.viewModel.isPlaying { + self.viewModel.currentPlayer?.start() +// self.play() + } + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + self.viewModel.currentPlayer?.pause() + ///处理切换页面后,滑动代理scrollViewDidEndDecelerating不执行的问题 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in + guard let self = self else { return } + self.scrollDidEnd(self.collectionView) + } + } + + func play() { + if self.isDidAppear { + self.viewModel.currentPlayer?.start() + } + + self.viewModel.isPlaying = true + + if getDataCount() - currentIndexPath.row <= 2 { + self.loadMoreData() + } + +// if isFirstPlay { +// isFirstPlay = false +// let offset = self.collectionView.contentOffset.y + 0.2 +// self.collectionView.setContentOffset(CGPoint(x: 0, y: offset), animated: false) +// +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { +// let offset = self.collectionView.contentOffset.y +// self.collectionView.setContentOffset(CGPoint(x: 0, y: floor(offset)), animated: false) +// } +// } + } + + func pause() { + self.viewModel.isPlaying = false + self.viewModel.currentPlayer?.pause() + } + + func setDataArr(dataArr: [Any], completer: (() -> Void)?) { + self.dataArr = dataArr + reloadData(completion: completer) + } + + func addDataArr(dataArr: [Any]) { + guard dataArr.count > 0 else { return } + + var indexPaths: [IndexPath] = [] + var startRow = self.dataArr.count + + dataArr.forEach { _ in + indexPaths.append(IndexPath(row: startRow, section: 0)) + startRow += 1 + } + self.dataArr += dataArr + + CATransaction.setCompletionBlock(nil) + CATransaction.begin() + self.collectionView.insertItems(at: indexPaths) + CATransaction.commit() + } + + func reloadData(completion: (() -> Void)? = nil) { + CATransaction.setCompletionBlock { [weak self] in + guard let self = self else { return } + let cell = self.collectionView.cellForItem(at: self.currentIndexPath) as? VPVideoPlayerCell + self.viewModel.currentPlayer = cell + + completion?() + } + CATransaction.begin() + self.collectionView.reloadData() + CATransaction.commit() + } + + func scrollToItem(indexPath: IndexPath, animated: Bool = true, completer: (() -> Void)? = nil) { + CATransaction.setCompletionBlock { [weak self] in + guard let self = self else { return } + if !animated { + if self.currentIndexPath != indexPath { + self.skip(indexPath: indexPath) + } else { + self.play() + } + } + completer?() + } + CATransaction.begin() + self.collectionView.scrollToItem(at: indexPath, at: .top, animated: animated); + CATransaction.commit() + } + + ///当前播放完成 子类可重写 + func currentPlayFinish() { + if self.autoNextEpisode { + scrollToNextEpisode() + } + } + + ///当前播放进度变更 子类可重写 + func currentPlayTimeDidChange(time: Int) { + + } +} + +extension VPVideoPlayerViewController { + func getDataCount() -> Int { + return self.collectionView(self.collectionView, numberOfItemsInSection: 0) + } + + ///点击播放或暂停 + private func clickPauseOrPlay() { + if self.viewModel.isPlaying { + self.pause() + } else { + self.play() + } + } + + + ///滑动至下一级 + private func scrollToNextEpisode() { + + var contentOffset = self.collectionView.contentOffset + + if hasNextEpisode() { + contentOffset.y = floor(contentOffset.y + self.contentSize.height) + self.collectionView.setContentOffset(contentOffset, animated: true) + } else { + self.viewModel.currentPlayer?.replay() + } + } + + + ///是否还有下一级 + private func hasNextEpisode() -> Bool { + let contentOffset = self.collectionView.contentOffset + let contentSize = self.collectionView.contentSize + if contentOffset.y >= contentSize.height - self.contentSize.height { + return false + } + return true + } + + +} + + +extension VPVideoPlayerViewController { + private func vp_setupUI() { + view.addSubview(collectionView) + + collectionView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalToSuperview() + make.width.equalTo(self.contentSize.width) + make.height.equalTo(self.contentSize.height) + } + } + + private func vp_addActio() { + self.viewModel.handlePauseOrPlay = { [weak self] in + self?.clickPauseOrPlay() + } + + self.viewModel.handlePlayFinish = { [weak self] in + self?.currentPlayFinish() + } + + self.viewModel.handlePlayTimeDidChange = { [weak self] time in + self?.currentPlayTimeDidChange(time: time) + } + } +} + + +//MARK: -------------- UICollectionViewDelegate UICollectionViewDataSource -------------- +extension VPVideoPlayerViewController: UICollectionViewDelegate, UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + var cell: UICollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) + + if let newCell = self.dataSource?.vp_playerListViewController(self, collectionView, cellForItemAt: indexPath, oldCell: cell) { + cell = newCell + } + + if let cell = cell as? VPVideoPlayerCell { + if cell.viewModel == nil { + cell.viewModel = viewModel + } + } + + if self.viewModel.currentPlayer == nil, indexPath == currentIndexPath, let playerProtocol = cell as? VPPlayerProtocol { + self.currentIndexPath = indexPath + self.viewModel.currentPlayer = playerProtocol + didChangeIndexPathForVisible() + } + + + return cell + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + let count = dataArr.count + if let newCount = self.dataSource?.vp_playerListViewController(self, collectionView, numberOfItemsInSection: section, oldNumber: count) { + return newCount + } else { + return count + } + } + + //滑动停止 + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + scrollDidEnd(scrollView) + } + + func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { + scrollDidEnd(scrollView) + } + + private func scrollDidEnd(_ scrollView: UIScrollView) { + let offsetY = scrollView.contentOffset.y + let indexPaths = self.collectionView.indexPathsForVisibleItems + for indexPath in indexPaths { + guard let cell = self.collectionView.cellForItem(at: indexPath) else { continue } + if floor(offsetY) == floor(cell.frame.origin.y) { + if self.currentIndexPath != indexPath { + self.skip(indexPath: indexPath) + } + } + } + } + + private func skip(indexPath: IndexPath) { + currentIndexPath = indexPath + guard let currentPlayer = self.collectionView.cellForItem(at: indexPath) as? VPPlayerProtocol else { return } + self.viewModel.currentPlayer = currentPlayer +// currentCell = self.collectionView.cellForItem(at: indexPath) as? BCListPlayerCell + didChangeIndexPathForVisible() + self.play() + } +} + + +extension VPVideoPlayerViewController { + + private func loadMoreData() { + let isLoad = self.delegate?.vp_playerViewControllerShouldLoadMoreData?(playerViewController: self) + if isLoad != false { + self.delegate?.vp_playerViewControllerLoadMoreData?(playerViewController: self) + } + } + + private func loadUpMoreData() { + self.delegate?.vp_playerViewControllerLoadUpMoreData?(playerViewController: self) + } + + private func didChangeIndexPathForVisible() { + self.delegate?.vp_playerListViewController?(self, didChangeIndexPathForVisible: self.currentIndexPath) + } +} + +//MARK: -------------- APP生命周期 -------------- +extension VPVideoPlayerViewController { + + @objc func didBecomeActiveNotification() { + if getDataCount() > 0 && self.viewModel.isPlaying && isDidAppear { + self.viewModel.currentPlayer?.start() + } + } + + @objc func willResignActiveNotification() { + self.viewModel.currentPlayer?.pause() + } + + +} diff --git a/Veloria/Class/Player/Model/VPPlayerProtocol.swift b/Veloria/Class/Player/Model/VPPlayerProtocol.swift new file mode 100644 index 0000000..861cbe7 --- /dev/null +++ b/Veloria/Class/Player/Model/VPPlayerProtocol.swift @@ -0,0 +1,46 @@ +// +// VPPlayerProtocol.swift +// Veloria +// +// Created by Veloria on 2025/5/22. +// + +import UIKit + +@objc protocol VPPlayerProtocol: NSObjectProtocol { + + ///播放完成 + var playerFinishHadle: (() -> Void)? { get set } + +// var model: Any? { get set } +// var videoInfo: VPVideoInfoModel? { get set } + + var isCurrent: Bool { get set } + + ///上一集是否加锁 + @objc optional var hasLockUpEpisode: Bool { get set } + + ///总进度 + var duration: Int { get } + ///当前进度 + var currentPosition: Int { get } + + var rate: Float { get set } + + ///播放准备 + func prepare() + + ///开始播放 + func start() + + ///暂停播放 + func pause() + + ///从头播放 + func replay() + + ///设置进度 + func seekToTime(toTime: Int) + + +} diff --git a/Veloria/Class/Player/Model/VPVideoDetailModel.swift b/Veloria/Class/Player/Model/VPVideoDetailModel.swift new file mode 100644 index 0000000..dcb7cdd --- /dev/null +++ b/Veloria/Class/Player/Model/VPVideoDetailModel.swift @@ -0,0 +1,23 @@ +// +// VPVideoDetailModel.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit +import SmartCodable + +class VPVideoDetailModel: VPModel, SmartCodable { + var business_model: String? + var video_info: VPVideoInfoModel? + var shortPlayInfo: VPShortModel? + var episodeList: [VPVideoInfoModel]? + var is_collect: Bool? + var show_share_coin: Int? + var share_coin: Int? + var install_coins: Int? + var revolution: Int? + var unlock_video_ad_count: Int? + var discount: Int? +} diff --git a/Veloria/Class/Player/Model/VPVideoRateModel.swift b/Veloria/Class/Player/Model/VPVideoRateModel.swift new file mode 100644 index 0000000..ed5a097 --- /dev/null +++ b/Veloria/Class/Player/Model/VPVideoRateModel.swift @@ -0,0 +1,69 @@ +// +// VPVideoRateModel.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit + +class VPVideoRateModel: NSObject { + + enum Rate: String { + case x0_5 = "0.5x" + case x0_75 = "0.75x" + case x1 = "1.0x" + case x1_25 = "1.25x" + case x1_5 = "1.5x" + case x1_75 = "1.75x" + case x2 = "2.0x" + + func getRate() -> Float { + switch self { + case .x0_5: + return 0.5 + + case .x0_75: + return 0.75 + + case .x1: + return 1 + + case .x1_25: + return 1.25 + + case .x1_5: + return 1.5 + + case .x1_75: + return 1.75 + + case .x2: + return 2 + } + } + } + + static func getAllRate() -> [VPVideoRateModel] { + return [ + VPVideoRateModel(rate: .x0_75), + VPVideoRateModel(rate: .x1), + VPVideoRateModel(rate: .x1_25), + VPVideoRateModel(rate: .x1_5), + VPVideoRateModel(rate: .x1_75), + VPVideoRateModel(rate: .x2) + ] + } + + var rate: Rate = .x1 + + init(rate: Rate) { + super.init() + self.rate = rate + } + + func formatString() -> String { + return self.rate.rawValue + } + +} diff --git a/Veloria/Class/Player/View/VPDetailPlayerCell.swift b/Veloria/Class/Player/View/VPDetailPlayerCell.swift new file mode 100644 index 0000000..a632c92 --- /dev/null +++ b/Veloria/Class/Player/View/VPDetailPlayerCell.swift @@ -0,0 +1,16 @@ +// +// VPDetailPlayerCell.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit + +class VPDetailPlayerCell: VPVideoPlayerCell { + + override var ControlViewClass: VPVideoPlayerControlView.Type { + return VPDetailPlayerControlView.self + } + +} diff --git a/Veloria/Class/Player/View/VPDetailPlayerControlView.swift b/Veloria/Class/Player/View/VPDetailPlayerControlView.swift new file mode 100644 index 0000000..cfe0d7b --- /dev/null +++ b/Veloria/Class/Player/View/VPDetailPlayerControlView.swift @@ -0,0 +1,221 @@ +// +// VPDetailPlayerControlView.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit + +class VPDetailPlayerControlView: VPVideoPlayerControlView { + + override var viewModel: VPVideoPlayViewModel? { + didSet { + self.viewModel?.addObserver(self, forKeyPath: "rateModel", options: .new, context: nil) + + rateButton.setTitle(self.viewModel?.rateModel.formatString(), for: .normal) + } + } + + + override var videoInfo: VPVideoInfoModel? { + didSet { + epView.setTitle(String(format: "EP.%@".localized, "\(videoInfo?.episode ?? "0")"), for: .normal) + } + } + + override var shortModel: VPShortModel? { + didSet { + allEpView.setTitle(String(format: "All %@ Episodes".localized, "\(shortModel?.episode_total ?? 0)"), for: .normal) + } + } + + override var durationTime: Int { + didSet { + updateTimeLabel() + } + } + + override var currentTime: Int { + didSet { + updateTimeLabel() + } + } + + //MARK: -------------- UI属性 -------------- + private lazy var bottomView: VPGradientView = { + let view = VPGradientView() + view.isUserInteractionEnabled = false + view.colors = [UIColor.color000000(alpha: 0).cgColor, UIColor.color000000(alpha: 0.5).cgColor, UIColor.color000000(alpha: 1).cgColor] + view.locations = [0, 0.5, 1] + view.startPoint = .init(x: 0.5, y: 0) + view.endPoint = .init(x: 0.5, y: 1) + return view + }() + + private lazy var epBgView: UIView = { + let view = UIButton(type: .custom) + view.setBackgroundImage(UIImage(color: .color949494(alpha: 0.4)), for: .normal) + view.layer.cornerRadius = 15 + view.layer.masksToBounds = true + view.addTarget(self, action: #selector(handleEpisodesButton), for: .touchUpInside) + return view + }() + + private lazy var epView: UIButton = { + let view = JXButton(type: .custom) + view.isUserInteractionEnabled = false + view.titleDirection = .right + view.jx_font = .fontRegular(ofSize: 13) + view.space = 5 + view.setTitleColor(.colorFFFFFF(), for: .normal) + view.setImage(UIImage(named: "ep_icon_01"), for: .normal) + return view + }() + + private lazy var allEpView: UIButton = { + let view = JXButton(type: .custom) + view.isUserInteractionEnabled = false + view.titleDirection = .left + view.jx_font = .fontRegular(ofSize: 13) + view.space = 4 + view.setTitleColor(.colorBEBEBE(), for: .normal) + view.setImage(UIImage(named: "arrow_up_icon_01"), for: .normal) + return view + }() + + private lazy var rateButton: UIButton = { + let button = UIButton(type: .custom) + button.setBackgroundImage(UIImage(color: .color949494(alpha: 0.4)), for: .normal) + button.layer.cornerRadius = 15 + button.layer.masksToBounds = true + button.setTitleColor(.colorFFFFFF(), for: .normal) + button.titleLabel?.font = .fontRegular(ofSize: 13) + button.addTarget(self, action: #selector(handleRateButton), for: .touchUpInside) + return button + }() + + private lazy var timeLabel: UILabel = { + let label = UILabel() + label.font = .fontRegular(ofSize: 12) + label.textColor = .colorFFFFFB(alpha: 0.9) + label.isUserInteractionEnabled = false + return label + }() + + ///倍速选择 + private lazy var rateSelectedView: VPRateSelectedView = { + let view = VPRateSelectedView() + view.didSelected = { [weak self] model in + guard let self = self else { return } + self.viewModel?.rateModel = model + } + return view + }() + + deinit { + self.viewModel?.removeObserver(self, forKeyPath: "rateModel") + } + + override init(frame: CGRect) { + super.init(frame: frame) + + vp_setupUI() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) + + if keyPath == "rateModel" { + rateButton.setTitle(self.viewModel?.rateModel.formatString(), for: .normal) + } + } +} + +extension VPDetailPlayerControlView { + + private func updateTimeLabel() { + let currentTime = self.currentTime.formatTimeGroup() + let durationTime = self.durationTime.formatTimeGroup() + + timeLabel.text = "\(currentTime.1):\(currentTime.2)/\(durationTime.1):\(durationTime.2)" + } + + @objc private func handleEpisodesButton() { + self.viewModel?.handleEpisode?() + } + + @objc private func handleRateButton() { + addSubview(rateSelectedView) + rateSelectedView.currentRateModel = self.viewModel?.rateModel + + rateSelectedView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + +} + +extension VPDetailPlayerControlView { + + private func vp_setupUI() { + progressView.lineWidth = 3 + progressView.insets = .init(top: 20, left: 15, bottom: 10, right: 15) + + addSubview(bottomView) + addSubview(epBgView) + epBgView.addSubview(epView) + epBgView.addSubview(allEpView) + addSubview(rateButton) + addSubview(timeLabel) + + self.sendSubviewToBack(self.bottomView) + + progressView.snp.remakeConstraints { make in + make.left.right.equalToSuperview() + make.bottom.equalTo(epBgView.snp.top) + } + + rightToolView.snp.updateConstraints { make in + make.bottom.equalToSuperview().offset(-(UIScreen.tabbarSafeBottomMargin + 150)) + } + + bottomView.snp.makeConstraints { make in + make.left.right.bottom.equalToSuperview() + make.height.equalTo(UIScreen.tabbarSafeBottomMargin + 100) + } + + epBgView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(15) + make.bottom.equalToSuperview().offset(-(UIScreen.tabbarSafeBottomMargin + 10)) + make.height.equalTo(30) + } + + epView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(10) + make.centerY.equalToSuperview() + } + + allEpView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.right.equalToSuperview().offset(-10) + } + + rateButton.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-15) + make.left.equalTo(epBgView.snp.right).offset(10) + make.height.top.equalTo(epBgView) + make.width.equalTo(50) + } + + timeLabel.snp.makeConstraints { make in + make.left.equalToSuperview().offset(15) + make.bottom.equalTo(epBgView.snp.top).offset(-16) + } + } + +} diff --git a/Veloria/Class/Player/View/VPEpisodeCell.swift b/Veloria/Class/Player/View/VPEpisodeCell.swift new file mode 100644 index 0000000..ba2545f --- /dev/null +++ b/Veloria/Class/Player/View/VPEpisodeCell.swift @@ -0,0 +1,78 @@ +// +// VPEpisodeCell.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit + +class VPEpisodeCell: VPCollectionViewCell { + + + var videoInfoModel: VPVideoInfoModel? { + didSet { + numLabel.text = videoInfoModel?.episode + } + } + + var vp_isSelected: Bool = false { + didSet { + if vp_isSelected { + contentView.vp_setGradientBorder() + contentView.backgroundColor = .color05CEA0(alpha: 0.1) + } else { + contentView.vp_removeGradientBorder() + contentView.backgroundColor = .colorFFFFFF(alpha: 0.1) + } + } + } + + private lazy var bgView: UIView = { + let view = UIView() + view.vp_setGradientBorder() + view.layer.cornerRadius = 6 + view.layer.masksToBounds = true + return view + }() + + + + private lazy var numLabel: UILabel = { + let label = UILabel() + label.font = .fontRegular(ofSize: 14) + label.textColor = .colorFFFFFF() + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + vp_setupUI() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +extension VPEpisodeCell { + + private func vp_setupUI() { + contentView.layer.cornerRadius = 6 + contentView.layer.masksToBounds = true + +// contentView.addSubview(bgView) + contentView.addSubview(numLabel) + +// bgView.snp.makeConstraints { make in +// make.edges.equalToSuperview() +// } + + numLabel.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } + +} diff --git a/Veloria/Class/Player/View/VPEpisodeMenuView.swift b/Veloria/Class/Player/View/VPEpisodeMenuView.swift new file mode 100644 index 0000000..ec0cdea --- /dev/null +++ b/Veloria/Class/Player/View/VPEpisodeMenuView.swift @@ -0,0 +1,152 @@ +// +// VPEpisodeMenuView.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit + +class VPEpisodeMenuView: UIView { + + override var intrinsicContentSize: CGSize { + return CGSize(width: UIScreen.width, height: 35) + } + + var didSelectedIndex: ((_ index: Int) -> Void)? + + var dataArr: [String] = [] { + didSet { + self.reloadData() + } + } + + var selectedIndex: Int = 0 { + didSet { + self.buttonArr.forEach { + $0.isSelected = $0.tag == selectedIndex + } + self.progressSlide() + } + } + + + private lazy var buttonArr: [UIButton] = [] + + //MARK: UI属性 + private lazy var progressView: VPGradientView = { + let view = VPGradientView() + view.colors = [UIColor.color05CEA0().cgColor, UIColor.color7C174F().cgColor] + view.startPoint = .init(x: 0, y: 0.3) + view.endPoint = .init(x: 1, y: 0.8) + view.locations = [0, 1] + view.layer.cornerRadius = 1 + view.layer.masksToBounds = true + return view + }() + + private lazy var scrollView: VPScrollView = { + let scrollView = VPScrollView() + scrollView.showsVerticalScrollIndicator = false + scrollView.showsHorizontalScrollIndicator = false + return scrollView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + _setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func reloadData() { + buttonArr.forEach { + $0.removeFromSuperview() + } + buttonArr.removeAll() + + let count = self.dataArr.count + + var previousButton: UIButton? + + dataArr.enumerated().forEach { + let normalStrig = NSMutableAttributedString(string: $1) + normalStrig.color = .colorFFFFFF(alpha: 0.6) + normalStrig.font = .fontMedium(ofSize: 14) + + let selectedString = NSMutableAttributedString(string: $1) + selectedString.color = .colorFFFFFF() + selectedString.font = .fontMedium(ofSize: 14) + + + let button = UIButton(type: .custom) + button.tag = $0 + button.setAttributedTitle(normalStrig, for: .normal) + button.setAttributedTitle(selectedString, for: .selected) + button.setAttributedTitle(selectedString, for: [.selected, .highlighted]) + button.addTarget(self, action: #selector(handleButton), for: .touchUpInside) + button.isSelected = $0 == selectedIndex + + self.scrollView.addSubview(button) + self.buttonArr.append(button) + + if previousButton == nil { + button.snp.makeConstraints { make in + make.top.left.equalToSuperview() + make.height.equalTo(35) + } + } else if let previousButton = previousButton, count - 1 == $0 { + button.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalTo(previousButton.snp.right).offset(27) + make.height.equalTo(35) + make.right.equalToSuperview() + } + } else if let previousButton = previousButton { + button.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalTo(previousButton.snp.right).offset(27) + make.height.equalTo(35) + } + } + + previousButton = button + } + + progressSlide() + } + + private func progressSlide() { + if self.selectedIndex >= self.buttonArr.count { return } + let currentButton = self.buttonArr[self.selectedIndex] + + self.progressView.snp.remakeConstraints { make in + make.bottom.width.equalTo(currentButton) + make.centerX.equalTo(currentButton) + make.height.equalTo(2) + } + } + + + @objc private func handleButton(sender: UIButton) { + self.selectedIndex = sender.tag + self.didSelectedIndex?(self.selectedIndex) + } + +} + +extension VPEpisodeMenuView { + + private func _setupUI() { + addSubview(scrollView) + scrollView.addSubview(progressView) + + scrollView.snp.makeConstraints { make in + make.left.right.top.equalToSuperview() + make.bottom.equalToSuperview() + } + } + +} diff --git a/Veloria/Class/Player/View/VPEpisodeView.swift b/Veloria/Class/Player/View/VPEpisodeView.swift new file mode 100644 index 0000000..e66ae84 --- /dev/null +++ b/Veloria/Class/Player/View/VPEpisodeView.swift @@ -0,0 +1,333 @@ +// +// VPEpisodeView.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit + +class VPEpisodeView: HWPanModalContentView { + + var currentIndex: Int = 0 { + didSet { + self.collectionView.reloadData() + } + } + + var shortModel: VPShortModel? { + didSet { + coverImageView.vp_setImage(url: shortModel?.image_url) + videoNameLabel.text = shortModel?.name + if let category = shortModel?.category?.first, !category.isEmpty { + tagView.setTitle(category, for: .normal) + tagView.isHidden = false + } else { + tagView.isHidden = true + } + desLabel.text = shortModel?.sp_description + } + } + + var dataArr: [VPVideoInfoModel] = [] { + didSet { + self.collectionView.reloadData() + + var menuDataArr = [String]() + let totalEpisode = dataArr.count + var index = 0 + var remainingEpisodes = totalEpisode + + while remainingEpisodes > 0 { + let minIndex = index * 30 + var maxIndex = minIndex + 29 + if maxIndex >= dataArr.count { + maxIndex = dataArr.count - 1 + } + + let minEpisode = dataArr[minIndex].episode ?? "0" + let maxEpisode = dataArr[maxIndex].episode ?? "0" + + if minEpisode == maxEpisode { + menuDataArr.append("\(minEpisode)") + } else { + menuDataArr.append("\(minEpisode)-\(maxEpisode)") + } + + remainingEpisodes -= 30 + index += 1 + } + + self.menuView.dataArr = menuDataArr + } + } + + var didSelectedIndex: ((_ index: Int) -> Void)? + + var isDecelerating = false + var isDragging = false + + //MARK: UI属性 + private lazy var bgView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "bg_image_01")) + return imageView + }() + + private lazy var closeButton: UIButton = { + let button = UIButton(type: .custom) + button.setImage(UIImage(named: "close_icon_01"), for: .normal) + button.addTarget(self, action: #selector(handleCloseButton), for: .touchUpInside) + return button + }() + + private lazy var coverImageView: VPImageView = { + let imageView = VPImageView() + imageView.layer.cornerRadius = 6 + imageView.layer.masksToBounds = true + return imageView + }() + + private lazy var videoNameLabel: UILabel = { + let label = UILabel() + label.font = .fontMedium(ofSize: 15) + label.textColor = .colorFFFFFF() + label.numberOfLines = 2 + return label + }() + + private lazy var tagView: JXButton = { + let view = JXButton(type: .custom) + view.isUserInteractionEnabled = false + view.backgroundColor = .colorFFFFFF(alpha: 0.1) + view.leftAndRightMargin = 6 + view.layer.cornerRadius = 3 + view.layer.masksToBounds = true + view.jx_font = .fontRegular(ofSize: 12) + view.setTitleColor(.colorAFAFAF(), for: .normal) + return view + }() + + private lazy var desLabel: UILabel = { + let label = UILabel() + label.font = .fontRegular(ofSize: 12) + label.textColor = .colorFFFFFF(alpha: 0.6) + label.numberOfLines = 3 + return label + }() + + private lazy var lineView: UIView = { + let view = UIView() + view.backgroundColor = .color545458(alpha: 0.45) + return view + }() + + private lazy var collectionViewLayout: UICollectionViewFlowLayout = { + let itemWidth = floor((UIScreen.width - 8 * 4 - 30) / 5) + + let layout = UICollectionViewFlowLayout() + layout.itemSize = .init(width: itemWidth, height: 54) + layout.minimumLineSpacing = 9 + layout.minimumInteritemSpacing = 8 + layout.sectionInset = .init(top: 0, left: 15, bottom: 0, right: 15) + return layout + }() + + private lazy var collectionView: VPCollectionView = { + let collectionView = VPCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) + collectionView.delegate = self + collectionView.dataSource = self + collectionView.contentInset = .init(top: 0, left: 0, bottom: UIScreen.tabbarSafeBottomMargin, right: 0) + collectionView.register(VPEpisodeCell.self, forCellWithReuseIdentifier: "cell") + return collectionView + }() + + private lazy var menuView: VPEpisodeMenuView = { + let view = VPEpisodeMenuView() + view.didSelectedIndex = { [weak self] index in + guard let self = self else { return } + var row = 0 + if index > 0 { + row = index * 30 + 10 + let count = self.dataArr.count + if row >= count { + row = count - 1 + } + } + let indexPath = IndexPath.init(row: row, section: 0) + self.collectionView.scrollToItem(at: indexPath, at: .centeredVertically, animated: true) + + } + return view + }() + + override init(frame: CGRect) { + super.init(frame: frame) + vp_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func handleCloseButton() { + self.dismiss(animated: true) { + + } + } + + //MARK: HWPanModalPresentable + override func panScrollable() -> UIScrollView? { + return collectionView + } + + override func longFormHeight() -> PanModalHeight { + return PanModalHeightMake(.content, UIScreen.height * (2 / 3)) + } + + override func showDragIndicator() -> Bool { + return false + } + + override func backgroundConfig() -> HWBackgroundConfig { + let config = HWBackgroundConfig() + config.backgroundAlpha = 0.6 + return config + } +} + +extension VPEpisodeView { + + private func vp_setupUI() { + addSubview(bgView) + addSubview(closeButton) + addSubview(coverImageView) + addSubview(videoNameLabel) + addSubview(tagView) + addSubview(desLabel) + addSubview(lineView) + addSubview(menuView) + addSubview(collectionView) + + bgView.snp.makeConstraints { make in + make.left.right.top.equalToSuperview() + } + + closeButton.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-5) + make.top.equalToSuperview().offset(5) + make.width.equalTo(40) + make.height.equalTo(40) + } + + coverImageView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(15) + make.top.equalToSuperview().offset(45) + make.width.equalTo(64) + make.height.equalTo(82) + } + + videoNameLabel.snp.makeConstraints { make in + make.left.equalTo(coverImageView.snp.right).offset(10) + make.centerY.equalTo(coverImageView.snp.top).offset(20) + make.right.lessThanOrEqualToSuperview().offset(-15) + } + + tagView.snp.makeConstraints { make in + make.left.equalTo(videoNameLabel) + make.bottom.equalTo(coverImageView).offset(-12) + } + + desLabel.snp.makeConstraints { make in + make.left.equalToSuperview().offset(15) + make.right.lessThanOrEqualToSuperview().offset(-15) + make.top.equalTo(coverImageView.snp.bottom).offset(10) + } + + lineView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(15) + make.centerX.equalToSuperview() + make.top.equalTo(desLabel.snp.bottom).offset(46) + make.height.equalTo(1) + } + + menuView.snp.makeConstraints { make in + make.left.right.equalTo(self.lineView) + make.bottom.equalTo(self.lineView).offset(0.5) + } + + collectionView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.bottom.equalToSuperview() + make.top.equalTo(lineView.snp.bottom).offset(14) + } + + } + +} + +//MARK: -------------- UICollectionViewDelegate UICollectionViewDataSource -------------- +extension VPEpisodeView: UICollectionViewDelegate, UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! VPEpisodeCell + cell.videoInfoModel = self.dataArr[indexPath.row] + cell.vp_isSelected = indexPath.row == currentIndex + return cell + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return self.dataArr.count + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard indexPath.row != currentIndex else { return } + self.didSelectedIndex?(indexPath.row) + self.dismiss(animated: true) { + + } + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if isDragging || isDecelerating { + updateMuneSelectedIndex() + } + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + isDecelerating = false + updateMuneSelectedIndex() + } + + func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) { + isDecelerating = true + } + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + isDragging = true + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + isDragging = false + } + + func updateMuneSelectedIndex() { + let indexPathArr = collectionView.indexPathsForVisibleItems + + var minRow = dataArr.count - 1 + var maxRow = 0 + + for indexPath in indexPathArr { + if indexPath.row < minRow { + minRow = indexPath.row + } + if indexPath.row > maxRow { + maxRow = indexPath.row + } + } + + let selectedIndex = maxRow / 30 + if menuView.selectedIndex != selectedIndex { + menuView.selectedIndex = selectedIndex + } + } +} diff --git a/Veloria/Class/Player/View/VPPlayerProgressView.swift b/Veloria/Class/Player/View/VPPlayerProgressView.swift new file mode 100644 index 0000000..2ace123 --- /dev/null +++ b/Veloria/Class/Player/View/VPPlayerProgressView.swift @@ -0,0 +1,212 @@ +// +// VPPlayerProgressView.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/22. +// + +import UIKit + +class VPPlayerProgressView: UIView { + + ///滑动开始 + var panStart: (() -> Void)? + + ///滑动中 + var panChange: ((_ progress: CGFloat) -> Void)? + + ///滑动完成回调 + var panFinish: ((_ progress: CGFloat) -> Void)? + + var progress: CGFloat = 0 { + didSet { + if !isPaning { + setNeedsDisplay() + } + } + } + + ///用来记录滑动时的当前进度 + private var tempProgress: CGFloat = 0 + + ///滑动进度 + private var panProgress: CGFloat = 0 + + var progressColor: UIColor = .colorFFFFFF(alpha: 0.2) + var currentProgress: UIColor = .colorFFFFFF() + + var lineWidth: CGFloat = 2 + + ///加载中状态 + var isLoading = false { + didSet { + if isLoading { + if gradientTimer == nil { + gradientTimer = Timer.scheduledTimer(timeInterval: 0.05, target: YYWeakProxy(target: self), selector: #selector(handleGradientTimer), userInfo: nil, repeats: true) + } + } else { + gradientTimer?.invalidate() + gradientTimer = nil + } + } + } + + var insets: UIEdgeInsets = .init(top: 0, left: 0, bottom: 0, right: 0) { + didSet { + self.invalidateIntrinsicContentSize() + setNeedsDisplay() + } + } + + private(set) lazy var panGesture: UIPanGestureRecognizer = { + let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(sender:))) + return pan + }() + + private(set) lazy var tagGesture: UITapGestureRecognizer = { + let tap = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(sender:))) + return tap + }() + + ///是否在滑动中 + private var isPaning: Bool = false + + private var gradientTimer: Timer? + + private var gradientValue: CGFloat = 0 + + override var intrinsicContentSize: CGSize { + return .init(width: UIScreen.width, height: lineWidth + insets.top + insets.bottom) + } + + override init(frame: CGRect) { + super.init(frame: frame) +// self.backgroundColor = progressColor + self.backgroundColor = .clear + + self.addGestureRecognizer(panGesture) + self.addGestureRecognizer(tagGesture) + + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + setNeedsDisplay() + } + + @objc private func handleGradientTimer() { + gradientValue += 0.1 + if gradientValue > 1 { + gradientValue = 0 + } + setNeedsDisplay() + } + + override func draw(_ rect: CGRect) { + super.draw(rect) + guard let context = UIGraphicsGetCurrentContext() else { return } + let width = rect.width + let height = rect.height + + let progressX = insets.left + let progressY = insets.top +// let progressY = height - lineWidth + let progressWidth = width - insets.left - insets.right + + if isLoading, !isPaning { + // 定义颜色空间 + let colorSpace = CGColorSpaceCreateDeviceRGB() + let colors: [CGColor] = [ + UIColor.clear.cgColor, + UIColor.white.cgColor, + UIColor.clear.cgColor + ] + let locations: [CGFloat] = [0.0, gradientValue, 1.0] + + guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: locations) else { + return + } + + let gradientRect = CGRect(x: progressX, + y: progressY, + width: progressWidth, + height: lineWidth) + + // 定义渐变的起点和终点 + let startPoint = CGPoint(x: rect.minX, y: rect.minY) + let endPoint = CGPoint(x: rect.maxX, y: rect.maxY) + + // 裁剪到渐变区域 + context.saveGState() + context.clip(to: gradientRect) + + // 绘制渐变 + context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: []) + } else { + var progress = self.progress + if self.isPaning { + progress = self.panProgress + } + + +// let y = height - lineWidth + + ///绘制进度 + let progressPath = UIBezierPath(roundedRect: CGRect(x: progressX, y: progressY, width: progressWidth, height: lineWidth), cornerRadius: lineWidth / 2) + context.addPath(progressPath.cgPath) + context.setFillColor(progressColor.cgColor) + context.fillPath() + + ///绘制当前进度 + let currentPath = UIBezierPath(roundedRect: CGRect(x: progressX, y: progressY, width: progressWidth * progress, height: lineWidth), cornerRadius: lineWidth / 2) + context.addPath(currentPath.cgPath) + context.setFillColor(currentProgress.cgColor) + context.fillPath() + } + + + } + +} + +extension VPPlayerProgressView { + + @objc func handlePanGesture(sender: UIPanGestureRecognizer) { + + switch sender.state { + case .began: + self.isPaning = true + self.tempProgress = self.progress + sender.setTranslation(CGPoint(x: 0, y: 0), in: self) + self.panStart?() + + case .changed: + let point = sender.translation(in: self) + let offsetX = point.x / self.width + self.panProgress = self.tempProgress + offsetX + if self.panProgress < 0 { + self.panProgress = 0 + } + self.panChange?(self.panProgress) + setNeedsDisplay() + + default: + self.isPaning = false + self.panFinish?(self.panProgress) + + self.panProgress = 0 + } + } + + @objc func handleTapGesture(sender: UITapGestureRecognizer) { + let point = sender.location(in: self) + let offsetX = point.x / self.width + self.panFinish?(offsetX) + } +} diff --git a/Veloria/Class/Player/View/VPRateSelectedCell.swift b/Veloria/Class/Player/View/VPRateSelectedCell.swift new file mode 100644 index 0000000..9795575 --- /dev/null +++ b/Veloria/Class/Player/View/VPRateSelectedCell.swift @@ -0,0 +1,63 @@ +// +// VPRateSelectedCell.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit + +class VPRateSelectedCell: VPCollectionViewCell { + + var model: VPVideoRateModel? { + didSet { + label.text = model?.formatString() + } + } + + var vp_isSelected: Bool = false { + didSet { + if vp_isSelected { + contentView.vp_setGradientBorder() + contentView.backgroundColor = .color1C2D2F(alpha: 0.6) + + } else { + contentView.vp_removeGradientBorder() + contentView.backgroundColor = .color000000(alpha: 0.3) + } + } + } + + private lazy var label: UILabel = { + let label = UILabel() + label.font = .fontRegular(ofSize: 14) + label.textColor = .colorFFFFFF() + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + vp_setupUI() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension VPRateSelectedCell { + + private func vp_setupUI() { + contentView.layer.cornerRadius = 6 + contentView.layer.masksToBounds = true + + + + contentView.addSubview(label) + + label.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } + +} diff --git a/Veloria/Class/Player/View/VPRateSelectedView.swift b/Veloria/Class/Player/View/VPRateSelectedView.swift new file mode 100644 index 0000000..473651a --- /dev/null +++ b/Veloria/Class/Player/View/VPRateSelectedView.swift @@ -0,0 +1,102 @@ +// +// VPRateSelectedView.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/23. +// + +import UIKit + +class VPRateSelectedView: UIView { + + var currentRateModel: VPVideoRateModel? { + didSet { + collectionView.reloadData() + } + } + + var didSelected: ((_ rateModel: VPVideoRateModel) -> Void)? + + private lazy var rateArr: [VPVideoRateModel] = VPVideoRateModel.getAllRate() + + private lazy var collectionViewLayout: UICollectionViewFlowLayout = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.itemSize = .init(width: 70, height: 54) + layout.minimumLineSpacing = 10 + layout.sectionInset = .init(top: 0, left: 15, bottom: 0, right: 15) + return layout + }() + + private lazy var collectionView: VPCollectionView = { + let collectionView = VPCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) + collectionView.delegate = self + collectionView.dataSource = self + collectionView.register(VPRateSelectedCell.self, forCellWithReuseIdentifier: "cell") + collectionView.showsHorizontalScrollIndicator = false + return collectionView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + let tap = UITapGestureRecognizer(target: self, action: #selector(handleDismiss)) + tap.delegate = self + self.addGestureRecognizer(tap) + vp_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func handleDismiss() { + self.removeFromSuperview() + } +} + +extension VPRateSelectedView { + + private func vp_setupUI() { + addSubview(collectionView) + + collectionView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.bottom.equalToSuperview().offset(-(UIScreen.tabbarSafeBottomMargin + 85)) + make.height.equalTo(54) + } + } + +} + +//MARK: -------------- UICollectionViewDelegate UICollectionViewDataSource -------------- +extension VPRateSelectedView: UICollectionViewDelegate, UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let model = rateArr[indexPath.row] + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! VPRateSelectedCell + cell.model = model + cell.vp_isSelected = model.rate == currentRateModel?.rate + return cell + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return rateArr.count + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let model = rateArr[indexPath.row] + self.didSelected?(model) + self.handleDismiss() + } +} + +//MARK: -------------- UIGestureRecognizerDelegate -------------- +extension VPRateSelectedView: UIGestureRecognizerDelegate { + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + if touch.view != self { + return false + } else { + return true + } + } +} diff --git a/Veloria/Class/Player/View/VPVideoPlayerCell.swift b/Veloria/Class/Player/View/VPVideoPlayerCell.swift new file mode 100644 index 0000000..74ceefc --- /dev/null +++ b/Veloria/Class/Player/View/VPVideoPlayerCell.swift @@ -0,0 +1,214 @@ +// +// VPVideoPlayerCell.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/22. +// + +import UIKit + +class VPVideoPlayerCell: VPCollectionViewCell, VPPlayerProtocol { + + var ControlViewClass: VPVideoPlayerControlView.Type { + return VPVideoPlayerControlView.self + } + + + weak var viewModel: VPVideoPlayViewModel? { + didSet { + self.controlView.viewModel = self.viewModel + } + } + + var shortModel: VPShortModel? { + didSet { + self.controlView.shortModel = shortModel + coverImageView.vp_setImage(url: shortModel?.image_url) + } + } + + var videoInfo: VPVideoInfoModel? { + didSet { + self.controlView.progress = 0 + self.controlView.currentTime = 0 + self.controlView.durationTime = 0 + + self.controlView.videoInfo = videoInfo + + player.setPlayUrl(url: videoInfo?.video_url ?? "") + + } + } + + var isLoop = true { + didSet { + player.isLoop = isLoop + } + } + + + private lazy var player: VPPlayer = { + let player = VPPlayer() + player.playerView = playerView + player.delegate = self + return player + }() + + //MARK: UI属性 + private lazy var playerView: UIView = { + let view = UIView() + return view + }() + + private lazy var coverImageView: VPImageView = { + let imageView = VPImageView() + return imageView + }() + + private lazy var controlView: VPVideoPlayerControlView = { + let view = ControlViewClass.init() + view.panProgressFinishBlock = { [weak self] progress in + guard let self = self else { return } + let duration = CGFloat(self.player.duration) + let toTime = progress * duration + self.player.seekToTime(toTime: Int(toTime)) + } + return view + }() + + + override init(frame: CGRect) { + super.init(frame: frame) + + vp_setupUI() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + //MARK: VPPlayerProtocol + var playerFinishHadle: (() -> Void)? + + var isCurrent: Bool = false { + didSet { + controlView.isCurrent = isCurrent + if !isCurrent { +// self.player.replay() +// self.player.seekToTime(toTime: 0) + self.coverImageView.isHidden = false + } + } + } + + var duration: Int { + get { + return player.duration + } + } + + var currentPosition: Int { + get { + player.currentPosition + } + } + + var rate: Float { + set { + player.rate = newValue + } + get { + player.rate + } + } + + func prepare() { + + } + + func start() { + player.start() + } + + func pause() { + player.pause() + } + + func replay() { + player.replay() + } + + func seekToTime(toTime: Int) { + player.seekToTime(toTime: toTime) + } + + + + +} + +extension VPVideoPlayerCell { + + private func vp_setupUI() { + contentView.addSubview(playerView) + contentView.addSubview(coverImageView) + contentView.addSubview(controlView) + + coverImageView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + playerView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + controlView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + +} + +//MARK: -------------- SPPlayerDelegate -------------- +extension VPVideoPlayerCell: VPPlayerDelegate { + + func vp_playCompletion(_ player: VPPlayer) { +// self.playerFinishHadle?() + self.viewModel?.handlePlayFinish?() + } + + func vp_playLoadingEnd(_ player: VPPlayer) { + + } + + func vp_playTimeChanged(_ player: VPPlayer, currentTime: Int, duration: Int) { + controlView.progress = CGFloat(currentTime) / CGFloat(duration) + controlView.currentTime = currentTime + controlView.durationTime = duration + self.viewModel?.handlePlayTimeDidChange?(currentTime) + } + + func vp_firstRenderedStart(_ player: VPPlayer) { + + } + + func vp_player(_ player: VPPlayer, playStateDidChanged state: VPPlayer.PlayState) { + if state == .playing { + self.coverImageView.isHidden = true + } + } + + func vp_player(_ player: VPPlayer, loadStateDidChange state: VPPlayer.LoadState) { + if state == .prepare || state == .stalled { + self.controlView.isLoading = true + } else { + self.controlView.isLoading = false + } + } + + func vp_playerReadyToPlay(_ player: VPPlayer) { + self.seekToTime(toTime: (videoInfo?.play_seconds ?? 0) / 1000) + } + +} diff --git a/Veloria/Class/Player/View/VPVideoPlayerControlView.swift b/Veloria/Class/Player/View/VPVideoPlayerControlView.swift new file mode 100644 index 0000000..59cce59 --- /dev/null +++ b/Veloria/Class/Player/View/VPVideoPlayerControlView.swift @@ -0,0 +1,299 @@ +// +// VPVideoPlayerControlView.swift +// Veloria +// +// Created by 湖南秦九 on 2025/5/22. +// + +import UIKit + +class VPVideoPlayerControlView: UIView { + + weak var viewModel: VPVideoPlayViewModel? { + didSet { + viewModel?.addObserver(self, forKeyPath: "isPlaying", context: nil) + } + } + + var shortModel: VPShortModel? { + didSet { + updateCollectButtonState() + } + } + + var videoInfo: VPVideoInfoModel? { + didSet { + + } + } + + + ///滑动进度条 + var panProgressFinishBlock: ((_ progress: CGFloat) -> Void)? + + ///0-1 + var progress: CGFloat = 0 { + didSet { + progressView.progress = progress + } + } + + ///加载中状态 + var isLoading = false { + didSet { + progressView.isLoading = isLoading + } + } + + var durationTime: Int = 0 + var currentTime: Int = 0 + + var isCurrent: Bool = false { + didSet { + updatePlayIconState() + } + } + + private var hiddenPlayButtonTimer: Timer? + + //MARK: UI属性 + private(set) lazy var progressView: VPPlayerProgressView = { + let view = VPPlayerProgressView() + view.panStart = { [weak self] in + guard let self = self else { return } + self.panProgressStart() + } + view.panChange = { [weak self] progress in + guard let self = self else { return } + self.panProgressChange(progress: progress) + } + + view.panFinish = { [weak self] progress in + guard let self = self else { return } + self.panProgressFinish(progress: progress) + } + return view + }() + + private(set) lazy var rightToolView: UIStackView = { + let view = UIStackView(arrangedSubviews: [collectButton]) + view.axis = .vertical + view.spacing = 28 + return view + }() + + ///收藏按钮 + private lazy var collectButton: UIButton = { + let button = createRightButton(title: "0", image: UIImage(named: "collect_icon_01"), selectedImage: UIImage(named: "collect_icon_01_selected")) + button.addTarget(self, action: #selector(handleCollectButton), for: .touchUpInside) + return button + }() + + private lazy var playButton: UIControl = { + let button = UIControl() +// let button = UIButton(type: .custom) +// button.setImage(UIImage(named: "pause_icon_01"), for: .normal) +// button.setImage(UIImage(named: "play_icon_01"), for: .selected) + button.addEffectView(style: .light) + button.addTarget(self, action: #selector(handlePlayButton), for: .touchUpInside) + button.layer.cornerRadius = 6 + button.layer.masksToBounds = true + button.isHidden = true + return button + }() + + private lazy var playIconImageView: UIImageView = { + let imageView = UIImageView() + return imageView + }() + + deinit { + viewModel?.removeObserver(self, forKeyPath: "isPlaying") + NotificationCenter.default.removeObserver(self) + } + + override init(frame: CGRect) { + super.init(frame: frame) + NotificationCenter.default.addObserver(self, selector: #selector(updateShortCollectStateNotification), name: VPVideoAPI.updateShortCollectStateNotification, object: nil) + + let tap = UITapGestureRecognizer(target: self, action: #selector(handleScreen)) + tap.delegate = self + self.addGestureRecognizer(tap) + + vp_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == "isPlaying" { + updatePlayIconState() + } + } + + func createRightButton(title: String?, selectedTitle: String? = nil, image: UIImage?, selectedImage: UIImage? = nil) -> UIButton { + let button = JXButton(type: .custom) + button.titleDirection = .down + button.space = 8 + button.setImage(image, for: .normal) + button.setImage(selectedImage, for: .selected) + button.setImage(selectedImage, for: [.selected, .highlighted]) + button.setTitle(title, for: .normal); + button.setTitle(selectedTitle, for: .selected); + button.setTitle(selectedTitle, for: [.selected, .highlighted]) + button.setTitleColor(.colorFFFFFF(), for: .normal) + button.setTitleColor(.colorFFBD36(), for: .selected) + button.setTitleColor(.colorFFBD36(), for: [.selected, .highlighted]) + button.jx_font = .fontRegular(ofSize: 10) + return button + } + + + + func updatePlayIconState() { + let isPlaying = self.viewModel?.isPlaying ?? false + if isCurrent { + if isPlaying { + playIconImageView.image = UIImage(named: "pause_icon_01") + hiddenPlayButtonTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(handleHiddenPlayButtonTimer), userInfo: nil, repeats: false) + } else { + playIconImageView.image = UIImage(named: "play_icon_01") + hiddenPlayButtonTimer?.invalidate() + hiddenPlayButtonTimer = nil + } + } else { + playButton.isHidden = true + playIconImageView.image = UIImage(named: "pause_icon_01") + } + } +} + +extension VPVideoPlayerControlView { + @objc private func handlePlayButton() { + self.hadlePlayAndOrPaused() + } + + @objc func handleScreen() { + if self.viewModel?.isPlaying != true { return } + + self.playButton.isHidden = !self.playButton.isHidden + + if self.playButton.isHidden { + self.hiddenPlayButtonTimer?.invalidate() + self.hiddenPlayButtonTimer = nil + } else { + self.hiddenPlayButtonTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(handleHiddenPlayButtonTimer), userInfo: nil, repeats: false) + } + } + + @objc private func hadlePlayAndOrPaused() { + self.viewModel?.handlePauseOrPlay?() + } + + @objc private func handleHiddenPlayButtonTimer() { + if self.viewModel?.isPlaying != true { return } + + self.playButton.isHidden = true + hiddenPlayButtonTimer?.invalidate() + hiddenPlayButtonTimer = nil + } + + @objc private func handleCollectButton() { + guard let shortPlayId = self.videoInfo?.short_play_id else { return } + guard let videoId = self.videoInfo?.short_play_video_id else { return } + + let isCollect = !(self.shortModel?.is_collect ?? false) + + VPVideoAPI.requestCollectShort(isCollect: isCollect, shortPlayId: shortPlayId, videoId: videoId) { [weak self] in + guard let self = self else { return } + var count = self.shortModel?.collect_total ?? 0 + if isCollect { + count += 1 + } else { + count -= 1 + } + if count < 0 { + count = 0 + } + self.shortModel?.collect_total = count + } + } + + @objc private func updateShortCollectStateNotification(sender: Notification) { + guard let userInfo = sender.userInfo else { return } + guard let shortPlayId = userInfo["id"] as? String else { return } + guard let isCollect = userInfo["state"] as? Bool else { return } + guard shortPlayId == self.videoInfo?.short_play_id else { return } + + self.shortModel?.is_collect = isCollect; + updateCollectButtonState() + + } + + private func updateCollectButtonState() { + self.collectButton.isSelected = self.shortModel?.is_collect ?? false + self.collectButton.setTitle("\(self.shortModel?.collect_total ?? 0)", for: .normal) + } + + +} + +extension VPVideoPlayerControlView { + + private func vp_setupUI() { + addSubview(progressView) + + addSubview(rightToolView) + addSubview(playButton) + playButton.addSubview(playIconImageView) + + rightToolView.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-15) + make.bottom.equalToSuperview().offset(-110) + } + + playButton.snp.makeConstraints { make in + make.center.equalToSuperview() + make.width.equalTo(70) + make.height.equalTo(50) + } + + playIconImageView.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } +} + +extension VPVideoPlayerControlView { + ///滑动进度开始 + private func panProgressStart() { + + + } + + ///滑动进度中 + private func panProgressChange(progress: CGFloat) { + + } + + ///滑动进度结束 + private func panProgressFinish(progress: CGFloat) { + self.panProgressFinishBlock?(progress) + } + + +} + +//MARK: -------------- UIGestureRecognizerDelegate -------------- +extension VPVideoPlayerControlView: UIGestureRecognizerDelegate { + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + if touch.view != self { + return false + } else { + return true + } + } +} diff --git a/Veloria/Class/Player/ViewModel/VPVideoPlayViewModel.swift b/Veloria/Class/Player/ViewModel/VPVideoPlayViewModel.swift new file mode 100644 index 0000000..517550b --- /dev/null +++ b/Veloria/Class/Player/ViewModel/VPVideoPlayViewModel.swift @@ -0,0 +1,48 @@ +// +// VPVideoPlayViewModel.swift +// Veloria +// +// Created by Veloria on 2025/5/22. +// + +import UIKit + +class VPVideoPlayViewModel: NSObject { + + @objc dynamic var isPlaying: Bool = true + + + var currentPlayer: VPPlayerProtocol? { + didSet { + oldValue?.isCurrent = false + oldValue?.pause() + +// self.currentPlayer?.playerFinishHadle = { [weak self] in +// self?.handlePlayFinish?() +// } + self.currentPlayer?.isCurrent = true + self.currentPlayer?.rate = rateModel.rate.getRate() + } + } + + ///倍速播放 + @objc dynamic lazy var rateModel = VPVideoRateModel(rate: .x1) { + didSet { + self.currentPlayer?.rate = rateModel.rate.getRate() + } + } + + ///设置进度 + func seekToTime(toTime: Int) { + self.currentPlayer?.seekToTime(toTime: toTime) + } + + ///点暂停或播放 + var handlePauseOrPlay: (() -> Void)? + ///播放完成 + var handlePlayFinish: (() -> Void)? + ///播放进度变更 + var handlePlayTimeDidChange: ((_ time: Int) -> Void)? + ///选集 + var handleEpisode: (() -> Void)? +} diff --git a/Veloria/Libs/LocalizedManager/VPLocalizedManager.swift b/Veloria/Libs/LocalizedManager/VPLocalizedManager.swift index 92e1725..f40124e 100644 --- a/Veloria/Libs/LocalizedManager/VPLocalizedManager.swift +++ b/Veloria/Libs/LocalizedManager/VPLocalizedManager.swift @@ -37,16 +37,17 @@ class VPLocalizedManager: NSObject { // 获取当前语言代码(如果用户未手动设置,则返回系统语言) var currentLocalizedKey: String { get { - var key = (UserDefaults.standard.string(forKey: LocalizedUserDefaultsKey) ?? Locale.preferredLanguages.first) ?? "en" - if key.contains("zh-Hans") { - key = "zh" - } else if key.contains("zh-Hant") { - key = "zh_hk" - } else { - let arr = key.components(separatedBy: "-") - key = arr.first ?? "en" - } - return key +// var key = (UserDefaults.standard.string(forKey: LocalizedUserDefaultsKey) ?? Locale.preferredLanguages.first) ?? "en" +// if key.contains("zh-Hans") { +// key = "zh" +// } else if key.contains("zh-Hant") { +// key = "zh_hk" +// } else { +// let arr = key.components(separatedBy: "-") +// key = arr.first ?? "en" +// } +// return key + return "en" } set { UserDefaults.standard.set(newValue, forKey: LocalizedUserDefaultsKey) diff --git a/Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Contents.json b/Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Contents.json new file mode 100644 index 0000000..cf16d81 --- /dev/null +++ b/Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Symbol@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Symbol@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Symbol@2x.png b/Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Symbol@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d8456c6e8f16fccc70a2924a8ed7ce643e245b53 GIT binary patch literal 292 zcmeAS@N?(olHy`uVBq!ia0vp^qCl+5!3HGP<}R`ZQk(@Ik;M!QVyYm_=ozH)0Vv2= z9OUlAurTHQGVk3w{?>Y@ieGh#tqdVASeAFJcpUxcHE+L!&lZ!z zQH=kZRqw@T+~vr%kuy6WcWV|)`p+pb2aUIU{PB#}j_X4(bI*k%hXa`B^s%}zeo`~l zcp)aSp!c}!+U^Z1=`VjTVfsBklkNB3zdU@thxE%MKBO0a*xQg>|Bpv*c6+^Q#BE-i l15(P@gIe9+e6jq+>ULjCNPha!W}qh+JYD@<);T3K0RU_=YpehO literal 0 HcmV?d00001 diff --git a/Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Symbol@3x.png b/Veloria/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Symbol@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..93bb34826a74486fe1ad51c862bb0ace4feadb10 GIT binary patch literal 410 zcmV;L0cHM)P)KrZQ^&9yHEIyF0!NOXe$$k4YL~AY2!D5zsM;M8~^|S literal 0 HcmV?d00001 diff --git a/Veloria/Source/Assets.xcassets/icon/arrow_right_icon_01.imageset/Contents.json b/Veloria/Source/Assets.xcassets/icon/arrow_right_icon_01.imageset/Contents.json new file mode 100644 index 0000000..5c4d3b1 --- /dev/null +++ b/Veloria/Source/Assets.xcassets/icon/arrow_right_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Veloria/Source/Assets.xcassets/icon/arrow_right_icon_01.imageset/Frame@2x.png b/Veloria/Source/Assets.xcassets/icon/arrow_right_icon_01.imageset/Frame@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fde74ffa1def85021e6db4125c5c9a535c6d7b59 GIT binary patch literal 198 zcmeAS@N?(olHy`uVBq!ia0vp^q9Dw{1|(OCFP#RYI14-?iy0WiR6&^0Gf3qFP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBezd2HV@L(#+w%uG4=C`oK6FDH4tWEc}yXTs}D;Xs%IdFh73AQSIcP4PG1itCtyA*09Tj|Cf6+wY|JyTh*rx ps*bapS#q29EK6`+tXYx6deP*X%;sIC&w`2( literal 0 HcmV?d00001 diff --git a/Veloria/Source/Assets.xcassets/icon/arrow_right_icon_01.imageset/Frame@3x.png b/Veloria/Source/Assets.xcassets/icon/arrow_right_icon_01.imageset/Frame@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..425bc1a2c052d97642f00a7842f4bf4c71519f7c GIT binary patch literal 247 zcmeAS@N?(olHy`uVBq!ia0vp^@*vE?1|rvqSpq4}0*}aI1_nh75N33pW|#mJWGoJH zcVbv~PUa<$!;&U>cv7h@-A}f&J5v`B1RBr;JZY40EQg(5-s{P|Rw9x$fqEh*~i_3*Zf_%4v*vFL?m>&z<| zvJa-#-VHL2pOMeNC;&tb!Vk_Zs9gRt&h*N`+C>s4roy{ez2pTAFN ldDpya^Sg`JsV5v@%YD9n(YHOT#DMN%@O1TaS?83{1OOnkQC0u| literal 0 HcmV?d00001 diff --git a/Veloria/Source/Assets.xcassets/icon/arrow_up_icon_01.imageset/Contents.json b/Veloria/Source/Assets.xcassets/icon/arrow_up_icon_01.imageset/Contents.json new file mode 100644 index 0000000..5c4d3b1 --- /dev/null +++ b/Veloria/Source/Assets.xcassets/icon/arrow_up_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Veloria/Source/Assets.xcassets/icon/arrow_up_icon_01.imageset/Frame@2x.png b/Veloria/Source/Assets.xcassets/icon/arrow_up_icon_01.imageset/Frame@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f4a9ea465edb988505b1b4dc249cf0200cd6f22e GIT binary patch literal 250 zcmeAS@N?(olHy`uVBq!ia0vp^B0wz4!3HGVuAW-~q&N#aB8wRq#8g3;(KATp15l8$ zILO_JVcj{Imp~3nx}&cn1H;CC?mvmFKz_fci(^Oy)_ia)U~w;S za!(L-D`3`HCVyXVd9t%2{7;<`Dc{JJNMA)Vi zp^v&m!YViKC<~g|?0QdS-|l;xHpKm2{o}3g+I&CTpxoHW5z@=nE*DgsW+$b3_1k5S zm1@t+^Bz1BoqNgY<;3^XQnWqHRwbpqo@#fZQ2W`T*5hX$-&fvyO0~S{am_5*2I*~A UT<@Lh0=k*O)78&qol`;+0A?p^KL7v# literal 0 HcmV?d00001 diff --git a/Veloria/Source/Assets.xcassets/icon/close_icon_01.imageset/Contents.json b/Veloria/Source/Assets.xcassets/icon/close_icon_01.imageset/Contents.json new file mode 100644 index 0000000..5c4d3b1 --- /dev/null +++ b/Veloria/Source/Assets.xcassets/icon/close_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Veloria/Source/Assets.xcassets/icon/close_icon_01.imageset/Frame@2x.png b/Veloria/Source/Assets.xcassets/icon/close_icon_01.imageset/Frame@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..650dda2c94b83b9fc82a2c13cf9c87a4cc04ae86 GIT binary patch literal 500 zcmV1#6;}p^ zkp;*ZKr+w;$1wpp0y2#W$61jv5zq;TIxCK22F4fSTsU;$QD9h~LFsKxV=gc(Z?QO) z#MZ6A(9VOS^PFcDq*+OdCSfmR?OI&5LUVa_T>c zTUA0zWS9%5MTR+X8f2IoN0msaSh#PEPYF%Ud qi{ELQ2V?ShJnkpP{G}yJ_9z<~?kSuOiMpNu0000()pAm$yF4aNE~cI}KA z=g$4gf)x$Ca+Tp@LahC_yP&cRUC#;6N7|~A_OXn`a}nyp3BLr|tB_`7!9LWF6Mmxd z8u+ZJ^by%NDw|N>;G*RRKB4M?%30tN^Mx!zb{$%<04wY#s75+h7917!6BU=1kOfh~ ze!^nXQnDag*iUROEh!6$pSH9Fh$x@7KjRi6lrT&ATA7Iqm^>5leT}v(P#5+fZ#-u{$6c^=23XwE+`pc`sAo>J z7p)Z89#v_zTRLJ>Q6vSdA{?`W^2JShA&$z0^{TY)`KAj2Rejv{fYw201eBXt@aW z=gz~|f%amuJrUAZp}w5(ex$9qY)^#rD4Z~U2k(QwtC8&qNTcmB*vB!@Zvm-QEDk3$ s_|8=f#d_&}(!I|$eJPDbqtQs=3#Ee5O2=&ZPXGV_07*qoM6N<$f?hr|#Q*>R literal 0 HcmV?d00001 diff --git a/Veloria/Source/Assets.xcassets/icon/collect_icon_01.imageset/Contents.json b/Veloria/Source/Assets.xcassets/icon/collect_icon_01.imageset/Contents.json new file mode 100644 index 0000000..6a4d508 --- /dev/null +++ b/Veloria/Source/Assets.xcassets/icon/collect_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Vector@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Vector@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Veloria/Source/Assets.xcassets/icon/collect_icon_01.imageset/Vector@2x.png b/Veloria/Source/Assets.xcassets/icon/collect_icon_01.imageset/Vector@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..bfdc8767c7c3c7d30e99687ac32f14204f51b549 GIT binary patch literal 820 zcmV-41Izr0P)Xb^O-Lx6|5H`D0BSME(u5k zaAtPMkuW6p;^H9?0um>~ks+dUKHP8=9J&3t;ke^iW?9xCRyg_Z92F7WMu8;m1d7m&W^ zKZy%`I^Y^lGrci{=K_bd@7)Ey9N?<jCy* za*mlj`l|gt4cfJXHOJN*c@1HUfgBXzl_Wp+&ZqK=Z6^d;asKwsn?vHf5P;g>;#X)_ zY`Ww_rW41+0}Hb3f4LXHv|m2lqD?$z8jf$>(%Vh|oBRsXv9xa*SP?n_YJ_ifEe*%z z2Ts1^lM?A+-2Tr@gszb;SDBeyAf2}VoUIXnkT9kRG)R}V5#Ryovd-+xZ-F$qII~m3 z-$;-7ej;RGC;UM=L*gALUz(Hsf{6WKj`*2($-fFap`v)HAc5i(L6&@^B2*Xx#UVn4 zF;LhdRB!=>DMAI8P*@^V@Bv@=7fqi^lauPb0JUp}HKxraPjuGO!rE73Ii^ppDXbM> zhUt@Q3Tp-Uis|!3Q#i&ZpmM3U6AWSfaLb+GCuw1^1332(+P~r4j#eTxO&~`&h$~AY zKqw9{+zU(}KyFyP*Pqz?)Z_-YuH_4^XgCW}bfjFiq;W#5 z;2Ij{f|^GB5pskO1$;pAg;BGow}c~_S$dV+k9R?$>7b>)LOgX y_{i}e?N-$LF&meK_ch|0d(V3JzJI<2R>~tCM1wpxoGIG?0000;fC>l|5Go*4039TB02LfmkWhg|1ql@_TAu_*Vv{>br`_cC zo6*=woEhIfz0+D+S%4wR0(TH1TKb2TZ_Ds2=i9|%vABXU9X{|!zO}R;{@W3ZviP7r z^X;65v+=Egk&h4L%C}1z5C6|I80l~?rnKPqMQC885pE^9oWmFobpviGj-eE_X{Zm{ z#y>PDgYzls0Tl883ZX8QY*3Ddl%}UD9ZxRJlNn11j|Xh$$B@1Ai802`) zx1dYd2Q4YCSO9_{2H%2gYo{DnEWiZ}DYl;jzQd5>2af@q1=&V684ezV*|{$;YIDXb zZT-0*c7dg%b#T6|S%B?aP+0TissPJ~riWwvDy}pCvmg`31D+Q$Crkqef8Ae{*|j2O zB22@L;4?edgiVCv>9wuXGYbAnh3YzFe3QO=;B-xVJ{9NgU=*YN1F&Y za7YL~lU$x|BK*N2G5AdK4w>Er-kev|_}UM1HgJ^Kpuy7>JeA#GUO05Y&zeEb-Xj*? zzz58a4~qidRb6V6p2PfbC_pIz8qALd1t=xJ9LpPvTyrS_o??D9D1bR2og$sCR2q7T zdGc614aL06+Z$wJ0_2f>?Ii?gFke&%Fvkoil6~zZ2H`pHu(|Ss@MC0<<$zE>H0Ro( zWyk`P$-M z@ISwSTy;irEOG>>T@lw0{K@r|sPMg<>+l+fx;WH^x|6PJz=txQjG6SJ?jYzhs)tNM zLS+tI-pA;`?|6STgpm854Jv!+a-UNwcoaEJE5s2b@o%7|a1kZ1w?iRYC@w94!--o(Eb2IP)R#3tSAviHOMnQf9 X?TJp_jm5v000000NkvXXu0mjfwu3AP literal 0 HcmV?d00001 diff --git a/Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Contents.json b/Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Contents.json new file mode 100644 index 0000000..5c4d3b1 --- /dev/null +++ b/Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Frame@2x.png b/Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Frame@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..80037c240f9e17401a647303e071629a84f543bb GIT binary patch literal 3157 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 literal 0 HcmV?d00001 diff --git a/Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Frame@3x.png b/Veloria/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Frame@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..2f08eaacf1200340bb9674b039c4cc8751eb12fc GIT binary patch literal 6041 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 TopXa&00000NkvXXu0mjfwwaz1 literal 0 HcmV?d00001 diff --git a/Veloria/Source/Assets.xcassets/icon/ep_icon_01.imageset/Contents.json b/Veloria/Source/Assets.xcassets/icon/ep_icon_01.imageset/Contents.json new file mode 100644 index 0000000..5c4d3b1 --- /dev/null +++ b/Veloria/Source/Assets.xcassets/icon/ep_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Veloria/Source/Assets.xcassets/icon/ep_icon_01.imageset/Frame@2x.png b/Veloria/Source/Assets.xcassets/icon/ep_icon_01.imageset/Frame@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f8ce65971fa0fa8220db5be78aca79dc72f098ae GIT binary patch literal 400 zcmeAS@N?(olHy`uVBq!ia0vp^GC(ZL!3HFE2J#pJDb50q$YKTtF;x&|^bAt@02E{_ z4sv&5Sa(k5C6L3C?&#~tz_78O`%fY(P)^m;#WAFU@$IyH-a`fgN2Ns+5``uNaC#hI zEM^cYFpyDL2;|D}u=i`fJ`}d@_57`z@)ajlPnT8hU%UHWXvh9KM}}=`F1e8xIu|X! z@NJji(x(kEk_FmcooD#2nW#)I7TD>nE$&wSS;>Tn#+$WFiaQ(;&1CYVf)z4*}Q$iB}sMwPM literal 0 HcmV?d00001 diff --git a/Veloria/Source/Assets.xcassets/icon/ep_icon_01.imageset/Frame@3x.png b/Veloria/Source/Assets.xcassets/icon/ep_icon_01.imageset/Frame@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..ba0616025a692cfe5e5cc43e1e6a25eeab48d9a9 GIT binary patch literal 526 zcmV+p0`dKcP)smslGI?7i;LdT2HDcPnu2U z;J7x0zcGh$qi(~aUKX6 ztW&^_x-HCu&*1uUhk$nj3gAHi-{?r7+Sqt}#_PbTK+%F_9%Y|X3i)Q}5%J3lb-nv& z7g!hq(5H^+4CL*j0@&+W4*Mn2#B5~vU0W0QBN5n=Z!ix99Q;k)b$9EdwwvY}P{-EZ zI?9z;um<*qePdQ&EEw`cly{;}1eXUIz^;kEEO3K0{Y9wy;Jw2l<(}$-}Gc Qw*UYD07*qoM6N<$f+myW1^@s6 literal 0 HcmV?d00001 diff --git a/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/Component 20@2x.png b/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/Component 20@2x.png deleted file mode 100644 index b9e80c274e24d1de1490d96820879524af75964b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1543 zcmV+i2Kf1jP)Xbp-`x0Ik2Tbk#<5hC#8%;$7XubK7heem$D^gTV!rEJ1|vXB<>9u zVS7>@O4-L?!P+(?r3B;YwuszK9`uYrk@t#hPD!aqd4vF=MApKJNWAMl5ECf!UW*Rg zz~EHZtS}-^OW6^Lx9!fNy%9xTkk=eGVRUmMa@C+APw|=t&UhNf78s5a>w|=t&Uh$qVQ$m?D~x zGA?BlCY5es24p=dtLTyJQ7;Ijq!}sGFw$Bv>D*qEvLfXdbdiGOw9d-*7V^i6`w;O+ zbJ>kbE3r(oFQGdk$GRnu@A(DdVr&)t9VWUq5U-|`JRX%Pn5yo==PX3e%ZsiEyddSg zbDxsRQkKzPyEYoV*0i&a@+ff)`Jg87B23j>gpp&JMf?NvF;+jH4uK-)5RYa`T!B7g zTBkA7i7j!Xg~IqD7vbNVGU~p9iC_!As0rkNmO$GM*{GPoLA9l@D>x)VPX6}o)!|>i z`T2uCzn?-ju1UG>+`ojwdv$NL1g=M0HvEi)Po2l+oySu-zjouyUadAODc{Mz_qRVm zcGw&&JMVo7%4iEDbIxr>h;{>i&3O;@*fUT>y9y`wEk(C^#c>vTuzj^{E1QlIl+d~o z`7dQ3KhT$mw@Vay9i4|?7OS$AH>31Esu7s%UG4R)i&|vBoFdu3VG>GcUJLUZznIuZ zvFp*6^XxRw)$Emxo1EzQRf?uSR-v0PAHYh>JA<-+ z{IVn!7XI3)OzqWcr?Ncfvsd5#A!WDiJXA8(J>3Q_O?=EMnW_D2g4^@1LxFzph24b3 zDir853M!ZcT zF!F9f32lKqd9_*ST*fG1(pGNt%|nqq+Q2dn+8;*61?a&q$sHpuL#gwyELJ3EW@X8ma9ETl$;qj4>}?m2-_SwVrnjZdtg>)W+|ERGr-+oI ztsNwCp)d|&EhAsXx;ep;v=W(xqfU+M6kQi+p-8rmX%i=txDzo~(A~MxJtNS?&O|LC zeUf>eGf)p>tQYfv6qVNuQ*XlmiFgtOdJ+VB5(Ih@1bPw#dJ+VB5(Ih@1bRY&T*7Ej zR_E;`TywEXfn2$52!VEC&^@9+UV8?C?ZZXewkVLR^)ehodzc+YA2UU}#c0KDE@?S} zKvw8#Q`}W;)Nx_eA%v=&1v+9Hu#uN+2#lhTNmZ_s9D3yDu&JY?{~3MUkz9zI>#4pD zQ>DYAv~h-Es_tEpw`q5#Ed@`@Z%Luyur#u%krc;j%gBo@*xU5i97H4sdN?=P{vZK` t*3tjVh1#F}VBx6>#j%gLpY{_OPXL{dc~~f&{`~*|002ovPDHLkV1fXp-@^a^ diff --git a/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/Component 20@3x.png b/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/Component 20@3x.png deleted file mode 100644 index 111dd77d9a168da944568b4e63c4a69e59edcf77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2243 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 A?*IS* literal 0 HcmV?d00001 diff --git a/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/观看历史@3x.png b/Veloria/Source/Assets.xcassets/icon/history_icon_01.imageset/观看历史@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e939db7baf7d961782e05b7d7f02014e98ec9aaf GIT binary patch literal 2298 zcmVcq+CqclR`8ma5Cc23kN)!aP;ccgBPzTF$NPaqNgQt z0Z)QN6MGOt5rZtHw3hC+yS(qtd)?{Vnf=$<3Dg{-xkuWUQRH>=5s>(_(ms{%RmvjA`tmH!I9#!_L0?`ds*7X}$Ct+BO z>4MK6&g!Z_bVUiTb^GO>B%yZkO-_!H<;a<){-oN>+jqz56Lh#ZM`SV1XBqUDQWJ^ke&kSjkH8 zwEks?T_A~G?o3gT!rMeg)`*p0qyY$Cp&)_<{=gIJOGV|cu0%gEKgio>>KgP61u6O& z2`+(37&7mE<*{I%%txGffPyQcLXx6g1h-ZM=t1+uCdtY^+Q3NFOC;@HS3zJRbr*ES zxh;}W*@D~^5?rRL3+h!Z$+|s^idbJ=W&&H_F|WMt4oC7N1@~f!;%tCOhR?lW$va6p zJF*4>w$PNUn@vd7A~{>YXWm5exIT6r(GZ-X>C;FyJzvsn$`(zZb|j1B43Z}y^N3^* z5Xl}Ol085qdw@vx0FmqgBH05(vImG{4-m;7Ad)>mBzu5J_JFLfDaP#6BdW|&u!O`M z!(e{-iayTi<9*5yS(JQKm1DXTdpJLQ&MAE~!f&baCpk`Y!CPo?+e2~_MCVBg#2%q! zf$(b-$gYrk%w$1wNtM&oo`#bKg0HJmCbw`}GVZVEsC(qjQ;>SGoU=l$doU=5{?@cU zcKdty{mc4qx`4BiORAjh@N)>bsmdMdf3W~E0@D6*eT)Ms{0HKXbCMwd(fdZs&rwmr z2cOiv6Q9YM4zGtS@g?!TSF@3f*Dg_zE+J_y=A4i8IW{;SDRfvLb|=|*&CiLiXf~41 zQ|kjndi|6(r&+wWi`3pE!a2AouG471dpr~IJ~)e>;nL$3uI!jji|}JaaMTC;8I4Xb z!0jMPX52-{0xpv0t;f@$?{N>FjW{2I`-+@Hzg;K9Av)l22)|J?X^{^k@74qh7-u3Ogg$Vlgi8C)Z| zsLN^Vd0Y(maQj|Ey|7l@h_VLJ)VELWy-Qo?Dz(3Nbd4t6>$WAhNVW$A5Xl#<=OC6_ z<;Fy`{hn|^4Z8=fXSk+o6UkwcZ2u;S=>ZF4nuILiBH0Y}#eqtWU5gHHBAPa4#3~HZ zHjDSiX{ZtRwzwt;KEx6QX?#|nXFkC1h{vD-?$783HSY_@5;5rdI^=0h^`vPLq+r zbxQ6WwPsGb1_awek5Y=ryzAj$OP-|^5y`{BmYiy*h)5m|QGM~LA(Dp!t6IOe|9Y=d zdw%Kg-EW>wq**ngAxQ25B^wNywKz6sWt_@k^JDq`Uu3ua{6Q9Q`()1kmsGM1d!wNj*Qj*GnGR$pEtCU8#I=mxYWs$V2Vs|6{3 z7X(go!C6a22ZCLzHrqCE5siTkdumVI#x|+Jz2b%Dlf7EdtX)nsb>kv~V7uc37bcrD zZE&G>9krT2P0=Qs*cC0a3j#Y_kQ}#~A0PH7G}^UBNA+vkDVp_OUM`I%ciOBi>7DqZ zUK<;}M6GL*&Ds)X)Y~qah2(L&2B3m9&_*>Q#Mup0(Jt-O3l`pxykFsviN14ONS(S< z%M|Q#z#XTzLhf}JJhK&YcwEstzCnnwo^7c8r&y$ z7r)1n^~v={9*c3^w2(Po9w3rEKqPyBNcI4c>;WR#14Ob1h-42C$sQn*JwPOTfJpWL zk?a9Ra@~3?WC$a5T_1BKWDZ-hL~^zm#}^~Ds*l@A$P#1t(v+Ob{g|wye`_Q8UH;E z_IHusGRVe==jH_Ub1Q&s~jVQ(wQI)KJsk!?;?IWp#xyT+0L~rcQ&?Zqg zr?6ZG&8bNeTmpj8Yb;2?Q_h;mGiD>@7&lClf=Eut4LimRr`>Gp_#$I(e zKtZ=>g<9R80w0i?5s1jV5ZtzotLuP-5{>Xbr2hyiWoS=}?DRqOB63m5!YJYcU2$%s zr%@**8h;S00r%;rOf9@h!l*&h2tAXymBQZ>RVqqwJ?ooID=3$j~2oetT+Z9KUTtVB_UN5^c#@Djp2d8BgkB(;C8v%DROOI05=Ca UIGHxlFaQ7m07*qoM6N<$g6ox7`v3p{ literal 0 HcmV?d00001 diff --git a/Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Contents.json b/Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Contents.json new file mode 100644 index 0000000..a2f740e --- /dev/null +++ b/Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame 76@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame 76@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Frame 76@2x.png b/Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Frame 76@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0cdc78dafd6b682e7770403a6e1f0c77f1fa3e74 GIT binary patch literal 305 zcmeAS@N?(olHy`uVBq!ia0vp^hCpn4nJh^c}wqi2xH2cRHh zage(c!@6@aFM%AEbVpxD28NCO+9W^>bJmn zc}oeCY);Rc)yXH<995a6u2{&>zt4i{1i!)N8SAI4zuWb>?qEvZ?w?Dq-U{=1w5>WU zI=;pD`rThYtAytCzH2*{*tSo(OkCkWwqVe;p=`eY~6b~=Ie(4 R{6HTuc)I$ztaD0e0szI#cf0@q literal 0 HcmV?d00001 diff --git a/Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Frame 76@3x.png b/Veloria/Source/Assets.xcassets/icon/pause_icon_01.imageset/Frame 76@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..c1d4372649de51dd8fb565d96f732deee2d3f11b GIT binary patch literal 438 zcmeAS@N?(olHy`uVBq!ia0vp^oEEaXQk(@Ik;M!QiWVTu=sL|X0Vv2= z9OUlAu_tL>h~klx_1C*|r~Fr$AGXas{?=+`m9rYg?@v|fy`Ex!?{t-I zeK%+6+?_H%CY1UcPF!a^pWiF5DIW~ESn>Ky>SMj@w~sQ^ z8-DhEdhU0``qcdHv+3(xclCaa`nK&x&9f+#)`Vw;4A9~w<)7Ea~l1wl&pke zKt>hA$XII%g*^N;qCUuin8ENUn3SwHni90HLcYwTGzD;5jfQZ9$0@e7w6ADfHmnk! z@}>2SEki}qvSWRC-2osBC4A-!!cwB6nOLS%8wKmdWr{5+UJ`Ol{+J`tq+)GtQHePc zP1da377;Bbgf%N`yP(O0kf3Foug!$;Y&K{#k(doyR;M#~l{)LU#z>$^Plryr3l#1z8LTv74x<{-&@R5FRK_q{9c+{#xg+g0SpS zmk*qYDG~)aevX>NkP`*fNxRi}WFx1HDUAjEorv2yzeC4)-^au!#-Vf44l$9yFF8U~ zk5Hdn59NrEr7wQ?2K|rt<-Tc)DmHo;nI>twg1Ua^lIR%%6&juYQ6N#_pOp1P^+A>( i+v9HFHSj literal 0 HcmV?d00001 diff --git a/Veloria/Source/Assets.xcassets/icon/play_icon_01.imageset/Frame 75@3x.png b/Veloria/Source/Assets.xcassets/icon/play_icon_01.imageset/Frame 75@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..be533bf1062cbe3a8e6db164c523015df8e26409 GIT binary patch literal 772 zcmV+f1N;1mP)BHGC{L}(+T7anodA&kZy3j14XC;BvCI)B6%P1 zBY6`Hk~YlmcR-5~Ofh6y+Xb0hbr_KR+?_zCu&FYj6ot7Mf+Y zCJICAD>O@C3sf+&&~FzFp~Q`s;9cJ!nln!Z+mcfhZdmSjj`0lkLjgwCX>D09NqfA8 zIa7EE7O)a7NqZ%u@bXh@$O=`GB?BYt>T?N(F3Iu;9?LxzNtS}bZ@^EmAhAhe_yfQ5 zl6@7UBufT$x#uv0m?c>r3h5J^q)wu2(6N!E5yBsG!c zHWiW7M7JFjNlkRyL6KCWyMDGPl4@>U10t#BscS$a8D40q>8?SMWXk&7_kh$TY06ic zMSuGql(HlZMv!(rFjYw}l!qaLm`{)vH_Fr4Z5*jcdZ+xDy2mB<<;I1~avtVv0*ZAe zu^jH5XV;l)2r)`}1=Bkk79u7|H<(b$Lx@3=rhN0c@i64#wx#^V_s&e$OqyMK9$2)L zhv1T=VPCv~nLQfz0-CL)d!WRbKk4&I^#Zp-;fNnw`C>?M|AO`5uHjD#`AYc>LIBs@ z&h?BP-^uS9deJ%hr^E>M;hxbCCGNSD$Fi^}g)!b6fLTD)NA*9#>zy&6+2MV~@q}5* zbEss!V*lB%41MOzzBP~q>stg__*zG=_5Ml__Rl{J$y*nl15W4w0000 #import #import +#import diff --git a/Veloria/Source/en.lproj/Localizable.strings b/Veloria/Source/en.lproj/Localizable.strings index d760936..f0cdf23 100644 --- a/Veloria/Source/en.lproj/Localizable.strings +++ b/Veloria/Source/en.lproj/Localizable.strings @@ -10,6 +10,8 @@ "All" = "All"; "Drama Champions" = "Drama Champions"; "Explore" = "Explore"; +"EP.%@" = "EP.%@"; +"All %@ Episodes" = "All %@ Episodes"; diff --git a/app账号信息.txt b/app账号信息.txt new file mode 100644 index 0000000..2080a9d --- /dev/null +++ b/app账号信息.txt @@ -0,0 +1,13 @@ +湖南秦九开发者账号信息 +认证人 :张文祺 +认证设备 :Mac Mini +手机型号 :MU9D3CH/A  +设备序列号:K73J20237W +手机号码 :18173178983 +企业邮箱 :app@qjwl168.com密码:1q1w1e1r +D-U-N-S :616751820 +公司主体 :Hunan Qinjiu Network Technology Co., Ltd. +公司地址 :9010, Xijing Apartment Sanqi Shopping Plaza, No.383, Jinxing M. Road +认证官网 :https://www.qjwl168.com +账号 :hn.qinjiu.developer@icloud.com +密码 :Discover2024 \ No newline at end of file