diff --git a/ShortPlay/AppDelegate/AppDelegate.swift b/ShortPlay/AppDelegate/AppDelegate.swift index 7a0747e..c8d5fa8 100644 --- a/ShortPlay/AppDelegate/AppDelegate.swift +++ b/ShortPlay/AppDelegate/AppDelegate.swift @@ -14,7 +14,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - self.appConfig() SPLoginManager.manager.requestVisitorLogin(completer: nil) diff --git a/ShortPlay/Base/Extension/String+SPAdd.swift b/ShortPlay/Base/Extension/String+SPAdd.swift index 3741755..c95d4c4 100644 --- a/ShortPlay/Base/Extension/String+SPAdd.swift +++ b/ShortPlay/Base/Extension/String+SPAdd.swift @@ -26,3 +26,11 @@ extension String: SmartCodable { } } + +extension String { + ///获取文字Size + func size(font: UIFont, size: CGSize = CGSize(width: CGFloat(MAXFLOAT), height: CGFloat(MAXFLOAT))) -> CGSize{ + let string: NSString = self as NSString + return string.size(for: font, size: size, mode: .byWordWrapping) + } +} diff --git a/ShortPlay/Base/Extension/UIColor+SPAdd.swift b/ShortPlay/Base/Extension/UIColor+SPAdd.swift index 8b47eca..9af98a0 100644 --- a/ShortPlay/Base/Extension/UIColor+SPAdd.swift +++ b/ShortPlay/Base/Extension/UIColor+SPAdd.swift @@ -63,5 +63,7 @@ extension UIColor { static func colorF56490(alpha: CGFloat = 1) -> UIColor { return color(hex: 0xF56490, alpha: alpha) } + + } diff --git a/ShortPlay/Base/Networking/API/SPVideoAPI.swift b/ShortPlay/Base/Networking/API/SPVideoAPI.swift index b28d529..862bb85 100644 --- a/ShortPlay/Base/Networking/API/SPVideoAPI.swift +++ b/ShortPlay/Base/Networking/API/SPVideoAPI.swift @@ -41,4 +41,37 @@ class SPVideoAPI: NSObject { } } + + ///收藏短剧 + static func requestCollectShort(isCollect: Bool, shortPlayId: String, success: (() -> Void)?) { + let path: String + if isCollect { + path = "/collect" + } else { + path = "/cancelCollect" + } + + var param = SPNetworkParameters(path: path) + param.isLoding = true + param.parameters = [ + "short_play_id" : shortPlayId + ] + + SPNetwork.request(parameters: param) { (response: SPNetworkResponse) in + if response.code == SPNetworkCodeSucceed { + success?() + NotificationCenter.default.post(name: SPVideoAPI.updateShortCollectStateNotification, object: nil, userInfo: [ + "state" : isCollect, + "id" : shortPlayId, + ]) + } + } + } + +} + +extension SPVideoAPI { + ///更新短剧关注状态 [ "state" : isCollect, "id" : shortPlayId,] + @objc static let updateShortCollectStateNotification = NSNotification.Name(rawValue: "SPVideoAPI.updateShortCollectStateNotification") + } diff --git a/ShortPlay/Base/Networking/Base/CryptorService.swift b/ShortPlay/Base/Networking/Base/CryptorService.swift new file mode 100644 index 0000000..9dad416 --- /dev/null +++ b/ShortPlay/Base/Networking/Base/CryptorService.swift @@ -0,0 +1,101 @@ +//// CryptorService.swift +// QJDrama +// +// Created by Yao on 2025/4/16 +// +// + +import Foundation + +class CryptorService { + // 定义常量 + static let EN_STR_TAG: String = "$" // 替换为实际的加密标记 + + // 解密字符串 + static func decrypt(data: String) -> String { + guard data.hasPrefix(EN_STR_TAG) else { +// fatalError("Invalid encoded string") + return data + } + + let decryptedData = deStrBytes(data: data) + return String(data: decryptedData, encoding: .utf8) ?? "" + } + + // 从十六进制字符串解密字节 + static func deStrBytes(data: String) -> Data { + + let hexData = String(data.dropFirst()) + var bytes = Data() + + var index = hexData.startIndex + while index < hexData.endIndex { + let nextIndex = hexData.index(index, offsetBy: 2, limitedBy: hexData.endIndex) ?? hexData.endIndex + let byteString = String(hexData[index.. Data { + guard !data.isEmpty else { + return data + } + + let saltLen = Int(data[data.startIndex]) + guard data.count >= 1 + saltLen else { + return data + } + + let salt = data.subdata(in: 1..<1+saltLen) + let encryptedData = data.subdata(in: 1+saltLen.. Data { + let decryptedData = cxEd(data: data) + return removeSalt(data: decryptedData, salt: salt) + } + + // 加密/解密数据(按位取反) + static func cxEd(data: Data) -> Data { + return Data(data.map { $0 ^ 0xFF }) + } + + // 从数据中移除盐值 + static func removeSalt(data: Data, salt: Data) -> Data { + guard !salt.isEmpty else { + return data + } + + var result = Data() + let saltBytes = [UInt8](salt) + let saltCount = saltBytes.count + + for (index, byte) in data.enumerated() { + let saltByte = saltBytes[index % saltCount] + let decryptedByte = calRemoveSalt(v: byte, s: saltByte) + result.append(decryptedByte) + } + + return result + } + + // 计算移除盐值后的字节 + static func calRemoveSalt(v: UInt8, s: UInt8) -> UInt8 { + if v >= s { + return v - s + } else { + return UInt8(0xFF) - (s - v) + 1 + } + } +} diff --git a/ShortPlay/Base/Networking/Base/SPNetwork.swift b/ShortPlay/Base/Networking/Base/SPNetwork.swift index ec514c5..eac6053 100644 --- a/ShortPlay/Base/Networking/Base/SPNetwork.swift +++ b/ShortPlay/Base/Networking/Base/SPNetwork.swift @@ -34,11 +34,14 @@ class SPNetwork: NSObject { while loding { if !SPLoginManager.manager.isRefreshingToken { loding = false + spLog(message: "======等待结束") + } else { + spLog(message: "======等待中") } RunLoop.current.run(mode: .default, before: Date.distantFuture) } } - + spLog(message: "======开始请求") _request(parameters: parameters, completion: completion) } @@ -140,11 +143,11 @@ class SPNetwork: NSObject { var response: SPNetworkResponse? let time = Date().timeIntervalSince1970 - if let decrypted = SPCryptService.decrypt(data) { - spLog(message: decrypted) - response = SPNetworkResponse.deserialize(from: decrypted) - response?.rawData = decrypted - } + let decrypted = CryptorService.decrypt(data: data) + spLog(message: decrypted) + response = SPNetworkResponse.deserialize(from: decrypted) + response?.rawData = decrypted + spLog(message: Date().timeIntervalSince1970 - time) if let response = response { diff --git a/ShortPlay/Base/Networking/Base/SPURLPath.swift b/ShortPlay/Base/Networking/Base/SPURLPath.swift index e88ad32..d83137a 100644 --- a/ShortPlay/Base/Networking/Base/SPURLPath.swift +++ b/ShortPlay/Base/Networking/Base/SPURLPath.swift @@ -7,6 +7,18 @@ import UIKit +/* + https://api-moviatv.moviatv.com/93f03506/ + + https://api-mireotv.mireotv.com/4da6fd4c/ + + https://api-vibeoshort.vibeoshort.com/bf86d973/ + + https://api-viontv.viontv.com/b7afef99/ + + https://api-zyreotv.zyreotv.com/7834f11d/ + */ + #if DEBUG let SPBaseURL = "https://test1-api.guyantv.com" let SPWebBaseURL = "https://www.guyantv.com" diff --git a/ShortPlay/Class/ForYou/Controller/SPForYouViewController.swift b/ShortPlay/Class/ForYou/Controller/SPForYouViewController.swift index ee4bee3..f352ee2 100644 --- a/ShortPlay/Class/ForYou/Controller/SPForYouViewController.swift +++ b/ShortPlay/Class/ForYou/Controller/SPForYouViewController.swift @@ -46,7 +46,7 @@ extension SPForYouViewController: SPPlayerListViewControllerDataSource { if let cell = oldCell as? SPPlayerListCell { if let model = dataArr[indexPath.row] as? SPShortModel { - cell.model = model + cell.shortModel = model cell.videoInfo = model.video_info } } diff --git a/ShortPlay/Class/Home/View/SPHomeHeaderView.swift b/ShortPlay/Class/Home/View/SPHomeHeaderView.swift index 28c2d0e..0721233 100644 --- a/ShortPlay/Class/Home/View/SPHomeHeaderView.swift +++ b/ShortPlay/Class/Home/View/SPHomeHeaderView.swift @@ -106,7 +106,7 @@ extension SPHomeHeaderView: ZKCycleScrollViewDelegate, ZKCycleScrollViewDataSour func cycleScrollView(_ cycleScrollView: ZKCycleScrollView, didSelectItemAt index: Int) { let model = moduleModel?.bannerData?[index] - let vc = SPTVPlayerListViewController() + let vc = SPPlayerDetailViewController() vc.shortPlayId = model?.short_play_id self.viewController?.navigationController?.pushViewController(vc, animated: true) } diff --git a/ShortPlay/Class/Home/View/SPHomeHotView.swift b/ShortPlay/Class/Home/View/SPHomeHotView.swift index 41203db..dbe622d 100644 --- a/ShortPlay/Class/Home/View/SPHomeHotView.swift +++ b/ShortPlay/Class/Home/View/SPHomeHotView.swift @@ -83,7 +83,7 @@ extension SPHomeHotView: UICollectionViewDelegate, UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let model = self.dataArr?[indexPath.row] - let vc = SPTVPlayerListViewController() + let vc = SPPlayerDetailViewController() vc.shortPlayId = model?.short_play_id self.viewController?.navigationController?.pushViewController(vc, animated: true) } diff --git a/ShortPlay/Class/Home/View/SPHomeTrendingView.swift b/ShortPlay/Class/Home/View/SPHomeTrendingView.swift index 1fd9a23..60fad83 100644 --- a/ShortPlay/Class/Home/View/SPHomeTrendingView.swift +++ b/ShortPlay/Class/Home/View/SPHomeTrendingView.swift @@ -101,7 +101,7 @@ extension SPHomeTrendingView: UICollectionViewDataSource, UICollectionViewDelega func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let model = self.dataArr?[indexPath.row] - let vc = SPTVPlayerListViewController() + let vc = SPPlayerDetailViewController() vc.shortPlayId = model?.short_play_id self.viewController?.navigationController?.pushViewController(vc, animated: true) } diff --git a/ShortPlay/Class/Player/Controller/SPTVPlayerListViewController.swift b/ShortPlay/Class/Player/Controller/SPPlayerDetailViewController.swift similarity index 62% rename from ShortPlay/Class/Player/Controller/SPTVPlayerListViewController.swift rename to ShortPlay/Class/Player/Controller/SPPlayerDetailViewController.swift index 9eae165..ad12a5d 100644 --- a/ShortPlay/Class/Player/Controller/SPTVPlayerListViewController.swift +++ b/ShortPlay/Class/Player/Controller/SPPlayerDetailViewController.swift @@ -1,5 +1,5 @@ // -// SPTVPlayerListViewController.swift +// SPPlayerDetailViewController.swift // ShortPlay // // Created by 曾觉新 on 2025/4/10. @@ -7,10 +7,10 @@ import UIKit -class SPTVPlayerListViewController: SPPlayerListViewController { +class SPPlayerDetailViewController: SPPlayerListViewController { override var PlayerCellClass: SPPlayerListCell.Type { - return SPTVPlayerListCell.self + return SPPlayerDetailCell.self } override var contentSize: CGSize { @@ -23,12 +23,17 @@ class SPTVPlayerListViewController: SPPlayerListViewController { private var detailModel: SPVideoDetailModel? + private weak var episodeView: SPEpisodeView? + override func viewDidLoad() { super.viewDidLoad() self.autoNextEpisode = true self.dataSource = self + self.delegate = self requestDetailData() + + _addAction() } @@ -49,11 +54,33 @@ class SPTVPlayerListViewController: SPPlayerListViewController { } +extension SPPlayerDetailViewController { + + private func _addAction() { + self.viewModel.handleEpisode = { [weak self] in + self?.onEpisode() + } + } + + private func onEpisode() { + let view = SPEpisodeView() + 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)) + } + view.present(in: nil) + self.episodeView = view + } + +} + //MARK: -------------- SPPlayerListViewControllerDataSource -------------- -extension SPTVPlayerListViewController: SPPlayerListViewControllerDataSource { +extension SPPlayerDetailViewController: SPPlayerListViewControllerDataSource, SPPlayerListViewControllerDelegate { func sp_playerListViewController(_ viewController: SPPlayerListViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, oldCell: UICollectionViewCell) -> UICollectionViewCell { - if let cell = oldCell as? SPPlayerListCell { - cell.model = detailModel + if let cell = oldCell as? SPPlayerDetailCell { + cell.shortModel = detailModel?.shortPlayInfo cell.videoInfo = detailModel?.episodeList?[indexPath.row] cell.isLoop = false } @@ -63,10 +90,14 @@ extension SPTVPlayerListViewController: SPPlayerListViewControllerDataSource { func sp_playerListViewController(_ viewController: SPPlayerListViewController, _ collectionView: UICollectionView, numberOfItemsInSection section: Int, oldNumber: Int) -> Int { return detailModel?.episodeList?.count ?? 0 } + + func sp_playerListViewController(_ viewController: SPPlayerListViewController, didChangeIndexPathForVisible indexPath: IndexPath) { + self.episodeView?.currentIndex = indexPath.row + } } -extension SPTVPlayerListViewController { +extension SPPlayerDetailViewController { private func requestDetailData() { guard let shortPlayId = self.shortPlayId else { return } diff --git a/ShortPlay/Class/Player/Controller/SPPlayerListViewController.swift b/ShortPlay/Class/Player/Controller/SPPlayerListViewController.swift index 81d437f..54d6dff 100644 --- a/ShortPlay/Class/Player/Controller/SPPlayerListViewController.swift +++ b/ShortPlay/Class/Player/Controller/SPPlayerListViewController.swift @@ -19,6 +19,8 @@ import UIKit ///向上加载更多数据 @objc optional func sp_playerViewControllerLoadUpMoreData(playerViewController: SPPlayerListViewController) + ///当前展示的发生变化 + @objc optional func sp_playerListViewController(_ viewController: SPPlayerListViewController, didChangeIndexPathForVisible indexPath: IndexPath) ///新页面展示完成 // @objc optional func yd_playerViewController(playerListViewController: BCListPlayerViewController, didShowPlayerPage playerViewController: YDBasePlayerViewController) } @@ -31,6 +33,7 @@ import UIKit func sp_playerListViewController(_ viewController: SPPlayerListViewController, _ collectionView: UICollectionView, numberOfItemsInSection section: Int, oldNumber: Int) -> Int + } class SPPlayerListViewController: SPViewController { @@ -153,6 +156,10 @@ class SPPlayerListViewController: SPViewController { func getDataCount() -> Int { return self.collectionView(self.collectionView, numberOfItemsInSection: 0) } + + func scrollToItem(indexPath: IndexPath) { + self.collectionView.scrollToItem(at: indexPath, at: .top, animated: true); + } } extension SPPlayerListViewController { @@ -175,6 +182,7 @@ extension SPPlayerListViewController { self.viewModel.handlePlayFinish = { [weak self] in self?.currentPlayFinish() } + } } @@ -227,6 +235,8 @@ extension SPPlayerListViewController { } return true } + + } //MARK: -------------- UICollectionViewDelegate & UICollectionViewDataSource -------------- @@ -251,6 +261,7 @@ extension SPPlayerListViewController: UICollectionViewDelegate, UICollectionView if self.viewModel.currentPlayer == nil, indexPath == currentIndexPath, let playerProtocol = cell as? SPPlayerProtocol { self.currentIndexPath = indexPath self.viewModel.currentPlayer = playerProtocol + didChangeIndexPathForVisible() } return cell @@ -293,6 +304,7 @@ extension SPPlayerListViewController: UICollectionViewDelegate, UICollectionView guard let currentPlayer = self.collectionView.cellForItem(at: indexPath) as? SPPlayerProtocol else { return } self.viewModel.currentPlayer = currentPlayer // currentCell = self.collectionView.cellForItem(at: indexPath) as? BCListPlayerCell + didChangeIndexPathForVisible() self.play() } @@ -329,4 +341,8 @@ extension SPPlayerListViewController { // } self.delegate?.sp_playerViewControllerLoadUpMoreData?(playerViewController: self) } + + private func didChangeIndexPathForVisible() { + self.delegate?.sp_playerListViewController?(self, didChangeIndexPathForVisible: self.currentIndexPath) + } } diff --git a/ShortPlay/Class/Player/View/SPEpisodeCell.swift b/ShortPlay/Class/Player/View/SPEpisodeCell.swift new file mode 100644 index 0000000..dfb8f7b --- /dev/null +++ b/ShortPlay/Class/Player/View/SPEpisodeCell.swift @@ -0,0 +1,70 @@ +// +// SPEpisodeCell.swift +// ShortPlay +// +// Created by 曾觉新 on 2025/4/16. +// + +import UIKit + +class SPEpisodeCell: SPCollectionViewCell { + + + var videoInfoModel: SPVideoInfoModel? { + didSet { + textLabel.text = "\(videoInfoModel?.episode ?? 0)" + } + } + + var sp_isSelected: Bool = false { + didSet { + textLabel.isHidden = sp_isSelected + playImageView.isHidden = !sp_isSelected + } + } + + private lazy var textLabel: UILabel = { + let label = UILabel() + label.font = .fontLight(ofSize: 14) + label.textColor = .colorD2D2D2() + return label + }() + + private lazy var playImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "play_icon_01")) + return imageView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + contentView.backgroundColor = .colorFFFFFF(alpha: 0.14) + contentView.layer.cornerRadius = 7 + contentView.layer.masksToBounds = true + + + _setupUI() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension SPEpisodeCell { + + private func _setupUI() { + contentView.addSubview(textLabel) + contentView.addSubview(playImageView) + + textLabel.snp.makeConstraints { make in + make.center.equalToSuperview() + } + + playImageView.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } + + +} diff --git a/ShortPlay/Class/Player/View/SPEpisodeView.swift b/ShortPlay/Class/Player/View/SPEpisodeView.swift new file mode 100644 index 0000000..8a8a700 --- /dev/null +++ b/ShortPlay/Class/Player/View/SPEpisodeView.swift @@ -0,0 +1,153 @@ +// +// SPEpisodeView.swift +// ShortPlay +// +// Created by 曾觉新 on 2025/4/16. +// + +import UIKit + +class SPEpisodeView: HWPanModalContentView { + + var currentIndex: Int = 0 { + didSet { + self.collectionView.reloadData() + } + } + + var shortModel: SPShortModel? { + didSet { + coverImageView.sp_setImage(url: shortModel?.image_url) + } + } + + var dataArr: [SPVideoInfoModel] = [] { + didSet { + self.collectionView.reloadData() + } + } + + var didSelectedIndex: ((_ index: Int) -> Void)? + + //MARK: UI属性 + private lazy var collectionViewLayout: UICollectionViewFlowLayout = { + let itemWidth = floor((kSPScreenWidth - 10 * 4 - 30) / 5) + + let layout = UICollectionViewFlowLayout() + layout.itemSize = .init(width: itemWidth, height: 50) + layout.minimumLineSpacing = 10 + layout.minimumInteritemSpacing = 10 + layout.sectionInset = .init(top: 0, left: 15, bottom: 0, right: 15) + return layout + }() + + private lazy var collectionView: SPCollectionView = { + let collectionView = SPCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) + collectionView.delegate = self + collectionView.dataSource = self + SPEpisodeCell.registerCell(collectionView: collectionView) + return collectionView + }() + + private lazy var indicatorView: UIView = { + let view = UIView() + view.backgroundColor = .colorFFFFFF() + view.layer.cornerRadius = 2 + view.layer.masksToBounds = true + return view + }() + + private lazy var coverImageView: SPImageView = { + let imageView = SPImageView() + imageView.layer.cornerRadius = 6 + imageView.layer.masksToBounds = true + return imageView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + _setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + //MARK: HWPanModalPresentable + override func panScrollable() -> UIScrollView? { + return collectionView + } + + override func longFormHeight() -> PanModalHeight { + return PanModalHeightMake(.content, kSPScreenHeight * (2 / 3)) + } + + override func showDragIndicator() -> Bool { + return false + } + + override func backgroundConfig() -> HWBackgroundConfig { + let config = HWBackgroundConfig() + config.backgroundAlpha = 0.4 + return config + } + + override func present(in view: UIView?) { + super.present(in: view) + self.hw_contentView.addEffectView(style: .dark) + } + + + +} + +extension SPEpisodeView { + + private func _setupUI() { + addSubview(indicatorView) + addSubview(coverImageView) + addSubview(self.collectionView) + + self.indicatorView.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.top.equalToSuperview().offset(15) + make.width.equalTo(30) + make.height.equalTo(4) + } + + self.coverImageView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(15) + make.top.equalToSuperview().offset(39) + make.width.equalTo(55) + make.height.equalTo(74) + } + + self.collectionView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + +} + +//MARK: -------------- UICollectionViewDelegate & UICollectionViewDataSource -------------- +extension SPEpisodeView: UICollectionViewDelegate, UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = SPEpisodeCell.dequeueReusableCell(collectionView: collectionView, indexPath: indexPath) + cell.videoInfoModel = self.dataArr[indexPath.row] + cell.sp_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) { + + } + } +} diff --git a/ShortPlay/Class/Player/View/SPPlayerControlView.swift b/ShortPlay/Class/Player/View/SPPlayerControlView.swift index 4403040..28c2158 100644 --- a/ShortPlay/Class/Player/View/SPPlayerControlView.swift +++ b/ShortPlay/Class/Player/View/SPPlayerControlView.swift @@ -15,15 +15,20 @@ class SPPlayerControlView: UIView { } } - var model: Any? { + + var shortModel: SPShortModel? { + didSet { + updateCollectButtonState() + } + } + + var videoInfo: SPVideoInfoModel? { didSet { - guard let model = model as? SPShortModel else { return } - - } } + ///滑动进度条 var panProgressFinishBlock: ((_ progress: CGFloat) -> Void)? @@ -39,7 +44,8 @@ class SPPlayerControlView: UIView { updatePlayIconState() } } - + + //MARK: UI属性 private(set) lazy var progressView: SPPlayerProgressView = { let view = SPPlayerProgressView() view.panStart = { [weak self] in @@ -65,17 +71,36 @@ class SPPlayerControlView: UIView { return imageView }() + ///右边功能区 + private(set) lazy var rightFeatureView: UIStackView = { + let view = UIStackView(arrangedSubviews: [collectButton]) + view.axis = .vertical + view.spacing = 25 + return view + }() + + ///收藏按钮 + private lazy var collectButton: UIButton = { + let button = createFeatureButton(title: "Save".localized, selectedTitle: "Added".localized, image: UIImage(named: "collect_icon_01"), selectedImage: UIImage(named: "collect_icon_01_selected")) + button.addTarget(self, action: #selector(handleCollectButton), for: .touchUpInside) + return button + }() + 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: SPVideoAPI.updateShortCollectStateNotification, object: nil) + let tap = UITapGestureRecognizer(target: self, action: #selector(hadlePlayAndOrPaused)) self.addGestureRecognizer(tap) + sp_setupUI() } @@ -89,6 +114,23 @@ class SPPlayerControlView: UIView { fatalError("init(coder:) has not been implemented") } + + func createFeatureButton(title: String?, selectedTitle: String? = nil, image: UIImage?, selectedImage: UIImage? = nil) -> UIButton { + let button = JXButton(type: .custom) + button.titleDirection = .down + 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(alpha: 0.9), for: .normal) + button.setTitleColor(.colorF564B6(), for: .selected) + button.jx_font = .fontLight(ofSize: 11); + return button + } + + } extension SPPlayerControlView { @@ -96,6 +138,7 @@ extension SPPlayerControlView { private func sp_setupUI() { addSubview(progressView) addSubview(playImageView) + addSubview(rightFeatureView) progressView.snp.makeConstraints { make in make.left.equalToSuperview().offset(10) @@ -108,6 +151,11 @@ extension SPPlayerControlView { make.center.equalToSuperview() make.width.height.equalTo(100) } + + rightFeatureView.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-15) + make.bottom.equalToSuperview().offset(-200) + } } } @@ -132,7 +180,29 @@ extension SPPlayerControlView { // SPAPPTool.topViewController()?.navigationController?.pushViewController(vc, animated: true) } + @objc private func handleCollectButton() { + guard let shortPlayId = self.videoInfo?.short_play_id else { return } + + let isCollect = !(self.shortModel?.is_collect ?? false) + + SPVideoAPI.requestCollectShort(isCollect: isCollect, shortPlayId: shortPlayId) { + } + } + @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 + } } diff --git a/ShortPlay/Class/Player/View/SPPlayerDetailCell.swift b/ShortPlay/Class/Player/View/SPPlayerDetailCell.swift new file mode 100644 index 0000000..bfbec5e --- /dev/null +++ b/ShortPlay/Class/Player/View/SPPlayerDetailCell.swift @@ -0,0 +1,26 @@ +// +// SPPlayerDetailCell.swift +// ShortPlay +// +// Created by 曾觉新 on 2025/4/10. +// + +import UIKit + +class SPPlayerDetailCell: SPPlayerListCell { + + override var PlayerControlViewClass: SPPlayerControlView.Type { + return SPPlayerDetailControlView.self + } + + + override init(frame: CGRect) { + super.init(frame: frame) + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + +} diff --git a/ShortPlay/Class/Player/View/SPPlayerDetailControlView.swift b/ShortPlay/Class/Player/View/SPPlayerDetailControlView.swift new file mode 100644 index 0000000..65ba386 --- /dev/null +++ b/ShortPlay/Class/Player/View/SPPlayerDetailControlView.swift @@ -0,0 +1,44 @@ +// +// SPPlayerDetailControlView.swift +// ShortPlay +// +// Created by 曾觉新 on 2025/4/16. +// + +import UIKit + +class SPPlayerDetailControlView: SPPlayerControlView { + + + private lazy var episodeButton: UIButton = { + let button = createFeatureButton(title: "Episodes".localized, image: UIImage(named: "episodes_icon_01")) + button.addTarget(self, action: #selector(handleEpisodeButton), for: .touchUpInside) + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + _setupUI() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +extension SPPlayerDetailControlView { + + private func _setupUI() { + self.rightFeatureView.addArrangedSubview(episodeButton) + + } + +} + +extension SPPlayerDetailControlView { + @objc private func handleEpisodeButton() { + self.viewModel?.handleEpisode?() + } +} diff --git a/ShortPlay/Class/Player/View/SPPlayerListCell.swift b/ShortPlay/Class/Player/View/SPPlayerListCell.swift index ea6b46a..1d543c1 100644 --- a/ShortPlay/Class/Player/View/SPPlayerListCell.swift +++ b/ShortPlay/Class/Player/View/SPPlayerListCell.swift @@ -9,6 +9,10 @@ import UIKit class SPPlayerListCell: SPCollectionViewCell, SPPlayerProtocol { + var PlayerControlViewClass: SPPlayerControlView.Type { + return SPPlayerControlView.self + } + weak var viewModel: SPPlayerListViewModel? { didSet { controlView.viewModel = viewModel @@ -39,7 +43,7 @@ class SPPlayerListCell: SPCollectionViewCell, SPPlayerProtocol { }() private lazy var controlView: SPPlayerControlView = { - let view = SPPlayerControlView() + let view = PlayerControlViewClass.init() view.panProgressFinishBlock = { [weak self] progress in guard let self = self else { return } let duration = CGFloat(self.player.duration) @@ -65,19 +69,30 @@ class SPPlayerListCell: SPCollectionViewCell, SPPlayerProtocol { self.controlView.progress = 0 self.coverImageView.isHidden = false - if let model = model as? SPShortModel { - self.controlView.model = model - coverImageView.sp_setImage(url: model.image_url) - } else if let model = model as? SPVideoDetailModel { - self.controlView.model = model.shortPlayInfo - coverImageView.sp_setImage(url: model.shortPlayInfo?.image_url) - } +// if let model = model as? SPShortModel { +// self.controlView.model = model +// coverImageView.sp_setImage(url: model.image_url) +// } else if let model = model as? SPVideoDetailModel { +// self.controlView.model = model.shortPlayInfo +// coverImageView.sp_setImage(url: model.shortPlayInfo?.image_url) +// } } } + var shortModel: SPShortModel? { + didSet { + self.controlView.shortModel = shortModel + coverImageView.sp_setImage(url: shortModel?.image_url) + } + } + var videoInfo: SPVideoInfoModel? { didSet { + self.controlView.progress = 0 + self.coverImageView.isHidden = false + self.controlView.videoInfo = videoInfo + player.setPlayUrl(url: videoInfo?.video_url ?? "") } } diff --git a/ShortPlay/Class/Player/View/SPTVPlayerListCell.swift b/ShortPlay/Class/Player/View/SPTVPlayerListCell.swift deleted file mode 100644 index 8084c20..0000000 --- a/ShortPlay/Class/Player/View/SPTVPlayerListCell.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// SPTVPlayerListCell.swift -// ShortPlay -// -// Created by 曾觉新 on 2025/4/10. -// - -import UIKit - -class SPTVPlayerListCell: SPPlayerListCell { - -} diff --git a/ShortPlay/Class/Player/ViewModel/SPPlayerListViewModel.swift b/ShortPlay/Class/Player/ViewModel/SPPlayerListViewModel.swift index a2812c8..b6e4c36 100644 --- a/ShortPlay/Class/Player/ViewModel/SPPlayerListViewModel.swift +++ b/ShortPlay/Class/Player/ViewModel/SPPlayerListViewModel.swift @@ -41,6 +41,7 @@ class SPPlayerListViewModel: NSObject { var handlePauseOrPlay: (() -> Void)? ///播放完成 var handlePlayFinish: (() -> Void)? - + ///选集 + var handleEpisode: (() -> Void)? } diff --git a/ShortPlay/Source/Assets.xcassets/icon/collect_icon_01.imageset/Contents.json b/ShortPlay/Source/Assets.xcassets/icon/collect_icon_01.imageset/Contents.json new file mode 100644 index 0000000..2fcc7b2 --- /dev/null +++ b/ShortPlay/Source/Assets.xcassets/icon/collect_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "收藏@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "收藏@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ShortPlay/Source/Assets.xcassets/icon/collect_icon_01.imageset/收藏@2x.png b/ShortPlay/Source/Assets.xcassets/icon/collect_icon_01.imageset/收藏@2x.png new file mode 100644 index 0000000..b1c64bf Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/icon/collect_icon_01.imageset/收藏@2x.png differ diff --git a/ShortPlay/Source/Assets.xcassets/icon/collect_icon_01.imageset/收藏@3x.png b/ShortPlay/Source/Assets.xcassets/icon/collect_icon_01.imageset/收藏@3x.png new file mode 100644 index 0000000..0b8fb79 Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/icon/collect_icon_01.imageset/收藏@3x.png differ diff --git a/ShortPlay/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Contents.json b/ShortPlay/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Contents.json new file mode 100644 index 0000000..2fcc7b2 --- /dev/null +++ b/ShortPlay/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "收藏@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "收藏@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ShortPlay/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/收藏@2x.png b/ShortPlay/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/收藏@2x.png new file mode 100644 index 0000000..f1e659f Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/收藏@2x.png differ diff --git a/ShortPlay/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/收藏@3x.png b/ShortPlay/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/收藏@3x.png new file mode 100644 index 0000000..74c35e7 Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/icon/collect_icon_01_selected.imageset/收藏@3x.png differ diff --git a/ShortPlay/Source/Assets.xcassets/icon/episodes_icon_01.imageset/Contents.json b/ShortPlay/Source/Assets.xcassets/icon/episodes_icon_01.imageset/Contents.json new file mode 100644 index 0000000..292c6c2 --- /dev/null +++ b/ShortPlay/Source/Assets.xcassets/icon/episodes_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Episodes@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Episodes@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ShortPlay/Source/Assets.xcassets/icon/episodes_icon_01.imageset/Episodes@2x.png b/ShortPlay/Source/Assets.xcassets/icon/episodes_icon_01.imageset/Episodes@2x.png new file mode 100644 index 0000000..86c068c Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/icon/episodes_icon_01.imageset/Episodes@2x.png differ diff --git a/ShortPlay/Source/Assets.xcassets/icon/episodes_icon_01.imageset/Episodes@3x.png b/ShortPlay/Source/Assets.xcassets/icon/episodes_icon_01.imageset/Episodes@3x.png new file mode 100644 index 0000000..c97f7e8 Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/icon/episodes_icon_01.imageset/Episodes@3x.png differ diff --git a/ShortPlay/Source/Assets.xcassets/icon/play_icon_01.imageset/Contents.json b/ShortPlay/Source/Assets.xcassets/icon/play_icon_01.imageset/Contents.json new file mode 100644 index 0000000..d6a5fe3 --- /dev/null +++ b/ShortPlay/Source/Assets.xcassets/icon/play_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "play1@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "play1@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ShortPlay/Source/Assets.xcassets/icon/play_icon_01.imageset/play1@2x.png b/ShortPlay/Source/Assets.xcassets/icon/play_icon_01.imageset/play1@2x.png new file mode 100644 index 0000000..8b4d23f Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/icon/play_icon_01.imageset/play1@2x.png differ diff --git a/ShortPlay/Source/Assets.xcassets/icon/play_icon_01.imageset/play1@3x.png b/ShortPlay/Source/Assets.xcassets/icon/play_icon_01.imageset/play1@3x.png new file mode 100644 index 0000000..0fd4d72 Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/icon/play_icon_01.imageset/play1@3x.png differ diff --git a/ShortPlay/Source/ShortPlay-Bridging-Header.h b/ShortPlay/Source/ShortPlay-Bridging-Header.h index a8edb74..e70f190 100644 --- a/ShortPlay/Source/ShortPlay-Bridging-Header.h +++ b/ShortPlay/Source/ShortPlay-Bridging-Header.h @@ -11,3 +11,4 @@ #import #import "NSUserDefaults+JXAdd.h" #import +#import diff --git a/ShortPlay/Source/en.lproj/Localizable.strings b/ShortPlay/Source/en.lproj/Localizable.strings index 7a783b8..e31b88f 100644 --- a/ShortPlay/Source/en.lproj/Localizable.strings +++ b/ShortPlay/Source/en.lproj/Localizable.strings @@ -17,3 +17,6 @@ "Trending Now" = "Trending Now"; "Editor's Hotlist" = "Editor's Hotlist"; "Shorts for You" = "Shorts for You"; +"Episodes" = "Episodes"; +"Save" = "Save"; +"Added" = "Added"; diff --git a/ShortPlay/Thirdparty/JXButton/JXButton.swift b/ShortPlay/Thirdparty/JXButton/JXButton.swift new file mode 100644 index 0000000..dbf2ae8 --- /dev/null +++ b/ShortPlay/Thirdparty/JXButton/JXButton.swift @@ -0,0 +1,218 @@ +// +// JXButton.swift +// BoJia +// +// Created by 火山传媒 on 2024/5/31. +// + +import UIKit + +class JXButton: UIButton { + lazy var jx_font: UIFont? = self.titleLabel?.font { + didSet { + self.titleLabel?.font = jx_font + } + } + + var maxTitleWidth: CGFloat = 0 + var titleDirection: UITextLayoutDirection? + ///左右边距 + var leftAnyRightmargin: CGFloat = 0 + + ///文字与图片的间距 + var space: CGFloat = 0 + + private var imageRect: CGRect = .zero + private var titleRect: CGRect = .zero + + + override func layoutSubviews() { + super.layoutSubviews() + } + + override var intrinsicContentSize: CGSize { + + var width: CGFloat = 0 + var height: CGFloat = 0 + switch titleDirection { + case .left: + width = imageRect.width + titleRect.width + space + if imageRect.height > titleRect.height { + height = imageRect.height + } else { + height = titleRect.height + } + + case .up: + if imageRect.width > titleRect.width { + width = imageRect.width + } else { + width = titleRect.width + } + height = titleRect.height + imageRect.height + space + + case .down: + if imageRect.width > titleRect.width { + width = imageRect.width + } else { + width = titleRect.width + } + height = titleRect.height + imageRect.height + space + + default: + width = imageRect.width + titleRect.width + space + if imageRect.height > titleRect.height { + height = imageRect.height + } else { + height = titleRect.height + } + } + + let size = CGSize(width: width + leftAnyRightmargin * 2, height: height) + return size + } + + override func imageRect(forContentRect contentRect: CGRect) -> CGRect { + let imageSize = currentImage?.size ?? .zero + let textSize = self._textSize() + let contentWidth = self.frame.size.width + let contentHeight = self.frame.size.height + + var x: CGFloat = 0 + var y: CGFloat = 0 + var width: CGFloat = 0 + var height: CGFloat = 0 + + switch titleDirection { + case .left: + x = (contentWidth - space) / 2 - imageSize.width / 2 + textSize.width / 2 + space + y = contentHeight / 2 - imageSize.height / 2 + width = imageSize.width + height = imageSize.height + + case .up: + x = contentWidth / 2 - imageSize.width / 2 + y = (contentHeight - space) / 2 - imageSize.height / 2 + textSize.height / 2 + space + width = imageSize.width + height = imageSize.height + + case .down: + x = contentWidth / 2 - imageSize.width / 2 + y = (contentHeight - space) / 2 - imageSize.height / 2 - textSize.height / 2 + width = imageSize.width + height = imageSize.height + + default: + x = (contentWidth - space) / 2 - imageSize.width / 2 - textSize.width / 2 + y = contentHeight / 2 - imageSize.height / 2 + width = imageSize.width + height = imageSize.height + } + self.imageRect = CGRect(x: x, y: y, width: width, height: height) + + return self.imageRect + } + + + override func titleRect(forContentRect contentRect: CGRect) -> CGRect { + let imageSize = currentImage?.size ?? .zero + let textSize = self._textSize() + let contentWidth = self.frame.size.width + let contentHeight = self.frame.size.height + + var x: CGFloat = 0 + var y: CGFloat = 0 + var width: CGFloat = 0 + var height: CGFloat = 0 + + switch titleDirection { + case .left: + x = (contentWidth - space) / 2 - imageSize.width / 2 - textSize.width / 2 + y = contentHeight / 2 - textSize.height / 2 + width = textSize.width + height = textSize.height + + case .up: + x = contentWidth / 2 - textSize.width / 2 + y = (contentHeight - space) / 2 - textSize.height / 2 - imageSize.height / 2 + width = textSize.width + height = textSize.height + + case .down: + x = contentWidth / 2 - textSize.width / 2 + y = (contentHeight - space) / 2 - textSize.height / 2 + imageSize.height / 2 + space + width = textSize.width + height = textSize.height + + default: + x = (contentWidth - space) / 2 + imageSize.width / 2 - textSize.width / 2 + space + y = contentHeight / 2 - textSize.height / 2 + width = textSize.width + height = textSize.height + } + + self.titleRect = CGRect(x: x, y: y, width: width, height: height) + return self.titleRect + } + + + private var borderColors: [UInt : UIColor] = [:] + + func jx_setBorderColor(_ color: UIColor?, for state: UIControl.State) { + var arr = self.borderColors + let index = state.rawValue + arr[index] = color + if state == .selected { + let i = index + UIControl.State.highlighted.rawValue + arr[i] = color + } + self.borderColors = arr + updateStatus() + } + + func updateStatus() { + var color = self.borderColors[self.state.rawValue]?.cgColor + if (color == nil) { + color = self.borderColors[UIControl.State.normal.rawValue]?.cgColor + } + self.layer.borderColor = color + } + + override var isSelected: Bool { + didSet { + updateStatus() + } + } +} + + +extension JXButton { + + private func _textSize() -> CGSize { +// let size = CGSize(width: self.bounds.size.width, height: self.bounds.size.height) + + var attr: [NSAttributedString.Key : Any] = [:] + attr[NSAttributedString.Key.font] = jx_font + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineBreakMode = .byTruncatingMiddle + attr[NSAttributedString.Key.paragraphStyle] = paragraphStyle + +// var rect = self.currentTitle?.ocString.boundingRect(with: size, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: attr, context: nil) ?? .zero + + if let font = jx_font { + var size = self.currentTitle?.size(font: font) ?? .zero + + if maxTitleWidth != 0 && maxTitleWidth < size.width { + size = CGSize(width: maxTitleWidth, height: size.height) + } + // if maxTitleWidth != 0 && maxTitleWidth < rect.size.width { + // rect.size = CGSize(width: maxTitleWidth, height: rect.size.height) + // } + return size + } else { + return .zero + } + } + +}