From 6a54c45a7cac37bb4c1568cbf66b4952ce236e32 Mon Sep 17 00:00:00 2001 From: zjx Date: Sat, 21 Jun 2025 10:53:52 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B8=85=E6=99=B0=E5=BA=A6=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Veloria.xcodeproj/project.pbxproj | 36 ++++-- Veloria/Base/Networking/API/VPVideoAPI.swift | 6 +- .../Class/Login/View/VPLoginContentView.swift | 3 + .../VPDetailPlayerViewController.swift | 11 +- .../VPVideoPlayerViewController.swift | 20 ++-- Veloria/Class/Player/Model/VPShortModel.swift | 28 +++++ .../View/VPDetailPlayerControlView.swift | 56 ++++++++- .../Player/View/VPPlayerRechargeView.swift | 8 +- .../View/VPRevolutionSelectedCell.swift | 86 ++++++++++++++ .../View/VPRevolutionSelectedView.swift | 107 ++++++++++++++++++ .../ViewModel/VPVideoPlayViewModel.swift | 46 ++++++++ .../Controller/VPStoreViewController.swift | 7 ++ Veloria/Libs/Login/VPLoginManager.swift | 3 +- .../icon/mark_icon_01.imageset/Contents.json | 22 ++++ .../mark_icon_01.imageset/Group 79@2x.png | Bin 0 -> 1227 bytes .../mark_icon_01.imageset/Group 79@3x.png | Bin 0 -> 1737 bytes .../icon/mark_icon_02.imageset/Contents.json | 22 ++++ .../mark_icon_02.imageset/Group 78@2x.png | Bin 0 -> 2869 bytes .../mark_icon_02.imageset/Group 78@3x.png | Bin 0 -> 5351 bytes 19 files changed, 431 insertions(+), 30 deletions(-) create mode 100644 Veloria/Class/Player/View/VPRevolutionSelectedCell.swift create mode 100644 Veloria/Class/Player/View/VPRevolutionSelectedView.swift create mode 100644 Veloria/Source/Assets.xcassets/icon/mark_icon_01.imageset/Contents.json create mode 100644 Veloria/Source/Assets.xcassets/icon/mark_icon_01.imageset/Group 79@2x.png create mode 100644 Veloria/Source/Assets.xcassets/icon/mark_icon_01.imageset/Group 79@3x.png create mode 100644 Veloria/Source/Assets.xcassets/icon/mark_icon_02.imageset/Contents.json create mode 100644 Veloria/Source/Assets.xcassets/icon/mark_icon_02.imageset/Group 78@2x.png create mode 100644 Veloria/Source/Assets.xcassets/icon/mark_icon_02.imageset/Group 78@3x.png diff --git a/Veloria.xcodeproj/project.pbxproj b/Veloria.xcodeproj/project.pbxproj index b524f58..b60b857 100644 --- a/Veloria.xcodeproj/project.pbxproj +++ b/Veloria.xcodeproj/project.pbxproj @@ -146,6 +146,14 @@ BF5E75D32DE5692D00DE9DFE /* JXBaseAnimatedTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5E75C12DE5692D00DE9DFE /* JXBaseAnimatedTransition.swift */; }; BF5E75D52DE56B2000DE9DFE /* VPHomeCagetoryRecommandContentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5E75D42DE56B2000DE9DFE /* VPHomeCagetoryRecommandContentCell.swift */; }; BF5E75DB2DE5B8B700DE9DFE /* VPMarqueeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5E75DA2DE5B8B700DE9DFE /* VPMarqueeView.swift */; }; + BF692AB52E0644B500A5C2DA /* VPRevolutionSelectedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF692AB42E0644B500A5C2DA /* VPRevolutionSelectedView.swift */; }; + BF692AB72E06450C00A5C2DA /* VPRevolutionSelectedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF692AB62E06450C00A5C2DA /* VPRevolutionSelectedCell.swift */; }; + BFA21D982E01477200B3573D /* VPStoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA21D972E01477200B3573D /* VPStoreViewController.swift */; }; + BFA21D9A2E01497F00B3573D /* VPStoreVipBuyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA21D992E01497F00B3573D /* VPStoreVipBuyView.swift */; }; + BFA21D9C2E01628F00B3573D /* VPStoreCoinsBuyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA21D9B2E01628F00B3573D /* VPStoreCoinsBuyView.swift */; }; + BFA21D9E2E01684E00B3573D /* VPStoreCoinsItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA21D9D2E01684E00B3573D /* VPStoreCoinsItemView.swift */; }; + BFA21DA02E01688D00B3573D /* VPStoreCoinsBigItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA21D9F2E01688D00B3573D /* VPStoreCoinsBigItemView.swift */; }; + BFA21DA22E01696700B3573D /* VPStoreCoinsSmallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA21DA12E01696700B3573D /* VPStoreCoinsSmallItemView.swift */; }; BFCCE10D2DF951F600EDE165 /* SceneDelegate+APNS.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFCCE10C2DF951ED00EDE165 /* SceneDelegate+APNS.swift */; }; BFCCE1122DF9638B00EDE165 /* VPVipAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFCCE1112DF9638B00EDE165 /* VPVipAlertView.swift */; }; BFCCE1142DFAAC0900EDE165 /* VPLanguageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFCCE1132DFAAC0900EDE165 /* VPLanguageViewController.swift */; }; @@ -162,12 +170,6 @@ BFCCE13D2DFFFAC500EDE165 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFCCE13B2DFFFAC500EDE165 /* NotificationService.swift */; }; BFCCE1402E000ACE00EDE165 /* VPAnpsAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFCCE13F2E000ACE00EDE165 /* VPAnpsAlertView.swift */; }; BFCCE1422E000F4800EDE165 /* VPHomePlayHistoricalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFCCE1412E000F4800EDE165 /* VPHomePlayHistoricalView.swift */; }; - BFA21D982E01477200B3573D /* VPStoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA21D972E01477200B3573D /* VPStoreViewController.swift */; }; - BFA21D9A2E01497F00B3573D /* VPStoreVipBuyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA21D992E01497F00B3573D /* VPStoreVipBuyView.swift */; }; - BFA21D9C2E01628F00B3573D /* VPStoreCoinsBuyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA21D9B2E01628F00B3573D /* VPStoreCoinsBuyView.swift */; }; - BFA21D9E2E01684E00B3573D /* VPStoreCoinsItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA21D9D2E01684E00B3573D /* VPStoreCoinsItemView.swift */; }; - BFA21DA02E01688D00B3573D /* VPStoreCoinsBigItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA21D9F2E01688D00B3573D /* VPStoreCoinsBigItemView.swift */; }; - BFA21DA22E01696700B3573D /* VPStoreCoinsSmallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA21DA12E01696700B3573D /* VPStoreCoinsSmallItemView.swift */; }; BFF5AFA42DE6F15E0044227A /* VPMeVipCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF5AFA32DE6F15E0044227A /* VPMeVipCell.swift */; }; BFF5AFA62DE700420044227A /* VPMeVipPrivilegeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF5AFA52DE700420044227A /* VPMeVipPrivilegeItemView.swift */; }; BFF5AFA82DE704DC0044227A /* VPMeCoinCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF5AFA72DE704DC0044227A /* VPMeCoinCell.swift */; }; @@ -418,6 +420,14 @@ BF5E75C92DE5692D00DE9DFE /* UIViewController+JXTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+JXTransition.swift"; sourceTree = ""; }; BF5E75D42DE56B2000DE9DFE /* VPHomeCagetoryRecommandContentCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPHomeCagetoryRecommandContentCell.swift; sourceTree = ""; }; BF5E75DA2DE5B8B700DE9DFE /* VPMarqueeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPMarqueeView.swift; sourceTree = ""; }; + BF692AB42E0644B500A5C2DA /* VPRevolutionSelectedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPRevolutionSelectedView.swift; sourceTree = ""; }; + BF692AB62E06450C00A5C2DA /* VPRevolutionSelectedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPRevolutionSelectedCell.swift; sourceTree = ""; }; + BFA21D972E01477200B3573D /* VPStoreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPStoreViewController.swift; sourceTree = ""; }; + BFA21D992E01497F00B3573D /* VPStoreVipBuyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPStoreVipBuyView.swift; sourceTree = ""; }; + BFA21D9B2E01628F00B3573D /* VPStoreCoinsBuyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPStoreCoinsBuyView.swift; sourceTree = ""; }; + BFA21D9D2E01684E00B3573D /* VPStoreCoinsItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPStoreCoinsItemView.swift; sourceTree = ""; }; + BFA21D9F2E01688D00B3573D /* VPStoreCoinsBigItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPStoreCoinsBigItemView.swift; sourceTree = ""; }; + BFA21DA12E01696700B3573D /* VPStoreCoinsSmallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPStoreCoinsSmallItemView.swift; sourceTree = ""; }; BFCCE10C2DF951ED00EDE165 /* SceneDelegate+APNS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SceneDelegate+APNS.swift"; sourceTree = ""; }; BFCCE1112DF9638B00EDE165 /* VPVipAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPVipAlertView.swift; sourceTree = ""; }; BFCCE1132DFAAC0900EDE165 /* VPLanguageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPLanguageViewController.swift; sourceTree = ""; }; @@ -433,12 +443,6 @@ BFCCE13B2DFFFAC500EDE165 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; BFCCE13F2E000ACE00EDE165 /* VPAnpsAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPAnpsAlertView.swift; sourceTree = ""; }; BFCCE1412E000F4800EDE165 /* VPHomePlayHistoricalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPHomePlayHistoricalView.swift; sourceTree = ""; }; - BFA21D972E01477200B3573D /* VPStoreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPStoreViewController.swift; sourceTree = ""; }; - BFA21D992E01497F00B3573D /* VPStoreVipBuyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPStoreVipBuyView.swift; sourceTree = ""; }; - BFA21D9B2E01628F00B3573D /* VPStoreCoinsBuyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPStoreCoinsBuyView.swift; sourceTree = ""; }; - BFA21D9D2E01684E00B3573D /* VPStoreCoinsItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPStoreCoinsItemView.swift; sourceTree = ""; }; - BFA21D9F2E01688D00B3573D /* VPStoreCoinsBigItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPStoreCoinsBigItemView.swift; sourceTree = ""; }; - BFA21DA12E01696700B3573D /* VPStoreCoinsSmallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPStoreCoinsSmallItemView.swift; sourceTree = ""; }; BFF5AFA32DE6F15E0044227A /* VPMeVipCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPMeVipCell.swift; sourceTree = ""; }; BFF5AFA52DE700420044227A /* VPMeVipPrivilegeItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPMeVipPrivilegeItemView.swift; sourceTree = ""; }; BFF5AFA72DE704DC0044227A /* VPMeCoinCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPMeCoinCell.swift; sourceTree = ""; }; @@ -1002,6 +1006,8 @@ BFF5B2742DF2C3750044227A /* VPDetailRecommandView.swift */, BFF5B2762DF2CA4B0044227A /* VPDetailRecommandBannerCell.swift */, BFF5B4AE2DF6B6630044227A /* VPMutualCollectionView.swift */, + BF692AB42E0644B500A5C2DA /* VPRevolutionSelectedView.swift */, + BF692AB62E06450C00A5C2DA /* VPRevolutionSelectedCell.swift */, ); path = View; sourceTree = ""; @@ -1480,10 +1486,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Veloria/Pods-Veloria-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Veloria/Pods-Veloria-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Veloria/Pods-Veloria-frameworks.sh\"\n"; @@ -1511,6 +1521,7 @@ BF0FA7B32DE447FE00C9E5F2 /* VPMeCell.swift in Sources */, BF0FA6DF2DDC5E4D00C9E5F2 /* String+VPAdd.swift in Sources */, BF0FA7002DDC665300C9E5F2 /* VPShortModel.swift in Sources */, + BF692AB72E06450C00A5C2DA /* VPRevolutionSelectedCell.swift in Sources */, 1B056E7B2DDB37BA007EE38D /* VPGradientView.swift in Sources */, BF0FA7262DDC8F7600C9E5F2 /* VPCollectionView.swift in Sources */, BF5E75DB2DE5B8B700DE9DFE /* VPMarqueeView.swift in Sources */, @@ -1554,6 +1565,7 @@ BF0FA73B2DDED1C700C9E5F2 /* VPHomeListCell.swift in Sources */, BF0FA79F2DE1A29A00C9E5F2 /* VPCollectListCell.swift in Sources */, BF0FA7592DDF1C2800C9E5F2 /* VPPlayerProgressView.swift in Sources */, + BF692AB52E0644B500A5C2DA /* VPRevolutionSelectedView.swift in Sources */, BFF5AFE02DEEC5AB0044227A /* VPPlayerVipBuyView.swift in Sources */, BFF5AFCA2DE97B7A0044227A /* VPWalletViewController.swift in Sources */, BFF5B26C2DF28FD50044227A /* VPStatAPI.swift in Sources */, diff --git a/Veloria/Base/Networking/API/VPVideoAPI.swift b/Veloria/Base/Networking/API/VPVideoAPI.swift index 6eeb411..3be2f4a 100644 --- a/Veloria/Base/Networking/API/VPVideoAPI.swift +++ b/Veloria/Base/Networking/API/VPVideoAPI.swift @@ -10,7 +10,7 @@ import UIKit class VPVideoAPI: NSObject { ///获取视频详情 - static func requestVideoDetail(shortPlayId: String, activityId: String? = nil, completer: ((_ model: VPVideoDetailModel?) -> Void)?) { + static func requestVideoDetail(shortPlayId: String, activityId: String? = nil, revolution: VPShortModel.VideoRevolution? = nil, completer: ((_ model: VPVideoDetailModel?) -> Void)?) { var parameters: [String : Any] = [ "short_play_id" : shortPlayId, "video_id" : "0" @@ -20,6 +20,10 @@ class VPVideoAPI: NSObject { parameters["activity_id"] = activityId } + if let revolution = revolution?.rawValue { + parameters["revolution"] = revolution + } + var param = VPNetworkParameters(path: "/getVideoDetails") param.method = .get param.parameters = parameters diff --git a/Veloria/Class/Login/View/VPLoginContentView.swift b/Veloria/Class/Login/View/VPLoginContentView.swift index 323edd2..915f8a7 100644 --- a/Veloria/Class/Login/View/VPLoginContentView.swift +++ b/Veloria/Class/Login/View/VPLoginContentView.swift @@ -9,6 +9,8 @@ import UIKit class VPLoginContentView: HWPanModalContentView { + var loginFinishBlock: (() -> Void)? + private lazy var bgView: UIImageView = { let imageView = UIImageView(image: UIImage(named: "bg_image_01")) return imageView @@ -167,6 +169,7 @@ extension VPLoginContentView { VPHUD.dismiss() guard let self = self else { return } if isFinish { + self.loginFinishBlock?() self.dismiss(animated: true) { } diff --git a/Veloria/Class/Player/Controller/VPDetailPlayerViewController.swift b/Veloria/Class/Player/Controller/VPDetailPlayerViewController.swift index 87c3c5f..28c07b4 100644 --- a/Veloria/Class/Player/Controller/VPDetailPlayerViewController.swift +++ b/Veloria/Class/Player/Controller/VPDetailPlayerViewController.swift @@ -162,6 +162,11 @@ extension VPDetailPlayerViewController { self.viewModel.handleUnlock = { [weak self] in self?.unlockVideo() } + + self.viewModel.updateDetailDataBlock = { [weak self] toIndexPath in + guard let self = self else { return } + self.requestDetailData(indexPath: self.viewModel.currentIndexPath) + } } } @@ -171,7 +176,7 @@ extension VPDetailPlayerViewController { let view = VPEpisodeView() view.dataArr = detailModel?.episodeList ?? [] view.shortModel = detailModel?.shortPlayInfo - view.currentIndex = self.currentIndexPath.row + view.currentIndex = self.viewModel.currentIndexPath.row view.didSelectedIndex = { [weak self] (index) in self?.scrollToItem(indexPath: IndexPath(row: index, section: 0), animated: false) } @@ -224,7 +229,7 @@ extension VPDetailPlayerViewController { view.videoId = videoInfo.short_play_video_id view.buyFinishBlock = { [weak self] in guard let self = self else { return } - self.requestDetailData(indexPath: self.currentIndexPath) + self.requestDetailData(indexPath: self.viewModel.currentIndexPath) } view.present(in: nil) } @@ -343,7 +348,7 @@ extension VPDetailPlayerViewController { VPHUD.show(containerView: self.view) - VPVideoAPI.requestVideoDetail(shortPlayId: shortPlayId, activityId: activityId) { [weak self] model in + VPVideoAPI.requestVideoDetail(shortPlayId: shortPlayId, activityId: activityId, revolution: self.viewModel.revolution) { [weak self] model in VPHUD.dismiss() guard let self = self else { return } guard let model = model else { return } diff --git a/Veloria/Class/Player/Controller/VPVideoPlayerViewController.swift b/Veloria/Class/Player/Controller/VPVideoPlayerViewController.swift index bdd34cd..14315ff 100644 --- a/Veloria/Class/Player/Controller/VPVideoPlayerViewController.swift +++ b/Veloria/Class/Player/Controller/VPVideoPlayerViewController.swift @@ -52,8 +52,6 @@ class VPVideoPlayerViewController: VPViewController { private(set) var dataArr: [Any] = [] - private(set) var currentIndexPath = IndexPath(row: 0, section: 0) - ///自动下一级 var autoNextEpisode = true @@ -120,7 +118,7 @@ class VPVideoPlayerViewController: VPViewController { self.viewModel.isPlaying = true - if getDataCount() - currentIndexPath.row <= 2 { + if getDataCount() - viewModel.currentIndexPath.row <= 2 { self.loadMoreData() } @@ -144,7 +142,7 @@ class VPVideoPlayerViewController: VPViewController { func clearDataArr() { self.dataArr.removeAll() self.viewModel.currentPlayer = nil - self.currentIndexPath = .init(row: 0, section: 0) + viewModel.currentIndexPath = .init(row: 0, section: 0) self.collectionView.contentOffset = .init(x: 0, y: 0) self.collectionView.reloadData() } @@ -176,7 +174,7 @@ class VPVideoPlayerViewController: VPViewController { 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 + let cell = self.collectionView.cellForItem(at: viewModel.currentIndexPath) as? VPVideoPlayerCell self.viewModel.currentPlayer = cell completion?() @@ -190,7 +188,7 @@ class VPVideoPlayerViewController: VPViewController { CATransaction.setCompletionBlock { [weak self] in guard let self = self else { return } if !animated { - if self.currentIndexPath != indexPath, indexPath.row < self.getDataCount() { + if viewModel.currentIndexPath != indexPath, indexPath.row < self.getDataCount() { self.skip(indexPath: indexPath) } else { self.play() @@ -315,8 +313,8 @@ extension VPVideoPlayerViewController: UICollectionViewDelegate, UICollectionVie } } - if self.viewModel.currentPlayer == nil, indexPath == currentIndexPath, let playerProtocol = cell as? VPPlayerProtocol { - self.currentIndexPath = indexPath + if self.viewModel.currentPlayer == nil, indexPath == viewModel.currentIndexPath, let playerProtocol = cell as? VPPlayerProtocol { + viewModel.currentIndexPath = indexPath self.viewModel.currentPlayer = playerProtocol didChangeIndexPathForVisible() } @@ -349,7 +347,7 @@ extension VPVideoPlayerViewController: UICollectionViewDelegate, UICollectionVie 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 { + if viewModel.currentIndexPath != indexPath { self.skip(indexPath: indexPath) } } @@ -357,7 +355,7 @@ extension VPVideoPlayerViewController: UICollectionViewDelegate, UICollectionVie } private func skip(indexPath: IndexPath) { - currentIndexPath = indexPath + viewModel.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 @@ -381,7 +379,7 @@ extension VPVideoPlayerViewController { } private func didChangeIndexPathForVisible() { - self.delegate?.vp_playerListViewController?(self, didChangeIndexPathForVisible: self.currentIndexPath) + self.delegate?.vp_playerListViewController?(self, didChangeIndexPathForVisible: viewModel.currentIndexPath) } } diff --git a/Veloria/Class/Player/Model/VPShortModel.swift b/Veloria/Class/Player/Model/VPShortModel.swift index e435b95..4f489a4 100644 --- a/Veloria/Class/Player/Model/VPShortModel.swift +++ b/Veloria/Class/Player/Model/VPShortModel.swift @@ -10,6 +10,32 @@ import SmartCodable class VPShortModel: VPModel, SmartCodable { + enum VideoRevolution: String, SmartCaseDefaultable { + case r_540 = "540" + case r_720 = "720" + case r_1080 = "1080" + + var needLogin: Bool { + if self == .r_720 { + return true + } else { + return false + } + } + + var needVip: Bool { + if self == .r_1080 { + return true + } else { + return false + } + } + + var toString: String { + return "\(self.rawValue)P" + } + } + enum TagType: String, SmartCaseDefaultable { case hot = "hot" case new = "new" @@ -39,6 +65,8 @@ class VPShortModel: VPModel, SmartCodable { var video_url: String? var updated_at: String? + var revolution: VideoRevolution? + @IgnoredKey var titleAttributedString: NSAttributedString? @IgnoredKey diff --git a/Veloria/Class/Player/View/VPDetailPlayerControlView.swift b/Veloria/Class/Player/View/VPDetailPlayerControlView.swift index 2cb9971..23fee99 100644 --- a/Veloria/Class/Player/View/VPDetailPlayerControlView.swift +++ b/Veloria/Class/Player/View/VPDetailPlayerControlView.swift @@ -14,6 +14,7 @@ class VPDetailPlayerControlView: VPVideoPlayerControlView { self.viewModel?.addObserver(self, forKeyPath: "rateModel", options: .new, context: nil) rateButton.setTitle(self.viewModel?.rateModel.formatString(), for: .normal) + revolutionButton.setNeedsUpdateConfiguration() } } @@ -30,6 +31,8 @@ class VPDetailPlayerControlView: VPVideoPlayerControlView { epView.setTitle("veloria_EP.".localizedReplace(text: videoInfo?.episode ?? "0"), for: .normal) lockView.isHidden = !(videoInfo?.is_lock ?? false) lockView.videoInfo = videoInfo + + revolutionButton.setNeedsUpdateConfiguration() } } @@ -51,6 +54,15 @@ class VPDetailPlayerControlView: VPVideoPlayerControlView { } } + override var isCurrent: Bool { + didSet { + if !isCurrent { + rateSelectedView.removeFromSuperview() + revolutionSelectedView.removeFromSuperview() + } + } + } + //MARK: -------------- UI属性 -------------- private lazy var bottomView: VPGradientView = { let view = VPGradientView() @@ -104,6 +116,23 @@ class VPDetailPlayerControlView: VPVideoPlayerControlView { return button }() + ///分辨率 + private lazy var revolutionButton: UIButton = { + var config = UIButton.Configuration.plain() + config.contentInsets = .init(top: 0, leading: 12, bottom: 0, trailing: 12) + config.background.backgroundColor = .color949494(alpha: 0.4) + + let button = UIButton(configuration: config) + button.layer.cornerRadius = 15 + button.layer.masksToBounds = true + button.configurationUpdateHandler = { [weak self] button in + guard let self = self else { return } + button.configuration?.attributedTitle = AttributedString.createAttributedString(string: self.viewModel?.revolution.toString ?? "", color: .colorFFFFFF(), font: .fontRegular(ofSize: 13)) + } + button.addTarget(self, action: #selector(handleRevolutionButton), for: .touchUpInside) + return button + }() + private lazy var timeLabel: UILabel = { let label = UILabel() label.font = .fontRegular(ofSize: 12) @@ -122,6 +151,16 @@ class VPDetailPlayerControlView: VPVideoPlayerControlView { return view }() + ///分辨率选择 + private lazy var revolutionSelectedView: VPRevolutionSelectedView = { + let view = VPRevolutionSelectedView() + view.didSelected = { [weak self] revolution in + guard let self = self else { return } + self.viewModel?.selectedRevolution(revolution: revolution) + } + return view + }() + private lazy var lockView: VPVideoLockView = { let view = VPVideoLockView() view.clickUnlockButton = { [weak self] in @@ -176,6 +215,15 @@ extension VPDetailPlayerControlView { } } + @objc private func handleRevolutionButton() { + addSubview(revolutionSelectedView) + revolutionSelectedView.currentRevolution = self.viewModel?.revolution + + revolutionSelectedView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + } extension VPDetailPlayerControlView { @@ -188,6 +236,7 @@ extension VPDetailPlayerControlView { addSubview(epBgView) epBgView.addSubview(epView) epBgView.addSubview(allEpView) + addSubview(revolutionButton) addSubview(rateButton) addSubview(timeLabel) addSubview(lockView) @@ -224,9 +273,14 @@ extension VPDetailPlayerControlView { make.right.equalToSuperview().offset(-10) } + revolutionButton.snp.makeConstraints { make in + make.height.top.equalTo(epBgView) + make.left.equalTo(epBgView.snp.right).offset(10) + } + rateButton.snp.makeConstraints { make in make.right.equalToSuperview().offset(-15) - make.left.equalTo(epBgView.snp.right).offset(10) + make.left.equalTo(revolutionButton.snp.right).offset(10) make.height.top.equalTo(epBgView) make.width.equalTo(50) } diff --git a/Veloria/Class/Player/View/VPPlayerRechargeView.swift b/Veloria/Class/Player/View/VPPlayerRechargeView.swift index 61dbbd1..05b82be 100644 --- a/Veloria/Class/Player/View/VPPlayerRechargeView.swift +++ b/Veloria/Class/Player/View/VPPlayerRechargeView.swift @@ -23,6 +23,7 @@ class VPPlayerRechargeView: HWPanModalContentView { } var buyFinishBlock: (() -> Void)? + var vipBuyFinishBlock: (() -> Void)? //MARK: UI属性 private lazy var bgView: UIImageView = { @@ -64,7 +65,12 @@ class VPPlayerRechargeView: HWPanModalContentView { private lazy var vipView: VPStoreVipBuyView = { let view = VPStoreVipBuyView() view.buyFinishBlock = { [weak self] in - self?.buyFinishBlock?() + if let vipBuyFinishBlock = self?.vipBuyFinishBlock { + self?.vipBuyFinishBlock?() + } else { + self?.buyFinishBlock?() + } + self?.dismiss(animated: true) { } } diff --git a/Veloria/Class/Player/View/VPRevolutionSelectedCell.swift b/Veloria/Class/Player/View/VPRevolutionSelectedCell.swift new file mode 100644 index 0000000..0d3227a --- /dev/null +++ b/Veloria/Class/Player/View/VPRevolutionSelectedCell.swift @@ -0,0 +1,86 @@ +// +// VPRevolutionSelectedCell.swift +// Veloria +// +// Created by 湖南秦九 on 2025/6/21. +// + +import UIKit + +class VPRevolutionSelectedCell: VPCollectionViewCell { + + var videoRevolution: VPShortModel.VideoRevolution? { + didSet { + label.text = videoRevolution?.toString + + if videoRevolution?.needLogin == true { + markImageView.isHidden = false + markImageView.image = UIImage(named: "mark_icon_01") + } else if videoRevolution?.needVip == true { + markImageView.isHidden = false + markImageView.image = UIImage(named: "mark_icon_02") + } else { + markImageView.isHidden = true + } + + } + } + + 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 + }() + + private lazy var markImageView: UIImageView = { + let imageView = UIImageView() + return imageView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + vp_setupUI() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + +extension VPRevolutionSelectedCell { + + private func vp_setupUI() { + contentView.layer.cornerRadius = 6 + contentView.layer.masksToBounds = false + + + + contentView.addSubview(label) + contentView.addSubview(markImageView) + + label.snp.makeConstraints { make in + make.center.equalToSuperview() + } + + markImageView.snp.makeConstraints { make in + make.right.equalToSuperview() + make.top.equalToSuperview().offset(-5) + } + } + +} diff --git a/Veloria/Class/Player/View/VPRevolutionSelectedView.swift b/Veloria/Class/Player/View/VPRevolutionSelectedView.swift new file mode 100644 index 0000000..91c45c9 --- /dev/null +++ b/Veloria/Class/Player/View/VPRevolutionSelectedView.swift @@ -0,0 +1,107 @@ +// +// VPRevolutionSelectedView.swift +// Veloria +// +// Created by 湖南秦九 on 2025/6/21. +// + +import UIKit + +class VPRevolutionSelectedView: UIView { + + var currentRevolution: VPShortModel.VideoRevolution? { + didSet { + collectionView.reloadData() + } + } + + var didSelected: ((_ revolution: VPShortModel.VideoRevolution) -> Void)? + + private lazy var dataArr: [VPShortModel.VideoRevolution] = { + return [.r_540, .r_720, .r_1080] + }() + + private lazy var collectionViewLayout: UICollectionViewFlowLayout = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.itemSize = .init(width: 100, 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(VPRevolutionSelectedCell.self, forCellWithReuseIdentifier: "cell") + collectionView.showsHorizontalScrollIndicator = false + collectionView.layer.masksToBounds = 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 VPRevolutionSelectedView { + + 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 VPRevolutionSelectedView: UICollectionViewDelegate, UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let videoRevolution = dataArr[indexPath.row] + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! VPRevolutionSelectedCell + cell.videoRevolution = videoRevolution + cell.vp_isSelected = currentRevolution == videoRevolution + return cell + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return dataArr.count + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let videoRevolution = dataArr[indexPath.row] + self.didSelected?(videoRevolution) + self.handleDismiss() + } +} + +//MARK: -------------- UIGestureRecognizerDelegate -------------- +extension VPRevolutionSelectedView: 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 index b25f972..283abf1 100644 --- a/Veloria/Class/Player/ViewModel/VPVideoPlayViewModel.swift +++ b/Veloria/Class/Player/ViewModel/VPVideoPlayViewModel.swift @@ -11,6 +11,7 @@ class VPVideoPlayViewModel: NSObject { @objc dynamic var isPlaying: Bool = true + var currentIndexPath = IndexPath(row: 0, section: 0) var currentPlayer: VPPlayerProtocol? { didSet { @@ -29,6 +30,9 @@ class VPVideoPlayViewModel: NSObject { } } + ///当前分辨率 + lazy var revolution: VPShortModel.VideoRevolution = .r_540 + ///设置进度 func seekToTime(toTime: Int) { self.currentPlayer?.seekToTime(toTime: toTime) @@ -44,4 +48,46 @@ class VPVideoPlayViewModel: NSObject { var handleEpisode: (() -> Void)? var handleUnlock: (() -> Void)? + + ///更新数据 + var updateDetailDataBlock: ((_ toIndexPath: IndexPath?) -> Void)? +} + +extension VPVideoPlayViewModel { + ///选择了新的分辨率 + func selectedRevolution(revolution: VPShortModel.VideoRevolution) { + guard self.revolution != revolution else { return } + let userInfo = VPLoginManager.manager.userInfo + + if revolution.needLogin, userInfo?.is_tourist != false { + + VPLoginManager.manager.openLogin { [weak self] in + guard let self = self else { return } + self.revolution = revolution + self.updateDetailDataBlock?(self.currentIndexPath) + } + + } else if revolution.needVip, userInfo?.is_vip != true { + guard let videoInfo = self.currentPlayer?.videoInfo else { return } + + let view = VPPlayerRechargeView() + view.shortPlayId = videoInfo.short_play_id + view.videoId = videoInfo.short_play_video_id + view.buyFinishBlock = { [weak self] in + guard let self = self else { return } + self.updateDetailDataBlock?(self.currentIndexPath) + } + view.vipBuyFinishBlock = { [weak self] in + guard let self = self else { return } + self.revolution = revolution + self.updateDetailDataBlock?(self.currentIndexPath) + } + view.present(in: nil) + } else { + self.revolution = revolution + self.updateDetailDataBlock?(self.currentIndexPath) + } + + } + } diff --git a/Veloria/Class/Wallet/Controller/VPStoreViewController.swift b/Veloria/Class/Wallet/Controller/VPStoreViewController.swift index 64f2b99..adac84b 100644 --- a/Veloria/Class/Wallet/Controller/VPStoreViewController.swift +++ b/Veloria/Class/Wallet/Controller/VPStoreViewController.swift @@ -8,6 +8,9 @@ import UIKit class VPStoreViewController: VPViewController { + + ///vip购买成功回调 + var vipBuyFinishBlock: (() -> Void)? private lazy var scrollView: VPScrollView = { let scrollView = VPScrollView() @@ -23,6 +26,10 @@ class VPStoreViewController: VPViewController { private lazy var vipView: VPStoreVipBuyView = { let view = VPStoreVipBuyView() + view.buyFinishBlock = { [weak self] in + guard let self = self else { return } + self.vipBuyFinishBlock?() + } return view }() diff --git a/Veloria/Libs/Login/VPLoginManager.swift b/Veloria/Libs/Login/VPLoginManager.swift index 5785521..f1ad00d 100644 --- a/Veloria/Libs/Login/VPLoginManager.swift +++ b/Veloria/Libs/Login/VPLoginManager.swift @@ -38,8 +38,9 @@ class VPLoginManager: NSObject { UserDefaults.vp_setObject(token, forKey: kVPLoginTokenDefaultsKey) } - func openLogin() { + func openLogin(finishHandle: (() -> Void)? = nil) { let view = VPLoginContentView() + view.loginFinishBlock = finishHandle view.present(in: nil) } diff --git a/Veloria/Source/Assets.xcassets/icon/mark_icon_01.imageset/Contents.json b/Veloria/Source/Assets.xcassets/icon/mark_icon_01.imageset/Contents.json new file mode 100644 index 0000000..8cfadf7 --- /dev/null +++ b/Veloria/Source/Assets.xcassets/icon/mark_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group 79@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group 79@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Veloria/Source/Assets.xcassets/icon/mark_icon_01.imageset/Group 79@2x.png b/Veloria/Source/Assets.xcassets/icon/mark_icon_01.imageset/Group 79@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f04f1e00676fa6b3c185115e712475ee51ed8337 GIT binary patch literal 1227 zcmV;+1T_1JP)35t*5~il8!*FwEN3Mh!t* zg!Ti91nI)i-B>Lg+-Q+#_;Wko^}R`-@4b1x86llM7@c?TefORF-E+Qk-W@>>Q0NQn z5^_9OlGr7P{E{e%<{;Si0k*wEknE7c^SUfl_s+pR*0|*hkL-68zNQgV7 zp}OZbW2U3n7x0ftS<7EHQXBw8B?ysK9IERv_IZpAMJo=S8k4e};)EOk5yB49Ubr$4 zF!oLHAT>ZJ&xu=jgeSiwRQp1|4ikj1g6GUt0gfRCyh5bw;pWv_nVjaCkdv0Pjdq+p zNGTcXY2bSweSXzM&G%l?JV27Bzv;M0z#n8QCMVzN3?Sz4!;7>$d8N8eT((FNh3l?2 zZ8Q(y%UqK7^~103jtPF?(i>)S4<0)FbC433E;8-!Y3Za`0Q0BkWvl?w)yVfSZe9wNLknO z6mcM}_HsFGXVPqsNL@f^M&(N>tL+A2w`QU%FdAt!axrENeN+0s^-ak@K5hqb#<@`n zfV^0qDuf9yT$LN~6cM3eCXPnH_{j^3HTH250C}j1bm02@+xx05mZ3H$56tw83P99# zy+ar_#rRBWO2|41wSNgITEPmyYoZBfD@@ZgoJ$KKtN$OpPQdbKAMFU%PjQa8{^8IG p9F+|?SyKmVf;I>#H^x)={Q<8w>`)1Om!SXv002ovPDHLkV1jwNHN5}; literal 0 HcmV?d00001 diff --git a/Veloria/Source/Assets.xcassets/icon/mark_icon_01.imageset/Group 79@3x.png b/Veloria/Source/Assets.xcassets/icon/mark_icon_01.imageset/Group 79@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..5de74f55bfb76e61aa33a35a7f79347e3869686f GIT binary patch literal 1737 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 T2}wv@00000NkvXXu0mjfgt%U? literal 0 HcmV?d00001 diff --git a/Veloria/Source/Assets.xcassets/icon/mark_icon_02.imageset/Group 78@3x.png b/Veloria/Source/Assets.xcassets/icon/mark_icon_02.imageset/Group 78@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..ce114c74af5734dfe4664a954c4236021a7c9fb6 GIT binary patch literal 5351 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